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 <Vincent.Bernat@exoscale.ch>
This commit is contained in:
Vincent Bernat 2014-12-05 17:17:44 +01:00
parent 7dcd4c2dfd
commit fd569c8fdf
15 changed files with 1580 additions and 0 deletions

4
Godeps/Godeps.json generated
View File

@ -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"

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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 (?P<name>Ubuntu|Debian) (?P<version>[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
}

View File

@ -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"`
}

View File

@ -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)
}
}

View File

@ -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"

View File

@ -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)

View File

@ -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
}

View File

@ -0,0 +1 @@
package exoscale

View File

@ -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 ]
}