diff --git a/Gopkg.lock b/Gopkg.lock index ce49a77b51..339e550e4f 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -129,8 +129,8 @@ [[projects]] name = "github.com/exoscale/egoscale" packages = ["."] - revision = "401e52c49deedb261ddf0e2432a645abbb66bb4e" - version = "v0.9.10" + revision = "5c191f1d82540498eb0552346f4a4db03a63e657" + version = "v0.9.14" [[projects]] name = "github.com/go-ini/ini" @@ -147,6 +147,12 @@ packages = ["query"] revision = "30f7a39f4a218feb5325f3aebc60c32a572a8274" +[[projects]] + branch = "master" + name = "github.com/jinzhu/copier" + packages = ["."] + revision = "7e38e58719c33e0d44d585c4ab477a30f8cb82dd" + [[projects]] name = "github.com/jmespath/go-jmespath" packages = ["."] @@ -336,6 +342,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "e99aac76aae722f534984877f01c84fa023d10b3cf07e4acba9f33b792fd23a3" + inputs-digest = "4bf60991beceac36c64aa2749d5150563e6f1bd2a0ec1f45aa20438755b7f219" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index d6310fdea3..90fa11dfc3 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -1,30 +1,3 @@ -# Gopkg.toml example -# -# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html -# for detailed Gopkg.toml documentation. -# -# required = ["github.com/user/thing/cmd/thing"] -# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] -# -# [[constraint]] -# name = "github.com/user/project" -# version = "1.0.0" -# -# [[constraint]] -# name = "github.com/user/project2" -# branch = "dev" -# source = "github.com/myfork/project2" -# -# [[override]] -# name = "github.com/x/y" -# version = "2.4.0" -# -# [prune] -# non-go = false -# go-tests = true -# unused-packages = true - - [[constraint]] name = "github.com/Azure/azure-sdk-for-go" version = "5.0.0-beta" @@ -43,7 +16,7 @@ [[constraint]] name = "github.com/exoscale/egoscale" - version = "0.9.10" + version = "0.9.14" [[constraint]] branch = "master" diff --git a/drivers/exoscale/exoscale.go b/drivers/exoscale/exoscale.go index d5dde85963..19740e219c 100644 --- a/drivers/exoscale/exoscale.go +++ b/drivers/exoscale/exoscale.go @@ -2,6 +2,7 @@ package exoscale import ( "bytes" + "context" "encoding/base64" "errors" "fmt" @@ -10,6 +11,7 @@ import ( "os" "os/user" "path/filepath" + "regexp" "strings" "github.com/docker/machine/libmachine/drivers" @@ -34,10 +36,10 @@ type Driver struct { AvailabilityZone string SSHKey string KeyPair string + Password string PublicKey string UserDataFile string ID string `json:"Id"` - async egoscale.AsyncInfo } const ( @@ -136,10 +138,6 @@ func NewDriver(machineName, storePath string) drivers.Driver { DiskSize: defaultDiskSize, Image: defaultImage, AvailabilityZone: defaultAvailabilityZone, - async: egoscale.AsyncInfo{ - Retries: 3, - Delay: 20, - }, BaseDriver: &drivers.BaseDriver{ MachineName: machineName, StorePath: storePath, @@ -246,19 +244,15 @@ func (d *Driver) client() *egoscale.Client { func (d *Driver) virtualMachine() (*egoscale.VirtualMachine, error) { cs := d.client() - resp, err := cs.Request(&egoscale.ListVirtualMachines{ + virtualMachine := &egoscale.VirtualMachine{ ID: d.ID, - }) - if err != nil { - return nil, err } - vms := resp.(*egoscale.ListVirtualMachinesResponse) - if vms.Count == 0 { - return nil, fmt.Errorf("No VM found. %s", d.ID) + + if err := cs.GetWithContext(context.TODO(), virtualMachine); err != nil { + return nil, err } - virtualMachine := vms.VirtualMachine[0] - return &virtualMachine, nil + return virtualMachine, nil } // GetState returns a github.com/machine/libmachine/state.State representing the state of the host (running, stopped, etc.) @@ -294,7 +288,7 @@ func (d *Driver) GetState() (state.State, error) { func (d *Driver) createDefaultSecurityGroup(group string) (*egoscale.SecurityGroup, error) { cs := d.client() - resp, err := cs.Request(&egoscale.CreateSecurityGroup{ + resp, err := cs.RequestWithContext(context.TODO(), &egoscale.CreateSecurityGroup{ Name: group, Description: "created by docker-machine", }) @@ -372,7 +366,7 @@ func (d *Driver) createDefaultSecurityGroup(group string) (*egoscale.SecurityGro } for _, req := range requests { - _, err := cs.AsyncRequest(&req, d.async) + _, err := cs.RequestWithContext(context.TODO(), &req) if err != nil { return nil, err } @@ -383,11 +377,11 @@ func (d *Driver) createDefaultSecurityGroup(group string) (*egoscale.SecurityGro func (d *Driver) createDefaultAffinityGroup(group string) (*egoscale.AffinityGroup, error) { cs := d.client() - resp, err := cs.AsyncRequest(&egoscale.CreateAffinityGroup{ + resp, err := cs.RequestWithContext(context.TODO(), &egoscale.CreateAffinityGroup{ Name: group, Type: defaultAffinityGroupType, Description: "created by docker-machine", - }, d.async) + }) if err != nil { return nil, err @@ -406,45 +400,77 @@ 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() + + resp, err := client.RequestWithContext(context.TODO(), &egoscale.ListZones{ + Name: d.AvailabilityZone, + }) if err != nil { return err } - // Availability zone UUID - zone, ok := topology.Zones[strings.ToLower(d.AvailabilityZone)] - if !ok { + zones := resp.(*egoscale.ListZonesResponse) + if len(zones.Zone) != 1 { return fmt.Errorf("Availability zone %v doesn't exist", d.AvailabilityZone) } + zone := zones.Zone[0].ID log.Debugf("Availability zone %v = %s", d.AvailabilityZone, zone) // Image UUID var tpl string - images, ok := topology.Images[strings.ToLower(d.Image)] - if ok { - smallestDiskSize := d.DiskSize - for s := range images { - if s < smallestDiskSize { - smallestDiskSize = s - } + resp, err = client.RequestWithContext(context.TODO(), &egoscale.ListTemplates{ + TemplateFilter: "featured", + ZoneID: "1", // GVA2 + }) + if err != nil { + return err + } + + image := strings.ToLower(d.Image) + re := regexp.MustCompile(`^Linux (?P.+?) (?P[0-9.]+)\b`) + for _, template := range resp.(*egoscale.ListTemplatesResponse).Template { + // Keep only 10GiB images + if template.Size>>30 != 10 { + continue + } + + fullname := strings.ToLower(template.Name) + if image == fullname { + tpl = template.ID + break } - tpl, ok = images[smallestDiskSize] + submatch := re.FindStringSubmatch(template.Name) + if len(submatch) > 0 { + name := strings.Replace(strings.ToLower(submatch[1]), " ", "-", -1) + version := submatch[2] + shortname := fmt.Sprintf("%s-%s", name, version) + + if image == shortname { + tpl = template.ID + break + } + } } - if !ok { - return fmt.Errorf("Unable to find image %v with size %d", - d.Image, d.DiskSize) + if tpl == "" { + return fmt.Errorf("Unable to find image %v", d.Image) } - log.Debugf("Image %v(%d) = %s", d.Image, d.DiskSize, tpl) + log.Debugf("Image %v(10) = %s", d.Image, tpl) // Profile UUID - profile, ok := topology.Profiles[strings.ToLower(d.InstanceProfile)] - if !ok { + resp, err = client.RequestWithContext(context.TODO(), &egoscale.ListServiceOfferings{ + Name: d.InstanceProfile, + }) + if err != nil { + return err + } + profiles := resp.(*egoscale.ListServiceOfferingsResponse) + if len(profiles.ServiceOffering) != 1 { return fmt.Errorf("Unable to find the %s profile", d.InstanceProfile) } + profile := profiles.ServiceOffering[0].ID log.Debugf("Profile %v = %s", d.InstanceProfile, profile) // Security groups @@ -454,17 +480,21 @@ func (d *Driver) Create() error { continue } - sg, ok := topology.SecurityGroups[group] - if !ok { - log.Infof("Security group %v does not exist, create it", group) + sg := &egoscale.SecurityGroup{Name: group} + if err := client.Get(sg); err != nil { + if _, ok := err.(*egoscale.ErrorResponse); !ok { + return err + } + log.Infof("Security group %v does not exist. Creating it...", group) securityGroup, err := d.createDefaultSecurityGroup(group) if err != nil { return err } - sg = securityGroup.ID + sg.ID = securityGroup.ID } - log.Debugf("Security group %v = %s", group, sg) - sgs = append(sgs, sg) + + log.Debugf("Security group %v = %s", group, sg.ID) + sgs = append(sgs, sg.ID) } // Affinity Groups @@ -473,17 +503,20 @@ func (d *Driver) Create() error { if group == "" { continue } - ag, ok := topology.AffinityGroups[group] - if !ok { + ag := &egoscale.AffinityGroup{Name: group} + if err := client.Get(ag); err != nil { + if _, ok := err.(*egoscale.ErrorResponse); !ok { + return err + } log.Infof("Affinity Group %v does not exist, create it", group) affinityGroup, err := d.createDefaultAffinityGroup(group) if err != nil { return err } - ag = affinityGroup.ID + ag.ID = affinityGroup.ID } - log.Debugf("Affinity group %v = %s", group, ag) - ags = append(ags, ag) + log.Debugf("Affinity group %v = %s", group, ag.ID) + ags = append(ags, ag.ID) } // SSH key pair @@ -491,7 +524,7 @@ func (d *Driver) Create() error { var keyPairName string keyPairName = fmt.Sprintf("docker-machine-%s", d.MachineName) log.Infof("Generate an SSH keypair...") - resp, err := client.Request(&egoscale.CreateSSHKeyPair{ + resp, err := client.RequestWithContext(context.TODO(), &egoscale.CreateSSHKeyPair{ Name: keyPairName, }) if err != nil { @@ -556,7 +589,7 @@ ssh_authorized_keys: AffinityGroupIDs: ags, } log.Infof("Deploy %#v", req) - resp, err := client.AsyncRequest(req, d.async) + resp, err = client.RequestWithContext(context.TODO(), req) if err != nil { return err } @@ -570,13 +603,20 @@ ssh_authorized_keys: d.ID = vm.ID log.Infof("IP Address: %v, SSH User: %v", d.IPAddress, d.GetSSHUsername()) + if vm.PasswordEnabled { + d.Password = vm.Password + } + // Destroy the SSH key from CloudStack if d.KeyPair != "" { if err := drivers.WaitForSSH(d); err != nil { return err } - if err := client.BooleanRequest(&egoscale.DeleteSSHKeyPair{Name: d.KeyPair}); err != nil { + key := &egoscale.SSHKeyPair{ + Name: d.KeyPair, + } + if err := client.DeleteWithContext(context.TODO(), key); err != nil { return err } d.KeyPair = "" @@ -588,9 +628,9 @@ ssh_authorized_keys: // Start starts the existing VM instance. func (d *Driver) Start() error { cs := d.client() - _, err := cs.AsyncRequest(&egoscale.StartVirtualMachine{ + _, err := cs.RequestWithContext(context.TODO(), &egoscale.StartVirtualMachine{ ID: d.ID, - }, d.async) + }) return err } @@ -598,9 +638,9 @@ func (d *Driver) Start() error { // Stop stops the existing VM instance. func (d *Driver) Stop() error { cs := d.client() - _, err := cs.AsyncRequest(&egoscale.StopVirtualMachine{ + _, err := cs.RequestWithContext(context.TODO(), &egoscale.StopVirtualMachine{ ID: d.ID, - }, d.async) + }) return err } @@ -608,9 +648,9 @@ func (d *Driver) Stop() error { // Restart reboots the existing VM instance. func (d *Driver) Restart() error { cs := d.client() - _, err := cs.AsyncRequest(&egoscale.RebootVirtualMachine{ + _, err := cs.RequestWithContext(context.TODO(), &egoscale.RebootVirtualMachine{ ID: d.ID, - }, d.async) + }) return err } @@ -626,14 +666,16 @@ func (d *Driver) Remove() error { // Destroy the SSH key from CloudStack if d.KeyPair != "" { - if err := client.BooleanRequest(&egoscale.DeleteSSHKeyPair{Name: d.KeyPair}); err != nil { + key := &egoscale.SSHKeyPair{Name: d.KeyPair} + if err := client.DeleteWithContext(context.TODO(), key); err != nil { return err } } // Destroy the virtual machine if d.ID != "" { - if _, err := client.AsyncRequest(&egoscale.DestroyVirtualMachine{ID: d.ID}, d.async); err != nil { + vm := &egoscale.VirtualMachine{ID: d.ID} + if err := client.DeleteWithContext(context.TODO(), vm); err != nil { return err } } diff --git a/vendor/github.com/exoscale/egoscale/.gitignore b/vendor/github.com/exoscale/egoscale/.gitignore index e8a2bb777f..c44ccfeb19 100644 --- a/vendor/github.com/exoscale/egoscale/.gitignore +++ b/vendor/github.com/exoscale/egoscale/.gitignore @@ -1,2 +1,3 @@ build/ exo +vendor diff --git a/vendor/github.com/exoscale/egoscale/CHANGELOG.md b/vendor/github.com/exoscale/egoscale/CHANGELOG.md index 5ace7c8f12..1a728b364b 100644 --- a/vendor/github.com/exoscale/egoscale/CHANGELOG.md +++ b/vendor/github.com/exoscale/egoscale/CHANGELOG.md @@ -1,6 +1,42 @@ Changelog ========= +0.9.14 +------ + +- fix: `GetVMPassword` response +- remove: deprecated `GetTopology`, `GetImages`, and al + +0.9.13 +------ + +- feat: IP4 and IP6 flags to DeployVirtualMachine +- feat: add ActivateIP6 +- fix: error message was gobbled on 40x + +0.9.12 +------ + +- feat: add `BooleanRequestWithContext` +- feat: add `client.Get`, `client.GetWithContext` to fetch a resource +- feat: add `cleint.Delete`, `client.DeleteWithContext` to delete a resource +- feat: `SSHKeyPair` is `Gettable` and `Deletable` +- feat: `VirtualMachine` is `Gettable` and `Deletable` +- feat: `AffinityGroup` is `Gettable` and `Deletable` +- feat: `SecurityGroup` is `Gettable` and `Deletable` +- remove: deprecated methods `CreateAffinityGroup`, `DeleteAffinityGroup` +- remove: deprecated methods `CreateKeypair`, `DeleteKeypair`, `RegisterKeypair` +- remove: deprecated method `GetSecurityGroupID` + +0.9.11 +------ + +- feat: CloudStack API name is now public `APIName()` +- feat: enforce the mutual exclusivity of some fields +- feat: add `context.Context` to `RequestWithContext` +- change: `AsyncRequest` and `BooleanAsyncRequest` are gone, use `Request` and `BooleanRequest` instead. +- change: `AsyncInfo` is no more + 0.9.10 ------ @@ -33,7 +69,7 @@ Changelog ----- - fix: update UpdateVirtualMachine userdata -- fix. Network's name/displaytext might be empty +- fix: Network's name/displaytext might be empty 0.9.5 ----- diff --git a/vendor/github.com/exoscale/egoscale/accounts.go b/vendor/github.com/exoscale/egoscale/accounts.go index aeb8a214b5..16767733ec 100644 --- a/vendor/github.com/exoscale/egoscale/accounts.go +++ b/vendor/github.com/exoscale/egoscale/accounts.go @@ -83,7 +83,8 @@ type ListAccounts struct { State string `json:"state,omitempty"` } -func (*ListAccounts) name() string { +// APIName returns the CloudStack API command name +func (*ListAccounts) APIName() string { return "listAccounts" } diff --git a/vendor/github.com/exoscale/egoscale/addresses.go b/vendor/github.com/exoscale/egoscale/addresses.go index 9c012dffd0..de7650e6ee 100644 --- a/vendor/github.com/exoscale/egoscale/addresses.go +++ b/vendor/github.com/exoscale/egoscale/addresses.go @@ -58,7 +58,8 @@ type AssociateIPAddress struct { ZoneID string `json:"zoneid,omitempty"` } -func (*AssociateIPAddress) name() string { +// APIName returns the CloudStack API command name +func (*AssociateIPAddress) APIName() string { return "associateIpAddress" } @@ -78,7 +79,8 @@ type DisassociateIPAddress struct { ID string `json:"id"` } -func (*DisassociateIPAddress) name() string { +// APIName returns the CloudStack API command name +func (*DisassociateIPAddress) APIName() string { return "disassociateIpAddress" } func (*DisassociateIPAddress) asyncResponse() interface{} { @@ -94,7 +96,8 @@ type UpdateIPAddress struct { ForDisplay *bool `json:"fordisplay,omitempty"` } -func (*UpdateIPAddress) name() string { +// APIName returns the CloudStack API command name +func (*UpdateIPAddress) APIName() string { return "updateIpAddress" } func (*UpdateIPAddress) asyncResponse() interface{} { @@ -133,7 +136,8 @@ type ListPublicIPAddresses struct { ZoneID string `json:"zoneid,omitempty"` } -func (*ListPublicIPAddresses) name() string { +// APIName returns the CloudStack API command name +func (*ListPublicIPAddresses) APIName() string { return "listPublicIpAddresses" } diff --git a/vendor/github.com/exoscale/egoscale/affinity_groups.go b/vendor/github.com/exoscale/egoscale/affinity_groups.go index 1f5bf80e79..ba23780199 100644 --- a/vendor/github.com/exoscale/egoscale/affinity_groups.go +++ b/vendor/github.com/exoscale/egoscale/affinity_groups.go @@ -1,7 +1,11 @@ package egoscale import ( + "context" + "fmt" "net/url" + + "github.com/jinzhu/copier" ) // AffinityGroup represents an (anti-)affinity group @@ -12,6 +16,8 @@ type AffinityGroup struct { Domain string `json:"domain,omitempty"` DomainID string `json:"domainid,omitempty"` Name string `json:"name,omitempty"` + Project string `json:"project,omitempty"` + ProjectID string `json:"projectid,omitempty"` Type string `json:"type,omitempty"` VirtualMachineIDs []string `json:"virtualmachineIds,omitempty"` // *I*ds is not a typo } @@ -21,6 +27,56 @@ type AffinityGroupType struct { Type string `json:"type"` } +// Get loads the given Affinity Group +func (ag *AffinityGroup) Get(ctx context.Context, client *Client) error { + if ag.ID == "" && ag.Name == "" { + return fmt.Errorf("An Affinity Group may only be searched using ID or Name") + } + + resp, err := client.RequestWithContext(ctx, &ListAffinityGroups{ + ID: ag.ID, + Name: ag.Name, + }) + + if err != nil { + return err + } + + ags := resp.(*ListAffinityGroupsResponse) + count := len(ags.AffinityGroup) + if count == 0 { + return &ErrorResponse{ + ErrorCode: ParamError, + ErrorText: fmt.Sprintf("AffinityGroup not found id: %s, name: %s", ag.ID, ag.Name), + } + } else if count > 1 { + return fmt.Errorf("More than one Affinity Group was found. Query; id: %s, name: %s", ag.ID, ag.Name) + } + + return copier.Copy(ag, ags.AffinityGroup[0]) +} + +// Delete removes the given Affinity Group +func (ag *AffinityGroup) Delete(ctx context.Context, client *Client) error { + if ag.ID == "" && ag.Name == "" { + return fmt.Errorf("An Affinity Group may only be deleted using ID or Name") + } + + req := &DeleteAffinityGroup{ + Account: ag.Account, + DomainID: ag.DomainID, + ProjectID: ag.ProjectID, + } + + if ag.ID != "" { + req.ID = ag.ID + } else { + req.Name = ag.Name + } + + return client.BooleanRequestWithContext(ctx, req) +} + // CreateAffinityGroup (Async) represents a new (anti-)affinity group // // CloudStack API: http://cloudstack.apache.org/api/apidocs-4.10/apis/createAffinityGroup.html @@ -32,7 +88,8 @@ type CreateAffinityGroup struct { DomainID string `json:"domainid,omitempty"` } -func (*CreateAffinityGroup) name() string { +// APIName returns the CloudStack API command name +func (*CreateAffinityGroup) APIName() string { return "createAffinityGroup" } @@ -54,7 +111,8 @@ type UpdateVMAffinityGroup struct { AffinityGroupNames []string `json:"affinitygroupnames,omitempty"` // mutually exclusive with ids } -func (*UpdateVMAffinityGroup) name() string { +// APIName returns the CloudStack API command name +func (*UpdateVMAffinityGroup) APIName() string { return "updateVMAffinityGroup" } @@ -77,15 +135,15 @@ type UpdateVMAffinityGroupResponse VirtualMachineResponse // // CloudStack API: http://cloudstack.apache.org/api/apidocs-4.10/apis/deleteAffinityGroup.html type DeleteAffinityGroup struct { - ID string `json:"id,omitempty"` - Name string `json:"name,omitempty"` - Type string `json:"type,omitempty"` - Account string `json:"account,omitempty"` - Description string `json:"description,omitempty"` - DomainID string `json:"domainid,omitempty"` + Account string `json:"account,omitempty"` // must be specified with DomainID + DomainID string `json:"domainid,omitempty"` + ID string `json:"id,omitempty"` // mutually exclusive with Name + Name string `json:"name,omitempty"` // mutually exclusive with ID + ProjectID string `json:"projectid,omitempty"` } -func (*DeleteAffinityGroup) name() string { +// APIName returns the CloudStack API command name +func (*DeleteAffinityGroup) APIName() string { return "deleteAffinityGroup" } @@ -110,7 +168,8 @@ type ListAffinityGroups struct { VirtualMachineID string `json:"virtualmachineid,omitempty"` } -func (*ListAffinityGroups) name() string { +// APIName returns the CloudStack API command name +func (*ListAffinityGroups) APIName() string { return "listAffinityGroups" } @@ -127,7 +186,8 @@ type ListAffinityGroupTypes struct { PageSize int `json:"pagesize,omitempty"` } -func (*ListAffinityGroupTypes) name() string { +// APIName returns the CloudStack API command name +func (*ListAffinityGroupTypes) APIName() string { return "listAffinityGroupTypes" } @@ -146,31 +206,3 @@ type ListAffinityGroupTypesResponse struct { Count int `json:"count"` AffinityGroupType []AffinityGroupType `json:"affinitygrouptype"` } - -// Legacy methods - -// CreateAffinityGroup creates a group -// -// Deprecated: Use the API directly -func (exo *Client) CreateAffinityGroup(name string, async AsyncInfo) (*AffinityGroup, error) { - req := &CreateAffinityGroup{ - Name: name, - } - resp, err := exo.AsyncRequest(req, async) - if err != nil { - return nil, err - } - - ag := resp.(*CreateAffinityGroupResponse).AffinityGroup - return &ag, nil -} - -// DeleteAffinityGroup deletes a group -// -// Deprecated: Use the API directly -func (exo *Client) DeleteAffinityGroup(name string, async AsyncInfo) error { - req := &DeleteAffinityGroup{ - Name: name, - } - return exo.BooleanAsyncRequest(req, async) -} diff --git a/vendor/github.com/exoscale/egoscale/apis.go b/vendor/github.com/exoscale/egoscale/apis.go index 18083d1a17..63f7e83741 100644 --- a/vendor/github.com/exoscale/egoscale/apis.go +++ b/vendor/github.com/exoscale/egoscale/apis.go @@ -37,7 +37,8 @@ type ListAPIs struct { Name string `json:"name,omitempty"` } -func (*ListAPIs) name() string { +// APIName returns the CloudStack API command name +func (*ListAPIs) APIName() string { return "listApis" } diff --git a/vendor/github.com/exoscale/egoscale/async_jobs.go b/vendor/github.com/exoscale/egoscale/async_jobs.go index 35625de06c..e7578f7be8 100644 --- a/vendor/github.com/exoscale/egoscale/async_jobs.go +++ b/vendor/github.com/exoscale/egoscale/async_jobs.go @@ -27,7 +27,8 @@ type QueryAsyncJobResult struct { JobID string `json:"jobid"` } -func (*QueryAsyncJobResult) name() string { +// APIName returns the CloudStack API command name +func (*QueryAsyncJobResult) APIName() string { return "queryAsyncJobResult" } @@ -51,7 +52,8 @@ type ListAsyncJobs struct { StartDate string `json:"startdate,omitempty"` } -func (*ListAsyncJobs) name() string { +// APIName returns the CloudStack API command name +func (*ListAsyncJobs) APIName() string { return "listAsyncJobs" } diff --git a/vendor/github.com/exoscale/egoscale/client.go b/vendor/github.com/exoscale/egoscale/client.go new file mode 100644 index 0000000000..3581d17f19 --- /dev/null +++ b/vendor/github.com/exoscale/egoscale/client.go @@ -0,0 +1,106 @@ +package egoscale + +import ( + "context" + "crypto/tls" + "net/http" + "time" +) + +// Gettable represents an Interface that can be "Get" by the client +type Gettable interface { + // Get populates the given resource or throws + Get(context context.Context, client *Client) error +} + +// Deletable represents an Interface that can be "Delete" by the client +type Deletable interface { + // Delete removes the given resources or throws + Delete(context context.Context, client *Client) error +} + +// Client represents the CloudStack API client +type Client struct { + client *http.Client + endpoint string + apiKey string + apiSecret string + // Timeout represents the default timeout for the async requests + Timeout time.Duration + // RetryStrategy represents the waiting strategy for polling the async requests + RetryStrategy RetryStrategyFunc +} + +// Get populates the given resource or fails +func (client *Client) Get(g Gettable) error { + ctx, cancel := context.WithTimeout(context.Background(), client.Timeout) + defer cancel() + + return g.Get(ctx, client) +} + +// GetWithContext populates the given resource or fails +func (client *Client) GetWithContext(ctx context.Context, g Gettable) error { + return g.Get(ctx, client) +} + +// Delete removes the given resource of fails +func (client *Client) Delete(g Deletable) error { + ctx, cancel := context.WithTimeout(context.Background(), client.Timeout) + defer cancel() + + return g.Delete(ctx, client) +} + +// DeleteWithContext removes the given resource of fails +func (client *Client) DeleteWithContext(ctx context.Context, g Deletable) error { + return g.Delete(ctx, client) +} + +// RetryStrategyFunc represents a how much time to wait between two calls to CloudStack +type RetryStrategyFunc func(int64) time.Duration + +// NewClientWithTimeout creates a CloudStack API client +// +// Timeout is set to booth the HTTP client and the client itself. +func NewClientWithTimeout(endpoint, apiKey, apiSecret string, timeout time.Duration) *Client { + client := &http.Client{ + Timeout: timeout, + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: false, + }, + }, + } + + cs := &Client{ + client: client, + endpoint: endpoint, + apiKey: apiKey, + apiSecret: apiSecret, + Timeout: timeout, + RetryStrategy: FibonacciRetryStrategy, + } + + return cs +} + +// NewClient creates a CloudStack API client with default timeout (60) +func NewClient(endpoint, apiKey, apiSecret string) *Client { + timeout := time.Duration(60 * time.Second) + return NewClientWithTimeout(endpoint, apiKey, apiSecret, timeout) +} + +// FibonacciRetryStrategy waits for an increasing amount of time following the Fibonacci sequence +func FibonacciRetryStrategy(iteration int64) time.Duration { + var a, b, i, tmp int64 + a = 0 + b = 1 + for i = 0; i < iteration; i++ { + tmp = a + b + a = b + b = tmp + } + return time.Duration(a) * time.Second +} diff --git a/vendor/github.com/exoscale/egoscale/doc.go b/vendor/github.com/exoscale/egoscale/doc.go index bd8e1396f2..c41de78647 100644 --- a/vendor/github.com/exoscale/egoscale/doc.go +++ b/vendor/github.com/exoscale/egoscale/doc.go @@ -4,7 +4,7 @@ Package egoscale is a mapping for with the CloudStack API (http://cloudstack.apa Requests and Responses -The paradigm used in this library is that CloudStack defines two types of requests synchronous (client.Request) and asynchronous (client.AsyncRequest). And when the expected responses is a success message, you may use the boolean requests variants (client.BooleanRequest, client.BooleanAsyncRequest). To build a request, construct the adequate struct. This library expects a pointer for efficiency reasons only. The response is a struct corresponding to the request itself. E.g. DeployVirtualMachine gives DeployVirtualMachineResponse, as a pointer as well to avoid big copies. +To build a request, construct the adequate struct. This library expects a pointer for efficiency reasons only. The response is a struct corresponding to the request itself. E.g. DeployVirtualMachine gives DeployVirtualMachineResponse, as a pointer as well to avoid big copies. Then everything within the struct is not a pointer. @@ -59,6 +59,19 @@ Security Groups provide a way to isolate traffic to VMs. }) // ... +Security Group also implement the generic Get and Delete interface (Gettable and Deletable). + + sg := &egoscale.SecurityGroup{Name: "Load balancer"} + if err := cs.Get(sg); err != nil { + ... + } + // The SecurityGroup has been loaded with the SecurityGroup informations + + if err := cs.Delete(sg); err != nil { + ... + } + // The SecurityGroup has been deleted + See: http://docs.cloudstack.apache.org/projects/cloudstack-administration/en/stable/networking_and_traffic.html#security-groups Service Offerings @@ -75,19 +88,19 @@ See: http://docs.cloudstack.apache.org/projects/cloudstack-administration/en/sta Virtual Machines -... todo ... +The VM object is the big contenter, it implements the Get and Delete interface. See: http://docs.cloudstack.apache.org/projects/cloudstack-administration/en/stable/virtual_machines.html Templates -... todo ... +A Template corresponds to the kind of machines that can be deployed. See: http://docs.cloudstack.apache.org/projects/cloudstack-administration/en/latest/templates.html Zones -A Zone corresponds to a Data Center. +A Zone corresponds to a Data Center. You may list them. */ package egoscale diff --git a/vendor/github.com/exoscale/egoscale/events.go b/vendor/github.com/exoscale/egoscale/events.go index 018c00b402..7e34a0ecf4 100644 --- a/vendor/github.com/exoscale/egoscale/events.go +++ b/vendor/github.com/exoscale/egoscale/events.go @@ -43,7 +43,8 @@ type ListEvents struct { Type string `json:"type,omitempty"` } -func (*ListEvents) name() string { +// APIName returns the CloudStack API command name +func (*ListEvents) APIName() string { return "listEvents" } @@ -62,7 +63,8 @@ type ListEventsResponse struct { // CloudStack API: http://cloudstack.apache.org/api/apidocs-4.10/apis/listEventTypes.html type ListEventTypes struct{} -func (*ListEventTypes) name() string { +// APIName returns the CloudStack API command name +func (*ListEventTypes) APIName() string { return "listEventTypes" } diff --git a/vendor/github.com/exoscale/egoscale/init.go b/vendor/github.com/exoscale/egoscale/init.go deleted file mode 100644 index b0814cdebd..0000000000 --- a/vendor/github.com/exoscale/egoscale/init.go +++ /dev/null @@ -1,22 +0,0 @@ -package egoscale - -import ( - "crypto/tls" - "net/http" -) - -// NewClient creates a CloudStack API client -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/vendor/github.com/exoscale/egoscale/keypairs.go b/vendor/github.com/exoscale/egoscale/keypairs.go index fc8cea1c41..3557af3667 100644 --- a/vendor/github.com/exoscale/egoscale/keypairs.go +++ b/vendor/github.com/exoscale/egoscale/keypairs.go @@ -1,8 +1,15 @@ package egoscale +import ( + "context" + "fmt" + + "github.com/jinzhu/copier" +) + // SSHKeyPair represents an SSH key pair type SSHKeyPair struct { - Account string `json:"account,omitempty"` + Account string `json:"account,omitempty"` // must be used with a Domain ID DomainID string `json:"domainid,omitempty"` ProjectID string `json:"projectid,omitempty"` Fingerprint string `json:"fingerprint,omitempty"` @@ -10,6 +17,48 @@ type SSHKeyPair struct { PrivateKey string `json:"privatekey,omitempty"` } +// Get populates the given SSHKeyPair +func (ssh *SSHKeyPair) Get(ctx context.Context, client *Client) error { + resp, err := client.RequestWithContext(ctx, &ListSSHKeyPairs{ + Account: ssh.Account, + DomainID: ssh.DomainID, + Name: ssh.Name, + Fingerprint: ssh.Fingerprint, + ProjectID: ssh.ProjectID, + }) + + if err != nil { + return err + } + + sshs := resp.(*ListSSHKeyPairsResponse) + count := len(sshs.SSHKeyPair) + if count == 0 { + return &ErrorResponse{ + ErrorCode: ParamError, + ErrorText: fmt.Sprintf("SSHKeyPair not found"), + } + } else if count > 1 { + return fmt.Errorf("More than one SSHKeyPair was found") + } + + return copier.Copy(ssh, sshs.SSHKeyPair[0]) +} + +// Delete removes the given SSH key, by Name +func (ssh *SSHKeyPair) Delete(ctx context.Context, client *Client) error { + if ssh.Name == "" { + return fmt.Errorf("An SSH Key Pair may only be deleted using Name") + } + + return client.BooleanRequestWithContext(ctx, &DeleteSSHKeyPair{ + Name: ssh.Name, + Account: ssh.Account, + DomainID: ssh.DomainID, + ProjectID: ssh.ProjectID, + }) +} + // CreateSSHKeyPair represents a new keypair to be created // // CloudStack API: http://cloudstack.apache.org/api/apidocs-4.10/apis/createSSHKeyPair.html @@ -20,7 +69,8 @@ type CreateSSHKeyPair struct { ProjectID string `json:"projectid,omitempty"` } -func (*CreateSSHKeyPair) name() string { +// APIName returns the CloudStack API command name +func (*CreateSSHKeyPair) APIName() string { return "createSSHKeyPair" } @@ -43,7 +93,8 @@ type DeleteSSHKeyPair struct { ProjectID string `json:"projectid,omitempty"` } -func (*DeleteSSHKeyPair) name() string { +// APIName returns the CloudStack API command name +func (*DeleteSSHKeyPair) APIName() string { return "deleteSSHKeyPair" } @@ -62,7 +113,8 @@ type RegisterSSHKeyPair struct { ProjectID string `json:"projectid,omitempty"` } -func (*RegisterSSHKeyPair) name() string { +// APIName returns the CloudStack API command name +func (*RegisterSSHKeyPair) APIName() string { return "registerSSHKeyPair" } @@ -91,7 +143,8 @@ type ListSSHKeyPairs struct { ProjectID string `json:"projectid,omitempty"` } -func (*ListSSHKeyPairs) name() string { +// APIName returns the CloudStack API command name +func (*ListSSHKeyPairs) APIName() string { return "listSSHKeyPairs" } @@ -116,7 +169,8 @@ type ResetSSHKeyForVirtualMachine struct { ProjectID string `json:"projectid,omitempty"` } -func (*ResetSSHKeyForVirtualMachine) name() string { +// APIName returns the CloudStack API command name +func (*ResetSSHKeyForVirtualMachine) APIName() string { return "resetSSHKeyForVirtualMachine" } @@ -126,46 +180,3 @@ func (*ResetSSHKeyForVirtualMachine) asyncResponse() interface{} { // ResetSSHKeyForVirtualMachineResponse represents the modified VirtualMachine type ResetSSHKeyForVirtualMachineResponse VirtualMachineResponse - -// CreateKeypair create a new SSH Key Pair -// -// Deprecated: will go away, use the API directly -func (exo *Client) CreateKeypair(name string) (*SSHKeyPair, error) { - req := &CreateSSHKeyPair{ - Name: name, - } - resp, err := exo.Request(req) - if err != nil { - return nil, err - } - - keypair := resp.(*CreateSSHKeyPairResponse).KeyPair - return &keypair, nil -} - -// DeleteKeypair deletes an SSH key pair -// -// Deprecated: will go away, use the API directly -func (exo *Client) DeleteKeypair(name string) error { - req := &DeleteSSHKeyPair{ - Name: name, - } - return exo.BooleanRequest(req) -} - -// RegisterKeypair registers a public key in a keypair -// -// Deprecated: will go away, use the API directly -func (exo *Client) RegisterKeypair(name string, publicKey string) (*SSHKeyPair, error) { - req := &RegisterSSHKeyPair{ - Name: name, - PublicKey: publicKey, - } - resp, err := exo.Request(req) - if err != nil { - return nil, err - } - - keypair := resp.(*RegisterSSHKeyPairResponse).KeyPair - return &keypair, nil -} diff --git a/vendor/github.com/exoscale/egoscale/limits.go b/vendor/github.com/exoscale/egoscale/limits.go index f2e1f67911..cff5aaa3e6 100644 --- a/vendor/github.com/exoscale/egoscale/limits.go +++ b/vendor/github.com/exoscale/egoscale/limits.go @@ -79,7 +79,8 @@ type ListResourceLimits struct { ResourceTypeName ResourceTypeName `json:"resourcetypename,omitempty"` } -func (*ListResourceLimits) name() string { +// APIName returns the CloudStack API command name +func (*ListResourceLimits) APIName() string { return "listResourceLimits" } diff --git a/vendor/github.com/exoscale/egoscale/network_offerings.go b/vendor/github.com/exoscale/egoscale/network_offerings.go index 1fd0a02654..dcac81f280 100644 --- a/vendor/github.com/exoscale/egoscale/network_offerings.go +++ b/vendor/github.com/exoscale/egoscale/network_offerings.go @@ -53,7 +53,8 @@ type ListNetworkOfferings struct { ZoneID string `json:"zoneid,omitempty"` } -func (*ListNetworkOfferings) name() string { +// APIName returns the CloudStack API command name +func (*ListNetworkOfferings) APIName() string { return "listNetworkOfferings" } diff --git a/vendor/github.com/exoscale/egoscale/networks.go b/vendor/github.com/exoscale/egoscale/networks.go index 82c243a004..debc577230 100644 --- a/vendor/github.com/exoscale/egoscale/networks.go +++ b/vendor/github.com/exoscale/egoscale/networks.go @@ -120,7 +120,8 @@ type CreateNetwork struct { VpcID string `json:"vpcid,omitempty"` } -func (*CreateNetwork) name() string { +// APIName returns the CloudStack API command name +func (*CreateNetwork) APIName() string { return "createNetwork" } @@ -159,7 +160,8 @@ type UpdateNetwork struct { UpdateInSequence *bool `json:"updateinsequence,omitempty"` } -func (*UpdateNetwork) name() string { +// APIName returns the CloudStack API command name +func (*UpdateNetwork) APIName() string { return "updateNetwork" } @@ -178,7 +180,8 @@ type RestartNetwork struct { Cleanup *bool `json:"cleanup,omitempty"` } -func (*RestartNetwork) name() string { +// APIName returns the CloudStack API command name +func (*RestartNetwork) APIName() string { return "restartNetwork" } @@ -197,7 +200,8 @@ type DeleteNetwork struct { Forced *bool `json:"forced,omitempty"` } -func (*DeleteNetwork) name() string { +// APIName returns the CloudStack API command name +func (*DeleteNetwork) APIName() string { return "deleteNetwork" } @@ -234,7 +238,8 @@ type ListNetworks struct { ZoneID string `json:"zoneid,omitempty"` } -func (*ListNetworks) name() string { +// APIName returns the CloudStack API command name +func (*ListNetworks) APIName() string { return "listNetworks" } diff --git a/vendor/github.com/exoscale/egoscale/nics.go b/vendor/github.com/exoscale/egoscale/nics.go index 22a9ce6ef9..7819952bbd 100644 --- a/vendor/github.com/exoscale/egoscale/nics.go +++ b/vendor/github.com/exoscale/egoscale/nics.go @@ -36,6 +36,8 @@ type NicSecondaryIP struct { } // ListNics represents the NIC search +// +// CloudStack API: http://cloudstack.apache.org/api/apidocs-4.10/apis/listNics.html type ListNics struct { VirtualMachineID string `json:"virtualmachineid"` ForDisplay bool `json:"fordisplay,omitempty"` @@ -46,7 +48,8 @@ type ListNics struct { PageSize int `json:"pagesize,omitempty"` } -func (*ListNics) name() string { +// APIName returns the CloudStack API command name +func (*ListNics) APIName() string { return "listNics" } @@ -60,13 +63,16 @@ type ListNicsResponse struct { Nic []Nic `json:"nic"` } -// AddIPToNic represents the assignation of a secondary IP +// AddIPToNic (Async) represents the assignation of a secondary IP +// +// CloudStack API: http://cloudstack.apache.org/api/apidocs-4.10/apis/addIpToNic.html type AddIPToNic struct { NicID string `json:"nicid"` IPAddress net.IP `json:"ipaddress"` } -func (*AddIPToNic) name() string { +// APIName returns the CloudStack API command name: addIpToNic +func (*AddIPToNic) APIName() string { return "addIpToNic" } func (*AddIPToNic) asyncResponse() interface{} { @@ -78,12 +84,15 @@ type AddIPToNicResponse struct { NicSecondaryIP NicSecondaryIP `json:"nicsecondaryip"` } -// RemoveIPFromNic represents a deletion request +// RemoveIPFromNic (Async) represents a deletion request +// +// CloudStack API: http://cloudstack.apache.org/api/apidocs-4.10/apis/removeIpFromNic.html type RemoveIPFromNic struct { ID string `json:"id"` } -func (*RemoveIPFromNic) name() string { +// APIName returns the CloudStack API command name: removeIpFromNic +func (*RemoveIPFromNic) APIName() string { return "removeIpFromNic" } @@ -91,6 +100,27 @@ func (*RemoveIPFromNic) asyncResponse() interface{} { return new(booleanAsyncResponse) } +// ActivateIP6 (Async) activates the IP6 on the given NIC +// +// Exoscale specific API: https://community.exoscale.ch/api/compute/#activateip6_GET +type ActivateIP6 struct { + NicID string `json:"nicid"` +} + +// APIName returns the CloudStack API command name: activateIp6 +func (*ActivateIP6) APIName() string { + return "activateIp6" +} + +func (*ActivateIP6) asyncResponse() interface{} { + return new(ActivateIP6Response) +} + +// ActivateIP6Response represents the modified NIC +type ActivateIP6Response struct { + Nic Nic `json:"nic"` +} + // ListNics lists the NIC of a VM // // Deprecated: use the API directly @@ -106,7 +136,7 @@ func (exo *Client) ListNics(req *ListNics) ([]Nic, error) { // AddIPToNic adds an IP to a NIC // // Deprecated: use the API directly -func (exo *Client) AddIPToNic(nicID string, ipAddress string, async AsyncInfo) (*NicSecondaryIP, error) { +func (exo *Client) AddIPToNic(nicID string, ipAddress string) (*NicSecondaryIP, error) { ip := net.ParseIP(ipAddress) if ip == nil { return nil, fmt.Errorf("%s is not a valid IP address", ipAddress) @@ -115,7 +145,7 @@ func (exo *Client) AddIPToNic(nicID string, ipAddress string, async AsyncInfo) ( NicID: nicID, IPAddress: ip, } - resp, err := exo.AsyncRequest(req, async) + resp, err := exo.Request(req) if err != nil { return nil, err } @@ -127,9 +157,9 @@ func (exo *Client) AddIPToNic(nicID string, ipAddress string, async AsyncInfo) ( // RemoveIPFromNic removes an IP from a NIC // // Deprecated: use the API directly -func (exo *Client) RemoveIPFromNic(secondaryNicID string, async AsyncInfo) error { +func (exo *Client) RemoveIPFromNic(secondaryNicID string) error { req := &RemoveIPFromNic{ ID: secondaryNicID, } - return exo.BooleanAsyncRequest(req, async) + return exo.BooleanRequest(req) } diff --git a/vendor/github.com/exoscale/egoscale/request.go b/vendor/github.com/exoscale/egoscale/request.go index 1d165fd291..fda35dca9c 100644 --- a/vendor/github.com/exoscale/egoscale/request.go +++ b/vendor/github.com/exoscale/egoscale/request.go @@ -9,34 +9,35 @@ import ( "encoding/json" "fmt" "io/ioutil" - "log" - "net" "net/http" "net/url" - "reflect" "sort" "strconv" "strings" "time" ) -// Command represent a CloudStack request +// Command represents a CloudStack request type Command interface { // CloudStack API command name - name() string + APIName() string +} + +// SyncCommand represents a CloudStack synchronous request +type syncCommand interface { + Command // Response interface to Unmarshal the JSON into response() interface{} } -// AsyncCommand represents a async CloudStack request -type AsyncCommand interface { - // CloudStack API command name - name() string +// asyncCommand represents a async CloudStack request +type asyncCommand interface { + Command // Response interface to Unmarshal the JSON into asyncResponse() interface{} } -// Command represents an action to be done on the params before sending them +// onBeforeHook represents an action to be done on the params before sending them // // This little took helps with issue of relying on JSON serialization logic only. // `omitempty` may make sense in some cases but not all the time. @@ -44,14 +45,6 @@ type onBeforeHook interface { onBeforeSend(params *url.Values) error } -// AsyncInfo represents the details for any async call -// -// It retries at most Retries time and waits for Delay between each retry -type AsyncInfo struct { - Retries int - Delay int -} - const ( // Pending represents a job in progress Pending JobStatusType = iota @@ -162,48 +155,6 @@ func (e *booleanSyncResponse) Error() error { return fmt.Errorf("API error: %s", e.DisplayText) } -type asyncJob struct { - command AsyncCommand - delay int - retries int - responseChan chan<- *AsyncJobResult - errorChan chan<- error - ctx context.Context -} - -func csQuotePlus(s string) string { - s = strings.Replace(s, "+", "%20", -1) - s = strings.Replace(s, "%5B", "[", -1) - s = strings.Replace(s, "%5D", "]", -1) - return s -} - -func csEncode(s string) string { - return csQuotePlus(url.QueryEscape(s)) -} - -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, nil -} - -func rawValues(b json.RawMessage) (json.RawMessage, error) { - var i []json.RawMessage - - if err := json.Unmarshal(b, &i); err != nil { - return nil, nil - } - - return i[0], nil -} - func (exo *Client) parseResponse(resp *http.Response) (json.RawMessage, error) { b, err := ioutil.ReadAll(resp.Body) if err != nil { @@ -220,163 +171,153 @@ func (exo *Client) parseResponse(resp *http.Response) (json.RawMessage, error) { } if resp.StatusCode >= 400 { - var e ErrorResponse - if err := json.Unmarshal(b, &e); err != nil { - return nil, err + errorResponse := new(ErrorResponse) + if json.Unmarshal(b, errorResponse) == nil { + return nil, errorResponse } - return b, &e + return nil, fmt.Errorf("%d %s", resp.StatusCode, b) } + return b, nil } -func (exo *Client) processAsyncJob(ctx context.Context, job *asyncJob) { - defer close(job.responseChan) - defer close(job.errorChan) - - body, err := exo.request(job.command.name(), job.command) +// asyncRequest perform an asynchronous job with a context +func (exo *Client) asyncRequest(ctx context.Context, request asyncCommand) (interface{}, error) { + body, err := exo.request(ctx, request.APIName(), request) if err != nil { - job.errorChan <- err - return + return nil, err } jobResult := new(AsyncJobResult) if err := json.Unmarshal(body, jobResult); err != nil { r := new(ErrorResponse) if e := json.Unmarshal(body, r); e != nil { - job.errorChan <- r - return + return nil, r } - job.errorChan <- err - return + return nil, err } // Successful response if jobResult.JobID == "" || jobResult.JobStatus != Pending { - job.responseChan <- jobResult - return + response := request.asyncResponse() + if err := json.Unmarshal(*(jobResult.JobResult), response); err != nil { + return nil, err + } + return response, nil } - for job.retries > 0 { - select { - case <-ctx.Done(): - job.errorChan <- ctx.Err() - return - default: - job.retries-- - - end, _ := ctx.Deadline() + for iteration := 0; ; iteration++ { + time.Sleep(exo.RetryStrategy(int64(iteration))) - when := end.Add(time.Duration(job.delay*-job.retries) * time.Second) - time.Sleep(when.Sub(time.Now())) + req := &QueryAsyncJobResult{JobID: jobResult.JobID} + resp, err := exo.Request(req) + if err != nil { + return nil, err + } - req := &QueryAsyncJobResult{JobID: jobResult.JobID} - resp, err := exo.Request(req) - if err != nil { - job.errorChan <- err - return + result := resp.(*QueryAsyncJobResultResponse) + if result.JobStatus == Success { + response := request.asyncResponse() + if err := json.Unmarshal(*(result.JobResult), response); err != nil { + return nil, err } + return response, nil - result := resp.(*QueryAsyncJobResultResponse) - if result.JobStatus == Success { - job.responseChan <- (*AsyncJobResult)(result) - return - } else if result.JobStatus == Failure { - r := new(ErrorResponse) - e := json.Unmarshal(*result.JobResult, r) - if e != nil { - job.errorChan <- e - return - } - job.errorChan <- r - return + } else if result.JobStatus == Failure { + r := new(ErrorResponse) + if e := json.Unmarshal(*result.JobResult, r); e != nil { + return nil, e } + return nil, r } } - - job.errorChan <- fmt.Errorf("Maximum number of retries reached") } -// AsyncRequest performs an asynchronous request and polls it for retries * day [s] -func (exo *Client) AsyncRequest(req AsyncCommand, async AsyncInfo) (interface{}, error) { - totalTime := time.Duration(async.Delay*async.Retries) * time.Second - end := time.Now().Add(totalTime) - - ctx, cancel := context.WithDeadline(context.Background(), end) - defer cancel() - - responseChan := make(chan *AsyncJobResult, 1) - errorChan := make(chan error, 1) - - go exo.processAsyncJob(ctx, &asyncJob{ - command: req, - delay: async.Delay, - retries: async.Retries, - responseChan: responseChan, - errorChan: errorChan, - ctx: ctx, - }) - - select { - case result := <-responseChan: - cancel() - resp := req.asyncResponse() - if err := json.Unmarshal(*(result.JobResult), resp); err != nil { - return nil, err - } - return resp, nil - - case err := <-errorChan: - cancel() +// syncRequest performs a sync request with a context +func (exo *Client) syncRequest(ctx context.Context, request syncCommand) (interface{}, error) { + body, err := exo.request(ctx, request.APIName(), request) + if err != nil { return nil, err + } - case <-ctx.Done(): - cancel() - err := <-errorChan + response := request.response() + if err := json.Unmarshal(body, response); err != nil { + errResponse := new(ErrorResponse) + if json.Unmarshal(body, errResponse) == nil { + return errResponse, nil + } return nil, err } + + return response, nil } -// BooleanRequest performs a sync request on a boolean call +// BooleanRequest performs the given boolean command func (exo *Client) BooleanRequest(req Command) error { resp, err := exo.Request(req) if err != nil { return err } - return resp.(*booleanSyncResponse).Error() + // CloudStack returns a different type between sync and async success responses + if b, ok := resp.(*booleanSyncResponse); ok { + return b.Error() + } + + if b, ok := resp.(*booleanAsyncResponse); ok { + return b.Error() + } + + panic(fmt.Errorf("The command %s is not a proper boolean response. %#v", req.APIName(), resp)) } -// BooleanAsyncRequest performs a sync request on a boolean call -func (exo *Client) BooleanAsyncRequest(req AsyncCommand, async AsyncInfo) error { - resp, err := exo.AsyncRequest(req, async) +// BooleanRequestWithContext performs the given boolean command +func (exo *Client) BooleanRequestWithContext(ctx context.Context, req Command) error { + resp, err := exo.RequestWithContext(ctx, req) if err != nil { return err } - return resp.(*booleanAsyncResponse).Error() + // CloudStack returns a different type between sync and async success responses + if b, ok := resp.(*booleanSyncResponse); ok { + return b.Error() + } + if b, ok := resp.(*booleanAsyncResponse); ok { + return b.Error() + } + + panic(fmt.Errorf("The command %s is not a proper boolean response. %#v", req.APIName(), resp)) } -// Request performs a sync request on a generic command -func (exo *Client) Request(req Command) (interface{}, error) { - body, err := exo.request(req.name(), req) - if err != nil { - return nil, err - } +// Request performs the given command +func (exo *Client) Request(request Command) (interface{}, error) { + ctx, cancel := context.WithTimeout(context.Background(), exo.Timeout) + defer cancel() - resp := req.response() - if err := json.Unmarshal(body, resp); err != nil { - r := new(ErrorResponse) - if e := json.Unmarshal(body, r); e != nil { - return nil, r - } - return nil, err + switch request.(type) { + case syncCommand: + return exo.syncRequest(ctx, request.(syncCommand)) + case asyncCommand: + return exo.asyncRequest(ctx, request.(asyncCommand)) + default: + panic(fmt.Errorf("The command %s is not a proper Sync or Async command", request.APIName())) } +} - return resp, nil +// RequestWithContext preforms a request with a context +func (exo *Client) RequestWithContext(ctx context.Context, request Command) (interface{}, error) { + switch request.(type) { + case syncCommand: + return exo.syncRequest(ctx, request.(syncCommand)) + case asyncCommand: + return exo.asyncRequest(ctx, request.(asyncCommand)) + default: + panic(fmt.Errorf("The command %s is not a proper Sync or Async command", request.APIName())) + } } // request makes a Request while being close to the metal -func (exo *Client) request(command string, req interface{}) (json.RawMessage, error) { +func (exo *Client) request(ctx context.Context, command string, req interface{}) (json.RawMessage, error) { params := url.Values{} err := prepareValues("", ¶ms, req) if err != nil { @@ -416,8 +357,18 @@ func (exo *Client) request(command string, req interface{}) (json.RawMessage, er mac.Write([]byte(strings.ToLower(query))) signature := csEncode(base64.StdEncoding.EncodeToString(mac.Sum(nil))) - reader := strings.NewReader(fmt.Sprintf("%s&signature=%s", csQuotePlus(query), signature)) - resp, err := exo.client.Post(exo.endpoint, "application/x-www-form-urlencoded", reader) + payload := fmt.Sprintf("%s&signature=%s", csQuotePlus(query), signature) + + request, err := http.NewRequest("POST", exo.endpoint, strings.NewReader(payload)) + if err != nil { + return nil, err + } + + request.Header.Add("Content-Type", "application/x-www-form-urlencoded") + request.Header.Add("Content-Length", strconv.Itoa(len(payload))) + request = request.WithContext(ctx) + + resp, err := exo.client.Do(request) if err != nil { return nil, err } @@ -430,210 +381,3 @@ func (exo *Client) request(command string, req interface{}) (json.RawMessage, er return body, nil } - -// prepareValues uses a command to build a POST request -// -// command is not a Command so it's easier to Test -func prepareValues(prefix string, params *url.Values, command interface{}) error { - value := reflect.ValueOf(command) - typeof := reflect.TypeOf(command) - // Going up the pointer chain to find the underlying struct - for typeof.Kind() == reflect.Ptr { - typeof = typeof.Elem() - value = value.Elem() - } - - for i := 0; i < typeof.NumField(); i++ { - field := typeof.Field(i) - val := value.Field(i) - tag := field.Tag - if json, ok := tag.Lookup("json"); ok { - n, required := extractJSONTag(field.Name, json) - name := prefix + n - - switch val.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - v := val.Int() - if v == 0 { - if required { - return fmt.Errorf("%s.%s (%v) is required, got 0", typeof.Name(), field.Name, val.Kind()) - } - } else { - (*params).Set(name, strconv.FormatInt(v, 10)) - } - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - v := val.Uint() - if v == 0 { - if required { - return fmt.Errorf("%s.%s (%v) is required, got 0", typeof.Name(), field.Name, val.Kind()) - } - } else { - (*params).Set(name, strconv.FormatUint(v, 10)) - } - case reflect.Float32, reflect.Float64: - v := val.Float() - if v == 0 { - if required { - return fmt.Errorf("%s.%s (%v) is required, got 0", typeof.Name(), field.Name, val.Kind()) - } - } else { - (*params).Set(name, strconv.FormatFloat(v, 'f', -1, 64)) - } - case reflect.String: - v := val.String() - if v == "" { - if required { - return fmt.Errorf("%s.%s (%v) is required, got \"\"", typeof.Name(), field.Name, val.Kind()) - } - } else { - (*params).Set(name, v) - } - case reflect.Bool: - v := val.Bool() - if v == false { - if required { - params.Set(name, "false") - } - } else { - (*params).Set(name, "true") - } - case reflect.Ptr: - if val.IsNil() { - if required { - return fmt.Errorf("%s.%s (%v) is required, got empty ptr", typeof.Name(), field.Name, val.Kind()) - } - } else { - switch field.Type.Elem().Kind() { - case reflect.Bool: - params.Set(name, strconv.FormatBool(val.Elem().Bool())) - default: - log.Printf("[SKIP] %s.%s (%v) not supported", typeof.Name(), field.Name, field.Type.Elem().Kind()) - } - } - case reflect.Slice: - switch field.Type.Elem().Kind() { - case reflect.Uint8: - switch field.Type { - case reflect.TypeOf(net.IPv4zero): - ip := (net.IP)(val.Bytes()) - if ip == nil || ip.Equal(net.IPv4zero) { - if required { - return fmt.Errorf("%s.%s (%v) is required, got zero IPv4 address", typeof.Name(), field.Name, val.Kind()) - } - } else { - (*params).Set(name, ip.String()) - } - default: - if val.Len() == 0 { - if required { - return fmt.Errorf("%s.%s (%v) is required, got empty slice", typeof.Name(), field.Name, val.Kind()) - } - } else { - v := val.Bytes() - (*params).Set(name, base64.StdEncoding.EncodeToString(v)) - } - } - case reflect.String: - { - if val.Len() == 0 { - if required { - return fmt.Errorf("%s.%s (%v) is required, got empty slice", typeof.Name(), field.Name, val.Kind()) - } - } else { - elems := make([]string, 0, val.Len()) - for i := 0; i < val.Len(); i++ { - // XXX what if the value contains a comma? Double encode? - s := val.Index(i).String() - elems = append(elems, s) - } - (*params).Set(name, strings.Join(elems, ",")) - } - } - default: - if val.Len() == 0 { - if required { - return fmt.Errorf("%s.%s (%v) is required, got empty slice", typeof.Name(), field.Name, val.Kind()) - } - } else { - err := prepareList(name, params, val.Interface()) - if err != nil { - return err - } - } - } - case reflect.Map: - if val.Len() == 0 { - if required { - return fmt.Errorf("%s.%s (%v) is required, got empty map", typeof.Name(), field.Name, val.Kind()) - } - } else { - err := prepareMap(name, params, val.Interface()) - if err != nil { - return err - } - } - default: - if required { - return fmt.Errorf("Unsupported type %s.%s (%v)", typeof.Name(), field.Name, val.Kind()) - } - } - } else { - log.Printf("[SKIP] %s.%s no json label found", typeof.Name(), field.Name) - } - } - - return nil -} - -func prepareList(prefix string, params *url.Values, slice interface{}) error { - value := reflect.ValueOf(slice) - - for i := 0; i < value.Len(); i++ { - prepareValues(fmt.Sprintf("%s[%d].", prefix, i), params, value.Index(i).Interface()) - } - - return nil -} - -func prepareMap(prefix string, params *url.Values, m interface{}) error { - value := reflect.ValueOf(m) - - for i, key := range value.MapKeys() { - var keyName string - var keyValue string - - switch key.Kind() { - case reflect.String: - keyName = key.String() - default: - return fmt.Errorf("Only map[string]string are supported (XXX)") - } - - val := value.MapIndex(key) - switch val.Kind() { - case reflect.String: - keyValue = val.String() - default: - return fmt.Errorf("Only map[string]string are supported (XXX)") - } - params.Set(fmt.Sprintf("%s[%d].%s", prefix, i, keyName), keyValue) - } - return nil -} - -// extractJSONTag returns the variable name or defaultName as well as if the field is required (!omitempty) -func extractJSONTag(defaultName, jsonTag string) (string, bool) { - tags := strings.Split(jsonTag, ",") - name := tags[0] - required := true - for _, tag := range tags { - if tag == "omitempty" { - required = false - } - } - - if name == "" || name == "omitempty" { - name = defaultName - } - return name, required -} diff --git a/vendor/github.com/exoscale/egoscale/security_groups.go b/vendor/github.com/exoscale/egoscale/security_groups.go index c970515287..683dc56dff 100644 --- a/vendor/github.com/exoscale/egoscale/security_groups.go +++ b/vendor/github.com/exoscale/egoscale/security_groups.go @@ -1,8 +1,12 @@ package egoscale import ( + "context" + "fmt" "net/url" "strconv" + + "github.com/jinzhu/copier" ) // SecurityGroup represent a firewalling set of rules @@ -29,6 +33,56 @@ func (*SecurityGroup) ResourceType() string { return "SecurityGroup" } +// Get loads the given Security Group +func (sg *SecurityGroup) Get(ctx context.Context, client *Client) error { + if sg.ID == "" && sg.Name == "" { + return fmt.Errorf("A SecurityGroup may only be searched using ID or Name") + } + resp, err := client.RequestWithContext(ctx, &ListSecurityGroups{ + ID: sg.ID, + SecurityGroupName: sg.Name, + }) + + if err != nil { + return err + } + + sgs := resp.(*ListSecurityGroupsResponse) + count := len(sgs.SecurityGroup) + if count == 0 { + err := &ErrorResponse{ + ErrorCode: ParamError, + ErrorText: fmt.Sprintf("SecurityGroup not found id: %s, name: %s", sg.ID, sg.Name), + } + return err + } else if count > 1 { + return fmt.Errorf("More than one SecurityGroup was found. Query: id: %s, name: %s", sg.ID, sg.Name) + } + + return copier.Copy(sg, sgs.SecurityGroup[0]) +} + +// Delete deletes the given Security Group +func (sg *SecurityGroup) Delete(ctx context.Context, client *Client) error { + if sg.ID == "" && sg.Name == "" { + return fmt.Errorf("A SecurityGroup may only be deleted using ID or Name") + } + + req := &DeleteSecurityGroup{ + Account: sg.Account, + DomainID: sg.DomainID, + ProjectID: sg.ProjectID, + } + + if sg.ID != "" { + req.ID = sg.ID + } else { + req.Name = sg.Name + } + + return client.BooleanRequestWithContext(ctx, req) +} + // IngressRule represents the ingress rule type IngressRule struct { RuleID string `json:"ruleid"` @@ -70,7 +124,8 @@ type CreateSecurityGroup struct { Description string `json:"description,omitempty"` } -func (*CreateSecurityGroup) name() string { +// APIName returns the CloudStack API command name +func (*CreateSecurityGroup) APIName() string { return "createSecurityGroup" } @@ -85,14 +140,15 @@ type CreateSecurityGroupResponse SecurityGroupResponse // // CloudStack API: https://cloudstack.apache.org/api/apidocs-4.10/apis/deleteSecurityGroup.html type DeleteSecurityGroup struct { - Account string `json:"account,omitempty"` - DomainID string `json:"domainid,omitempty"` - ID string `json:"id,omitempty"` // Mutually exclusive with name - Name string `json:"name,omitempty"` // Mutually exclusive with id + Account string `json:"account,omitempty"` // Must be specified with Domain ID + DomainID string `json:"domainid,omitempty"` // Must be specified with Account + ID string `json:"id,omitempty"` // Mutually exclusive with name + Name string `json:"name,omitempty"` // Mutually exclusive with id ProjectID string `json:"project,omitempty"` } -func (*DeleteSecurityGroup) name() string { +// APIName returns the CloudStack API command name +func (*DeleteSecurityGroup) APIName() string { return "deleteSecurityGroup" } @@ -119,7 +175,8 @@ type AuthorizeSecurityGroupIngress struct { UserSecurityGroupList []UserSecurityGroup `json:"usersecuritygrouplist,omitempty"` } -func (*AuthorizeSecurityGroupIngress) name() string { +// APIName returns the CloudStack API command name +func (*AuthorizeSecurityGroupIngress) APIName() string { return "authorizeSecurityGroupIngress" } @@ -145,7 +202,8 @@ type AuthorizeSecurityGroupIngressResponse SecurityGroupResponse // CloudStack API: https://cloudstack.apache.org/api/apidocs-4.10/apis/authorizeSecurityGroupEgress.html type AuthorizeSecurityGroupEgress AuthorizeSecurityGroupIngress -func (*AuthorizeSecurityGroupEgress) name() string { +// APIName returns the CloudStack API command name +func (*AuthorizeSecurityGroupEgress) APIName() string { return "authorizeSecurityGroupEgress" } @@ -168,7 +226,8 @@ type RevokeSecurityGroupIngress struct { ID string `json:"id"` } -func (*RevokeSecurityGroupIngress) name() string { +// APIName returns the CloudStack API command name +func (*RevokeSecurityGroupIngress) APIName() string { return "revokeSecurityGroupIngress" } @@ -183,7 +242,8 @@ type RevokeSecurityGroupEgress struct { ID string `json:"id"` } -func (*RevokeSecurityGroupEgress) name() string { +// APIName returns the CloudStack API command name +func (*RevokeSecurityGroupEgress) APIName() string { return "revokeSecurityGroupEgress" } @@ -210,7 +270,8 @@ type ListSecurityGroups struct { VirtualMachineID string `json:"virtualmachineid,omitempty"` } -func (*ListSecurityGroups) name() string { +// APIName returns the CloudStack API command name +func (*ListSecurityGroups) APIName() string { return "listSecurityGroups" } @@ -227,8 +288,8 @@ type ListSecurityGroupsResponse struct { // CreateIngressRule creates a set of ingress rules // // Deprecated: use the API directly -func (exo *Client) CreateIngressRule(req *AuthorizeSecurityGroupIngress, async AsyncInfo) ([]IngressRule, error) { - resp, err := exo.AsyncRequest(req, async) +func (exo *Client) CreateIngressRule(req *AuthorizeSecurityGroupIngress) ([]IngressRule, error) { + resp, err := exo.Request(req) if err != nil { return nil, err } @@ -238,8 +299,8 @@ func (exo *Client) CreateIngressRule(req *AuthorizeSecurityGroupIngress, async A // CreateEgressRule creates a set of egress rules // // Deprecated: use the API directly -func (exo *Client) CreateEgressRule(req *AuthorizeSecurityGroupEgress, async AsyncInfo) ([]EgressRule, error) { - resp, err := exo.AsyncRequest(req, async) +func (exo *Client) CreateEgressRule(req *AuthorizeSecurityGroupEgress) ([]EgressRule, error) { + resp, err := exo.Request(req) if err != nil { return nil, err } @@ -250,7 +311,7 @@ func (exo *Client) CreateEgressRule(req *AuthorizeSecurityGroupEgress, async Asy // Warning: it doesn't rollback in case of a failure! // // Deprecated: use the API directly -func (exo *Client) CreateSecurityGroupWithRules(name string, ingress []AuthorizeSecurityGroupIngress, egress []AuthorizeSecurityGroupEgress, async AsyncInfo) (*SecurityGroup, error) { +func (exo *Client) CreateSecurityGroupWithRules(name string, ingress []AuthorizeSecurityGroupIngress, egress []AuthorizeSecurityGroupEgress) (*SecurityGroup, error) { req := &CreateSecurityGroup{ Name: name, } @@ -260,7 +321,7 @@ func (exo *Client) CreateSecurityGroupWithRules(name string, ingress []Authorize } sg := resp.(*SecurityGroupResponse).SecurityGroup - reqs := make([]AsyncCommand, 0, len(ingress)+len(egress)) + reqs := make([]asyncCommand, 0, len(ingress)+len(egress)) // Egress rules for _, ereq := range egress { ereq.SecurityGroupID = sg.ID @@ -274,7 +335,7 @@ func (exo *Client) CreateSecurityGroupWithRules(name string, ingress []Authorize } for _, r := range reqs { - _, err := exo.AsyncRequest(r, async) + _, err := exo.Request(r) if err != nil { return nil, err } @@ -290,13 +351,3 @@ func (exo *Client) CreateSecurityGroupWithRules(name string, ingress []Authorize sg = r.(*ListSecurityGroupsResponse).SecurityGroup[0] return &sg, nil } - -// DeleteSecurityGroup deletes a security group -// -// Deprecated: use the API directly -func (exo *Client) DeleteSecurityGroup(name string) error { - req := &DeleteSecurityGroup{ - Name: name, - } - return exo.BooleanRequest(req) -} diff --git a/vendor/github.com/exoscale/egoscale/serialization.go b/vendor/github.com/exoscale/egoscale/serialization.go new file mode 100644 index 0000000000..b0780c8c52 --- /dev/null +++ b/vendor/github.com/exoscale/egoscale/serialization.go @@ -0,0 +1,241 @@ +package egoscale + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "log" + "net" + "net/url" + "reflect" + "strconv" + "strings" +) + +func csQuotePlus(s string) string { + s = strings.Replace(s, "+", "%20", -1) + s = strings.Replace(s, "%5B", "[", -1) + s = strings.Replace(s, "%5D", "]", -1) + return s +} + +func csEncode(s string) string { + return csQuotePlus(url.QueryEscape(s)) +} + +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, nil +} + +func rawValues(b json.RawMessage) (json.RawMessage, error) { + var i []json.RawMessage + + if err := json.Unmarshal(b, &i); err != nil { + return nil, nil + } + + return i[0], nil +} + +// prepareValues uses a command to build a POST request +// +// command is not a Command so it's easier to Test +func prepareValues(prefix string, params *url.Values, command interface{}) error { + value := reflect.ValueOf(command) + typeof := reflect.TypeOf(command) + + // Going up the pointer chain to find the underlying struct + for typeof.Kind() == reflect.Ptr { + typeof = typeof.Elem() + value = value.Elem() + } + + for i := 0; i < typeof.NumField(); i++ { + field := typeof.Field(i) + val := value.Field(i) + tag := field.Tag + if json, ok := tag.Lookup("json"); ok { + n, required := extractJSONTag(field.Name, json) + name := prefix + n + + switch val.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + v := val.Int() + if v == 0 { + if required { + return fmt.Errorf("%s.%s (%v) is required, got 0", typeof.Name(), field.Name, val.Kind()) + } + } else { + (*params).Set(name, strconv.FormatInt(v, 10)) + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + v := val.Uint() + if v == 0 { + if required { + return fmt.Errorf("%s.%s (%v) is required, got 0", typeof.Name(), field.Name, val.Kind()) + } + } else { + (*params).Set(name, strconv.FormatUint(v, 10)) + } + case reflect.Float32, reflect.Float64: + v := val.Float() + if v == 0 { + if required { + return fmt.Errorf("%s.%s (%v) is required, got 0", typeof.Name(), field.Name, val.Kind()) + } + } else { + (*params).Set(name, strconv.FormatFloat(v, 'f', -1, 64)) + } + case reflect.String: + v := val.String() + if v == "" { + if required { + return fmt.Errorf("%s.%s (%v) is required, got \"\"", typeof.Name(), field.Name, val.Kind()) + } + } else { + (*params).Set(name, v) + } + case reflect.Bool: + v := val.Bool() + if v == false { + if required { + params.Set(name, "false") + } + } else { + (*params).Set(name, "true") + } + case reflect.Slice: + switch field.Type.Elem().Kind() { + case reflect.Uint8: + switch field.Type { + case reflect.TypeOf(net.IPv4zero): + ip := (net.IP)(val.Bytes()) + if ip == nil || ip.Equal(net.IPv4zero) { + if required { + return fmt.Errorf("%s.%s (%v) is required, got zero IPv4 address", typeof.Name(), field.Name, val.Kind()) + } + } else { + (*params).Set(name, ip.String()) + } + default: + if val.Len() == 0 { + if required { + return fmt.Errorf("%s.%s (%v) is required, got empty slice", typeof.Name(), field.Name, val.Kind()) + } + } else { + v := val.Bytes() + (*params).Set(name, base64.StdEncoding.EncodeToString(v)) + } + } + case reflect.String: + { + if val.Len() == 0 { + if required { + return fmt.Errorf("%s.%s (%v) is required, got empty slice", typeof.Name(), field.Name, val.Kind()) + } + } else { + elems := make([]string, 0, val.Len()) + for i := 0; i < val.Len(); i++ { + // XXX what if the value contains a comma? Double encode? + s := val.Index(i).String() + elems = append(elems, s) + } + (*params).Set(name, strings.Join(elems, ",")) + } + } + default: + if val.Len() == 0 { + if required { + return fmt.Errorf("%s.%s (%v) is required, got empty slice", typeof.Name(), field.Name, val.Kind()) + } + } else { + err := prepareList(name, params, val.Interface()) + if err != nil { + return err + } + } + } + case reflect.Map: + if val.Len() == 0 { + if required { + return fmt.Errorf("%s.%s (%v) is required, got empty map", typeof.Name(), field.Name, val.Kind()) + } + } else { + err := prepareMap(name, params, val.Interface()) + if err != nil { + return err + } + } + default: + if required { + return fmt.Errorf("Unsupported type %s.%s (%v)", typeof.Name(), field.Name, val.Kind()) + } + } + } else { + log.Printf("[SKIP] %s.%s no json label found", typeof.Name(), field.Name) + } + } + + return nil +} + +func prepareList(prefix string, params *url.Values, slice interface{}) error { + value := reflect.ValueOf(slice) + + for i := 0; i < value.Len(); i++ { + prepareValues(fmt.Sprintf("%s[%d].", prefix, i), params, value.Index(i).Interface()) + } + + return nil +} + +func prepareMap(prefix string, params *url.Values, m interface{}) error { + value := reflect.ValueOf(m) + + for i, key := range value.MapKeys() { + var keyName string + var keyValue string + + switch key.Kind() { + case reflect.String: + keyName = key.String() + default: + return fmt.Errorf("Only map[string]string are supported (XXX)") + } + + val := value.MapIndex(key) + switch val.Kind() { + case reflect.String: + keyValue = val.String() + default: + return fmt.Errorf("Only map[string]string are supported (XXX)") + } + params.Set(fmt.Sprintf("%s[%d].%s", prefix, i, keyName), keyValue) + } + return nil +} + +// extractJSONTag returns the variable name or defaultName as well as if the field is required (!omitempty) +func extractJSONTag(defaultName, jsonTag string) (string, bool) { + tags := strings.Split(jsonTag, ",") + name := tags[0] + required := true + for _, tag := range tags { + if tag == "omitempty" { + required = false + } + } + + if name == "" || name == "omitempty" { + name = defaultName + } + return name, required +} diff --git a/vendor/github.com/exoscale/egoscale/service_offerings.go b/vendor/github.com/exoscale/egoscale/service_offerings.go index c3a6dac67d..aefd8b711d 100644 --- a/vendor/github.com/exoscale/egoscale/service_offerings.go +++ b/vendor/github.com/exoscale/egoscale/service_offerings.go @@ -50,7 +50,8 @@ type ListServiceOfferings struct { VirtualMachineID string `json:"virtualmachineid,omitempty"` } -func (*ListServiceOfferings) name() string { +// APIName returns the CloudStack API command name +func (*ListServiceOfferings) APIName() string { return "listServiceOfferings" } diff --git a/vendor/github.com/exoscale/egoscale/snapshots.go b/vendor/github.com/exoscale/egoscale/snapshots.go index 55e5956c34..b247af9905 100644 --- a/vendor/github.com/exoscale/egoscale/snapshots.go +++ b/vendor/github.com/exoscale/egoscale/snapshots.go @@ -41,7 +41,8 @@ type CreateSnapshot struct { QuiesceVM *bool `json:"quiescevm,omitempty"` } -func (*CreateSnapshot) name() string { +// APIName returns the CloudStack API command name +func (*CreateSnapshot) APIName() string { return "createSnapshot" } @@ -75,7 +76,8 @@ type ListSnapshots struct { ZoneID string `json:"zoneid,omitempty"` } -func (*ListSnapshots) name() string { +// APIName returns the CloudStack API command name +func (*ListSnapshots) APIName() string { return "listSnapshots" } @@ -96,7 +98,8 @@ type DeleteSnapshot struct { ID string `json:"id"` } -func (*DeleteSnapshot) name() string { +// APIName returns the CloudStack API command name +func (*DeleteSnapshot) APIName() string { return "deleteSnapshot" } @@ -111,7 +114,8 @@ type RevertSnapshot struct { ID string `json:"id"` } -func (*RevertSnapshot) name() string { +// APIName returns the CloudStack API command name +func (*RevertSnapshot) APIName() string { return "revertSnapshot" } diff --git a/vendor/github.com/exoscale/egoscale/tags.go b/vendor/github.com/exoscale/egoscale/tags.go index e921231af6..e542894279 100644 --- a/vendor/github.com/exoscale/egoscale/tags.go +++ b/vendor/github.com/exoscale/egoscale/tags.go @@ -34,7 +34,8 @@ type CreateTags struct { Customer string `json:"customer,omitempty"` } -func (*CreateTags) name() string { +// APIName returns the CloudStack API command name +func (*CreateTags) APIName() string { return "createTags" } @@ -51,7 +52,8 @@ type DeleteTags struct { Tags []ResourceTag `json:"tags,omitempty"` } -func (*DeleteTags) name() string { +// APIName returns the CloudStack API command name +func (*DeleteTags) APIName() string { return "deleteTags" } @@ -78,7 +80,8 @@ type ListTags struct { Value string `json:"value,omitempty"` } -func (*ListTags) name() string { +// APIName returns the CloudStack API command name +func (*ListTags) APIName() string { return "listTags" } diff --git a/vendor/github.com/exoscale/egoscale/templates.go b/vendor/github.com/exoscale/egoscale/templates.go index 7f30bdf055..453349861e 100644 --- a/vendor/github.com/exoscale/egoscale/templates.go +++ b/vendor/github.com/exoscale/egoscale/templates.go @@ -61,7 +61,8 @@ type ListTemplates struct { ZoneID string `json:"zoneid,omitempty"` } -func (*ListTemplates) name() string { +// APIName returns the CloudStack API command name +func (*ListTemplates) APIName() string { return "listTemplates" } diff --git a/vendor/github.com/exoscale/egoscale/topology.go b/vendor/github.com/exoscale/egoscale/topology.go deleted file mode 100644 index 9ee270681e..0000000000 --- a/vendor/github.com/exoscale/egoscale/topology.go +++ /dev/null @@ -1,208 +0,0 @@ -package egoscale - -import ( - "fmt" - "regexp" - "strings" -) - -// GetSecurityGroups returns all security groups -// -// Deprecated: do it yourself -func (exo *Client) GetSecurityGroups() (map[string]SecurityGroup, error) { - var sgs map[string]SecurityGroup - resp, err := exo.Request(&ListSecurityGroups{}) - if err != nil { - return nil, err - } - - sgs = make(map[string]SecurityGroup) - for _, sg := range resp.(*ListSecurityGroupsResponse).SecurityGroup { - sgs[sg.Name] = sg - } - return sgs, nil -} - -// GetSecurityGroupID returns security group by name -// -// Deprecated: do it yourself -func (exo *Client) GetSecurityGroupID(name string) (string, error) { - resp, err := exo.Request(&ListSecurityGroups{SecurityGroupName: name}) - if err != nil { - return "", err - } - - for _, sg := range resp.(*ListSecurityGroupsResponse).SecurityGroup { - if sg.Name == name { - return sg.ID, nil - } - } - - return "", nil -} - -// GetAllZones returns all the zone id by name -// -// Deprecated: do it yourself -func (exo *Client) GetAllZones() (map[string]string, error) { - var zones map[string]string - resp, err := exo.Request(&ListZones{}) - if err != nil { - return zones, err - } - - zones = make(map[string]string) - for _, zone := range resp.(*ListZonesResponse).Zone { - zones[strings.ToLower(zone.Name)] = zone.ID - } - return zones, nil -} - -// GetProfiles returns a mapping of the service offerings by name -// -// Deprecated: do it yourself -func (exo *Client) GetProfiles() (map[string]string, error) { - profiles := make(map[string]string) - resp, err := exo.Request(&ListServiceOfferings{}) - if err != nil { - return profiles, nil - } - - for _, offering := range resp.(*ListServiceOfferingsResponse).ServiceOffering { - profiles[strings.ToLower(offering.Name)] = offering.ID - } - - return profiles, nil -} - -// GetKeypairs returns the list of SSH keyPairs -// -// Deprecated: do it yourself -func (exo *Client) GetKeypairs() ([]SSHKeyPair, error) { - var keypairs []SSHKeyPair - - resp, err := exo.Request(&ListSSHKeyPairs{}) - if err != nil { - return keypairs, err - } - - r := resp.(*ListSSHKeyPairsResponse) - keypairs = make([]SSHKeyPair, r.Count) - for i, keypair := range r.SSHKeyPair { - keypairs[i] = keypair - } - return keypairs, nil -} - -// GetAffinityGroups returns a mapping of the (anti-)affinity groups -// -// Deprecated: do it yourself -func (exo *Client) GetAffinityGroups() (map[string]string, error) { - var affinitygroups map[string]string - - resp, err := exo.Request(&ListAffinityGroups{}) - if err != nil { - return affinitygroups, err - } - - affinitygroups = make(map[string]string) - for _, affinitygroup := range resp.(*ListAffinityGroupsResponse).AffinityGroup { - affinitygroups[affinitygroup.Name] = affinitygroup.ID - } - return affinitygroups, nil -} - -// GetImages list the available featured images and group them by name, then size. -// -// Deprecated: do it yourself -func (exo *Client) GetImages() (map[string]map[int64]string, error) { - var images map[string]map[int64]string - images = make(map[string]map[int64]string) - re := regexp.MustCompile(`^Linux (?P.+?) (?P[0-9.]+)\b`) - - resp, err := exo.Request(&ListTemplates{ - TemplateFilter: "featured", - ZoneID: "1", // XXX: Hack to list only CH-GVA - }) - if err != nil { - return images, err - } - - for _, template := range resp.(*ListTemplatesResponse).Template { - size := int64(template.Size >> 30) // B to GiB - - fullname := strings.ToLower(template.Name) - - if _, present := images[fullname]; !present { - images[fullname] = make(map[int64]string) - } - images[fullname][size] = template.ID - - submatch := re.FindStringSubmatch(template.Name) - if len(submatch) > 0 { - name := strings.Replace(strings.ToLower(submatch[1]), " ", "-", -1) - version := submatch[2] - image := fmt.Sprintf("%s-%s", name, version) - - if _, present := images[image]; !present { - images[image] = make(map[int64]string) - } - images[image][size] = template.ID - } - } - return images, nil -} - -// GetTopology returns an big, yet incomplete view of the world -// -// Deprecated: will go away in the future -func (exo *Client) GetTopology() (*Topology, error) { - zones, err := exo.GetAllZones() - if err != nil { - return nil, err - } - images, err := exo.GetImages() - if err != nil { - return nil, err - } - securityGroups, err := exo.GetSecurityGroups() - if err != nil { - return nil, err - } - groups := make(map[string]string) - for k, v := range securityGroups { - groups[k] = v.ID - } - - keypairs, err := exo.GetKeypairs() - if err != nil { - return nil, err - } - - /* Convert the ssh keypair to contain just the name */ - keynames := make([]string, len(keypairs)) - for i, k := range keypairs { - keynames[i] = k.Name - } - - affinitygroups, err := exo.GetAffinityGroups() - if err != nil { - return nil, err - } - - profiles, err := exo.GetProfiles() - if err != nil { - return nil, err - } - - topo := &Topology{ - Zones: zones, - Images: images, - Keypairs: keynames, - Profiles: profiles, - AffinityGroups: affinitygroups, - SecurityGroups: groups, - } - - return topo, nil -} diff --git a/vendor/github.com/exoscale/egoscale/types.go b/vendor/github.com/exoscale/egoscale/types.go deleted file mode 100644 index 484dc31f55..0000000000 --- a/vendor/github.com/exoscale/egoscale/types.go +++ /dev/null @@ -1,23 +0,0 @@ -package egoscale - -import ( - "net/http" -) - -// Client represents the CloudStack API client -type Client struct { - client *http.Client - endpoint string - apiKey string - apiSecret string -} - -// Topology represents a view of the servers -type Topology struct { - Zones map[string]string - Images map[string]map[int64]string - Profiles map[string]string - Keypairs []string - SecurityGroups map[string]string - AffinityGroups map[string]string -} diff --git a/vendor/github.com/exoscale/egoscale/users.go b/vendor/github.com/exoscale/egoscale/users.go index 353e4b6bab..97868c3034 100644 --- a/vendor/github.com/exoscale/egoscale/users.go +++ b/vendor/github.com/exoscale/egoscale/users.go @@ -27,7 +27,8 @@ type RegisterUserKeys struct { ID string `json:"id"` } -func (*RegisterUserKeys) name() string { +// APIName returns the CloudStack API command name +func (*RegisterUserKeys) APIName() string { return "registerUserKeys" } diff --git a/vendor/github.com/exoscale/egoscale/virtual_machines.go b/vendor/github.com/exoscale/egoscale/virtual_machines.go index efc6ae5b72..ed715e3154 100644 --- a/vendor/github.com/exoscale/egoscale/virtual_machines.go +++ b/vendor/github.com/exoscale/egoscale/virtual_machines.go @@ -1,6 +1,13 @@ package egoscale -import "net" +import ( + "context" + "fmt" + "net" + "net/url" + + "github.com/jinzhu/copier" +) // VirtualMachine reprents a virtual machine type VirtualMachine struct { @@ -82,6 +89,44 @@ func (*VirtualMachine) ResourceType() string { return "UserVM" } +// Get fills the VM +func (vm *VirtualMachine) Get(ctx context.Context, client *Client) error { + if vm.ID == "" && vm.Name == "" { + return fmt.Errorf("A VirtualMachine may only be searched using ID or Name") + } + + resp, err := client.RequestWithContext(ctx, &ListVirtualMachines{ + ID: vm.ID, + Name: vm.Name, + }) + + if err != nil { + return err + } + + vms := resp.(*ListVirtualMachinesResponse) + count := len(vms.VirtualMachine) + if count == 0 { + return &ErrorResponse{ + ErrorCode: ParamError, + ErrorText: fmt.Sprintf("VirtualMachine not found. id: %s, name: %s", vm.ID, vm.Name), + } + } else if count > 1 { + return fmt.Errorf("More than one VirtualMachine was found. Query: id: %s, name: %s", vm.ID, vm.Name) + } + + return copier.Copy(vm, vms.VirtualMachine[0]) +} + +// Delete destroys the VM +func (vm *VirtualMachine) Delete(ctx context.Context, client *Client) error { + _, err := client.RequestWithContext(ctx, &DestroyVirtualMachine{ + ID: vm.ID, + }) + + return err +} + // DefaultNic returns the default nic func (vm *VirtualMachine) DefaultNic() *Nic { for _, nic := range vm.Nic { @@ -138,6 +183,13 @@ type IPToNetwork struct { NetworkID string `json:"networkid,omitempty"` } +// Password represents an encrypted password +// +// TODO: method to decrypt it, https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=34014652 +type Password struct { + EncryptedPassword string `json:"encryptedpassword"` +} + // VirtualMachineResponse represents a generic Virtual Machine response type VirtualMachineResponse struct { VirtualMachine VirtualMachine `json:"virtualmachine"` @@ -151,10 +203,10 @@ type DeployVirtualMachine struct { TemplateID string `json:"templateid"` ZoneID string `json:"zoneid"` Account string `json:"account,omitempty"` - AffinityGroupIDs []string `json:"affinitygroupids,omitempty"` - AffinityGroupNames []string `json:"affinitygroupnames,omitempty"` - CustomID string `json:"customid,omitempty"` // root only - DeploymentPlanner string `json:"deploymentplanner,omitempty"` // root only + AffinityGroupIDs []string `json:"affinitygroupids,omitempty"` // mutually exclusive with AffinityGroupNames + AffinityGroupNames []string `json:"affinitygroupnames,omitempty"` // mutually exclusive with AffinityGroupIDs + CustomID string `json:"customid,omitempty"` // root only + DeploymentPlanner string `json:"deploymentplanner,omitempty"` // root only Details map[string]string `json:"details,omitempty"` DiskOfferingID string `json:"diskofferingid,omitempty"` DisplayName string `json:"displayname,omitempty"` @@ -163,26 +215,43 @@ type DeployVirtualMachine struct { Group string `json:"group,omitempty"` HostID string `json:"hostid,omitempty"` Hypervisor string `json:"hypervisor,omitempty"` - IP6Address net.IP `json:"ip6address,omitempty"` + IP4 *bool `json:"ip4,omitempty"` // Exoscale specific + IP6 *bool `json:"ip6,omitempty"` // Exoscale specific IPAddress net.IP `json:"ipaddress,omitempty"` + IP6Address net.IP `json:"ip6address,omitempty"` IPToNetworkList []IPToNetwork `json:"iptonetworklist,omitempty"` Keyboard string `json:"keyboard,omitempty"` KeyPair string `json:"keypair,omitempty"` Name string `json:"name,omitempty"` NetworkIDs []string `json:"networkids,omitempty"` // mutually exclusive with IPToNetworkList ProjectID string `json:"projectid,omitempty"` - RootDiskSize int64 `json:"rootdisksize,omitempty"` // in GiB - SecurityGroupIDs []string `json:"securitygroupids,omitempty"` - SecurityGroupNames []string `json:"securitygroupnames,omitempty"` // does nothing, mutually exclusive + RootDiskSize int64 `json:"rootdisksize,omitempty"` // in GiB + SecurityGroupIDs []string `json:"securitygroupids,omitempty"` // mutually exclusive with SecurityGroupNames + SecurityGroupNames []string `json:"securitygroupnames,omitempty"` // mutually exclusive with SecurityGroupIDs Size string `json:"size,omitempty"` // mutually exclusive with DiskOfferingID StartVM *bool `json:"startvm,omitempty"` UserData string `json:"userdata,omitempty"` // the client is responsible to base64/gzip it } -func (*DeployVirtualMachine) name() string { +// APIName returns the CloudStack API command name +func (*DeployVirtualMachine) APIName() string { return "deployVirtualMachine" } +func (req *DeployVirtualMachine) onBeforeSend(params *url.Values) error { + // Either AffinityGroupIDs or AffinityGroupNames must be set + if len(req.AffinityGroupIDs) > 0 && len(req.AffinityGroupNames) > 0 { + return fmt.Errorf("Either AffinityGroupIDs or AffinityGroupNames must be set") + } + + // Either SecurityGroupIDs or SecurityGroupNames must be set + if len(req.SecurityGroupIDs) > 0 && len(req.SecurityGroupNames) > 0 { + return fmt.Errorf("Either SecurityGroupIDs or SecurityGroupNames must be set") + } + + return nil +} + func (*DeployVirtualMachine) asyncResponse() interface{} { return new(DeployVirtualMachineResponse) } @@ -199,7 +268,8 @@ type StartVirtualMachine struct { HostID string `json:"hostid,omitempty"` // root only } -func (*StartVirtualMachine) name() string { +// APIName returns the CloudStack API command name +func (*StartVirtualMachine) APIName() string { return "startVirtualMachine" } func (*StartVirtualMachine) asyncResponse() interface{} { @@ -217,7 +287,8 @@ type StopVirtualMachine struct { Forced *bool `json:"forced,omitempty"` } -func (*StopVirtualMachine) name() string { +// APIName returns the CloudStack API command name +func (*StopVirtualMachine) APIName() string { return "stopVirtualMachine" } @@ -235,7 +306,8 @@ type RebootVirtualMachine struct { ID string `json:"id"` } -func (*RebootVirtualMachine) name() string { +// APIName returns the CloudStack API command name +func (*RebootVirtualMachine) APIName() string { return "rebootVirtualMachine" } @@ -255,7 +327,8 @@ type RestoreVirtualMachine struct { RootDiskSize string `json:"rootdisksize,omitempty"` // in GiB, Exoscale specific } -func (*RestoreVirtualMachine) name() string { +// APIName returns the CloudStack API command name +func (*RestoreVirtualMachine) APIName() string { return "restoreVirtualMachine" } @@ -273,7 +346,8 @@ type RecoverVirtualMachine struct { ID string `json:"virtualmachineid"` } -func (*RecoverVirtualMachine) name() string { +// APIName returns the CloudStack API command name +func (*RecoverVirtualMachine) APIName() string { return "recoverVirtualMachine" } @@ -292,7 +366,8 @@ type DestroyVirtualMachine struct { Expunge *bool `json:"expunge,omitempty"` } -func (*DestroyVirtualMachine) name() string { +// APIName returns the CloudStack API command name +func (*DestroyVirtualMachine) APIName() string { return "destroyVirtualMachine" } @@ -316,12 +391,13 @@ type UpdateVirtualMachine struct { HAEnable *bool `json:"haenable,omitempty"` IsDynamicallyScalable *bool `json:"isdynamicallyscalable,omitempty"` Name string `json:"name,omitempty"` // must reboot - OsTypeID int64 `json:"ostypeid,omitempty"` + OSTypeID int64 `json:"ostypeid,omitempty"` SecurityGroupIDs []string `json:"securitygroupids,omitempty"` UserData string `json:"userdata,omitempty"` } -func (*UpdateVirtualMachine) name() string { +// APIName returns the CloudStack API command name +func (*UpdateVirtualMachine) APIName() string { return "updateVirtualMachine" } @@ -337,7 +413,8 @@ type ExpungeVirtualMachine struct { ID string `json:"id"` } -func (*ExpungeVirtualMachine) name() string { +// APIName returns the CloudStack API command name +func (*ExpungeVirtualMachine) APIName() string { return "expungeVirtualMachine" } @@ -357,7 +434,8 @@ type ScaleVirtualMachine struct { Details map[string]string `json:"details,omitempty"` } -func (*ScaleVirtualMachine) name() string { +// APIName returns the CloudStack API command name +func (*ScaleVirtualMachine) APIName() string { return "scaleVirtualMachine" } @@ -370,7 +448,8 @@ func (*ScaleVirtualMachine) asyncResponse() interface{} { // CloudStack API: https://cloudstack.apache.org/api/apidocs-4.10/apis/changeServiceForVirtualMachine.html type ChangeServiceForVirtualMachine ScaleVirtualMachine -func (*ChangeServiceForVirtualMachine) name() string { +// APIName returns the CloudStack API command name +func (*ChangeServiceForVirtualMachine) APIName() string { return "changeServiceForVirtualMachine" } @@ -386,7 +465,8 @@ type ChangeServiceForVirtualMachineResponse VirtualMachineResponse // CloudStack API: https://cloudstack.apache.org/api/apidocs-4.10/apis/resetPasswordForVirtualMachine.html type ResetPasswordForVirtualMachine ScaleVirtualMachine -func (*ResetPasswordForVirtualMachine) name() string { +// APIName returns the CloudStack API command name +func (*ResetPasswordForVirtualMachine) APIName() string { return "resetPasswordForVirtualMachine" } @@ -404,7 +484,8 @@ type GetVMPassword struct { ID string `json:"id"` } -func (*GetVMPassword) name() string { +// APIName returns the CloudStack API command name +func (*GetVMPassword) APIName() string { return "getVMPassword" } @@ -415,7 +496,7 @@ func (*GetVMPassword) response() interface{} { // GetVMPasswordResponse represents the encrypted password type GetVMPasswordResponse struct { // Base64 encrypted password for the VM - EncryptedPassword string `json:"encryptedpassword"` + Password Password `json:"password"` } // ListVirtualMachines represents a search for a VM @@ -454,7 +535,8 @@ type ListVirtualMachines struct { ZoneID string `json:"zoneid,omitempty"` } -func (*ListVirtualMachines) name() string { +// APIName returns the CloudStack API command name +func (*ListVirtualMachines) APIName() string { return "listVirtualMachines" } @@ -477,7 +559,8 @@ type AddNicToVirtualMachine struct { IPAddress net.IP `json:"ipaddress,omitempty"` } -func (*AddNicToVirtualMachine) name() string { +// APIName returns the CloudStack API command name +func (*AddNicToVirtualMachine) APIName() string { return "addNicToVirtualMachine" } @@ -496,7 +579,8 @@ type RemoveNicFromVirtualMachine struct { VirtualMachineID string `json:"virtualmachineid"` } -func (*RemoveNicFromVirtualMachine) name() string { +// APIName returns the CloudStack API command name +func (*RemoveNicFromVirtualMachine) APIName() string { return "removeNicFromVirtualMachine" } @@ -516,7 +600,8 @@ type UpdateDefaultNicForVirtualMachine struct { IPAddress net.IP `json:"ipaddress,omitempty"` } -func (*UpdateDefaultNicForVirtualMachine) name() string { +// APIName returns the CloudStack API command name +func (*UpdateDefaultNicForVirtualMachine) APIName() string { return "updateDefaultNicForVirtualMachine" } diff --git a/vendor/github.com/exoscale/egoscale/vm_groups.go b/vendor/github.com/exoscale/egoscale/vm_groups.go index 12dffa54da..be8bb691af 100644 --- a/vendor/github.com/exoscale/egoscale/vm_groups.go +++ b/vendor/github.com/exoscale/egoscale/vm_groups.go @@ -27,7 +27,8 @@ type CreateInstanceGroup struct { ProjectID string `json:"projectid,omitempty"` } -func (*CreateInstanceGroup) name() string { +// APIName returns the CloudStack API command name +func (*CreateInstanceGroup) APIName() string { return "createInstanceGroup" } @@ -46,7 +47,8 @@ type UpdateInstanceGroup struct { Name string `json:"name,omitempty"` } -func (*UpdateInstanceGroup) name() string { +// APIName returns the CloudStack API command name +func (*UpdateInstanceGroup) APIName() string { return "updateInstanceGroup" } @@ -67,7 +69,8 @@ type DeleteInstanceGroup struct { ProjectID string `json:"projectid,omitempty"` } -func (*DeleteInstanceGroup) name() string { +// APIName returns the CloudStack API command name +func (*DeleteInstanceGroup) APIName() string { return "deleteInstanceGroup" } @@ -91,7 +94,8 @@ type ListInstanceGroups struct { ProjectID string `json:"projectid,omitempty"` } -func (*ListInstanceGroups) name() string { +// APIName returns the CloudStack API command name +func (*ListInstanceGroups) APIName() string { return "listInstanceGroups" } diff --git a/vendor/github.com/exoscale/egoscale/volumes.go b/vendor/github.com/exoscale/egoscale/volumes.go index 3914275655..48a6226b8c 100644 --- a/vendor/github.com/exoscale/egoscale/volumes.go +++ b/vendor/github.com/exoscale/egoscale/volumes.go @@ -48,7 +48,8 @@ type ResizeVolume struct { Size int64 `json:"size,omitempty"` // in GiB } -func (*ResizeVolume) name() string { +// APIName returns the CloudStack API command name +func (*ResizeVolume) APIName() string { return "resizeVolume" } @@ -86,7 +87,8 @@ type ListVolumes struct { ZoneID string `json:"zoneid,omitempty"` } -func (*ListVolumes) name() string { +// APIName returns the CloudStack API command name +func (*ListVolumes) APIName() string { return "listVolumes" } diff --git a/vendor/github.com/exoscale/egoscale/zones.go b/vendor/github.com/exoscale/egoscale/zones.go index 59642f0389..d45be94920 100644 --- a/vendor/github.com/exoscale/egoscale/zones.go +++ b/vendor/github.com/exoscale/egoscale/zones.go @@ -45,7 +45,8 @@ type ListZones struct { Tags []ResourceTag `json:"tags,omitempty"` } -func (*ListZones) name() string { +// APIName returns the CloudStack API command name +func (*ListZones) APIName() string { return "listZones" } diff --git a/vendor/github.com/jinzhu/copier/Guardfile b/vendor/github.com/jinzhu/copier/Guardfile new file mode 100644 index 0000000000..0b860b0653 --- /dev/null +++ b/vendor/github.com/jinzhu/copier/Guardfile @@ -0,0 +1,3 @@ +guard 'gotest' do + watch(%r{\.go$}) +end diff --git a/vendor/github.com/jinzhu/copier/License b/vendor/github.com/jinzhu/copier/License new file mode 100644 index 0000000000..e2dc5381e1 --- /dev/null +++ b/vendor/github.com/jinzhu/copier/License @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2015 Jinzhu + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/jinzhu/copier/README.md b/vendor/github.com/jinzhu/copier/README.md new file mode 100644 index 0000000000..f929b46793 --- /dev/null +++ b/vendor/github.com/jinzhu/copier/README.md @@ -0,0 +1,100 @@ +# Copier + + I am a copier, I copy everything from one to another + +[![wercker status](https://app.wercker.com/status/9d44ad2d4e6253929c8fb71359effc0b/s/master "wercker status")](https://app.wercker.com/project/byKey/9d44ad2d4e6253929c8fb71359effc0b) + +## Features + +* Copy from field to field with same name +* Copy from method to field with same name +* Copy from field to method with same name +* Copy from slice to slice +* Copy from struct to slice + +## Usage + +```go +package main + +import ( + "fmt" + "github.com/jinzhu/copier" +) + +type User struct { + Name string + Role string + Age int32 +} + +func (user *User) DoubleAge() int32 { + return 2 * user.Age +} + +type Employee struct { + Name string + Age int32 + DoubleAge int32 + EmployeId int64 + SuperRule string +} + +func (employee *Employee) Role(role string) { + employee.SuperRule = "Super " + role +} + +func main() { + var ( + user = User{Name: "Jinzhu", Age: 18, Role: "Admin"} + users = []User{{Name: "Jinzhu", Age: 18, Role: "Admin"}, {Name: "jinzhu 2", Age: 30, Role: "Dev"}} + employee = Employee{} + employees = []Employee{} + ) + + copier.Copy(&employee, &user) + + fmt.Printf("%#v \n", employee) + // Employee{ + // Name: "Jinzhu", // Copy from field + // Age: 18, // Copy from field + // DoubleAge: 36, // Copy from method + // EmployeeId: 0, // Ignored + // SuperRule: "Super Admin", // Copy to method + // } + + // Copy struct to slice + copier.Copy(&employees, &user) + + fmt.Printf("%#v \n", employees) + // []Employee{ + // {Name: "Jinzhu", Age: 18, DoubleAge: 36, EmployeId: 0, SuperRule: "Super Admin"} + // } + + // Copy slice to slice + employees = []Employee{} + copier.Copy(&employees, &users) + + fmt.Printf("%#v \n", employees) + // []Employee{ + // {Name: "Jinzhu", Age: 18, DoubleAge: 36, EmployeId: 0, SuperRule: "Super Admin"}, + // {Name: "jinzhu 2", Age: 30, DoubleAge: 60, EmployeId: 0, SuperRule: "Super Dev"}, + // } +} +``` + +## Contributing + +You can help to make the project better, check out [http://gorm.io/contribute.html](http://gorm.io/contribute.html) for things you can do. + +# Author + +**jinzhu** + +* +* +* + +## License + +Released under the [MIT License](https://github.com/jinzhu/copier/blob/master/License). diff --git a/vendor/github.com/jinzhu/copier/copier.go b/vendor/github.com/jinzhu/copier/copier.go new file mode 100644 index 0000000000..ecbddffb0f --- /dev/null +++ b/vendor/github.com/jinzhu/copier/copier.go @@ -0,0 +1,185 @@ +package copier + +import ( + "database/sql" + "errors" + "reflect" +) + +// Copy copy things +func Copy(toValue interface{}, fromValue interface{}) (err error) { + var ( + isSlice bool + amount = 1 + from = indirect(reflect.ValueOf(fromValue)) + to = indirect(reflect.ValueOf(toValue)) + ) + + if !to.CanAddr() { + return errors.New("copy to value is unaddressable") + } + + // Return is from value is invalid + if !from.IsValid() { + return + } + + // Just set it if possible to assign + if from.Type().AssignableTo(to.Type()) { + to.Set(from) + return + } + + fromType := indirectType(from.Type()) + toType := indirectType(to.Type()) + + if fromType.Kind() != reflect.Struct || toType.Kind() != reflect.Struct { + return + } + + if to.Kind() == reflect.Slice { + isSlice = true + if from.Kind() == reflect.Slice { + amount = from.Len() + } + } + + for i := 0; i < amount; i++ { + var dest, source reflect.Value + + if isSlice { + // source + if from.Kind() == reflect.Slice { + source = indirect(from.Index(i)) + } else { + source = indirect(from) + } + + // dest + dest = indirect(reflect.New(toType).Elem()) + } else { + source = indirect(from) + dest = indirect(to) + } + + // Copy from field to field or method + for _, field := range deepFields(fromType) { + name := field.Name + + if fromField := source.FieldByName(name); fromField.IsValid() { + // has field + if toField := dest.FieldByName(name); toField.IsValid() { + if toField.CanSet() { + if !set(toField, fromField) { + if err := Copy(toField.Addr().Interface(), fromField.Interface()); err != nil { + return err + } + } + } + } else { + // try to set to method + var toMethod reflect.Value + if dest.CanAddr() { + toMethod = dest.Addr().MethodByName(name) + } else { + toMethod = dest.MethodByName(name) + } + + if toMethod.IsValid() && toMethod.Type().NumIn() == 1 && fromField.Type().AssignableTo(toMethod.Type().In(0)) { + toMethod.Call([]reflect.Value{fromField}) + } + } + } + } + + // Copy from method to field + for _, field := range deepFields(toType) { + name := field.Name + + var fromMethod reflect.Value + if source.CanAddr() { + fromMethod = source.Addr().MethodByName(name) + } else { + fromMethod = source.MethodByName(name) + } + + if fromMethod.IsValid() && fromMethod.Type().NumIn() == 0 && fromMethod.Type().NumOut() == 1 { + if toField := dest.FieldByName(name); toField.IsValid() && toField.CanSet() { + values := fromMethod.Call([]reflect.Value{}) + if len(values) >= 1 { + set(toField, values[0]) + } + } + } + } + + if isSlice { + if dest.Addr().Type().AssignableTo(to.Type().Elem()) { + to.Set(reflect.Append(to, dest.Addr())) + } else if dest.Type().AssignableTo(to.Type().Elem()) { + to.Set(reflect.Append(to, dest)) + } + } + } + return +} + +func deepFields(reflectType reflect.Type) []reflect.StructField { + var fields []reflect.StructField + + if reflectType = indirectType(reflectType); reflectType.Kind() == reflect.Struct { + for i := 0; i < reflectType.NumField(); i++ { + v := reflectType.Field(i) + if v.Anonymous { + fields = append(fields, deepFields(v.Type)...) + } else { + fields = append(fields, v) + } + } + } + + return fields +} + +func indirect(reflectValue reflect.Value) reflect.Value { + for reflectValue.Kind() == reflect.Ptr { + reflectValue = reflectValue.Elem() + } + return reflectValue +} + +func indirectType(reflectType reflect.Type) reflect.Type { + for reflectType.Kind() == reflect.Ptr || reflectType.Kind() == reflect.Slice { + reflectType = reflectType.Elem() + } + return reflectType +} + +func set(to, from reflect.Value) bool { + if from.IsValid() { + if to.Kind() == reflect.Ptr { + //set `to` to nil if from is nil + if from.Kind() == reflect.Ptr && from.IsNil() { + to.Set(reflect.Zero(to.Type())) + return true + } else if to.IsNil() { + to.Set(reflect.New(to.Type().Elem())) + } + to = to.Elem() + } + + if from.Type().ConvertibleTo(to.Type()) { + to.Set(from.Convert(to.Type())) + } else if scanner, ok := to.Addr().Interface().(sql.Scanner); ok { + err := scanner.Scan(from.Interface()) + if err != nil { + return false + } + } else if from.Kind() == reflect.Ptr { + return set(to, from.Elem()) + } else { + return false + } + } + return true +} diff --git a/vendor/github.com/jinzhu/copier/wercker.yml b/vendor/github.com/jinzhu/copier/wercker.yml new file mode 100644 index 0000000000..5e6ce981dc --- /dev/null +++ b/vendor/github.com/jinzhu/copier/wercker.yml @@ -0,0 +1,23 @@ +box: golang + +build: + steps: + - setup-go-workspace + + # Gets the dependencies + - script: + name: go get + code: | + go get + + # Build the project + - script: + name: go build + code: | + go build ./... + + # Test the project + - script: + name: go test + code: | + go test ./...