From fd569c8fdfaff7daf512a1eae79b93b6553db7f8 Mon Sep 17 00:00:00 2001 From: Vincent Bernat Date: Fri, 5 Dec 2014 17:17:44 +0100 Subject: [PATCH] driver: exoscale driver Add support for exoscale, a Swiss cloud provider. This pull "egoscale", a Go binding for exoscale, in godeps. Signed-off-by: Vincent Bernat --- Godeps/Godeps.json | 4 + .../pyr/egoscale/src/egoscale/async.go | 36 ++ .../pyr/egoscale/src/egoscale/error.go | 9 + .../pyr/egoscale/src/egoscale/groups.go | 105 ++++ .../pyr/egoscale/src/egoscale/init.go | 21 + .../pyr/egoscale/src/egoscale/keypair.go | 23 + .../pyr/egoscale/src/egoscale/request.go | 65 +++ .../pyr/egoscale/src/egoscale/topology.go | 170 +++++++ .../pyr/egoscale/src/egoscale/types.go | 404 +++++++++++++++ .../pyr/egoscale/src/egoscale/vm.go | 142 ++++++ commands/commands.go | 1 + docs/index.md | 15 + drivers/exoscale/exoscale.go | 470 ++++++++++++++++++ drivers/exoscale/exoscale_test.go | 1 + test/integration/driver-exoscale.bats | 114 +++++ 15 files changed, 1580 insertions(+) create mode 100644 Godeps/_workspace/src/github.com/pyr/egoscale/src/egoscale/async.go create mode 100644 Godeps/_workspace/src/github.com/pyr/egoscale/src/egoscale/error.go create mode 100644 Godeps/_workspace/src/github.com/pyr/egoscale/src/egoscale/groups.go create mode 100644 Godeps/_workspace/src/github.com/pyr/egoscale/src/egoscale/init.go create mode 100644 Godeps/_workspace/src/github.com/pyr/egoscale/src/egoscale/keypair.go create mode 100644 Godeps/_workspace/src/github.com/pyr/egoscale/src/egoscale/request.go create mode 100644 Godeps/_workspace/src/github.com/pyr/egoscale/src/egoscale/topology.go create mode 100644 Godeps/_workspace/src/github.com/pyr/egoscale/src/egoscale/types.go create mode 100644 Godeps/_workspace/src/github.com/pyr/egoscale/src/egoscale/vm.go create mode 100644 drivers/exoscale/exoscale.go create mode 100644 drivers/exoscale/exoscale_test.go create mode 100644 test/integration/driver-exoscale.bats diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 8f66b8e9ae..3cabd8df92 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -130,6 +130,10 @@ "ImportPath": "github.com/stretchr/testify/assert", "Rev": "e4ec8152c15fc46bd5056ce65997a07c7d415325" }, + { + "ImportPath": "github.com/pyr/egoscale/src/egoscale", + "Rev": "8bdfe1d0420634bdd37d73d00d51f86f8d08e481" + }, { "ImportPath": "github.com/tent/http-link-go", "Rev": "ac974c61c2f990f4115b119354b5e0b47550e888" diff --git a/Godeps/_workspace/src/github.com/pyr/egoscale/src/egoscale/async.go b/Godeps/_workspace/src/github.com/pyr/egoscale/src/egoscale/async.go new file mode 100644 index 0000000000..84401e7a4c --- /dev/null +++ b/Godeps/_workspace/src/github.com/pyr/egoscale/src/egoscale/async.go @@ -0,0 +1,36 @@ +package egoscale + +import ( + "encoding/json" + "net/url" +) + +func (exo *Client) PollAsyncJob(jobid string) (*QueryAsyncJobResultResponse, error) { + params := url.Values{} + + params.Set("jobid", jobid) + + resp, err := exo.Request("queryAsyncJobResult", params) + + if err != nil { + return nil, err + } + + var r QueryAsyncJobResultResponse + + if err := json.Unmarshal(resp, &r); err != nil { + return nil, err + } + + return &r, nil +} + +func (exo *Client) AsyncToVirtualMachine(resp QueryAsyncJobResultResponse) (*DeployVirtualMachineResponse, error) { + var r DeployVirtualMachineWrappedResponse + + if err := json.Unmarshal(resp.Jobresult, &r); err != nil { + return nil, err + } + + return &r.Wrapped, nil +} diff --git a/Godeps/_workspace/src/github.com/pyr/egoscale/src/egoscale/error.go b/Godeps/_workspace/src/github.com/pyr/egoscale/src/egoscale/error.go new file mode 100644 index 0000000000..3e7d2c77ae --- /dev/null +++ b/Godeps/_workspace/src/github.com/pyr/egoscale/src/egoscale/error.go @@ -0,0 +1,9 @@ +package egoscale + +import ( + "fmt" +) + +func (e *Error) Error() error { + return fmt.Errorf("exoscale API error %d (internal code: %d): %s", e.ErrorCode, e.CSErrorCode, e.ErrorText) +} diff --git a/Godeps/_workspace/src/github.com/pyr/egoscale/src/egoscale/groups.go b/Godeps/_workspace/src/github.com/pyr/egoscale/src/egoscale/groups.go new file mode 100644 index 0000000000..45ceef3608 --- /dev/null +++ b/Godeps/_workspace/src/github.com/pyr/egoscale/src/egoscale/groups.go @@ -0,0 +1,105 @@ +package egoscale + +import ( + "encoding/json" + "net/url" + "fmt" +) + +func (exo *Client) CreateEgressRule(rule SecurityGroupRule) (*AuthorizeSecurityGroupEgressResponse, error) { + + params := url.Values{} + params.Set("securitygroupid", rule.SecurityGroupId) + params.Set("cidrlist", rule.Cidr) + params.Set("protocol", rule.Protocol) + + if rule.Protocol == "ICMP" { + params.Set("icmpcode", fmt.Sprintf("%d", rule.IcmpCode)) + params.Set("icmptype", fmt.Sprintf("%d", rule.IcmpType)) + } else if (rule.Protocol == "TCP" || rule.Protocol == "UDP") { + params.Set("startport", fmt.Sprintf("%d", rule.Port)) + params.Set("endport", fmt.Sprintf("%d", rule.Port)) + } else { + return nil, fmt.Errorf("Invalid Egress rule Protocol: %s", rule.Protocol) + } + + resp, err := exo.Request("authorizeSecurityGroupEgress", params) + if err != nil { + return nil, err + } + + var r AuthorizeSecurityGroupEgressResponse + if err := json.Unmarshal(resp, &r); err != nil { + return nil, err + } + + return &r, nil +} + +func (exo *Client) CreateIngressRule(rule SecurityGroupRule) (*AuthorizeSecurityGroupIngressResponse, error) { + + params := url.Values{} + params.Set("securitygroupid", rule.SecurityGroupId) + params.Set("cidrlist", rule.Cidr) + params.Set("protocol", rule.Protocol) + + if rule.Protocol == "ICMP" { + params.Set("icmpcode", fmt.Sprintf("%d", rule.IcmpCode)) + params.Set("icmptype", fmt.Sprintf("%d", rule.IcmpType)) + } else if (rule.Protocol == "TCP" || rule.Protocol == "UDP") { + params.Set("startport", fmt.Sprintf("%d", rule.Port)) + params.Set("endport", fmt.Sprintf("%d", rule.Port)) + } else { + return nil, fmt.Errorf("Invalid Egress rule Protocol: %s", rule.Protocol) + } + + resp, err := exo.Request("authorizeSecurityGroupIngress", params) + + if err != nil { + return nil, err + } + + var r AuthorizeSecurityGroupIngressResponse + if err := json.Unmarshal(resp, &r); err != nil { + return nil, err + } + + return &r, nil +} + +func (exo *Client) CreateSecurityGroupWithRules(name string, ingress []SecurityGroupRule, egress []SecurityGroupRule) (*CreateSecurityGroupResponse, error) { + + params := url.Values{} + params.Set("name", name) + + resp, err := exo.Request("createSecurityGroup", params) + + var r CreateSecurityGroupResponseWrapper + if err := json.Unmarshal(resp, &r); err != nil { + return nil, err + } + + if err != nil { + return nil, err + } + + sgid := r.Wrapped.Id + + for _, erule := range(egress) { + erule.SecurityGroupId = sgid + _, err = exo.CreateEgressRule(erule) + if (err != nil) { + return nil, err + } + } + + for _, inrule := range(ingress) { + inrule.SecurityGroupId = sgid + _, err = exo.CreateIngressRule(inrule) + if (err != nil) { + return nil, err + } + } + + return &r.Wrapped, nil +} diff --git a/Godeps/_workspace/src/github.com/pyr/egoscale/src/egoscale/init.go b/Godeps/_workspace/src/github.com/pyr/egoscale/src/egoscale/init.go new file mode 100644 index 0000000000..13355bb9d4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/pyr/egoscale/src/egoscale/init.go @@ -0,0 +1,21 @@ +package egoscale + +import ( + "net/http" + "crypto/tls" +) + +func NewClient(endpoint string, apiKey string, apiSecret string) *Client { + cs := &Client { + client: &http.Client { + Transport: &http.Transport { + Proxy: http.ProxyFromEnvironment, + TLSClientConfig: &tls.Config { InsecureSkipVerify: false }, + }, + }, + endpoint: endpoint, + apiKey: apiKey, + apiSecret: apiSecret, + } + return cs +} diff --git a/Godeps/_workspace/src/github.com/pyr/egoscale/src/egoscale/keypair.go b/Godeps/_workspace/src/github.com/pyr/egoscale/src/egoscale/keypair.go new file mode 100644 index 0000000000..8f126e25eb --- /dev/null +++ b/Godeps/_workspace/src/github.com/pyr/egoscale/src/egoscale/keypair.go @@ -0,0 +1,23 @@ +package egoscale + +import ( + "encoding/json" + "net/url" +) + +func (exo *Client) CreateKeypair(name string) (*CreateSSHKeyPairResponse, error) { + params := url.Values{} + params.Set("name", name) + + resp, err := exo.Request("createSSHKeyPair", params) + if err != nil { + return nil, err + } + + var r CreateSSHKeyPairWrappedResponse + if err := json.Unmarshal(resp, &r); err != nil { + return nil, err + } + + return &r.Wrapped, nil +} diff --git a/Godeps/_workspace/src/github.com/pyr/egoscale/src/egoscale/request.go b/Godeps/_workspace/src/github.com/pyr/egoscale/src/egoscale/request.go new file mode 100644 index 0000000000..a194d649ee --- /dev/null +++ b/Godeps/_workspace/src/github.com/pyr/egoscale/src/egoscale/request.go @@ -0,0 +1,65 @@ +package egoscale + +import ( + "crypto/hmac" + "crypto/sha1" + "encoding/base64" + "encoding/json" + "fmt" + "io/ioutil" + "net/url" + "strings" +) + +func rawValue(b json.RawMessage) (json.RawMessage, error) { + var m map[string]json.RawMessage + + if err := json.Unmarshal(b, &m); err != nil { + return nil, err + } + for _, v := range m { + return v, nil + } + return nil, fmt.Errorf("Unable to extract raw value from:\n\n%s\n\n", string(b)) +} + +func (exo *Client) Request (command string, params url.Values) (json.RawMessage, error) { + + mac := hmac.New(sha1.New, []byte(exo.apiSecret)) + + params.Set("apikey", exo.apiKey) + params.Set("command", command) + params.Set("response", "json") + + s := strings.Replace(strings.ToLower(params.Encode()), "+", "%20", -1) + mac.Write([]byte(s)) + signature := url.QueryEscape(base64.StdEncoding.EncodeToString(mac.Sum(nil))) + + s = params.Encode() + url := exo.endpoint + "?" + s + "&signature=" + signature + + resp, err := exo.client.Get(url) + if err != nil { + return nil, err + } + + b, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return nil, err + } + + b, err = rawValue(b) + if err != nil { + return nil, err + } + + if resp.StatusCode != 200 { + var e Error + if err := json.Unmarshal(b, &e); err != nil { + return nil, err + } + return nil, e.Error() + } + return b, nil +} diff --git a/Godeps/_workspace/src/github.com/pyr/egoscale/src/egoscale/topology.go b/Godeps/_workspace/src/github.com/pyr/egoscale/src/egoscale/topology.go new file mode 100644 index 0000000000..77c65777d4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/pyr/egoscale/src/egoscale/topology.go @@ -0,0 +1,170 @@ +package egoscale + +import ( + "encoding/json" + "net/url" + "strings" + "fmt" + "regexp" +) + +func (exo *Client) GetSecurityGroups() (map[string]string, error) { + var sgs map[string]string + params := url.Values{} + resp, err := exo.Request("listSecurityGroups", params) + + if err != nil { + return nil, err + } + + var r ListSecurityGroupsResponse + if err := json.Unmarshal(resp, &r); err != nil { + return nil, err + } + + sgs = make(map[string]string) + for _, sg := range(r.SecurityGroups) { + sgs[sg.Name] = sg.Id + } + return sgs, nil +} + +func (exo *Client) GetZones() (map[string]string, error) { + var zones map[string]string + params := url.Values{} + resp, err := exo.Request("listZones", params) + + if err != nil { + return nil, err + } + + var r ListZonesResponse + if err := json.Unmarshal(resp, &r); err != nil { + return nil, err + } + + zones = make(map[string]string) + for _, zone := range(r.Zones) { + zones[zone.Name] = zone.Id + } + return zones, nil +} + +func (exo *Client) GetProfiles() (map[string]string, error) { + + var profiles map[string]string + params := url.Values{} + resp, err := exo.Request("listServiceOfferings", params) + + if err != nil { + return nil, err + } + + var r ListServiceOfferingsResponse + if err := json.Unmarshal(resp, &r); err != nil { + return nil, err + } + + profiles = make(map[string]string) + for _, offering := range(r.ServiceOfferings) { + profiles[strings.ToLower(offering.Name)] = offering.Id + } + + return profiles, nil +} + +func (exo *Client) GetKeypairs() ([]string, error) { + + var keypairs []string + params := url.Values{} + + resp, err := exo.Request("listSSHKeyPairs", params) + + if err != nil { + return nil, err + } + + var r ListSSHKeyPairsResponse + if err := json.Unmarshal(resp, &r); err != nil { + return nil, err + } + + keypairs = make([]string, r.Count, r.Count) + for i, keypair := range(r.SSHKeyPairs) { + keypairs[i] = keypair.Name + } + return keypairs, nil +} + +func (exo *Client) GetImages() (map[string]map[int]string, error) { + var images map[string]map[int]string + images = make(map[string]map[int]string) + + params := url.Values{} + params.Set("templatefilter", "featured") + + resp, err := exo.Request("listTemplates", params) + + if err != nil { + return nil, err + } + + var r ListTemplatesResponse + if err := json.Unmarshal(resp, &r); err != nil { + return nil, err + } + + re := regexp.MustCompile(`^Linux (?PUbuntu|Debian) (?P[0-9.]+).*$`) + for _, template := range(r.Templates) { + size := template.Size / (1024 * 1024 * 1024) + submatch := re.FindStringSubmatch(template.Name) + if len(submatch) > 0 { + name := strings.ToLower(submatch[1]) + version := submatch[2] + image := fmt.Sprintf("%s-%s", name, version) + + _, present := images[image] + if (!present) { + images[image] = make(map[int]string) + } + images[image][size] = template.Id + + images[fmt.Sprintf("%s-%s", name, version)][size] = template.Id + } + } + return images, nil +} + +func (exo *Client) GetTopology() (*Topology, error) { + + zones, err := exo.GetZones() + if (err != nil) { + return nil, err + } + images, err := exo.GetImages() + if (err != nil) { + return nil, err + } + groups, err := exo.GetSecurityGroups() + if (err != nil) { + return nil, err + } + keypairs, err := exo.GetKeypairs() + if (err != nil) { + return nil, err + } + profiles, err := exo.GetProfiles() + if (err != nil) { + return nil, err + } + + topo := &Topology{ + Zones: zones, + Profiles: profiles, + Images: images, + Keypairs: keypairs, + SecurityGroups: groups, + } + + return topo, nil +} diff --git a/Godeps/_workspace/src/github.com/pyr/egoscale/src/egoscale/types.go b/Godeps/_workspace/src/github.com/pyr/egoscale/src/egoscale/types.go new file mode 100644 index 0000000000..0abc100369 --- /dev/null +++ b/Godeps/_workspace/src/github.com/pyr/egoscale/src/egoscale/types.go @@ -0,0 +1,404 @@ +package egoscale + +import ( + "net/http" + "encoding/json" +) + +type Client struct { + client *http.Client + endpoint string + apiKey string + apiSecret string +} + + +type Error struct { + ErrorCode int `json:"errorcode"` + CSErrorCode int `json:"cserrorcode"` + ErrorText string `json:"errortext"` +} + +type Topology struct { + Zones map[string]string + Images map[string]map[int]string + Profiles map[string]string + Keypairs []string + SecurityGroups map[string]string +} + +type SecurityGroupRule struct { + Cidr string + IcmpType int + IcmpCode int + Port int + Protocol string + SecurityGroupId string +} + +type MachineProfile struct { + Name string + SecurityGroups []string + Keypair string + Userdata string + ServiceOffering string + Template string + Zone string +} + +type ListZonesResponse struct { + Count int `json:"count"` + Zones []*Zone `json:"zone"` +} + +type Zone struct { + Allocationstate string `json:"allocationstate,omitempty"` + Description string `json:"description,omitempty"` + Displaytext string `json:"displaytext,omitempty"` + Domain string `json:"domain,omitempty"` + Domainid string `json:"domainid,omitempty"` + Domainname string `json:"domainname,omitempty"` + Id string `json:"id,omitempty"` + Internaldns1 string `json:"internaldns1,omitempty"` + Internaldns2 string `json:"internaldns2,omitempty"` + Ip6dns1 string `json:"ip6dns1,omitempty"` + Ip6dns2 string `json:"ip6dns2,omitempty"` + Localstorageenabled bool `json:"localstorageenabled,omitempty"` + Name string `json:"name,omitempty"` + Networktype string `json:"networktype,omitempty"` + Resourcedetails map[string]string `json:"resourcedetails,omitempty"` + Securitygroupsenabled bool `json:"securitygroupsenabled,omitempty"` + Vlan string `json:"vlan,omitempty"` + Zonetoken string `json:"zonetoken,omitempty"` +} + +type ListServiceOfferingsResponse struct { + Count int `json:"count"` + ServiceOfferings []*ServiceOffering `json:"serviceoffering"` +} + +type ServiceOffering struct { + Cpunumber int `json:"cpunumber,omitempty"` + Cpuspeed int `json:"cpuspeed,omitempty"` + Displaytext string `json:"displaytext,omitempty"` + Domain string `json:"domain,omitempty"` + Domainid string `json:"domainid,omitempty"` + Hosttags string `json:"hosttags,omitempty"` + Id string `json:"id,omitempty"` + Iscustomized bool `json:"iscustomized,omitempty"` + Issystem bool `json:"issystem,omitempty"` + Isvolatile bool `json:"isvolatile,omitempty"` + Memory int `json:"memory,omitempty"` + Name string `json:"name,omitempty"` + Networkrate int `json:"networkrate,omitempty"` + Serviceofferingdetails map[string]string `json:"serviceofferingdetails,omitempty"` +} + +type ListTemplatesResponse struct { + Count int `json:"count"` + Templates []*Template `json:"template"` +} + +type Template struct { + Account string `json:"account,omitempty"` + Accountid string `json:"accountid,omitempty"` + Bootable bool `json:"bootable,omitempty"` + Checksum string `json:"checksum,omitempty"` + Created string `json:"created,omitempty"` + CrossZones bool `json:"crossZones,omitempty"` + Details map[string]string `json:"details,omitempty"` + Displaytext string `json:"displaytext,omitempty"` + Domain string `json:"domain,omitempty"` + Domainid string `json:"domainid,omitempty"` + Format string `json:"format,omitempty"` + Hostid string `json:"hostid,omitempty"` + Hostname string `json:"hostname,omitempty"` + Hypervisor string `json:"hypervisor,omitempty"` + Id string `json:"id,omitempty"` + Isdynamicallyscalable bool `json:"isdynamicallyscalable,omitempty"` + Isextractable bool `json:"isextractable,omitempty"` + Isfeatured bool `json:"isfeatured,omitempty"` + Ispublic bool `json:"ispublic,omitempty"` + Isready bool `json:"isready,omitempty"` + Name string `json:"name,omitempty"` + Ostypeid string `json:"ostypeid,omitempty"` + Ostypename string `json:"ostypename,omitempty"` + Passwordenabled bool `json:"passwordenabled,omitempty"` + Project string `json:"project,omitempty"` + Projectid string `json:"projectid,omitempty"` + Removed string `json:"removed,omitempty"` + Size int `json:"size,omitempty"` + Sourcetemplateid string `json:"sourcetemplateid,omitempty"` + Sshkeyenabled bool `json:"sshkeyenabled,omitempty"` + Status string `json:"status,omitempty"` + Zoneid string `json:"zoneid,omitempty"` + Zonename string `json:"zonename,omitempty"` +} + +type ListSSHKeyPairsResponse struct { + Count int `json:"count"` + SSHKeyPairs []*SSHKeyPair `json:"sshkeypair"` +} + +type SSHKeyPair struct { + Fingerprint string `json:"fingerprint,omitempty"` + Name string `json:"name,omitempty"` +} + + +type ListSecurityGroupsResponse struct { + Count int `json:"count"` + SecurityGroups []*SecurityGroup `json:"securitygroup"` +} + +type SecurityGroup struct { + Account string `json:"account,omitempty"` + Description string `json:"description,omitempty"` + Domain string `json:"domain,omitempty"` + Domainid string `json:"domainid,omitempty"` + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Project string `json:"project,omitempty"` + Projectid string `json:"projectid,omitempty"` +} + +type CreateSecurityGroupResponseWrapper struct { + Wrapped CreateSecurityGroupResponse `json:"securitygroup"` +} +type CreateSecurityGroupResponse struct { + Account string `json:"account,omitempty"` + Description string `json:"description,omitempty"` + Domain string `json:"domain,omitempty"` + Domainid string `json:"domainid,omitempty"` + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Project string `json:"project,omitempty"` + Projectid string `json:"projectid,omitempty"` +} + +type AuthorizeSecurityGroupIngressResponse struct { + JobID string `json:"jobid,omitempty"` + Account string `json:"account,omitempty"` + Cidr string `json:"cidr,omitempty"` + Endport int `json:"endport,omitempty"` + Icmpcode int `json:"icmpcode,omitempty"` + Icmptype int `json:"icmptype,omitempty"` + Protocol string `json:"protocol,omitempty"` + Ruleid string `json:"ruleid,omitempty"` + Securitygroupname string `json:"securitygroupname,omitempty"` + Startport int `json:"startport,omitempty"` +} + +type AuthorizeSecurityGroupEgressResponse struct { + JobID string `json:"jobid,omitempty"` + Account string `json:"account,omitempty"` + Cidr string `json:"cidr,omitempty"` + Endport int `json:"endport,omitempty"` + Icmpcode int `json:"icmpcode,omitempty"` + Icmptype int `json:"icmptype,omitempty"` + Protocol string `json:"protocol,omitempty"` + Ruleid string `json:"ruleid,omitempty"` + Securitygroupname string `json:"securitygroupname,omitempty"` + Startport int `json:"startport,omitempty"` +} + +type DeployVirtualMachineWrappedResponse struct { + Wrapped DeployVirtualMachineResponse `json:"virtualmachine"` +} + +type DeployVirtualMachineResponse struct { + JobID string `json:"jobid,omitempty"` + Account string `json:"account,omitempty"` + Affinitygroup []struct { + Account string `json:"account,omitempty"` + Description string `json:"description,omitempty"` + Domain string `json:"domain,omitempty"` + Domainid string `json:"domainid,omitempty"` + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + VirtualmachineIds []string `json:"virtualmachineIds,omitempty"` + + } `json:"affinitygroup,omitempty"` + Cpunumber int `json:"cpunumber,omitempty"` + Cpuspeed int `json:"cpuspeed,omitempty"` + Cpuused string `json:"cpuused,omitempty"` + Created string `json:"created,omitempty"` + Details map[string]string `json:"details,omitempty"` + Diskioread int `json:"diskioread,omitempty"` + Diskiowrite int `json:"diskiowrite,omitempty"` + Diskkbsread int `json:"diskkbsread,omitempty"` + Diskkbswrite int `json:"diskkbswrite,omitempty"` + Displayname string `json:"displayname,omitempty"` + Displayvm bool `json:"displayvm,omitempty"` + Domain string `json:"domain,omitempty"` + Domainid string `json:"domainid,omitempty"` + Forvirtualnetwork bool `json:"forvirtualnetwork,omitempty"` + Group string `json:"group,omitempty"` + Groupid string `json:"groupid,omitempty"` + Guestosid string `json:"guestosid,omitempty"` + Haenable bool `json:"haenable,omitempty"` + Hostid string `json:"hostid,omitempty"` + Hostname string `json:"hostname,omitempty"` + Hypervisor string `json:"hypervisor,omitempty"` + Id string `json:"id,omitempty"` + Instancename string `json:"instancename,omitempty"` + Isdynamicallyscalable bool `json:"isdynamicallyscalable,omitempty"` + Isodisplaytext string `json:"isodisplaytext,omitempty"` + Isoid string `json:"isoid,omitempty"` + Isoname string `json:"isoname,omitempty"` + Keypair string `json:"keypair,omitempty"` + Memory int `json:"memory,omitempty"` + Name string `json:"name,omitempty"` + Networkkbsread int `json:"networkkbsread,omitempty"` + Networkkbswrite int `json:"networkkbswrite,omitempty"` + Nic []struct { + Broadcasturi string `json:"broadcasturi,omitempty"` + Gateway string `json:"gateway,omitempty"` + Id string `json:"id,omitempty"` + Ipaddress string `json:"ipaddress,omitempty"` + Isdefault bool `json:"isdefault,omitempty"` + Isolationuri string `json:"isolationuri,omitempty"` + Macaddress string `json:"macaddress,omitempty"` + Netmask string `json:"netmask,omitempty"` + Networkid string `json:"networkid,omitempty"` + Networkname string `json:"networkname,omitempty"` + Secondaryip []string `json:"secondaryip,omitempty"` + Traffictype string `json:"traffictype,omitempty"` + Type string `json:"type,omitempty"` + } `json:"nic,omitempty"` + Password string `json:"password,omitempty"` + Passwordenabled bool `json:"passwordenabled,omitempty"` + Project string `json:"project,omitempty"` + Projectid string `json:"projectid,omitempty"` + Publicip string `json:"publicip,omitempty"` + Publicipid string `json:"publicipid,omitempty"` + Rootdeviceid int `json:"rootdeviceid,omitempty"` + Rootdevicetype string `json:"rootdevicetype,omitempty"` + Serviceofferingid string `json:"serviceofferingid,omitempty"` + Serviceofferingname string `json:"serviceofferingname,omitempty"` + Servicestate string `json:"servicestate,omitempty"` + State string `json:"state,omitempty"` + Templatedisplaytext string `json:"templatedisplaytext,omitempty"` + Templateid string `json:"templateid,omitempty"` + Templatename string `json:"templatename,omitempty"` + Zoneid string `json:"zoneid,omitempty"` + Zonename string `json:"zonename,omitempty"` +} + +type QueryAsyncJobResultResponse struct { + Accountid string `json:"accountid,omitempty"` + Cmd string `json:"cmd,omitempty"` + Created string `json:"created,omitempty"` + Jobinstanceid string `json:"jobinstanceid,omitempty"` + Jobinstancetype string `json:"jobinstancetype,omitempty"` + Jobprocstatus int `json:"jobprocstatus,omitempty"` + Jobresult json.RawMessage `json:"jobresult,omitempty"` + Jobresultcode int `json:"jobresultcode,omitempty"` + Jobresulttype string `json:"jobresulttype,omitempty"` + Jobstatus int `json:"jobstatus,omitempty"` + Userid string `json:"userid,omitempty"` +} + +type ListVirtualMachinesResponse struct { + Count int `json:"count"` + VirtualMachines []*VirtualMachine `json:"virtualmachine"` +} + +type VirtualMachine struct { + Account string `json:"account,omitempty"` + Cpunumber int `json:"cpunumber,omitempty"` + Cpuspeed int `json:"cpuspeed,omitempty"` + Cpuused string `json:"cpuused,omitempty"` + Created string `json:"created,omitempty"` + Details map[string]string `json:"details,omitempty"` + Diskioread int `json:"diskioread,omitempty"` + Diskiowrite int `json:"diskiowrite,omitempty"` + Diskkbsread int `json:"diskkbsread,omitempty"` + Diskkbswrite int `json:"diskkbswrite,omitempty"` + Displayname string `json:"displayname,omitempty"` + Displayvm bool `json:"displayvm,omitempty"` + Domain string `json:"domain,omitempty"` + Domainid string `json:"domainid,omitempty"` + Forvirtualnetwork bool `json:"forvirtualnetwork,omitempty"` + Group string `json:"group,omitempty"` + Groupid string `json:"groupid,omitempty"` + Guestosid string `json:"guestosid,omitempty"` + Haenable bool `json:"haenable,omitempty"` + Hostid string `json:"hostid,omitempty"` + Hostname string `json:"hostname,omitempty"` + Hypervisor string `json:"hypervisor,omitempty"` + Id string `json:"id,omitempty"` + Instancename string `json:"instancename,omitempty"` + Isdynamicallyscalable bool `json:"isdynamicallyscalable,omitempty"` + Isodisplaytext string `json:"isodisplaytext,omitempty"` + Isoid string `json:"isoid,omitempty"` + Isoname string `json:"isoname,omitempty"` + Keypair string `json:"keypair,omitempty"` + Memory int `json:"memory,omitempty"` + Name string `json:"name,omitempty"` + Networkkbsread int `json:"networkkbsread,omitempty"` + Networkkbswrite int `json:"networkkbswrite,omitempty"` + Nic []struct { + Broadcasturi string `json:"broadcasturi,omitempty"` + Gateway string `json:"gateway,omitempty"` + Id string `json:"id,omitempty"` + Ip6address string `json:"ip6address,omitempty"` + Ip6cidr string `json:"ip6cidr,omitempty"` + Ip6gateway string `json:"ip6gateway,omitempty"` + Ipaddress string `json:"ipaddress,omitempty"` + Isdefault bool `json:"isdefault,omitempty"` + Isolationuri string `json:"isolationuri,omitempty"` + Macaddress string `json:"macaddress,omitempty"` + Netmask string `json:"netmask,omitempty"` + Networkid string `json:"networkid,omitempty"` + Networkname string `json:"networkname,omitempty"` + Secondaryip []string `json:"secondaryip,omitempty"` + Traffictype string `json:"traffictype,omitempty"` + Type string `json:"type,omitempty"` + + } `json:"nic,omitempty"` + Password string `json:"password,omitempty"` + Passwordenabled bool `json:"passwordenabled,omitempty"` + Project string `json:"project,omitempty"` + Projectid string `json:"projectid,omitempty"` + Publicip string `json:"publicip,omitempty"` + Publicipid string `json:"publicipid,omitempty"` + Rootdeviceid int `json:"rootdeviceid,omitempty"` + Rootdevicetype string `json:"rootdevicetype,omitempty"` + Serviceofferingid string `json:"serviceofferingid,omitempty"` + Serviceofferingname string `json:"serviceofferingname,omitempty"` + Servicestate string `json:"servicestate,omitempty"` + State string `json:"state,omitempty"` + Templatedisplaytext string `json:"templatedisplaytext,omitempty"` + Templateid string `json:"templateid,omitempty"` + Templatename string `json:"templatename,omitempty"` + Zoneid string `json:"zoneid,omitempty"` + Zonename string `json:"zonename,omitempty"` +} + + +type StartVirtualMachineResponse struct { + JobID string `json:"jobid,omitempty"` +} + +type StopVirtualMachineResponse struct { + JobID string `json:"jobid,omitempty"` +} + +type DestroyVirtualMachineResponse struct { + JobID string `json:"jobid,omitempty"` +} + + +type RebootVirtualMachineResponse struct { + JobID string `json:"jobid,omitempty"` +} + +type CreateSSHKeyPairWrappedResponse struct { + Wrapped CreateSSHKeyPairResponse `json:"keypair,omitempty"` +} + +type CreateSSHKeyPairResponse struct { + Privatekey string `json:"privatekey,omitempty"` +} diff --git a/Godeps/_workspace/src/github.com/pyr/egoscale/src/egoscale/vm.go b/Godeps/_workspace/src/github.com/pyr/egoscale/src/egoscale/vm.go new file mode 100644 index 0000000000..75495e1adf --- /dev/null +++ b/Godeps/_workspace/src/github.com/pyr/egoscale/src/egoscale/vm.go @@ -0,0 +1,142 @@ +package egoscale + +import ( + "encoding/base64" + "encoding/json" + "net/url" + "strings" + "fmt" +) + +func (exo *Client) CreateVirtualMachine(p MachineProfile) (string, error) { + + params := url.Values{} + params.Set("serviceofferingid", p.ServiceOffering) + params.Set("templateid", p.Template) + params.Set("zoneid", p.Zone) + + params.Set("displayname", p.Name) + if len(p.Userdata) > 0 { + params.Set("userdata", base64.StdEncoding.EncodeToString([]byte(p.Userdata))) + } + if len(p.Keypair) > 0 { + params.Set("keypair", p.Keypair) + } + + params.Set("securitygroupids", strings.Join(p.SecurityGroups, ",")) + + resp, err := exo.Request("deployVirtualMachine", params) + + if err != nil { + return "", err + } + + var r DeployVirtualMachineResponse + + if err := json.Unmarshal(resp, &r); err != nil { + return "", err + } + + return r.JobID, nil +} + +func (exo *Client) StartVirtualMachine(id string) (string, error) { + params := url.Values{} + params.Set("id", id) + + resp, err := exo.Request("startVirtualMachine", params) + + if err != nil { + return "", err + } + + var r StartVirtualMachineResponse + + if err := json.Unmarshal(resp, &r); err != nil { + return "", err + } + + return r.JobID, nil +} + +func (exo *Client) StopVirtualMachine(id string) (string, error) { + params := url.Values{} + params.Set("id", id) + + resp, err := exo.Request("stopVirtualMachine", params) + + if err != nil { + return "", err + } + + var r StopVirtualMachineResponse + + if err := json.Unmarshal(resp, &r); err != nil { + return "", err + } + + return r.JobID, nil +} + +func (exo *Client) RebootVirtualMachine(id string) (string, error) { + params := url.Values{} + params.Set("id", id) + + resp, err := exo.Request("rebootVirtualMachine", params) + + if err != nil { + return "", err + } + + var r RebootVirtualMachineResponse + + if err := json.Unmarshal(resp, &r); err != nil { + return "", err + } + + return r.JobID, nil +} + +func (exo *Client) DestroyVirtualMachine(id string) (string, error) { + params := url.Values{} + params.Set("id", id) + + resp, err := exo.Request("destroyVirtualMachine", params) + + if err != nil { + return "", err + } + + var r DestroyVirtualMachineResponse + + if err := json.Unmarshal(resp, &r); err != nil { + return "", err + } + + return r.JobID, nil +} + +func (exo *Client) GetVirtualMachine(id string) (*VirtualMachine, error) { + + params := url.Values{} + params.Set("id", id) + + resp, err := exo.Request("listVirtualMachines", params) + + if err != nil { + return nil, err + } + + var r ListVirtualMachinesResponse + + if err := json.Unmarshal(resp, &r); err != nil { + return nil, err + } + + if len(r.VirtualMachines) == 1 { + machine := r.VirtualMachines[0] + return machine, nil + } else { + return nil, fmt.Errorf("cannot retrieve virtualmachine with id %s", id) + } +} diff --git a/commands/commands.go b/commands/commands.go index a4dadb3075..4b407d81a7 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -16,6 +16,7 @@ import ( _ "github.com/docker/machine/drivers/amazonec2" _ "github.com/docker/machine/drivers/azure" _ "github.com/docker/machine/drivers/digitalocean" + _ "github.com/docker/machine/drivers/exoscale" _ "github.com/docker/machine/drivers/google" _ "github.com/docker/machine/drivers/hyperv" _ "github.com/docker/machine/drivers/none" diff --git a/docs/index.md b/docs/index.md index de529ecfd6..82af95888b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1120,6 +1120,21 @@ Options: The VMware vSphere driver uses the latest boot2docker image. +#### exoscale +Create machines on [exoscale](https://www.exoscale.ch/). + +Get your API key and API secret key from [API details](https://portal.exoscale.ch/account/api) and pass them to `machine create` with the `--exoscale-api-key` and `--exoscale-api-secret-key` options. + +Options: + + - `--exoscale-api-key`: Your API key. + - `--exoscale-api-secret-key`: Your API secret key. + - `--exoscale-instance-profile`: Instance profile. Default: `small`. + - `--exoscale-disk-size`: Disk size for the host in GB. Default: `50`. + - `--exoscale-security-group`: Security group. It will be created if it doesn't exist. Default: `docker-machine`. + +If a custom security group is provided, you need to ensure that you allow TCP port 2376 in an ingress rule. + ## Release Notes ### Version 0.2.0 (April 16, 2015) diff --git a/drivers/exoscale/exoscale.go b/drivers/exoscale/exoscale.go new file mode 100644 index 0000000000..ce0ff79bf2 --- /dev/null +++ b/drivers/exoscale/exoscale.go @@ -0,0 +1,470 @@ +package exoscale + +import ( + "bytes" + "fmt" + "io/ioutil" + "path/filepath" + "strings" + "text/template" + "time" + + "github.com/codegangsta/cli" + "github.com/docker/machine/drivers" + "github.com/docker/machine/log" + "github.com/docker/machine/provider" + "github.com/docker/machine/state" + "github.com/pyr/egoscale/src/egoscale" +) + +type Driver struct { + URL string + ApiKey string + ApiSecretKey string + InstanceProfile string + DiskSize int + Image string + SecurityGroup string + AvailabilityZone string + MachineName string + KeyPair string + IPAddress string + PublicKey string + Id string + CaCertPath string + PrivateKeyPath string + DriverKeyPath string + SwarmMaster bool + SwarmHost string + SwarmDiscovery string + storePath string +} + +func init() { + drivers.Register("exoscale", &drivers.RegisteredDriver{ + New: NewDriver, + GetCreateFlags: GetCreateFlags, + }) +} + +// RegisterCreateFlags registers the flags this driver adds to +// "docker hosts create" +func GetCreateFlags() []cli.Flag { + return []cli.Flag{ + cli.StringFlag{ + EnvVar: "EXOSCALE_ENDPOINT", + Name: "exoscale-url", + Usage: "exoscale API endpoint", + }, + cli.StringFlag{ + EnvVar: "EXOSCALE_API_KEY", + Name: "exoscale-api-key", + Usage: "exoscale API key", + }, + cli.StringFlag{ + EnvVar: "EXOSCALE_API_SECRET", + Name: "exoscale-api-secret-key", + Usage: "exoscale API secret key", + }, + cli.StringFlag{ + EnvVar: "EXOSCALE_INSTANCE_PROFILE", + Name: "exoscale-instance-profile", + Value: "small", + Usage: "exoscale instance profile (small, medium, large, ...)", + }, + cli.IntFlag{ + EnvVar: "EXOSCALE_DISK_SIZE", + Name: "exoscale-disk-size", + Value: 50, + Usage: "exoscale disk size (10, 50, 100, 200, 400)", + }, + cli.StringFlag{ + EnvVar: "EXSOCALE_IMAGE", + Name: "exoscale-image", + Value: "ubuntu-14.04", + Usage: "exoscale image template", + }, + cli.StringFlag{ + EnvVar: "EXOSCALE_SECURITY_GROUP", + Name: "exoscale-security-group", + Value: "docker-machine", + Usage: "exoscale security group", + }, + cli.StringFlag{ + EnvVar: "EXOSCALE_AVAILABILITY_ZONE", + Name: "exoscale-availability-zone", + Value: "ch-gva-2", + Usage: "exoscale availibility zone", + }, + cli.StringFlag{ + EnvVar: "EXOSCALE_KEYPAIR", + Name: "exoscale-keypair", + Usage: "exoscale keypair name", + }, + } +} + +func NewDriver(machineName string, storePath string, caCert string, privateKey string) (drivers.Driver, error) { + return &Driver{MachineName: machineName, storePath: storePath, CaCertPath: caCert, PrivateKeyPath: privateKey}, nil +} + +func (d *Driver) AuthorizePort(ports []*drivers.Port) error { + return nil +} + +func (d *Driver) DeauthorizePort(ports []*drivers.Port) error { + return nil +} + +func (d *Driver) GetMachineName() string { + return d.MachineName +} + +func (d *Driver) GetSSHHostname() (string, error) { + return d.GetIP() +} + +func (d *Driver) GetSSHKeyPath() string { + return filepath.Join(d.storePath, "id_rsa") +} + +func (d *Driver) GetSSHPort() (int, error) { + return 22, nil +} + +func (d *Driver) GetSSHUsername() string { + return "ubuntu" +} + +func (d *Driver) GetProviderType() provider.ProviderType { + return provider.Remote +} + +func (d *Driver) DriverName() string { + return "exoscale" +} + +func (d *Driver) SetConfigFromFlags(flags drivers.DriverOptions) error { + d.URL = flags.String("exoscale-endpoint") + d.ApiKey = flags.String("exoscale-api-key") + d.ApiSecretKey = flags.String("exoscale-api-secret-key") + d.InstanceProfile = flags.String("exoscale-instance-profile") + d.DiskSize = flags.Int("exoscale-disk-size") + d.Image = flags.String("exoscale-image") + d.SecurityGroup = flags.String("exoscale-security-group") + d.AvailabilityZone = flags.String("exoscale-availability-zone") + d.KeyPair = flags.String("exoscale-keypair") + d.SwarmMaster = flags.Bool("swarm-master") + d.SwarmHost = flags.String("swarm-host") + d.SwarmDiscovery = flags.String("swarm-discovery") + + if d.URL == "" { + d.URL = "https://api.exoscale.ch/compute" + } + if d.ApiKey == "" || d.ApiSecretKey == "" { + return fmt.Errorf("Please specify an API key (--exoscale-api-key) and an API secret key (--exoscale-api-secret-key).") + } + + return nil +} + +func (d *Driver) GetURL() (string, error) { + ip, err := d.GetIP() + if err != nil { + return "", err + } + return fmt.Sprintf("tcp://%s:2376", ip), nil +} + +func (d *Driver) GetIP() (string, error) { + if d.IPAddress == "" { + return "", fmt.Errorf("IP address is not set") + } + return d.IPAddress, nil +} + +func (d *Driver) GetState() (state.State, error) { + client := egoscale.NewClient(d.URL, d.ApiKey, d.ApiSecretKey) + vm, err := client.GetVirtualMachine(d.Id) + if err != nil { + return state.Error, err + } + switch vm.State { + case "Starting": + return state.Starting, nil + case "Running": + return state.Running, nil + case "Stopping": + return state.Running, nil + case "Stopped": + return state.Stopped, nil + case "Destroyed": + return state.Stopped, nil + case "Expunging": + return state.Stopped, nil + case "Migrating": + return state.Paused, nil + case "Error": + return state.Error, nil + case "Unknown": + return state.Error, nil + case "Shutdowned": + return state.Stopped, nil + } + return state.None, nil +} + +func (d *Driver) PreCreateCheck() error { + return nil +} + +func (d *Driver) Create() error { + log.Infof("Querying exoscale for the requested parameters...") + client := egoscale.NewClient(d.URL, d.ApiKey, d.ApiSecretKey) + topology, err := client.GetTopology() + if err != nil { + return err + } + + // Availability zone UUID + zone, ok := topology.Zones[d.AvailabilityZone] + if !ok { + return fmt.Errorf("Availability zone %v doesn't exist", + d.AvailabilityZone) + } + log.Debugf("Availability zone %v = %s", d.AvailabilityZone, zone) + + // Image UUID + var tpl string + images, ok := topology.Images[strings.ToLower(d.Image)] + if ok { + tpl, ok = images[d.DiskSize] + } + if !ok { + return fmt.Errorf("Unable to find image %v with size %d", + d.Image, d.DiskSize) + } + log.Debugf("Image %v(%d) = %s", d.Image, d.DiskSize, tpl) + + // Profile UUID + profile, ok := topology.Profiles[strings.ToLower(d.InstanceProfile)] + if !ok { + return fmt.Errorf("Unable to find the %s profile", + d.InstanceProfile) + } + log.Debugf("Profile %v = %s", d.InstanceProfile, profile) + + // Security group + sg, ok := topology.SecurityGroups[d.SecurityGroup] + if !ok { + log.Infof("Security group %v does not exist, create it", + d.SecurityGroup) + rules := []egoscale.SecurityGroupRule{ + { + SecurityGroupId: "", + Cidr: "0.0.0.0/0", + Protocol: "TCP", + Port: 22, + }, + { + SecurityGroupId: "", + Cidr: "0.0.0.0/0", + Protocol: "TCP", + Port: 2376, + }, + { + SecurityGroupId: "", + Cidr: "0.0.0.0/0", + Protocol: "ICMP", + IcmpType: 8, + IcmpCode: 0, + }, + } + sgresp, err := client.CreateSecurityGroupWithRules(d.SecurityGroup, + rules, + make([]egoscale.SecurityGroupRule, 0, 0)) + if err != nil { + return err + } + sg = sgresp.Id + } + log.Debugf("Security group %v = %s", d.SecurityGroup, sg) + + if d.KeyPair == "" { + log.Infof("Generate an SSH keypair...") + kpresp, err := client.CreateKeypair(d.MachineName) + if err != nil { + return err + } + err = ioutil.WriteFile(d.GetSSHKeyPath(), []byte(kpresp.Privatekey), 0600) + if err != nil { + return err + } + d.KeyPair = d.MachineName + } + + log.Infof("Spawn exoscale host...") + + userdata, err := d.getCloudInit() + if err != nil { + return err + } + log.Debugf("Using the following cloud-init file:") + log.Debugf("%s", userdata) + + machineProfile := egoscale.MachineProfile{ + Template: tpl, + ServiceOffering: profile, + SecurityGroups: []string{sg}, + Userdata: userdata, + Zone: zone, + Keypair: d.KeyPair, + Name: d.MachineName, + } + + cvmresp, err := client.CreateVirtualMachine(machineProfile) + if err != nil { + return err + } + + vm, err := d.waitForVM(client, cvmresp) + if err != nil { + return err + } + d.IPAddress = vm.Nic[0].Ipaddress + d.Id = vm.Id + + return nil +} + +func (d *Driver) Start() error { + vmstate, err := d.GetState() + if err != nil { + return err + } + if vmstate == state.Running || vmstate == state.Starting { + log.Infof("Host is already running or starting") + return nil + } + + client := egoscale.NewClient(d.URL, d.ApiKey, d.ApiSecretKey) + svmresp, err := client.StartVirtualMachine(d.Id) + if err != nil { + return err + } + _, err = d.waitForVM(client, svmresp) + if err != nil { + return err + } + return nil +} + +func (d *Driver) Stop() error { + vmstate, err := d.GetState() + if err != nil { + return err + } + if vmstate == state.Stopped { + log.Infof("Host is already stopped") + return nil + } + + client := egoscale.NewClient(d.URL, d.ApiKey, d.ApiSecretKey) + svmresp, err := client.StopVirtualMachine(d.Id) + if err != nil { + return err + } + _, err = d.waitForVM(client, svmresp) + if err != nil { + return err + } + return nil +} + +func (d *Driver) Remove() error { + client := egoscale.NewClient(d.URL, d.ApiKey, d.ApiSecretKey) + dvmresp, err := client.DestroyVirtualMachine(d.Id) + if err != nil { + return err + } + _, err = d.waitForVM(client, dvmresp) + if err != nil { + return err + } + return nil +} + +func (d *Driver) Restart() error { + vmstate, err := d.GetState() + if err != nil { + return err + } + if vmstate == state.Stopped { + return fmt.Errorf("Host is stopped, use start command to start it") + } + + client := egoscale.NewClient(d.URL, d.ApiKey, d.ApiSecretKey) + svmresp, err := client.RebootVirtualMachine(d.Id) + if err != nil { + return err + } + _, err = d.waitForVM(client, svmresp) + if err != nil { + return err + } + + return nil +} + +func (d *Driver) Kill() error { + return d.Stop() +} + +func (d *Driver) waitForVM(client *egoscale.Client, jobid string) (*egoscale.DeployVirtualMachineResponse, error) { + log.Infof("Waiting for VM...") + maxRepeats := 60 + i := 0 + var resp *egoscale.QueryAsyncJobResultResponse + var err error + for ; i < maxRepeats; i++ { + resp, err = client.PollAsyncJob(jobid) + if err != nil { + return nil, err + } + + if resp.Jobstatus == 1 { + break + } + time.Sleep(2 * time.Second) + } + if i == maxRepeats { + return nil, fmt.Errorf("Timeout while waiting for VM") + } + vm, err := client.AsyncToVirtualMachine(*resp) + if err != nil { + return nil, err + } + + return vm, nil +} + +// Build a cloud-init user data string that will install and run +// docker. +func (d *Driver) getCloudInit() (string, error) { + const tpl = `#cloud-config +manage_etc_hosts: true +fqdn: {{ .MachineName }} +resize_rootfs: true +` + var buffer bytes.Buffer + + tmpl, err := template.New("cloud-init").Parse(tpl) + if err != nil { + return "", err + } + err = tmpl.Execute(&buffer, d) + if err != nil { + return "", err + } + return buffer.String(), nil +} diff --git a/drivers/exoscale/exoscale_test.go b/drivers/exoscale/exoscale_test.go new file mode 100644 index 0000000000..b6e101732a --- /dev/null +++ b/drivers/exoscale/exoscale_test.go @@ -0,0 +1 @@ +package exoscale diff --git a/test/integration/driver-exoscale.bats b/test/integration/driver-exoscale.bats new file mode 100644 index 0000000000..e6c0accabf --- /dev/null +++ b/test/integration/driver-exoscale.bats @@ -0,0 +1,114 @@ +#!/usr/bin/env bats + +load helpers + +export DRIVER=exoscale +export NAME="bats-$DRIVER-test" +export MACHINE_STORAGE_PATH=/tmp/machine-bats-test-$DRIVER + +@test "$DRIVER: machine should not exist" { + run machine active $NAME + [ "$status" -eq 1 ] +} + +@test "$DRIVER: create" { + run machine create -d $DRIVER $NAME + [ "$status" -eq 0 ] +} + +@test "$DRIVER: active" { + run machine active $NAME + [ "$status" -eq 0 ] +} + +@test "$DRIVER: ls" { + run machine ls + [ "$status" -eq 0 ] + [[ ${lines[1]} == *"$NAME"* ]] +} + +@test "$DRIVER: run busybox container" { + run docker $(machine config $NAME) run busybox echo hello world + [ "$status" -eq 0 ] +} + +@test "$DRIVER: url" { + run machine url $NAME + [ "$status" -eq 0 ] +} + +@test "$DRIVER: ip" { + run machine ip $NAME + [ "$status" -eq 0 ] +} + +@test "$DRIVER: ssh" { + run machine ssh $NAME -- ls -lah / + [ "$status" -eq 0 ] + [[ ${lines[0]} =~ "total" ]] +} + +@test "$DRIVER: docker commands with the socket should work" { + run machine ssh $NAME -- docker version +} + +@test "$DRIVER: stop" { + run machine stop $NAME + [ "$status" -eq 0 ] +} + +@test "$DRIVER: machine should show stopped after stop" { + run machine ls + [ "$status" -eq 0 ] + [[ ${lines[1]} == *"Stopped"* ]] +} + +@test "$DRIVER: start" { + run machine start $NAME + [ "$status" -eq 0 ] +} + +@test "$DRIVER: machine should show running after start" { + run machine ls + [ "$status" -eq 0 ] + [[ ${lines[1]} == *"Running"* ]] +} + +@test "$DRIVER: kill" { + run machine kill $NAME + [ "$status" -eq 0 ] +} + +@test "$DRIVER: machine should show stopped after kill" { + run machine ls + [ "$status" -eq 0 ] + [[ ${lines[1]} == *"Stopped"* ]] +} + +@test "$DRIVER: restart" { + run machine restart $NAME + [ "$status" -eq 0 ] +} + +@test "$DRIVER: machine should show running after restart" { + run machine ls + [ "$status" -eq 0 ] + [[ ${lines[1]} == *"Running"* ]] +} + +@test "$DRIVER: remove" { + run sleep 20 + run machine rm -f $NAME + [ "$status" -eq 0 ] +} + +@test "$DRIVER: machine should not exist" { + run machine active $NAME + [ "$status" -eq 1 ] +} + +@test "$DRIVER: cleanup" { + run rm -rf $MACHINE_STORAGE_PATH + [ "$status" -eq 0 ] +} +