PageRenderTime 66ms CodeModel.GetById 25ms RepoModel.GetById 1ms app.codeStats 0ms

/drivers/exoscale/exoscale.go

https://gitlab.com/wilane/machine
Go | 464 lines | 402 code | 51 blank | 11 comment | 67 complexity | e208c445103bf78ddf3f7d30df3ff83f MD5 | raw file
  1. package exoscale
  2. import (
  3. "fmt"
  4. "io/ioutil"
  5. "net"
  6. "os"
  7. "strings"
  8. "time"
  9. "github.com/docker/machine/libmachine/drivers"
  10. "github.com/docker/machine/libmachine/log"
  11. "github.com/docker/machine/libmachine/mcnflag"
  12. "github.com/docker/machine/libmachine/mcnutils"
  13. "github.com/docker/machine/libmachine/state"
  14. "github.com/pyr/egoscale/src/egoscale"
  15. )
  16. type Driver struct {
  17. *drivers.BaseDriver
  18. URL string
  19. APIKey string `json:"ApiKey"`
  20. APISecretKey string `json:"ApiSecretKey"`
  21. InstanceProfile string
  22. DiskSize int
  23. Image string
  24. SecurityGroup string
  25. AvailabilityZone string
  26. KeyPair string
  27. PublicKey string
  28. UserDataFile string
  29. ID string `json:"Id"`
  30. }
  31. const (
  32. defaultInstanceProfile = "small"
  33. defaultDiskSize = 50
  34. defaultImage = "ubuntu-15.10"
  35. defaultAvailabilityZone = "ch-gva-2"
  36. defaultSSHUser = "ubuntu"
  37. )
  38. // GetCreateFlags registers the flags this driver adds to
  39. // "docker hosts create"
  40. func (d *Driver) GetCreateFlags() []mcnflag.Flag {
  41. return []mcnflag.Flag{
  42. mcnflag.StringFlag{
  43. EnvVar: "EXOSCALE_ENDPOINT",
  44. Name: "exoscale-url",
  45. Usage: "exoscale API endpoint",
  46. },
  47. mcnflag.StringFlag{
  48. EnvVar: "EXOSCALE_API_KEY",
  49. Name: "exoscale-api-key",
  50. Usage: "exoscale API key",
  51. },
  52. mcnflag.StringFlag{
  53. EnvVar: "EXOSCALE_API_SECRET",
  54. Name: "exoscale-api-secret-key",
  55. Usage: "exoscale API secret key",
  56. },
  57. mcnflag.StringFlag{
  58. EnvVar: "EXOSCALE_INSTANCE_PROFILE",
  59. Name: "exoscale-instance-profile",
  60. Value: defaultInstanceProfile,
  61. Usage: "exoscale instance profile (small, medium, large, ...)",
  62. },
  63. mcnflag.IntFlag{
  64. EnvVar: "EXOSCALE_DISK_SIZE",
  65. Name: "exoscale-disk-size",
  66. Value: defaultDiskSize,
  67. Usage: "exoscale disk size (10, 50, 100, 200, 400)",
  68. },
  69. mcnflag.StringFlag{
  70. EnvVar: "EXSOCALE_IMAGE",
  71. Name: "exoscale-image",
  72. Value: defaultImage,
  73. Usage: "exoscale image template",
  74. },
  75. mcnflag.StringSliceFlag{
  76. EnvVar: "EXOSCALE_SECURITY_GROUP",
  77. Name: "exoscale-security-group",
  78. Value: []string{},
  79. Usage: "exoscale security group",
  80. },
  81. mcnflag.StringFlag{
  82. EnvVar: "EXOSCALE_AVAILABILITY_ZONE",
  83. Name: "exoscale-availability-zone",
  84. Value: defaultAvailabilityZone,
  85. Usage: "exoscale availibility zone",
  86. },
  87. mcnflag.StringFlag{
  88. EnvVar: "EXOSCALE_SSH_USER",
  89. Name: "exoscale-ssh-user",
  90. Value: defaultSSHUser,
  91. Usage: "Set the name of the ssh user",
  92. },
  93. mcnflag.StringFlag{
  94. EnvVar: "EXOSCALE_USERDATA",
  95. Name: "exoscale-userdata",
  96. Usage: "path to file with cloud-init user-data",
  97. },
  98. }
  99. }
  100. func NewDriver(hostName, storePath string) drivers.Driver {
  101. return &Driver{
  102. InstanceProfile: defaultInstanceProfile,
  103. DiskSize: defaultDiskSize,
  104. Image: defaultImage,
  105. AvailabilityZone: defaultAvailabilityZone,
  106. BaseDriver: &drivers.BaseDriver{
  107. MachineName: hostName,
  108. StorePath: storePath,
  109. },
  110. }
  111. }
  112. func (d *Driver) GetSSHHostname() (string, error) {
  113. return d.GetIP()
  114. }
  115. func (d *Driver) GetSSHUsername() string {
  116. if d.SSHUser == "" {
  117. d.SSHUser = defaultSSHUser
  118. }
  119. return d.SSHUser
  120. }
  121. // DriverName returns the name of the driver
  122. func (d *Driver) DriverName() string {
  123. return "exoscale"
  124. }
  125. func (d *Driver) SetConfigFromFlags(flags drivers.DriverOptions) error {
  126. d.URL = flags.String("exoscale-url")
  127. d.APIKey = flags.String("exoscale-api-key")
  128. d.APISecretKey = flags.String("exoscale-api-secret-key")
  129. d.InstanceProfile = flags.String("exoscale-instance-profile")
  130. d.DiskSize = flags.Int("exoscale-disk-size")
  131. d.Image = flags.String("exoscale-image")
  132. securityGroups := flags.StringSlice("exoscale-security-group")
  133. if len(securityGroups) == 0 {
  134. securityGroups = []string{"docker-machine"}
  135. }
  136. d.SecurityGroup = strings.Join(securityGroups, ",")
  137. d.AvailabilityZone = flags.String("exoscale-availability-zone")
  138. d.SSHUser = flags.String("exoscale-ssh-user")
  139. d.UserDataFile = flags.String("exoscale-userdata")
  140. d.SetSwarmConfigFromFlags(flags)
  141. if d.URL == "" {
  142. d.URL = "https://api.exoscale.ch/compute"
  143. }
  144. if d.APIKey == "" || d.APISecretKey == "" {
  145. return fmt.Errorf("Please specify an API key (--exoscale-api-key) and an API secret key (--exoscale-api-secret-key).")
  146. }
  147. return nil
  148. }
  149. func (d *Driver) PreCreateCheck() error {
  150. if d.UserDataFile != "" {
  151. if _, err := os.Stat(d.UserDataFile); os.IsNotExist(err) {
  152. return fmt.Errorf("user-data file %s could not be found", d.UserDataFile)
  153. }
  154. }
  155. return nil
  156. }
  157. func (d *Driver) GetURL() (string, error) {
  158. if err := drivers.MustBeRunning(d); err != nil {
  159. return "", err
  160. }
  161. ip, err := d.GetIP()
  162. if err != nil {
  163. return "", err
  164. }
  165. return fmt.Sprintf("tcp://%s", net.JoinHostPort(ip, "2376")), nil
  166. }
  167. func (d *Driver) GetState() (state.State, error) {
  168. client := egoscale.NewClient(d.URL, d.APIKey, d.APISecretKey)
  169. vm, err := client.GetVirtualMachine(d.ID)
  170. if err != nil {
  171. return state.Error, err
  172. }
  173. switch vm.State {
  174. case "Starting":
  175. return state.Starting, nil
  176. case "Running":
  177. return state.Running, nil
  178. case "Stopping":
  179. return state.Running, nil
  180. case "Stopped":
  181. return state.Stopped, nil
  182. case "Destroyed":
  183. return state.Stopped, nil
  184. case "Expunging":
  185. return state.Stopped, nil
  186. case "Migrating":
  187. return state.Paused, nil
  188. case "Error":
  189. return state.Error, nil
  190. case "Unknown":
  191. return state.Error, nil
  192. case "Shutdowned":
  193. return state.Stopped, nil
  194. }
  195. return state.None, nil
  196. }
  197. func (d *Driver) createDefaultSecurityGroup(client *egoscale.Client, group string) (string, error) {
  198. rules := []egoscale.SecurityGroupRule{
  199. {
  200. SecurityGroupId: "",
  201. Cidr: "0.0.0.0/0",
  202. Protocol: "TCP",
  203. Port: 22,
  204. },
  205. {
  206. SecurityGroupId: "",
  207. Cidr: "0.0.0.0/0",
  208. Protocol: "TCP",
  209. Port: 2376,
  210. },
  211. {
  212. SecurityGroupId: "",
  213. Cidr: "0.0.0.0/0",
  214. Protocol: "TCP",
  215. Port: 3376,
  216. },
  217. {
  218. SecurityGroupId: "",
  219. Cidr: "0.0.0.0/0",
  220. Protocol: "ICMP",
  221. IcmpType: 8,
  222. IcmpCode: 0,
  223. },
  224. }
  225. sgresp, err := client.CreateSecurityGroupWithRules(
  226. group,
  227. rules,
  228. make([]egoscale.SecurityGroupRule, 0, 0))
  229. if err != nil {
  230. return "", err
  231. }
  232. sg := sgresp.Id
  233. return sg, nil
  234. }
  235. func (d *Driver) Create() error {
  236. userdata, err := d.getCloudInit()
  237. if err != nil {
  238. return err
  239. }
  240. log.Infof("Querying exoscale for the requested parameters...")
  241. client := egoscale.NewClient(d.URL, d.APIKey, d.APISecretKey)
  242. topology, err := client.GetTopology()
  243. if err != nil {
  244. return err
  245. }
  246. // Availability zone UUID
  247. zone, ok := topology.Zones[d.AvailabilityZone]
  248. if !ok {
  249. return fmt.Errorf("Availability zone %v doesn't exist",
  250. d.AvailabilityZone)
  251. }
  252. log.Debugf("Availability zone %v = %s", d.AvailabilityZone, zone)
  253. // Image UUID
  254. var tpl string
  255. images, ok := topology.Images[strings.ToLower(d.Image)]
  256. if ok {
  257. tpl, ok = images[d.DiskSize]
  258. }
  259. if !ok {
  260. return fmt.Errorf("Unable to find image %v with size %d",
  261. d.Image, d.DiskSize)
  262. }
  263. log.Debugf("Image %v(%d) = %s", d.Image, d.DiskSize, tpl)
  264. // Profile UUID
  265. profile, ok := topology.Profiles[strings.ToLower(d.InstanceProfile)]
  266. if !ok {
  267. return fmt.Errorf("Unable to find the %s profile",
  268. d.InstanceProfile)
  269. }
  270. log.Debugf("Profile %v = %s", d.InstanceProfile, profile)
  271. // Security groups
  272. securityGroups := strings.Split(d.SecurityGroup, ",")
  273. sgs := make([]string, len(securityGroups))
  274. for idx, group := range securityGroups {
  275. sg, ok := topology.SecurityGroups[group]
  276. if !ok {
  277. log.Infof("Security group %v does not exist, create it",
  278. group)
  279. sg, err = d.createDefaultSecurityGroup(client, group)
  280. if err != nil {
  281. return err
  282. }
  283. }
  284. log.Debugf("Security group %v = %s", group, sg)
  285. sgs[idx] = sg
  286. }
  287. log.Infof("Generate an SSH keypair...")
  288. keypairName := fmt.Sprintf("docker-machine-%s", d.MachineName)
  289. kpresp, err := client.CreateKeypair(keypairName)
  290. if err != nil {
  291. return err
  292. }
  293. err = ioutil.WriteFile(d.GetSSHKeyPath(), []byte(kpresp.Privatekey), 0600)
  294. if err != nil {
  295. return err
  296. }
  297. d.KeyPair = keypairName
  298. log.Infof("Spawn exoscale host...")
  299. log.Debugf("Using the following cloud-init file:")
  300. log.Debugf("%s", userdata)
  301. machineProfile := egoscale.MachineProfile{
  302. Template: tpl,
  303. ServiceOffering: profile,
  304. SecurityGroups: sgs,
  305. Userdata: userdata,
  306. Zone: zone,
  307. Keypair: d.KeyPair,
  308. Name: d.MachineName,
  309. }
  310. cvmresp, err := client.CreateVirtualMachine(machineProfile)
  311. if err != nil {
  312. return err
  313. }
  314. vm, err := d.waitForVM(client, cvmresp)
  315. if err != nil {
  316. return err
  317. }
  318. d.IPAddress = vm.Nic[0].Ipaddress
  319. d.ID = vm.Id
  320. return nil
  321. }
  322. func (d *Driver) Start() error {
  323. client := egoscale.NewClient(d.URL, d.APIKey, d.APISecretKey)
  324. svmresp, err := client.StartVirtualMachine(d.ID)
  325. if err != nil {
  326. return err
  327. }
  328. return d.waitForJob(client, svmresp)
  329. }
  330. func (d *Driver) Stop() error {
  331. client := egoscale.NewClient(d.URL, d.APIKey, d.APISecretKey)
  332. svmresp, err := client.StopVirtualMachine(d.ID)
  333. if err != nil {
  334. return err
  335. }
  336. return d.waitForJob(client, svmresp)
  337. }
  338. func (d *Driver) Restart() error {
  339. client := egoscale.NewClient(d.URL, d.APIKey, d.APISecretKey)
  340. svmresp, err := client.RebootVirtualMachine(d.ID)
  341. if err != nil {
  342. return err
  343. }
  344. return d.waitForJob(client, svmresp)
  345. }
  346. func (d *Driver) Kill() error {
  347. return d.Stop()
  348. }
  349. func (d *Driver) Remove() error {
  350. client := egoscale.NewClient(d.URL, d.APIKey, d.APISecretKey)
  351. // Destroy the SSH key
  352. if _, err := client.DeleteKeypair(d.KeyPair); err != nil {
  353. return err
  354. }
  355. // Destroy the virtual machine
  356. dvmresp, err := client.DestroyVirtualMachine(d.ID)
  357. if err != nil {
  358. return err
  359. }
  360. if err = d.waitForJob(client, dvmresp); err != nil {
  361. return err
  362. }
  363. return nil
  364. }
  365. func (d *Driver) jobIsDone(client *egoscale.Client, jobid string) (bool, error) {
  366. resp, err := client.PollAsyncJob(jobid)
  367. if err != nil {
  368. return true, err
  369. }
  370. switch resp.Jobstatus {
  371. case 0: // Job is still in progress
  372. case 1: // Job has successfully completed
  373. return true, nil
  374. case 2: // Job has failed to complete
  375. return true, fmt.Errorf("Operation failed to complete")
  376. default: // Some other code
  377. }
  378. return false, nil
  379. }
  380. func (d *Driver) waitForJob(client *egoscale.Client, jobid string) error {
  381. log.Infof("Waiting for job to complete...")
  382. return mcnutils.WaitForSpecificOrError(func() (bool, error) {
  383. return d.jobIsDone(client, jobid)
  384. }, 60, 2*time.Second)
  385. }
  386. func (d *Driver) waitForVM(client *egoscale.Client, jobid string) (*egoscale.DeployVirtualMachineResponse, error) {
  387. if err := d.waitForJob(client, jobid); err != nil {
  388. return nil, err
  389. }
  390. resp, err := client.PollAsyncJob(jobid)
  391. if err != nil {
  392. return nil, err
  393. }
  394. vm, err := client.AsyncToVirtualMachine(*resp)
  395. if err != nil {
  396. return nil, err
  397. }
  398. return vm, nil
  399. }
  400. // Build a cloud-init user data string that will install and run
  401. // docker.
  402. func (d *Driver) getCloudInit() (string, error) {
  403. if d.UserDataFile != "" {
  404. buf, err := ioutil.ReadFile(d.UserDataFile)
  405. if err != nil {
  406. return "", err
  407. }
  408. return string(buf), nil
  409. }
  410. return `#cloud-config
  411. manage_etc_hosts: localhost
  412. `, nil
  413. }