diff --git a/glide.lock b/glide.lock index c64c179a51..ed5b4c93a9 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: d1ca92323760e7bdb44d2b80d066dd21bcd49c714faa88372a87a81a9520dffc -updated: 2017-06-16T11:52:01.08864027+02:00 +hash: 16145fd21935177a6d06a0570740b844c8f7d4280a48a0b1145bf3c835f58f41 +updated: 2017-08-03T20:37:19.172220412-07:00 imports: - name: bitbucket.org/ww/goautoneg version: 75cd24fc2f2c2a2088577d12123ddee5f54e0675 @@ -125,6 +125,8 @@ imports: - query - name: github.com/google/gofuzz version: 44d81051d367757e1c7c6a5a86423ece9afcf63c +- name: github.com/gophercloud/gophercloud + version: 3172ddf2336933bc38b355df89cb015e0954df67 - name: github.com/howeyc/gopass version: 3ca23474a7c7203e0a0a070fd33508f6efdb9b3d - name: github.com/imdario/mergo diff --git a/glide.yaml b/glide.yaml index 71eb385210..c0bb27e2f2 100644 --- a/glide.yaml +++ b/glide.yaml @@ -54,3 +54,4 @@ import: - package: github.com/digitalocean/godo version: ~1.1.0 - package: github.com/coreos/go-oidc +- package: github.com/gophercloud/gophercloud diff --git a/main.go b/main.go index ea33daa30c..f6d05d0ff6 100644 --- a/main.go +++ b/main.go @@ -103,6 +103,8 @@ func main() { p, err = provider.NewDigitalOceanProvider(domainFilter, cfg.DryRun) case "inmemory": p, err = provider.NewInMemoryProvider(provider.InMemoryWithDomain(domainFilter), provider.InMemoryWithLogging()), nil + case "designate": + p, err = provider.NewDesignateProvider(domainFilter, cfg.DryRun) default: log.Fatalf("unknown dns provider: %s", cfg.Provider) } diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index f6f097b3a8..c0649d4655 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -114,7 +114,7 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("publish-internal-services", "Allow external-dns to publish DNS records for ClusterIP services (optional)").BoolVar(&cfg.PublishInternal) // Flags related to providers - app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, google, azure, cloudflare, digitalocean, inmemory)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "google", "azure", "cloudflare", "digitalocean", "inmemory") + app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, google, azure, cloudflare, digitalocean, inmemory, designate)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "google", "azure", "cloudflare", "digitalocean", "inmemory", "designate") app.Flag("domain-filter", "Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional)").Default("").StringsVar(&cfg.DomainFilter) app.Flag("google-project", "When using the Google provider, specify the Google project (required when --provider=google)").Default(defaultConfig.GoogleProject).StringVar(&cfg.GoogleProject) app.Flag("aws-zone-type", "When using the AWS provider, filter for zones of this type (optional, options: public, private)").Default(defaultConfig.AWSZoneType).EnumVar(&cfg.AWSZoneType, "", "public", "private") diff --git a/provider/designate.go b/provider/designate.go new file mode 100644 index 0000000000..4f4a7a50b6 --- /dev/null +++ b/provider/designate.go @@ -0,0 +1,417 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import ( + "fmt" + "os" + "strings" + + log "github.com/Sirupsen/logrus" + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack" + "github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets" + "github.com/gophercloud/gophercloud/openstack/dns/v2/zones" + "github.com/gophercloud/gophercloud/pagination" + + "github.com/kubernetes-incubator/external-dns/endpoint" + "github.com/kubernetes-incubator/external-dns/plan" +) + +const ( + // ID of the RecordSet from which endpoint was created + designateRecordSetID = "designate-recordset-id" + // Zone ID of the RecordSet + designateZoneID = "designate-record-id" + + // Initial records values of the RecordSet. This label is required in order not to loose records that haven't + // changed where there are several targets per domain and only some of them changed. + // Values are joined by zero-byte to in order to get a single string + designateOriginalRecords = "designate-original-records" +) + +// interface between provider and OpenStack DNS API +type designateClientInterface interface { + // ForEachZone calls handler for each zone managed by the Designate + ForEachZone(handler func(zone *zones.Zone) error) error + + // ForEachRecordSet calls handler for each recordset in the given DNS zone + ForEachRecordSet(zoneID string, handler func(recordSet *recordsets.RecordSet) error) error + + // CreateRecordSet creates recordset in the given DNS zone + CreateRecordSet(zoneID string, opts recordsets.CreateOpts) (string, error) + + // UpdateRecordSet updates recordset in the given DNS zone + UpdateRecordSet(zoneID, recordSetID string, opts recordsets.UpdateOpts) error + + // DeleteRecordSet deletes recordset in the given DNS zone + DeleteRecordSet(zoneID, recordSetID string) error +} + +// implementation of the designateClientInterface +type designateClient struct { + serviceClient *gophercloud.ServiceClient +} + +// factory function for the designateClientInterface +func newDesignateClient() (designateClientInterface, error) { + serviceClient, err := createDesignateServiceClient() + if err != nil { + return nil, err + } + return &designateClient{serviceClient}, nil +} + +// copies environment variables to new names without overwriting existing values +func remapEnv(mapping map[string]string) { + for k, v := range mapping { + currentVal := os.Getenv(k) + newVal := os.Getenv(v) + if currentVal == "" && newVal != "" { + os.Setenv(k, newVal) + } + } +} + +// returns OpenStack Keystone authentication settings by obtaining values from standard environment variables. +// also fixes incompatibilities between gophercloud implementation and *-stackrc files that can be downloaded +// from OpenStack dashboard in latest versions +func getAuthSettings() (gophercloud.AuthOptions, error) { + remapEnv(map[string]string{ + "OS_TENANT_NAME": "OS_PROJECT_NAME", + "OS_TENANT_ID": "OS_PROJECT_ID", + "OS_DOMAIN_NAME": "OS_USER_DOMAIN_NAME", + "OS_DOMAIN_ID": "OS_USER_DOMAIN_ID", + }) + + opts, err := openstack.AuthOptionsFromEnv() + if err != nil { + return gophercloud.AuthOptions{}, err + } + opts.AllowReauth = true + if !strings.HasSuffix(opts.IdentityEndpoint, "/") { + opts.IdentityEndpoint += "/" + } + if !strings.HasSuffix(opts.IdentityEndpoint, "/v2.0/") && !strings.HasSuffix(opts.IdentityEndpoint, "/v3/") { + opts.IdentityEndpoint += "v2.0/" + } + return opts, nil +} + +// authenticate in OpenStack and obtain Designate service endpoint +func createDesignateServiceClient() (*gophercloud.ServiceClient, error) { + opts, err := getAuthSettings() + if err != nil { + return nil, err + } + log.Infof("Using OpenStack Keystone at %s", opts.IdentityEndpoint) + authProvider, err := openstack.AuthenticatedClient(opts) + if err != nil { + return nil, err + } + + eo := gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + } + + client, err := openstack.NewDNSV2(authProvider, eo) + if err != nil { + return nil, err + } + log.Infof("Found OpenStack Designate service at %s", client.Endpoint) + return client, nil +} + +// ForEachZone calls handler for each zone managed by the Designate +func (c designateClient) ForEachZone(handler func(zone *zones.Zone) error) error { + pager := zones.List(c.serviceClient, zones.ListOpts{}) + return pager.EachPage( + func(page pagination.Page) (bool, error) { + list, err := zones.ExtractZones(page) + if err != nil { + return false, err + } + for _, zone := range list { + err := handler(&zone) + if err != nil { + return false, err + } + } + return true, nil + }, + ) +} + +// ForEachRecordSet calls handler for each recordset in the given DNS zone +func (c designateClient) ForEachRecordSet(zoneID string, handler func(recordSet *recordsets.RecordSet) error) error { + pager := recordsets.ListByZone(c.serviceClient, zoneID, recordsets.ListOpts{}) + return pager.EachPage( + func(page pagination.Page) (bool, error) { + list, err := recordsets.ExtractRecordSets(page) + if err != nil { + return false, err + } + for _, recordSet := range list { + err := handler(&recordSet) + if err != nil { + return false, err + } + } + return true, nil + }, + ) +} + +// CreateRecordSet creates recordset in the given DNS zone +func (c designateClient) CreateRecordSet(zoneID string, opts recordsets.CreateOpts) (string, error) { + r, err := recordsets.Create(c.serviceClient, zoneID, opts).Extract() + if err != nil { + return "", err + } + return r.ID, nil +} + +// UpdateRecordSet updates recordset in the given DNS zone +func (c designateClient) UpdateRecordSet(zoneID, recordSetID string, opts recordsets.UpdateOpts) error { + _, err := recordsets.Update(c.serviceClient, zoneID, recordSetID, opts).Extract() + return err +} + +// DeleteRecordSet deletes recordset in the given DNS zone +func (c designateClient) DeleteRecordSet(zoneID, recordSetID string) error { + return recordsets.Delete(c.serviceClient, zoneID, recordSetID).ExtractErr() +} + +// designate provider type +type designateProvider struct { + client designateClientInterface + + // only consider hosted zones managing domains ending in this suffix + domainFilter DomainFilter + dryRun bool +} + +// NewDesignateProvider is a factory function for OpenStack designate providers +func NewDesignateProvider(domainFilter DomainFilter, dryRun bool) (Provider, error) { + client, err := newDesignateClient() + if err != nil { + return nil, err + } + return &designateProvider{ + client: client, + domainFilter: domainFilter, + dryRun: dryRun, + }, nil +} + +// converts domain name to FQDN +func canonicalizeDomainName(domain string) string { + if !strings.HasSuffix(domain, ".") { + domain += "." + } + return strings.ToLower(domain) +} + +// returns ZoneID -> ZoneName mapping for zones that are managed by the Designate and match domain filter +func (p designateProvider) getZones() (map[string]string, error) { + result := map[string]string{} + + err := p.client.ForEachZone( + func(zone *zones.Zone) error { + if zone.Type != "" && strings.ToUpper(zone.Type) != "PRIMARY" || zone.Status != "ACTIVE" { + return nil + } + + zoneName := canonicalizeDomainName(zone.Name) + if !p.domainFilter.Match(zoneName) { + return nil + } + result[zone.ID] = zoneName + return nil + }, + ) + + return result, err +} + +// finds best suitable DNS zone for the hostname +func (p designateProvider) getHostZoneID(hostname string, managedZones map[string]string) (string, error) { + longestZoneLength := 0 + resultID := "" + + for zoneID, zoneName := range managedZones { + if !strings.HasSuffix(hostname, zoneName) { + continue + } + ln := len(zoneName) + if ln > longestZoneLength { + resultID = zoneID + longestZoneLength = ln + } + } + + return resultID, nil +} + +// Records returns the list of records. +func (p designateProvider) Records() ([]*endpoint.Endpoint, error) { + var result []*endpoint.Endpoint + managedZones, err := p.getZones() + if err != nil { + return nil, err + } + for zoneID := range managedZones { + err = p.client.ForEachRecordSet(zoneID, + func(recordSet *recordsets.RecordSet) error { + if recordSet.Type != endpoint.RecordTypeA && recordSet.Type != endpoint.RecordTypeTXT && recordSet.Type != endpoint.RecordTypeCNAME { + return nil + } + for _, record := range recordSet.Records { + ep := endpoint.NewEndpoint(recordSet.Name, record, recordSet.Type) + ep.Labels[designateRecordSetID] = recordSet.ID + ep.Labels[designateZoneID] = recordSet.ZoneID + ep.Labels[designateOriginalRecords] = strings.Join(recordSet.Records, "\000") + result = append(result, ep) + } + return nil + }, + ) + if err != nil { + return nil, err + } + } + + return result, nil +} + +// temporary structure to hold recordset parameters so that we could aggregate endpoints into recordsets +type recordSet struct { + dnsName string + recordType string + zoneID string + recordSetID string + names map[string]bool +} + +// adds endpoint into recordset aggregation, loading original values from endpoint labels first +func addEndpoint(ep *endpoint.Endpoint, recordSets map[string]*recordSet, delete bool) { + key := fmt.Sprintf("%s/%s", ep.DNSName, ep.RecordType) + rs := recordSets[key] + if rs == nil { + rs = &recordSet{ + dnsName: canonicalizeDomainName(ep.DNSName), + recordType: ep.RecordType, + names: make(map[string]bool), + } + } + if rs.zoneID == "" { + rs.zoneID = ep.Labels[designateZoneID] + } + if rs.recordSetID == "" { + rs.recordSetID = ep.Labels[designateRecordSetID] + } + for _, rec := range strings.Split(ep.Labels[designateOriginalRecords], "\000") { + if _, ok := rs.names[rec]; !ok && rec != "" { + rs.names[rec] = true + } + } + target := ep.Target + if ep.RecordType == endpoint.RecordTypeCNAME { + target = canonicalizeDomainName(target) + } + rs.names[target] = !delete + recordSets[key] = rs +} + +// ApplyChanges applies a given set of changes in a given zone. +func (p designateProvider) ApplyChanges(changes *plan.Changes) error { + managedZones, err := p.getZones() + if err != nil { + return err + } + recordSets := map[string]*recordSet{} + for _, ep := range changes.Create { + addEndpoint(ep, recordSets, false) + } + for _, ep := range changes.UpdateNew { + addEndpoint(ep, recordSets, false) + } + for _, ep := range changes.UpdateOld { + addEndpoint(ep, recordSets, true) + } + for _, ep := range changes.Delete { + addEndpoint(ep, recordSets, true) + } + for _, rs := range recordSets { + if err2 := p.upsertRecordSet(rs, managedZones); err == nil { + err = err2 + } + } + return err +} + +// apply recordset changes by inserting/updating/deleting recordsets +func (p designateProvider) upsertRecordSet(rs *recordSet, managedZones map[string]string) error { + if rs.zoneID == "" { + var err error + rs.zoneID, err = p.getHostZoneID(rs.dnsName, managedZones) + if err != nil { + return err + } + if rs.zoneID == "" { + log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected ", rs.dnsName) + return nil + } + } + var records []string + for rec, v := range rs.names { + if v { + records = append(records, rec) + } + } + if rs.recordSetID == "" && records == nil { + return nil + } + if rs.recordSetID == "" { + opts := recordsets.CreateOpts{ + Name: rs.dnsName, + Type: rs.recordType, + Records: records, + } + log.Infof("Creating records: %s/%s: %s", rs.dnsName, rs.recordType, strings.Join(records, ",")) + if p.dryRun { + return nil + } + _, err := p.client.CreateRecordSet(rs.zoneID, opts) + return err + } else if len(records) == 0 { + log.Infof("Deleting records for %s/%s", rs.dnsName, rs.recordType) + if p.dryRun { + return nil + } + return p.client.DeleteRecordSet(rs.zoneID, rs.recordSetID) + } else { + opts := recordsets.UpdateOpts{ + Records: records, + } + log.Infof("Updating records: %s/%s: %s", rs.dnsName, rs.recordType, strings.Join(records, ",")) + if p.dryRun { + return nil + } + return p.client.UpdateRecordSet(rs.zoneID, rs.recordSetID, opts) + } +} diff --git a/provider/designate_test.go b/provider/designate_test.go new file mode 100644 index 0000000000..fe1579ff6c --- /dev/null +++ b/provider/designate_test.go @@ -0,0 +1,519 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import ( + "fmt" + "reflect" + "sort" + "sync/atomic" + "testing" + + "github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets" + "github.com/gophercloud/gophercloud/openstack/dns/v2/zones" + "github.com/kubernetes-incubator/external-dns/endpoint" + "github.com/kubernetes-incubator/external-dns/plan" +) + +var lastGeneratedDesignateID int32 + +func generateDesignateID() string { + return fmt.Sprintf("id-%d", atomic.AddInt32(&lastGeneratedDesignateID, 1)) +} + +type fakeDesignateClient struct { + managedZones map[string]*struct { + zone *zones.Zone + recordSets map[string]*recordsets.RecordSet + } +} + +func (c fakeDesignateClient) AddZone(zone zones.Zone) string { + if zone.ID == "" { + zone.ID = zone.Name + } + c.managedZones[zone.ID] = &struct { + zone *zones.Zone + recordSets map[string]*recordsets.RecordSet + }{ + zone: &zone, + recordSets: make(map[string]*recordsets.RecordSet), + } + return zone.ID +} + +func (c fakeDesignateClient) ForEachZone(handler func(zone *zones.Zone) error) error { + for _, zone := range c.managedZones { + if err := handler(zone.zone); err != nil { + return err + } + } + return nil +} + +func (c fakeDesignateClient) ForEachRecordSet(zoneID string, handler func(recordSet *recordsets.RecordSet) error) error { + zone := c.managedZones[zoneID] + if zone == nil { + return fmt.Errorf("unknown zone %s", zoneID) + } + for _, recordSet := range zone.recordSets { + if err := handler(recordSet); err != nil { + return err + } + } + return nil +} + +func (c fakeDesignateClient) CreateRecordSet(zoneID string, opts recordsets.CreateOpts) (string, error) { + zone := c.managedZones[zoneID] + if zone == nil { + return "", fmt.Errorf("unknown zone %s", zoneID) + } + rs := &recordsets.RecordSet{ + ID: generateDesignateID(), + ZoneID: zoneID, + Name: opts.Name, + Description: opts.Description, + Records: opts.Records, + TTL: opts.TTL, + Type: opts.Type, + } + zone.recordSets[rs.ID] = rs + return rs.ID, nil +} + +func (c fakeDesignateClient) UpdateRecordSet(zoneID, recordSetID string, opts recordsets.UpdateOpts) error { + zone := c.managedZones[zoneID] + if zone == nil { + return fmt.Errorf("unknown zone %s", zoneID) + } + rs := zone.recordSets[recordSetID] + if rs == nil { + return fmt.Errorf("unknown record-set %s", recordSetID) + } + rs.Description = opts.Description + rs.TTL = opts.TTL + rs.Records = opts.Records + return nil +} + +func (c fakeDesignateClient) DeleteRecordSet(zoneID, recordSetID string) error { + zone := c.managedZones[zoneID] + if zone == nil { + return fmt.Errorf("unknown zone %s", zoneID) + } + delete(zone.recordSets, recordSetID) + return nil +} + +func (c fakeDesignateClient) ToProvider() Provider { + return &designateProvider{client: c} +} + +func newFakeDesignateClient() *fakeDesignateClient { + return &fakeDesignateClient{ + make(map[string]*struct { + zone *zones.Zone + recordSets map[string]*recordsets.RecordSet + }), + } +} + +func TestDesignateRecords(t *testing.T) { + client := newFakeDesignateClient() + + zone1ID := client.AddZone(zones.Zone{ + Name: "example.com.", + Type: "PRIMARY", + Status: "ACTIVE", + }) + rs11ID, _ := client.CreateRecordSet(zone1ID, recordsets.CreateOpts{ + Name: "www.example.com.", + Type: endpoint.RecordTypeA, + Records: []string{"10.1.1.1"}, + }) + rs12ID, _ := client.CreateRecordSet(zone1ID, recordsets.CreateOpts{ + Name: "www.example.com.", + Type: endpoint.RecordTypeTXT, + Records: []string{"text1"}, + }) + client.CreateRecordSet(zone1ID, recordsets.CreateOpts{ + Name: "xxx.example.com.", + Type: "SRV", + Records: []string{"http://test.com:1234"}, + }) + rs14ID, _ := client.CreateRecordSet(zone1ID, recordsets.CreateOpts{ + Name: "ftp.example.com.", + Type: endpoint.RecordTypeA, + Records: []string{"10.1.1.2"}, + }) + + zone2ID := client.AddZone(zones.Zone{ + Name: "test.net.", + Type: "PRIMARY", + Status: "ACTIVE", + }) + rs21ID, _ := client.CreateRecordSet(zone2ID, recordsets.CreateOpts{ + Name: "srv.test.net.", + Type: endpoint.RecordTypeA, + Records: []string{"10.2.1.1", "10.2.1.2"}, + }) + rs22ID, _ := client.CreateRecordSet(zone2ID, recordsets.CreateOpts{ + Name: "db.test.net.", + Type: endpoint.RecordTypeCNAME, + Records: []string{"sql.test.net."}, + }) + expected := []*endpoint.Endpoint{ + { + DNSName: "www.example.com", + RecordType: endpoint.RecordTypeA, + Target: "10.1.1.1", + Labels: map[string]string{ + designateRecordSetID: rs11ID, + designateZoneID: zone1ID, + designateOriginalRecords: "10.1.1.1", + }, + }, + { + DNSName: "www.example.com", + RecordType: endpoint.RecordTypeTXT, + Target: "text1", + Labels: map[string]string{ + designateRecordSetID: rs12ID, + designateZoneID: zone1ID, + designateOriginalRecords: "text1", + }, + }, + { + DNSName: "ftp.example.com", + RecordType: endpoint.RecordTypeA, + Target: "10.1.1.2", + Labels: map[string]string{ + designateRecordSetID: rs14ID, + designateZoneID: zone1ID, + designateOriginalRecords: "10.1.1.2", + }, + }, + { + DNSName: "srv.test.net", + RecordType: endpoint.RecordTypeA, + Target: "10.2.1.1", + Labels: map[string]string{ + designateRecordSetID: rs21ID, + designateZoneID: zone2ID, + designateOriginalRecords: "10.2.1.1\00010.2.1.2", + }, + }, + { + DNSName: "srv.test.net", + RecordType: endpoint.RecordTypeA, + Target: "10.2.1.2", + Labels: map[string]string{ + designateRecordSetID: rs21ID, + designateZoneID: zone2ID, + designateOriginalRecords: "10.2.1.1\00010.2.1.2", + }, + }, + { + DNSName: "db.test.net", + RecordType: endpoint.RecordTypeCNAME, + Target: "sql.test.net", + Labels: map[string]string{ + designateRecordSetID: rs22ID, + designateZoneID: zone2ID, + designateOriginalRecords: "sql.test.net.", + }, + }, + } + + endpoints, err := client.ToProvider().Records() + if err != nil { + t.Fatal(err) + } +out: + for _, ep := range endpoints { + for i, ex := range expected { + if reflect.DeepEqual(ep, ex) { + expected = append(expected[:i], expected[i+1:]...) + continue out + } + } + t.Errorf("unexpected endpoint %s/%s -> %s", ep.DNSName, ep.RecordType, ep.Target) + } + if len(expected) != 0 { + t.Errorf("not all expected endpoints were returned. Remained: %v", expected) + } +} + +func TestDesignateCreateRecords(t *testing.T) { + client := newFakeDesignateClient() + testDesignateCreateRecords(t, client) +} + +func testDesignateCreateRecords(t *testing.T, client *fakeDesignateClient) []*recordsets.RecordSet { + + for i, zoneName := range []string{"example.com.", "test.net."} { + client.AddZone(zones.Zone{ + ID: fmt.Sprintf("zone-%d", i+1), + Name: zoneName, + Type: "PRIMARY", + Status: "ACTIVE", + }) + } + endpoints := []*endpoint.Endpoint{ + { + DNSName: "www.example.com", + RecordType: endpoint.RecordTypeA, + Target: "10.1.1.1", + Labels: map[string]string{}, + }, + { + DNSName: "www.example.com", + RecordType: endpoint.RecordTypeTXT, + Target: "text1", + Labels: map[string]string{}, + }, + { + DNSName: "ftp.example.com", + RecordType: endpoint.RecordTypeA, + Target: "10.1.1.2", + Labels: map[string]string{}, + }, + { + DNSName: "srv.test.net", + RecordType: endpoint.RecordTypeA, + Target: "10.2.1.1", + Labels: map[string]string{}, + }, + { + DNSName: "srv.test.net", + RecordType: endpoint.RecordTypeA, + Target: "10.2.1.2", + Labels: map[string]string{}, + }, + { + DNSName: "db.test.net", + RecordType: endpoint.RecordTypeCNAME, + Target: "sql.test.net", + Labels: map[string]string{}, + }, + } + expected := []*recordsets.RecordSet{ + { + Name: "www.example.com.", + Type: endpoint.RecordTypeA, + Records: []string{"10.1.1.1"}, + ZoneID: "zone-1", + }, + { + Name: "www.example.com.", + Type: endpoint.RecordTypeTXT, + Records: []string{"text1"}, + ZoneID: "zone-1", + }, + { + Name: "ftp.example.com.", + Type: endpoint.RecordTypeA, + Records: []string{"10.1.1.2"}, + ZoneID: "zone-1", + }, + { + Name: "srv.test.net.", + Type: endpoint.RecordTypeA, + Records: []string{"10.2.1.1", "10.2.1.2"}, + ZoneID: "zone-2", + }, + { + Name: "db.test.net.", + Type: endpoint.RecordTypeCNAME, + Records: []string{"sql.test.net."}, + ZoneID: "zone-2", + }, + } + expectedCopy := make([]*recordsets.RecordSet, len(expected)) + copy(expectedCopy, expected) + + err := client.ToProvider().ApplyChanges(&plan.Changes{Create: endpoints}) + if err != nil { + t.Fatal(err) + } + + client.ForEachZone(func(zone *zones.Zone) error { + client.ForEachRecordSet(zone.ID, func(recordSet *recordsets.RecordSet) error { + id := recordSet.ID + recordSet.ID = "" + for i, ex := range expected { + sort.Strings(recordSet.Records) + if reflect.DeepEqual(ex, recordSet) { + ex.ID = id + recordSet.ID = id + expected = append(expected[:i], expected[i+1:]...) + return nil + } + } + t.Errorf("unexpected record-set %s/%s -> %v", recordSet.Name, recordSet.Type, recordSet.Records) + return nil + }) + return nil + }) + + if len(expected) != 0 { + t.Errorf("not all expected record-sets were created. Remained: %v", expected) + } + return expectedCopy +} + +func TestDesignateUpdateRecords(t *testing.T) { + client := newFakeDesignateClient() + testDesignateUpdateRecords(t, client) +} + +func testDesignateUpdateRecords(t *testing.T, client *fakeDesignateClient) []*recordsets.RecordSet { + expected := testDesignateCreateRecords(t, client) + + updatesOld := []*endpoint.Endpoint{ + { + DNSName: "ftp.example.com", + RecordType: endpoint.RecordTypeA, + Target: "10.1.1.2", + Labels: map[string]string{ + designateZoneID: "zone-1", + designateRecordSetID: expected[2].ID, + designateOriginalRecords: "10.1.1.2", + }, + }, + { + DNSName: "srv.test.net.", + RecordType: endpoint.RecordTypeA, + Target: "10.2.1.2", + Labels: map[string]string{ + designateZoneID: "zone-2", + designateRecordSetID: expected[3].ID, + designateOriginalRecords: "10.2.1.1\00010.2.1.2", + }, + }, + } + updatesNew := []*endpoint.Endpoint{ + { + DNSName: "ftp.example.com", + RecordType: endpoint.RecordTypeA, + Target: "10.3.3.1", + Labels: map[string]string{ + designateZoneID: "zone-1", + designateRecordSetID: expected[2].ID, + designateOriginalRecords: "10.1.1.2", + }, + }, + { + DNSName: "srv.test.net.", + RecordType: endpoint.RecordTypeA, + Target: "10.3.3.2", + Labels: map[string]string{ + designateZoneID: "zone-2", + designateRecordSetID: expected[3].ID, + designateOriginalRecords: "10.2.1.1\00010.2.1.2", + }, + }, + } + expectedCopy := make([]*recordsets.RecordSet, len(expected)) + copy(expectedCopy, expected) + + expected[2].Records = []string{"10.3.3.1"} + expected[3].Records = []string{"10.2.1.1", "10.3.3.2"} + + err := client.ToProvider().ApplyChanges(&plan.Changes{UpdateOld: updatesOld, UpdateNew: updatesNew}) + if err != nil { + t.Fatal(err) + } + + client.ForEachZone(func(zone *zones.Zone) error { + client.ForEachRecordSet(zone.ID, func(recordSet *recordsets.RecordSet) error { + for i, ex := range expected { + sort.Strings(recordSet.Records) + if reflect.DeepEqual(ex, recordSet) { + expected = append(expected[:i], expected[i+1:]...) + return nil + } + } + t.Errorf("unexpected record-set %s/%s -> %v", recordSet.Name, recordSet.Type, recordSet.Records) + return nil + }) + return nil + }) + + if len(expected) != 0 { + t.Errorf("not all expected record-sets were updated. Remained: %v", expected) + } + return expectedCopy +} + +func TestDesignateDeleteRecords(t *testing.T) { + client := newFakeDesignateClient() + testDesignateDeleteRecords(t, client) +} + +func testDesignateDeleteRecords(t *testing.T, client *fakeDesignateClient) { + expected := testDesignateUpdateRecords(t, client) + deletes := []*endpoint.Endpoint{ + { + DNSName: "www.example.com.", + RecordType: endpoint.RecordTypeA, + Target: "10.1.1.1", + Labels: map[string]string{ + designateZoneID: "zone-1", + designateRecordSetID: expected[0].ID, + designateOriginalRecords: "10.1.1.1", + }, + }, + { + DNSName: "srv.test.net.", + RecordType: endpoint.RecordTypeA, + Target: "10.2.1.1", + Labels: map[string]string{ + designateZoneID: "zone-2", + designateRecordSetID: expected[3].ID, + designateOriginalRecords: "10.2.1.1\00010.3.3.2", + }, + }, + } + expected[3].Records = []string{"10.3.3.2"} + expected = expected[1:] + + err := client.ToProvider().ApplyChanges(&plan.Changes{Delete: deletes}) + if err != nil { + t.Fatal(err) + } + + client.ForEachZone(func(zone *zones.Zone) error { + client.ForEachRecordSet(zone.ID, func(recordSet *recordsets.RecordSet) error { + for i, ex := range expected { + sort.Strings(recordSet.Records) + if reflect.DeepEqual(ex, recordSet) { + expected = append(expected[:i], expected[i+1:]...) + return nil + } + } + t.Errorf("unexpected record-set %s/%s -> %v", recordSet.Name, recordSet.Type, recordSet.Records) + return nil + }) + return nil + }) + + if len(expected) != 0 { + t.Errorf("not all expected record-sets were deleted. Remained: %v", expected) + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/.github/CONTRIBUTING.md b/vendor/github.com/gophercloud/gophercloud/.github/CONTRIBUTING.md new file mode 100644 index 0000000000..a9a0c8a765 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/.github/CONTRIBUTING.md @@ -0,0 +1,235 @@ +# Contributing to Gophercloud + +- [Getting started](#getting-started) +- [Tests](#tests) +- [Style guide](#basic-style-guide) +- [3 ways to get involved](#5-ways-to-get-involved) + +## Setting up your git workspace + +As a contributor you will need to setup your workspace in a slightly different +way than just downloading it. Here are the basic installation instructions: + +1. Configure your `$GOPATH` and run `go get` as described in the main +[README](/README.md#how-to-install) but add `-tags "fixtures acceptance"` to +get dependencies for unit and acceptance tests. + + ```bash + go get -tags "fixtures acceptance" github.com/gophercloud/gophercloud + ``` + +2. Move into the directory that houses your local repository: + + ```bash + cd ${GOPATH}/src/github.com/gophercloud/gophercloud + ``` + +3. Fork the `gophercloud/gophercloud` repository and update your remote refs. You +will need to rename the `origin` remote branch to `upstream`, and add your +fork as `origin` instead: + + ```bash + git remote rename origin upstream + git remote add origin git@github.com:/gophercloud.git + ``` + +4. Checkout the latest development branch: + + ```bash + git checkout master + ``` + +5. If you're working on something (discussed more in detail below), you will +need to checkout a new feature branch: + + ```bash + git checkout -b my-new-feature + ``` + +Another thing to bear in mind is that you will need to add a few extra +environment variables for acceptance tests - this is documented in our +[acceptance tests readme](/acceptance). + +## Tests + +When working on a new or existing feature, testing will be the backbone of your +work since it helps uncover and prevent regressions in the codebase. There are +two types of test we use in Gophercloud: unit tests and acceptance tests, which +are both described below. + +### Unit tests + +Unit tests are the fine-grained tests that establish and ensure the behavior +of individual units of functionality. We usually test on an +operation-by-operation basis (an operation typically being an API action) with +the use of mocking to set up explicit expectations. Each operation will set up +its HTTP response expectation, and then test how the system responds when fed +this controlled, pre-determined input. + +To make life easier, we've introduced a bunch of test helpers to simplify the +process of testing expectations with assertions: + +```go +import ( + "testing" + + "github.com/gophercloud/gophercloud/testhelper" +) + +func TestSomething(t *testing.T) { + result, err := Operation() + + testhelper.AssertEquals(t, "foo", result.Bar) + testhelper.AssertNoErr(t, err) +} + +func TestSomethingElse(t *testing.T) { + testhelper.CheckEquals(t, "expected", "actual") +} +``` + +`AssertEquals` and `AssertNoErr` will throw a fatal error if a value does not +match an expected value or if an error has been declared, respectively. You can +also use `CheckEquals` and `CheckNoErr` for the same purpose; the only difference +being that `t.Errorf` is raised rather than `t.Fatalf`. + +Here is a truncated example of mocked HTTP responses: + +```go +import ( + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" + "github.com/gophercloud/gophercloud/openstack/networking/v2/networks" +) + +func TestGet(t *testing.T) { + // Setup the HTTP request multiplexer and server + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/networks/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + // Test we're using the correct HTTP method + th.TestMethod(t, r, "GET") + + // Test we're setting the auth token + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + // Set the appropriate headers for our mocked response + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + // Set the HTTP body + fmt.Fprintf(w, ` +{ + "network": { + "status": "ACTIVE", + "name": "private-network", + "admin_state_up": true, + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "shared": true, + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22" + } +} + `) + }) + + // Call our API operation + network, err := networks.Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract() + + // Assert no errors and equality + th.AssertNoErr(t, err) + th.AssertEquals(t, n.Status, "ACTIVE") +} +``` + +### Acceptance tests + +As we've already mentioned, unit tests have a very narrow and confined focus - +they test small units of behavior. Acceptance tests on the other hand have a +far larger scope: they are fully functional tests that test the entire API of a +service in one fell swoop. They don't care about unit isolation or mocking +expectations, they instead do a full run-through and consequently test how the +entire system _integrates_ together. When an API satisfies expectations, it +proves by default that the requirements for a contract have been met. + +Please be aware that acceptance tests will hit a live API - and may incur +service charges from your provider. Although most tests handle their own +teardown procedures, it is always worth manually checking that resources are +deleted after the test suite finishes. + +### Running tests + +To run all tests: + +```bash +go test -tags fixtures ./... +``` + +To run all tests with verbose output: + +```bash +go test -v -tags fixtures ./... +``` + +To run tests that match certain [build tags](): + +```bash +go test -tags "fixtures foo bar" ./... +``` + +To run tests for a particular sub-package: + +```bash +cd ./path/to/package && go test -tags fixtures . +``` + +## Style guide + +See [here](/STYLEGUIDE.md) + +## 3 ways to get involved + +There are five main ways you can get involved in our open-source project, and +each is described briefly below. Once you've made up your mind and decided on +your fix, you will need to follow the same basic steps that all submissions are +required to adhere to: + +1. [fork](https://help.github.com/articles/fork-a-repo/) the `gophercloud/gophercloud` repository +2. checkout a [new branch](https://github.com/Kunena/Kunena-Forum/wiki/Create-a-new-branch-with-git-and-manage-branches) +3. submit your branch as a [pull request](https://help.github.com/articles/creating-a-pull-request/) + +### 1. Fixing bugs + +If you want to start fixing open bugs, we'd really appreciate that! Bug fixing +is central to any project. The best way to get started is by heading to our +[bug tracker](https://github.com/gophercloud/gophercloud/issues) and finding open +bugs that you think nobody is working on. It might be useful to comment on the +thread to see the current state of the issue and if anybody has made any +breakthroughs on it so far. + +### 2. Improving documentation +The best source of documentation is on [godoc.org](http://godoc.org). It is +automatically generated from the source code. + +If you feel that a certain section could be improved - whether it's to clarify +ambiguity, correct a technical mistake, or to fix a grammatical error - please +feel entitled to do so! We welcome doc pull requests with the same childlike +enthusiasm as any other contribution! + +### 3. Working on a new feature + +If you've found something we've left out, definitely feel free to start work on +introducing that feature. It's always useful to open an issue or submit a pull +request early on to indicate your intent to a core contributor - this enables +quick/early feedback and can help steer you in the right direction by avoiding +known issues. It might also help you avoid losing time implementing something +that might not ever work. One tip is to prefix your Pull Request issue title +with [wip] - then people know it's a work in progress. + +You must ensure that all of your work is well tested - both in terms of unit +and acceptance tests. Untested code will not be merged because it introduces +too much of a risk to end-users. + +Happy hacking! diff --git a/vendor/github.com/gophercloud/gophercloud/.github/ISSUE_TEMPLATE b/vendor/github.com/gophercloud/gophercloud/.github/ISSUE_TEMPLATE new file mode 100644 index 0000000000..1451b81b4b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/.github/ISSUE_TEMPLATE @@ -0,0 +1 @@ +Before starting a PR, please read the [style guide](https://github.com/gophercloud/gophercloud/blob/master/STYLEGUIDE.md). diff --git a/vendor/github.com/gophercloud/gophercloud/.github/PULL_REQUEST_TEMPLATE b/vendor/github.com/gophercloud/gophercloud/.github/PULL_REQUEST_TEMPLATE new file mode 100644 index 0000000000..43aafa02f8 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/.github/PULL_REQUEST_TEMPLATE @@ -0,0 +1,9 @@ +Prior to a PR being reviewed, there needs to be a Github issue that the PR +addresses. Replace the brackets and text below with that issue number. + +For #[PUT ISSUE NUMBER HERE] + +Links to the line numbers/files in the OpenStack source code that support the +code in this PR: + +[PUT URLS HERE] diff --git a/vendor/github.com/gophercloud/gophercloud/.gitignore b/vendor/github.com/gophercloud/gophercloud/.gitignore new file mode 100644 index 0000000000..ead84456eb --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/.gitignore @@ -0,0 +1 @@ +**/*.swp diff --git a/vendor/github.com/gophercloud/gophercloud/.travis.yml b/vendor/github.com/gophercloud/gophercloud/.travis.yml new file mode 100644 index 0000000000..5d1486901d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/.travis.yml @@ -0,0 +1,19 @@ +language: go +sudo: false +install: +- go get golang.org/x/crypto/ssh +- go get -v -tags 'fixtures acceptance' ./... +- go get github.com/wadey/gocovmerge +- go get github.com/mattn/goveralls +- go get golang.org/x/tools/cmd/goimports +go: +- 1.8 +- tip +env: + global: + - secure: "xSQsAG5wlL9emjbCdxzz/hYQsSpJ/bABO1kkbwMSISVcJ3Nk0u4ywF+LS4bgeOnwPfmFvNTOqVDu3RwEvMeWXSI76t1piCPcObutb2faKLVD/hLoAS76gYX+Z8yGWGHrSB7Do5vTPj1ERe2UljdrnsSeOXzoDwFxYRaZLX4bBOB4AyoGvRniil5QXPATiA1tsWX1VMicj8a4F8X+xeESzjt1Q5Iy31e7vkptu71bhvXCaoo5QhYwT+pLR9dN0S1b7Ro0KVvkRefmr1lUOSYd2e74h6Lc34tC1h3uYZCS4h47t7v5cOXvMNxinEj2C51RvbjvZI1RLVdkuAEJD1Iz4+Ote46nXbZ//6XRZMZz/YxQ13l7ux1PFjgEB6HAapmF5Xd8PRsgeTU9LRJxpiTJ3P5QJ3leS1va8qnziM5kYipj/Rn+V8g2ad/rgkRox9LSiR9VYZD2Pe45YCb1mTKSl2aIJnV7nkOqsShY5LNB4JZSg7xIffA+9YVDktw8dJlATjZqt7WvJJ49g6A61mIUV4C15q2JPGKTkZzDiG81NtmS7hFa7k0yaE2ELgYocbcuyUcAahhxntYTC0i23nJmEHVNiZmBO3u7EgpWe4KGVfumU+lt12tIn5b3dZRBBUk3QakKKozSK1QPHGpk/AZGrhu7H6l8to6IICKWtDcyMPQ=" +script: +- ./script/coverage +- ./script/format +after_success: +- $HOME/gopath/bin/goveralls -service=travis-ci -coverprofile=cover.out diff --git a/vendor/github.com/gophercloud/gophercloud/CHANGELOG.md b/vendor/github.com/gophercloud/gophercloud/CHANGELOG.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vendor/github.com/gophercloud/gophercloud/FAQ.md b/vendor/github.com/gophercloud/gophercloud/FAQ.md new file mode 100644 index 0000000000..88a366a288 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/FAQ.md @@ -0,0 +1,148 @@ +# Tips + +## Implementing default logging and re-authentication attempts + +You can implement custom logging and/or limit re-auth attempts by creating a custom HTTP client +like the following and setting it as the provider client's HTTP Client (via the +`gophercloud.ProviderClient.HTTPClient` field): + +```go +//... + +// LogRoundTripper satisfies the http.RoundTripper interface and is used to +// customize the default Gophercloud RoundTripper to allow for logging. +type LogRoundTripper struct { + rt http.RoundTripper + numReauthAttempts int +} + +// newHTTPClient return a custom HTTP client that allows for logging relevant +// information before and after the HTTP request. +func newHTTPClient() http.Client { + return http.Client{ + Transport: &LogRoundTripper{ + rt: http.DefaultTransport, + }, + } +} + +// RoundTrip performs a round-trip HTTP request and logs relevant information about it. +func (lrt *LogRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) { + glog.Infof("Request URL: %s\n", request.URL) + + response, err := lrt.rt.RoundTrip(request) + if response == nil { + return nil, err + } + + if response.StatusCode == http.StatusUnauthorized { + if lrt.numReauthAttempts == 3 { + return response, fmt.Errorf("Tried to re-authenticate 3 times with no success.") + } + lrt.numReauthAttempts++ + } + + glog.Debugf("Response Status: %s\n", response.Status) + + return response, nil +} + +endpoint := "https://127.0.0.1/auth" +pc := openstack.NewClient(endpoint) +pc.HTTPClient = newHTTPClient() + +//... +``` + + +## Implementing custom objects + +OpenStack request/response objects may differ among variable names or types. + +### Custom request objects + +To pass custom options to a request, implement the desired `OptsBuilder` interface. For +example, to pass in + +```go +type MyCreateServerOpts struct { + Name string + Size int +} +``` + +to `servers.Create`, simply implement the `servers.CreateOptsBuilder` interface: + +```go +func (o MyCreateServeropts) ToServerCreateMap() (map[string]interface{}, error) { + return map[string]interface{}{ + "name": o.Name, + "size": o.Size, + }, nil +} +``` + +create an instance of your custom options object, and pass it to `servers.Create`: + +```go +// ... +myOpts := MyCreateServerOpts{ + Name: "s1", + Size: "100", +} +server, err := servers.Create(computeClient, myOpts).Extract() +// ... +``` + +### Custom response objects + +Some OpenStack services have extensions. Extensions that are supported in Gophercloud can be +combined to create a custom object: + +```go +// ... +type MyVolume struct { + volumes.Volume + tenantattr.VolumeExt +} + +var v struct { + MyVolume `json:"volume"` +} + +err := volumes.Get(client, volID).ExtractInto(&v) +// ... +``` + +## Overriding default `UnmarshalJSON` method + +For some response objects, a field may be a custom type or may be allowed to take on +different types. In these cases, overriding the default `UnmarshalJSON` method may be +necessary. To do this, declare the JSON `struct` field tag as "-" and create an `UnmarshalJSON` +method on the type: + +```go +// ... +type MyVolume struct { + ID string `json: "id"` + TimeCreated time.Time `json: "-"` +} + +func (r *MyVolume) UnmarshalJSON(b []byte) error { + type tmp MyVolume + var s struct { + tmp + TimeCreated gophercloud.JSONRFC3339MilliNoZ `json:"created_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Volume(s.tmp) + + r.TimeCreated = time.Time(s.CreatedAt) + + return err +} +// ... +``` diff --git a/vendor/github.com/gophercloud/gophercloud/LICENSE b/vendor/github.com/gophercloud/gophercloud/LICENSE new file mode 100644 index 0000000000..fbbbc9e4cb --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/LICENSE @@ -0,0 +1,191 @@ +Copyright 2012-2013 Rackspace, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. + +------ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/vendor/github.com/gophercloud/gophercloud/MIGRATING.md b/vendor/github.com/gophercloud/gophercloud/MIGRATING.md new file mode 100644 index 0000000000..aa383c9cc9 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/MIGRATING.md @@ -0,0 +1,32 @@ +# Compute + +## Floating IPs + +* `github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingip` is now `github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips` +* `floatingips.Associate` and `floatingips.Disassociate` have been removed. +* `floatingips.DisassociateOpts` is now required to disassociate a Floating IP. + +## Security Groups + +* `secgroups.AddServerToGroup` is now `secgroups.AddServer`. +* `secgroups.RemoveServerFromGroup` is now `secgroups.RemoveServer`. + +## Servers + +* `servers.Reboot` now requires a `servers.RebootOpts` struct: + + ```golang + rebootOpts := &servers.RebootOpts{ + Type: servers.SoftReboot, + } + res := servers.Reboot(client, server.ID, rebootOpts) + ``` + +# Identity + +## V3 + +### Tokens + +* `Token.ExpiresAt` is now of type `gophercloud.JSONRFC3339Milli` instead of + `time.Time` diff --git a/vendor/github.com/gophercloud/gophercloud/README.md b/vendor/github.com/gophercloud/gophercloud/README.md new file mode 100644 index 0000000000..60ca479de8 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/README.md @@ -0,0 +1,143 @@ +# Gophercloud: an OpenStack SDK for Go +[![Build Status](https://travis-ci.org/gophercloud/gophercloud.svg?branch=master)](https://travis-ci.org/gophercloud/gophercloud) +[![Coverage Status](https://coveralls.io/repos/github/gophercloud/gophercloud/badge.svg?branch=master)](https://coveralls.io/github/gophercloud/gophercloud?branch=master) + +Gophercloud is an OpenStack Go SDK. + +## Useful links + +* [Reference documentation](http://godoc.org/github.com/gophercloud/gophercloud) +* [Effective Go](https://golang.org/doc/effective_go.html) + +## How to install + +Before installing, you need to ensure that your [GOPATH environment variable](https://golang.org/doc/code.html#GOPATH) +is pointing to an appropriate directory where you want to install Gophercloud: + +```bash +mkdir $HOME/go +export GOPATH=$HOME/go +``` + +To protect yourself against changes in your dependencies, we highly recommend choosing a +[dependency management solution](https://github.com/golang/go/wiki/PackageManagementTools) for +your projects, such as [godep](https://github.com/tools/godep). Once this is set up, you can install +Gophercloud as a dependency like so: + +```bash +go get github.com/gophercloud/gophercloud + +# Edit your code to import relevant packages from "github.com/gophercloud/gophercloud" + +godep save ./... +``` + +This will install all the source files you need into a `Godeps/_workspace` directory, which is +referenceable from your own source files when you use the `godep go` command. + +## Getting started + +### Credentials + +Because you'll be hitting an API, you will need to retrieve your OpenStack +credentials and either store them as environment variables or in your local Go +files. The first method is recommended because it decouples credential +information from source code, allowing you to push the latter to your version +control system without any security risk. + +You will need to retrieve the following: + +* username +* password +* a valid Keystone identity URL + +For users that have the OpenStack dashboard installed, there's a shortcut. If +you visit the `project/access_and_security` path in Horizon and click on the +"Download OpenStack RC File" button at the top right hand corner, you will +download a bash file that exports all of your access details to environment +variables. To execute the file, run `source admin-openrc.sh` and you will be +prompted for your password. + +### Authentication + +Once you have access to your credentials, you can begin plugging them into +Gophercloud. The next step is authentication, and this is handled by a base +"Provider" struct. To get one, you can either pass in your credentials +explicitly, or tell Gophercloud to use environment variables: + +```go +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack" + "github.com/gophercloud/gophercloud/openstack/utils" +) + +// Option 1: Pass in the values yourself +opts := gophercloud.AuthOptions{ + IdentityEndpoint: "https://openstack.example.com:5000/v2.0", + Username: "{username}", + Password: "{password}", +} + +// Option 2: Use a utility function to retrieve all your environment variables +opts, err := openstack.AuthOptionsFromEnv() +``` + +Once you have the `opts` variable, you can pass it in and get back a +`ProviderClient` struct: + +```go +provider, err := openstack.AuthenticatedClient(opts) +``` + +The `ProviderClient` is the top-level client that all of your OpenStack services +derive from. The provider contains all of the authentication details that allow +your Go code to access the API - such as the base URL and token ID. + +### Provision a server + +Once we have a base Provider, we inject it as a dependency into each OpenStack +service. In order to work with the Compute API, we need a Compute service +client; which can be created like so: + +```go +client, err := openstack.NewComputeV2(provider, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), +}) +``` + +We then use this `client` for any Compute API operation we want. In our case, +we want to provision a new server - so we invoke the `Create` method and pass +in the flavor ID (hardware specification) and image ID (operating system) we're +interested in: + +```go +import "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + +server, err := servers.Create(client, servers.CreateOpts{ + Name: "My new server!", + FlavorRef: "flavor_id", + ImageRef: "image_id", +}).Extract() +``` + +The above code sample creates a new server with the parameters, and embodies the +new resource in the `server` variable (a +[`servers.Server`](http://godoc.org/github.com/gophercloud/gophercloud) struct). + +## Advanced Usage + +Have a look at the [FAQ](./FAQ.md) for some tips on customizing the way Gophercloud works. + +## Backwards-Compatibility Guarantees + +None. Vendor it and write tests covering the parts you use. + +## Contributing + +See the [contributing guide](./.github/CONTRIBUTING.md). + +## Help and feedback + +If you're struggling with something or have spotted a potential bug, feel free +to submit an issue to our [bug tracker](/issues). diff --git a/vendor/github.com/gophercloud/gophercloud/STYLEGUIDE.md b/vendor/github.com/gophercloud/gophercloud/STYLEGUIDE.md new file mode 100644 index 0000000000..e7531a83d9 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/STYLEGUIDE.md @@ -0,0 +1,74 @@ + +## On Pull Requests + +- Before you start a PR there needs to be a Github issue and a discussion about it + on that issue with a core contributor, even if it's just a 'SGTM'. + +- A PR's description must reference the issue it closes with a `For ` (e.g. For #293). + +- A PR's description must contain link(s) to the line(s) in the OpenStack + source code (on Github) that prove(s) the PR code to be valid. Links to documentation + are not good enough. The link(s) should be to a non-`master` branch. For example, + a pull request implementing the creation of a Neutron v2 subnet might put the + following link in the description: + + https://github.com/openstack/neutron/blob/stable/mitaka/neutron/api/v2/attributes.py#L749 + + From that link, a reviewer (or user) can verify the fields in the request/response + objects in the PR. + +- A PR that is in-progress should have `[wip]` in front of the PR's title. When + ready for review, remove the `[wip]` and ping a core contributor with an `@`. + +- Forcing PRs to be small can have the effect of users submitting PRs in a hierarchical chain, with + one depending on the next. If a PR depends on another one, it should have a [Pending #PRNUM] + prefix in the PR title. In addition, it will be the PR submitter's responsibility to remove the + [Pending #PRNUM] tag once the PR has been updated with the merged, dependent PR. That will + let reviewers know it is ready to review. + +- A PR should be small. Even if you intend on implementing an entire + service, a PR should only be one route of that service + (e.g. create server or get server, but not both). + +- Unless explicitly asked, do not squash commits in the middle of a review; only + append. It makes it difficult for the reviewer to see what's changed from one + review to the next. + +## On Code + +- In re design: follow as closely as is reasonable the code already in the library. + Most operations (e.g. create, delete) admit the same design. + +- Unit tests and acceptance (integration) tests must be written to cover each PR. + Tests for operations with several options (e.g. list, create) should include all + the options in the tests. This will allow users to verify an operation on their + own infrastructure and see an example of usage. + +- If in doubt, ask in-line on the PR. + +### File Structure + +- The following should be used in most cases: + + - `requests.go`: contains all the functions that make HTTP requests and the + types associated with the HTTP request (parameters for URL, body, etc) + - `results.go`: contains all the response objects and their methods + - `urls.go`: contains the endpoints to which the requests are made + +### Naming + +- For methods on a type in `results.go`, the receiver should be named `r` and the + variable into which it will be unmarshalled `s`. + +- Functions in `requests.go`, with the exception of functions that return a + `pagination.Pager`, should be named returns of the name `r`. + +- Functions in `requests.go` that accept request bodies should accept as their + last parameter an `interface` named `OptsBuilder` (eg `CreateOptsBuilder`). + This `interface` should have at the least a method named `ToMap` + (eg `ToPortCreateMap`). + +- Functions in `requests.go` that accept query strings should accept as their + last parameter an `interface` named `OptsBuilder` (eg `ListOptsBuilder`). + This `interface` should have at the least a method named `ToQuery` + (eg `ToServerListQuery`). diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/README.md b/vendor/github.com/gophercloud/gophercloud/acceptance/README.md new file mode 100644 index 0000000000..2254aa1ecc --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/README.md @@ -0,0 +1,86 @@ +# Gophercloud Acceptance tests + +The purpose of these acceptance tests is to validate that SDK features meet +the requirements of a contract - to consumers, other parts of the library, and +to a remote API. + +> **Note:** Because every test will be run against a real API endpoint, you +> may incur bandwidth and service charges for all the resource usage. These +> tests *should* remove their remote products automatically. However, there may +> be certain cases where this does not happen; always double-check to make sure +> you have no stragglers left behind. + +### Step 1. Set environment variables + +A lot of tests rely on environment variables for configuration - so you will need +to set them before running the suite. If you're testing against pure OpenStack APIs, +you can download a file that contains all of these variables for you: just visit +the `project/access_and_security` page in your control panel and click the "Download +OpenStack RC File" button at the top right. For all other providers, you will need +to set them manually. + +#### Authentication + +|Name|Description| +|---|---| +|`OS_USERNAME`|Your API username| +|`OS_PASSWORD`|Your API password| +|`OS_AUTH_URL`|The identity URL you need to authenticate| +|`OS_TENANT_NAME`|Your API tenant name| +|`OS_TENANT_ID`|Your API tenant ID| + +#### General + +|Name|Description| +|---|---| +|`OS_REGION_NAME`|The region you want your resources to reside in| + +#### Compute + +|Name|Description| +|---|---| +|`OS_IMAGE_ID`|The ID of the image your want your server to be based on| +|`OS_FLAVOR_ID`|The ID of the flavor you want your server to be based on| +|`OS_FLAVOR_ID_RESIZE`|The ID of the flavor you want your server to be resized to| +|`OS_POOL_NAME`|The Pool from where to obtain Floating IPs| +|`OS_NETWORK_NAME`|The network to launch instances on| + +#### Shared file systems +|Name|Description| +|---|---| +|`OS_SHARE_NETWORK_ID`| The share network ID to use when creating shares| + +### 2. Run the test suite + +From the root directory, run: + +``` +./script/acceptancetest +``` + +Alternatively, add the following to your `.bashrc`: + +```bash +gophercloudtest() { + if [[ -n $1 ]] && [[ -n $2 ]]; then + pushd $GOPATH/src/github.com/gophercloud/gophercloud + go test -v -tags "fixtures acceptance" -run "$1" github.com/gophercloud/gophercloud/acceptance/openstack/$2 | tee ~/gophercloud.log + popd +fi +} +``` + +Then run either groups or individual tests by doing: + +```shell +$ gophercloudtest TestFlavorsList compute/v2 +$ gophercloudtest TestFlavors compute/v2 +$ gophercloudtest Test compute/v2 +``` + +### 3. Notes + +#### Compute Tests + +* In order to run the `TestBootFromVolumeMultiEphemeral` test, a flavor with ephemeral disk space must be used. +* The `TestDefSecRules` tests require a compatible network driver and admin privileges. diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/clients/clients.go b/vendor/github.com/gophercloud/gophercloud/acceptance/clients/clients.go new file mode 100644 index 0000000000..aa57497de0 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/clients/clients.go @@ -0,0 +1,324 @@ +// Package clients contains functions for creating OpenStack service clients +// for use in acceptance tests. It also manages the required environment +// variables to run the tests. +package clients + +import ( + "fmt" + "os" + "strings" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack" +) + +// AcceptanceTestChoices contains image and flavor selections for use by the acceptance tests. +type AcceptanceTestChoices struct { + // ImageID contains the ID of a valid image. + ImageID string + + // FlavorID contains the ID of a valid flavor. + FlavorID string + + // FlavorIDResize contains the ID of a different flavor available on the same OpenStack installation, that is distinct + // from FlavorID. + FlavorIDResize string + + // FloatingIPPool contains the name of the pool from where to obtain floating IPs. + FloatingIPPoolName string + + // NetworkName is the name of a network to launch the instance on. + NetworkName string + + // ExternalNetworkID is the network ID of the external network. + ExternalNetworkID string + + // ShareNetworkID is the Manila Share network ID + ShareNetworkID string +} + +// AcceptanceTestChoicesFromEnv populates a ComputeChoices struct from environment variables. +// If any required state is missing, an `error` will be returned that enumerates the missing properties. +func AcceptanceTestChoicesFromEnv() (*AcceptanceTestChoices, error) { + imageID := os.Getenv("OS_IMAGE_ID") + flavorID := os.Getenv("OS_FLAVOR_ID") + flavorIDResize := os.Getenv("OS_FLAVOR_ID_RESIZE") + networkName := os.Getenv("OS_NETWORK_NAME") + floatingIPPoolName := os.Getenv("OS_POOL_NAME") + externalNetworkID := os.Getenv("OS_EXTGW_ID") + shareNetworkID := os.Getenv("OS_SHARE_NETWORK_ID") + + missing := make([]string, 0, 3) + if imageID == "" { + missing = append(missing, "OS_IMAGE_ID") + } + if flavorID == "" { + missing = append(missing, "OS_FLAVOR_ID") + } + if flavorIDResize == "" { + missing = append(missing, "OS_FLAVOR_ID_RESIZE") + } + if floatingIPPoolName == "" { + missing = append(missing, "OS_POOL_NAME") + } + if externalNetworkID == "" { + missing = append(missing, "OS_EXTGW_ID") + } + if networkName == "" { + networkName = "private" + } + if shareNetworkID == "" { + missing = append(missing, "OS_SHARE_NETWORK_ID") + } + notDistinct := "" + if flavorID == flavorIDResize { + notDistinct = "OS_FLAVOR_ID and OS_FLAVOR_ID_RESIZE must be distinct." + } + + if len(missing) > 0 || notDistinct != "" { + text := "You're missing some important setup:\n" + if len(missing) > 0 { + text += " * These environment variables must be provided: " + strings.Join(missing, ", ") + "\n" + } + if notDistinct != "" { + text += " * " + notDistinct + "\n" + } + + return nil, fmt.Errorf(text) + } + + return &AcceptanceTestChoices{ + ImageID: imageID, + FlavorID: flavorID, + FlavorIDResize: flavorIDResize, + FloatingIPPoolName: floatingIPPoolName, + NetworkName: networkName, + ExternalNetworkID: externalNetworkID, + ShareNetworkID: shareNetworkID, + }, nil +} + +// NewBlockStorageV1Client returns a *ServiceClient for making calls +// to the OpenStack Block Storage v1 API. An error will be returned +// if authentication or client creation was not possible. +func NewBlockStorageV1Client() (*gophercloud.ServiceClient, error) { + ao, err := openstack.AuthOptionsFromEnv() + if err != nil { + return nil, err + } + + client, err := openstack.AuthenticatedClient(ao) + if err != nil { + return nil, err + } + + return openstack.NewBlockStorageV1(client, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) +} + +// NewBlockStorageV2Client returns a *ServiceClient for making calls +// to the OpenStack Block Storage v2 API. An error will be returned +// if authentication or client creation was not possible. +func NewBlockStorageV2Client() (*gophercloud.ServiceClient, error) { + ao, err := openstack.AuthOptionsFromEnv() + if err != nil { + return nil, err + } + + client, err := openstack.AuthenticatedClient(ao) + if err != nil { + return nil, err + } + + return openstack.NewBlockStorageV2(client, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) +} + +// NewSharedFileSystemV2Client returns a *ServiceClient for making calls +// to the OpenStack Shared File System v2 API. An error will be returned +// if authentication or client creation was not possible. +func NewSharedFileSystemV2Client() (*gophercloud.ServiceClient, error) { + ao, err := openstack.AuthOptionsFromEnv() + if err != nil { + return nil, err + } + + client, err := openstack.AuthenticatedClient(ao) + if err != nil { + return nil, err + } + + return openstack.NewSharedFileSystemV2(client, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) +} + +// NewComputeV2Client returns a *ServiceClient for making calls +// to the OpenStack Compute v2 API. An error will be returned +// if authentication or client creation was not possible. +func NewComputeV2Client() (*gophercloud.ServiceClient, error) { + ao, err := openstack.AuthOptionsFromEnv() + if err != nil { + return nil, err + } + + client, err := openstack.AuthenticatedClient(ao) + if err != nil { + return nil, err + } + + return openstack.NewComputeV2(client, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) +} + +// NewDNSV2Client returns a *ServiceClient for making calls +// to the OpenStack Compute v2 API. An error will be returned +// if authentication or client creation was not possible. +func NewDNSV2Client() (*gophercloud.ServiceClient, error) { + ao, err := openstack.AuthOptionsFromEnv() + if err != nil { + return nil, err + } + + client, err := openstack.AuthenticatedClient(ao) + if err != nil { + return nil, err + } + + return openstack.NewDNSV2(client, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) +} + +// NewIdentityV2Client returns a *ServiceClient for making calls +// to the OpenStack Identity v2 API. An error will be returned +// if authentication or client creation was not possible. +func NewIdentityV2Client() (*gophercloud.ServiceClient, error) { + ao, err := openstack.AuthOptionsFromEnv() + if err != nil { + return nil, err + } + + client, err := openstack.AuthenticatedClient(ao) + if err != nil { + return nil, err + } + + return openstack.NewIdentityV2(client, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) +} + +// NewIdentityV2AdminClient returns a *ServiceClient for making calls +// to the Admin Endpoint of the OpenStack Identity v2 API. An error +// will be returned if authentication or client creation was not possible. +func NewIdentityV2AdminClient() (*gophercloud.ServiceClient, error) { + ao, err := openstack.AuthOptionsFromEnv() + if err != nil { + return nil, err + } + + client, err := openstack.AuthenticatedClient(ao) + if err != nil { + return nil, err + } + + return openstack.NewIdentityV2(client, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + Availability: gophercloud.AvailabilityAdmin, + }) +} + +// NewIdentityV2UnauthenticatedClient returns an unauthenticated *ServiceClient +// for the OpenStack Identity v2 API. An error will be returned if +// authentication or client creation was not possible. +func NewIdentityV2UnauthenticatedClient() (*gophercloud.ServiceClient, error) { + ao, err := openstack.AuthOptionsFromEnv() + if err != nil { + return nil, err + } + + client, err := openstack.NewClient(ao.IdentityEndpoint) + if err != nil { + return nil, err + } + + return openstack.NewIdentityV2(client, gophercloud.EndpointOpts{}) +} + +// NewIdentityV3Client returns a *ServiceClient for making calls +// to the OpenStack Identity v3 API. An error will be returned +// if authentication or client creation was not possible. +func NewIdentityV3Client() (*gophercloud.ServiceClient, error) { + ao, err := openstack.AuthOptionsFromEnv() + if err != nil { + return nil, err + } + + client, err := openstack.AuthenticatedClient(ao) + if err != nil { + return nil, err + } + + return openstack.NewIdentityV3(client, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) +} + +// NewIdentityV3UnauthenticatedClient returns an unauthenticated *ServiceClient +// for the OpenStack Identity v3 API. An error will be returned if +// authentication or client creation was not possible. +func NewIdentityV3UnauthenticatedClient() (*gophercloud.ServiceClient, error) { + ao, err := openstack.AuthOptionsFromEnv() + if err != nil { + return nil, err + } + + client, err := openstack.NewClient(ao.IdentityEndpoint) + if err != nil { + return nil, err + } + + return openstack.NewIdentityV3(client, gophercloud.EndpointOpts{}) +} + +// NewImageServiceV2Client returns a *ServiceClient for making calls to the +// OpenStack Image v2 API. An error will be returned if authentication or +// client creation was not possible. +func NewImageServiceV2Client() (*gophercloud.ServiceClient, error) { + ao, err := openstack.AuthOptionsFromEnv() + if err != nil { + return nil, err + } + + client, err := openstack.AuthenticatedClient(ao) + if err != nil { + return nil, err + } + + return openstack.NewImageServiceV2(client, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) +} + +// NewNetworkV2Client returns a *ServiceClient for making calls to the +// OpenStack Networking v2 API. An error will be returned if authentication +// or client creation was not possible. +func NewNetworkV2Client() (*gophercloud.ServiceClient, error) { + ao, err := openstack.AuthOptionsFromEnv() + if err != nil { + return nil, err + } + + client, err := openstack.AuthenticatedClient(ao) + if err != nil { + return nil, err + } + + return openstack.NewNetworkV2(client, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/extensions/extensions.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/extensions/extensions.go new file mode 100644 index 0000000000..2785392062 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/extensions/extensions.go @@ -0,0 +1,153 @@ +// Package extensions contains common functions for creating block storage +// resources that are extensions of the block storage API. See the `*_test.go` +// files for example usages. +package extensions + +import ( + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes" + "github.com/gophercloud/gophercloud/openstack/compute/v2/images" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" +) + +// CreateUploadImage will upload volume it as volume-baked image. An name of new image or err will be +// returned +func CreateUploadImage(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) (volumeactions.VolumeImage, error) { + if testing.Short() { + t.Skip("Skipping test that requires volume-backed image uploading in short mode.") + } + + imageName := tools.RandomString("ACPTTEST", 16) + uploadImageOpts := volumeactions.UploadImageOpts{ + ImageName: imageName, + Force: true, + } + + volumeImage, err := volumeactions.UploadImage(client, volume.ID, uploadImageOpts).Extract() + if err != nil { + return volumeImage, err + } + + t.Logf("Uploading volume %s as volume-backed image %s", volume.ID, imageName) + + if err := volumes.WaitForStatus(client, volume.ID, "available", 60); err != nil { + return volumeImage, err + } + + t.Logf("Uploaded volume %s as volume-backed image %s", volume.ID, imageName) + + return volumeImage, nil + +} + +// DeleteUploadedImage deletes uploaded image. An error will be returned +// if the deletion request failed. +func DeleteUploadedImage(t *testing.T, client *gophercloud.ServiceClient, imageName string) error { + if testing.Short() { + t.Skip("Skipping test that requires volume-backed image removing in short mode.") + } + + t.Logf("Getting image id for image name %s", imageName) + + imageID, err := images.IDFromName(client, imageName) + if err != nil { + return err + } + + t.Logf("Removing image %s", imageID) + + err = images.Delete(client, imageID).ExtractErr() + if err != nil { + return err + } + + return nil +} + +// CreateVolumeAttach will attach a volume to an instance. An error will be +// returned if the attachment failed. +func CreateVolumeAttach(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume, server *servers.Server) error { + if testing.Short() { + t.Skip("Skipping test that requires volume attachment in short mode.") + } + + attachOpts := volumeactions.AttachOpts{ + MountPoint: "/mnt", + Mode: "rw", + InstanceUUID: server.ID, + } + + t.Logf("Attempting to attach volume %s to server %s", volume.ID, server.ID) + + if err := volumeactions.Attach(client, volume.ID, attachOpts).ExtractErr(); err != nil { + return err + } + + if err := volumes.WaitForStatus(client, volume.ID, "in-use", 60); err != nil { + return err + } + + t.Logf("Attached volume %s to server %s", volume.ID, server.ID) + + return nil +} + +// CreateVolumeReserve creates a volume reservation. An error will be returned +// if the reservation failed. +func CreateVolumeReserve(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) error { + if testing.Short() { + t.Skip("Skipping test that requires volume reservation in short mode.") + } + + t.Logf("Attempting to reserve volume %s", volume.ID) + + if err := volumeactions.Reserve(client, volume.ID).ExtractErr(); err != nil { + return err + } + + t.Logf("Reserved volume %s", volume.ID) + + return nil +} + +// DeleteVolumeAttach will detach a volume from an instance. A fatal error will +// occur if the snapshot failed to be deleted. This works best when used as a +// deferred function. +func DeleteVolumeAttach(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) { + t.Logf("Attepting to detach volume volume: %s", volume.ID) + + detachOpts := volumeactions.DetachOpts{ + AttachmentID: volume.Attachments[0].AttachmentID, + } + + if err := volumeactions.Detach(client, volume.ID, detachOpts).ExtractErr(); err != nil { + t.Fatalf("Unable to detach volume %s: %v", volume.ID, err) + } + + if err := volumes.WaitForStatus(client, volume.ID, "available", 60); err != nil { + t.Fatalf("Volume %s failed to become unavailable in 60 seconds: %v", volume.ID, err) + } + + t.Logf("Detached volume: %s", volume.ID) +} + +// DeleteVolumeReserve deletes a volume reservation. A fatal error will occur +// if the deletion request failed. This works best when used as a deferred +// function. +func DeleteVolumeReserve(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) { + if testing.Short() { + t.Skip("Skipping test that requires volume reservation in short mode.") + } + + t.Logf("Attempting to unreserve volume %s", volume.ID) + + if err := volumeactions.Unreserve(client, volume.ID).ExtractErr(); err != nil { + t.Fatalf("Unable to unreserve volume %s: %v", volume.ID, err) + } + + t.Logf("Unreserved volume %s", volume.ID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/extensions/pkg.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/extensions/pkg.go new file mode 100644 index 0000000000..f18039dcb1 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/extensions/pkg.go @@ -0,0 +1,3 @@ +// The extensions package contains acceptance tests for the Openstack Cinder extensions service. + +package extensions diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/extensions/volumeactions_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/extensions/volumeactions_test.go new file mode 100644 index 0000000000..b202852790 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/extensions/volumeactions_test.go @@ -0,0 +1,143 @@ +// +build acceptance blockstorage + +package extensions + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes" + + blockstorage "github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/v2" + compute "github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2" +) + +func TestVolumeActionsUploadImageDestroy(t *testing.T) { + blockClient, err := clients.NewBlockStorageV2Client() + if err != nil { + t.Fatalf("Unable to create a blockstorage client: %v", err) + } + computeClient, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + volume, err := blockstorage.CreateVolume(t, blockClient) + if err != nil { + t.Fatalf("Unable to create volume: %v", err) + } + defer blockstorage.DeleteVolume(t, blockClient, volume) + + volumeImage, err := CreateUploadImage(t, blockClient, volume) + if err != nil { + t.Fatalf("Unable to upload volume-backed image: %v", err) + } + + tools.PrintResource(t, volumeImage) + + err = DeleteUploadedImage(t, computeClient, volumeImage.ImageName) + if err != nil { + t.Fatalf("Unable to delete volume-backed image: %v", err) + } +} + +func TestVolumeActionsAttachCreateDestroy(t *testing.T) { + blockClient, err := clients.NewBlockStorageV2Client() + if err != nil { + t.Fatalf("Unable to create a blockstorage client: %v", err) + } + + computeClient, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + server, err := compute.CreateServer(t, computeClient) + if err != nil { + t.Fatalf("Unable to create server: %v", err) + } + defer compute.DeleteServer(t, computeClient, server) + + volume, err := blockstorage.CreateVolume(t, blockClient) + if err != nil { + t.Fatalf("Unable to create volume: %v", err) + } + defer blockstorage.DeleteVolume(t, blockClient, volume) + + err = CreateVolumeAttach(t, blockClient, volume, server) + if err != nil { + t.Fatalf("Unable to attach volume: %v", err) + } + + newVolume, err := volumes.Get(blockClient, volume.ID).Extract() + if err != nil { + t.Fatal("Unable to get updated volume information: %v", err) + } + + DeleteVolumeAttach(t, blockClient, newVolume) +} + +func TestVolumeActionsReserveUnreserve(t *testing.T) { + client, err := clients.NewBlockStorageV2Client() + if err != nil { + t.Fatalf("Unable to create blockstorage client: %v", err) + } + + volume, err := blockstorage.CreateVolume(t, client) + if err != nil { + t.Fatalf("Unable to create volume: %v", err) + } + defer blockstorage.DeleteVolume(t, client, volume) + + err = CreateVolumeReserve(t, client, volume) + if err != nil { + t.Fatalf("Unable to create volume reserve: %v", err) + } + defer DeleteVolumeReserve(t, client, volume) +} + +// Note(jtopjian): I plan to work on this at some point, but it requires +// setting up a server with iscsi utils. +/* +func TestVolumeConns(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + t.Logf("Creating volume") + cv, err := volumes.Create(client, &volumes.CreateOpts{ + Size: 1, + Name: "blockv2-volume", + }).Extract() + th.AssertNoErr(t, err) + + defer func() { + err = volumes.WaitForStatus(client, cv.ID, "available", 60) + th.AssertNoErr(t, err) + + t.Logf("Deleting volume") + err = volumes.Delete(client, cv.ID).ExtractErr() + th.AssertNoErr(t, err) + }() + + err = volumes.WaitForStatus(client, cv.ID, "available", 60) + th.AssertNoErr(t, err) + + connOpts := &volumeactions.ConnectorOpts{ + IP: "127.0.0.1", + Host: "stack", + Initiator: "iqn.1994-05.com.redhat:17cf566367d2", + Multipath: false, + Platform: "x86_64", + OSType: "linux2", + } + + t.Logf("Initializing connection") + _, err = volumeactions.InitializeConnection(client, cv.ID, connOpts).Extract() + th.AssertNoErr(t, err) + + t.Logf("Terminating connection") + err = volumeactions.TerminateConnection(client, cv.ID, connOpts).ExtractErr() + th.AssertNoErr(t, err) +} +*/ diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/v1/blockstorage.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/v1/blockstorage.go new file mode 100644 index 0000000000..41f24e1ab2 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/v1/blockstorage.go @@ -0,0 +1,142 @@ +// Package v1 contains common functions for creating block storage based +// resources for use in acceptance tests. See the `*_test.go` files for +// example usages. +package v1 + +import ( + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v1/snapshots" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumetypes" +) + +// CreateSnapshot will create a volume snapshot based off of a given volume and +// with a random name. An error will be returned if the snapshot failed to be +// created. +func CreateSnapshot(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) (*snapshots.Snapshot, error) { + if testing.Short() { + t.Skip("Skipping test that requires snapshot creation in short mode.") + } + + snapshotName := tools.RandomString("ACPTTEST", 16) + t.Logf("Attempting to create snapshot %s based on volume %s", snapshotName, volume.ID) + + createOpts := snapshots.CreateOpts{ + Name: snapshotName, + VolumeID: volume.ID, + } + + snapshot, err := snapshots.Create(client, createOpts).Extract() + if err != nil { + return snapshot, err + } + + err = snapshots.WaitForStatus(client, snapshot.ID, "available", 60) + if err != nil { + return snapshot, err + } + + return snapshot, nil +} + +// CreateVolume will create a volume with a random name and size of 1GB. An +// error will be returned if the volume was unable to be created. +func CreateVolume(t *testing.T, client *gophercloud.ServiceClient) (*volumes.Volume, error) { + if testing.Short() { + t.Skip("Skipping test that requires volume creation in short mode.") + } + + volumeName := tools.RandomString("ACPTTEST", 16) + t.Logf("Attempting to create volume: %s", volumeName) + + createOpts := volumes.CreateOpts{ + Size: 1, + Name: volumeName, + } + + volume, err := volumes.Create(client, createOpts).Extract() + if err != nil { + return volume, err + } + + err = volumes.WaitForStatus(client, volume.ID, "available", 60) + if err != nil { + return volume, err + } + + return volume, nil +} + +// CreateVolumeType will create a volume type with a random name. An error will +// be returned if the volume type was unable to be created. +func CreateVolumeType(t *testing.T, client *gophercloud.ServiceClient) (*volumetypes.VolumeType, error) { + volumeTypeName := tools.RandomString("ACPTTEST", 16) + t.Logf("Attempting to create volume type: %s", volumeTypeName) + + createOpts := volumetypes.CreateOpts{ + Name: volumeTypeName, + ExtraSpecs: map[string]interface{}{ + "capabilities": "ssd", + "priority": 3, + }, + } + + volumeType, err := volumetypes.Create(client, createOpts).Extract() + if err != nil { + return volumeType, err + } + + return volumeType, nil +} + +// DeleteSnapshot will delete a snapshot. A fatal error will occur if the +// snapshot failed to be deleted. This works best when used as a deferred +// function. +func DeleteSnapshotshot(t *testing.T, client *gophercloud.ServiceClient, snapshot *snapshots.Snapshot) { + err := snapshots.Delete(client, snapshot.ID).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete snapshot %s: %v", snapshot.ID, err) + } + + // Volumes can't be deleted until their snapshots have been, + // so block up to 120 seconds for the snapshot to delete. + err = gophercloud.WaitFor(120, func() (bool, error) { + _, err := snapshots.Get(client, snapshot.ID).Extract() + if err != nil { + return true, nil + } + + return false, nil + }) + if err != nil { + t.Fatalf("Unable to wait for snapshot to delete: %v", err) + } + + t.Logf("Deleted snapshot: %s", snapshot.ID) +} + +// DeleteVolume will delete a volume. A fatal error will occur if the volume +// failed to be deleted. This works best when used as a deferred function. +func DeleteVolume(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) { + err := volumes.Delete(client, volume.ID).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete volume %s: %v", volume.ID, err) + } + + t.Logf("Deleted volume: %s", volume.ID) +} + +// DeleteVolumeType will delete a volume type. A fatal error will occur if the +// volume type failed to be deleted. This works best when used as a deferred +// function. +func DeleteVolumeType(t *testing.T, client *gophercloud.ServiceClient, volumeType *volumetypes.VolumeType) { + err := volumetypes.Delete(client, volumeType.ID).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete volume type %s: %v", volumeType.ID, err) + } + + t.Logf("Deleted volume type: %s", volumeType.ID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/v1/pkg.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/v1/pkg.go new file mode 100644 index 0000000000..4efa6fbf1e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/v1/pkg.go @@ -0,0 +1,2 @@ +// Package v1 contains openstack cinder acceptance tests +package v1 diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/v1/snapshots_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/v1/snapshots_test.go new file mode 100644 index 0000000000..354537187a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/v1/snapshots_test.go @@ -0,0 +1,58 @@ +// +build acceptance blockstorage + +package v1 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v1/snapshots" +) + +func TestSnapshotsList(t *testing.T) { + client, err := clients.NewBlockStorageV1Client() + if err != nil { + t.Fatalf("Unable to create a blockstorage client: %v", err) + } + + allPages, err := snapshots.List(client, snapshots.ListOpts{}).AllPages() + if err != nil { + t.Fatalf("Unable to retrieve snapshots: %v", err) + } + + allSnapshots, err := snapshots.ExtractSnapshots(allPages) + if err != nil { + t.Fatalf("Unable to extract snapshots: %v", err) + } + + for _, snapshot := range allSnapshots { + tools.PrintResource(t, snapshot) + } +} + +func TestSnapshotsCreateDelete(t *testing.T) { + client, err := clients.NewBlockStorageV1Client() + if err != nil { + t.Fatalf("Unable to create a blockstorage client: %v", err) + } + + volume, err := CreateVolume(t, client) + if err != nil { + t.Fatalf("Unable to create volume: %v", err) + } + defer DeleteVolume(t, client, volume) + + snapshot, err := CreateSnapshot(t, client, volume) + if err != nil { + t.Fatalf("Unable to create snapshot: %v", err) + } + defer DeleteSnapshotshot(t, client, snapshot) + + newSnapshot, err := snapshots.Get(client, snapshot.ID).Extract() + if err != nil { + t.Errorf("Unable to retrieve snapshot: %v", err) + } + + tools.PrintResource(t, newSnapshot) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/v1/volumes_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/v1/volumes_test.go new file mode 100644 index 0000000000..9a555009fb --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/v1/volumes_test.go @@ -0,0 +1,52 @@ +// +build acceptance blockstorage + +package v1 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes" +) + +func TestVolumesList(t *testing.T) { + client, err := clients.NewBlockStorageV1Client() + if err != nil { + t.Fatalf("Unable to create a blockstorage client: %v", err) + } + + allPages, err := volumes.List(client, volumes.ListOpts{}).AllPages() + if err != nil { + t.Fatalf("Unable to retrieve volumes: %v", err) + } + + allVolumes, err := volumes.ExtractVolumes(allPages) + if err != nil { + t.Fatalf("Unable to extract volumes: %v", err) + } + + for _, volume := range allVolumes { + tools.PrintResource(t, volume) + } +} + +func TestVolumesCreateDestroy(t *testing.T) { + client, err := clients.NewBlockStorageV1Client() + if err != nil { + t.Fatalf("Unable to create blockstorage client: %v", err) + } + + volume, err := CreateVolume(t, client) + if err != nil { + t.Fatalf("Unable to create volume: %v", err) + } + defer DeleteVolume(t, client, volume) + + newVolume, err := volumes.Get(client, volume.ID).Extract() + if err != nil { + t.Errorf("Unable to retrieve volume: %v", err) + } + + tools.PrintResource(t, newVolume) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/v1/volumetypes_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/v1/volumetypes_test.go new file mode 100644 index 0000000000..ace09bc4d0 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/v1/volumetypes_test.go @@ -0,0 +1,47 @@ +// +build acceptance blockstorage + +package v1 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumetypes" +) + +func TestVolumeTypesList(t *testing.T) { + client, err := clients.NewBlockStorageV1Client() + if err != nil { + t.Fatalf("Unable to create a blockstorage client: %v", err) + } + + allPages, err := volumetypes.List(client).AllPages() + if err != nil { + t.Fatalf("Unable to retrieve volume types: %v", err) + } + + allVolumeTypes, err := volumetypes.ExtractVolumeTypes(allPages) + if err != nil { + t.Fatalf("Unable to extract volume types: %v", err) + } + + for _, volumeType := range allVolumeTypes { + tools.PrintResource(t, volumeType) + } +} + +func TestVolumeTypesCreateDestroy(t *testing.T) { + client, err := clients.NewBlockStorageV1Client() + if err != nil { + t.Fatalf("Unable to create a blockstorage client: %v", err) + } + + volumeType, err := CreateVolumeType(t, client) + if err != nil { + t.Fatalf("Unable to create volume type: %v", err) + } + defer DeleteVolumeType(t, client, volumeType) + + tools.PrintResource(t, volumeType) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/v2/blockstorage.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/v2/blockstorage.go new file mode 100644 index 0000000000..51c8e59cad --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/v2/blockstorage.go @@ -0,0 +1,142 @@ +// Package v2 contains common functions for creating block storage based +// resources for use in acceptance tests. See the `*_test.go` files for +// example usages. +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v2/snapshots" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes" +) + +// CreateVolume will create a volume with a random name and size of 1GB. An +// error will be returned if the volume was unable to be created. +func CreateVolume(t *testing.T, client *gophercloud.ServiceClient) (*volumes.Volume, error) { + if testing.Short() { + t.Skip("Skipping test that requires volume creation in short mode.") + } + + volumeName := tools.RandomString("ACPTTEST", 16) + t.Logf("Attempting to create volume: %s", volumeName) + + createOpts := volumes.CreateOpts{ + Size: 1, + Name: volumeName, + } + + volume, err := volumes.Create(client, createOpts).Extract() + if err != nil { + return volume, err + } + + err = volumes.WaitForStatus(client, volume.ID, "available", 60) + if err != nil { + return volume, err + } + + return volume, nil +} + +// CreateVolumeFromImage will create a volume from with a random name and size of +// 1GB. An error will be returned if the volume was unable to be created. +func CreateVolumeFromImage(t *testing.T, client *gophercloud.ServiceClient) (*volumes.Volume, error) { + if testing.Short() { + t.Skip("Skipping test that requires volume creation in short mode.") + } + + choices, err := clients.AcceptanceTestChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + volumeName := tools.RandomString("ACPTTEST", 16) + t.Logf("Attempting to create volume: %s", volumeName) + + createOpts := volumes.CreateOpts{ + Size: 1, + Name: volumeName, + ImageID: choices.ImageID, + } + + volume, err := volumes.Create(client, createOpts).Extract() + if err != nil { + return volume, err + } + + err = volumes.WaitForStatus(client, volume.ID, "available", 60) + if err != nil { + return volume, err + } + + return volume, nil +} + +// DeleteVolume will delete a volume. A fatal error will occur if the volume +// failed to be deleted. This works best when used as a deferred function. +func DeleteVolume(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) { + err := volumes.Delete(client, volume.ID).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete volume %s: %v", volume.ID, err) + } + + t.Logf("Deleted volume: %s", volume.ID) +} + +// CreateSnapshot will create a snapshot of the specified volume. +// Snapshot will be assigned a random name and description. +func CreateSnapshot(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) (*snapshots.Snapshot, error) { + if testing.Short() { + t.Skip("Skipping test that requires snapshot creation in short mode.") + } + + snapshotName := tools.RandomString("ACPTTEST", 16) + snapshotDescription := tools.RandomString("ACPTTEST", 16) + t.Logf("Attempting to create snapshot: %s", snapshotName) + + createOpts := snapshots.CreateOpts{ + VolumeID: volume.ID, + Name: snapshotName, + Description: snapshotDescription, + } + + snapshot, err := snapshots.Create(client, createOpts).Extract() + if err != nil { + return snapshot, err + } + + err = snapshots.WaitForStatus(client, snapshot.ID, "available", 60) + if err != nil { + return snapshot, err + } + + return snapshot, nil +} + +// DeleteSnapshot will delete a snapshot. A fatal error will occur if the +// snapshot failed to be deleted. +func DeleteSnapshot(t *testing.T, client *gophercloud.ServiceClient, snapshot *snapshots.Snapshot) { + err := snapshots.Delete(client, snapshot.ID).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete snapshot %s: %+v", snapshot.ID, err) + } + + // Volumes can't be deleted until their snapshots have been, + // so block up to 120 seconds for the snapshot to delete. + err = gophercloud.WaitFor(120, func() (bool, error) { + _, err := snapshots.Get(client, snapshot.ID).Extract() + if err != nil { + return true, nil + } + + return false, nil + }) + if err != nil { + t.Fatalf("Error waiting for snapshot to delete: %v", err) + } + + t.Logf("Deleted snapshot: %s", snapshot.ID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/v2/pkg.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/v2/pkg.go new file mode 100644 index 0000000000..31dd0ffcb0 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/v2/pkg.go @@ -0,0 +1,3 @@ +// The v2 package contains acceptance tests for the Openstack Cinder V2 service. + +package v2 diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/v2/snapshots_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/v2/snapshots_test.go new file mode 100644 index 0000000000..7c1a4e5a56 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/v2/snapshots_test.go @@ -0,0 +1,58 @@ +// +build acceptance blockstorage + +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v2/snapshots" +) + +func TestSnapshotsList(t *testing.T) { + client, err := clients.NewBlockStorageV2Client() + if err != nil { + t.Fatalf("Unable to create a blockstorage client: %v", err) + } + + allPages, err := snapshots.List(client, snapshots.ListOpts{}).AllPages() + if err != nil { + t.Fatalf("Unable to retrieve snapshots: %v", err) + } + + allSnapshots, err := snapshots.ExtractSnapshots(allPages) + if err != nil { + t.Fatalf("Unable to extract snapshots: %v", err) + } + + for _, snapshot := range allSnapshots { + tools.PrintResource(t, snapshot) + } +} + +func TestSnapshotsCreateDelete(t *testing.T) { + client, err := clients.NewBlockStorageV2Client() + if err != nil { + t.Fatalf("Unable to create a blockstorage client: %v", err) + } + + volume, err := CreateVolume(t, client) + if err != nil { + t.Fatalf("Unable to create volume: %v", err) + } + defer DeleteVolume(t, client, volume) + + snapshot, err := CreateSnapshot(t, client, volume) + if err != nil { + t.Fatalf("Unable to create snapshot: %v", err) + } + defer DeleteSnapshot(t, client, snapshot) + + newSnapshot, err := snapshots.Get(client, snapshot.ID).Extract() + if err != nil { + t.Errorf("Unable to retrieve snapshot: %v", err) + } + + tools.PrintResource(t, newSnapshot) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/v2/volumes_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/v2/volumes_test.go new file mode 100644 index 0000000000..9003ca7111 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/v2/volumes_test.go @@ -0,0 +1,52 @@ +// +build acceptance blockstorage + +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes" +) + +func TestVolumesList(t *testing.T) { + client, err := clients.NewBlockStorageV2Client() + if err != nil { + t.Fatalf("Unable to create a blockstorage client: %v", err) + } + + allPages, err := volumes.List(client, volumes.ListOpts{}).AllPages() + if err != nil { + t.Fatalf("Unable to retrieve volumes: %v", err) + } + + allVolumes, err := volumes.ExtractVolumes(allPages) + if err != nil { + t.Fatalf("Unable to extract volumes: %v", err) + } + + for _, volume := range allVolumes { + tools.PrintResource(t, volume) + } +} + +func TestVolumesCreateDestroy(t *testing.T) { + client, err := clients.NewBlockStorageV2Client() + if err != nil { + t.Fatalf("Unable to create blockstorage client: %v", err) + } + + volume, err := CreateVolume(t, client) + if err != nil { + t.Fatalf("Unable to create volume: %v", err) + } + defer DeleteVolume(t, client, volume) + + newVolume, err := volumes.Get(client, volume.ID).Extract() + if err != nil { + t.Errorf("Unable to retrieve volume: %v", err) + } + + tools.PrintResource(t, newVolume) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/client_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/client_test.go new file mode 100644 index 0000000000..eed3a82ff4 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/client_test.go @@ -0,0 +1,86 @@ +// +build acceptance + +package openstack + +import ( + "os" + "testing" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack" +) + +func TestAuthenticatedClient(t *testing.T) { + // Obtain credentials from the environment. + ao, err := openstack.AuthOptionsFromEnv() + if err != nil { + t.Fatalf("Unable to acquire credentials: %v", err) + } + + client, err := openstack.AuthenticatedClient(ao) + if err != nil { + t.Fatalf("Unable to authenticate: %v", err) + } + + if client.TokenID == "" { + t.Errorf("No token ID assigned to the client") + } + + t.Logf("Client successfully acquired a token: %v", client.TokenID) + + // Find the storage service in the service catalog. + storage, err := openstack.NewObjectStorageV1(client, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) + if err != nil { + t.Errorf("Unable to locate a storage service: %v", err) + } else { + t.Logf("Located a storage service at endpoint: [%s]", storage.Endpoint) + } +} + +func TestReauth(t *testing.T) { + ao, err := openstack.AuthOptionsFromEnv() + if err != nil { + t.Fatalf("Unable to obtain environment auth options: %v", err) + } + + // Allow reauth + ao.AllowReauth = true + + provider, err := openstack.NewClient(ao.IdentityEndpoint) + if err != nil { + t.Fatalf("Unable to create provider: %v", err) + } + + err = openstack.Authenticate(provider, ao) + if err != nil { + t.Fatalf("Unable to authenticate: %v", err) + } + + t.Logf("Creating a compute client") + _, err = openstack.NewComputeV2(provider, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) + if err != nil { + t.Fatalf("Unable to create compute client: %v", err) + } + + t.Logf("Sleeping for 1 second") + time.Sleep(1 * time.Second) + t.Logf("Attempting to reauthenticate") + + err = provider.ReauthFunc() + if err != nil { + t.Fatalf("Unable to reauthenticate: %v", err) + } + + t.Logf("Creating a compute client") + _, err = openstack.NewComputeV2(provider, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) + if err != nil { + t.Fatalf("Unable to create compute client: %v", err) + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/common.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/common.go new file mode 100644 index 0000000000..ba78cb635d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/common.go @@ -0,0 +1,19 @@ +// Package openstack contains common functions that can be used +// across all OpenStack components for acceptance testing. +package openstack + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/common/extensions" +) + +// PrintExtension prints an extension and all of its attributes. +func PrintExtension(t *testing.T, extension *extensions.Extension) { + t.Logf("Name: %s", extension.Name) + t.Logf("Namespace: %s", extension.Namespace) + t.Logf("Alias: %s", extension.Alias) + t.Logf("Description: %s", extension.Description) + t.Logf("Updated: %s", extension.Updated) + t.Logf("Links: %v", extension.Links) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/bootfromvolume_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/bootfromvolume_test.go new file mode 100644 index 0000000000..2ba8888bf2 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/bootfromvolume_test.go @@ -0,0 +1,261 @@ +// +build acceptance compute bootfromvolume + +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + blockstorage "github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/v2" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume" +) + +func TestBootFromImage(t *testing.T) { + if testing.Short() { + t.Skip("Skipping test that requires server creation in short mode.") + } + + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + choices, err := clients.AcceptanceTestChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + blockDevices := []bootfromvolume.BlockDevice{ + bootfromvolume.BlockDevice{ + BootIndex: 0, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationLocal, + SourceType: bootfromvolume.SourceImage, + UUID: choices.ImageID, + }, + } + + server, err := CreateBootableVolumeServer(t, client, blockDevices) + if err != nil { + t.Fatalf("Unable to create server: %v", err) + } + defer DeleteServer(t, client, server) + + tools.PrintResource(t, server) +} + +func TestBootFromNewVolume(t *testing.T) { + if testing.Short() { + t.Skip("Skipping test that requires server creation in short mode.") + } + + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + choices, err := clients.AcceptanceTestChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + blockDevices := []bootfromvolume.BlockDevice{ + bootfromvolume.BlockDevice{ + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationVolume, + SourceType: bootfromvolume.SourceImage, + UUID: choices.ImageID, + VolumeSize: 2, + }, + } + + server, err := CreateBootableVolumeServer(t, client, blockDevices) + if err != nil { + t.Fatalf("Unable to create server: %v", err) + } + defer DeleteServer(t, client, server) + + tools.PrintResource(t, server) +} + +func TestBootFromExistingVolume(t *testing.T) { + if testing.Short() { + t.Skip("Skipping test that requires server creation in short mode.") + } + + computeClient, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + blockStorageClient, err := clients.NewBlockStorageV2Client() + if err != nil { + t.Fatalf("Unable to create a block storage client: %v", err) + } + + volume, err := blockstorage.CreateVolumeFromImage(t, blockStorageClient) + if err != nil { + t.Fatal(err) + } + + blockDevices := []bootfromvolume.BlockDevice{ + bootfromvolume.BlockDevice{ + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationVolume, + SourceType: bootfromvolume.SourceVolume, + UUID: volume.ID, + }, + } + + server, err := CreateBootableVolumeServer(t, computeClient, blockDevices) + if err != nil { + t.Fatalf("Unable to create server: %v", err) + } + defer DeleteServer(t, computeClient, server) + + tools.PrintResource(t, server) +} + +func TestBootFromMultiEphemeralServer(t *testing.T) { + if testing.Short() { + t.Skip("Skipping test that requires server creation in short mode.") + } + + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + choices, err := clients.AcceptanceTestChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + blockDevices := []bootfromvolume.BlockDevice{ + bootfromvolume.BlockDevice{ + BootIndex: 0, + DestinationType: bootfromvolume.DestinationLocal, + DeleteOnTermination: true, + SourceType: bootfromvolume.SourceImage, + UUID: choices.ImageID, + VolumeSize: 5, + }, + bootfromvolume.BlockDevice{ + BootIndex: -1, + DestinationType: bootfromvolume.DestinationLocal, + DeleteOnTermination: true, + GuestFormat: "ext4", + SourceType: bootfromvolume.SourceBlank, + VolumeSize: 1, + }, + bootfromvolume.BlockDevice{ + BootIndex: -1, + DestinationType: bootfromvolume.DestinationLocal, + DeleteOnTermination: true, + GuestFormat: "ext4", + SourceType: bootfromvolume.SourceBlank, + VolumeSize: 1, + }, + } + + server, err := CreateMultiEphemeralServer(t, client, blockDevices) + if err != nil { + t.Fatalf("Unable to create server: %v", err) + } + defer DeleteServer(t, client, server) + + tools.PrintResource(t, server) +} + +func TestAttachNewVolume(t *testing.T) { + if testing.Short() { + t.Skip("Skipping test that requires server creation in short mode.") + } + + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + choices, err := clients.AcceptanceTestChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + blockDevices := []bootfromvolume.BlockDevice{ + bootfromvolume.BlockDevice{ + BootIndex: 0, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationLocal, + SourceType: bootfromvolume.SourceImage, + UUID: choices.ImageID, + }, + bootfromvolume.BlockDevice{ + BootIndex: 1, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationVolume, + SourceType: bootfromvolume.SourceBlank, + VolumeSize: 2, + }, + } + + server, err := CreateBootableVolumeServer(t, client, blockDevices) + if err != nil { + t.Fatalf("Unable to create server: %v", err) + } + defer DeleteServer(t, client, server) + + tools.PrintResource(t, server) +} + +func TestAttachExistingVolume(t *testing.T) { + if testing.Short() { + t.Skip("Skipping test that requires server creation in short mode.") + } + + computeClient, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + blockStorageClient, err := clients.NewBlockStorageV2Client() + if err != nil { + t.Fatalf("Unable to create a block storage client: %v", err) + } + + choices, err := clients.AcceptanceTestChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + volume, err := blockstorage.CreateVolume(t, blockStorageClient) + if err != nil { + t.Fatal(err) + } + + blockDevices := []bootfromvolume.BlockDevice{ + bootfromvolume.BlockDevice{ + BootIndex: 0, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationLocal, + SourceType: bootfromvolume.SourceImage, + UUID: choices.ImageID, + }, + bootfromvolume.BlockDevice{ + BootIndex: 1, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationVolume, + SourceType: bootfromvolume.SourceVolume, + UUID: volume.ID, + }, + } + + server, err := CreateBootableVolumeServer(t, computeClient, blockDevices) + if err != nil { + t.Fatalf("Unable to create server: %v", err) + } + defer DeleteServer(t, computeClient, server) + + tools.PrintResource(t, server) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/compute.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/compute.go new file mode 100644 index 0000000000..6859bb7810 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/compute.go @@ -0,0 +1,767 @@ +// Package v2 contains common functions for creating compute-based resources +// for use in acceptance tests. See the `*_test.go` files for example usages. +package v2 + +import ( + "crypto/rand" + "crypto/rsa" + "fmt" + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume" + dsr "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/defsecrules" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/networks" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/schedulerhints" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/tenantnetworks" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + + "golang.org/x/crypto/ssh" +) + +// AssociateFloatingIP will associate a floating IP with an instance. An error +// will be returned if the floating IP was unable to be associated. +func AssociateFloatingIP(t *testing.T, client *gophercloud.ServiceClient, floatingIP *floatingips.FloatingIP, server *servers.Server) error { + associateOpts := floatingips.AssociateOpts{ + FloatingIP: floatingIP.IP, + } + + t.Logf("Attempting to associate floating IP %s to instance %s", floatingIP.IP, server.ID) + err := floatingips.AssociateInstance(client, server.ID, associateOpts).ExtractErr() + if err != nil { + return err + } + + return nil +} + +// AssociateFloatingIPWithFixedIP will associate a floating IP with an +// instance's specific fixed IP. An error will be returend if the floating IP +// was unable to be associated. +func AssociateFloatingIPWithFixedIP(t *testing.T, client *gophercloud.ServiceClient, floatingIP *floatingips.FloatingIP, server *servers.Server, fixedIP string) error { + associateOpts := floatingips.AssociateOpts{ + FloatingIP: floatingIP.IP, + FixedIP: fixedIP, + } + + t.Logf("Attempting to associate floating IP %s to fixed IP %s on instance %s", floatingIP.IP, fixedIP, server.ID) + err := floatingips.AssociateInstance(client, server.ID, associateOpts).ExtractErr() + if err != nil { + return err + } + + return nil +} + +// CreateBootableVolumeServer works like CreateServer but is configured with +// one or more block devices defined by passing in []bootfromvolume.BlockDevice. +// An error will be returned if a server was unable to be created. +func CreateBootableVolumeServer(t *testing.T, client *gophercloud.ServiceClient, blockDevices []bootfromvolume.BlockDevice) (*servers.Server, error) { + if testing.Short() { + t.Skip("Skipping test that requires server creation in short mode.") + } + + var server *servers.Server + + choices, err := clients.AcceptanceTestChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + networkID, err := GetNetworkIDFromTenantNetworks(t, client, choices.NetworkName) + if err != nil { + return server, err + } + + name := tools.RandomString("ACPTTEST", 16) + t.Logf("Attempting to create bootable volume server: %s", name) + + serverCreateOpts := servers.CreateOpts{ + Name: name, + FlavorRef: choices.FlavorID, + Networks: []servers.Network{ + servers.Network{UUID: networkID}, + }, + } + + if blockDevices[0].SourceType == bootfromvolume.SourceImage && blockDevices[0].DestinationType == bootfromvolume.DestinationLocal { + serverCreateOpts.ImageRef = blockDevices[0].UUID + } + + server, err = bootfromvolume.Create(client, bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: serverCreateOpts, + BlockDevice: blockDevices, + }).Extract() + + if err != nil { + return server, err + } + + if err := WaitForComputeStatus(client, server, "ACTIVE"); err != nil { + return server, err + } + + newServer, err := servers.Get(client, server.ID).Extract() + + return newServer, nil +} + +// CreateDefaultRule will create a default security group rule with a +// random port range between 80 and 90. An error will be returned if +// a default rule was unable to be created. +func CreateDefaultRule(t *testing.T, client *gophercloud.ServiceClient) (dsr.DefaultRule, error) { + createOpts := dsr.CreateOpts{ + FromPort: tools.RandomInt(80, 89), + ToPort: tools.RandomInt(90, 99), + IPProtocol: "TCP", + CIDR: "0.0.0.0/0", + } + + defaultRule, err := dsr.Create(client, createOpts).Extract() + if err != nil { + return *defaultRule, err + } + + t.Logf("Created default rule: %s", defaultRule.ID) + + return *defaultRule, nil +} + +// CreateFloatingIP will allocate a floating IP. +// An error will be returend if one was unable to be allocated. +func CreateFloatingIP(t *testing.T, client *gophercloud.ServiceClient) (*floatingips.FloatingIP, error) { + choices, err := clients.AcceptanceTestChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + createOpts := floatingips.CreateOpts{ + Pool: choices.FloatingIPPoolName, + } + floatingIP, err := floatingips.Create(client, createOpts).Extract() + if err != nil { + return floatingIP, err + } + + t.Logf("Created floating IP: %s", floatingIP.ID) + return floatingIP, nil +} + +func createKey() (string, error) { + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return "", err + } + + publicKey := privateKey.PublicKey + pub, err := ssh.NewPublicKey(&publicKey) + if err != nil { + return "", err + } + + pubBytes := ssh.MarshalAuthorizedKey(pub) + pk := string(pubBytes) + return pk, nil +} + +// CreateKeyPair will create a KeyPair with a random name. An error will occur +// if the keypair failed to be created. An error will be returned if the +// keypair was unable to be created. +func CreateKeyPair(t *testing.T, client *gophercloud.ServiceClient) (*keypairs.KeyPair, error) { + keyPairName := tools.RandomString("keypair_", 5) + + t.Logf("Attempting to create keypair: %s", keyPairName) + createOpts := keypairs.CreateOpts{ + Name: keyPairName, + } + keyPair, err := keypairs.Create(client, createOpts).Extract() + if err != nil { + return keyPair, err + } + + t.Logf("Created keypair: %s", keyPairName) + return keyPair, nil +} + +// CreateMultiEphemeralServer works like CreateServer but is configured with +// one or more block devices defined by passing in []bootfromvolume.BlockDevice. +// These block devices act like block devices when booting from a volume but +// are actually local ephemeral disks. +// An error will be returned if a server was unable to be created. +func CreateMultiEphemeralServer(t *testing.T, client *gophercloud.ServiceClient, blockDevices []bootfromvolume.BlockDevice) (*servers.Server, error) { + if testing.Short() { + t.Skip("Skipping test that requires server creation in short mode.") + } + + var server *servers.Server + + choices, err := clients.AcceptanceTestChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + networkID, err := GetNetworkIDFromTenantNetworks(t, client, choices.NetworkName) + if err != nil { + return server, err + } + + name := tools.RandomString("ACPTTEST", 16) + t.Logf("Attempting to create bootable volume server: %s", name) + + serverCreateOpts := servers.CreateOpts{ + Name: name, + FlavorRef: choices.FlavorID, + ImageRef: choices.ImageID, + Networks: []servers.Network{ + servers.Network{UUID: networkID}, + }, + } + + server, err = bootfromvolume.Create(client, bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: serverCreateOpts, + BlockDevice: blockDevices, + }).Extract() + + if err != nil { + return server, err + } + + if err := WaitForComputeStatus(client, server, "ACTIVE"); err != nil { + return server, err + } + + newServer, err := servers.Get(client, server.ID).Extract() + + return newServer, nil +} + +// CreateSecurityGroup will create a security group with a random name. +// An error will be returned if one was failed to be created. +func CreateSecurityGroup(t *testing.T, client *gophercloud.ServiceClient) (secgroups.SecurityGroup, error) { + createOpts := secgroups.CreateOpts{ + Name: tools.RandomString("secgroup_", 5), + Description: "something", + } + + securityGroup, err := secgroups.Create(client, createOpts).Extract() + if err != nil { + return *securityGroup, err + } + + t.Logf("Created security group: %s", securityGroup.ID) + return *securityGroup, nil +} + +// CreateSecurityGroupRule will create a security group rule with a random name +// and a random TCP port range between port 80 and 99. An error will be +// returned if the rule failed to be created. +func CreateSecurityGroupRule(t *testing.T, client *gophercloud.ServiceClient, securityGroupID string) (secgroups.Rule, error) { + createOpts := secgroups.CreateRuleOpts{ + ParentGroupID: securityGroupID, + FromPort: tools.RandomInt(80, 89), + ToPort: tools.RandomInt(90, 99), + IPProtocol: "TCP", + CIDR: "0.0.0.0/0", + } + + rule, err := secgroups.CreateRule(client, createOpts).Extract() + if err != nil { + return *rule, err + } + + t.Logf("Created security group rule: %s", rule.ID) + return *rule, nil +} + +// CreateServer creates a basic instance with a randomly generated name. +// The flavor of the instance will be the value of the OS_FLAVOR_ID environment variable. +// The image will be the value of the OS_IMAGE_ID environment variable. +// The instance will be launched on the network specified in OS_NETWORK_NAME. +// An error will be returned if the instance was unable to be created. +func CreateServer(t *testing.T, client *gophercloud.ServiceClient) (*servers.Server, error) { + if testing.Short() { + t.Skip("Skipping test that requires server creation in short mode.") + } + + var server *servers.Server + + choices, err := clients.AcceptanceTestChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + networkID, err := GetNetworkIDFromTenantNetworks(t, client, choices.NetworkName) + if err != nil { + return server, err + } + + name := tools.RandomString("ACPTTEST", 16) + t.Logf("Attempting to create server: %s", name) + + pwd := tools.MakeNewPassword("") + + server, err = servers.Create(client, servers.CreateOpts{ + Name: name, + FlavorRef: choices.FlavorID, + ImageRef: choices.ImageID, + AdminPass: pwd, + Networks: []servers.Network{ + servers.Network{UUID: networkID}, + }, + Metadata: map[string]string{ + "abc": "def", + }, + Personality: servers.Personality{ + &servers.File{ + Path: "/etc/test", + Contents: []byte("hello world"), + }, + }, + }).Extract() + if err != nil { + return server, err + } + + if err := WaitForComputeStatus(client, server, "ACTIVE"); err != nil { + return server, err + } + + return server, nil +} + +// CreateServerWithoutImageRef creates a basic instance with a randomly generated name. +// The flavor of the instance will be the value of the OS_FLAVOR_ID environment variable. +// The image is intentionally missing to trigger an error. +// The instance will be launched on the network specified in OS_NETWORK_NAME. +// An error will be returned if the instance was unable to be created. +func CreateServerWithoutImageRef(t *testing.T, client *gophercloud.ServiceClient) (*servers.Server, error) { + if testing.Short() { + t.Skip("Skipping test that requires server creation in short mode.") + } + + var server *servers.Server + + choices, err := clients.AcceptanceTestChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + networkID, err := GetNetworkIDFromTenantNetworks(t, client, choices.NetworkName) + if err != nil { + return server, err + } + + name := tools.RandomString("ACPTTEST", 16) + t.Logf("Attempting to create server: %s", name) + + pwd := tools.MakeNewPassword("") + + server, err = servers.Create(client, servers.CreateOpts{ + Name: name, + FlavorRef: choices.FlavorID, + AdminPass: pwd, + Networks: []servers.Network{ + servers.Network{UUID: networkID}, + }, + Personality: servers.Personality{ + &servers.File{ + Path: "/etc/test", + Contents: []byte("hello world"), + }, + }, + }).Extract() + if err != nil { + return server, err + } + + if err := WaitForComputeStatus(client, server, "ACTIVE"); err != nil { + return server, err + } + + return server, nil +} + +// CreateServerGroup will create a server with a random name. An error will be +// returned if the server group failed to be created. +func CreateServerGroup(t *testing.T, client *gophercloud.ServiceClient, policy string) (*servergroups.ServerGroup, error) { + sg, err := servergroups.Create(client, &servergroups.CreateOpts{ + Name: "test", + Policies: []string{policy}, + }).Extract() + + if err != nil { + return sg, err + } + + return sg, nil +} + +// CreateServerInServerGroup works like CreateServer but places the instance in +// a specified Server Group. +func CreateServerInServerGroup(t *testing.T, client *gophercloud.ServiceClient, serverGroup *servergroups.ServerGroup) (*servers.Server, error) { + if testing.Short() { + t.Skip("Skipping test that requires server creation in short mode.") + } + + var server *servers.Server + + choices, err := clients.AcceptanceTestChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + networkID, err := GetNetworkIDFromTenantNetworks(t, client, choices.NetworkName) + if err != nil { + return server, err + } + + name := tools.RandomString("ACPTTEST", 16) + t.Logf("Attempting to create server: %s", name) + + pwd := tools.MakeNewPassword("") + + serverCreateOpts := servers.CreateOpts{ + Name: name, + FlavorRef: choices.FlavorID, + ImageRef: choices.ImageID, + AdminPass: pwd, + Networks: []servers.Network{ + servers.Network{UUID: networkID}, + }, + } + + schedulerHintsOpts := schedulerhints.CreateOptsExt{ + CreateOptsBuilder: serverCreateOpts, + SchedulerHints: schedulerhints.SchedulerHints{ + Group: serverGroup.ID, + }, + } + server, err = servers.Create(client, schedulerHintsOpts).Extract() + if err != nil { + return server, err + } + + return server, nil +} + +// CreateServerWithPublicKey works the same as CreateServer, but additionally +// configures the server with a specified Key Pair name. +func CreateServerWithPublicKey(t *testing.T, client *gophercloud.ServiceClient, keyPairName string) (*servers.Server, error) { + if testing.Short() { + t.Skip("Skipping test that requires server creation in short mode.") + } + + var server *servers.Server + + choices, err := clients.AcceptanceTestChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + networkID, err := GetNetworkIDFromTenantNetworks(t, client, choices.NetworkName) + if err != nil { + return server, err + } + + name := tools.RandomString("ACPTTEST", 16) + t.Logf("Attempting to create server: %s", name) + + serverCreateOpts := servers.CreateOpts{ + Name: name, + FlavorRef: choices.FlavorID, + ImageRef: choices.ImageID, + Networks: []servers.Network{ + servers.Network{UUID: networkID}, + }, + } + + server, err = servers.Create(client, keypairs.CreateOptsExt{ + CreateOptsBuilder: serverCreateOpts, + KeyName: keyPairName, + }).Extract() + if err != nil { + return server, err + } + + if err := WaitForComputeStatus(client, server, "ACTIVE"); err != nil { + return server, err + } + + return server, nil +} + +// CreateVolumeAttachment will attach a volume to a server. An error will be +// returned if the volume failed to attach. +func CreateVolumeAttachment(t *testing.T, client *gophercloud.ServiceClient, blockClient *gophercloud.ServiceClient, server *servers.Server, volume *volumes.Volume) (*volumeattach.VolumeAttachment, error) { + volumeAttachOptions := volumeattach.CreateOpts{ + VolumeID: volume.ID, + } + + t.Logf("Attempting to attach volume %s to server %s", volume.ID, server.ID) + volumeAttachment, err := volumeattach.Create(client, server.ID, volumeAttachOptions).Extract() + if err != nil { + return volumeAttachment, err + } + + if err := volumes.WaitForStatus(blockClient, volume.ID, "in-use", 60); err != nil { + return volumeAttachment, err + } + + return volumeAttachment, nil +} + +// DeleteDefaultRule deletes a default security group rule. +// A fatal error will occur if the rule failed to delete. This works best when +// using it as a deferred function. +func DeleteDefaultRule(t *testing.T, client *gophercloud.ServiceClient, defaultRule dsr.DefaultRule) { + err := dsr.Delete(client, defaultRule.ID).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete default rule %s: %v", defaultRule.ID, err) + } + + t.Logf("Deleted default rule: %s", defaultRule.ID) +} + +// DeleteFloatingIP will de-allocate a floating IP. A fatal error will occur if +// the floating IP failed to de-allocate. This works best when using it as a +// deferred function. +func DeleteFloatingIP(t *testing.T, client *gophercloud.ServiceClient, floatingIP *floatingips.FloatingIP) { + err := floatingips.Delete(client, floatingIP.ID).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete floating IP %s: %v", floatingIP.ID, err) + } + + t.Logf("Deleted floating IP: %s", floatingIP.ID) +} + +// DeleteKeyPair will delete a specified keypair. A fatal error will occur if +// the keypair failed to be deleted. This works best when used as a deferred +// function. +func DeleteKeyPair(t *testing.T, client *gophercloud.ServiceClient, keyPair *keypairs.KeyPair) { + err := keypairs.Delete(client, keyPair.Name).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete keypair %s: %v", keyPair.Name, err) + } + + t.Logf("Deleted keypair: %s", keyPair.Name) +} + +// DeleteSecurityGroup will delete a security group. A fatal error will occur +// if the group failed to be deleted. This works best as a deferred function. +func DeleteSecurityGroup(t *testing.T, client *gophercloud.ServiceClient, securityGroup secgroups.SecurityGroup) { + err := secgroups.Delete(client, securityGroup.ID).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete security group %s: %s", securityGroup.ID, err) + } + + t.Logf("Deleted security group: %s", securityGroup.ID) +} + +// DeleteSecurityGroupRule will delete a security group rule. A fatal error +// will occur if the rule failed to be deleted. This works best when used +// as a deferred function. +func DeleteSecurityGroupRule(t *testing.T, client *gophercloud.ServiceClient, rule secgroups.Rule) { + err := secgroups.DeleteRule(client, rule.ID).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete rule: %v", err) + } + + t.Logf("Deleted security group rule: %s", rule.ID) +} + +// DeleteServer deletes an instance via its UUID. +// A fatal error will occur if the instance failed to be destroyed. This works +// best when using it as a deferred function. +func DeleteServer(t *testing.T, client *gophercloud.ServiceClient, server *servers.Server) { + err := servers.Delete(client, server.ID).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete server %s: %s", server.ID, err) + } + + t.Logf("Deleted server: %s", server.ID) +} + +// DeleteServerGroup will delete a server group. A fatal error will occur if +// the server group failed to be deleted. This works best when used as a +// deferred function. +func DeleteServerGroup(t *testing.T, client *gophercloud.ServiceClient, serverGroup *servergroups.ServerGroup) { + err := servergroups.Delete(client, serverGroup.ID).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete server group %s: %v", serverGroup.ID, err) + } + + t.Logf("Deleted server group %s", serverGroup.ID) +} + +// DeleteVolumeAttachment will disconnect a volume from an instance. A fatal +// error will occur if the volume failed to detach. This works best when used +// as a deferred function. +func DeleteVolumeAttachment(t *testing.T, client *gophercloud.ServiceClient, blockClient *gophercloud.ServiceClient, server *servers.Server, volumeAttachment *volumeattach.VolumeAttachment) { + + err := volumeattach.Delete(client, server.ID, volumeAttachment.VolumeID).ExtractErr() + if err != nil { + t.Fatalf("Unable to detach volume: %v", err) + } + + if err := volumes.WaitForStatus(blockClient, volumeAttachment.ID, "available", 60); err != nil { + t.Fatalf("Unable to wait for volume: %v", err) + } + t.Logf("Deleted volume: %s", volumeAttachment.VolumeID) +} + +// DisassociateFloatingIP will disassociate a floating IP from an instance. A +// fatal error will occur if the floating IP failed to disassociate. This works +// best when using it as a deferred function. +func DisassociateFloatingIP(t *testing.T, client *gophercloud.ServiceClient, floatingIP *floatingips.FloatingIP, server *servers.Server) { + disassociateOpts := floatingips.DisassociateOpts{ + FloatingIP: floatingIP.IP, + } + + err := floatingips.DisassociateInstance(client, server.ID, disassociateOpts).ExtractErr() + if err != nil { + t.Fatalf("Unable to disassociate floating IP %s from server %s: %v", floatingIP.IP, server.ID, err) + } + + t.Logf("Disassociated floating IP %s from server %s", floatingIP.IP, server.ID) +} + +// GetNetworkIDFromNetworks will return the network ID from a specified network +// UUID using the os-networks API extension. An error will be returned if the +// network could not be retrieved. +func GetNetworkIDFromNetworks(t *testing.T, client *gophercloud.ServiceClient, networkName string) (string, error) { + allPages, err := networks.List(client).AllPages() + if err != nil { + t.Fatalf("Unable to list networks: %v", err) + } + + networkList, err := networks.ExtractNetworks(allPages) + if err != nil { + t.Fatalf("Unable to list networks: %v", err) + } + + networkID := "" + for _, network := range networkList { + t.Logf("Network: %v", network) + if network.Label == networkName { + networkID = network.ID + } + } + + t.Logf("Found network ID for %s: %s", networkName, networkID) + + return networkID, nil +} + +// GetNetworkIDFromTenantNetworks will return the network UUID for a given +// network name using the os-tenant-networks API extension. An error will be +// returned if the network could not be retrieved. +func GetNetworkIDFromTenantNetworks(t *testing.T, client *gophercloud.ServiceClient, networkName string) (string, error) { + allPages, err := tenantnetworks.List(client).AllPages() + if err != nil { + return "", err + } + + allTenantNetworks, err := tenantnetworks.ExtractNetworks(allPages) + if err != nil { + return "", err + } + + for _, network := range allTenantNetworks { + if network.Name == networkName { + return network.ID, nil + } + } + + return "", fmt.Errorf("Failed to obtain network ID for network %s", networkName) +} + +// ImportPublicKey will create a KeyPair with a random name and a specified +// public key. An error will be returned if the keypair failed to be created. +func ImportPublicKey(t *testing.T, client *gophercloud.ServiceClient, publicKey string) (*keypairs.KeyPair, error) { + keyPairName := tools.RandomString("keypair_", 5) + + t.Logf("Attempting to create keypair: %s", keyPairName) + createOpts := keypairs.CreateOpts{ + Name: keyPairName, + PublicKey: publicKey, + } + keyPair, err := keypairs.Create(client, createOpts).Extract() + if err != nil { + return keyPair, err + } + + t.Logf("Created keypair: %s", keyPairName) + return keyPair, nil +} + +// ResizeServer performs a resize action on an instance. An error will be +// returned if the instance failed to resize. +// The new flavor that the instance will be resized to is specified in OS_FLAVOR_ID_RESIZE. +func ResizeServer(t *testing.T, client *gophercloud.ServiceClient, server *servers.Server) error { + choices, err := clients.AcceptanceTestChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + opts := &servers.ResizeOpts{ + FlavorRef: choices.FlavorIDResize, + } + if res := servers.Resize(client, server.ID, opts); res.Err != nil { + return res.Err + } + + if err := WaitForComputeStatus(client, server, "VERIFY_RESIZE"); err != nil { + return err + } + + return nil +} + +// WaitForComputeStatus will poll an instance's status until it either matches +// the specified status or the status becomes ERROR. +func WaitForComputeStatus(client *gophercloud.ServiceClient, server *servers.Server, status string) error { + return tools.WaitFor(func() (bool, error) { + latest, err := servers.Get(client, server.ID).Extract() + if err != nil { + return false, err + } + + if latest.Status == status { + // Success! + return true, nil + } + + if latest.Status == "ERROR" { + return false, fmt.Errorf("Instance in ERROR state") + } + + return false, nil + }) +} + +//Convenience method to fill an QuotaSet-UpdateOpts-struct from a QuotaSet-struct +func FillUpdateOptsFromQuotaSet(src quotasets.QuotaSet, dest *quotasets.UpdateOpts) { + dest.FixedIps = &src.FixedIps + dest.FloatingIps = &src.FloatingIps + dest.InjectedFileContentBytes = &src.InjectedFileContentBytes + dest.InjectedFilePathBytes = &src.InjectedFilePathBytes + dest.InjectedFiles = &src.InjectedFiles + dest.KeyPairs = &src.KeyPairs + dest.Ram = &src.Ram + dest.SecurityGroupRules = &src.SecurityGroupRules + dest.SecurityGroups = &src.SecurityGroups + dest.Cores = &src.Cores + dest.Instances = &src.Instances + dest.ServerGroups = &src.ServerGroups + dest.ServerGroupMembers = &src.ServerGroupMembers + dest.MetadataItems = &src.MetadataItems +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/defsecrules_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/defsecrules_test.go new file mode 100644 index 0000000000..16c43f4c75 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/defsecrules_test.go @@ -0,0 +1,67 @@ +// +build acceptance compute defsecrules + +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + dsr "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/defsecrules" +) + +func TestDefSecRulesList(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + allPages, err := dsr.List(client).AllPages() + if err != nil { + t.Fatalf("Unable to list default rules: %v", err) + } + + allDefaultRules, err := dsr.ExtractDefaultRules(allPages) + if err != nil { + t.Fatalf("Unable to extract default rules: %v", err) + } + + for _, defaultRule := range allDefaultRules { + tools.PrintResource(t, defaultRule) + } +} + +func TestDefSecRulesCreate(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + defaultRule, err := CreateDefaultRule(t, client) + if err != nil { + t.Fatalf("Unable to create default rule: %v", err) + } + defer DeleteDefaultRule(t, client, defaultRule) + + tools.PrintResource(t, defaultRule) +} + +func TestDefSecRulesGet(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + defaultRule, err := CreateDefaultRule(t, client) + if err != nil { + t.Fatalf("Unable to create default rule: %v", err) + } + defer DeleteDefaultRule(t, client, defaultRule) + + newDefaultRule, err := dsr.Get(client, defaultRule.ID).Extract() + if err != nil { + t.Fatalf("Unable to get default rule %s: %v", defaultRule.ID, err) + } + + tools.PrintResource(t, newDefaultRule) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/extension_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/extension_test.go new file mode 100644 index 0000000000..5b2cf4a42d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/extension_test.go @@ -0,0 +1,46 @@ +// +build acceptance compute extensions + +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/common/extensions" +) + +func TestExtensionsList(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + allPages, err := extensions.List(client).AllPages() + if err != nil { + t.Fatalf("Unable to list extensions: %v", err) + } + + allExtensions, err := extensions.ExtractExtensions(allPages) + if err != nil { + t.Fatalf("Unable to extract extensions: %v", err) + } + + for _, extension := range allExtensions { + tools.PrintResource(t, extension) + } +} + +func TestExtensionGet(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + extension, err := extensions.Get(client, "os-admin-actions").Extract() + if err != nil { + t.Fatalf("Unable to get extension os-admin-actions: %v", err) + } + + tools.PrintResource(t, extension) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/flavors_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/flavors_test.go new file mode 100644 index 0000000000..64ffaccf85 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/flavors_test.go @@ -0,0 +1,74 @@ +// +build acceptance compute flavors + +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors" +) + +func TestFlavorsList(t *testing.T) { + t.Logf("** Default flavors (same as Project flavors): **") + t.Logf("") + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + allPages, err := flavors.ListDetail(client, nil).AllPages() + if err != nil { + t.Fatalf("Unable to retrieve flavors: %v", err) + } + + allFlavors, err := flavors.ExtractFlavors(allPages) + if err != nil { + t.Fatalf("Unable to extract flavor results: %v", err) + } + + for _, flavor := range allFlavors { + tools.PrintResource(t, flavor) + } + + flavorAccessTypes := [3]flavors.AccessType{flavors.PublicAccess, flavors.PrivateAccess, flavors.AllAccess} + for _, flavorAccessType := range flavorAccessTypes { + t.Logf("** %s flavors: **", flavorAccessType) + t.Logf("") + allPages, err := flavors.ListDetail(client, flavors.ListOpts{AccessType: flavorAccessType}).AllPages() + if err != nil { + t.Fatalf("Unable to retrieve flavors: %v", err) + } + + allFlavors, err := flavors.ExtractFlavors(allPages) + if err != nil { + t.Fatalf("Unable to extract flavor results: %v", err) + } + + for _, flavor := range allFlavors { + tools.PrintResource(t, flavor) + t.Logf("") + } + } + +} + +func TestFlavorsGet(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + choices, err := clients.AcceptanceTestChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + flavor, err := flavors.Get(client, choices.FlavorID).Extract() + if err != nil { + t.Fatalf("Unable to get flavor information: %v", err) + } + + tools.PrintResource(t, flavor) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/floatingip_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/floatingip_test.go new file mode 100644 index 0000000000..26b7bfe16a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/floatingip_test.go @@ -0,0 +1,148 @@ +// +build acceptance compute servers + +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" +) + +func TestFloatingIPsList(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + allPages, err := floatingips.List(client).AllPages() + if err != nil { + t.Fatalf("Unable to retrieve floating IPs: %v", err) + } + + allFloatingIPs, err := floatingips.ExtractFloatingIPs(allPages) + if err != nil { + t.Fatalf("Unable to extract floating IPs: %v", err) + } + + for _, floatingIP := range allFloatingIPs { + tools.PrintResource(t, floatingIP) + } +} + +func TestFloatingIPsCreate(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + floatingIP, err := CreateFloatingIP(t, client) + if err != nil { + t.Fatalf("Unable to create floating IP: %v", err) + } + defer DeleteFloatingIP(t, client, floatingIP) + + tools.PrintResource(t, floatingIP) +} + +func TestFloatingIPsAssociate(t *testing.T) { + if testing.Short() { + t.Skip("Skipping test that requires server creation in short mode.") + } + + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + server, err := CreateServer(t, client) + if err != nil { + t.Fatalf("Unable to create server: %v", err) + } + defer DeleteServer(t, client, server) + + floatingIP, err := CreateFloatingIP(t, client) + if err != nil { + t.Fatalf("Unable to create floating IP: %v", err) + } + defer DeleteFloatingIP(t, client, floatingIP) + + tools.PrintResource(t, floatingIP) + + err = AssociateFloatingIP(t, client, floatingIP, server) + if err != nil { + t.Fatalf("Unable to associate floating IP %s with server %s: %v", floatingIP.IP, server.ID, err) + } + defer DisassociateFloatingIP(t, client, floatingIP, server) + + newFloatingIP, err := floatingips.Get(client, floatingIP.ID).Extract() + if err != nil { + t.Fatalf("Unable to get floating IP %s: %v", floatingIP.ID, err) + } + + t.Logf("Floating IP %s is associated with Fixed IP %s", floatingIP.IP, newFloatingIP.FixedIP) + + tools.PrintResource(t, newFloatingIP) +} + +func TestFloatingIPsFixedIPAssociate(t *testing.T) { + if testing.Short() { + t.Skip("Skipping test that requires server creation in short mode.") + } + + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + choices, err := clients.AcceptanceTestChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + server, err := CreateServer(t, client) + if err != nil { + t.Fatalf("Unable to create server: %v", err) + } + defer DeleteServer(t, client, server) + + newServer, err := servers.Get(client, server.ID).Extract() + if err != nil { + t.Fatalf("Unable to get server %s: %v", server.ID, err) + } + + floatingIP, err := CreateFloatingIP(t, client) + if err != nil { + t.Fatalf("Unable to create floating IP: %v", err) + } + defer DeleteFloatingIP(t, client, floatingIP) + + tools.PrintResource(t, floatingIP) + + var fixedIP string + for _, networkAddresses := range newServer.Addresses[choices.NetworkName].([]interface{}) { + address := networkAddresses.(map[string]interface{}) + if address["OS-EXT-IPS:type"] == "fixed" { + if address["version"].(float64) == 4 { + fixedIP = address["addr"].(string) + } + } + } + + err = AssociateFloatingIPWithFixedIP(t, client, floatingIP, newServer, fixedIP) + if err != nil { + t.Fatalf("Unable to associate floating IP %s with server %s: %v", floatingIP.IP, newServer.ID, err) + } + defer DisassociateFloatingIP(t, client, floatingIP, newServer) + + newFloatingIP, err := floatingips.Get(client, floatingIP.ID).Extract() + if err != nil { + t.Fatalf("Unable to get floating IP %s: %v", floatingIP.ID, err) + } + + t.Logf("Floating IP %s is associated with Fixed IP %s", floatingIP.IP, newFloatingIP.FixedIP) + + tools.PrintResource(t, newFloatingIP) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/hypervisors_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/hypervisors_test.go new file mode 100644 index 0000000000..627dc76345 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/hypervisors_test.go @@ -0,0 +1,32 @@ +// +build acceptance compute hypervisors + +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors" +) + +func TestHypervisorsList(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + allPages, err := hypervisors.List(client).AllPages() + if err != nil { + t.Fatalf("Unable to list hypervisors: %v", err) + } + + allHypervisors, err := hypervisors.ExtractHypervisors(allPages) + if err != nil { + t.Fatalf("Unable to extract hypervisors") + } + + for _, h := range allHypervisors { + tools.PrintResource(t, h) + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/images_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/images_test.go new file mode 100644 index 0000000000..a34ce3ea62 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/images_test.go @@ -0,0 +1,51 @@ +// +build acceptance compute images + +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/compute/v2/images" +) + +func TestImagesList(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute: client: %v", err) + } + + allPages, err := images.ListDetail(client, nil).AllPages() + if err != nil { + t.Fatalf("Unable to retrieve images: %v", err) + } + + allImages, err := images.ExtractImages(allPages) + if err != nil { + t.Fatalf("Unable to extract image results: %v", err) + } + + for _, image := range allImages { + tools.PrintResource(t, image) + } +} + +func TestImagesGet(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute: client: %v", err) + } + + choices, err := clients.AcceptanceTestChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + image, err := images.Get(client, choices.ImageID).Extract() + if err != nil { + t.Fatalf("Unable to get image information: %v", err) + } + + tools.PrintResource(t, image) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/keypairs_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/keypairs_test.go new file mode 100644 index 0000000000..c4b91ec854 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/keypairs_test.go @@ -0,0 +1,107 @@ +// +build acceptance compute keypairs + +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" +) + +const keyName = "gophercloud_test_key_pair" + +func TestKeypairsList(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + allPages, err := keypairs.List(client).AllPages() + if err != nil { + t.Fatalf("Unable to retrieve keypairs: %s", err) + } + + allKeys, err := keypairs.ExtractKeyPairs(allPages) + if err != nil { + t.Fatalf("Unable to extract keypairs results: %s", err) + } + + for _, keypair := range allKeys { + tools.PrintResource(t, keypair) + } +} + +func TestKeypairsCreate(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + keyPair, err := CreateKeyPair(t, client) + if err != nil { + t.Fatalf("Unable to create key pair: %v", err) + } + defer DeleteKeyPair(t, client, keyPair) + + tools.PrintResource(t, keyPair) +} + +func TestKeypairsImportPublicKey(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + publicKey, err := createKey() + if err != nil { + t.Fatalf("Unable to create public key: %s", err) + } + + keyPair, err := ImportPublicKey(t, client, publicKey) + if err != nil { + t.Fatalf("Unable to create keypair: %s", err) + } + defer DeleteKeyPair(t, client, keyPair) + + tools.PrintResource(t, keyPair) +} + +func TestKeypairsServerCreateWithKey(t *testing.T) { + if testing.Short() { + t.Skip("Skipping test that requires server creation in short mode.") + } + + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + publicKey, err := createKey() + if err != nil { + t.Fatalf("Unable to create public key: %s", err) + } + + keyPair, err := ImportPublicKey(t, client, publicKey) + if err != nil { + t.Fatalf("Unable to create keypair: %s", err) + } + defer DeleteKeyPair(t, client, keyPair) + + server, err := CreateServerWithPublicKey(t, client, keyPair.Name) + if err != nil { + t.Fatalf("Unable to create server: %s", err) + } + defer DeleteServer(t, client, server) + + server, err = servers.Get(client, server.ID).Extract() + if err != nil { + t.Fatalf("Unable to retrieve server: %s", err) + } + + if server.KeyName != keyPair.Name { + t.Fatalf("key name of server %s is %s, not %s", server.ID, server.KeyName, keyPair.Name) + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/limits_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/limits_test.go new file mode 100644 index 0000000000..2bf5ce6b85 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/limits_test.go @@ -0,0 +1,52 @@ +// +build acceptance compute limits + +package v2 + +import ( + "strings" + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/limits" +) + +func TestLimits(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + limits, err := limits.Get(client, nil).Extract() + if err != nil { + t.Fatalf("Unable to get limits: %v", err) + } + + t.Logf("Limits for scoped user:") + t.Logf("%#v", limits) +} + +func TestLimitsForTenant(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + // I think this is the easiest way to get the tenant ID while being + // agnostic to Identity v2 and v3. + // Technically we're just returning the limits for ourselves, but it's + // the fact that we're specifying a tenant ID that is important here. + endpointParts := strings.Split(client.Endpoint, "/") + tenantID := endpointParts[4] + + getOpts := limits.GetOpts{ + TenantID: tenantID, + } + + limits, err := limits.Get(client, getOpts).Extract() + if err != nil { + t.Fatalf("Unable to get absolute limits: %v", err) + } + + t.Logf("Limits for tenant %s:", tenantID) + t.Logf("%#v", limits) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/network_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/network_test.go new file mode 100644 index 0000000000..745151829d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/network_test.go @@ -0,0 +1,56 @@ +// +build acceptance compute servers + +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/networks" +) + +func TestNetworksList(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + allPages, err := networks.List(client).AllPages() + if err != nil { + t.Fatalf("Unable to list networks: %v", err) + } + + allNetworks, err := networks.ExtractNetworks(allPages) + if err != nil { + t.Fatalf("Unable to list networks: %v", err) + } + + for _, network := range allNetworks { + tools.PrintResource(t, network) + } +} + +func TestNetworksGet(t *testing.T) { + choices, err := clients.AcceptanceTestChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + networkID, err := GetNetworkIDFromNetworks(t, client, choices.NetworkName) + if err != nil { + t.Fatal(err) + } + + network, err := networks.Get(client, networkID).Extract() + if err != nil { + t.Fatalf("Unable to get network %s: %v", networkID, err) + } + + tools.PrintResource(t, network) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/pkg.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/pkg.go new file mode 100644 index 0000000000..a57c1e7bfa --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/pkg.go @@ -0,0 +1,2 @@ +// Package v2 package contains acceptance tests for the Openstack Compute V2 service. +package v2 diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/quotaset_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/quotaset_test.go new file mode 100644 index 0000000000..126bc239db --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/quotaset_test.go @@ -0,0 +1,184 @@ +// +build acceptance compute quotasets + +package v2 + +import ( + "fmt" + "os" + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets" + "github.com/gophercloud/gophercloud/openstack/identity/v2/tenants" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestQuotasetGet(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + identityClient, err := clients.NewIdentityV2Client() + if err != nil { + t.Fatalf("Unable to get a new identity client: %v", err) + } + + tenantID, err := getTenantID(t, identityClient) + if err != nil { + t.Fatal(err) + } + + quotaSet, err := quotasets.Get(client, tenantID).Extract() + if err != nil { + t.Fatal(err) + } + + tools.PrintResource(t, quotaSet) +} + +func getTenantID(t *testing.T, client *gophercloud.ServiceClient) (string, error) { + allPages, err := tenants.List(client, nil).AllPages() + if err != nil { + t.Fatalf("Unable to get list of tenants: %v", err) + } + + allTenants, err := tenants.ExtractTenants(allPages) + if err != nil { + t.Fatalf("Unable to extract tenants: %v", err) + } + + for _, tenant := range allTenants { + return tenant.ID, nil + } + + return "", fmt.Errorf("Unable to get tenant ID") +} + +func getTenantIDByName(t *testing.T, client *gophercloud.ServiceClient, name string) (string, error) { + allPages, err := tenants.List(client, nil).AllPages() + if err != nil { + t.Fatalf("Unable to get list of tenants: %v", err) + } + + allTenants, err := tenants.ExtractTenants(allPages) + if err != nil { + t.Fatalf("Unable to extract tenants: %v", err) + } + + for _, tenant := range allTenants { + if tenant.Name == name { + return tenant.ID, nil + } + } + + return "", fmt.Errorf("Unable to get tenant ID") +} + +//What will be sent as desired Quotas to the Server +var UpdatQuotaOpts = quotasets.UpdateOpts{ + FixedIps: gophercloud.IntToPointer(10), + FloatingIps: gophercloud.IntToPointer(10), + InjectedFileContentBytes: gophercloud.IntToPointer(10240), + InjectedFilePathBytes: gophercloud.IntToPointer(255), + InjectedFiles: gophercloud.IntToPointer(5), + KeyPairs: gophercloud.IntToPointer(10), + MetadataItems: gophercloud.IntToPointer(128), + Ram: gophercloud.IntToPointer(20000), + SecurityGroupRules: gophercloud.IntToPointer(20), + SecurityGroups: gophercloud.IntToPointer(10), + Cores: gophercloud.IntToPointer(10), + Instances: gophercloud.IntToPointer(4), + ServerGroups: gophercloud.IntToPointer(2), + ServerGroupMembers: gophercloud.IntToPointer(3), +} + +//What the Server hopefully returns as the new Quotas +var UpdatedQuotas = quotasets.QuotaSet{ + FixedIps: 10, + FloatingIps: 10, + InjectedFileContentBytes: 10240, + InjectedFilePathBytes: 255, + InjectedFiles: 5, + KeyPairs: 10, + MetadataItems: 128, + Ram: 20000, + SecurityGroupRules: 20, + SecurityGroups: 10, + Cores: 10, + Instances: 4, + ServerGroups: 2, + ServerGroupMembers: 3, +} + +func TestQuotasetUpdateDelete(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + idclient, err := clients.NewIdentityV2Client() + if err != nil { + t.Fatalf("Could not create IdentityClient to look up tenant id!") + } + + tenantid, err := getTenantIDByName(t, idclient, os.Getenv("OS_TENANT_NAME")) + if err != nil { + t.Fatalf("Id for Tenant named '%' not found. Please set OS_TENANT_NAME appropriately", os.Getenv("OS_TENANT_NAME")) + } + + //save original quotas + orig, err := quotasets.Get(client, tenantid).Extract() + th.AssertNoErr(t, err) + + //Test Update + res, err := quotasets.Update(client, tenantid, UpdatQuotaOpts).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, UpdatedQuotas, *res) + + //Test Delete + _, err = quotasets.Delete(client, tenantid).Extract() + th.AssertNoErr(t, err) + //We dont know the default quotas, so just check if the quotas are not the same as before + newres, err := quotasets.Get(client, tenantid).Extract() + if newres == res { + t.Fatalf("Quotas after delete equal quotas before delete!") + } + + restore := quotasets.UpdateOpts{} + FillUpdateOptsFromQuotaSet(*orig, &restore) + + //restore original quotas + res, err = quotasets.Update(client, tenantid, restore).Extract() + th.AssertNoErr(t, err) + + orig.ID = "" + th.AssertEquals(t, *orig, *res) + +} + +// Makes sure that the FillUpdateOptsFromQuotaSet() helper function works properly +func TestFillFromQuotaSetHelperFunction(t *testing.T) { + op := "asets.UpdateOpts{} + expected := ` + { + "fixed_ips": 10, + "floating_ips": 10, + "injected_file_content_bytes": 10240, + "injected_file_path_bytes": 255, + "injected_files": 5, + "key_pairs": 10, + "metadata_items": 128, + "ram": 20000, + "security_group_rules": 20, + "security_groups": 10, + "cores": 10, + "instances": 4, + "server_groups": 2, + "server_group_members": 3 + }` + FillUpdateOptsFromQuotaSet(UpdatedQuotas, op) + th.AssertJSONEquals(t, expected, op) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/secgroup_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/secgroup_test.go new file mode 100644 index 0000000000..c0d023037d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/secgroup_test.go @@ -0,0 +1,137 @@ +// +build acceptance compute secgroups + +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups" +) + +func TestSecGroupsList(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + allPages, err := secgroups.List(client).AllPages() + if err != nil { + t.Fatalf("Unable to retrieve security groups: %v", err) + } + + allSecGroups, err := secgroups.ExtractSecurityGroups(allPages) + if err != nil { + t.Fatalf("Unable to extract security groups: %v", err) + } + + for _, secgroup := range allSecGroups { + tools.PrintResource(t, secgroup) + } +} + +func TestSecGroupsCreate(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + securityGroup, err := CreateSecurityGroup(t, client) + if err != nil { + t.Fatalf("Unable to create security group: %v", err) + } + defer DeleteSecurityGroup(t, client, securityGroup) +} + +func TestSecGroupsUpdate(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + securityGroup, err := CreateSecurityGroup(t, client) + if err != nil { + t.Fatalf("Unable to create security group: %v", err) + } + defer DeleteSecurityGroup(t, client, securityGroup) + + updateOpts := secgroups.UpdateOpts{ + Name: tools.RandomString("secgroup_", 4), + Description: tools.RandomString("dec_", 10), + } + updatedSecurityGroup, err := secgroups.Update(client, securityGroup.ID, updateOpts).Extract() + if err != nil { + t.Fatalf("Unable to update security group: %v", err) + } + + t.Logf("Updated %s's name to %s", updatedSecurityGroup.ID, updatedSecurityGroup.Name) +} + +func TestSecGroupsRuleCreate(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + securityGroup, err := CreateSecurityGroup(t, client) + if err != nil { + t.Fatalf("Unable to create security group: %v", err) + } + defer DeleteSecurityGroup(t, client, securityGroup) + + rule, err := CreateSecurityGroupRule(t, client, securityGroup.ID) + if err != nil { + t.Fatalf("Unable to create rule: %v", err) + } + defer DeleteSecurityGroupRule(t, client, rule) + + newSecurityGroup, err := secgroups.Get(client, securityGroup.ID).Extract() + if err != nil { + t.Fatalf("Unable to obtain security group: %v", err) + } + + tools.PrintResource(t, newSecurityGroup) + +} + +func TestSecGroupsAddGroupToServer(t *testing.T) { + if testing.Short() { + t.Skip("Skipping test that requires server creation in short mode.") + } + + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + server, err := CreateServer(t, client) + if err != nil { + t.Fatalf("Unable to create server: %v", err) + } + defer DeleteServer(t, client, server) + + securityGroup, err := CreateSecurityGroup(t, client) + if err != nil { + t.Fatalf("Unable to create security group: %v", err) + } + defer DeleteSecurityGroup(t, client, securityGroup) + + rule, err := CreateSecurityGroupRule(t, client, securityGroup.ID) + if err != nil { + t.Fatalf("Unable to create rule: %v", err) + } + defer DeleteSecurityGroupRule(t, client, rule) + + t.Logf("Adding group %s to server %s", securityGroup.ID, server.ID) + err = secgroups.AddServer(client, server.ID, securityGroup.Name).ExtractErr() + if err != nil && err.Error() != "EOF" { + t.Fatalf("Unable to add group %s to server %s: %s", securityGroup.ID, server.ID, err) + } + + t.Logf("Removing group %s from server %s", securityGroup.ID, server.ID) + err = secgroups.RemoveServer(client, server.ID, securityGroup.Name).ExtractErr() + if err != nil && err.Error() != "EOF" { + t.Fatalf("Unable to remove group %s from server %s: %s", securityGroup.ID, server.ID, err) + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/servergroup_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/servergroup_test.go new file mode 100644 index 0000000000..547b82fd5a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/servergroup_test.go @@ -0,0 +1,93 @@ +// +build acceptance compute servergroups + +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" +) + +func TestServergroupsList(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + allPages, err := servergroups.List(client).AllPages() + if err != nil { + t.Fatalf("Unable to list server groups: %v", err) + } + + allServerGroups, err := servergroups.ExtractServerGroups(allPages) + if err != nil { + t.Fatalf("Unable to extract server groups: %v", err) + } + + for _, serverGroup := range allServerGroups { + tools.PrintResource(t, serverGroup) + } +} + +func TestServergroupsCreate(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + serverGroup, err := CreateServerGroup(t, client, "anti-affinity") + if err != nil { + t.Fatalf("Unable to create server group: %v", err) + } + defer DeleteServerGroup(t, client, serverGroup) + + serverGroup, err = servergroups.Get(client, serverGroup.ID).Extract() + if err != nil { + t.Fatalf("Unable to get server group: %v", err) + } + + tools.PrintResource(t, serverGroup) +} + +func TestServergroupsAffinityPolicy(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + serverGroup, err := CreateServerGroup(t, client, "affinity") + if err != nil { + t.Fatalf("Unable to create server group: %v", err) + } + defer DeleteServerGroup(t, client, serverGroup) + + firstServer, err := CreateServerInServerGroup(t, client, serverGroup) + if err != nil { + t.Fatalf("Unable to create server: %v", err) + } + if err = WaitForComputeStatus(client, firstServer, "ACTIVE"); err != nil { + t.Fatalf("Unable to wait for server: %v", err) + } + defer DeleteServer(t, client, firstServer) + + firstServer, err = servers.Get(client, firstServer.ID).Extract() + + secondServer, err := CreateServerInServerGroup(t, client, serverGroup) + if err != nil { + t.Fatalf("Unable to create server: %v", err) + } + + if err = WaitForComputeStatus(client, secondServer, "ACTIVE"); err != nil { + t.Fatalf("Unable to wait for server: %v", err) + } + defer DeleteServer(t, client, secondServer) + + secondServer, err = servers.Get(client, secondServer.ID).Extract() + + if firstServer.HostID != secondServer.HostID { + t.Fatalf("%s and %s were not scheduled on the same host.", firstServer.ID, secondServer.ID) + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/servers_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/servers_test.go new file mode 100644 index 0000000000..d545a43252 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/servers_test.go @@ -0,0 +1,479 @@ +// +build acceptance compute servers + +package v2 + +import ( + "strings" + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/attachinterfaces" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/availabilityzones" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/pauseunpause" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/suspendresume" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestServersList(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + allPages, err := servers.List(client, servers.ListOpts{}).AllPages() + if err != nil { + t.Fatalf("Unable to retrieve servers: %v", err) + } + + allServers, err := servers.ExtractServers(allPages) + if err != nil { + t.Fatalf("Unable to extract servers: %v", err) + } + + for _, server := range allServers { + tools.PrintResource(t, server) + } +} + +func TestServersCreateDestroy(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + choices, err := clients.AcceptanceTestChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + server, err := CreateServer(t, client) + if err != nil { + t.Fatalf("Unable to create server: %v", err) + } + + defer DeleteServer(t, client, server) + + newServer, err := servers.Get(client, server.ID).Extract() + if err != nil { + t.Errorf("Unable to retrieve server: %v", err) + } + tools.PrintResource(t, newServer) + + allAddressPages, err := servers.ListAddresses(client, server.ID).AllPages() + if err != nil { + t.Errorf("Unable to list server addresses: %v", err) + } + + allAddresses, err := servers.ExtractAddresses(allAddressPages) + if err != nil { + t.Errorf("Unable to extract server addresses: %v", err) + } + + for network, address := range allAddresses { + t.Logf("Addresses on %s: %+v", network, address) + } + + allInterfacePages, err := attachinterfaces.List(client, server.ID).AllPages() + if err != nil { + t.Errorf("Unable to list server Interfaces: %v", err) + } + + allInterfaces, err := attachinterfaces.ExtractInterfaces(allInterfacePages) + if err != nil { + t.Errorf("Unable to extract server Interfaces: %v", err) + } + + for _, Interface := range allInterfaces { + t.Logf("Interfaces: %+v", Interface) + } + + allNetworkAddressPages, err := servers.ListAddressesByNetwork(client, server.ID, choices.NetworkName).AllPages() + if err != nil { + t.Errorf("Unable to list server addresses: %v", err) + } + + allNetworkAddresses, err := servers.ExtractNetworkAddresses(allNetworkAddressPages) + if err != nil { + t.Errorf("Unable to extract server addresses: %v", err) + } + + t.Logf("Addresses on %s:", choices.NetworkName) + for _, address := range allNetworkAddresses { + t.Logf("%+v", address) + } +} + +func TestServersCreateDestroyWithExtensions(t *testing.T) { + var extendedServer struct { + servers.Server + availabilityzones.ServerExt + } + + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + server, err := CreateServer(t, client) + if err != nil { + t.Fatalf("Unable to create server: %v", err) + } + defer DeleteServer(t, client, server) + + err = servers.Get(client, server.ID).ExtractInto(&extendedServer) + if err != nil { + t.Errorf("Unable to retrieve server: %v", err) + } + tools.PrintResource(t, extendedServer) +} + +func TestServersWithoutImageRef(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + server, err := CreateServerWithoutImageRef(t, client) + if err != nil { + if err400, ok := err.(*gophercloud.ErrUnexpectedResponseCode); ok { + if !strings.Contains("Missing imageRef attribute", string(err400.Body)) { + defer DeleteServer(t, client, server) + } + } + } +} + +func TestServersUpdate(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + server, err := CreateServer(t, client) + if err != nil { + t.Fatal(err) + } + defer DeleteServer(t, client, server) + + alternateName := tools.RandomString("ACPTTEST", 16) + for alternateName == server.Name { + alternateName = tools.RandomString("ACPTTEST", 16) + } + + t.Logf("Attempting to rename the server to %s.", alternateName) + + updateOpts := servers.UpdateOpts{ + Name: alternateName, + } + + updated, err := servers.Update(client, server.ID, updateOpts).Extract() + if err != nil { + t.Fatalf("Unable to rename server: %v", err) + } + + if updated.ID != server.ID { + t.Errorf("Updated server ID [%s] didn't match original server ID [%s]!", updated.ID, server.ID) + } + + err = tools.WaitFor(func() (bool, error) { + latest, err := servers.Get(client, updated.ID).Extract() + if err != nil { + return false, err + } + + return latest.Name == alternateName, nil + }) +} + +func TestServersMetadata(t *testing.T) { + t.Parallel() + + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + server, err := CreateServer(t, client) + if err != nil { + t.Fatal(err) + } + defer DeleteServer(t, client, server) + + metadata, err := servers.UpdateMetadata(client, server.ID, servers.MetadataOpts{ + "foo": "bar", + "this": "that", + }).Extract() + if err != nil { + t.Fatalf("Unable to update metadata: %v", err) + } + t.Logf("UpdateMetadata result: %+v\n", metadata) + + err = servers.DeleteMetadatum(client, server.ID, "foo").ExtractErr() + if err != nil { + t.Fatalf("Unable to delete metadatum: %v", err) + } + + metadata, err = servers.CreateMetadatum(client, server.ID, servers.MetadatumOpts{ + "foo": "baz", + }).Extract() + if err != nil { + t.Fatalf("Unable to create metadatum: %v", err) + } + t.Logf("CreateMetadatum result: %+v\n", metadata) + + metadata, err = servers.Metadatum(client, server.ID, "foo").Extract() + if err != nil { + t.Fatalf("Unable to get metadatum: %v", err) + } + t.Logf("Metadatum result: %+v\n", metadata) + th.AssertEquals(t, "baz", metadata["foo"]) + + metadata, err = servers.Metadata(client, server.ID).Extract() + if err != nil { + t.Fatalf("Unable to get metadata: %v", err) + } + t.Logf("Metadata result: %+v\n", metadata) + + metadata, err = servers.ResetMetadata(client, server.ID, servers.MetadataOpts{}).Extract() + if err != nil { + t.Fatalf("Unable to reset metadata: %v", err) + } + t.Logf("ResetMetadata result: %+v\n", metadata) + th.AssertDeepEquals(t, map[string]string{}, metadata) +} + +func TestServersActionChangeAdminPassword(t *testing.T) { + t.Parallel() + + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + server, err := CreateServer(t, client) + if err != nil { + t.Fatal(err) + } + defer DeleteServer(t, client, server) + + randomPassword := tools.MakeNewPassword(server.AdminPass) + res := servers.ChangeAdminPassword(client, server.ID, randomPassword) + if res.Err != nil { + t.Fatal(res.Err) + } + + if err = WaitForComputeStatus(client, server, "PASSWORD"); err != nil { + t.Fatal(err) + } + + if err = WaitForComputeStatus(client, server, "ACTIVE"); err != nil { + t.Fatal(err) + } +} + +func TestServersActionReboot(t *testing.T) { + t.Parallel() + + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + server, err := CreateServer(t, client) + if err != nil { + t.Fatal(err) + } + defer DeleteServer(t, client, server) + + rebootOpts := &servers.RebootOpts{ + Type: servers.SoftReboot, + } + + t.Logf("Attempting reboot of server %s", server.ID) + res := servers.Reboot(client, server.ID, rebootOpts) + if res.Err != nil { + t.Fatalf("Unable to reboot server: %v", res.Err) + } + + if err = WaitForComputeStatus(client, server, "REBOOT"); err != nil { + t.Fatal(err) + } + + if err = WaitForComputeStatus(client, server, "ACTIVE"); err != nil { + t.Fatal(err) + } +} + +func TestServersActionRebuild(t *testing.T) { + t.Parallel() + + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + choices, err := clients.AcceptanceTestChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + server, err := CreateServer(t, client) + if err != nil { + t.Fatal(err) + } + defer DeleteServer(t, client, server) + + t.Logf("Attempting to rebuild server %s", server.ID) + + rebuildOpts := servers.RebuildOpts{ + Name: tools.RandomString("ACPTTEST", 16), + AdminPass: tools.MakeNewPassword(server.AdminPass), + ImageID: choices.ImageID, + } + + rebuilt, err := servers.Rebuild(client, server.ID, rebuildOpts).Extract() + if err != nil { + t.Fatal(err) + } + + if rebuilt.ID != server.ID { + t.Errorf("Expected rebuilt server ID of [%s]; got [%s]", server.ID, rebuilt.ID) + } + + if err = WaitForComputeStatus(client, rebuilt, "REBUILD"); err != nil { + t.Fatal(err) + } + + if err = WaitForComputeStatus(client, rebuilt, "ACTIVE"); err != nil { + t.Fatal(err) + } +} + +func TestServersActionResizeConfirm(t *testing.T) { + t.Parallel() + + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + server, err := CreateServer(t, client) + if err != nil { + t.Fatal(err) + } + defer DeleteServer(t, client, server) + + t.Logf("Attempting to resize server %s", server.ID) + ResizeServer(t, client, server) + + t.Logf("Attempting to confirm resize for server %s", server.ID) + if res := servers.ConfirmResize(client, server.ID); res.Err != nil { + t.Fatal(res.Err) + } + + if err = WaitForComputeStatus(client, server, "ACTIVE"); err != nil { + t.Fatal(err) + } +} + +func TestServersActionResizeRevert(t *testing.T) { + t.Parallel() + + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + server, err := CreateServer(t, client) + if err != nil { + t.Fatal(err) + } + defer DeleteServer(t, client, server) + + t.Logf("Attempting to resize server %s", server.ID) + ResizeServer(t, client, server) + + t.Logf("Attempting to revert resize for server %s", server.ID) + if res := servers.RevertResize(client, server.ID); res.Err != nil { + t.Fatal(res.Err) + } + + if err = WaitForComputeStatus(client, server, "ACTIVE"); err != nil { + t.Fatal(err) + } +} + +func TestServersActionPause(t *testing.T) { + t.Parallel() + + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + server, err := CreateServer(t, client) + if err != nil { + t.Fatal(err) + } + defer DeleteServer(t, client, server) + + t.Logf("Attempting to pause server %s", server.ID) + err = pauseunpause.Pause(client, server.ID).ExtractErr() + if err != nil { + t.Fatal(err) + } + + err = WaitForComputeStatus(client, server, "PAUSED") + if err != nil { + t.Fatal(err) + } + + err = pauseunpause.Unpause(client, server.ID).ExtractErr() + if err != nil { + t.Fatal(err) + } + + err = WaitForComputeStatus(client, server, "ACTIVE") + if err != nil { + t.Fatal(err) + } +} + +func TestServersActionSuspend(t *testing.T) { + t.Parallel() + + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + server, err := CreateServer(t, client) + if err != nil { + t.Fatal(err) + } + defer DeleteServer(t, client, server) + + t.Logf("Attempting to suspend server %s", server.ID) + err = suspendresume.Suspend(client, server.ID).ExtractErr() + if err != nil { + t.Fatal(err) + } + + err = WaitForComputeStatus(client, server, "SUSPENDED") + if err != nil { + t.Fatal(err) + } + + err = suspendresume.Resume(client, server.ID).ExtractErr() + if err != nil { + t.Fatal(err) + } + + err = WaitForComputeStatus(client, server, "ACTIVE") + if err != nil { + t.Fatal(err) + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/tenantnetworks_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/tenantnetworks_test.go new file mode 100644 index 0000000000..9b6b527022 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/tenantnetworks_test.go @@ -0,0 +1,56 @@ +// +build acceptance compute servers + +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/tenantnetworks" +) + +func TestTenantNetworksList(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + allPages, err := tenantnetworks.List(client).AllPages() + if err != nil { + t.Fatalf("Unable to list networks: %v", err) + } + + allTenantNetworks, err := tenantnetworks.ExtractNetworks(allPages) + if err != nil { + t.Fatalf("Unable to list networks: %v", err) + } + + for _, network := range allTenantNetworks { + tools.PrintResource(t, network) + } +} + +func TestTenantNetworksGet(t *testing.T) { + choices, err := clients.AcceptanceTestChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + networkID, err := GetNetworkIDFromTenantNetworks(t, client, choices.NetworkName) + if err != nil { + t.Fatal(err) + } + + network, err := tenantnetworks.Get(client, networkID).Extract() + if err != nil { + t.Fatalf("Unable to get network %s: %v", networkID, err) + } + + tools.PrintResource(t, network) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/volumeattach_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/volumeattach_test.go new file mode 100644 index 0000000000..78d85a9bfc --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2/volumeattach_test.go @@ -0,0 +1,78 @@ +// +build acceptance compute volumeattach + +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes" +) + +func TestVolumeAttachAttachment(t *testing.T) { + if testing.Short() { + t.Skip("Skipping test that requires server creation in short mode.") + } + + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + blockClient, err := clients.NewBlockStorageV1Client() + if err != nil { + t.Fatalf("Unable to create a blockstorage client: %v", err) + } + + server, err := CreateServer(t, client) + if err != nil { + t.Fatalf("Unable to create server: %v", err) + } + defer DeleteServer(t, client, server) + + volume, err := createVolume(t, blockClient) + if err != nil { + t.Fatalf("Unable to create volume: %v", err) + } + + if err = volumes.WaitForStatus(blockClient, volume.ID, "available", 60); err != nil { + t.Fatalf("Unable to wait for volume: %v", err) + } + defer deleteVolume(t, blockClient, volume) + + volumeAttachment, err := CreateVolumeAttachment(t, client, blockClient, server, volume) + if err != nil { + t.Fatalf("Unable to attach volume: %v", err) + } + defer DeleteVolumeAttachment(t, client, blockClient, server, volumeAttachment) + + tools.PrintResource(t, volumeAttachment) + +} + +func createVolume(t *testing.T, blockClient *gophercloud.ServiceClient) (*volumes.Volume, error) { + volumeName := tools.RandomString("ACPTTEST", 16) + createOpts := volumes.CreateOpts{ + Size: 1, + Name: volumeName, + } + + volume, err := volumes.Create(blockClient, createOpts).Extract() + if err != nil { + return volume, err + } + + t.Logf("Created volume: %s", volume.ID) + return volume, nil +} + +func deleteVolume(t *testing.T, blockClient *gophercloud.ServiceClient, volume *volumes.Volume) { + err := volumes.Delete(blockClient, volume.ID).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete volume: %v", err) + } + + t.Logf("Deleted volume: %s", volume.ID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/db/v1/common.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/db/v1/common.go new file mode 100644 index 0000000000..bbe7ebd6fd --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/db/v1/common.go @@ -0,0 +1,70 @@ +// +build acceptance db + +package v1 + +import ( + "os" + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack" + "github.com/gophercloud/gophercloud/openstack/db/v1/instances" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func newClient(t *testing.T) *gophercloud.ServiceClient { + ao, err := openstack.AuthOptionsFromEnv() + th.AssertNoErr(t, err) + + client, err := openstack.AuthenticatedClient(ao) + th.AssertNoErr(t, err) + + c, err := openstack.NewDBV1(client, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) + th.AssertNoErr(t, err) + + return c +} + +type context struct { + test *testing.T + client *gophercloud.ServiceClient + instanceID string + DBIDs []string + users []string +} + +func newContext(t *testing.T) context { + return context{ + test: t, + client: newClient(t), + } +} + +func (c context) Logf(msg string, args ...interface{}) { + if len(args) > 0 { + c.test.Logf(msg, args...) + } else { + c.test.Log(msg) + } +} + +func (c context) AssertNoErr(err error) { + th.AssertNoErr(c.test, err) +} + +func (c context) WaitUntilActive(id string) { + err := gophercloud.WaitFor(60, func() (bool, error) { + inst, err := instances.Get(c.client, id).Extract() + if err != nil { + return false, err + } + if inst.Status == "ACTIVE" { + return true, nil + } + return false, nil + }) + + c.AssertNoErr(err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/db/v1/database_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/db/v1/database_test.go new file mode 100644 index 0000000000..c52357a6be --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/db/v1/database_test.go @@ -0,0 +1,45 @@ +// +build acceptance db + +package v1 + +import ( + db "github.com/gophercloud/gophercloud/openstack/db/v1/databases" + "github.com/gophercloud/gophercloud/pagination" +) + +func (c context) createDBs() { + opts := db.BatchCreateOpts{ + db.CreateOpts{Name: "db1"}, + db.CreateOpts{Name: "db2"}, + db.CreateOpts{Name: "db3"}, + } + + err := db.Create(c.client, c.instanceID, opts).ExtractErr() + c.AssertNoErr(err) + c.Logf("Created three databases on instance %s: db1, db2, db3", c.instanceID) +} + +func (c context) listDBs() { + c.Logf("Listing databases on instance %s", c.instanceID) + + err := db.List(c.client, c.instanceID).EachPage(func(page pagination.Page) (bool, error) { + dbList, err := db.ExtractDBs(page) + c.AssertNoErr(err) + + for _, db := range dbList { + c.Logf("DB: %#v", db) + } + + return true, nil + }) + + c.AssertNoErr(err) +} + +func (c context) deleteDBs() { + for _, id := range []string{"db1", "db2", "db3"} { + err := db.Delete(c.client, c.instanceID, id).ExtractErr() + c.AssertNoErr(err) + c.Logf("Deleted DB %s", id) + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/db/v1/flavor_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/db/v1/flavor_test.go new file mode 100644 index 0000000000..6440cc9278 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/db/v1/flavor_test.go @@ -0,0 +1,31 @@ +// +build acceptance db + +package v1 + +import ( + "github.com/gophercloud/gophercloud/openstack/db/v1/flavors" + "github.com/gophercloud/gophercloud/pagination" +) + +func (c context) listFlavors() { + c.Logf("Listing flavors") + + err := flavors.List(c.client).EachPage(func(page pagination.Page) (bool, error) { + flavorList, err := flavors.ExtractFlavors(page) + c.AssertNoErr(err) + + for _, f := range flavorList { + c.Logf("Flavor: ID [%s] Name [%s] RAM [%d]", f.ID, f.Name, f.RAM) + } + + return true, nil + }) + + c.AssertNoErr(err) +} + +func (c context) getFlavor() { + flavor, err := flavors.Get(c.client, "1").Extract() + c.Logf("Getting flavor %s", flavor.ID) + c.AssertNoErr(err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/db/v1/instance_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/db/v1/instance_test.go new file mode 100644 index 0000000000..75668a2297 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/db/v1/instance_test.go @@ -0,0 +1,138 @@ +// +build acceptance db + +package v1 + +import ( + "os" + "testing" + + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/db/v1/instances" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" +) + +const envDSType = "DATASTORE_TYPE_ID" + +func TestRunner(t *testing.T) { + c := newContext(t) + + // FLAVOR tests + c.listFlavors() + c.getFlavor() + + // INSTANCE tests + c.createInstance() + c.listInstances() + c.getInstance() + c.isRootEnabled() + c.enableRootUser() + c.isRootEnabled() + c.restartInstance() + //c.resizeInstance() + //c.resizeVol() + + // DATABASE tests + c.createDBs() + c.listDBs() + + // USER tests + c.createUsers() + c.listUsers() + + // TEARDOWN + c.deleteUsers() + c.deleteDBs() + c.deleteInstance() +} + +func (c context) createInstance() { + if os.Getenv(envDSType) == "" { + c.test.Fatalf("%s must be set as an environment var", envDSType) + } + + opts := instances.CreateOpts{ + FlavorRef: "2", + Size: 5, + Name: tools.RandomString("gopher_db", 5), + Datastore: &instances.DatastoreOpts{Type: os.Getenv(envDSType)}, + } + + instance, err := instances.Create(c.client, opts).Extract() + th.AssertNoErr(c.test, err) + + c.Logf("Restarting %s. Waiting...", instance.ID) + c.WaitUntilActive(instance.ID) + c.Logf("Created Instance %s", instance.ID) + + c.instanceID = instance.ID +} + +func (c context) listInstances() { + c.Logf("Listing instances") + + err := instances.List(c.client).EachPage(func(page pagination.Page) (bool, error) { + instanceList, err := instances.ExtractInstances(page) + c.AssertNoErr(err) + + for _, i := range instanceList { + c.Logf("Instance: ID [%s] Name [%s] Status [%s] VolSize [%d] Datastore Type [%s]", + i.ID, i.Name, i.Status, i.Volume.Size, i.Datastore.Type) + } + + return true, nil + }) + + c.AssertNoErr(err) +} + +func (c context) getInstance() { + instance, err := instances.Get(c.client, c.instanceID).Extract() + c.AssertNoErr(err) + c.Logf("Getting instance: %s", instance.ID) +} + +func (c context) deleteInstance() { + err := instances.Delete(c.client, c.instanceID).ExtractErr() + c.AssertNoErr(err) + c.Logf("Deleted instance %s", c.instanceID) +} + +func (c context) enableRootUser() { + _, err := instances.EnableRootUser(c.client, c.instanceID).Extract() + c.AssertNoErr(err) + c.Logf("Enabled root user on %s", c.instanceID) +} + +func (c context) isRootEnabled() { + enabled, err := instances.IsRootEnabled(c.client, c.instanceID) + c.AssertNoErr(err) + c.Logf("Is root enabled? %d", enabled) +} + +func (c context) restartInstance() { + id := c.instanceID + err := instances.Restart(c.client, id).ExtractErr() + c.AssertNoErr(err) + c.Logf("Restarting %s. Waiting...", id) + c.WaitUntilActive(id) + c.Logf("Restarted %s", id) +} + +func (c context) resizeInstance() { + id := c.instanceID + err := instances.Resize(c.client, id, "3").ExtractErr() + c.AssertNoErr(err) + c.Logf("Resizing %s. Waiting...", id) + c.WaitUntilActive(id) + c.Logf("Resized %s with flavorRef %s", id, "2") +} + +func (c context) resizeVol() { + id := c.instanceID + err := instances.ResizeVolume(c.client, id, 4).ExtractErr() + c.AssertNoErr(err) + c.Logf("Resizing volume of %s. Waiting...", id) + c.WaitUntilActive(id) + c.Logf("Resized the volume of %s to %d GB", id, 2) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/db/v1/pkg.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/db/v1/pkg.go new file mode 100644 index 0000000000..b7b1f993d5 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/db/v1/pkg.go @@ -0,0 +1 @@ +package v1 diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/db/v1/user_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/db/v1/user_test.go new file mode 100644 index 0000000000..0f5fcc24b1 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/db/v1/user_test.go @@ -0,0 +1,70 @@ +// +build acceptance db + +package v1 + +import ( + "github.com/gophercloud/gophercloud/acceptance/tools" + db "github.com/gophercloud/gophercloud/openstack/db/v1/databases" + u "github.com/gophercloud/gophercloud/openstack/db/v1/users" + "github.com/gophercloud/gophercloud/pagination" +) + +func (c context) createUsers() { + users := []string{ + tools.RandomString("user_", 5), + tools.RandomString("user_", 5), + tools.RandomString("user_", 5), + } + + db1 := db.CreateOpts{Name: "db1"} + db2 := db.CreateOpts{Name: "db2"} + db3 := db.CreateOpts{Name: "db3"} + + opts := u.BatchCreateOpts{ + u.CreateOpts{ + Name: users[0], + Password: tools.RandomString("", 5), + Databases: db.BatchCreateOpts{db1, db2, db3}, + }, + u.CreateOpts{ + Name: users[1], + Password: tools.RandomString("", 5), + Databases: db.BatchCreateOpts{db1, db2}, + }, + u.CreateOpts{ + Name: users[2], + Password: tools.RandomString("", 5), + Databases: db.BatchCreateOpts{db3}, + }, + } + + err := u.Create(c.client, c.instanceID, opts).ExtractErr() + c.AssertNoErr(err) + c.Logf("Created three users on instance %s: %s, %s, %s", c.instanceID, users[0], users[1], users[2]) + c.users = users +} + +func (c context) listUsers() { + c.Logf("Listing databases on instance %s", c.instanceID) + + err := db.List(c.client, c.instanceID).EachPage(func(page pagination.Page) (bool, error) { + dbList, err := db.ExtractDBs(page) + c.AssertNoErr(err) + + for _, db := range dbList { + c.Logf("DB: %#v", db) + } + + return true, nil + }) + + c.AssertNoErr(err) +} + +func (c context) deleteUsers() { + for _, id := range c.DBIDs { + err := db.Delete(c.client, c.instanceID, id).ExtractErr() + c.AssertNoErr(err) + c.Logf("Deleted DB %s", id) + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/dns/v2/dns.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/dns/v2/dns.go new file mode 100644 index 0000000000..7a0893ff5c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/dns/v2/dns.go @@ -0,0 +1,164 @@ +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets" + "github.com/gophercloud/gophercloud/openstack/dns/v2/zones" +) + +// CreateRecordSet will create a RecordSet with a random name. An error will +// be returned if the zone was unable to be created. +func CreateRecordSet(t *testing.T, client *gophercloud.ServiceClient, zone *zones.Zone) (*recordsets.RecordSet, error) { + t.Logf("Attempting to create recordset: %s", zone.Name) + + createOpts := recordsets.CreateOpts{ + Name: zone.Name, + Type: "A", + TTL: 3600, + Description: "Test recordset", + Records: []string{"10.1.0.2"}, + } + + rs, err := recordsets.Create(client, zone.ID, createOpts).Extract() + if err != nil { + return rs, err + } + + if err := WaitForRecordSetStatus(client, rs, "ACTIVE"); err != nil { + return rs, err + } + + newRS, err := recordsets.Get(client, rs.ZoneID, rs.ID).Extract() + if err != nil { + return newRS, err + } + + t.Logf("Created record set: %s", newRS.Name) + + return rs, nil +} + +// CreateZone will create a Zone with a random name. An error will +// be returned if the zone was unable to be created. +func CreateZone(t *testing.T, client *gophercloud.ServiceClient) (*zones.Zone, error) { + zoneName := tools.RandomString("ACPTTEST", 8) + ".com." + + t.Logf("Attempting to create zone: %s", zoneName) + createOpts := zones.CreateOpts{ + Name: zoneName, + Email: "root@example.com", + Type: "PRIMARY", + TTL: 7200, + Description: "Test zone", + } + + zone, err := zones.Create(client, createOpts).Extract() + if err != nil { + return zone, err + } + + if err := WaitForZoneStatus(client, zone, "ACTIVE"); err != nil { + return zone, err + } + + newZone, err := zones.Get(client, zone.ID).Extract() + if err != nil { + return zone, err + } + + t.Logf("Created Zone: %s", zoneName) + return newZone, nil +} + +// CreateSecondaryZone will create a Zone with a random name. An error will +// be returned if the zone was unable to be created. +// +// This is only for example purposes as it will try to do a zone transfer. +func CreateSecondaryZone(t *testing.T, client *gophercloud.ServiceClient) (*zones.Zone, error) { + zoneName := tools.RandomString("ACPTTEST", 8) + ".com." + + t.Logf("Attempting to create zone: %s", zoneName) + createOpts := zones.CreateOpts{ + Name: zoneName, + Type: "SECONDARY", + Masters: []string{"10.0.0.1"}, + } + + zone, err := zones.Create(client, createOpts).Extract() + if err != nil { + return zone, err + } + + if err := WaitForZoneStatus(client, zone, "ACTIVE"); err != nil { + return zone, err + } + + newZone, err := zones.Get(client, zone.ID).Extract() + if err != nil { + return zone, err + } + + t.Logf("Created Zone: %s", zoneName) + return newZone, nil +} + +// DeleteRecordSet will delete a specified record set. A fatal error will occur if +// the record set failed to be deleted. This works best when used as a deferred +// function. +func DeleteRecordSet(t *testing.T, client *gophercloud.ServiceClient, rs *recordsets.RecordSet) { + err := recordsets.Delete(client, rs.ZoneID, rs.ID).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete record set %s: %v", rs.ID, err) + } + + t.Logf("Deleted record set: %s", rs.ID) +} + +// DeleteZone will delete a specified zone. A fatal error will occur if +// the zone failed to be deleted. This works best when used as a deferred +// function. +func DeleteZone(t *testing.T, client *gophercloud.ServiceClient, zone *zones.Zone) { + _, err := zones.Delete(client, zone.ID).Extract() + if err != nil { + t.Fatalf("Unable to delete zone %s: %v", zone.ID, err) + } + + t.Logf("Deleted zone: %s", zone.ID) +} + +// WaitForRecordSetStatus will poll a record set's status until it either matches +// the specified status or the status becomes ERROR. +func WaitForRecordSetStatus(client *gophercloud.ServiceClient, rs *recordsets.RecordSet, status string) error { + return gophercloud.WaitFor(60, func() (bool, error) { + current, err := recordsets.Get(client, rs.ZoneID, rs.ID).Extract() + if err != nil { + return false, err + } + + if current.Status == status { + return true, nil + } + + return false, nil + }) +} + +// WaitForZoneStatus will poll a zone's status until it either matches +// the specified status or the status becomes ERROR. +func WaitForZoneStatus(client *gophercloud.ServiceClient, zone *zones.Zone, status string) error { + return gophercloud.WaitFor(60, func() (bool, error) { + current, err := zones.Get(client, zone.ID).Extract() + if err != nil { + return false, err + } + + if current.Status == status { + return true, nil + } + + return false, nil + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/dns/v2/recordsets_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/dns/v2/recordsets_test.go new file mode 100644 index 0000000000..17c40bb0ce --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/dns/v2/recordsets_test.go @@ -0,0 +1,105 @@ +// +build acceptance dns recordsets + +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets" +) + +func TestRecordSetsListByZone(t *testing.T) { + client, err := clients.NewDNSV2Client() + if err != nil { + t.Fatalf("Unable to create a DNS client: %v", err) + } + + zone, err := CreateZone(t, client) + if err != nil { + t.Fatal(err) + } + defer DeleteZone(t, client, zone) + + var allRecordSets []recordsets.RecordSet + allPages, err := recordsets.ListByZone(client, zone.ID, nil).AllPages() + if err != nil { + t.Fatalf("Unable to retrieve recordsets: %v", err) + } + + allRecordSets, err = recordsets.ExtractRecordSets(allPages) + if err != nil { + t.Fatalf("Unable to extract recordsets: %v", err) + } + + for _, recordset := range allRecordSets { + tools.PrintResource(t, &recordset) + } +} + +func TestRecordSetsListByZoneLimited(t *testing.T) { + client, err := clients.NewDNSV2Client() + if err != nil { + t.Fatalf("Unable to create a DNS client: %v", err) + } + + zone, err := CreateZone(t, client) + if err != nil { + t.Fatal(err) + } + defer DeleteZone(t, client, zone) + + var allRecordSets []recordsets.RecordSet + listOpts := recordsets.ListOpts{ + Limit: 1, + } + allPages, err := recordsets.ListByZone(client, zone.ID, listOpts).AllPages() + if err != nil { + t.Fatalf("Unable to retrieve recordsets: %v", err) + } + + allRecordSets, err = recordsets.ExtractRecordSets(allPages) + if err != nil { + t.Fatalf("Unable to extract recordsets: %v", err) + } + + for _, recordset := range allRecordSets { + tools.PrintResource(t, &recordset) + } +} + +func TestRecordSetCRUD(t *testing.T) { + client, err := clients.NewDNSV2Client() + if err != nil { + t.Fatalf("Unable to create a DNS client: %v", err) + } + + zone, err := CreateZone(t, client) + if err != nil { + t.Fatal(err) + } + defer DeleteZone(t, client, zone) + + tools.PrintResource(t, &zone) + + rs, err := CreateRecordSet(t, client, zone) + if err != nil { + t.Fatal(err) + } + defer DeleteRecordSet(t, client, rs) + + tools.PrintResource(t, &rs) + + updateOpts := recordsets.UpdateOpts{ + Description: "New description", + TTL: 0, + } + + newRS, err := recordsets.Update(client, rs.ZoneID, rs.ID, updateOpts).Extract() + if err != nil { + t.Fatal(err) + } + + tools.PrintResource(t, &newRS) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/dns/v2/zones_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/dns/v2/zones_test.go new file mode 100644 index 0000000000..8e71687898 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/dns/v2/zones_test.go @@ -0,0 +1,60 @@ +// +build acceptance dns zones + +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/dns/v2/zones" +) + +func TestZonesList(t *testing.T) { + client, err := clients.NewDNSV2Client() + if err != nil { + t.Fatalf("Unable to create a DNS client: %v", err) + } + + var allZones []zones.Zone + allPages, err := zones.List(client, nil).AllPages() + if err != nil { + t.Fatalf("Unable to retrieve zones: %v", err) + } + + allZones, err = zones.ExtractZones(allPages) + if err != nil { + t.Fatalf("Unable to extract zones: %v", err) + } + + for _, zone := range allZones { + tools.PrintResource(t, &zone) + } +} + +func TestZonesCRUD(t *testing.T) { + client, err := clients.NewDNSV2Client() + if err != nil { + t.Fatalf("Unable to create a DNS client: %v", err) + } + + zone, err := CreateZone(t, client) + if err != nil { + t.Fatal(err) + } + defer DeleteZone(t, client, zone) + + tools.PrintResource(t, &zone) + + updateOpts := zones.UpdateOpts{ + Description: "New description", + TTL: 0, + } + + newZone, err := zones.Update(client, zone.ID, updateOpts).Extract() + if err != nil { + t.Fatal(err) + } + + tools.PrintResource(t, &newZone) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v2/extension_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v2/extension_test.go new file mode 100644 index 0000000000..c6a2bdef41 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v2/extension_test.go @@ -0,0 +1,46 @@ +// +build acceptance identity + +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/identity/v2/extensions" +) + +func TestExtensionsList(t *testing.T) { + client, err := clients.NewIdentityV2Client() + if err != nil { + t.Fatalf("Unable to create an identity client: %v", err) + } + + allPages, err := extensions.List(client).AllPages() + if err != nil { + t.Fatalf("Unable to list extensions: %v", err) + } + + allExtensions, err := extensions.ExtractExtensions(allPages) + if err != nil { + t.Fatalf("Unable to extract extensions: %v", err) + } + + for _, extension := range allExtensions { + tools.PrintResource(t, extension) + } +} + +func TestExtensionsGet(t *testing.T) { + client, err := clients.NewIdentityV2Client() + if err != nil { + t.Fatalf("Unable to create an identity client: %v", err) + } + + extension, err := extensions.Get(client, "OS-KSCRUD").Extract() + if err != nil { + t.Fatalf("Unable to get extension OS-KSCRUD: %v", err) + } + + tools.PrintResource(t, extension) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v2/identity.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v2/identity.go new file mode 100644 index 0000000000..6d0d0f2090 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v2/identity.go @@ -0,0 +1,186 @@ +// Package v2 contains common functions for creating identity-based resources +// for use in acceptance tests. See the `*_test.go` files for example usages. +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/admin/roles" + "github.com/gophercloud/gophercloud/openstack/identity/v2/tenants" + "github.com/gophercloud/gophercloud/openstack/identity/v2/users" +) + +// AddUserRole will grant a role to a user in a tenant. An error will be +// returned if the grant was unsuccessful. +func AddUserRole(t *testing.T, client *gophercloud.ServiceClient, tenant *tenants.Tenant, user *users.User, role *roles.Role) error { + t.Logf("Attempting to grant user %s role %s in tenant %s", user.ID, role.ID, tenant.ID) + + err := roles.AddUser(client, tenant.ID, user.ID, role.ID).ExtractErr() + if err != nil { + return err + } + + t.Logf("Granted user %s role %s in tenant %s", user.ID, role.ID, tenant.ID) + + return nil +} + +// CreateTenant will create a project with a random name. +// It takes an optional createOpts parameter since creating a project +// has so many options. An error will be returned if the project was +// unable to be created. +func CreateTenant(t *testing.T, client *gophercloud.ServiceClient, c *tenants.CreateOpts) (*tenants.Tenant, error) { + name := tools.RandomString("ACPTTEST", 8) + t.Logf("Attempting to create tenant: %s", name) + + var createOpts tenants.CreateOpts + if c != nil { + createOpts = *c + } else { + createOpts = tenants.CreateOpts{} + } + + createOpts.Name = name + + tenant, err := tenants.Create(client, createOpts).Extract() + if err != nil { + t.Logf("Foo") + return tenant, err + } + + t.Logf("Successfully created project %s with ID %s", name, tenant.ID) + + return tenant, nil +} + +// CreateUser will create a user with a random name and adds them to the given +// tenant. An error will be returned if the user was unable to be created. +func CreateUser(t *testing.T, client *gophercloud.ServiceClient, tenant *tenants.Tenant) (*users.User, error) { + userName := tools.RandomString("user_", 5) + userEmail := userName + "@foo.com" + t.Logf("Creating user: %s", userName) + + createOpts := users.CreateOpts{ + Name: userName, + Enabled: gophercloud.Disabled, + TenantID: tenant.ID, + Email: userEmail, + } + + user, err := users.Create(client, createOpts).Extract() + if err != nil { + return user, err + } + + return user, nil +} + +// DeleteTenant will delete a tenant by ID. A fatal error will occur if +// the tenant ID failed to be deleted. This works best when using it as +// a deferred function. +func DeleteTenant(t *testing.T, client *gophercloud.ServiceClient, tenantID string) { + err := tenants.Delete(client, tenantID).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete tenant %s: %v", tenantID, err) + } + + t.Logf("Deleted tenant: %s", tenantID) +} + +// DeleteUser will delete a user. A fatal error will occur if the delete was +// unsuccessful. This works best when used as a deferred function. +func DeleteUser(t *testing.T, client *gophercloud.ServiceClient, user *users.User) { + t.Logf("Attempting to delete user: %s", user.Name) + + result := users.Delete(client, user.ID) + if result.Err != nil { + t.Fatalf("Unable to delete user") + } + + t.Logf("Deleted user: %s", user.Name) +} + +// DeleteUserRole will revoke a role of a user in a tenant. A fatal error will +// occur if the revoke was unsuccessful. This works best when used as a +// deferred function. +func DeleteUserRole(t *testing.T, client *gophercloud.ServiceClient, tenant *tenants.Tenant, user *users.User, role *roles.Role) { + t.Logf("Attempting to remove role %s from user %s in tenant %s", role.ID, user.ID, tenant.ID) + + err := roles.DeleteUser(client, tenant.ID, user.ID, role.ID).ExtractErr() + if err != nil { + t.Fatalf("Unable to remove role") + } + + t.Logf("Removed role %s from user %s in tenant %s", role.ID, user.ID, tenant.ID) +} + +// FindRole finds all roles that the current authenticated client has access +// to and returns the first one found. An error will be returned if the lookup +// was unsuccessful. +func FindRole(t *testing.T, client *gophercloud.ServiceClient) (*roles.Role, error) { + var role *roles.Role + + allPages, err := roles.List(client).AllPages() + if err != nil { + return role, err + } + + allRoles, err := roles.ExtractRoles(allPages) + if err != nil { + return role, err + } + + for _, r := range allRoles { + role = &r + break + } + + return role, nil +} + +// FindTenant finds all tenants that the current authenticated client has access +// to and returns the first one found. An error will be returned if the lookup +// was unsuccessful. +func FindTenant(t *testing.T, client *gophercloud.ServiceClient) (*tenants.Tenant, error) { + var tenant *tenants.Tenant + + allPages, err := tenants.List(client, nil).AllPages() + if err != nil { + return tenant, err + } + + allTenants, err := tenants.ExtractTenants(allPages) + if err != nil { + return tenant, err + } + + for _, t := range allTenants { + tenant = &t + break + } + + return tenant, nil +} + +// UpdateUser will update an existing user with a new randomly generated name. +// An error will be returned if the update was unsuccessful. +func UpdateUser(t *testing.T, client *gophercloud.ServiceClient, user *users.User) (*users.User, error) { + userName := tools.RandomString("user_", 5) + userEmail := userName + "@foo.com" + + t.Logf("Attempting to update user name from %s to %s", user.Name, userName) + + updateOpts := users.UpdateOpts{ + Name: userName, + Email: userEmail, + } + + newUser, err := users.Update(client, user.ID, updateOpts).Extract() + if err != nil { + return newUser, err + } + + return newUser, nil +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v2/pkg.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v2/pkg.go new file mode 100644 index 0000000000..5ec3cc8e83 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v2/pkg.go @@ -0,0 +1 @@ +package v2 diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v2/role_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v2/role_test.go new file mode 100644 index 0000000000..83fbd318fa --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v2/role_test.go @@ -0,0 +1,77 @@ +// +build acceptance identity roles + +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/admin/roles" + "github.com/gophercloud/gophercloud/openstack/identity/v2/users" +) + +func TestRolesAddToUser(t *testing.T) { + client, err := clients.NewIdentityV2AdminClient() + if err != nil { + t.Fatalf("Unable to obtain an identity client: %v", err) + } + + tenant, err := FindTenant(t, client) + if err != nil { + t.Fatalf("Unable to get a tenant: %v", err) + } + + role, err := FindRole(t, client) + if err != nil { + t.Fatalf("Unable to get a role: %v", err) + } + + user, err := CreateUser(t, client, tenant) + if err != nil { + t.Fatalf("Unable to create a user: %v", err) + } + defer DeleteUser(t, client, user) + + err = AddUserRole(t, client, tenant, user, role) + if err != nil { + t.Fatalf("Unable to add role to user: %v", err) + } + defer DeleteUserRole(t, client, tenant, user, role) + + allPages, err := users.ListRoles(client, tenant.ID, user.ID).AllPages() + if err != nil { + t.Fatalf("Unable to obtain roles for user: %v", err) + } + + allRoles, err := users.ExtractRoles(allPages) + if err != nil { + t.Fatalf("Unable to extract roles: %v", err) + } + + t.Logf("Roles of user %s:", user.Name) + for _, role := range allRoles { + tools.PrintResource(t, role) + } +} + +func TestRolesList(t *testing.T) { + client, err := clients.NewIdentityV2AdminClient() + if err != nil { + t.Fatalf("Unable to create an identity client: %v", err) + } + + allPages, err := roles.List(client).AllPages() + if err != nil { + t.Fatalf("Unable to list all roles: %v", err) + } + + allRoles, err := roles.ExtractRoles(allPages) + if err != nil { + t.Fatalf("Unable to extract roles: %v", err) + } + + for _, r := range allRoles { + tools.PrintResource(t, r) + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v2/tenant_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v2/tenant_test.go new file mode 100644 index 0000000000..049ec910a1 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v2/tenant_test.go @@ -0,0 +1,63 @@ +// +build acceptance identity + +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/identity/v2/tenants" +) + +func TestTenantsList(t *testing.T) { + client, err := clients.NewIdentityV2Client() + if err != nil { + t.Fatalf("Unable to obtain an identity client: %v") + } + + allPages, err := tenants.List(client, nil).AllPages() + if err != nil { + t.Fatalf("Unable to list tenants: %v", err) + } + + allTenants, err := tenants.ExtractTenants(allPages) + if err != nil { + t.Fatalf("Unable to extract tenants: %v", err) + } + + for _, tenant := range allTenants { + tools.PrintResource(t, tenant) + } +} + +func TestTenantsCRUD(t *testing.T) { + client, err := clients.NewIdentityV2AdminClient() + if err != nil { + t.Fatalf("Unable to obtain an identity client: %v") + } + + tenant, err := CreateTenant(t, client, nil) + if err != nil { + t.Fatalf("Unable to create tenant: %v", err) + } + defer DeleteTenant(t, client, tenant.ID) + + tenant, err = tenants.Get(client, tenant.ID).Extract() + if err != nil { + t.Fatalf("Unable to get tenant: %v", err) + } + + tools.PrintResource(t, tenant) + + updateOpts := tenants.UpdateOpts{ + Description: "some tenant", + } + + newTenant, err := tenants.Update(client, tenant.ID, updateOpts).Extract() + if err != nil { + t.Fatalf("Unable to update tenant: %v", err) + } + + tools.PrintResource(t, newTenant) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v2/token_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v2/token_test.go new file mode 100644 index 0000000000..82a317a157 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v2/token_test.go @@ -0,0 +1,69 @@ +// +build acceptance identity + +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack" + "github.com/gophercloud/gophercloud/openstack/identity/v2/tokens" +) + +func TestTokenAuthenticate(t *testing.T) { + client, err := clients.NewIdentityV2UnauthenticatedClient() + if err != nil { + t.Fatalf("Unable to obtain an identity client: %v", err) + } + + authOptions, err := openstack.AuthOptionsFromEnv() + if err != nil { + t.Fatalf("Unable to obtain authentication options: %v", err) + } + + result := tokens.Create(client, authOptions) + token, err := result.ExtractToken() + if err != nil { + t.Fatalf("Unable to extract token: %v", err) + } + + tools.PrintResource(t, token) + + catalog, err := result.ExtractServiceCatalog() + if err != nil { + t.Fatalf("Unable to extract service catalog: %v", err) + } + + for _, entry := range catalog.Entries { + tools.PrintResource(t, entry) + } +} + +func TestTokenValidate(t *testing.T) { + client, err := clients.NewIdentityV2Client() + if err != nil { + t.Fatalf("Unable to obtain an identity client: %v", err) + } + + authOptions, err := openstack.AuthOptionsFromEnv() + if err != nil { + t.Fatalf("Unable to obtain authentication options: %v", err) + } + + result := tokens.Create(client, authOptions) + token, err := result.ExtractToken() + if err != nil { + t.Fatalf("Unable to extract token: %v", err) + } + + tools.PrintResource(t, token) + + getResult := tokens.Get(client, token.ID) + user, err := getResult.ExtractUser() + if err != nil { + t.Fatalf("Unable to extract user: %v", err) + } + + tools.PrintResource(t, user) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v2/user_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v2/user_test.go new file mode 100644 index 0000000000..faa5bba2f8 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v2/user_test.go @@ -0,0 +1,59 @@ +// +build acceptance identity + +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/identity/v2/users" +) + +func TestUsersList(t *testing.T) { + client, err := clients.NewIdentityV2AdminClient() + if err != nil { + t.Fatalf("Unable to obtain an identity client: %v", err) + } + + allPages, err := users.List(client).AllPages() + if err != nil { + t.Fatalf("Unable to list users: %v", err) + } + + allUsers, err := users.ExtractUsers(allPages) + if err != nil { + t.Fatalf("Unable to extract users: %v", err) + } + + for _, user := range allUsers { + tools.PrintResource(t, user) + } +} + +func TestUsersCreateUpdateDelete(t *testing.T) { + client, err := clients.NewIdentityV2AdminClient() + if err != nil { + t.Fatalf("Unable to obtain an identity client: %v", err) + } + + tenant, err := FindTenant(t, client) + if err != nil { + t.Fatalf("Unable to get a tenant: %v", err) + } + + user, err := CreateUser(t, client, tenant) + if err != nil { + t.Fatalf("Unable to create a user: %v", err) + } + defer DeleteUser(t, client, user) + + tools.PrintResource(t, user) + + newUser, err := UpdateUser(t, client, user) + if err != nil { + t.Fatalf("Unable to update user: %v", err) + } + + tools.PrintResource(t, newUser) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v3/endpoint_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v3/endpoint_test.go new file mode 100644 index 0000000000..a589970606 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v3/endpoint_test.go @@ -0,0 +1,86 @@ +// +build acceptance + +package v3 + +import ( + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/identity/v3/endpoints" + "github.com/gophercloud/gophercloud/openstack/identity/v3/services" +) + +func TestEndpointsList(t *testing.T) { + client, err := clients.NewIdentityV3Client() + if err != nil { + t.Fatalf("Unable to obtain an identity client: %v") + } + + allPages, err := endpoints.List(client, nil).AllPages() + if err != nil { + t.Fatalf("Unable to list endpoints: %v", err) + } + + allEndpoints, err := endpoints.ExtractEndpoints(allPages) + if err != nil { + t.Fatalf("Unable to extract endpoints: %v", err) + } + + for _, endpoint := range allEndpoints { + tools.PrintResource(t, endpoint) + } +} + +func TestEndpointsNavigateCatalog(t *testing.T) { + client, err := clients.NewIdentityV3Client() + if err != nil { + t.Fatalf("Unable to obtain an identity client: %v") + } + + // Discover the service we're interested in. + serviceListOpts := services.ListOpts{ + ServiceType: "compute", + } + + allPages, err := services.List(client, serviceListOpts).AllPages() + if err != nil { + t.Fatalf("Unable to lookup compute service: %v", err) + } + + allServices, err := services.ExtractServices(allPages) + if err != nil { + t.Fatalf("Unable to extract service: %v") + } + + if len(allServices) != 1 { + t.Fatalf("Expected one service, got %d", len(allServices)) + } + + computeService := allServices[0] + tools.PrintResource(t, computeService) + + // Enumerate the endpoints available for this service. + endpointListOpts := endpoints.ListOpts{ + Availability: gophercloud.AvailabilityPublic, + ServiceID: computeService.ID, + } + + allPages, err = endpoints.List(client, endpointListOpts).AllPages() + if err != nil { + t.Fatalf("Unable to lookup compute endpoint: %v", err) + } + + allEndpoints, err := endpoints.ExtractEndpoints(allPages) + if err != nil { + t.Fatalf("Unable to extract endpoint: %v") + } + + if len(allEndpoints) != 1 { + t.Fatalf("Expected one endpoint, got %d", len(allEndpoints)) + } + + tools.PrintResource(t, allEndpoints[0]) + +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v3/identity.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v3/identity.go new file mode 100644 index 0000000000..4f2f6219d9 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v3/identity.go @@ -0,0 +1,88 @@ +package v3 + +import ( + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/identity/v3/projects" + "github.com/gophercloud/gophercloud/openstack/identity/v3/users" +) + +// CreateProject will create a project with a random name. +// It takes an optional createOpts parameter since creating a project +// has so many options. An error will be returned if the project was +// unable to be created. +func CreateProject(t *testing.T, client *gophercloud.ServiceClient, c *projects.CreateOpts) (*projects.Project, error) { + name := tools.RandomString("ACPTTEST", 8) + t.Logf("Attempting to create project: %s", name) + + var createOpts projects.CreateOpts + if c != nil { + createOpts = *c + } else { + createOpts = projects.CreateOpts{} + } + + createOpts.Name = name + + project, err := projects.Create(client, createOpts).Extract() + if err != nil { + return project, err + } + + t.Logf("Successfully created project %s with ID %s", name, project.ID) + + return project, nil +} + +// CreateUser will create a project with a random name. +// It takes an optional createOpts parameter since creating a user +// has so many options. An error will be returned if the user was +// unable to be created. +func CreateUser(t *testing.T, client *gophercloud.ServiceClient, c *users.CreateOpts) (*users.User, error) { + name := tools.RandomString("ACPTTEST", 8) + t.Logf("Attempting to create user: %s", name) + + var createOpts users.CreateOpts + if c != nil { + createOpts = *c + } else { + createOpts = users.CreateOpts{} + } + + createOpts.Name = name + + user, err := users.Create(client, createOpts).Extract() + if err != nil { + return user, err + } + + t.Logf("Successfully created user %s with ID %s", name, user.ID) + + return user, nil +} + +// DeleteProject will delete a project by ID. A fatal error will occur if +// the project ID failed to be deleted. This works best when using it as +// a deferred function. +func DeleteProject(t *testing.T, client *gophercloud.ServiceClient, projectID string) { + err := projects.Delete(client, projectID).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete project %s: %v", projectID, err) + } + + t.Logf("Deleted project: %s", projectID) +} + +// DeleteUser will delete a user by ID. A fatal error will occur if +// the user failed to be deleted. This works best when using it as +// a deferred function. +func DeleteUser(t *testing.T, client *gophercloud.ServiceClient, userID string) { + err := users.Delete(client, userID).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete user %s: %v", userID, err) + } + + t.Logf("Deleted user: %s", userID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v3/pkg.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v3/pkg.go new file mode 100644 index 0000000000..eac3ae96a1 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v3/pkg.go @@ -0,0 +1 @@ +package v3 diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v3/projects_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v3/projects_test.go new file mode 100644 index 0000000000..08a5cfdad4 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v3/projects_test.go @@ -0,0 +1,158 @@ +// +build acceptance + +package v3 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/identity/v3/projects" +) + +func TestProjectsList(t *testing.T) { + client, err := clients.NewIdentityV3Client() + if err != nil { + t.Fatalf("Unable to obtain an identity client: %v", err) + } + + var iTrue bool = true + listOpts := projects.ListOpts{ + Enabled: &iTrue, + } + + allPages, err := projects.List(client, listOpts).AllPages() + if err != nil { + t.Fatalf("Unable to list projects: %v", err) + } + + allProjects, err := projects.ExtractProjects(allPages) + if err != nil { + t.Fatalf("Unable to extract projects: %v", err) + } + + for _, project := range allProjects { + tools.PrintResource(t, project) + } +} + +func TestProjectsGet(t *testing.T) { + client, err := clients.NewIdentityV3Client() + if err != nil { + t.Fatalf("Unable to obtain an identity client: %v", err) + } + + allPages, err := projects.List(client, nil).AllPages() + if err != nil { + t.Fatalf("Unable to list projects: %v", err) + } + + allProjects, err := projects.ExtractProjects(allPages) + if err != nil { + t.Fatalf("Unable to extract projects: %v", err) + } + + project := allProjects[0] + p, err := projects.Get(client, project.ID).Extract() + if err != nil { + t.Fatalf("Unable to get project: %v", err) + } + + tools.PrintResource(t, p) +} + +func TestProjectsCRUD(t *testing.T) { + client, err := clients.NewIdentityV3Client() + if err != nil { + t.Fatalf("Unable to obtain an identity client: %v") + } + + project, err := CreateProject(t, client, nil) + if err != nil { + t.Fatalf("Unable to create project: %v", err) + } + defer DeleteProject(t, client, project.ID) + + tools.PrintResource(t, project) + + var iFalse bool = false + updateOpts := projects.UpdateOpts{ + Enabled: &iFalse, + } + + updatedProject, err := projects.Update(client, project.ID, updateOpts).Extract() + if err != nil { + t.Fatalf("Unable to update project: %v", err) + } + + tools.PrintResource(t, updatedProject) +} + +func TestProjectsDomain(t *testing.T) { + client, err := clients.NewIdentityV3Client() + if err != nil { + t.Fatalf("Unable to obtain an identity client: %v") + } + + var iTrue = true + createOpts := projects.CreateOpts{ + IsDomain: &iTrue, + } + + projectDomain, err := CreateProject(t, client, &createOpts) + if err != nil { + t.Fatalf("Unable to create project: %v", err) + } + defer DeleteProject(t, client, projectDomain.ID) + + tools.PrintResource(t, projectDomain) + + createOpts = projects.CreateOpts{ + DomainID: projectDomain.ID, + } + + project, err := CreateProject(t, client, &createOpts) + if err != nil { + t.Fatalf("Unable to create project: %v", err) + } + defer DeleteProject(t, client, project.ID) + + tools.PrintResource(t, project) + + var iFalse = false + updateOpts := projects.UpdateOpts{ + Enabled: &iFalse, + } + + _, err = projects.Update(client, projectDomain.ID, updateOpts).Extract() + if err != nil { + t.Fatalf("Unable to disable domain: %v") + } +} + +func TestProjectsNested(t *testing.T) { + client, err := clients.NewIdentityV3Client() + if err != nil { + t.Fatalf("Unable to obtain an identity client: %v") + } + + projectMain, err := CreateProject(t, client, nil) + if err != nil { + t.Fatalf("Unable to create project: %v", err) + } + defer DeleteProject(t, client, projectMain.ID) + + tools.PrintResource(t, projectMain) + + createOpts := projects.CreateOpts{ + ParentID: projectMain.ID, + } + + project, err := CreateProject(t, client, &createOpts) + if err != nil { + t.Fatalf("Unable to create project: %v", err) + } + defer DeleteProject(t, client, project.ID) + + tools.PrintResource(t, project) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v3/service_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v3/service_test.go new file mode 100644 index 0000000000..7a0c71f4fa --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v3/service_test.go @@ -0,0 +1,33 @@ +// +build acceptance + +package v3 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/identity/v3/services" +) + +func TestServicesList(t *testing.T) { + client, err := clients.NewIdentityV3Client() + if err != nil { + t.Fatalf("Unable to obtain an identity client: %v") + } + + allPages, err := services.List(client, nil).AllPages() + if err != nil { + t.Fatalf("Unable to list services: %v", err) + } + + allServices, err := services.ExtractServices(allPages) + if err != nil { + t.Fatalf("Unable to extract services: %v", err) + } + + for _, service := range allServices { + tools.PrintResource(t, service) + } + +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v3/token_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v3/token_test.go new file mode 100644 index 0000000000..0f471f776b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v3/token_test.go @@ -0,0 +1,60 @@ +// +build acceptance + +package v3 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack" + "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens" +) + +func TestGetToken(t *testing.T) { + client, err := clients.NewIdentityV3Client() + if err != nil { + t.Fatalf("Unable to obtain an identity client: %v") + } + + ao, err := openstack.AuthOptionsFromEnv() + if err != nil { + t.Fatalf("Unable to obtain environment auth options: %v", err) + } + + authOptions := tokens.AuthOptions{ + Username: ao.Username, + Password: ao.Password, + DomainName: "default", + } + + token, err := tokens.Create(client, &authOptions).Extract() + if err != nil { + t.Fatalf("Unable to get token: %v", err) + } + tools.PrintResource(t, token) + + catalog, err := tokens.Get(client, token.ID).ExtractServiceCatalog() + if err != nil { + t.Fatalf("Unable to get catalog from token: %v", err) + } + tools.PrintResource(t, catalog) + + user, err := tokens.Get(client, token.ID).ExtractUser() + if err != nil { + t.Fatalf("Unable to get user from token: %v", err) + } + tools.PrintResource(t, user) + + roles, err := tokens.Get(client, token.ID).ExtractRoles() + if err != nil { + t.Fatalf("Unable to get roles from token: %v", err) + } + tools.PrintResource(t, roles) + + project, err := tokens.Get(client, token.ID).ExtractProject() + if err != nil { + t.Fatalf("Unable to get project from token: %v", err) + } + tools.PrintResource(t, project) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v3/users_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v3/users_test.go new file mode 100644 index 0000000000..e87c88e44b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/identity/v3/users_test.go @@ -0,0 +1,156 @@ +// +build acceptance + +package v3 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/identity/v3/groups" + "github.com/gophercloud/gophercloud/openstack/identity/v3/users" +) + +func TestUsersList(t *testing.T) { + client, err := clients.NewIdentityV3Client() + if err != nil { + t.Fatalf("Unable to obtain an identity client: %v", err) + } + + var iTrue bool = true + listOpts := users.ListOpts{ + Enabled: &iTrue, + } + + allPages, err := users.List(client, listOpts).AllPages() + if err != nil { + t.Fatalf("Unable to list users: %v", err) + } + + allUsers, err := users.ExtractUsers(allPages) + if err != nil { + t.Fatalf("Unable to extract users: %v", err) + } + + for _, user := range allUsers { + tools.PrintResource(t, user) + tools.PrintResource(t, user.Extra) + } +} + +func TestUsersGet(t *testing.T) { + client, err := clients.NewIdentityV3Client() + if err != nil { + t.Fatalf("Unable to obtain an identity client: %v", err) + } + + allPages, err := users.List(client, nil).AllPages() + if err != nil { + t.Fatalf("Unable to list users: %v", err) + } + + allUsers, err := users.ExtractUsers(allPages) + if err != nil { + t.Fatalf("Unable to extract users: %v", err) + } + + user := allUsers[0] + p, err := users.Get(client, user.ID).Extract() + if err != nil { + t.Fatalf("Unable to get user: %v", err) + } + + tools.PrintResource(t, p) +} + +func TestUserCRUD(t *testing.T) { + client, err := clients.NewIdentityV3Client() + if err != nil { + t.Fatalf("Unable to obtain an identity client: %v", err) + } + + project, err := CreateProject(t, client, nil) + if err != nil { + t.Fatalf("Unable to create project: %v", err) + } + defer DeleteProject(t, client, project.ID) + + tools.PrintResource(t, project) + + createOpts := users.CreateOpts{ + DefaultProjectID: project.ID, + Password: "foobar", + DomainID: "default", + Options: map[users.Option]interface{}{ + users.IgnorePasswordExpiry: true, + users.MultiFactorAuthRules: []interface{}{ + []string{"password", "totp"}, + []string{"password", "custom-auth-method"}, + }, + }, + Extra: map[string]interface{}{ + "email": "jsmith@example.com", + }, + } + + user, err := CreateUser(t, client, &createOpts) + if err != nil { + t.Fatalf("Unable to create user: %v", err) + } + defer DeleteUser(t, client, user.ID) + + tools.PrintResource(t, user) + tools.PrintResource(t, user.Extra) + + iFalse := false + updateOpts := users.UpdateOpts{ + Enabled: &iFalse, + Options: map[users.Option]interface{}{ + users.MultiFactorAuthRules: nil, + }, + Extra: map[string]interface{}{ + "disabled_reason": "DDOS", + }, + } + + newUser, err := users.Update(client, user.ID, updateOpts).Extract() + if err != nil { + t.Fatalf("Unable to update user: %v", err) + } + + tools.PrintResource(t, newUser) + tools.PrintResource(t, newUser.Extra) +} + +func TestUsersListGroups(t *testing.T) { + client, err := clients.NewIdentityV3Client() + if err != nil { + t.Fatalf("Unable to obtain an identity client: %v", err) + } + allUserPages, err := users.List(client, nil).AllPages() + if err != nil { + t.Fatalf("Unable to list users: %v", err) + } + + allUsers, err := users.ExtractUsers(allUserPages) + if err != nil { + t.Fatalf("Unable to extract users: %v", err) + } + + user := allUsers[0] + + allGroupPages, err := users.ListGroups(client, user.ID).AllPages() + if err != nil { + t.Fatalf("Unable to list groups: %v", err) + } + + allGroups, err := groups.ExtractGroups(allGroupPages) + if err != nil { + t.Fatalf("Unable to extract groups: %v", err) + } + + for _, group := range allGroups { + tools.PrintResource(t, group) + tools.PrintResource(t, group.Extra) + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/imageservice/v2/images_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/imageservice/v2/images_test.go new file mode 100644 index 0000000000..c2a8987319 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/imageservice/v2/images_test.go @@ -0,0 +1,80 @@ +// +build acceptance imageservice images + +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images" + "github.com/gophercloud/gophercloud/pagination" +) + +func TestImagesListEachPage(t *testing.T) { + client, err := clients.NewImageServiceV2Client() + if err != nil { + t.Fatalf("Unable to create an image service client: %v", err) + } + + listOpts := images.ListOpts{ + Limit: 1, + } + + pager := images.List(client, listOpts) + err = pager.EachPage(func(page pagination.Page) (bool, error) { + images, err := images.ExtractImages(page) + if err != nil { + t.Fatalf("Unable to extract images: %v", err) + } + + for _, image := range images { + tools.PrintResource(t, image) + tools.PrintResource(t, image.Properties) + } + + return true, nil + }) +} + +func TestImagesListAllPages(t *testing.T) { + client, err := clients.NewImageServiceV2Client() + if err != nil { + t.Fatalf("Unable to create an image service client: %v", err) + } + + listOpts := images.ListOpts{ + Limit: 1, + } + + allPages, err := images.List(client, listOpts).AllPages() + if err != nil { + t.Fatalf("Unable to retrieve all images: %v", err) + } + + allImages, err := images.ExtractImages(allPages) + if err != nil { + t.Fatalf("Unable to extract images: %v", err) + } + + for _, image := range allImages { + tools.PrintResource(t, image) + tools.PrintResource(t, image.Properties) + } +} + +func TestImagesCreateDestroyEmptyImage(t *testing.T) { + client, err := clients.NewImageServiceV2Client() + if err != nil { + t.Fatalf("Unable to create an image service client: %v", err) + } + + image, err := CreateEmptyImage(t, client) + if err != nil { + t.Fatalf("Unable to create empty image: %v", err) + } + + defer DeleteImage(t, client, image) + + tools.PrintResource(t, image) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/imageservice/v2/imageservice.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/imageservice/v2/imageservice.go new file mode 100644 index 0000000000..8aaeeb74b8 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/imageservice/v2/imageservice.go @@ -0,0 +1,55 @@ +// Package v2 contains common functions for creating imageservice resources +// for use in acceptance tests. See the `*_test.go` files for example usages. +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images" +) + +// CreateEmptyImage will create an image, but with no actual image data. +// An error will be returned if an image was unable to be created. +func CreateEmptyImage(t *testing.T, client *gophercloud.ServiceClient) (*images.Image, error) { + var image *images.Image + + name := tools.RandomString("ACPTTEST", 16) + t.Logf("Attempting to create image: %s", name) + + protected := false + visibility := images.ImageVisibilityPrivate + createOpts := &images.CreateOpts{ + Name: name, + ContainerFormat: "bare", + DiskFormat: "qcow2", + MinDisk: 0, + MinRAM: 0, + Protected: &protected, + Visibility: &visibility, + Properties: map[string]string{ + "architecture": "x86_64", + }, + } + + image, err := images.Create(client, createOpts).Extract() + if err != nil { + return image, err + } + + t.Logf("Created image %s: %#v", name, image) + return image, nil +} + +// DeleteImage deletes an image. +// A fatal error will occur if the image failed to delete. This works best when +// used as a deferred function. +func DeleteImage(t *testing.T, client *gophercloud.ServiceClient, image *images.Image) { + err := images.Delete(client, image.ID).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete image %s: %v", image.ID, err) + } + + t.Logf("Deleted image: %s", image.ID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/apiversion_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/apiversion_test.go new file mode 100644 index 0000000000..c6f8f261b3 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/apiversion_test.go @@ -0,0 +1,53 @@ +// +build acceptance networking + +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/networking/v2/apiversions" +) + +func TestAPIVersionsList(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + allPages, err := apiversions.ListVersions(client).AllPages() + if err != nil { + t.Fatalf("Unable to list api versions: %v", err) + } + + allAPIVersions, err := apiversions.ExtractAPIVersions(allPages) + if err != nil { + t.Fatalf("Unable to extract api versions: %v", err) + } + + for _, apiVersion := range allAPIVersions { + tools.PrintResource(t, apiVersion) + } +} + +func TestAPIResourcesList(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + allPages, err := apiversions.ListVersionResources(client, "v2.0").AllPages() + if err != nil { + t.Fatalf("Unable to list api version reosources: %v", err) + } + + allVersionResources, err := apiversions.ExtractVersionResources(allPages) + if err != nil { + t.Fatalf("Unable to extract version resources: %v", err) + } + + for _, versionResource := range allVersionResources { + tools.PrintResource(t, versionResource) + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extension_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extension_test.go new file mode 100644 index 0000000000..5609e85261 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extension_test.go @@ -0,0 +1,46 @@ +// +build acceptance networking extensions + +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/common/extensions" +) + +func TestExtensionsList(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + allPages, err := extensions.List(client).AllPages() + if err != nil { + t.Fatalf("Unable to list extensions: %v", err) + } + + allExtensions, err := extensions.ExtractExtensions(allPages) + if err != nil { + t.Fatalf("Unable to extract extensions: %v", err) + } + + for _, extension := range allExtensions { + tools.PrintResource(t, extension) + } +} + +func TestExtensionGet(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + extension, err := extensions.Get(client, "router").Extract() + if err != nil { + t.Fatalf("Unable to get extension port-security: %v", err) + } + + tools.PrintResource(t, extension) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/extensions.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/extensions.go new file mode 100644 index 0000000000..154e34eac7 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/extensions.go @@ -0,0 +1,138 @@ +package extensions + +import ( + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/external" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules" + "github.com/gophercloud/gophercloud/openstack/networking/v2/networks" + "github.com/gophercloud/gophercloud/openstack/networking/v2/ports" +) + +// CreateExternalNetwork will create an external network. An error will be +// returned if the creation failed. +func CreateExternalNetwork(t *testing.T, client *gophercloud.ServiceClient) (*networks.Network, error) { + networkName := tools.RandomString("TESTACC-", 8) + + t.Logf("Attempting to create external network: %s", networkName) + + adminStateUp := true + isExternal := true + createOpts := external.CreateOpts{ + External: &isExternal, + } + + createOpts.Name = networkName + createOpts.AdminStateUp = &adminStateUp + + network, err := networks.Create(client, createOpts).Extract() + if err != nil { + return network, err + } + + t.Logf("Created external network: %s", networkName) + + return network, nil +} + +// CreatePortWithSecurityGroup will create a port with a security group +// attached. An error will be returned if the port could not be created. +func CreatePortWithSecurityGroup(t *testing.T, client *gophercloud.ServiceClient, networkID, subnetID, secGroupID string) (*ports.Port, error) { + portName := tools.RandomString("TESTACC-", 8) + iFalse := false + + t.Logf("Attempting to create port: %s", portName) + + createOpts := ports.CreateOpts{ + NetworkID: networkID, + Name: portName, + AdminStateUp: &iFalse, + FixedIPs: []ports.IP{ports.IP{SubnetID: subnetID}}, + SecurityGroups: []string{secGroupID}, + } + + port, err := ports.Create(client, createOpts).Extract() + if err != nil { + return port, err + } + + t.Logf("Successfully created port: %s", portName) + + return port, nil +} + +// CreateSecurityGroup will create a security group with a random name. +// An error will be returned if one was failed to be created. +func CreateSecurityGroup(t *testing.T, client *gophercloud.ServiceClient) (*groups.SecGroup, error) { + secGroupName := tools.RandomString("TESTACC-", 8) + + t.Logf("Attempting to create security group: %s", secGroupName) + + createOpts := groups.CreateOpts{ + Name: secGroupName, + } + + secGroup, err := groups.Create(client, createOpts).Extract() + if err != nil { + return secGroup, err + } + + t.Logf("Created security group: %s", secGroup.ID) + + return secGroup, nil +} + +// CreateSecurityGroupRule will create a security group rule with a random name +// and random port between 80 and 99. +// An error will be returned if one was failed to be created. +func CreateSecurityGroupRule(t *testing.T, client *gophercloud.ServiceClient, secGroupID string) (*rules.SecGroupRule, error) { + t.Logf("Attempting to create security group rule in group: %s", secGroupID) + + fromPort := tools.RandomInt(80, 89) + toPort := tools.RandomInt(90, 99) + + createOpts := rules.CreateOpts{ + Direction: "ingress", + EtherType: "IPv4", + SecGroupID: secGroupID, + PortRangeMin: fromPort, + PortRangeMax: toPort, + Protocol: rules.ProtocolTCP, + } + + rule, err := rules.Create(client, createOpts).Extract() + if err != nil { + return rule, err + } + + t.Logf("Created security group rule: %s", rule.ID) + + return rule, nil +} + +// DeleteSecurityGroup will delete a security group of a specified ID. +// A fatal error will occur if the deletion failed. This works best as a +// deferred function +func DeleteSecurityGroup(t *testing.T, client *gophercloud.ServiceClient, secGroupID string) { + t.Logf("Attempting to delete security group: %s", secGroupID) + + err := groups.Delete(client, secGroupID).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete security group: %v", err) + } +} + +// DeleteSecurityGroupRule will delete a security group rule of a specified ID. +// A fatal error will occur if the deletion failed. This works best as a +// deferred function +func DeleteSecurityGroupRule(t *testing.T, client *gophercloud.ServiceClient, ruleID string) { + t.Logf("Attempting to delete security group rule: %s", ruleID) + + err := rules.Delete(client, ruleID).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete security group rule: %v", err) + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/fwaas/firewall_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/fwaas/firewall_test.go new file mode 100644 index 0000000000..89d378ee7c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/fwaas/firewall_test.go @@ -0,0 +1,212 @@ +// +build acceptance networking fwaas + +package fwaas + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + layer3 "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/layer3" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/routerinsertion" +) + +func TestFirewallList(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + allPages, err := firewalls.List(client, nil).AllPages() + if err != nil { + t.Fatalf("Unable to list firewalls: %v", err) + } + + allFirewalls, err := firewalls.ExtractFirewalls(allPages) + if err != nil { + t.Fatalf("Unable to extract firewalls: %v", err) + } + + for _, firewall := range allFirewalls { + tools.PrintResource(t, firewall) + } +} + +func TestFirewallCRUD(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + router, err := layer3.CreateExternalRouter(t, client) + if err != nil { + t.Fatalf("Unable to create router: %v", err) + } + defer layer3.DeleteRouter(t, client, router.ID) + + rule, err := CreateRule(t, client) + if err != nil { + t.Fatalf("Unable to create rule: %v", err) + } + defer DeleteRule(t, client, rule.ID) + + tools.PrintResource(t, rule) + + policy, err := CreatePolicy(t, client, rule.ID) + if err != nil { + t.Fatalf("Unable to create policy: %v", err) + } + defer DeletePolicy(t, client, policy.ID) + + tools.PrintResource(t, policy) + + firewall, err := CreateFirewall(t, client, policy.ID) + if err != nil { + t.Fatalf("Unable to create firewall: %v", err) + } + defer DeleteFirewall(t, client, firewall.ID) + + tools.PrintResource(t, firewall) + + updateOpts := firewalls.UpdateOpts{ + PolicyID: policy.ID, + Description: "Some firewall description", + } + + _, err = firewalls.Update(client, firewall.ID, updateOpts).Extract() + if err != nil { + t.Fatalf("Unable to update firewall: %v", err) + } + + newFirewall, err := firewalls.Get(client, firewall.ID).Extract() + if err != nil { + t.Fatalf("Unable to get firewall: %v", err) + } + + tools.PrintResource(t, newFirewall) +} + +func TestFirewallCRUDRouter(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + router, err := layer3.CreateExternalRouter(t, client) + if err != nil { + t.Fatalf("Unable to create router: %v", err) + } + defer layer3.DeleteRouter(t, client, router.ID) + + rule, err := CreateRule(t, client) + if err != nil { + t.Fatalf("Unable to create rule: %v", err) + } + defer DeleteRule(t, client, rule.ID) + + tools.PrintResource(t, rule) + + policy, err := CreatePolicy(t, client, rule.ID) + if err != nil { + t.Fatalf("Unable to create policy: %v", err) + } + defer DeletePolicy(t, client, policy.ID) + + tools.PrintResource(t, policy) + + firewall, err := CreateFirewallOnRouter(t, client, policy.ID, router.ID) + if err != nil { + t.Fatalf("Unable to create firewall: %v", err) + } + defer DeleteFirewall(t, client, firewall.ID) + + tools.PrintResource(t, firewall) + + router2, err := layer3.CreateExternalRouter(t, client) + if err != nil { + t.Fatalf("Unable to create router: %v", err) + } + defer layer3.DeleteRouter(t, client, router2.ID) + + firewallUpdateOpts := firewalls.UpdateOpts{ + PolicyID: policy.ID, + Description: "Some firewall description", + } + + updateOpts := routerinsertion.UpdateOptsExt{ + firewallUpdateOpts, + []string{router2.ID}, + } + + _, err = firewalls.Update(client, firewall.ID, updateOpts).Extract() + if err != nil { + t.Fatalf("Unable to update firewall: %v", err) + } + + newFirewall, err := firewalls.Get(client, firewall.ID).Extract() + if err != nil { + t.Fatalf("Unable to get firewall: %v", err) + } + + tools.PrintResource(t, newFirewall) +} + +func TestFirewallCRUDRemoveRouter(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + router, err := layer3.CreateExternalRouter(t, client) + if err != nil { + t.Fatalf("Unable to create router: %v", err) + } + defer layer3.DeleteRouter(t, client, router.ID) + + rule, err := CreateRule(t, client) + if err != nil { + t.Fatalf("Unable to create rule: %v", err) + } + defer DeleteRule(t, client, rule.ID) + + tools.PrintResource(t, rule) + + policy, err := CreatePolicy(t, client, rule.ID) + if err != nil { + t.Fatalf("Unable to create policy: %v", err) + } + defer DeletePolicy(t, client, policy.ID) + + tools.PrintResource(t, policy) + + firewall, err := CreateFirewallOnRouter(t, client, policy.ID, router.ID) + if err != nil { + t.Fatalf("Unable to create firewall: %v", err) + } + defer DeleteFirewall(t, client, firewall.ID) + + tools.PrintResource(t, firewall) + + firewallUpdateOpts := firewalls.UpdateOpts{ + PolicyID: policy.ID, + Description: "Some firewall description", + } + + updateOpts := routerinsertion.UpdateOptsExt{ + firewallUpdateOpts, + []string{}, + } + + _, err = firewalls.Update(client, firewall.ID, updateOpts).Extract() + if err != nil { + t.Fatalf("Unable to update firewall: %v", err) + } + + newFirewall, err := firewalls.Get(client, firewall.ID).Extract() + if err != nil { + t.Fatalf("Unable to get firewall: %v", err) + } + + tools.PrintResource(t, newFirewall) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/fwaas/fwaas.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/fwaas/fwaas.go new file mode 100644 index 0000000000..83aa1a400f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/fwaas/fwaas.go @@ -0,0 +1,203 @@ +package fwaas + +import ( + "fmt" + "strconv" + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/policies" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/routerinsertion" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/rules" +) + +// CreateFirewall will create a Firewaill with a random name and a specified +// policy ID. An error will be returned if the firewall could not be created. +func CreateFirewall(t *testing.T, client *gophercloud.ServiceClient, policyID string) (*firewalls.Firewall, error) { + firewallName := tools.RandomString("TESTACC-", 8) + + t.Logf("Attempting to create firewall %s", firewallName) + + iTrue := true + createOpts := firewalls.CreateOpts{ + Name: firewallName, + PolicyID: policyID, + AdminStateUp: &iTrue, + } + + firewall, err := firewalls.Create(client, createOpts).Extract() + if err != nil { + return firewall, err + } + + t.Logf("Waiting for firewall to become active.") + if err := WaitForFirewallState(client, firewall.ID, "ACTIVE", 60); err != nil { + return firewall, err + } + + t.Logf("Successfully created firewall %s", firewallName) + + return firewall, nil +} + +// CreateFirewallOnRouter will create a Firewall with a random name and a +// specified policy ID attached to a specified Router. An error will be +// returned if the firewall could not be created. +func CreateFirewallOnRouter(t *testing.T, client *gophercloud.ServiceClient, policyID string, routerID string) (*firewalls.Firewall, error) { + firewallName := tools.RandomString("TESTACC-", 8) + + t.Logf("Attempting to create firewall %s", firewallName) + + firewallCreateOpts := firewalls.CreateOpts{ + Name: firewallName, + PolicyID: policyID, + } + + createOpts := routerinsertion.CreateOptsExt{ + CreateOptsBuilder: firewallCreateOpts, + RouterIDs: []string{routerID}, + } + + firewall, err := firewalls.Create(client, createOpts).Extract() + if err != nil { + return firewall, err + } + + t.Logf("Waiting for firewall to become active.") + if err := WaitForFirewallState(client, firewall.ID, "ACTIVE", 60); err != nil { + return firewall, err + } + + t.Logf("Successfully created firewall %s", firewallName) + + return firewall, nil +} + +// CreatePolicy will create a Firewall Policy with a random name and given +// rule. An error will be returned if the rule could not be created. +func CreatePolicy(t *testing.T, client *gophercloud.ServiceClient, ruleID string) (*policies.Policy, error) { + policyName := tools.RandomString("TESTACC-", 8) + + t.Logf("Attempting to create policy %s", policyName) + + createOpts := policies.CreateOpts{ + Name: policyName, + Rules: []string{ + ruleID, + }, + } + + policy, err := policies.Create(client, createOpts).Extract() + if err != nil { + return policy, err + } + + t.Logf("Successfully created policy %s", policyName) + + return policy, nil +} + +// CreateRule will create a Firewall Rule with a random source address and +//source port, destination address and port. An error will be returned if +// the rule could not be created. +func CreateRule(t *testing.T, client *gophercloud.ServiceClient) (*rules.Rule, error) { + ruleName := tools.RandomString("TESTACC-", 8) + sourceAddress := fmt.Sprintf("192.168.1.%d", tools.RandomInt(1, 100)) + sourcePort := strconv.Itoa(tools.RandomInt(1, 100)) + destinationAddress := fmt.Sprintf("192.168.2.%d", tools.RandomInt(1, 100)) + destinationPort := strconv.Itoa(tools.RandomInt(1, 100)) + + t.Logf("Attempting to create rule %s with source %s:%s and destination %s:%s", + ruleName, sourceAddress, sourcePort, destinationAddress, destinationPort) + + createOpts := rules.CreateOpts{ + Name: ruleName, + Protocol: rules.ProtocolTCP, + Action: "allow", + SourceIPAddress: sourceAddress, + SourcePort: sourcePort, + DestinationIPAddress: destinationAddress, + DestinationPort: destinationPort, + } + + rule, err := rules.Create(client, createOpts).Extract() + if err != nil { + return rule, err + } + + t.Logf("Rule %s successfully created", ruleName) + + return rule, nil +} + +// DeleteFirewall will delete a firewall with a specified ID. A fatal error +// will occur if the delete was not successful. This works best when used as +// a deferred function. +func DeleteFirewall(t *testing.T, client *gophercloud.ServiceClient, firewallID string) { + t.Logf("Attempting to delete firewall: %s", firewallID) + + err := firewalls.Delete(client, firewallID).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete firewall %s: %v", firewallID, err) + } + + t.Logf("Waiting for firewall to delete.") + if err := WaitForFirewallState(client, firewallID, "DELETED", 60); err != nil { + t.Logf("Unable to delete firewall: %s", firewallID) + } + + t.Logf("Firewall deleted: %s", firewallID) +} + +// DeletePolicy will delete a policy with a specified ID. A fatal error will +// occur if the delete was not successful. This works best when used as a +// deferred function. +func DeletePolicy(t *testing.T, client *gophercloud.ServiceClient, policyID string) { + t.Logf("Attempting to delete policy: %s", policyID) + + err := policies.Delete(client, policyID).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete policy %s: %v", policyID, err) + } + + t.Logf("Deleted policy: %s", policyID) +} + +// DeleteRule will delete a rule with a specified ID. A fatal error will occur +// if the delete was not successful. This works best when used as a deferred +// function. +func DeleteRule(t *testing.T, client *gophercloud.ServiceClient, ruleID string) { + t.Logf("Attempting to delete rule: %s", ruleID) + + err := rules.Delete(client, ruleID).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete rule %s: %v", ruleID, err) + } + + t.Logf("Deleted rule: %s", ruleID) +} + +// WaitForFirewallState will wait until a firewall reaches a given state. +func WaitForFirewallState(client *gophercloud.ServiceClient, firewallID, status string, secs int) error { + return gophercloud.WaitFor(secs, func() (bool, error) { + current, err := firewalls.Get(client, firewallID).Extract() + if err != nil { + if httpStatus, ok := err.(gophercloud.ErrDefault404); ok { + if httpStatus.Actual == 404 { + if status == "DELETED" { + return true, nil + } + } + } + return false, err + } + + if current.Status == status { + return true, nil + } + + return false, nil + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/fwaas/pkg.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/fwaas/pkg.go new file mode 100644 index 0000000000..206bf3313a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/fwaas/pkg.go @@ -0,0 +1 @@ +package fwaas diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/fwaas/policy_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/fwaas/policy_test.go new file mode 100644 index 0000000000..3220d821a3 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/fwaas/policy_test.go @@ -0,0 +1,71 @@ +// +build acceptance networking fwaas + +package fwaas + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/policies" +) + +func TestPolicyList(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + allPages, err := policies.List(client, nil).AllPages() + if err != nil { + t.Fatalf("Unable to list policies: %v", err) + } + + allPolicies, err := policies.ExtractPolicies(allPages) + if err != nil { + t.Fatalf("Unable to extract policies: %v", err) + } + + for _, policy := range allPolicies { + tools.PrintResource(t, policy) + } +} + +func TestPolicyCRUD(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + rule, err := CreateRule(t, client) + if err != nil { + t.Fatalf("Unable to create rule: %v", err) + } + defer DeleteRule(t, client, rule.ID) + + tools.PrintResource(t, rule) + + policy, err := CreatePolicy(t, client, rule.ID) + if err != nil { + t.Fatalf("Unable to create policy: %v", err) + } + defer DeletePolicy(t, client, policy.ID) + + tools.PrintResource(t, policy) + + updateOpts := policies.UpdateOpts{ + Description: "Some policy description", + } + + _, err = policies.Update(client, policy.ID, updateOpts).Extract() + if err != nil { + t.Fatalf("Unable to update policy: %v", err) + } + + newPolicy, err := policies.Get(client, policy.ID).Extract() + if err != nil { + t.Fatalf("Unable to get policy: %v", err) + } + + tools.PrintResource(t, newPolicy) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/fwaas/rule_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/fwaas/rule_test.go new file mode 100644 index 0000000000..4521a60b81 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/fwaas/rule_test.go @@ -0,0 +1,64 @@ +// +build acceptance networking fwaas + +package fwaas + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/rules" +) + +func TestRuleList(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + allPages, err := rules.List(client, nil).AllPages() + if err != nil { + t.Fatalf("Unable to list rules: %v", err) + } + + allRules, err := rules.ExtractRules(allPages) + if err != nil { + t.Fatalf("Unable to extract rules: %v", err) + } + + for _, rule := range allRules { + tools.PrintResource(t, rule) + } +} + +func TestRuleCRUD(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + rule, err := CreateRule(t, client) + if err != nil { + t.Fatalf("Unable to create rule: %v", err) + } + defer DeleteRule(t, client, rule.ID) + + tools.PrintResource(t, rule) + + ruleDescription := "Some rule description" + updateOpts := rules.UpdateOpts{ + Description: &ruleDescription, + } + + _, err = rules.Update(client, rule.ID, updateOpts).Extract() + if err != nil { + t.Fatalf("Unable to update rule: %v", err) + } + + newRule, err := rules.Get(client, rule.ID).Extract() + if err != nil { + t.Fatalf("Unable to get rule: %v", err) + } + + tools.PrintResource(t, newRule) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/layer3/floatingips_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/layer3/floatingips_test.go new file mode 100644 index 0000000000..351020410e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/layer3/floatingips_test.go @@ -0,0 +1,100 @@ +// +build acceptance networking layer3 floatingips + +package layer3 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + networking "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips" + "github.com/gophercloud/gophercloud/openstack/networking/v2/networks" +) + +func TestLayer3FloatingIPsList(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + listOpts := floatingips.ListOpts{ + Status: "DOWN", + } + allPages, err := floatingips.List(client, listOpts).AllPages() + if err != nil { + t.Fatalf("Unable to list floating IPs: %v", err) + } + + allFIPs, err := floatingips.ExtractFloatingIPs(allPages) + if err != nil { + t.Fatalf("Unable to extract floating IPs: %v", err) + } + + for _, fip := range allFIPs { + tools.PrintResource(t, fip) + } +} + +func TestLayer3FloatingIPsCreateDelete(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + choices, err := clients.AcceptanceTestChoicesFromEnv() + if err != nil { + t.Fatalf("Unable to get choices: %v", err) + } + + netid, err := networks.IDFromName(client, choices.NetworkName) + if err != nil { + t.Fatalf("Unable to find network id: %v", err) + } + + subnet, err := networking.CreateSubnet(t, client, netid) + if err != nil { + t.Fatalf("Unable to create subnet: %v", err) + } + defer networking.DeleteSubnet(t, client, subnet.ID) + + router, err := CreateExternalRouter(t, client) + if err != nil { + t.Fatalf("Unable to create router: %v", err) + } + defer DeleteRouter(t, client, router.ID) + + port, err := networking.CreatePort(t, client, netid, subnet.ID) + if err != nil { + t.Fatalf("Unable to create port: %v", err) + } + + _, err = CreateRouterInterface(t, client, port.ID, router.ID) + if err != nil { + t.Fatalf("Unable to create router interface: %v", err) + } + defer DeleteRouterInterface(t, client, port.ID, router.ID) + + fip, err := CreateFloatingIP(t, client, choices.ExternalNetworkID, port.ID) + if err != nil { + t.Fatalf("Unable to create floating IP: %v", err) + } + defer DeleteFloatingIP(t, client, fip.ID) + + newFip, err := floatingips.Get(client, fip.ID).Extract() + if err != nil { + t.Fatalf("Unable to get floating ip: %v", err) + } + + tools.PrintResource(t, newFip) + + // Disassociate the floating IP + updateOpts := floatingips.UpdateOpts{ + PortID: nil, + } + + newFip, err = floatingips.Update(client, fip.ID, updateOpts).Extract() + if err != nil { + t.Fatalf("Unable to disassociate floating IP: %v", err) + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/layer3/layer3.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/layer3/layer3.go new file mode 100644 index 0000000000..7bc0676d00 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/layer3/layer3.go @@ -0,0 +1,248 @@ +package layer3 + +import ( + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers" + "github.com/gophercloud/gophercloud/openstack/networking/v2/ports" +) + +// CreateFloatingIP creates a floating IP on a given network and port. An error +// will be returned if the creation failed. +func CreateFloatingIP(t *testing.T, client *gophercloud.ServiceClient, networkID, portID string) (*floatingips.FloatingIP, error) { + t.Logf("Attempting to create floating IP on port: %s", portID) + + createOpts := &floatingips.CreateOpts{ + FloatingNetworkID: networkID, + PortID: portID, + } + + floatingIP, err := floatingips.Create(client, createOpts).Extract() + if err != nil { + return floatingIP, err + } + + t.Logf("Created floating IP.") + + return floatingIP, err +} + +// CreateExternalRouter creates a router on the external network. This requires +// the OS_EXTGW_ID environment variable to be set. An error is returned if the +// creation failed. +func CreateExternalRouter(t *testing.T, client *gophercloud.ServiceClient) (*routers.Router, error) { + var router *routers.Router + choices, err := clients.AcceptanceTestChoicesFromEnv() + if err != nil { + return router, err + } + + routerName := tools.RandomString("TESTACC-", 8) + + t.Logf("Attempting to create external router: %s", routerName) + + adminStateUp := true + gatewayInfo := routers.GatewayInfo{ + NetworkID: choices.ExternalNetworkID, + } + + createOpts := routers.CreateOpts{ + Name: routerName, + AdminStateUp: &adminStateUp, + GatewayInfo: &gatewayInfo, + } + + router, err = routers.Create(client, createOpts).Extract() + if err != nil { + return router, err + } + + if err := WaitForRouterToCreate(client, router.ID, 60); err != nil { + return router, err + } + + t.Logf("Created router: %s", routerName) + + return router, nil +} + +// CreateRouter creates a router on a specified Network ID. An error will be +// returned if the creation failed. +func CreateRouter(t *testing.T, client *gophercloud.ServiceClient, networkID string) (*routers.Router, error) { + routerName := tools.RandomString("TESTACC-", 8) + + t.Logf("Attempting to create router: %s", routerName) + + adminStateUp := true + gatewayInfo := routers.GatewayInfo{ + NetworkID: networkID, + } + + createOpts := routers.CreateOpts{ + Name: routerName, + AdminStateUp: &adminStateUp, + GatewayInfo: &gatewayInfo, + } + + router, err := routers.Create(client, createOpts).Extract() + if err != nil { + return router, err + } + + if err := WaitForRouterToCreate(client, router.ID, 60); err != nil { + return router, err + } + + t.Logf("Created router: %s", routerName) + + return router, nil +} + +// CreateRouterInterface will attach a subnet to a router. An error will be +// returned if the operation fails. +func CreateRouterInterface(t *testing.T, client *gophercloud.ServiceClient, portID, routerID string) (*routers.InterfaceInfo, error) { + t.Logf("Attempting to add port %s to router %s", portID, routerID) + + aiOpts := routers.AddInterfaceOpts{ + PortID: portID, + } + + iface, err := routers.AddInterface(client, routerID, aiOpts).Extract() + if err != nil { + return iface, err + } + + if err := WaitForRouterInterfaceToAttach(client, portID, 60); err != nil { + return iface, err + } + + t.Logf("Successfully added port %s to router %s", portID, routerID) + return iface, nil +} + +// DeleteRouter deletes a router of a specified ID. A fatal error will occur +// if the deletion failed. This works best when used as a deferred function. +func DeleteRouter(t *testing.T, client *gophercloud.ServiceClient, routerID string) { + t.Logf("Attempting to delete router: %s", routerID) + + err := routers.Delete(client, routerID).ExtractErr() + if err != nil { + t.Fatalf("Error deleting router: %v", err) + } + + if err := WaitForRouterToDelete(client, routerID, 60); err != nil { + t.Fatalf("Error waiting for router to delete: %v", err) + } + + t.Logf("Deleted router: %s", routerID) +} + +// DeleteRouterInterface will detach a subnet to a router. A fatal error will +// occur if the deletion failed. This works best when used as a deferred +// function. +func DeleteRouterInterface(t *testing.T, client *gophercloud.ServiceClient, portID, routerID string) { + t.Logf("Attempting to detach port %s from router %s", portID, routerID) + + riOpts := routers.RemoveInterfaceOpts{ + PortID: portID, + } + + _, err := routers.RemoveInterface(client, routerID, riOpts).Extract() + if err != nil { + t.Fatalf("Failed to detach port %s from router %s", portID, routerID) + } + + if err := WaitForRouterInterfaceToDetach(client, portID, 60); err != nil { + t.Fatalf("Failed to wait for port %s to detach from router %s", portID, routerID) + } + + t.Logf("Successfully detached port %s from router %s", portID, routerID) +} + +// DeleteFloatingIP deletes a floatingIP of a specified ID. A fatal error will +// occur if the deletion failed. This works best when used as a deferred +// function. +func DeleteFloatingIP(t *testing.T, client *gophercloud.ServiceClient, floatingIPID string) { + t.Logf("Attempting to delete floating IP: %s", floatingIPID) + + err := floatingips.Delete(client, floatingIPID).ExtractErr() + if err != nil { + t.Fatalf("Failed to delete floating IP: %v", err) + } + + t.Logf("Deleted floating IP: %s", floatingIPID) +} + +func WaitForRouterToCreate(client *gophercloud.ServiceClient, routerID string, secs int) error { + return gophercloud.WaitFor(secs, func() (bool, error) { + r, err := routers.Get(client, routerID).Extract() + if err != nil { + return false, err + } + + if r.Status == "ACTIVE" { + return true, nil + } + + return false, nil + }) +} + +func WaitForRouterToDelete(client *gophercloud.ServiceClient, routerID string, secs int) error { + return gophercloud.WaitFor(secs, func() (bool, error) { + _, err := routers.Get(client, routerID).Extract() + if err != nil { + if _, ok := err.(gophercloud.ErrDefault404); ok { + return true, nil + } + + return false, err + } + + return false, nil + }) +} + +func WaitForRouterInterfaceToAttach(client *gophercloud.ServiceClient, routerInterfaceID string, secs int) error { + return gophercloud.WaitFor(secs, func() (bool, error) { + r, err := ports.Get(client, routerInterfaceID).Extract() + if err != nil { + return false, err + } + + if r.Status == "ACTIVE" { + return true, nil + } + + return false, nil + }) +} + +func WaitForRouterInterfaceToDetach(client *gophercloud.ServiceClient, routerInterfaceID string, secs int) error { + return gophercloud.WaitFor(secs, func() (bool, error) { + r, err := ports.Get(client, routerInterfaceID).Extract() + if err != nil { + if _, ok := err.(gophercloud.ErrDefault404); ok { + return true, nil + } + + if errCode, ok := err.(gophercloud.ErrUnexpectedResponseCode); ok { + if errCode.Actual == 409 { + return false, nil + } + } + + return false, err + } + + if r.Status == "ACTIVE" { + return true, nil + } + + return false, nil + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/layer3/routers_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/layer3/routers_test.go new file mode 100644 index 0000000000..06194af223 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/layer3/routers_test.go @@ -0,0 +1,119 @@ +// +build acceptance networking layer3 router + +package layer3 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + networking "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers" + "github.com/gophercloud/gophercloud/openstack/networking/v2/networks" +) + +func TestLayer3RouterList(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + listOpts := routers.ListOpts{} + allPages, err := routers.List(client, listOpts).AllPages() + if err != nil { + t.Fatalf("Unable to list routers: %v", err) + } + + allRouters, err := routers.ExtractRouters(allPages) + if err != nil { + t.Fatalf("Unable to extract routers: %v", err) + } + + for _, router := range allRouters { + tools.PrintResource(t, router) + } +} + +func TestLayer3RouterCreateDelete(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + router, err := CreateExternalRouter(t, client) + if err != nil { + t.Fatalf("Unable to create router: %v", err) + } + defer DeleteRouter(t, client, router.ID) + + tools.PrintResource(t, router) + + newName := tools.RandomString("TESTACC-", 8) + updateOpts := routers.UpdateOpts{ + Name: newName, + } + + _, err = routers.Update(client, router.ID, updateOpts).Extract() + if err != nil { + t.Fatalf("Unable to update router: %v", err) + } + + newRouter, err := routers.Get(client, router.ID).Extract() + if err != nil { + t.Fatalf("Unable to get router: %v", err) + } + + tools.PrintResource(t, newRouter) +} + +func TestLayer3RouterInterface(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + choices, err := clients.AcceptanceTestChoicesFromEnv() + if err != nil { + t.Fatalf("Unable to get choices: %v", err) + } + + netid, err := networks.IDFromName(client, choices.NetworkName) + if err != nil { + t.Fatalf("Unable to find network id: %v", err) + } + + subnet, err := networking.CreateSubnet(t, client, netid) + if err != nil { + t.Fatalf("Unable to create subnet: %v", err) + } + defer networking.DeleteSubnet(t, client, subnet.ID) + + tools.PrintResource(t, subnet) + + router, err := CreateExternalRouter(t, client) + if err != nil { + t.Fatalf("Unable to create router: %v", err) + } + defer DeleteRouter(t, client, router.ID) + + aiOpts := routers.AddInterfaceOpts{ + SubnetID: subnet.ID, + } + + iface, err := routers.AddInterface(client, router.ID, aiOpts).Extract() + if err != nil { + t.Fatalf("Failed to add interface to router: %v", err) + } + + tools.PrintResource(t, router) + tools.PrintResource(t, iface) + + riOpts := routers.RemoveInterfaceOpts{ + SubnetID: subnet.ID, + } + + _, err = routers.RemoveInterface(client, router.ID, riOpts).Extract() + if err != nil { + t.Fatalf("Failed to remove interface from router: %v", err) + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/lbaas.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/lbaas.go new file mode 100644 index 0000000000..b31d3e5b42 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/lbaas.go @@ -0,0 +1,160 @@ +package lbaas + +import ( + "fmt" + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/members" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/monitors" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/pools" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/vips" +) + +// CreateMember will create a load balancer member in a specified pool on a +// random port. An error will be returned if the member could not be created. +func CreateMember(t *testing.T, client *gophercloud.ServiceClient, poolID string) (*members.Member, error) { + protocolPort := tools.RandomInt(100, 1000) + address := tools.RandomInt(2, 200) + t.Logf("Attempting to create member in port %d", protocolPort) + + createOpts := members.CreateOpts{ + PoolID: poolID, + ProtocolPort: protocolPort, + Address: fmt.Sprintf("192.168.1.%d", address), + } + + member, err := members.Create(client, createOpts).Extract() + if err != nil { + return member, err + } + + t.Logf("Successfully created member %s", member.ID) + + return member, nil +} + +// CreateMonitor will create a monitor with a random name for a specific pool. +// An error will be returned if the monitor could not be created. +func CreateMonitor(t *testing.T, client *gophercloud.ServiceClient) (*monitors.Monitor, error) { + t.Logf("Attempting to create monitor.") + + createOpts := monitors.CreateOpts{ + Type: monitors.TypePING, + Delay: 90, + Timeout: 60, + MaxRetries: 10, + AdminStateUp: gophercloud.Enabled, + } + + monitor, err := monitors.Create(client, createOpts).Extract() + if err != nil { + return monitor, err + } + + t.Logf("Successfully created monitor %s", monitor.ID) + + return monitor, nil +} + +// CreatePool will create a pool with a random name. An error will be returned +// if the pool could not be deleted. +func CreatePool(t *testing.T, client *gophercloud.ServiceClient, subnetID string) (*pools.Pool, error) { + poolName := tools.RandomString("TESTACCT-", 8) + + t.Logf("Attempting to create pool %s", poolName) + + createOpts := pools.CreateOpts{ + Name: poolName, + SubnetID: subnetID, + Protocol: pools.ProtocolTCP, + LBMethod: pools.LBMethodRoundRobin, + } + + pool, err := pools.Create(client, createOpts).Extract() + if err != nil { + return pool, err + } + + t.Logf("Successfully created pool %s", poolName) + + return pool, nil +} + +// CreateVIP will create a vip with a random name and a random port in a +// specified subnet and pool. An error will be returned if the vip could +// not be created. +func CreateVIP(t *testing.T, client *gophercloud.ServiceClient, subnetID, poolID string) (*vips.VirtualIP, error) { + vipName := tools.RandomString("TESTACCT-", 8) + vipPort := tools.RandomInt(100, 10000) + + t.Logf("Attempting to create VIP %s", vipName) + + createOpts := vips.CreateOpts{ + Name: vipName, + SubnetID: subnetID, + PoolID: poolID, + Protocol: "TCP", + ProtocolPort: vipPort, + } + + vip, err := vips.Create(client, createOpts).Extract() + if err != nil { + return vip, err + } + + t.Logf("Successfully created vip %s", vipName) + + return vip, nil +} + +// DeleteMember will delete a specified member. A fatal error will occur if +// the member could not be deleted. This works best when used as a deferred +// function. +func DeleteMember(t *testing.T, client *gophercloud.ServiceClient, memberID string) { + t.Logf("Attempting to delete member %s", memberID) + + if err := members.Delete(client, memberID).ExtractErr(); err != nil { + t.Fatalf("Unable to delete member: %v", err) + } + + t.Logf("Successfully deleted member %s", memberID) +} + +// DeleteMonitor will delete a specified monitor. A fatal error will occur if +// the monitor could not be deleted. This works best when used as a deferred +// function. +func DeleteMonitor(t *testing.T, client *gophercloud.ServiceClient, monitorID string) { + t.Logf("Attempting to delete monitor %s", monitorID) + + if err := monitors.Delete(client, monitorID).ExtractErr(); err != nil { + t.Fatalf("Unable to delete monitor: %v", err) + } + + t.Logf("Successfully deleted monitor %s", monitorID) +} + +// DeletePool will delete a specified pool. A fatal error will occur if the +// pool could not be deleted. This works best when used as a deferred function. +func DeletePool(t *testing.T, client *gophercloud.ServiceClient, poolID string) { + t.Logf("Attempting to delete pool %s", poolID) + + if err := pools.Delete(client, poolID).ExtractErr(); err != nil { + t.Fatalf("Unable to delete pool: %v", err) + } + + t.Logf("Successfully deleted pool %s", poolID) +} + +// DeleteVIP will delete a specified vip. A fatal error will occur if the vip +// could not be deleted. This works best when used as a deferred function. +func DeleteVIP(t *testing.T, client *gophercloud.ServiceClient, vipID string) { + t.Logf("Attempting to delete vip %s", vipID) + + if err := vips.Delete(client, vipID).ExtractErr(); err != nil { + t.Fatalf("Unable to delete vip: %v", err) + } + + t.Logf("Successfully deleted vip %s", vipID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/members_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/members_test.go new file mode 100644 index 0000000000..75dec83986 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/members_test.go @@ -0,0 +1,83 @@ +// +build acceptance networking lbaas member + +package lbaas + +import ( + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/acceptance/clients" + networking "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/members" +) + +func TestMembersList(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + allPages, err := members.List(client, members.ListOpts{}).AllPages() + if err != nil { + t.Fatalf("Unable to list members: %v", err) + } + + allMembers, err := members.ExtractMembers(allPages) + if err != nil { + t.Fatalf("Unable to extract members: %v", err) + } + + for _, member := range allMembers { + tools.PrintResource(t, member) + } +} + +func TestMembersCRUD(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + network, err := networking.CreateNetwork(t, client) + if err != nil { + t.Fatalf("Unable to create network: %v", err) + } + defer networking.DeleteNetwork(t, client, network.ID) + + subnet, err := networking.CreateSubnet(t, client, network.ID) + if err != nil { + t.Fatalf("Unable to create subnet: %v", err) + } + defer networking.DeleteSubnet(t, client, subnet.ID) + + pool, err := CreatePool(t, client, subnet.ID) + if err != nil { + t.Fatalf("Unable to create pool: %v", err) + } + defer DeletePool(t, client, pool.ID) + + member, err := CreateMember(t, client, pool.ID) + if err != nil { + t.Fatalf("Unable to create member: %v", err) + } + defer DeleteMember(t, client, member.ID) + + tools.PrintResource(t, member) + + updateOpts := members.UpdateOpts{ + AdminStateUp: gophercloud.Enabled, + } + + _, err = members.Update(client, member.ID, updateOpts).Extract() + if err != nil { + t.Fatalf("Unable to update member: %v") + } + + newMember, err := members.Get(client, member.ID).Extract() + if err != nil { + t.Fatalf("Unable to get member: %v") + } + + tools.PrintResource(t, newMember) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/monitors_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/monitors_test.go new file mode 100644 index 0000000000..56b413afb0 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/monitors_test.go @@ -0,0 +1,63 @@ +// +build acceptance networking lbaas monitors + +package lbaas + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/monitors" +) + +func TestMonitorsList(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + allPages, err := monitors.List(client, monitors.ListOpts{}).AllPages() + if err != nil { + t.Fatalf("Unable to list monitors: %v", err) + } + + allMonitors, err := monitors.ExtractMonitors(allPages) + if err != nil { + t.Fatalf("Unable to extract monitors: %v", err) + } + + for _, monitor := range allMonitors { + tools.PrintResource(t, monitor) + } +} + +func TestMonitorsCRUD(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + monitor, err := CreateMonitor(t, client) + if err != nil { + t.Fatalf("Unable to create monitor: %v", err) + } + defer DeleteMonitor(t, client, monitor.ID) + + tools.PrintResource(t, monitor) + + updateOpts := monitors.UpdateOpts{ + Delay: 999, + } + + _, err = monitors.Update(client, monitor.ID, updateOpts).Extract() + if err != nil { + t.Fatalf("Unable to update monitor: %v") + } + + newMonitor, err := monitors.Get(client, monitor.ID).Extract() + if err != nil { + t.Fatalf("Unable to get monitor: %v") + } + + tools.PrintResource(t, newMonitor) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/pkg.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/pkg.go new file mode 100644 index 0000000000..f5a7df7b75 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/pkg.go @@ -0,0 +1 @@ +package lbaas diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/pools_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/pools_test.go new file mode 100644 index 0000000000..b53237c0e3 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/pools_test.go @@ -0,0 +1,118 @@ +// +build acceptance networking lbaas pool + +package lbaas + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + networking "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/pools" +) + +func TestPoolsList(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + allPages, err := pools.List(client, pools.ListOpts{}).AllPages() + if err != nil { + t.Fatalf("Unable to list pools: %v", err) + } + + allPools, err := pools.ExtractPools(allPages) + if err != nil { + t.Fatalf("Unable to extract pools: %v", err) + } + + for _, pool := range allPools { + tools.PrintResource(t, pool) + } +} + +func TestPoolsCRUD(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + network, err := networking.CreateNetwork(t, client) + if err != nil { + t.Fatalf("Unable to create network: %v", err) + } + defer networking.DeleteNetwork(t, client, network.ID) + + subnet, err := networking.CreateSubnet(t, client, network.ID) + if err != nil { + t.Fatalf("Unable to create subnet: %v", err) + } + defer networking.DeleteSubnet(t, client, subnet.ID) + + pool, err := CreatePool(t, client, subnet.ID) + if err != nil { + t.Fatalf("Unable to create pool: %v", err) + } + defer DeletePool(t, client, pool.ID) + + tools.PrintResource(t, pool) + + updateOpts := pools.UpdateOpts{ + LBMethod: pools.LBMethodLeastConnections, + } + + _, err = pools.Update(client, pool.ID, updateOpts).Extract() + if err != nil { + t.Fatalf("Unable to update pool: %v") + } + + newPool, err := pools.Get(client, pool.ID).Extract() + if err != nil { + t.Fatalf("Unable to get pool: %v") + } + + tools.PrintResource(t, newPool) +} + +func TestPoolsMonitors(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + network, err := networking.CreateNetwork(t, client) + if err != nil { + t.Fatalf("Unable to create network: %v", err) + } + defer networking.DeleteNetwork(t, client, network.ID) + + subnet, err := networking.CreateSubnet(t, client, network.ID) + if err != nil { + t.Fatalf("Unable to create subnet: %v", err) + } + defer networking.DeleteSubnet(t, client, subnet.ID) + + pool, err := CreatePool(t, client, subnet.ID) + if err != nil { + t.Fatalf("Unable to create pool: %v", err) + } + defer DeletePool(t, client, pool.ID) + + monitor, err := CreateMonitor(t, client) + if err != nil { + t.Fatalf("Unable to create monitor: %v", err) + } + defer DeleteMonitor(t, client, monitor.ID) + + t.Logf("Associating monitor %s with pool %s", monitor.ID, pool.ID) + if res := pools.AssociateMonitor(client, pool.ID, monitor.ID); res.Err != nil { + t.Fatalf("Unable to associate monitor to pool") + } + + t.Logf("Disassociating monitor %s with pool %s", monitor.ID, pool.ID) + if res := pools.DisassociateMonitor(client, pool.ID, monitor.ID); res.Err != nil { + t.Fatalf("Unable to disassociate monitor from pool") + } + +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/vips_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/vips_test.go new file mode 100644 index 0000000000..a63dc63c6c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/vips_test.go @@ -0,0 +1,83 @@ +// +build acceptance networking lbaas vip + +package lbaas + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + networking "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/vips" +) + +func TestVIPsList(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + allPages, err := vips.List(client, vips.ListOpts{}).AllPages() + if err != nil { + t.Fatalf("Unable to list vips: %v", err) + } + + allVIPs, err := vips.ExtractVIPs(allPages) + if err != nil { + t.Fatalf("Unable to extract vips: %v", err) + } + + for _, vip := range allVIPs { + tools.PrintResource(t, vip) + } +} + +func TestVIPsCRUD(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + network, err := networking.CreateNetwork(t, client) + if err != nil { + t.Fatalf("Unable to create network: %v", err) + } + defer networking.DeleteNetwork(t, client, network.ID) + + subnet, err := networking.CreateSubnet(t, client, network.ID) + if err != nil { + t.Fatalf("Unable to create subnet: %v", err) + } + defer networking.DeleteSubnet(t, client, subnet.ID) + + pool, err := CreatePool(t, client, subnet.ID) + if err != nil { + t.Fatalf("Unable to create pool: %v", err) + } + defer DeletePool(t, client, pool.ID) + + vip, err := CreateVIP(t, client, subnet.ID, pool.ID) + if err != nil { + t.Fatalf("Unable to create vip: %v", err) + } + defer DeleteVIP(t, client, vip.ID) + + tools.PrintResource(t, vip) + + connLimit := 100 + updateOpts := vips.UpdateOpts{ + ConnLimit: &connLimit, + } + + _, err = vips.Update(client, vip.ID, updateOpts).Extract() + if err != nil { + t.Fatalf("Unable to update vip: %v") + } + + newVIP, err := vips.Get(client, vip.ID).Extract() + if err != nil { + t.Fatalf("Unable to get vip: %v") + } + + tools.PrintResource(t, newVIP) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas_v2/lbaas_v2.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas_v2/lbaas_v2.go new file mode 100644 index 0000000000..093f835b9b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas_v2/lbaas_v2.go @@ -0,0 +1,282 @@ +package lbaas_v2 + +import ( + "fmt" + "strings" + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools" +) + +const loadbalancerActiveTimeoutSeconds = 300 +const loadbalancerDeleteTimeoutSeconds = 300 + +// CreateListener will create a listener for a given load balancer on a random +// port with a random name. An error will be returned if the listener could not +// be created. +func CreateListener(t *testing.T, client *gophercloud.ServiceClient, lb *loadbalancers.LoadBalancer) (*listeners.Listener, error) { + listenerName := tools.RandomString("TESTACCT-", 8) + listenerPort := tools.RandomInt(1, 100) + + t.Logf("Attempting to create listener %s on port %d", listenerName, listenerPort) + + createOpts := listeners.CreateOpts{ + Name: listenerName, + LoadbalancerID: lb.ID, + Protocol: "TCP", + ProtocolPort: listenerPort, + } + + listener, err := listeners.Create(client, createOpts).Extract() + if err != nil { + return listener, err + } + + t.Logf("Successfully created listener %s", listenerName) + + if err := WaitForLoadBalancerState(client, lb.ID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil { + return listener, fmt.Errorf("Timed out waiting for loadbalancer to become active") + } + + return listener, nil +} + +// CreateLoadBalancer will create a load balancer with a random name on a given +// subnet. An error will be returned if the loadbalancer could not be created. +func CreateLoadBalancer(t *testing.T, client *gophercloud.ServiceClient, subnetID string) (*loadbalancers.LoadBalancer, error) { + lbName := tools.RandomString("TESTACCT-", 8) + + t.Logf("Attempting to create loadbalancer %s on subnet %s", lbName, subnetID) + + createOpts := loadbalancers.CreateOpts{ + Name: lbName, + VipSubnetID: subnetID, + AdminStateUp: gophercloud.Enabled, + } + + lb, err := loadbalancers.Create(client, createOpts).Extract() + if err != nil { + return lb, err + } + + t.Logf("Successfully created loadbalancer %s on subnet %s", lbName, subnetID) + t.Logf("Waiting for loadbalancer %s to become active", lbName) + + if err := WaitForLoadBalancerState(client, lb.ID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil { + return lb, err + } + + t.Logf("LoadBalancer %s is active", lbName) + + return lb, nil +} + +// CreateMember will create a member with a random name, port, address, and +// weight. An error will be returned if the member could not be created. +func CreateMember(t *testing.T, client *gophercloud.ServiceClient, lb *loadbalancers.LoadBalancer, pool *pools.Pool, subnetID, subnetCIDR string) (*pools.Member, error) { + memberName := tools.RandomString("TESTACCT-", 8) + memberPort := tools.RandomInt(100, 1000) + memberWeight := tools.RandomInt(1, 10) + + cidrParts := strings.Split(subnetCIDR, "/") + subnetParts := strings.Split(cidrParts[0], ".") + memberAddress := fmt.Sprintf("%s.%s.%s.%d", subnetParts[0], subnetParts[1], subnetParts[2], tools.RandomInt(10, 100)) + + t.Logf("Attempting to create member %s", memberName) + + createOpts := pools.CreateMemberOpts{ + Name: memberName, + ProtocolPort: memberPort, + Weight: memberWeight, + Address: memberAddress, + SubnetID: subnetID, + } + + t.Logf("Member create opts: %#v", createOpts) + + member, err := pools.CreateMember(client, pool.ID, createOpts).Extract() + if err != nil { + return member, err + } + + t.Logf("Successfully created member %s", memberName) + + if err := WaitForLoadBalancerState(client, lb.ID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil { + return member, fmt.Errorf("Timed out waiting for loadbalancer to become active") + } + + return member, nil +} + +// CreateMonitor will create a monitor with a random name for a specific pool. +// An error will be returned if the monitor could not be created. +func CreateMonitor(t *testing.T, client *gophercloud.ServiceClient, lb *loadbalancers.LoadBalancer, pool *pools.Pool) (*monitors.Monitor, error) { + monitorName := tools.RandomString("TESTACCT-", 8) + + t.Logf("Attempting to create monitor %s", monitorName) + + createOpts := monitors.CreateOpts{ + PoolID: pool.ID, + Name: monitorName, + Delay: 10, + Timeout: 5, + MaxRetries: 5, + Type: "PING", + } + + monitor, err := monitors.Create(client, createOpts).Extract() + if err != nil { + return monitor, err + } + + t.Logf("Successfully created monitor: %s", monitorName) + + if err := WaitForLoadBalancerState(client, lb.ID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil { + return monitor, fmt.Errorf("Timed out waiting for loadbalancer to become active") + } + + return monitor, nil +} + +// CreatePool will create a pool with a random name with a specified listener +// and loadbalancer. An error will be returned if the pool could not be +// created. +func CreatePool(t *testing.T, client *gophercloud.ServiceClient, lb *loadbalancers.LoadBalancer) (*pools.Pool, error) { + poolName := tools.RandomString("TESTACCT-", 8) + + t.Logf("Attempting to create pool %s", poolName) + + createOpts := pools.CreateOpts{ + Name: poolName, + Protocol: pools.ProtocolTCP, + LoadbalancerID: lb.ID, + LBMethod: pools.LBMethodLeastConnections, + } + + pool, err := pools.Create(client, createOpts).Extract() + if err != nil { + return pool, err + } + + t.Logf("Successfully created pool %s", poolName) + + if err := WaitForLoadBalancerState(client, lb.ID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil { + return pool, fmt.Errorf("Timed out waiting for loadbalancer to become active") + } + + return pool, nil +} + +// DeleteListener will delete a specified listener. A fatal error will occur if +// the listener could not be deleted. This works best when used as a deferred +// function. +func DeleteListener(t *testing.T, client *gophercloud.ServiceClient, lbID, listenerID string) { + t.Logf("Attempting to delete listener %s", listenerID) + + if err := listeners.Delete(client, listenerID).ExtractErr(); err != nil { + t.Fatalf("Unable to delete listener: %v", err) + } + + if err := WaitForLoadBalancerState(client, lbID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil { + t.Fatalf("Timed out waiting for loadbalancer to become active") + } + + t.Logf("Successfully deleted listener %s", listenerID) +} + +// DeleteMember will delete a specified member. A fatal error will occur if the +// member could not be deleted. This works best when used as a deferred +// function. +func DeleteMember(t *testing.T, client *gophercloud.ServiceClient, lbID, poolID, memberID string) { + t.Logf("Attempting to delete member %s", memberID) + + if err := pools.DeleteMember(client, poolID, memberID).ExtractErr(); err != nil { + t.Fatalf("Unable to delete member: %s", memberID) + } + + if err := WaitForLoadBalancerState(client, lbID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil { + t.Fatalf("Timed out waiting for loadbalancer to become active") + } + + t.Logf("Successfully deleted member %s", memberID) +} + +// DeleteLoadBalancer will delete a specified loadbalancer. A fatal error will +// occur if the loadbalancer could not be deleted. This works best when used +// as a deferred function. +func DeleteLoadBalancer(t *testing.T, client *gophercloud.ServiceClient, lbID string) { + t.Logf("Attempting to delete loadbalancer %s", lbID) + + if err := loadbalancers.Delete(client, lbID).ExtractErr(); err != nil { + t.Fatalf("Unable to delete loadbalancer: %v", err) + } + + t.Logf("Waiting for loadbalancer %s to delete", lbID) + + if err := WaitForLoadBalancerState(client, lbID, "DELETED", loadbalancerActiveTimeoutSeconds); err != nil { + t.Fatalf("Loadbalancer did not delete in time.") + } + + t.Logf("Successfully deleted loadbalancer %s", lbID) +} + +// DeleteMonitor will delete a specified monitor. A fatal error will occur if +// the monitor could not be deleted. This works best when used as a deferred +// function. +func DeleteMonitor(t *testing.T, client *gophercloud.ServiceClient, lbID, monitorID string) { + t.Logf("Attempting to delete monitor %s", monitorID) + + if err := monitors.Delete(client, monitorID).ExtractErr(); err != nil { + t.Fatalf("Unable to delete monitor: %v", err) + } + + if err := WaitForLoadBalancerState(client, lbID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil { + t.Fatalf("Timed out waiting for loadbalancer to become active") + } + + t.Logf("Successfully deleted monitor %s", monitorID) +} + +// DeletePool will delete a specified pool. A fatal error will occur if the +// pool could not be deleted. This works best when used as a deferred function. +func DeletePool(t *testing.T, client *gophercloud.ServiceClient, lbID, poolID string) { + t.Logf("Attempting to delete pool %s", poolID) + + if err := pools.Delete(client, poolID).ExtractErr(); err != nil { + t.Fatalf("Unable to delete pool: %v", err) + } + + if err := WaitForLoadBalancerState(client, lbID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil { + t.Fatalf("Timed out waiting for loadbalancer to become active") + } + + t.Logf("Successfully deleted pool %s", poolID) +} + +// WaitForLoadBalancerState will wait until a loadbalancer reaches a given state. +func WaitForLoadBalancerState(client *gophercloud.ServiceClient, lbID, status string, secs int) error { + return gophercloud.WaitFor(secs, func() (bool, error) { + current, err := loadbalancers.Get(client, lbID).Extract() + if err != nil { + if httpStatus, ok := err.(gophercloud.ErrDefault404); ok { + if httpStatus.Actual == 404 { + if status == "DELETED" { + return true, nil + } + } + } + return false, err + } + + if current.ProvisioningStatus == status { + return true, nil + } + + return false, nil + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas_v2/listeners_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas_v2/listeners_test.go new file mode 100644 index 0000000000..2d2dd03695 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas_v2/listeners_test.go @@ -0,0 +1,32 @@ +// +build acceptance networking lbaas_v2 listeners + +package lbaas_v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners" +) + +func TestListenersList(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + allPages, err := listeners.List(client, nil).AllPages() + if err != nil { + t.Fatalf("Unable to list listeners: %v", err) + } + + allListeners, err := listeners.ExtractListeners(allPages) + if err != nil { + t.Fatalf("Unable to extract listeners: %v", err) + } + + for _, listener := range allListeners { + tools.PrintResource(t, listener) + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas_v2/loadbalancers_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas_v2/loadbalancers_test.go new file mode 100644 index 0000000000..650eb2cc49 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas_v2/loadbalancers_test.go @@ -0,0 +1,178 @@ +// +build acceptance networking lbaas_v2 loadbalancers + +package lbaas_v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + networking "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools" +) + +func TestLoadbalancersList(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + allPages, err := loadbalancers.List(client, nil).AllPages() + if err != nil { + t.Fatalf("Unable to list loadbalancers: %v", err) + } + + allLoadbalancers, err := loadbalancers.ExtractLoadBalancers(allPages) + if err != nil { + t.Fatalf("Unable to extract loadbalancers: %v", err) + } + + for _, lb := range allLoadbalancers { + tools.PrintResource(t, lb) + } +} + +func TestLoadbalancersCRUD(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + network, err := networking.CreateNetwork(t, client) + if err != nil { + t.Fatalf("Unable to create network: %v", err) + } + defer networking.DeleteNetwork(t, client, network.ID) + + subnet, err := networking.CreateSubnet(t, client, network.ID) + if err != nil { + t.Fatalf("Unable to create subnet: %v", err) + } + defer networking.DeleteSubnet(t, client, subnet.ID) + + lb, err := CreateLoadBalancer(t, client, subnet.ID) + if err != nil { + t.Fatalf("Unable to create loadbalancer: %v", err) + } + defer DeleteLoadBalancer(t, client, lb.ID) + + newLB, err := loadbalancers.Get(client, lb.ID).Extract() + if err != nil { + t.Fatalf("Unable to get loadbalancer: %v", err) + } + + tools.PrintResource(t, newLB) + + // Because of the time it takes to create a loadbalancer, + // this test will include some other resources. + + // Listener + listener, err := CreateListener(t, client, lb) + if err != nil { + t.Fatalf("Unable to create listener: %v", err) + } + defer DeleteListener(t, client, lb.ID, listener.ID) + + updateListenerOpts := listeners.UpdateOpts{ + Description: "Some listener description", + } + _, err = listeners.Update(client, listener.ID, updateListenerOpts).Extract() + if err != nil { + t.Fatalf("Unable to update listener") + } + + if err := WaitForLoadBalancerState(client, lb.ID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil { + t.Fatalf("Timed out waiting for loadbalancer to become active") + } + + newListener, err := listeners.Get(client, listener.ID).Extract() + if err != nil { + t.Fatalf("Unable to get listener") + } + + tools.PrintResource(t, newListener) + + // Pool + pool, err := CreatePool(t, client, lb) + if err != nil { + t.Fatalf("Unable to create pool: %v", err) + } + defer DeletePool(t, client, lb.ID, pool.ID) + + updatePoolOpts := pools.UpdateOpts{ + Description: "Some pool description", + } + _, err = pools.Update(client, pool.ID, updatePoolOpts).Extract() + if err != nil { + t.Fatalf("Unable to update pool") + } + + if err := WaitForLoadBalancerState(client, lb.ID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil { + t.Fatalf("Timed out waiting for loadbalancer to become active") + } + + newPool, err := pools.Get(client, pool.ID).Extract() + if err != nil { + t.Fatalf("Unable to get pool") + } + + tools.PrintResource(t, newPool) + + // Member + member, err := CreateMember(t, client, lb, newPool, subnet.ID, subnet.CIDR) + if err != nil { + t.Fatalf("Unable to create member: %v", err) + } + defer DeleteMember(t, client, lb.ID, pool.ID, member.ID) + + newWeight := tools.RandomInt(11, 100) + updateMemberOpts := pools.UpdateMemberOpts{ + Weight: newWeight, + } + _, err = pools.UpdateMember(client, pool.ID, member.ID, updateMemberOpts).Extract() + if err != nil { + t.Fatalf("Unable to update pool") + } + + if err := WaitForLoadBalancerState(client, lb.ID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil { + t.Fatalf("Timed out waiting for loadbalancer to become active") + } + + newMember, err := pools.GetMember(client, pool.ID, member.ID).Extract() + if err != nil { + t.Fatalf("Unable to get member") + } + + tools.PrintResource(t, newMember) + + // Monitor + monitor, err := CreateMonitor(t, client, lb, newPool) + if err != nil { + t.Fatalf("Unable to create monitor: %v", err) + } + defer DeleteMonitor(t, client, lb.ID, monitor.ID) + + newDelay := tools.RandomInt(20, 30) + updateMonitorOpts := monitors.UpdateOpts{ + Delay: newDelay, + } + _, err = monitors.Update(client, monitor.ID, updateMonitorOpts).Extract() + if err != nil { + t.Fatalf("Unable to update monitor") + } + + if err := WaitForLoadBalancerState(client, lb.ID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil { + t.Fatalf("Timed out waiting for loadbalancer to become active") + } + + newMonitor, err := monitors.Get(client, monitor.ID).Extract() + if err != nil { + t.Fatalf("Unable to get monitor") + } + + tools.PrintResource(t, newMonitor) + +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas_v2/monitors_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas_v2/monitors_test.go new file mode 100644 index 0000000000..b312370722 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas_v2/monitors_test.go @@ -0,0 +1,32 @@ +// +build acceptance networking lbaas_v2 monitors + +package lbaas_v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors" +) + +func TestMonitorsList(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + allPages, err := monitors.List(client, nil).AllPages() + if err != nil { + t.Fatalf("Unable to list monitors: %v", err) + } + + allMonitors, err := monitors.ExtractMonitors(allPages) + if err != nil { + t.Fatalf("Unable to extract monitors: %v", err) + } + + for _, monitor := range allMonitors { + tools.PrintResource(t, monitor) + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas_v2/pkg.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas_v2/pkg.go new file mode 100644 index 0000000000..24b7482a56 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas_v2/pkg.go @@ -0,0 +1 @@ +package lbaas_v2 diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas_v2/pools_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas_v2/pools_test.go new file mode 100644 index 0000000000..b4f55a0f63 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas_v2/pools_test.go @@ -0,0 +1,32 @@ +// +build acceptance networking lbaas_v2 pools + +package lbaas_v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools" +) + +func TestPoolsList(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + allPages, err := pools.List(client, nil).AllPages() + if err != nil { + t.Fatalf("Unable to list pools: %v", err) + } + + allPools, err := pools.ExtractPools(allPages) + if err != nil { + t.Fatalf("Unable to extract pools: %v", err) + } + + for _, pool := range allPools { + tools.PrintResource(t, pool) + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/pkg.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/pkg.go new file mode 100644 index 0000000000..aeec0fa756 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/pkg.go @@ -0,0 +1 @@ +package extensions diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/portsbinding/pkg.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/portsbinding/pkg.go new file mode 100644 index 0000000000..5dae1b1660 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/portsbinding/pkg.go @@ -0,0 +1 @@ +package portsbinding diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/portsbinding/portsbinding.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/portsbinding/portsbinding.go new file mode 100644 index 0000000000..a6d75f3814 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/portsbinding/portsbinding.go @@ -0,0 +1,38 @@ +package portsbinding + +import ( + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/portsbinding" + "github.com/gophercloud/gophercloud/openstack/networking/v2/ports" +) + +// CreatePortsbinding will create a port on the specified subnet. An error will be +// returned if the port could not be created. +func CreatePortsbinding(t *testing.T, client *gophercloud.ServiceClient, networkID, subnetID, hostID string) (*portsbinding.Port, error) { + portName := tools.RandomString("TESTACC-", 8) + iFalse := false + + t.Logf("Attempting to create port: %s", portName) + + createOpts := portsbinding.CreateOpts{ + CreateOptsBuilder: ports.CreateOpts{ + NetworkID: networkID, + Name: portName, + AdminStateUp: &iFalse, + FixedIPs: []ports.IP{ports.IP{SubnetID: subnetID}}, + }, + HostID: hostID, + } + + port, err := portsbinding.Create(client, createOpts).Extract() + if err != nil { + return port, err + } + + t.Logf("Successfully created port: %s", portName) + + return port, nil +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/portsbinding/portsbinding_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/portsbinding/portsbinding_test.go new file mode 100644 index 0000000000..803f62a3dd --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/portsbinding/portsbinding_test.go @@ -0,0 +1,58 @@ +// +build acceptance networking + +package portsbinding + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + networking "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/portsbinding" + "github.com/gophercloud/gophercloud/openstack/networking/v2/ports" +) + +func TestPortsbindingCRUD(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + // Create Network + network, err := networking.CreateNetwork(t, client) + if err != nil { + t.Fatalf("Unable to create network: %v", err) + } + defer networking.DeleteNetwork(t, client, network.ID) + + // Create Subnet + subnet, err := networking.CreateSubnet(t, client, network.ID) + if err != nil { + t.Fatalf("Unable to create subnet: %v", err) + } + defer networking.DeleteSubnet(t, client, subnet.ID) + + // Define a host + hostID := "localhost" + + // Create port + port, err := CreatePortsbinding(t, client, network.ID, subnet.ID, hostID) + if err != nil { + t.Fatalf("Unable to create port: %v", err) + } + defer networking.DeletePort(t, client, port.ID) + + tools.PrintResource(t, port) + + // Update port + newPortName := tools.RandomString("TESTACC-", 8) + updateOpts := ports.UpdateOpts{ + Name: newPortName, + } + newPort, err := portsbinding.Update(client, port.ID, updateOpts).Extract() + if err != nil { + t.Fatalf("Could not update port: %v", err) + } + + tools.PrintResource(t, newPort) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/provider_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/provider_test.go new file mode 100644 index 0000000000..b0d5846ddc --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/provider_test.go @@ -0,0 +1,35 @@ +// +build acceptance networking provider + +package extensions + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + networking "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/provider" + "github.com/gophercloud/gophercloud/openstack/networking/v2/networks" +) + +func TestNetworksProviderCRUD(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + // Create a network + network, err := networking.CreateNetwork(t, client) + if err != nil { + t.Fatalf("Unable to create network: %v", err) + } + defer networking.DeleteNetwork(t, client, network.ID) + + getResult := networks.Get(client, network.ID) + newNetwork, err := provider.ExtractGet(getResult) + if err != nil { + t.Fatalf("Unable to extract network: %v", err) + } + + tools.PrintResource(t, newNetwork) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/security_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/security_test.go new file mode 100644 index 0000000000..3810a42014 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/security_test.go @@ -0,0 +1,105 @@ +// +build acceptance networking security + +package extensions + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + networking "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups" +) + +func TestSecurityGroupsList(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + listOpts := groups.ListOpts{} + allPages, err := groups.List(client, listOpts).AllPages() + if err != nil { + t.Fatalf("Unable to list groups: %v", err) + } + + allGroups, err := groups.ExtractGroups(allPages) + if err != nil { + t.Fatalf("Unable to extract groups: %v", err) + } + + for _, group := range allGroups { + tools.PrintResource(t, group) + } +} + +func TestSecurityGroupsCreateUpdateDelete(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + group, err := CreateSecurityGroup(t, client) + if err != nil { + t.Fatalf("Unable to create security group: %v", err) + } + defer DeleteSecurityGroup(t, client, group.ID) + + rule, err := CreateSecurityGroupRule(t, client, group.ID) + if err != nil { + t.Fatalf("Unable to create security group rule: %v", err) + } + defer DeleteSecurityGroupRule(t, client, rule.ID) + + tools.PrintResource(t, group) + + updateOpts := groups.UpdateOpts{ + Description: "A security group", + } + + newGroup, err := groups.Update(client, group.ID, updateOpts).Extract() + if err != nil { + t.Fatalf("Unable to update security group: %v", err) + } + + tools.PrintResource(t, newGroup) +} + +func TestSecurityGroupsPort(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + network, err := networking.CreateNetwork(t, client) + if err != nil { + t.Fatalf("Unable to create network: %v", err) + } + defer networking.DeleteNetwork(t, client, network.ID) + + subnet, err := networking.CreateSubnet(t, client, network.ID) + if err != nil { + t.Fatalf("Unable to create subnet: %v", err) + } + defer networking.DeleteSubnet(t, client, subnet.ID) + + group, err := CreateSecurityGroup(t, client) + if err != nil { + t.Fatalf("Unable to create security group: %v", err) + } + defer DeleteSecurityGroup(t, client, group.ID) + + rule, err := CreateSecurityGroupRule(t, client, group.ID) + if err != nil { + t.Fatalf("Unable to create security group rule: %v", err) + } + defer DeleteSecurityGroupRule(t, client, rule.ID) + + port, err := CreatePortWithSecurityGroup(t, client, network.ID, subnet.ID, group.ID) + if err != nil { + t.Fatalf("Unable to create port: %v", err) + } + defer networking.DeletePort(t, client, port.ID) + + tools.PrintResource(t, port) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/networking.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/networking.go new file mode 100644 index 0000000000..c5e7ca2315 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/networking.go @@ -0,0 +1,211 @@ +package v2 + +import ( + "fmt" + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/networking/v2/networks" + "github.com/gophercloud/gophercloud/openstack/networking/v2/ports" + "github.com/gophercloud/gophercloud/openstack/networking/v2/subnets" +) + +// CreateNetwork will create basic network. An error will be returned if the +// network could not be created. +func CreateNetwork(t *testing.T, client *gophercloud.ServiceClient) (*networks.Network, error) { + networkName := tools.RandomString("TESTACC-", 8) + createOpts := networks.CreateOpts{ + Name: networkName, + AdminStateUp: gophercloud.Enabled, + } + + t.Logf("Attempting to create network: %s", networkName) + + network, err := networks.Create(client, createOpts).Extract() + if err != nil { + return network, err + } + + t.Logf("Successfully created network.") + return network, nil +} + +// CreatePort will create a port on the specified subnet. An error will be +// returned if the port could not be created. +func CreatePort(t *testing.T, client *gophercloud.ServiceClient, networkID, subnetID string) (*ports.Port, error) { + portName := tools.RandomString("TESTACC-", 8) + + t.Logf("Attempting to create port: %s", portName) + + createOpts := ports.CreateOpts{ + NetworkID: networkID, + Name: portName, + AdminStateUp: gophercloud.Enabled, + FixedIPs: []ports.IP{ports.IP{SubnetID: subnetID}}, + } + + port, err := ports.Create(client, createOpts).Extract() + if err != nil { + return port, err + } + + if err := WaitForPortToCreate(client, port.ID, 60); err != nil { + return port, err + } + + newPort, err := ports.Get(client, port.ID).Extract() + if err != nil { + return newPort, err + } + + t.Logf("Successfully created port: %s", portName) + + return newPort, nil +} + +// CreateSubnet will create a subnet on the specified Network ID. An error +// will be returned if the subnet could not be created. +func CreateSubnet(t *testing.T, client *gophercloud.ServiceClient, networkID string) (*subnets.Subnet, error) { + subnetName := tools.RandomString("TESTACC-", 8) + subnetOctet := tools.RandomInt(1, 250) + subnetCIDR := fmt.Sprintf("192.168.%d.0/24", subnetOctet) + subnetGateway := fmt.Sprintf("192.168.%d.1", subnetOctet) + createOpts := subnets.CreateOpts{ + NetworkID: networkID, + CIDR: subnetCIDR, + IPVersion: 4, + Name: subnetName, + EnableDHCP: gophercloud.Disabled, + GatewayIP: &subnetGateway, + } + + t.Logf("Attempting to create subnet: %s", subnetName) + + subnet, err := subnets.Create(client, createOpts).Extract() + if err != nil { + return subnet, err + } + + t.Logf("Successfully created subnet.") + return subnet, nil +} + +// CreateSubnetWithDefaultGateway will create a subnet on the specified Network +// ID and have Neutron set the gateway by default An error will be returned if +// the subnet could not be created. +func CreateSubnetWithDefaultGateway(t *testing.T, client *gophercloud.ServiceClient, networkID string) (*subnets.Subnet, error) { + subnetName := tools.RandomString("TESTACC-", 8) + subnetOctet := tools.RandomInt(1, 250) + subnetCIDR := fmt.Sprintf("192.168.%d.0/24", subnetOctet) + createOpts := subnets.CreateOpts{ + NetworkID: networkID, + CIDR: subnetCIDR, + IPVersion: 4, + Name: subnetName, + EnableDHCP: gophercloud.Disabled, + } + + t.Logf("Attempting to create subnet: %s", subnetName) + + subnet, err := subnets.Create(client, createOpts).Extract() + if err != nil { + return subnet, err + } + + t.Logf("Successfully created subnet.") + return subnet, nil +} + +// CreateSubnetWithNoGateway will create a subnet with no gateway on the +// specified Network ID. An error will be returned if the subnet could not be +// created. +func CreateSubnetWithNoGateway(t *testing.T, client *gophercloud.ServiceClient, networkID string) (*subnets.Subnet, error) { + var noGateway = "" + subnetName := tools.RandomString("TESTACC-", 8) + subnetOctet := tools.RandomInt(1, 250) + subnetCIDR := fmt.Sprintf("192.168.%d.0/24", subnetOctet) + dhcpStart := fmt.Sprintf("192.168.%d.10", subnetOctet) + dhcpEnd := fmt.Sprintf("192.168.%d.200", subnetOctet) + createOpts := subnets.CreateOpts{ + NetworkID: networkID, + CIDR: subnetCIDR, + IPVersion: 4, + Name: subnetName, + EnableDHCP: gophercloud.Disabled, + GatewayIP: &noGateway, + AllocationPools: []subnets.AllocationPool{ + { + Start: dhcpStart, + End: dhcpEnd, + }, + }, + } + + t.Logf("Attempting to create subnet: %s", subnetName) + + subnet, err := subnets.Create(client, createOpts).Extract() + if err != nil { + return subnet, err + } + + t.Logf("Successfully created subnet.") + return subnet, nil +} + +// DeleteNetwork will delete a network with a specified ID. A fatal error will +// occur if the delete was not successful. This works best when used as a +// deferred function. +func DeleteNetwork(t *testing.T, client *gophercloud.ServiceClient, networkID string) { + t.Logf("Attempting to delete network: %s", networkID) + + err := networks.Delete(client, networkID).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete network %s: %v", networkID, err) + } + + t.Logf("Deleted network: %s", networkID) +} + +// DeletePort will delete a port with a specified ID. A fatal error will +// occur if the delete was not successful. This works best when used as a +// deferred function. +func DeletePort(t *testing.T, client *gophercloud.ServiceClient, portID string) { + t.Logf("Attempting to delete port: %s", portID) + + err := ports.Delete(client, portID).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete port %s: %v", portID, err) + } + + t.Logf("Deleted port: %s", portID) +} + +// DeleteSubnet will delete a subnet with a specified ID. A fatal error will +// occur if the delete was not successful. This works best when used as a +// deferred function. +func DeleteSubnet(t *testing.T, client *gophercloud.ServiceClient, subnetID string) { + t.Logf("Attempting to delete subnet: %s", subnetID) + + err := subnets.Delete(client, subnetID).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete subnet %s: %v", subnetID, err) + } + + t.Logf("Deleted subnet: %s", subnetID) +} + +func WaitForPortToCreate(client *gophercloud.ServiceClient, portID string, secs int) error { + return gophercloud.WaitFor(secs, func() (bool, error) { + p, err := ports.Get(client, portID).Extract() + if err != nil { + return false, err + } + + if p.Status == "ACTIVE" || p.Status == "DOWN" { + return true, nil + } + + return false, nil + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/networks_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/networks_test.go new file mode 100644 index 0000000000..66f42f85a3 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/networks_test.go @@ -0,0 +1,65 @@ +// +build acceptance networking + +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/networking/v2/networks" +) + +func TestNetworksList(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + allPages, err := networks.List(client, nil).AllPages() + if err != nil { + t.Fatalf("Unable to list networks: %v", err) + } + + allNetworks, err := networks.ExtractNetworks(allPages) + if err != nil { + t.Fatalf("Unable to extract networks: %v", err) + } + + for _, network := range allNetworks { + tools.PrintResource(t, network) + } +} + +func TestNetworksCRUD(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + // Create a network + network, err := CreateNetwork(t, client) + if err != nil { + t.Fatalf("Unable to create network: %v", err) + } + defer DeleteNetwork(t, client, network.ID) + + tools.PrintResource(t, network) + + newName := tools.RandomString("TESTACC-", 8) + updateOpts := &networks.UpdateOpts{ + Name: newName, + } + + _, err = networks.Update(client, network.ID, updateOpts).Extract() + if err != nil { + t.Fatalf("Unable to update network: %v", err) + } + + newNetwork, err := networks.Get(client, network.ID).Extract() + if err != nil { + t.Fatalf("Unable to retrieve network: %v", err) + } + + tools.PrintResource(t, newNetwork) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/pkg.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/pkg.go new file mode 100644 index 0000000000..5ec3cc8e83 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/pkg.go @@ -0,0 +1 @@ +package v2 diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/ports_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/ports_test.go new file mode 100644 index 0000000000..394e90fca2 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/ports_test.go @@ -0,0 +1,192 @@ +// +build acceptance networking + +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + extensions "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/networking/v2/ports" +) + +func TestPortsList(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + allPages, err := ports.List(client, nil).AllPages() + if err != nil { + t.Fatalf("Unable to list ports: %v", err) + } + + allPorts, err := ports.ExtractPorts(allPages) + if err != nil { + t.Fatalf("Unable to extract ports: %v", err) + } + + for _, port := range allPorts { + tools.PrintResource(t, port) + } +} + +func TestPortsCRUD(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + // Create Network + network, err := CreateNetwork(t, client) + if err != nil { + t.Fatalf("Unable to create network: %v", err) + } + defer DeleteNetwork(t, client, network.ID) + + // Create Subnet + subnet, err := CreateSubnet(t, client, network.ID) + if err != nil { + t.Fatalf("Unable to create subnet: %v", err) + } + defer DeleteSubnet(t, client, subnet.ID) + + // Create port + port, err := CreatePort(t, client, network.ID, subnet.ID) + if err != nil { + t.Fatalf("Unable to create port: %v", err) + } + defer DeletePort(t, client, port.ID) + + tools.PrintResource(t, port) + + // Update port + newPortName := tools.RandomString("TESTACC-", 8) + updateOpts := ports.UpdateOpts{ + Name: newPortName, + } + newPort, err := ports.Update(client, port.ID, updateOpts).Extract() + if err != nil { + t.Fatalf("Could not update port: %v", err) + } + + tools.PrintResource(t, newPort) +} + +func TestPortsRemoveSecurityGroups(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + // Create Network + network, err := CreateNetwork(t, client) + if err != nil { + t.Fatalf("Unable to create network: %v", err) + } + defer DeleteNetwork(t, client, network.ID) + + // Create Subnet + subnet, err := CreateSubnet(t, client, network.ID) + if err != nil { + t.Fatalf("Unable to create subnet: %v", err) + } + defer DeleteSubnet(t, client, subnet.ID) + + // Create port + port, err := CreatePort(t, client, network.ID, subnet.ID) + if err != nil { + t.Fatalf("Unable to create port: %v", err) + } + defer DeletePort(t, client, port.ID) + + tools.PrintResource(t, port) + + // Create a Security Group + group, err := extensions.CreateSecurityGroup(t, client) + if err != nil { + t.Fatalf("Unable to create security group: %v", err) + } + defer extensions.DeleteSecurityGroup(t, client, group.ID) + + // Add the group to the port + updateOpts := ports.UpdateOpts{ + SecurityGroups: []string{group.ID}, + } + newPort, err := ports.Update(client, port.ID, updateOpts).Extract() + if err != nil { + t.Fatalf("Could not update port: %v", err) + } + + // Remove the group + updateOpts = ports.UpdateOpts{ + SecurityGroups: []string{}, + } + newPort, err = ports.Update(client, port.ID, updateOpts).Extract() + if err != nil { + t.Fatalf("Could not update port: %v", err) + } + + tools.PrintResource(t, newPort) + + if len(newPort.SecurityGroups) > 0 { + t.Fatalf("Unable to remove security group from port") + } +} + +func TestPortsRemoveAddressPair(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + // Create Network + network, err := CreateNetwork(t, client) + if err != nil { + t.Fatalf("Unable to create network: %v", err) + } + defer DeleteNetwork(t, client, network.ID) + + // Create Subnet + subnet, err := CreateSubnet(t, client, network.ID) + if err != nil { + t.Fatalf("Unable to create subnet: %v", err) + } + defer DeleteSubnet(t, client, subnet.ID) + + // Create port + port, err := CreatePort(t, client, network.ID, subnet.ID) + if err != nil { + t.Fatalf("Unable to create port: %v", err) + } + defer DeletePort(t, client, port.ID) + + tools.PrintResource(t, port) + + // Add an address pair to the port + updateOpts := ports.UpdateOpts{ + AllowedAddressPairs: []ports.AddressPair{ + ports.AddressPair{IPAddress: "192.168.255.10", MACAddress: "aa:bb:cc:dd:ee:ff"}, + }, + } + newPort, err := ports.Update(client, port.ID, updateOpts).Extract() + if err != nil { + t.Fatalf("Could not update port: %v", err) + } + + // Remove the address pair + updateOpts = ports.UpdateOpts{ + AllowedAddressPairs: []ports.AddressPair{}, + } + newPort, err = ports.Update(client, port.ID, updateOpts).Extract() + if err != nil { + t.Fatalf("Could not update port: %v", err) + } + + tools.PrintResource(t, newPort) + + if len(newPort.AllowedAddressPairs) > 0 { + t.Fatalf("Unable to remove the address pair") + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/subnets_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/subnets_test.go new file mode 100644 index 0000000000..fd50a1f84b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/subnets_test.go @@ -0,0 +1,158 @@ +// +build acceptance networking + +package v2 + +import ( + "fmt" + "strings" + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/networking/v2/subnets" +) + +func TestSubnetsList(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + allPages, err := subnets.List(client, nil).AllPages() + if err != nil { + t.Fatalf("Unable to list subnets: %v", err) + } + + allSubnets, err := subnets.ExtractSubnets(allPages) + if err != nil { + t.Fatalf("Unable to extract subnets: %v", err) + } + + for _, subnet := range allSubnets { + tools.PrintResource(t, subnet) + } +} + +func TestSubnetCRUD(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + // Create Network + network, err := CreateNetwork(t, client) + if err != nil { + t.Fatalf("Unable to create network: %v", err) + } + defer DeleteNetwork(t, client, network.ID) + + // Create Subnet + subnet, err := CreateSubnet(t, client, network.ID) + if err != nil { + t.Fatalf("Unable to create subnet: %v", err) + } + defer DeleteSubnet(t, client, subnet.ID) + + tools.PrintResource(t, subnet) + + // Update Subnet + newSubnetName := tools.RandomString("TESTACC-", 8) + updateOpts := subnets.UpdateOpts{ + Name: newSubnetName, + } + _, err = subnets.Update(client, subnet.ID, updateOpts).Extract() + if err != nil { + t.Fatalf("Unable to update subnet: %v", err) + } + + // Get subnet + newSubnet, err := subnets.Get(client, subnet.ID).Extract() + if err != nil { + t.Fatalf("Unable to get subnet: %v", err) + } + + tools.PrintResource(t, newSubnet) +} + +func TestSubnetsDefaultGateway(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + // Create Network + network, err := CreateNetwork(t, client) + if err != nil { + t.Fatalf("Unable to create network: %v", err) + } + defer DeleteNetwork(t, client, network.ID) + + // Create Subnet + subnet, err := CreateSubnetWithDefaultGateway(t, client, network.ID) + if err != nil { + t.Fatalf("Unable to create subnet: %v", err) + } + defer DeleteSubnet(t, client, subnet.ID) + + tools.PrintResource(t, subnet) + + if subnet.GatewayIP == "" { + t.Fatalf("A default gateway was not created.") + } + + var noGateway = "" + updateOpts := subnets.UpdateOpts{ + GatewayIP: &noGateway, + } + + newSubnet, err := subnets.Update(client, subnet.ID, updateOpts).Extract() + if err != nil { + t.Fatalf("Unable to update subnet") + } + + if newSubnet.GatewayIP != "" { + t.Fatalf("Gateway was not updated correctly") + } +} + +func TestSubnetsNoGateway(t *testing.T) { + client, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + // Create Network + network, err := CreateNetwork(t, client) + if err != nil { + t.Fatalf("Unable to create network: %v", err) + } + defer DeleteNetwork(t, client, network.ID) + + // Create Subnet + subnet, err := CreateSubnetWithNoGateway(t, client, network.ID) + if err != nil { + t.Fatalf("Unable to create subnet: %v", err) + } + defer DeleteSubnet(t, client, subnet.ID) + + tools.PrintResource(t, subnet) + + if subnet.GatewayIP != "" { + t.Fatalf("A gateway exists when it shouldn't.") + } + + subnetParts := strings.Split(subnet.CIDR, ".") + newGateway := fmt.Sprintf("%s.%s.%s.1", subnetParts[0], subnetParts[1], subnetParts[2]) + updateOpts := subnets.UpdateOpts{ + GatewayIP: &newGateway, + } + + newSubnet, err := subnets.Update(client, subnet.ID, updateOpts).Extract() + if err != nil { + t.Fatalf("Unable to update subnet") + } + + if newSubnet.GatewayIP == "" { + t.Fatalf("Gateway was not updated correctly") + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/objectstorage/v1/accounts_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/objectstorage/v1/accounts_test.go new file mode 100644 index 0000000000..5a29235fb6 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/objectstorage/v1/accounts_test.go @@ -0,0 +1,50 @@ +// +build acceptance + +package v1 + +import ( + "strings" + "testing" + + "github.com/gophercloud/gophercloud/openstack/objectstorage/v1/accounts" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestAccounts(t *testing.T) { + // Create a provider client for making the HTTP requests. + // See common.go in this directory for more information. + client := newClient(t) + + // Update an account's metadata. + updateres := accounts.Update(client, accounts.UpdateOpts{Metadata: metadata}) + t.Logf("Update Account Response: %+v\n", updateres) + updateHeaders, err := updateres.Extract() + th.AssertNoErr(t, err) + t.Logf("Update Account Response Headers: %+v\n", updateHeaders) + + // Defer the deletion of the metadata set above. + defer func() { + tempMap := make(map[string]string) + for k := range metadata { + tempMap[k] = "" + } + updateres = accounts.Update(client, accounts.UpdateOpts{Metadata: tempMap}) + th.AssertNoErr(t, updateres.Err) + }() + + // Extract the custom metadata from the 'Get' response. + res := accounts.Get(client, nil) + + h, err := res.Extract() + th.AssertNoErr(t, err) + t.Logf("Get Account Response Headers: %+v\n", h) + + am, err := res.ExtractMetadata() + th.AssertNoErr(t, err) + for k := range metadata { + if am[k] != metadata[strings.Title(k)] { + t.Errorf("Expected custom metadata with key: %s", k) + return + } + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/objectstorage/v1/common.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/objectstorage/v1/common.go new file mode 100644 index 0000000000..1114ed57f9 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/objectstorage/v1/common.go @@ -0,0 +1,28 @@ +// +build acceptance + +package v1 + +import ( + "os" + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack" + th "github.com/gophercloud/gophercloud/testhelper" +) + +var metadata = map[string]string{"gopher": "cloud"} + +func newClient(t *testing.T) *gophercloud.ServiceClient { + ao, err := openstack.AuthOptionsFromEnv() + th.AssertNoErr(t, err) + + client, err := openstack.AuthenticatedClient(ao) + th.AssertNoErr(t, err) + + c, err := openstack.NewObjectStorageV1(client, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) + th.AssertNoErr(t, err) + return c +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/objectstorage/v1/containers_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/objectstorage/v1/containers_test.go new file mode 100644 index 0000000000..056b2a9980 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/objectstorage/v1/containers_test.go @@ -0,0 +1,137 @@ +// +build acceptance + +package v1 + +import ( + "strings" + "testing" + + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/objectstorage/v1/containers" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" +) + +// numContainers is the number of containers to create for testing. +var numContainers = 2 + +func TestContainers(t *testing.T) { + // Create a new client to execute the HTTP requests. See common.go for newClient body. + client := newClient(t) + + // Create a slice of random container names. + cNames := make([]string, numContainers) + for i := 0; i < numContainers; i++ { + cNames[i] = tools.RandomString("gophercloud-test-container-", 8) + } + + // Create numContainers containers. + for i := 0; i < len(cNames); i++ { + res := containers.Create(client, cNames[i], nil) + th.AssertNoErr(t, res.Err) + } + // Delete the numContainers containers after function completion. + defer func() { + for i := 0; i < len(cNames); i++ { + res := containers.Delete(client, cNames[i]) + th.AssertNoErr(t, res.Err) + } + }() + + // List the numContainer names that were just created. To just list those, + // the 'prefix' parameter is used. + err := containers.List(client, &containers.ListOpts{Full: true, Prefix: "gophercloud-test-container-"}).EachPage(func(page pagination.Page) (bool, error) { + containerList, err := containers.ExtractInfo(page) + th.AssertNoErr(t, err) + + for _, n := range containerList { + t.Logf("Container: Name [%s] Count [%d] Bytes [%d]", + n.Name, n.Count, n.Bytes) + } + + return true, nil + }) + th.AssertNoErr(t, err) + + // List the info for the numContainer containers that were created. + err = containers.List(client, &containers.ListOpts{Full: false, Prefix: "gophercloud-test-container-"}).EachPage(func(page pagination.Page) (bool, error) { + containerList, err := containers.ExtractNames(page) + th.AssertNoErr(t, err) + for _, n := range containerList { + t.Logf("Container: Name [%s]", n) + } + + return true, nil + }) + th.AssertNoErr(t, err) + + // Update one of the numContainer container metadata. + updateres := containers.Update(client, cNames[0], &containers.UpdateOpts{Metadata: metadata}) + th.AssertNoErr(t, updateres.Err) + // After the tests are done, delete the metadata that was set. + defer func() { + tempMap := make(map[string]string) + for k := range metadata { + tempMap[k] = "" + } + res := containers.Update(client, cNames[0], &containers.UpdateOpts{Metadata: tempMap}) + th.AssertNoErr(t, res.Err) + }() + + // Retrieve a container's metadata. + cm, err := containers.Get(client, cNames[0]).ExtractMetadata() + th.AssertNoErr(t, err) + for k := range metadata { + if cm[k] != metadata[strings.Title(k)] { + t.Errorf("Expected custom metadata with key: %s", k) + } + } +} + +func TestListAllContainers(t *testing.T) { + // Create a new client to execute the HTTP requests. See common.go for newClient body. + client := newClient(t) + + numContainers := 20 + + // Create a slice of random container names. + cNames := make([]string, numContainers) + for i := 0; i < numContainers; i++ { + cNames[i] = tools.RandomString("gophercloud-test-container-", 8) + } + + // Create numContainers containers. + for i := 0; i < len(cNames); i++ { + res := containers.Create(client, cNames[i], nil) + th.AssertNoErr(t, res.Err) + } + // Delete the numContainers containers after function completion. + defer func() { + for i := 0; i < len(cNames); i++ { + res := containers.Delete(client, cNames[i]) + th.AssertNoErr(t, res.Err) + } + }() + + // List all the numContainer names that were just created. To just list those, + // the 'prefix' parameter is used. + allPages, err := containers.List(client, &containers.ListOpts{Full: true, Limit: 5, Prefix: "gophercloud-test-container-"}).AllPages() + th.AssertNoErr(t, err) + containerInfoList, err := containers.ExtractInfo(allPages) + th.AssertNoErr(t, err) + for _, n := range containerInfoList { + t.Logf("Container: Name [%s] Count [%d] Bytes [%d]", + n.Name, n.Count, n.Bytes) + } + th.AssertEquals(t, numContainers, len(containerInfoList)) + + // List the info for all the numContainer containers that were created. + allPages, err = containers.List(client, &containers.ListOpts{Full: false, Limit: 2, Prefix: "gophercloud-test-container-"}).AllPages() + th.AssertNoErr(t, err) + containerNamesList, err := containers.ExtractNames(allPages) + th.AssertNoErr(t, err) + for _, n := range containerNamesList { + t.Logf("Container: Name [%s]", n) + } + th.AssertEquals(t, numContainers, len(containerNamesList)) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/objectstorage/v1/objects_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/objectstorage/v1/objects_test.go new file mode 100644 index 0000000000..3a27738230 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/objectstorage/v1/objects_test.go @@ -0,0 +1,119 @@ +// +build acceptance + +package v1 + +import ( + "bytes" + "strings" + "testing" + + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/objectstorage/v1/containers" + "github.com/gophercloud/gophercloud/openstack/objectstorage/v1/objects" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" +) + +// numObjects is the number of objects to create for testing. +var numObjects = 2 + +func TestObjects(t *testing.T) { + // Create a provider client for executing the HTTP request. + // See common.go for more information. + client := newClient(t) + + // Make a slice of length numObjects to hold the random object names. + oNames := make([]string, numObjects) + for i := 0; i < len(oNames); i++ { + oNames[i] = tools.RandomString("test-object-", 8) + } + + // Create a container to hold the test objects. + cName := tools.RandomString("test-container-", 8) + header, err := containers.Create(client, cName, nil).ExtractHeader() + th.AssertNoErr(t, err) + t.Logf("Create object headers: %+v\n", header) + + // Defer deletion of the container until after testing. + defer func() { + res := containers.Delete(client, cName) + th.AssertNoErr(t, res.Err) + }() + + // Create a slice of buffers to hold the test object content. + oContents := make([]*bytes.Buffer, numObjects) + for i := 0; i < numObjects; i++ { + oContents[i] = bytes.NewBuffer([]byte(tools.RandomString("", 10))) + res := objects.Create(client, cName, oNames[i], oContents[i], nil) + th.AssertNoErr(t, res.Err) + } + // Delete the objects after testing. + defer func() { + for i := 0; i < numObjects; i++ { + res := objects.Delete(client, cName, oNames[i], nil) + th.AssertNoErr(t, res.Err) + } + }() + + ons := make([]string, 0, len(oNames)) + err = objects.List(client, cName, &objects.ListOpts{Full: false, Prefix: "test-object-"}).EachPage(func(page pagination.Page) (bool, error) { + names, err := objects.ExtractNames(page) + th.AssertNoErr(t, err) + ons = append(ons, names...) + + return true, nil + }) + th.AssertNoErr(t, err) + th.AssertEquals(t, len(ons), len(oNames)) + + ois := make([]objects.Object, 0, len(oNames)) + err = objects.List(client, cName, &objects.ListOpts{Full: true, Prefix: "test-object-"}).EachPage(func(page pagination.Page) (bool, error) { + info, err := objects.ExtractInfo(page) + th.AssertNoErr(t, err) + + ois = append(ois, info...) + + return true, nil + }) + th.AssertNoErr(t, err) + th.AssertEquals(t, len(ois), len(oNames)) + + // Copy the contents of one object to another. + copyres := objects.Copy(client, cName, oNames[0], &objects.CopyOpts{Destination: cName + "/" + oNames[1]}) + th.AssertNoErr(t, copyres.Err) + + // Download one of the objects that was created above. + o1Content, err := objects.Download(client, cName, oNames[0], nil).ExtractContent() + th.AssertNoErr(t, err) + + // Download the another object that was create above. + o2Content, err := objects.Download(client, cName, oNames[1], nil).ExtractContent() + th.AssertNoErr(t, err) + + // Compare the two object's contents to test that the copy worked. + th.AssertEquals(t, string(o2Content), string(o1Content)) + + // Update an object's metadata. + updateres := objects.Update(client, cName, oNames[0], &objects.UpdateOpts{Metadata: metadata}) + th.AssertNoErr(t, updateres.Err) + + // Delete the object's metadata after testing. + defer func() { + tempMap := make(map[string]string) + for k := range metadata { + tempMap[k] = "" + } + res := objects.Update(client, cName, oNames[0], &objects.UpdateOpts{Metadata: tempMap}) + th.AssertNoErr(t, res.Err) + }() + + // Retrieve an object's metadata. + om, err := objects.Get(client, cName, oNames[0], nil).ExtractMetadata() + th.AssertNoErr(t, err) + for k := range metadata { + if om[k] != metadata[strings.Title(k)] { + t.Errorf("Expected custom metadata with key: %s", k) + return + } + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/objectstorage/v1/pkg.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/objectstorage/v1/pkg.go new file mode 100644 index 0000000000..b7b1f993d5 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/objectstorage/v1/pkg.go @@ -0,0 +1 @@ +package v1 diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/orchestration/v1/buildinfo_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/orchestration/v1/buildinfo_test.go new file mode 100644 index 0000000000..1b48662916 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/orchestration/v1/buildinfo_test.go @@ -0,0 +1,20 @@ +// +build acceptance + +package v1 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/orchestration/v1/buildinfo" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestBuildInfo(t *testing.T) { + // Create a provider client for making the HTTP requests. + // See common.go in this directory for more information. + client := newClient(t) + + bi, err := buildinfo.Get(client).Extract() + th.AssertNoErr(t, err) + t.Logf("retrieved build info: %+v\n", bi) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/orchestration/v1/common.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/orchestration/v1/common.go new file mode 100644 index 0000000000..4eec2e3156 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/orchestration/v1/common.go @@ -0,0 +1,44 @@ +// +build acceptance + +package v1 + +import ( + "fmt" + "os" + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack" + th "github.com/gophercloud/gophercloud/testhelper" +) + +var template = fmt.Sprintf(` +{ + "heat_template_version": "2013-05-23", + "description": "Simple template to test heat commands", + "parameters": {}, + "resources": { + "hello_world": { + "type":"OS::Nova::Server", + "properties": { + "flavor": "%s", + "image": "%s", + "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n" + } + } + } +}`, os.Getenv("OS_FLAVOR_ID"), os.Getenv("OS_IMAGE_ID")) + +func newClient(t *testing.T) *gophercloud.ServiceClient { + ao, err := openstack.AuthOptionsFromEnv() + th.AssertNoErr(t, err) + + client, err := openstack.AuthenticatedClient(ao) + th.AssertNoErr(t, err) + + c, err := openstack.NewOrchestrationV1(client, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) + th.AssertNoErr(t, err) + return c +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/orchestration/v1/hello-compute.json b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/orchestration/v1/hello-compute.json new file mode 100644 index 0000000000..11cfc80534 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/orchestration/v1/hello-compute.json @@ -0,0 +1,13 @@ +{ + "heat_template_version": "2013-05-23", + "resources": { + "compute_instance": { + "type": "OS::Nova::Server", + "properties": { + "flavor": "m1.small", + "image": "cirros-0.3.2-x86_64-disk", + "name": "Single Compute Instance" + } + } + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/orchestration/v1/pkg.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/orchestration/v1/pkg.go new file mode 100644 index 0000000000..b7b1f993d5 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/orchestration/v1/pkg.go @@ -0,0 +1 @@ +package v1 diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/orchestration/v1/stackevents_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/orchestration/v1/stackevents_test.go new file mode 100644 index 0000000000..4be4bf676a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/orchestration/v1/stackevents_test.go @@ -0,0 +1,68 @@ +// +build acceptance + +package v1 + +import ( + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackevents" + "github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestStackEvents(t *testing.T) { + // Create a provider client for making the HTTP requests. + // See common.go in this directory for more information. + client := newClient(t) + + stackName := "postman_stack_2" + resourceName := "hello_world" + var eventID string + + createOpts := stacks.CreateOpts{ + Name: stackName, + Template: template, + Timeout: 5, + } + stack, err := stacks.Create(client, createOpts).Extract() + th.AssertNoErr(t, err) + t.Logf("Created stack: %+v\n", stack) + defer func() { + err := stacks.Delete(client, stackName, stack.ID).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Deleted stack (%s)", stackName) + }() + err = gophercloud.WaitFor(60, func() (bool, error) { + getStack, err := stacks.Get(client, stackName, stack.ID).Extract() + if err != nil { + return false, err + } + if getStack.Status == "CREATE_COMPLETE" { + return true, nil + } + return false, nil + }) + + err = stackevents.List(client, stackName, stack.ID, nil).EachPage(func(page pagination.Page) (bool, error) { + events, err := stackevents.ExtractEvents(page) + th.AssertNoErr(t, err) + t.Logf("listed events: %+v\n", events) + eventID = events[0].ID + return false, nil + }) + th.AssertNoErr(t, err) + + err = stackevents.ListResourceEvents(client, stackName, stack.ID, resourceName, nil).EachPage(func(page pagination.Page) (bool, error) { + resourceEvents, err := stackevents.ExtractEvents(page) + th.AssertNoErr(t, err) + t.Logf("listed resource events: %+v\n", resourceEvents) + return false, nil + }) + th.AssertNoErr(t, err) + + event, err := stackevents.Get(client, stackName, stack.ID, resourceName, eventID).Extract() + th.AssertNoErr(t, err) + t.Logf("retrieved event: %+v\n", event) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/orchestration/v1/stackresources_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/orchestration/v1/stackresources_test.go new file mode 100644 index 0000000000..50a0f06311 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/orchestration/v1/stackresources_test.go @@ -0,0 +1,62 @@ +// +build acceptance + +package v1 + +import ( + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackresources" + "github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestStackResources(t *testing.T) { + // Create a provider client for making the HTTP requests. + // See common.go in this directory for more information. + client := newClient(t) + + stackName := "postman_stack_2" + + createOpts := stacks.CreateOpts{ + Name: stackName, + Template: template, + Timeout: 5, + } + stack, err := stacks.Create(client, createOpts).Extract() + th.AssertNoErr(t, err) + t.Logf("Created stack: %+v\n", stack) + defer func() { + err := stacks.Delete(client, stackName, stack.ID).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Deleted stack (%s)", stackName) + }() + err = gophercloud.WaitFor(60, func() (bool, error) { + getStack, err := stacks.Get(client, stackName, stack.ID).Extract() + if err != nil { + return false, err + } + if getStack.Status == "CREATE_COMPLETE" { + return true, nil + } + return false, nil + }) + + resourceName := "hello_world" + resource, err := stackresources.Get(client, stackName, stack.ID, resourceName).Extract() + th.AssertNoErr(t, err) + t.Logf("Got stack resource: %+v\n", resource) + + metadata, err := stackresources.Metadata(client, stackName, stack.ID, resourceName).Extract() + th.AssertNoErr(t, err) + t.Logf("Got stack resource metadata: %+v\n", metadata) + + err = stackresources.List(client, stackName, stack.ID, stackresources.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + resources, err := stackresources.ExtractResources(page) + th.AssertNoErr(t, err) + t.Logf("resources: %+v\n", resources) + return false, nil + }) + th.AssertNoErr(t, err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/orchestration/v1/stacks_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/orchestration/v1/stacks_test.go new file mode 100644 index 0000000000..c87cc5d00e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/orchestration/v1/stacks_test.go @@ -0,0 +1,153 @@ +// +build acceptance + +package v1 + +import ( + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestStacks(t *testing.T) { + // Create a provider client for making the HTTP requests. + // See common.go in this directory for more information. + client := newClient(t) + + stackName1 := "gophercloud-test-stack-2" + createOpts := stacks.CreateOpts{ + Name: stackName1, + Template: template, + Timeout: 5, + } + stack, err := stacks.Create(client, createOpts).Extract() + th.AssertNoErr(t, err) + t.Logf("Created stack: %+v\n", stack) + defer func() { + err := stacks.Delete(client, stackName1, stack.ID).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Deleted stack (%s)", stackName1) + }() + err = gophercloud.WaitFor(60, func() (bool, error) { + getStack, err := stacks.Get(client, stackName1, stack.ID).Extract() + if err != nil { + return false, err + } + if getStack.Status == "CREATE_COMPLETE" { + return true, nil + } + return false, nil + }) + + updateOpts := stacks.UpdateOpts{ + Template: template, + Timeout: 20, + } + err = stacks.Update(client, stackName1, stack.ID, updateOpts).ExtractErr() + th.AssertNoErr(t, err) + err = gophercloud.WaitFor(60, func() (bool, error) { + getStack, err := stacks.Get(client, stackName1, stack.ID).Extract() + if err != nil { + return false, err + } + if getStack.Status == "UPDATE_COMPLETE" { + return true, nil + } + return false, nil + }) + + t.Logf("Updated stack") + + err = stacks.List(client, nil).EachPage(func(page pagination.Page) (bool, error) { + stackList, err := stacks.ExtractStacks(page) + th.AssertNoErr(t, err) + + t.Logf("Got stack list: %+v\n", stackList) + + return true, nil + }) + th.AssertNoErr(t, err) + + getStack, err := stacks.Get(client, stackName1, stack.ID).Extract() + th.AssertNoErr(t, err) + t.Logf("Got stack: %+v\n", getStack) + + abandonedStack, err := stacks.Abandon(client, stackName1, stack.ID).Extract() + th.AssertNoErr(t, err) + t.Logf("Abandonded stack %+v\n", abandonedStack) + th.AssertNoErr(t, err) +} + +// Test using the updated interface +func TestStacksNewTemplateFormat(t *testing.T) { + // Create a provider client for making the HTTP requests. + // See common.go in this directory for more information. + client := newClient(t) + + stackName1 := "gophercloud-test-stack-2" + templateOpts := new(osStacks.Template) + templateOpts.Bin = []byte(template) + createOpts := osStacks.CreateOpts{ + Name: stackName1, + TemplateOpts: templateOpts, + Timeout: 5, + } + stack, err := stacks.Create(client, createOpts).Extract() + th.AssertNoErr(t, err) + t.Logf("Created stack: %+v\n", stack) + defer func() { + err := stacks.Delete(client, stackName1, stack.ID).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Deleted stack (%s)", stackName1) + }() + err = gophercloud.WaitFor(60, func() (bool, error) { + getStack, err := stacks.Get(client, stackName1, stack.ID).Extract() + if err != nil { + return false, err + } + if getStack.Status == "CREATE_COMPLETE" { + return true, nil + } + return false, nil + }) + + updateOpts := osStacks.UpdateOpts{ + TemplateOpts: templateOpts, + Timeout: 20, + } + err = stacks.Update(client, stackName1, stack.ID, updateOpts).ExtractErr() + th.AssertNoErr(t, err) + err = gophercloud.WaitFor(60, func() (bool, error) { + getStack, err := stacks.Get(client, stackName1, stack.ID).Extract() + if err != nil { + return false, err + } + if getStack.Status == "UPDATE_COMPLETE" { + return true, nil + } + return false, nil + }) + + t.Logf("Updated stack") + + err = stacks.List(client, nil).EachPage(func(page pagination.Page) (bool, error) { + stackList, err := osStacks.ExtractStacks(page) + th.AssertNoErr(t, err) + + t.Logf("Got stack list: %+v\n", stackList) + + return true, nil + }) + th.AssertNoErr(t, err) + + getStack, err := stacks.Get(client, stackName1, stack.ID).Extract() + th.AssertNoErr(t, err) + t.Logf("Got stack: %+v\n", getStack) + + abandonedStack, err := stacks.Abandon(client, stackName1, stack.ID).Extract() + th.AssertNoErr(t, err) + t.Logf("Abandonded stack %+v\n", abandonedStack) + th.AssertNoErr(t, err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/orchestration/v1/stacktemplates_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/orchestration/v1/stacktemplates_test.go new file mode 100644 index 0000000000..9992e0c044 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/orchestration/v1/stacktemplates_test.go @@ -0,0 +1,75 @@ +// +build acceptance + +package v1 + +import ( + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks" + "github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacktemplates" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestStackTemplates(t *testing.T) { + // Create a provider client for making the HTTP requests. + // See common.go in this directory for more information. + client := newClient(t) + + stackName := "postman_stack_2" + + createOpts := stacks.CreateOpts{ + Name: stackName, + Template: template, + Timeout: 5, + } + stack, err := stacks.Create(client, createOpts).Extract() + th.AssertNoErr(t, err) + t.Logf("Created stack: %+v\n", stack) + defer func() { + err := stacks.Delete(client, stackName, stack.ID).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Deleted stack (%s)", stackName) + }() + err = gophercloud.WaitFor(60, func() (bool, error) { + getStack, err := stacks.Get(client, stackName, stack.ID).Extract() + if err != nil { + return false, err + } + if getStack.Status == "CREATE_COMPLETE" { + return true, nil + } + return false, nil + }) + + tmpl, err := stacktemplates.Get(client, stackName, stack.ID).Extract() + th.AssertNoErr(t, err) + t.Logf("retrieved template: %+v\n", tmpl) + + validateOpts := osStacktemplates.ValidateOpts{ + Template: `{"heat_template_version": "2013-05-23", + "description": "Simple template to test heat commands", + "parameters": { + "flavor": { + "default": "m1.tiny", + "type": "string", + }, + }, + "resources": { + "hello_world": { + "type": "OS::Nova::Server", + "properties": { + "key_name": "heat_key", + "flavor": { + "get_param": "flavor", + }, + "image": "ad091b52-742f-469e-8f3c-fd81cadf0743", + "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n", + }, + }, + }, + }`} + validatedTemplate, err := stacktemplates.Validate(client, validateOpts).Extract() + th.AssertNoErr(t, err) + t.Logf("validated template: %+v\n", validatedTemplate) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/pkg.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/pkg.go new file mode 100644 index 0000000000..ef11064a4e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/pkg.go @@ -0,0 +1,3 @@ +// +build acceptance + +package openstack diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/sharedfilesystems/v2/availabilityzones_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/sharedfilesystems/v2/availabilityzones_test.go new file mode 100644 index 0000000000..8841160a24 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/sharedfilesystems/v2/availabilityzones_test.go @@ -0,0 +1,29 @@ +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/availabilityzones" +) + +func TestAvailabilityZonesList(t *testing.T) { + client, err := clients.NewSharedFileSystemV2Client() + if err != nil { + t.Fatalf("Unable to create shared file system client: %v", err) + } + + allPages, err := availabilityzones.List(client).AllPages() + if err != nil { + t.Fatalf("Unable to list availability zones: %v", err) + } + + zones, err := availabilityzones.ExtractAvailabilityZones(allPages) + if err != nil { + t.Fatalf("Unable to extract availability zones: %v", err) + } + + if len(zones) == 0 { + t.Fatal("At least one availability zone was expected to be found") + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/sharedfilesystems/v2/pkg.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/sharedfilesystems/v2/pkg.go new file mode 100644 index 0000000000..5a5cd2b3f0 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/sharedfilesystems/v2/pkg.go @@ -0,0 +1,3 @@ +// The v2 package contains acceptance tests for the Openstack Manila V2 service. + +package v2 diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/sharedfilesystems/v2/securityservices.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/sharedfilesystems/v2/securityservices.go new file mode 100644 index 0000000000..265323d479 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/sharedfilesystems/v2/securityservices.go @@ -0,0 +1,60 @@ +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/securityservices" +) + +// CreateSecurityService will create a security service with a random name. An +// error will be returned if the security service was unable to be created. +func CreateSecurityService(t *testing.T, client *gophercloud.ServiceClient) (*securityservices.SecurityService, error) { + if testing.Short() { + t.Skip("Skipping test that requires share network creation in short mode.") + } + + securityServiceName := tools.RandomString("ACPTTEST", 16) + t.Logf("Attempting to create security service: %s", securityServiceName) + + createOpts := securityservices.CreateOpts{ + Name: securityServiceName, + Type: "kerberos", + } + + securityService, err := securityservices.Create(client, createOpts).Extract() + if err != nil { + return securityService, err + } + + return securityService, nil +} + +// DeleteSecurityService will delete a security service. An error will occur if +// the security service was unable to be deleted. +func DeleteSecurityService(t *testing.T, client *gophercloud.ServiceClient, securityService *securityservices.SecurityService) { + err := securityservices.Delete(client, securityService.ID).ExtractErr() + if err != nil { + t.Fatalf("Failed to delete security service %s: %v", securityService.ID, err) + } + + t.Logf("Deleted security service: %s", securityService.ID) +} + +// PrintSecurityService will print a security service and all of its attributes. +func PrintSecurityService(t *testing.T, securityService *securityservices.SecurityService) { + t.Logf("ID: %s", securityService.ID) + t.Logf("Project ID: %s", securityService.ProjectID) + t.Logf("Domain: %s", securityService.Domain) + t.Logf("Status: %s", securityService.Status) + t.Logf("Type: %s", securityService.Type) + t.Logf("Name: %s", securityService.Name) + t.Logf("Description: %s", securityService.Description) + t.Logf("DNS IP: %s", securityService.DNSIP) + t.Logf("User: %s", securityService.User) + t.Logf("Password: %s", securityService.Password) + t.Logf("Server: %s", securityService.Server) + t.Logf("Created at: %v", securityService.CreatedAt) + t.Logf("Updated at: %v", securityService.UpdatedAt) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/sharedfilesystems/v2/securityservices_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/sharedfilesystems/v2/securityservices_test.go new file mode 100644 index 0000000000..ecf8cc9a1c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/sharedfilesystems/v2/securityservices_test.go @@ -0,0 +1,142 @@ +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/securityservices" +) + +func TestSecurityServiceCreateDelete(t *testing.T) { + client, err := clients.NewSharedFileSystemV2Client() + if err != nil { + t.Fatalf("Unable to create shared file system client: %v", err) + } + + securityService, err := CreateSecurityService(t, client) + if err != nil { + t.Fatalf("Unable to create security service: %v", err) + } + + newSecurityService, err := securityservices.Get(client, securityService.ID).Extract() + if err != nil { + t.Errorf("Unable to retrieve the security service: %v", err) + } + + if newSecurityService.Name != securityService.Name { + t.Fatalf("Security service name was expeted to be: %s", securityService.Name) + } + + PrintSecurityService(t, securityService) + + defer DeleteSecurityService(t, client, securityService) +} + +func TestSecurityServiceList(t *testing.T) { + client, err := clients.NewSharedFileSystemV2Client() + if err != nil { + t.Fatalf("Unable to create a shared file system client: %v", err) + } + + allPages, err := securityservices.List(client, securityservices.ListOpts{}).AllPages() + if err != nil { + t.Fatalf("Unable to retrieve security services: %v", err) + } + + allSecurityServices, err := securityservices.ExtractSecurityServices(allPages) + if err != nil { + t.Fatalf("Unable to extract security services: %v", err) + } + + for _, securityService := range allSecurityServices { + PrintSecurityService(t, &securityService) + } +} + +// The test creates 2 security services and verifies that only the one(s) with +// a particular name are being listed +func TestSecurityServiceListFiltering(t *testing.T) { + client, err := clients.NewSharedFileSystemV2Client() + if err != nil { + t.Fatalf("Unable to create a shared file system client: %v", err) + } + + securityService, err := CreateSecurityService(t, client) + if err != nil { + t.Fatalf("Unable to create security service: %v", err) + } + defer DeleteSecurityService(t, client, securityService) + + securityService, err = CreateSecurityService(t, client) + if err != nil { + t.Fatalf("Unable to create security service: %v", err) + } + defer DeleteSecurityService(t, client, securityService) + + options := securityservices.ListOpts{ + Name: securityService.Name, + } + + allPages, err := securityservices.List(client, options).AllPages() + if err != nil { + t.Fatalf("Unable to retrieve security services: %v", err) + } + + allSecurityServices, err := securityservices.ExtractSecurityServices(allPages) + if err != nil { + t.Fatalf("Unable to extract security services: %v", err) + } + + for _, listedSecurityService := range allSecurityServices { + if listedSecurityService.Name != securityService.Name { + t.Fatalf("The name of the security service was expected to be %s", securityService.Name) + } + PrintSecurityService(t, &listedSecurityService) + } +} + +// Create a security service and update the name and description. Get the security +// service and verify that the name and description have been updated +func TestSecurityServiceUpdate(t *testing.T) { + client, err := clients.NewSharedFileSystemV2Client() + if err != nil { + t.Fatalf("Unable to create shared file system client: %v", err) + } + + securityService, err := CreateSecurityService(t, client) + if err != nil { + t.Fatalf("Unable to create security service: %v", err) + } + + options := securityservices.UpdateOpts{ + Name: "NewName", + Description: "New security service description", + Type: "ldap", + } + + _, err = securityservices.Update(client, securityService.ID, options).Extract() + if err != nil { + t.Errorf("Unable to update the security service: %v", err) + } + + newSecurityService, err := securityservices.Get(client, securityService.ID).Extract() + if err != nil { + t.Errorf("Unable to retrieve the security service: %v", err) + } + + if newSecurityService.Name != options.Name { + t.Fatalf("Security service name was expeted to be: %s", options.Name) + } + + if newSecurityService.Description != options.Description { + t.Fatalf("Security service description was expeted to be: %s", options.Description) + } + + if newSecurityService.Type != options.Type { + t.Fatalf("Security service type was expected to be: %s", options.Type) + } + + PrintSecurityService(t, securityService) + + defer DeleteSecurityService(t, client, securityService) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/sharedfilesystems/v2/sharenetworks.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/sharedfilesystems/v2/sharenetworks.go new file mode 100644 index 0000000000..b0aefd8577 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/sharedfilesystems/v2/sharenetworks.go @@ -0,0 +1,60 @@ +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharenetworks" +) + +// CreateShareNetwork will create a share network with a random name. An +// error will be returned if the share network was unable to be created. +func CreateShareNetwork(t *testing.T, client *gophercloud.ServiceClient) (*sharenetworks.ShareNetwork, error) { + if testing.Short() { + t.Skip("Skipping test that requires share network creation in short mode.") + } + + shareNetworkName := tools.RandomString("ACPTTEST", 16) + t.Logf("Attempting to create share network: %s", shareNetworkName) + + createOpts := sharenetworks.CreateOpts{ + Name: shareNetworkName, + Description: "This is a shared network", + } + + shareNetwork, err := sharenetworks.Create(client, createOpts).Extract() + if err != nil { + return shareNetwork, err + } + + return shareNetwork, nil +} + +// DeleteShareNetwork will delete a share network. An error will occur if +// the share network was unable to be deleted. +func DeleteShareNetwork(t *testing.T, client *gophercloud.ServiceClient, shareNetwork *sharenetworks.ShareNetwork) { + err := sharenetworks.Delete(client, shareNetwork.ID).ExtractErr() + if err != nil { + t.Fatalf("Failed to delete share network %s: %v", shareNetwork.ID, err) + } + + t.Logf("Deleted share network: %s", shareNetwork.ID) +} + +// PrintShareNetwork will print a share network and all of its attributes. +func PrintShareNetwork(t *testing.T, sharenetwork *sharenetworks.ShareNetwork) { + t.Logf("ID: %s", sharenetwork.ID) + t.Logf("Project ID: %s", sharenetwork.ProjectID) + t.Logf("Neutron network ID: %s", sharenetwork.NeutronNetID) + t.Logf("Neutron sub-network ID: %s", sharenetwork.NeutronSubnetID) + t.Logf("Nova network ID: %s", sharenetwork.NovaNetID) + t.Logf("Network type: %s", sharenetwork.NetworkType) + t.Logf("Segmentation ID: %d", sharenetwork.SegmentationID) + t.Logf("CIDR: %s", sharenetwork.CIDR) + t.Logf("IP version: %d", sharenetwork.IPVersion) + t.Logf("Name: %s", sharenetwork.Name) + t.Logf("Description: %s", sharenetwork.Description) + t.Logf("Created at: %v", sharenetwork.CreatedAt) + t.Logf("Updated at: %v", sharenetwork.UpdatedAt) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/sharedfilesystems/v2/sharenetworks_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/sharedfilesystems/v2/sharenetworks_test.go new file mode 100644 index 0000000000..7bf760f8ae --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/sharedfilesystems/v2/sharenetworks_test.go @@ -0,0 +1,223 @@ +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharenetworks" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestShareNetworkCreateDestroy(t *testing.T) { + client, err := clients.NewSharedFileSystemV2Client() + if err != nil { + t.Fatalf("Unable to create shared file system client: %v", err) + } + + shareNetwork, err := CreateShareNetwork(t, client) + if err != nil { + t.Fatalf("Unable to create share network: %v", err) + } + + newShareNetwork, err := sharenetworks.Get(client, shareNetwork.ID).Extract() + if err != nil { + t.Errorf("Unable to retrieve shareNetwork: %v", err) + } + + if newShareNetwork.Name != shareNetwork.Name { + t.Fatalf("Share network name was expeted to be: %s", shareNetwork.Name) + } + + PrintShareNetwork(t, shareNetwork) + + defer DeleteShareNetwork(t, client, shareNetwork) +} + +// Create a share network and update the name and description. Get the share +// network and verify that the name and description have been updated +func TestShareNetworkUpdate(t *testing.T) { + client, err := clients.NewSharedFileSystemV2Client() + if err != nil { + t.Fatalf("Unable to create shared file system client: %v", err) + } + + shareNetwork, err := CreateShareNetwork(t, client) + if err != nil { + t.Fatalf("Unable to create share network: %v", err) + } + + expectedShareNetwork, err := sharenetworks.Get(client, shareNetwork.ID).Extract() + if err != nil { + t.Errorf("Unable to retrieve shareNetwork: %v", err) + } + + options := sharenetworks.UpdateOpts{ + Name: "NewName", + Description: "New share network description", + } + + expectedShareNetwork.Name = options.Name + expectedShareNetwork.Description = options.Description + + _, err = sharenetworks.Update(client, shareNetwork.ID, options).Extract() + if err != nil { + t.Errorf("Unable to update shareNetwork: %v", err) + } + + updatedShareNetwork, err := sharenetworks.Get(client, shareNetwork.ID).Extract() + if err != nil { + t.Errorf("Unable to retrieve shareNetwork: %v", err) + } + + // Update time has to be set in order to get the assert equal to pass + expectedShareNetwork.UpdatedAt = updatedShareNetwork.UpdatedAt + + th.CheckDeepEquals(t, expectedShareNetwork, updatedShareNetwork) + + PrintShareNetwork(t, shareNetwork) + + defer DeleteShareNetwork(t, client, shareNetwork) +} + +func TestShareNetworkListDetail(t *testing.T) { + client, err := clients.NewSharedFileSystemV2Client() + if err != nil { + t.Fatalf("Unable to create a shared file system client: %v", err) + } + + allPages, err := sharenetworks.ListDetail(client, sharenetworks.ListOpts{}).AllPages() + if err != nil { + t.Fatalf("Unable to retrieve share networks: %v", err) + } + + allShareNetworks, err := sharenetworks.ExtractShareNetworks(allPages) + if err != nil { + t.Fatalf("Unable to extract share networks: %v", err) + } + + for _, shareNetwork := range allShareNetworks { + PrintShareNetwork(t, &shareNetwork) + } +} + +// The test creates 2 shared networks and verifies that only the one(s) with +// a particular name are being listed +func TestShareNetworkListFiltering(t *testing.T) { + client, err := clients.NewSharedFileSystemV2Client() + if err != nil { + t.Fatalf("Unable to create a shared file system client: %v", err) + } + + shareNetwork, err := CreateShareNetwork(t, client) + if err != nil { + t.Fatalf("Unable to create share network: %v", err) + } + defer DeleteShareNetwork(t, client, shareNetwork) + + shareNetwork, err = CreateShareNetwork(t, client) + if err != nil { + t.Fatalf("Unable to create share network: %v", err) + } + defer DeleteShareNetwork(t, client, shareNetwork) + + options := sharenetworks.ListOpts{ + Name: shareNetwork.Name, + } + + allPages, err := sharenetworks.ListDetail(client, options).AllPages() + if err != nil { + t.Fatalf("Unable to retrieve share networks: %v", err) + } + + allShareNetworks, err := sharenetworks.ExtractShareNetworks(allPages) + if err != nil { + t.Fatalf("Unable to extract share networks: %v", err) + } + + for _, listedShareNetwork := range allShareNetworks { + if listedShareNetwork.Name != shareNetwork.Name { + t.Fatalf("The name of the share network was expected to be %s", shareNetwork.Name) + } + PrintShareNetwork(t, &listedShareNetwork) + } +} + +func TestShareNetworkListPagination(t *testing.T) { + client, err := clients.NewSharedFileSystemV2Client() + if err != nil { + t.Fatalf("Unable to create a shared file system client: %v", err) + } + + shareNetwork, err := CreateShareNetwork(t, client) + if err != nil { + t.Fatalf("Unable to create share network: %v", err) + } + defer DeleteShareNetwork(t, client, shareNetwork) + + shareNetwork, err = CreateShareNetwork(t, client) + if err != nil { + t.Fatalf("Unable to create share network: %v", err) + } + defer DeleteShareNetwork(t, client, shareNetwork) + + count := 0 + + err = sharenetworks.ListDetail(client, sharenetworks.ListOpts{Offset: 0, Limit: 1}).EachPage(func(page pagination.Page) (bool, error) { + count++ + _, err := sharenetworks.ExtractShareNetworks(page) + if err != nil { + t.Fatalf("Failed to extract share networks: %v", err) + return false, err + } + + return true, nil + }) + if err != nil { + t.Fatalf("Unable to retrieve share networks: %v", err) + } + + if count < 2 { + t.Fatal("Expected to get at least 2 pages") + } + +} + +func TestShareNetworkAddRemoveSecurityService(t *testing.T) { + client, err := clients.NewSharedFileSystemV2Client() + if err != nil { + t.Fatalf("Unable to create a shared file system client: %v", err) + } + + securityService, err := CreateSecurityService(t, client) + if err != nil { + t.Fatalf("Unable to create security service: %v", err) + } + defer DeleteSecurityService(t, client, securityService) + + shareNetwork, err := CreateShareNetwork(t, client) + if err != nil { + t.Fatalf("Unable to create share network: %v", err) + } + defer DeleteShareNetwork(t, client, shareNetwork) + + options := sharenetworks.AddSecurityServiceOpts{ + SecurityServiceID: securityService.ID, + } + + _, err = sharenetworks.AddSecurityService(client, shareNetwork.ID, options).Extract() + if err != nil { + t.Errorf("Unable to add security service: %v", err) + } + + removeOptions := sharenetworks.RemoveSecurityServiceOpts{ + SecurityServiceID: securityService.ID, + } + + _, err = sharenetworks.RemoveSecurityService(client, shareNetwork.ID, removeOptions).Extract() + if err != nil { + t.Errorf("Unable to remove security service: %v", err) + } + + PrintShareNetwork(t, shareNetwork) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/sharedfilesystems/v2/shares.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/sharedfilesystems/v2/shares.go new file mode 100644 index 0000000000..82f2f8d1ff --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/sharedfilesystems/v2/shares.go @@ -0,0 +1,84 @@ +package v2 + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/shares" +) + +// CreateShare will create a share with a name, and a size of 1Gb. An +// error will be returned if the share could not be created +func CreateShare(t *testing.T, client *gophercloud.ServiceClient) (*shares.Share, error) { + if testing.Short() { + t.Skip("Skipping test that requres share creation in short mode.") + } + + choices, err := clients.AcceptanceTestChoicesFromEnv() + if err != nil { + t.Fatalf("Unable to fetch environment information") + } + + t.Logf("Share network id %s", choices.ShareNetworkID) + createOpts := shares.CreateOpts{ + Size: 1, + Name: "My Test Share", + ShareProto: "NFS", + ShareNetworkID: choices.ShareNetworkID, + } + + share, err := shares.Create(client, createOpts).Extract() + if err != nil { + return share, err + } + + err = waitForStatus(client, share.ID, "available", 600) + if err != nil { + return share, err + } + + return share, nil +} + +// DeleteShare will delete a share. A fatal error will occur if the share +// failed to be deleted. This works best when used as a deferred function. +func DeleteShare(t *testing.T, client *gophercloud.ServiceClient, share *shares.Share) { + err := shares.Delete(client, share.ID).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete share %s: %v", share.ID, err) + } + + t.Logf("Deleted share: %s", share.ID) +} + +// PrintShare prints some information of the share +func PrintShare(t *testing.T, share *shares.Share) { + asJSON, err := json.MarshalIndent(share, "", " ") + if err != nil { + t.Logf("Cannot print the contents of %s", share.ID) + } + + t.Logf("Share %s", string(asJSON)) +} + +func waitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error { + return gophercloud.WaitFor(secs, func() (bool, error) { + current, err := shares.Get(c, id).Extract() + if err != nil { + return false, err + } + + if current.Status == "error" { + return true, fmt.Errorf("An error occurred") + } + + if current.Status == status { + return true, nil + } + + return false, nil + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/sharedfilesystems/v2/shares_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/sharedfilesystems/v2/shares_test.go new file mode 100644 index 0000000000..abb9b3aefa --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/sharedfilesystems/v2/shares_test.go @@ -0,0 +1,28 @@ +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/shares" +) + +func TestShareCreate(t *testing.T) { + client, err := clients.NewSharedFileSystemV2Client() + if err != nil { + t.Fatalf("Unable to create a sharedfs client: %v", err) + } + + share, err := CreateShare(t, client) + if err != nil { + t.Fatalf("Unable to create a share: %v", err) + } + + defer DeleteShare(t, client, share) + + created, err := shares.Get(client, share.ID).Extract() + if err != nil { + t.Errorf("Unable to retrieve share: %v", err) + } + PrintShare(t, created) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/sharedfilesystems/v2/sharetypes.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/sharedfilesystems/v2/sharetypes.go new file mode 100644 index 0000000000..97b44bd0ef --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/sharedfilesystems/v2/sharetypes.go @@ -0,0 +1,56 @@ +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharetypes" +) + +// CreateShareType will create a share type with a random name. An +// error will be returned if the share type was unable to be created. +func CreateShareType(t *testing.T, client *gophercloud.ServiceClient) (*sharetypes.ShareType, error) { + if testing.Short() { + t.Skip("Skipping test that requires share type creation in short mode.") + } + + shareTypeName := tools.RandomString("ACPTTEST", 16) + t.Logf("Attempting to create share type: %s", shareTypeName) + + extraSpecsOps := sharetypes.ExtraSpecsOpts{ + DriverHandlesShareServers: true, + } + + createOpts := sharetypes.CreateOpts{ + Name: shareTypeName, + IsPublic: false, + ExtraSpecs: extraSpecsOps, + } + + shareType, err := sharetypes.Create(client, createOpts).Extract() + if err != nil { + return shareType, err + } + + return shareType, nil +} + +// DeleteShareType will delete a share type. An error will occur if +// the share type was unable to be deleted. +func DeleteShareType(t *testing.T, client *gophercloud.ServiceClient, shareType *sharetypes.ShareType) { + err := sharetypes.Delete(client, shareType.ID).ExtractErr() + if err != nil { + t.Fatalf("Failed to delete share type %s: %v", shareType.ID, err) + } + + t.Logf("Deleted share type: %s", shareType.ID) +} + +// PrintShareType will print a share type and all of its attributes. +func PrintShareType(t *testing.T, shareType *sharetypes.ShareType) { + t.Logf("Name: %s", shareType.Name) + t.Logf("ID: %s", shareType.ID) + t.Logf("OS share type access is public: %t", shareType.IsPublic) + t.Logf("Extra specs: %#v", shareType.ExtraSpecs) +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/sharedfilesystems/v2/sharetypes_test.go b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/sharedfilesystems/v2/sharetypes_test.go new file mode 100644 index 0000000000..f2f821951d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/openstack/sharedfilesystems/v2/sharetypes_test.go @@ -0,0 +1,162 @@ +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharetypes" +) + +func TestShareTypeCreateDestroy(t *testing.T) { + client, err := clients.NewSharedFileSystemV2Client() + if err != nil { + t.Fatalf("Unable to create shared file system client: %v", err) + } + + shareType, err := CreateShareType(t, client) + if err != nil { + t.Fatalf("Unable to create share type: %v", err) + } + + PrintShareType(t, shareType) + + defer DeleteShareType(t, client, shareType) +} + +func TestShareTypeList(t *testing.T) { + client, err := clients.NewSharedFileSystemV2Client() + if err != nil { + t.Fatalf("Unable to create a shared file system client: %v", err) + } + + allPages, err := sharetypes.List(client, sharetypes.ListOpts{}).AllPages() + if err != nil { + t.Fatalf("Unable to retrieve share types: %v", err) + } + + allShareTypes, err := sharetypes.ExtractShareTypes(allPages) + if err != nil { + t.Fatalf("Unable to extract share types: %v", err) + } + + for _, shareType := range allShareTypes { + PrintShareType(t, &shareType) + } +} + +func TestShareTypeGetDefault(t *testing.T) { + client, err := clients.NewSharedFileSystemV2Client() + if err != nil { + t.Fatalf("Unable to create a shared file system client: %v", err) + } + + shareType, err := sharetypes.GetDefault(client).Extract() + if err != nil { + t.Fatalf("Unable to retrieve the default share type: %v", err) + } + + PrintShareType(t, shareType) +} + +func TestShareTypeExtraSpecs(t *testing.T) { + client, err := clients.NewSharedFileSystemV2Client() + if err != nil { + t.Fatalf("Unable to create shared file system client: %v", err) + } + + shareType, err := CreateShareType(t, client) + if err != nil { + t.Fatalf("Unable to create share type: %v", err) + } + + options := sharetypes.SetExtraSpecsOpts{ + Specs: map[string]interface{}{"my_new_key": "my_value"}, + } + + _, err = sharetypes.SetExtraSpecs(client, shareType.ID, options).Extract() + if err != nil { + t.Fatalf("Unable to set extra specs for Share type: %s", shareType.Name) + } + + extraSpecs, err := sharetypes.GetExtraSpecs(client, shareType.ID).Extract() + if err != nil { + t.Fatalf("Unable to retrieve share type: %s", shareType.Name) + } + + if extraSpecs["driver_handles_share_servers"] != "True" { + t.Fatal("driver_handles_share_servers was expected to be true") + } + + if extraSpecs["my_new_key"] != "my_value" { + t.Fatal("my_new_key was expected to be equal to my_value") + } + + err = sharetypes.UnsetExtraSpecs(client, shareType.ID, "my_new_key").ExtractErr() + if err != nil { + t.Fatalf("Unable to unset extra specs for Share type: %s", shareType.Name) + } + + extraSpecs, err = sharetypes.GetExtraSpecs(client, shareType.ID).Extract() + if err != nil { + t.Fatalf("Unable to retrieve share type: %s", shareType.Name) + } + + if _, ok := extraSpecs["my_new_key"]; ok { + t.Fatalf("my_new_key was expected to be unset for Share type: %s", shareType.Name) + } + + PrintShareType(t, shareType) + + defer DeleteShareType(t, client, shareType) +} + +func TestShareTypeAccess(t *testing.T) { + client, err := clients.NewSharedFileSystemV2Client() + if err != nil { + t.Fatalf("Unable to create shared file system client: %v", err) + } + + shareType, err := CreateShareType(t, client) + if err != nil { + t.Fatalf("Unable to create share type: %v", err) + } + + options := sharetypes.AccessOpts{ + Project: "9e3a5a44e0134445867776ef53a37605", + } + + err = sharetypes.AddAccess(client, shareType.ID, options).ExtractErr() + if err != nil { + t.Fatalf("Unable to add a new access to a share type: %v", err) + } + + access, err := sharetypes.ShowAccess(client, shareType.ID).Extract() + if err != nil { + t.Fatalf("Unable to retrieve the access details for a share type: %v", err) + } + + expected := []sharetypes.ShareTypeAccess{{ShareTypeID: shareType.ID, ProjectID: options.Project}} + + if access[0] != expected[0] { + t.Fatal("Share type access is not the same than expected") + } + + err = sharetypes.RemoveAccess(client, shareType.ID, options).ExtractErr() + if err != nil { + t.Fatalf("Unable to remove an access from a share type: %v", err) + } + + access, err = sharetypes.ShowAccess(client, shareType.ID).Extract() + if err != nil { + t.Fatalf("Unable to retrieve the access details for a share type: %v", err) + } + + if len(access) > 0 { + t.Fatalf("No access should be left for the share type: %s", shareType.Name) + } + + PrintShareType(t, shareType) + + defer DeleteShareType(t, client, shareType) + +} diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/tools/pkg.go b/vendor/github.com/gophercloud/gophercloud/acceptance/tools/pkg.go new file mode 100644 index 0000000000..f7eca1298a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/tools/pkg.go @@ -0,0 +1 @@ +package tools diff --git a/vendor/github.com/gophercloud/gophercloud/acceptance/tools/tools.go b/vendor/github.com/gophercloud/gophercloud/acceptance/tools/tools.go new file mode 100644 index 0000000000..d2fd298d38 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/acceptance/tools/tools.go @@ -0,0 +1,73 @@ +package tools + +import ( + "crypto/rand" + "encoding/json" + "errors" + mrand "math/rand" + "testing" + "time" +) + +// ErrTimeout is returned if WaitFor takes longer than 300 second to happen. +var ErrTimeout = errors.New("Timed out") + +// WaitFor polls a predicate function once per second to wait for a certain state to arrive. +func WaitFor(predicate func() (bool, error)) error { + for i := 0; i < 300; i++ { + time.Sleep(1 * time.Second) + + satisfied, err := predicate() + if err != nil { + return err + } + if satisfied { + return nil + } + } + return ErrTimeout +} + +// MakeNewPassword generates a new string that's guaranteed to be different than the given one. +func MakeNewPassword(oldPass string) string { + randomPassword := RandomString("", 16) + for randomPassword == oldPass { + randomPassword = RandomString("", 16) + } + return randomPassword +} + +// RandomString generates a string of given length, but random content. +// All content will be within the ASCII graphic character set. +// (Implementation from Even Shaw's contribution on +// http://stackoverflow.com/questions/12771930/what-is-the-fastest-way-to-generate-a-long-random-string-in-go). +func RandomString(prefix string, n int) string { + const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + var bytes = make([]byte, n) + rand.Read(bytes) + for i, b := range bytes { + bytes[i] = alphanum[b%byte(len(alphanum))] + } + return prefix + string(bytes) +} + +// RandomInt will return a random integer between a specified range. +func RandomInt(min, max int) int { + mrand.Seed(time.Now().Unix()) + return mrand.Intn(max-min) + min +} + +// Elide returns the first bit of its input string with a suffix of "..." if it's longer than +// a comfortable 40 characters. +func Elide(value string) string { + if len(value) > 40 { + return value[0:37] + "..." + } + return value +} + +// PrintResource returns a resource as a readable structure +func PrintResource(t *testing.T, resource interface{}) { + b, _ := json.MarshalIndent(resource, "", " ") + t.Logf(string(b)) +} diff --git a/vendor/github.com/gophercloud/gophercloud/auth_options.go b/vendor/github.com/gophercloud/gophercloud/auth_options.go new file mode 100644 index 0000000000..19c08341af --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/auth_options.go @@ -0,0 +1,327 @@ +package gophercloud + +/* +AuthOptions stores information needed to authenticate to an OpenStack Cloud. +You can populate one manually, or use a provider's AuthOptionsFromEnv() function +to read relevant information from the standard environment variables. Pass one +to a provider's AuthenticatedClient function to authenticate and obtain a +ProviderClient representing an active session on that provider. + +Its fields are the union of those recognized by each identity implementation and +provider. +*/ +type AuthOptions struct { + // IdentityEndpoint specifies the HTTP endpoint that is required to work with + // the Identity API of the appropriate version. While it's ultimately needed by + // all of the identity services, it will often be populated by a provider-level + // function. + IdentityEndpoint string `json:"-"` + + // Username is required if using Identity V2 API. Consult with your provider's + // control panel to discover your account's username. In Identity V3, either + // UserID or a combination of Username and DomainID or DomainName are needed. + Username string `json:"username,omitempty"` + UserID string `json:"-"` + + Password string `json:"password,omitempty"` + + // At most one of DomainID and DomainName must be provided if using Username + // with Identity V3. Otherwise, either are optional. + DomainID string `json:"-"` + DomainName string `json:"name,omitempty"` + + // The TenantID and TenantName fields are optional for the Identity V2 API. + // The same fields are known as project_id and project_name in the Identity + // V3 API, but are collected as TenantID and TenantName here in both cases. + // Some providers allow you to specify a TenantName instead of the TenantId. + // Some require both. Your provider's authentication policies will determine + // how these fields influence authentication. + // If DomainID or DomainName are provided, they will also apply to TenantName. + // It is not currently possible to authenticate with Username and a Domain + // and scope to a Project in a different Domain by using TenantName. To + // accomplish that, the ProjectID will need to be provided to the TenantID + // option. + TenantID string `json:"tenantId,omitempty"` + TenantName string `json:"tenantName,omitempty"` + + // AllowReauth should be set to true if you grant permission for Gophercloud to + // cache your credentials in memory, and to allow Gophercloud to attempt to + // re-authenticate automatically if/when your token expires. If you set it to + // false, it will not cache these settings, but re-authentication will not be + // possible. This setting defaults to false. + // + // NOTE: The reauth function will try to re-authenticate endlessly if left unchecked. + // The way to limit the number of attempts is to provide a custom HTTP client to the provider client + // and provide a transport that implements the RoundTripper interface and stores the number of failed retries. + // For an example of this, see here: https://github.com/rackspace/rack/blob/1.0.0/auth/clients.go#L311 + AllowReauth bool `json:"-"` + + // TokenID allows users to authenticate (possibly as another user) with an + // authentication token ID. + TokenID string `json:"-"` +} + +// ToTokenV2CreateMap allows AuthOptions to satisfy the AuthOptionsBuilder +// interface in the v2 tokens package +func (opts AuthOptions) ToTokenV2CreateMap() (map[string]interface{}, error) { + // Populate the request map. + authMap := make(map[string]interface{}) + + if opts.Username != "" { + if opts.Password != "" { + authMap["passwordCredentials"] = map[string]interface{}{ + "username": opts.Username, + "password": opts.Password, + } + } else { + return nil, ErrMissingInput{Argument: "Password"} + } + } else if opts.TokenID != "" { + authMap["token"] = map[string]interface{}{ + "id": opts.TokenID, + } + } else { + return nil, ErrMissingInput{Argument: "Username"} + } + + if opts.TenantID != "" { + authMap["tenantId"] = opts.TenantID + } + if opts.TenantName != "" { + authMap["tenantName"] = opts.TenantName + } + + return map[string]interface{}{"auth": authMap}, nil +} + +func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[string]interface{}, error) { + type domainReq struct { + ID *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + } + + type projectReq struct { + Domain *domainReq `json:"domain,omitempty"` + Name *string `json:"name,omitempty"` + ID *string `json:"id,omitempty"` + } + + type userReq struct { + ID *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + Password string `json:"password"` + Domain *domainReq `json:"domain,omitempty"` + } + + type passwordReq struct { + User userReq `json:"user"` + } + + type tokenReq struct { + ID string `json:"id"` + } + + type identityReq struct { + Methods []string `json:"methods"` + Password *passwordReq `json:"password,omitempty"` + Token *tokenReq `json:"token,omitempty"` + } + + type authReq struct { + Identity identityReq `json:"identity"` + } + + type request struct { + Auth authReq `json:"auth"` + } + + // Populate the request structure based on the provided arguments. Create and return an error + // if insufficient or incompatible information is present. + var req request + + if opts.Password == "" { + if opts.TokenID != "" { + // Because we aren't using password authentication, it's an error to also provide any of the user-based authentication + // parameters. + if opts.Username != "" { + return nil, ErrUsernameWithToken{} + } + if opts.UserID != "" { + return nil, ErrUserIDWithToken{} + } + if opts.DomainID != "" { + return nil, ErrDomainIDWithToken{} + } + if opts.DomainName != "" { + return nil, ErrDomainNameWithToken{} + } + + // Configure the request for Token authentication. + req.Auth.Identity.Methods = []string{"token"} + req.Auth.Identity.Token = &tokenReq{ + ID: opts.TokenID, + } + } else { + // If no password or token ID are available, authentication can't continue. + return nil, ErrMissingPassword{} + } + } else { + // Password authentication. + req.Auth.Identity.Methods = []string{"password"} + + // At least one of Username and UserID must be specified. + if opts.Username == "" && opts.UserID == "" { + return nil, ErrUsernameOrUserID{} + } + + if opts.Username != "" { + // If Username is provided, UserID may not be provided. + if opts.UserID != "" { + return nil, ErrUsernameOrUserID{} + } + + // Either DomainID or DomainName must also be specified. + if opts.DomainID == "" && opts.DomainName == "" { + return nil, ErrDomainIDOrDomainName{} + } + + if opts.DomainID != "" { + if opts.DomainName != "" { + return nil, ErrDomainIDOrDomainName{} + } + + // Configure the request for Username and Password authentication with a DomainID. + req.Auth.Identity.Password = &passwordReq{ + User: userReq{ + Name: &opts.Username, + Password: opts.Password, + Domain: &domainReq{ID: &opts.DomainID}, + }, + } + } + + if opts.DomainName != "" { + // Configure the request for Username and Password authentication with a DomainName. + req.Auth.Identity.Password = &passwordReq{ + User: userReq{ + Name: &opts.Username, + Password: opts.Password, + Domain: &domainReq{Name: &opts.DomainName}, + }, + } + } + } + + if opts.UserID != "" { + // If UserID is specified, neither DomainID nor DomainName may be. + if opts.DomainID != "" { + return nil, ErrDomainIDWithUserID{} + } + if opts.DomainName != "" { + return nil, ErrDomainNameWithUserID{} + } + + // Configure the request for UserID and Password authentication. + req.Auth.Identity.Password = &passwordReq{ + User: userReq{ID: &opts.UserID, Password: opts.Password}, + } + } + } + + b, err := BuildRequestBody(req, "") + if err != nil { + return nil, err + } + + if len(scope) != 0 { + b["auth"].(map[string]interface{})["scope"] = scope + } + + return b, nil +} + +func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) { + + var scope struct { + ProjectID string + ProjectName string + DomainID string + DomainName string + } + + if opts.TenantID != "" { + scope.ProjectID = opts.TenantID + } else { + if opts.TenantName != "" { + scope.ProjectName = opts.TenantName + scope.DomainID = opts.DomainID + scope.DomainName = opts.DomainName + } + } + + if scope.ProjectName != "" { + // ProjectName provided: either DomainID or DomainName must also be supplied. + // ProjectID may not be supplied. + if scope.DomainID == "" && scope.DomainName == "" { + return nil, ErrScopeDomainIDOrDomainName{} + } + if scope.ProjectID != "" { + return nil, ErrScopeProjectIDOrProjectName{} + } + + if scope.DomainID != "" { + // ProjectName + DomainID + return map[string]interface{}{ + "project": map[string]interface{}{ + "name": &scope.ProjectName, + "domain": map[string]interface{}{"id": &scope.DomainID}, + }, + }, nil + } + + if scope.DomainName != "" { + // ProjectName + DomainName + return map[string]interface{}{ + "project": map[string]interface{}{ + "name": &scope.ProjectName, + "domain": map[string]interface{}{"name": &scope.DomainName}, + }, + }, nil + } + } else if scope.ProjectID != "" { + // ProjectID provided. ProjectName, DomainID, and DomainName may not be provided. + if scope.DomainID != "" { + return nil, ErrScopeProjectIDAlone{} + } + if scope.DomainName != "" { + return nil, ErrScopeProjectIDAlone{} + } + + // ProjectID + return map[string]interface{}{ + "project": map[string]interface{}{ + "id": &scope.ProjectID, + }, + }, nil + } else if scope.DomainID != "" { + // DomainID provided. ProjectID, ProjectName, and DomainName may not be provided. + if scope.DomainName != "" { + return nil, ErrScopeDomainIDOrDomainName{} + } + + // DomainID + return map[string]interface{}{ + "domain": map[string]interface{}{ + "id": &scope.DomainID, + }, + }, nil + } else if scope.DomainName != "" { + return nil, ErrScopeDomainName{} + } + + return nil, nil +} + +func (opts AuthOptions) CanReauth() bool { + return opts.AllowReauth +} diff --git a/vendor/github.com/gophercloud/gophercloud/doc.go b/vendor/github.com/gophercloud/gophercloud/doc.go new file mode 100644 index 0000000000..b559516f91 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/doc.go @@ -0,0 +1,69 @@ +/* +Package gophercloud provides a multi-vendor interface to OpenStack-compatible +clouds. The library has a three-level hierarchy: providers, services, and +resources. + +Provider structs represent the service providers that offer and manage a +collection of services. The IdentityEndpoint is typically refered to as +"auth_url" in information provided by the cloud operator. Additionally, +the cloud may refer to TenantID or TenantName as project_id and project_name. +These are defined like so: + + opts := gophercloud.AuthOptions{ + IdentityEndpoint: "https://openstack.example.com:5000/v2.0", + Username: "{username}", + Password: "{password}", + TenantID: "{tenant_id}", + } + + provider, err := openstack.AuthenticatedClient(opts) + +Service structs are specific to a provider and handle all of the logic and +operations for a particular OpenStack service. Examples of services include: +Compute, Object Storage, Block Storage. In order to define one, you need to +pass in the parent provider, like so: + + opts := gophercloud.EndpointOpts{Region: "RegionOne"} + + client := openstack.NewComputeV2(provider, opts) + +Resource structs are the domain models that services make use of in order +to work with and represent the state of API resources: + + server, err := servers.Get(client, "{serverId}").Extract() + +Intermediate Result structs are returned for API operations, which allow +generic access to the HTTP headers, response body, and any errors associated +with the network transaction. To turn a result into a usable resource struct, +you must call the Extract method which is chained to the response, or an +Extract function from an applicable extension: + + result := servers.Get(client, "{serverId}") + + // Attempt to extract the disk configuration from the OS-DCF disk config + // extension: + config, err := diskconfig.ExtractGet(result) + +All requests that enumerate a collection return a Pager struct that is used to +iterate through the results one page at a time. Use the EachPage method on that +Pager to handle each successive Page in a closure, then use the appropriate +extraction method from that request's package to interpret that Page as a slice +of results: + + err := servers.List(client, nil).EachPage(func (page pagination.Page) (bool, error) { + s, err := servers.ExtractServers(page) + if err != nil { + return false, err + } + + // Handle the []servers.Server slice. + + // Return "false" or an error to prematurely stop fetching new pages. + return true, nil + }) + +This top-level package contains utility functions and data types that are used +throughout the provider and service packages. Of particular note for end users +are the AuthOptions and EndpointOpts structs. +*/ +package gophercloud diff --git a/vendor/github.com/gophercloud/gophercloud/endpoint_search.go b/vendor/github.com/gophercloud/gophercloud/endpoint_search.go new file mode 100644 index 0000000000..9887947f61 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/endpoint_search.go @@ -0,0 +1,76 @@ +package gophercloud + +// Availability indicates to whom a specific service endpoint is accessible: +// the internet at large, internal networks only, or only to administrators. +// Different identity services use different terminology for these. Identity v2 +// lists them as different kinds of URLs within the service catalog ("adminURL", +// "internalURL", and "publicURL"), while v3 lists them as "Interfaces" in an +// endpoint's response. +type Availability string + +const ( + // AvailabilityAdmin indicates that an endpoint is only available to + // administrators. + AvailabilityAdmin Availability = "admin" + + // AvailabilityPublic indicates that an endpoint is available to everyone on + // the internet. + AvailabilityPublic Availability = "public" + + // AvailabilityInternal indicates that an endpoint is only available within + // the cluster's internal network. + AvailabilityInternal Availability = "internal" +) + +// EndpointOpts specifies search criteria used by queries against an +// OpenStack service catalog. The options must contain enough information to +// unambiguously identify one, and only one, endpoint within the catalog. +// +// Usually, these are passed to service client factory functions in a provider +// package, like "rackspace.NewComputeV2()". +type EndpointOpts struct { + // Type [required] is the service type for the client (e.g., "compute", + // "object-store"). Generally, this will be supplied by the service client + // function, but a user-given value will be honored if provided. + Type string + + // Name [optional] is the service name for the client (e.g., "nova") as it + // appears in the service catalog. Services can have the same Type but a + // different Name, which is why both Type and Name are sometimes needed. + Name string + + // Region [required] is the geographic region in which the endpoint resides, + // generally specifying which datacenter should house your resources. + // Required only for services that span multiple regions. + Region string + + // Availability [optional] is the visibility of the endpoint to be returned. + // Valid types include the constants AvailabilityPublic, AvailabilityInternal, + // or AvailabilityAdmin from this package. + // + // Availability is not required, and defaults to AvailabilityPublic. Not all + // providers or services offer all Availability options. + Availability Availability +} + +/* +EndpointLocator is an internal function to be used by provider implementations. + +It provides an implementation that locates a single endpoint from a service +catalog for a specific ProviderClient based on user-provided EndpointOpts. The +provider then uses it to discover related ServiceClients. +*/ +type EndpointLocator func(EndpointOpts) (string, error) + +// ApplyDefaults is an internal method to be used by provider implementations. +// +// It sets EndpointOpts fields if not already set, including a default type. +// Currently, EndpointOpts.Availability defaults to the public endpoint. +func (eo *EndpointOpts) ApplyDefaults(t string) { + if eo.Type == "" { + eo.Type = t + } + if eo.Availability == "" { + eo.Availability = AvailabilityPublic + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/errors.go b/vendor/github.com/gophercloud/gophercloud/errors.go new file mode 100644 index 0000000000..e0fe7c1e08 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/errors.go @@ -0,0 +1,408 @@ +package gophercloud + +import "fmt" + +// BaseError is an error type that all other error types embed. +type BaseError struct { + DefaultErrString string + Info string +} + +func (e BaseError) Error() string { + e.DefaultErrString = "An error occurred while executing a Gophercloud request." + return e.choseErrString() +} + +func (e BaseError) choseErrString() string { + if e.Info != "" { + return e.Info + } + return e.DefaultErrString +} + +// ErrMissingInput is the error when input is required in a particular +// situation but not provided by the user +type ErrMissingInput struct { + BaseError + Argument string +} + +func (e ErrMissingInput) Error() string { + e.DefaultErrString = fmt.Sprintf("Missing input for argument [%s]", e.Argument) + return e.choseErrString() +} + +// ErrInvalidInput is an error type used for most non-HTTP Gophercloud errors. +type ErrInvalidInput struct { + ErrMissingInput + Value interface{} +} + +func (e ErrInvalidInput) Error() string { + e.DefaultErrString = fmt.Sprintf("Invalid input provided for argument [%s]: [%+v]", e.Argument, e.Value) + return e.choseErrString() +} + +// ErrUnexpectedResponseCode is returned by the Request method when a response code other than +// those listed in OkCodes is encountered. +type ErrUnexpectedResponseCode struct { + BaseError + URL string + Method string + Expected []int + Actual int + Body []byte +} + +func (e ErrUnexpectedResponseCode) Error() string { + e.DefaultErrString = fmt.Sprintf( + "Expected HTTP response code %v when accessing [%s %s], but got %d instead\n%s", + e.Expected, e.Method, e.URL, e.Actual, e.Body, + ) + return e.choseErrString() +} + +// ErrDefault400 is the default error type returned on a 400 HTTP response code. +type ErrDefault400 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault401 is the default error type returned on a 401 HTTP response code. +type ErrDefault401 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault404 is the default error type returned on a 404 HTTP response code. +type ErrDefault404 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault405 is the default error type returned on a 405 HTTP response code. +type ErrDefault405 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault408 is the default error type returned on a 408 HTTP response code. +type ErrDefault408 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault429 is the default error type returned on a 429 HTTP response code. +type ErrDefault429 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault500 is the default error type returned on a 500 HTTP response code. +type ErrDefault500 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault503 is the default error type returned on a 503 HTTP response code. +type ErrDefault503 struct { + ErrUnexpectedResponseCode +} + +func (e ErrDefault400) Error() string { + return "Invalid request due to incorrect syntax or missing required parameters." +} +func (e ErrDefault401) Error() string { + return "Authentication failed" +} +func (e ErrDefault404) Error() string { + return "Resource not found" +} +func (e ErrDefault405) Error() string { + return "Method not allowed" +} +func (e ErrDefault408) Error() string { + return "The server timed out waiting for the request" +} +func (e ErrDefault429) Error() string { + return "Too many requests have been sent in a given amount of time. Pause" + + " requests, wait up to one minute, and try again." +} +func (e ErrDefault500) Error() string { + return "Internal Server Error" +} +func (e ErrDefault503) Error() string { + return "The service is currently unable to handle the request due to a temporary" + + " overloading or maintenance. This is a temporary condition. Try again later." +} + +// Err400er is the interface resource error types implement to override the error message +// from a 400 error. +type Err400er interface { + Error400(ErrUnexpectedResponseCode) error +} + +// Err401er is the interface resource error types implement to override the error message +// from a 401 error. +type Err401er interface { + Error401(ErrUnexpectedResponseCode) error +} + +// Err404er is the interface resource error types implement to override the error message +// from a 404 error. +type Err404er interface { + Error404(ErrUnexpectedResponseCode) error +} + +// Err405er is the interface resource error types implement to override the error message +// from a 405 error. +type Err405er interface { + Error405(ErrUnexpectedResponseCode) error +} + +// Err408er is the interface resource error types implement to override the error message +// from a 408 error. +type Err408er interface { + Error408(ErrUnexpectedResponseCode) error +} + +// Err429er is the interface resource error types implement to override the error message +// from a 429 error. +type Err429er interface { + Error429(ErrUnexpectedResponseCode) error +} + +// Err500er is the interface resource error types implement to override the error message +// from a 500 error. +type Err500er interface { + Error500(ErrUnexpectedResponseCode) error +} + +// Err503er is the interface resource error types implement to override the error message +// from a 503 error. +type Err503er interface { + Error503(ErrUnexpectedResponseCode) error +} + +// ErrTimeOut is the error type returned when an operations times out. +type ErrTimeOut struct { + BaseError +} + +func (e ErrTimeOut) Error() string { + e.DefaultErrString = "A time out occurred" + return e.choseErrString() +} + +// ErrUnableToReauthenticate is the error type returned when reauthentication fails. +type ErrUnableToReauthenticate struct { + BaseError + ErrOriginal error +} + +func (e ErrUnableToReauthenticate) Error() string { + e.DefaultErrString = fmt.Sprintf("Unable to re-authenticate: %s", e.ErrOriginal) + return e.choseErrString() +} + +// ErrErrorAfterReauthentication is the error type returned when reauthentication +// succeeds, but an error occurs afterword (usually an HTTP error). +type ErrErrorAfterReauthentication struct { + BaseError + ErrOriginal error +} + +func (e ErrErrorAfterReauthentication) Error() string { + e.DefaultErrString = fmt.Sprintf("Successfully re-authenticated, but got error executing request: %s", e.ErrOriginal) + return e.choseErrString() +} + +// ErrServiceNotFound is returned when no service in a service catalog matches +// the provided EndpointOpts. This is generally returned by provider service +// factory methods like "NewComputeV2()" and can mean that a service is not +// enabled for your account. +type ErrServiceNotFound struct { + BaseError +} + +func (e ErrServiceNotFound) Error() string { + e.DefaultErrString = "No suitable service could be found in the service catalog." + return e.choseErrString() +} + +// ErrEndpointNotFound is returned when no available endpoints match the +// provided EndpointOpts. This is also generally returned by provider service +// factory methods, and usually indicates that a region was specified +// incorrectly. +type ErrEndpointNotFound struct { + BaseError +} + +func (e ErrEndpointNotFound) Error() string { + e.DefaultErrString = "No suitable endpoint could be found in the service catalog." + return e.choseErrString() +} + +// ErrResourceNotFound is the error when trying to retrieve a resource's +// ID by name and the resource doesn't exist. +type ErrResourceNotFound struct { + BaseError + Name string + ResourceType string +} + +func (e ErrResourceNotFound) Error() string { + e.DefaultErrString = fmt.Sprintf("Unable to find %s with name %s", e.ResourceType, e.Name) + return e.choseErrString() +} + +// ErrMultipleResourcesFound is the error when trying to retrieve a resource's +// ID by name and multiple resources have the user-provided name. +type ErrMultipleResourcesFound struct { + BaseError + Name string + Count int + ResourceType string +} + +func (e ErrMultipleResourcesFound) Error() string { + e.DefaultErrString = fmt.Sprintf("Found %d %ss matching %s", e.Count, e.ResourceType, e.Name) + return e.choseErrString() +} + +// ErrUnexpectedType is the error when an unexpected type is encountered +type ErrUnexpectedType struct { + BaseError + Expected string + Actual string +} + +func (e ErrUnexpectedType) Error() string { + e.DefaultErrString = fmt.Sprintf("Expected %s but got %s", e.Expected, e.Actual) + return e.choseErrString() +} + +func unacceptedAttributeErr(attribute string) string { + return fmt.Sprintf("The base Identity V3 API does not accept authentication by %s", attribute) +} + +func redundantWithTokenErr(attribute string) string { + return fmt.Sprintf("%s may not be provided when authenticating with a TokenID", attribute) +} + +func redundantWithUserID(attribute string) string { + return fmt.Sprintf("%s may not be provided when authenticating with a UserID", attribute) +} + +// ErrAPIKeyProvided indicates that an APIKey was provided but can't be used. +type ErrAPIKeyProvided struct{ BaseError } + +func (e ErrAPIKeyProvided) Error() string { + return unacceptedAttributeErr("APIKey") +} + +// ErrTenantIDProvided indicates that a TenantID was provided but can't be used. +type ErrTenantIDProvided struct{ BaseError } + +func (e ErrTenantIDProvided) Error() string { + return unacceptedAttributeErr("TenantID") +} + +// ErrTenantNameProvided indicates that a TenantName was provided but can't be used. +type ErrTenantNameProvided struct{ BaseError } + +func (e ErrTenantNameProvided) Error() string { + return unacceptedAttributeErr("TenantName") +} + +// ErrUsernameWithToken indicates that a Username was provided, but token authentication is being used instead. +type ErrUsernameWithToken struct{ BaseError } + +func (e ErrUsernameWithToken) Error() string { + return redundantWithTokenErr("Username") +} + +// ErrUserIDWithToken indicates that a UserID was provided, but token authentication is being used instead. +type ErrUserIDWithToken struct{ BaseError } + +func (e ErrUserIDWithToken) Error() string { + return redundantWithTokenErr("UserID") +} + +// ErrDomainIDWithToken indicates that a DomainID was provided, but token authentication is being used instead. +type ErrDomainIDWithToken struct{ BaseError } + +func (e ErrDomainIDWithToken) Error() string { + return redundantWithTokenErr("DomainID") +} + +// ErrDomainNameWithToken indicates that a DomainName was provided, but token authentication is being used instead.s +type ErrDomainNameWithToken struct{ BaseError } + +func (e ErrDomainNameWithToken) Error() string { + return redundantWithTokenErr("DomainName") +} + +// ErrUsernameOrUserID indicates that neither username nor userID are specified, or both are at once. +type ErrUsernameOrUserID struct{ BaseError } + +func (e ErrUsernameOrUserID) Error() string { + return "Exactly one of Username and UserID must be provided for password authentication" +} + +// ErrDomainIDWithUserID indicates that a DomainID was provided, but unnecessary because a UserID is being used. +type ErrDomainIDWithUserID struct{ BaseError } + +func (e ErrDomainIDWithUserID) Error() string { + return redundantWithUserID("DomainID") +} + +// ErrDomainNameWithUserID indicates that a DomainName was provided, but unnecessary because a UserID is being used. +type ErrDomainNameWithUserID struct{ BaseError } + +func (e ErrDomainNameWithUserID) Error() string { + return redundantWithUserID("DomainName") +} + +// ErrDomainIDOrDomainName indicates that a username was provided, but no domain to scope it. +// It may also indicate that both a DomainID and a DomainName were provided at once. +type ErrDomainIDOrDomainName struct{ BaseError } + +func (e ErrDomainIDOrDomainName) Error() string { + return "You must provide exactly one of DomainID or DomainName to authenticate by Username" +} + +// ErrMissingPassword indicates that no password was provided and no token is available. +type ErrMissingPassword struct{ BaseError } + +func (e ErrMissingPassword) Error() string { + return "You must provide a password to authenticate" +} + +// ErrScopeDomainIDOrDomainName indicates that a domain ID or Name was required in a Scope, but not present. +type ErrScopeDomainIDOrDomainName struct{ BaseError } + +func (e ErrScopeDomainIDOrDomainName) Error() string { + return "You must provide exactly one of DomainID or DomainName in a Scope with ProjectName" +} + +// ErrScopeProjectIDOrProjectName indicates that both a ProjectID and a ProjectName were provided in a Scope. +type ErrScopeProjectIDOrProjectName struct{ BaseError } + +func (e ErrScopeProjectIDOrProjectName) Error() string { + return "You must provide at most one of ProjectID or ProjectName in a Scope" +} + +// ErrScopeProjectIDAlone indicates that a ProjectID was provided with other constraints in a Scope. +type ErrScopeProjectIDAlone struct{ BaseError } + +func (e ErrScopeProjectIDAlone) Error() string { + return "ProjectID must be supplied alone in a Scope" +} + +// ErrScopeDomainName indicates that a DomainName was provided alone in a Scope. +type ErrScopeDomainName struct{ BaseError } + +func (e ErrScopeDomainName) Error() string { + return "DomainName must be supplied with a ProjectName or ProjectID in a Scope" +} + +// ErrScopeEmpty indicates that no credentials were provided in a Scope. +type ErrScopeEmpty struct{ BaseError } + +func (e ErrScopeEmpty) Error() string { + return "You must provide either a Project or Domain in a Scope" +} diff --git a/vendor/github.com/gophercloud/gophercloud/internal/pkg.go b/vendor/github.com/gophercloud/gophercloud/internal/pkg.go new file mode 100644 index 0000000000..5bf0569ce8 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/internal/pkg.go @@ -0,0 +1 @@ +package internal diff --git a/vendor/github.com/gophercloud/gophercloud/internal/testing/util_test.go b/vendor/github.com/gophercloud/gophercloud/internal/testing/util_test.go new file mode 100644 index 0000000000..12fd172b60 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/internal/testing/util_test.go @@ -0,0 +1,42 @@ +package testing + +import ( + "reflect" + "testing" + + "github.com/gophercloud/gophercloud/internal" +) + +func TestRemainingKeys(t *testing.T) { + type User struct { + UserID string `json:"user_id"` + Username string `json:"username"` + Location string `json:"-"` + CreatedAt string `json:"-"` + Status string + IsAdmin bool + } + + userResponse := map[string]interface{}{ + "user_id": "abcd1234", + "username": "jdoe", + "location": "Hawaii", + "created_at": "2017-06-08T02:49:03.000000", + "status": "active", + "is_admin": "true", + "custom_field": "foo", + } + + expected := map[string]interface{}{ + "created_at": "2017-06-08T02:49:03.000000", + "is_admin": "true", + "custom_field": "foo", + } + + actual := internal.RemainingKeys(User{}, userResponse) + + isEqual := reflect.DeepEqual(expected, actual) + if !isEqual { + t.Fatalf("expected %s but got %s", expected, actual) + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/internal/util.go b/vendor/github.com/gophercloud/gophercloud/internal/util.go new file mode 100644 index 0000000000..8efb283e72 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/internal/util.go @@ -0,0 +1,34 @@ +package internal + +import ( + "reflect" + "strings" +) + +// RemainingKeys will inspect a struct and compare it to a map. Any struct +// field that does not have a JSON tag that matches a key in the map or +// a matching lower-case field in the map will be returned as an extra. +// +// This is useful for determining the extra fields returned in response bodies +// for resources that can contain an arbitrary or dynamic number of fields. +func RemainingKeys(s interface{}, m map[string]interface{}) (extras map[string]interface{}) { + extras = make(map[string]interface{}) + for k, v := range m { + extras[k] = v + } + + valueOf := reflect.ValueOf(s) + typeOf := reflect.TypeOf(s) + for i := 0; i < valueOf.NumField(); i++ { + field := typeOf.Field(i) + + lowerField := strings.ToLower(field.Name) + delete(extras, lowerField) + + if tagValue := field.Tag.Get("json"); tagValue != "" && tagValue != "-" { + delete(extras, tagValue) + } + } + + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/auth_env.go b/vendor/github.com/gophercloud/gophercloud/openstack/auth_env.go new file mode 100644 index 0000000000..f6d2eb194b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/auth_env.go @@ -0,0 +1,52 @@ +package openstack + +import ( + "os" + + "github.com/gophercloud/gophercloud" +) + +var nilOptions = gophercloud.AuthOptions{} + +// AuthOptionsFromEnv fills out an identity.AuthOptions structure with the settings found on the various OpenStack +// OS_* environment variables. The following variables provide sources of truth: OS_AUTH_URL, OS_USERNAME, +// OS_PASSWORD, OS_TENANT_ID, and OS_TENANT_NAME. Of these, OS_USERNAME, OS_PASSWORD, and OS_AUTH_URL must +// have settings, or an error will result. OS_TENANT_ID and OS_TENANT_NAME are optional. +func AuthOptionsFromEnv() (gophercloud.AuthOptions, error) { + authURL := os.Getenv("OS_AUTH_URL") + username := os.Getenv("OS_USERNAME") + userID := os.Getenv("OS_USERID") + password := os.Getenv("OS_PASSWORD") + tenantID := os.Getenv("OS_TENANT_ID") + tenantName := os.Getenv("OS_TENANT_NAME") + domainID := os.Getenv("OS_DOMAIN_ID") + domainName := os.Getenv("OS_DOMAIN_NAME") + + if authURL == "" { + err := gophercloud.ErrMissingInput{Argument: "authURL"} + return nilOptions, err + } + + if username == "" && userID == "" { + err := gophercloud.ErrMissingInput{Argument: "username"} + return nilOptions, err + } + + if password == "" { + err := gophercloud.ErrMissingInput{Argument: "password"} + return nilOptions, err + } + + ao := gophercloud.AuthOptions{ + IdentityEndpoint: authURL, + UserID: userID, + Username: username, + Password: password, + TenantID: tenantID, + TenantName: tenantName, + DomainID: domainID, + DomainName: domainName, + } + + return ao, nil +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/schedulerstats/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/schedulerstats/doc.go new file mode 100644 index 0000000000..965cc5654f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/schedulerstats/doc.go @@ -0,0 +1,3 @@ +// Package schedulerstats gives information about block storage pool capacity +// and utilisation +package schedulerstats diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/schedulerstats/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/schedulerstats/requests.go new file mode 100644 index 0000000000..73bd49eb05 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/schedulerstats/requests.go @@ -0,0 +1,43 @@ +package schedulerstats + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToStoragePoolsListQuery() (string, error) +} + +// ListOpts controls the view of data returned (e.g globally or per project) +// via tenant_id and the verbosity via detail +type ListOpts struct { + // ID of the tenant to look up storage pools for + TenantID string `q:"tenant_id"` + + // Whether to list extended details + Detail bool `q:"detail"` +} + +// Formats a ListOpts into a query string. +func (opts ListOpts) ToStoragePoolsListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List makes a request against the API to list hypervisors. +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := storagePoolsListURL(client) + if opts != nil { + query, err := opts.ToStoragePoolsListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return StoragePoolPage{pagination.SinglePageBase(r)} + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/schedulerstats/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/schedulerstats/results.go new file mode 100644 index 0000000000..68f33264d0 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/schedulerstats/results.go @@ -0,0 +1,89 @@ +package schedulerstats + +import ( + "encoding/json" + "math" + + "github.com/gophercloud/gophercloud/pagination" +) + +// Minimum set of driver capabilities only +type Capabilities struct { + // Required Fields + DriverVersion string `json:"driver_version"` + FreeCapacityGB float64 `json:"-"` + StorageProtocol string `json:"storage_protocol"` + TotalCapacityGB float64 `json:"-"` + VendorName string `json:"vendor_name"` + VolumeBackendName string `json:"volume_backend_name"` + // Optional Fields + ReservedPercentage int64 `json:"reserved_percentage"` + LocationInfo string `json:"location_info"` + QoSSupport bool `json:"QoS_support"` + ProvisionedCapacityGB float64 `json:"provisioned_capacity_gb"` + MaxOverSubscriptionRatio float64 `json:"max_over_subscription_ratio"` + ThinProvisioningSupport bool `json:"thin_provisioning_support"` + ThickProvisioningSupport bool `json:"thick_provisioning_support"` + TotalVolumes int64 `json:"total_volumes"` + FilterFunction string `json:"filter_function"` + GoodnessFuction string `json:"goodness_function"` + Mutliattach bool `json:"multiattach"` + SparseCopyVolume bool `json:"sparse_copy_volume"` +} + +type StoragePool struct { + Name string `json:"name"` + Capabilities Capabilities `json:"capabilities"` +} + +func (r *Capabilities) UnmarshalJSON(b []byte) error { + type tmp Capabilities + var s struct { + tmp + FreeCapacityGB interface{} `json:"free_capacity_gb"` + TotalCapacityGB interface{} `json:"total_capacity_gb"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Capabilities(s.tmp) + + // Generic function to parse a capacity value which may be a numeric + // value, "unknown", or "infinite" + parseCapacity := func(capacity interface{}) float64 { + if capacity != nil { + switch capacity.(type) { + case float64: + return capacity.(float64) + case string: + if capacity.(string) == "infinite" { + return math.Inf(1) + } + } + } + return 0.0 + } + + r.FreeCapacityGB = parseCapacity(s.FreeCapacityGB) + r.TotalCapacityGB = parseCapacity(s.TotalCapacityGB) + + return nil +} + +type StoragePoolPage struct { + pagination.SinglePageBase +} + +func (page StoragePoolPage) IsEmpty() (bool, error) { + va, err := ExtractStoragePools(page) + return len(va) == 0, err +} + +func ExtractStoragePools(p pagination.Page) ([]StoragePool, error) { + var s struct { + StoragePools []StoragePool `json:"pools"` + } + err := (p.(StoragePoolPage)).ExtractInto(&s) + return s.StoragePools, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/schedulerstats/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/schedulerstats/testing/fixtures.go new file mode 100644 index 0000000000..4031e29724 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/schedulerstats/testing/fixtures.go @@ -0,0 +1,106 @@ +package testing + +import ( + "fmt" + "math" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/schedulerstats" + "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +const StoragePoolsListBody = ` +{ + "pools": [ + { + "name": "rbd:cinder.volumes.ssd@cinder.volumes.ssd#cinder.volumes.ssd" + }, + { + "name": "rbd:cinder.volumes.hdd@cinder.volumes#cinder.volumes.hdd" + } + ] +} +` + +const StoragePoolsListBodyDetail = ` +{ + "pools": [ + { + "capabilities": { + "driver_version": "1.2.0", + "filter_function": null, + "free_capacity_gb": 64765, + "goodness_function": null, + "multiattach": false, + "reserved_percentage": 0, + "storage_protocol": "ceph", + "timestamp": "2016-11-24T10:33:51.248360", + "total_capacity_gb": 787947.93, + "vendor_name": "Open Source", + "volume_backend_name": "cinder.volumes.ssd" + }, + "name": "rbd:cinder.volumes.ssd@cinder.volumes.ssd#cinder.volumes.ssd" + }, + { + "capabilities": { + "driver_version": "1.2.0", + "filter_function": null, + "free_capacity_gb": "unknown", + "goodness_function": null, + "multiattach": false, + "reserved_percentage": 0, + "storage_protocol": "ceph", + "timestamp": "2016-11-24T10:33:43.138628", + "total_capacity_gb": "infinite", + "vendor_name": "Open Source", + "volume_backend_name": "cinder.volumes.hdd" + }, + "name": "rbd:cinder.volumes.hdd@cinder.volumes.hdd#cinder.volumes.hdd" + } + ] +} +` + +var ( + StoragePoolFake1 = schedulerstats.StoragePool{ + Name: "rbd:cinder.volumes.ssd@cinder.volumes.ssd#cinder.volumes.ssd", + Capabilities: schedulerstats.Capabilities{ + DriverVersion: "1.2.0", + FreeCapacityGB: 64765, + StorageProtocol: "ceph", + TotalCapacityGB: 787947.93, + VendorName: "Open Source", + VolumeBackendName: "cinder.volumes.ssd", + }, + } + + StoragePoolFake2 = schedulerstats.StoragePool{ + Name: "rbd:cinder.volumes.hdd@cinder.volumes.hdd#cinder.volumes.hdd", + Capabilities: schedulerstats.Capabilities{ + DriverVersion: "1.2.0", + FreeCapacityGB: 0.0, + StorageProtocol: "ceph", + TotalCapacityGB: math.Inf(1), + VendorName: "Open Source", + VolumeBackendName: "cinder.volumes.hdd", + }, + } +) + +func HandleStoragePoolsListSuccessfully(t *testing.T) { + testhelper.Mux.HandleFunc("/scheduler-stats/get_pools", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "GET") + testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + + r.ParseForm() + if r.FormValue("detail") == "true" { + fmt.Fprintf(w, StoragePoolsListBodyDetail) + } else { + fmt.Fprintf(w, StoragePoolsListBody) + } + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/schedulerstats/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/schedulerstats/testing/requests_test.go new file mode 100644 index 0000000000..8a4ef5180d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/schedulerstats/testing/requests_test.go @@ -0,0 +1,38 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/schedulerstats" + "github.com/gophercloud/gophercloud/pagination" + "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestListStoragePoolsDetail(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + HandleStoragePoolsListSuccessfully(t) + + pages := 0 + err := schedulerstats.List(client.ServiceClient(), schedulerstats.ListOpts{Detail: true}).EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := schedulerstats.ExtractStoragePools(page) + testhelper.AssertNoErr(t, err) + + if len(actual) != 2 { + t.Fatalf("Expected 2 backends, got %d", len(actual)) + } + testhelper.CheckDeepEquals(t, StoragePoolFake1, actual[0]) + testhelper.CheckDeepEquals(t, StoragePoolFake2, actual[1]) + + return true, nil + }) + + testhelper.AssertNoErr(t, err) + + if pages != 1 { + t.Errorf("Expected 1 page, saw %d", pages) + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/schedulerstats/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/schedulerstats/urls.go new file mode 100644 index 0000000000..c0ddb3695a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/schedulerstats/urls.go @@ -0,0 +1,7 @@ +package schedulerstats + +import "github.com/gophercloud/gophercloud" + +func storagePoolsListURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("scheduler-stats", "get_pools") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/doc.go new file mode 100644 index 0000000000..0935fdbd59 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/doc.go @@ -0,0 +1,5 @@ +// Package volumeactions provides information and interaction with volumes in the +// OpenStack Block Storage service. A volume is a detachable block storage +// device, akin to a USB hard drive. It can only be attached to one instance at +// a time. +package volumeactions diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/requests.go new file mode 100644 index 0000000000..939d397da9 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/requests.go @@ -0,0 +1,253 @@ +package volumeactions + +import ( + "github.com/gophercloud/gophercloud" +) + +// AttachOptsBuilder allows extensions to add additional parameters to the +// Attach request. +type AttachOptsBuilder interface { + ToVolumeAttachMap() (map[string]interface{}, error) +} + +// AttachMode describes the attachment mode for volumes. +type AttachMode string + +// These constants determine how a volume is attached +const ( + ReadOnly AttachMode = "ro" + ReadWrite AttachMode = "rw" +) + +// AttachOpts contains options for attaching a Volume. +type AttachOpts struct { + // The mountpoint of this volume + MountPoint string `json:"mountpoint,omitempty"` + // The nova instance ID, can't set simultaneously with HostName + InstanceUUID string `json:"instance_uuid,omitempty"` + // The hostname of baremetal host, can't set simultaneously with InstanceUUID + HostName string `json:"host_name,omitempty"` + // Mount mode of this volume + Mode AttachMode `json:"mode,omitempty"` +} + +// ToVolumeAttachMap assembles a request body based on the contents of a +// AttachOpts. +func (opts AttachOpts) ToVolumeAttachMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "os-attach") +} + +// Attach will attach a volume based on the values in AttachOpts. +func Attach(client *gophercloud.ServiceClient, id string, opts AttachOptsBuilder) (r AttachResult) { + b, err := opts.ToVolumeAttachMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(attachURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + return +} + +// BeginDetach will mark the volume as detaching +func BeginDetaching(client *gophercloud.ServiceClient, id string) (r BeginDetachingResult) { + b := map[string]interface{}{"os-begin_detaching": make(map[string]interface{})} + _, r.Err = client.Post(beginDetachingURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + return +} + +// DetachOptsBuilder allows extensions to add additional parameters to the +// Detach request. +type DetachOptsBuilder interface { + ToVolumeDetachMap() (map[string]interface{}, error) +} + +type DetachOpts struct { + AttachmentID string `json:"attachment_id,omitempty"` +} + +// ToVolumeDetachMap assembles a request body based on the contents of a +// DetachOpts. +func (opts DetachOpts) ToVolumeDetachMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "os-detach") +} + +// Detach will detach a volume based on volume id. +func Detach(client *gophercloud.ServiceClient, id string, opts DetachOptsBuilder) (r DetachResult) { + b, err := opts.ToVolumeDetachMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(detachURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + return +} + +// Reserve will reserve a volume based on volume id. +func Reserve(client *gophercloud.ServiceClient, id string) (r ReserveResult) { + b := map[string]interface{}{"os-reserve": make(map[string]interface{})} + _, r.Err = client.Post(reserveURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201, 202}, + }) + return +} + +// Unreserve will unreserve a volume based on volume id. +func Unreserve(client *gophercloud.ServiceClient, id string) (r UnreserveResult) { + b := map[string]interface{}{"os-unreserve": make(map[string]interface{})} + _, r.Err = client.Post(unreserveURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201, 202}, + }) + return +} + +// InitializeConnectionOptsBuilder allows extensions to add additional parameters to the +// InitializeConnection request. +type InitializeConnectionOptsBuilder interface { + ToVolumeInitializeConnectionMap() (map[string]interface{}, error) +} + +// InitializeConnectionOpts hosts options for InitializeConnection. +type InitializeConnectionOpts struct { + IP string `json:"ip,omitempty"` + Host string `json:"host,omitempty"` + Initiator string `json:"initiator,omitempty"` + Wwpns []string `json:"wwpns,omitempty"` + Wwnns string `json:"wwnns,omitempty"` + Multipath *bool `json:"multipath,omitempty"` + Platform string `json:"platform,omitempty"` + OSType string `json:"os_type,omitempty"` +} + +// ToVolumeInitializeConnectionMap assembles a request body based on the contents of a +// InitializeConnectionOpts. +func (opts InitializeConnectionOpts) ToVolumeInitializeConnectionMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "connector") + return map[string]interface{}{"os-initialize_connection": b}, err +} + +// InitializeConnection initializes iscsi connection. +func InitializeConnection(client *gophercloud.ServiceClient, id string, opts InitializeConnectionOptsBuilder) (r InitializeConnectionResult) { + b, err := opts.ToVolumeInitializeConnectionMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(initializeConnectionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201, 202}, + }) + return +} + +// TerminateConnectionOptsBuilder allows extensions to add additional parameters to the +// TerminateConnection request. +type TerminateConnectionOptsBuilder interface { + ToVolumeTerminateConnectionMap() (map[string]interface{}, error) +} + +// TerminateConnectionOpts hosts options for TerminateConnection. +type TerminateConnectionOpts struct { + IP string `json:"ip,omitempty"` + Host string `json:"host,omitempty"` + Initiator string `json:"initiator,omitempty"` + Wwpns []string `json:"wwpns,omitempty"` + Wwnns string `json:"wwnns,omitempty"` + Multipath *bool `json:"multipath,omitempty"` + Platform string `json:"platform,omitempty"` + OSType string `json:"os_type,omitempty"` +} + +// ToVolumeTerminateConnectionMap assembles a request body based on the contents of a +// TerminateConnectionOpts. +func (opts TerminateConnectionOpts) ToVolumeTerminateConnectionMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "connector") + return map[string]interface{}{"os-terminate_connection": b}, err +} + +// TerminateConnection terminates iscsi connection. +func TerminateConnection(client *gophercloud.ServiceClient, id string, opts TerminateConnectionOptsBuilder) (r TerminateConnectionResult) { + b, err := opts.ToVolumeTerminateConnectionMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(teminateConnectionURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + return +} + +// ExtendSizeOptsBuilder allows extensions to add additional parameters to the +// ExtendSize request. +type ExtendSizeOptsBuilder interface { + ToVolumeExtendSizeMap() (map[string]interface{}, error) +} + +// ExtendSizeOpts contain options for extending the size of an existing Volume. This object is passed +// to the volumes.ExtendSize function. +type ExtendSizeOpts struct { + // NewSize is the new size of the volume, in GB + NewSize int `json:"new_size" required:"true"` +} + +// ToVolumeExtendSizeMap assembles a request body based on the contents of an +// ExtendSizeOpts. +func (opts ExtendSizeOpts) ToVolumeExtendSizeMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "os-extend") +} + +// ExtendSize will extend the size of the volume based on the provided information. +// This operation does not return a response body. +func ExtendSize(client *gophercloud.ServiceClient, id string, opts ExtendSizeOptsBuilder) (r ExtendSizeResult) { + b, err := opts.ToVolumeExtendSizeMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(extendSizeURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + return +} + +// UploadImageOptsBuilder allows extensions to add additional parameters to the +// UploadImage request. +type UploadImageOptsBuilder interface { + ToVolumeUploadImageMap() (map[string]interface{}, error) +} + +// UploadImageOpts contains options for uploading a Volume to image storage. +type UploadImageOpts struct { + // Container format, may be bare, ofv, ova, etc. + ContainerFormat string `json:"container_format,omitempty"` + // Disk format, may be raw, qcow2, vhd, vdi, vmdk, etc. + DiskFormat string `json:"disk_format,omitempty"` + // The name of image that will be stored in glance + ImageName string `json:"image_name,omitempty"` + // Force image creation, usable if volume attached to instance + Force bool `json:"force,omitempty"` +} + +// ToVolumeUploadImageMap assembles a request body based on the contents of a +// UploadImageOpts. +func (opts UploadImageOpts) ToVolumeUploadImageMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "os-volume_upload_image") +} + +// UploadImage will upload image base on the values in UploadImageOptsBuilder +func UploadImage(client *gophercloud.ServiceClient, id string, opts UploadImageOptsBuilder) (r UploadImageResult) { + b, err := opts.ToVolumeUploadImageMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(uploadURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/results.go new file mode 100644 index 0000000000..ae97ac407a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/results.go @@ -0,0 +1,161 @@ +package volumeactions + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud" +) + +// AttachResult contains the response body and error from a Get request. +type AttachResult struct { + gophercloud.ErrResult +} + +// BeginDetachingResult contains the response body and error from a Get request. +type BeginDetachingResult struct { + gophercloud.ErrResult +} + +// DetachResult contains the response body and error from a Get request. +type DetachResult struct { + gophercloud.ErrResult +} + +// UploadImageResult contains the response body and error from a UploadImage request. +type UploadImageResult struct { + gophercloud.Result +} + +// ReserveResult contains the response body and error from a Get request. +type ReserveResult struct { + gophercloud.ErrResult +} + +// UnreserveResult contains the response body and error from a Get request. +type UnreserveResult struct { + gophercloud.ErrResult +} + +// TerminateConnectionResult contains the response body and error from a Get request. +type TerminateConnectionResult struct { + gophercloud.ErrResult +} + +type commonResult struct { + gophercloud.Result +} + +// Extract will get the Volume object out of the commonResult object. +func (r commonResult) Extract() (map[string]interface{}, error) { + var s struct { + ConnectionInfo map[string]interface{} `json:"connection_info"` + } + err := r.ExtractInto(&s) + return s.ConnectionInfo, err +} + +// ImageVolumeType contains volume type object obtained from UploadImage action. +type ImageVolumeType struct { + // The ID of a volume type. + ID string `json:"id"` + // Human-readable display name for the volume type. + Name string `json:"name"` + // Human-readable description for the volume type. + Description string `json:"display_description"` + // Flag for public access. + IsPublic bool `json:"is_public"` + // Extra specifications for volume type. + ExtraSpecs map[string]interface{} `json:"extra_specs"` + // ID of quality of service specs. + QosSpecsID string `json:"qos_specs_id"` + // Flag for deletion status of volume type. + Deleted bool `json:"deleted"` + // The date when volume type was deleted. + DeletedAt time.Time `json:"-"` + // The date when volume type was created. + CreatedAt time.Time `json:"-"` + // The date when this volume was last updated. + UpdatedAt time.Time `json:"-"` +} + +func (r *ImageVolumeType) UnmarshalJSON(b []byte) error { + type tmp ImageVolumeType + var s struct { + tmp + CreatedAt gophercloud.JSONRFC3339MilliNoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339MilliNoZ `json:"updated_at"` + DeletedAt gophercloud.JSONRFC3339MilliNoZ `json:"deleted_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = ImageVolumeType(s.tmp) + + r.CreatedAt = time.Time(s.CreatedAt) + r.UpdatedAt = time.Time(s.UpdatedAt) + r.DeletedAt = time.Time(s.DeletedAt) + + return err +} + +// VolumeImage contains information about volume upload to an image service. +type VolumeImage struct { + // The ID of a volume an image is created from. + VolumeID string `json:"id"` + // Container format, may be bare, ofv, ova, etc. + ContainerFormat string `json:"container_format"` + // Disk format, may be raw, qcow2, vhd, vdi, vmdk, etc. + DiskFormat string `json:"disk_format"` + // Human-readable description for the volume. + Description string `json:"display_description"` + // The ID of an image being created. + ImageID string `json:"image_id"` + // Human-readable display name for the image. + ImageName string `json:"image_name"` + // Size of the volume in GB. + Size int `json:"size"` + // Current status of the volume. + Status string `json:"status"` + // The date when this volume was last updated. + UpdatedAt time.Time `json:"-"` + // Volume type object of used volume. + VolumeType ImageVolumeType `json:"volume_type"` +} + +func (r *VolumeImage) UnmarshalJSON(b []byte) error { + type tmp VolumeImage + var s struct { + tmp + UpdatedAt gophercloud.JSONRFC3339MilliNoZ `json:"updated_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = VolumeImage(s.tmp) + + r.UpdatedAt = time.Time(s.UpdatedAt) + + return err +} + +// Extract will get an object with info about image being uploaded out of the UploadImageResult object. +func (r UploadImageResult) Extract() (VolumeImage, error) { + var s struct { + VolumeImage VolumeImage `json:"os-volume_upload_image"` + } + err := r.ExtractInto(&s) + return s.VolumeImage, err +} + +// InitializeConnectionResult contains the response body and error from a Get request. +type InitializeConnectionResult struct { + commonResult +} + +// ExtendSizeResult contains the response body and error from an ExtendSize request. +type ExtendSizeResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/testing/doc.go new file mode 100644 index 0000000000..e720733136 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/testing/doc.go @@ -0,0 +1,2 @@ +// volumeactions +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/testing/fixtures.go new file mode 100644 index 0000000000..d51c2b6ad4 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/testing/fixtures.go @@ -0,0 +1,278 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +func MockAttachResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes/cd281d77-8217-4830-be95-9528227c105c/action", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "os-attach": + { + "mountpoint": "/mnt", + "mode": "rw", + "instance_uuid": "50902f4f-a974-46a0-85e9-7efc5e22dfdd" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, `{}`) + }) +} + +func MockBeginDetachingResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes/cd281d77-8217-4830-be95-9528227c105c/action", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "os-begin_detaching": {} +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, `{}`) + }) +} + +func MockDetachResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes/cd281d77-8217-4830-be95-9528227c105c/action", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "os-detach": {} +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, `{}`) + }) +} + +func MockUploadImageResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes/cd281d77-8217-4830-be95-9528227c105c/action", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "os-volume_upload_image": { + "container_format": "bare", + "force": true, + "image_name": "test", + "disk_format": "raw" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, ` +{ + "os-volume_upload_image": { + "container_format": "bare", + "display_description": null, + "id": "cd281d77-8217-4830-be95-9528227c105c", + "image_id": "ecb92d98-de08-45db-8235-bbafe317269c", + "image_name": "test", + "disk_format": "raw", + "size": 5, + "status": "uploading", + "updated_at": "2017-07-17T09:29:22.000000", + "volume_type": { + "created_at": "2016-05-04T08:54:14.000000", + "deleted": false, + "deleted_at": null, + "description": null, + "extra_specs": { + "volume_backend_name": "basic.ru-2a" + }, + "id": "b7133444-62f6-4433-8da3-70ac332229b7", + "is_public": true, + "name": "basic.ru-2a", + "updated_at": "2016-05-04T09:15:33.000000" + } + } +} + `) + }) +} + +func MockReserveResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes/cd281d77-8217-4830-be95-9528227c105c/action", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "os-reserve": {} +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, `{}`) + }) +} + +func MockUnreserveResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes/cd281d77-8217-4830-be95-9528227c105c/action", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "os-unreserve": {} +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, `{}`) + }) +} + +func MockInitializeConnectionResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes/cd281d77-8217-4830-be95-9528227c105c/action", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "os-initialize_connection": + { + "connector": + { + "ip":"127.0.0.1", + "host":"stack", + "initiator":"iqn.1994-05.com.redhat:17cf566367d2", + "multipath": false, + "platform": "x86_64", + "os_type": "linux2" + } + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, `{ +"connection_info": { + "data": { + "target_portals": [ + "172.31.17.48:3260" + ], + "auth_method": "CHAP", + "auth_username": "5MLtcsTEmNN5jFVcT6ui", + "access_mode": "rw", + "target_lun": 0, + "volume_id": "cd281d77-8217-4830-be95-9528227c105c", + "target_luns": [ + 0 + ], + "target_iqns": [ + "iqn.2010-10.org.openstack:volume-cd281d77-8217-4830-be95-9528227c105c" + ], + "auth_password": "x854ZY5Re3aCkdNL", + "target_discovered": false, + "encrypted": false, + "qos_specs": null, + "target_iqn": "iqn.2010-10.org.openstack:volume-cd281d77-8217-4830-be95-9528227c105c", + "target_portal": "172.31.17.48:3260" + }, + "driver_volume_type": "iscsi" + } + }`) + }) +} + +func MockTerminateConnectionResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes/cd281d77-8217-4830-be95-9528227c105c/action", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "os-terminate_connection": + { + "connector": + { + "ip":"127.0.0.1", + "host":"stack", + "initiator":"iqn.1994-05.com.redhat:17cf566367d2", + "multipath": true, + "platform": "x86_64", + "os_type": "linux2" + } + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, `{}`) + }) +} + +func MockExtendSizeResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes/cd281d77-8217-4830-be95-9528227c105c/action", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "os-extend": + { + "new_size": 3 + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, `{}`) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/testing/requests_test.go new file mode 100644 index 0000000000..667a3edb81 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/testing/requests_test.go @@ -0,0 +1,156 @@ +package testing + +import ( + "testing" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestAttach(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockAttachResponse(t) + + options := &volumeactions.AttachOpts{ + MountPoint: "/mnt", + Mode: "rw", + InstanceUUID: "50902f4f-a974-46a0-85e9-7efc5e22dfdd", + } + err := volumeactions.Attach(client.ServiceClient(), "cd281d77-8217-4830-be95-9528227c105c", options).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestBeginDetaching(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockBeginDetachingResponse(t) + + err := volumeactions.BeginDetaching(client.ServiceClient(), "cd281d77-8217-4830-be95-9528227c105c").ExtractErr() + th.AssertNoErr(t, err) +} + +func TestDetach(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockDetachResponse(t) + + err := volumeactions.Detach(client.ServiceClient(), "cd281d77-8217-4830-be95-9528227c105c", &volumeactions.DetachOpts{}).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestUploadImage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + MockUploadImageResponse(t) + options := &volumeactions.UploadImageOpts{ + ContainerFormat: "bare", + DiskFormat: "raw", + ImageName: "test", + Force: true, + } + + actual, err := volumeactions.UploadImage(client.ServiceClient(), "cd281d77-8217-4830-be95-9528227c105c", options).Extract() + th.AssertNoErr(t, err) + + expected := volumeactions.VolumeImage{ + VolumeID: "cd281d77-8217-4830-be95-9528227c105c", + ContainerFormat: "bare", + DiskFormat: "raw", + Description: "", + ImageID: "ecb92d98-de08-45db-8235-bbafe317269c", + ImageName: "test", + Size: 5, + Status: "uploading", + UpdatedAt: time.Date(2017, 7, 17, 9, 29, 22, 0, time.UTC), + VolumeType: volumeactions.ImageVolumeType{ + ID: "b7133444-62f6-4433-8da3-70ac332229b7", + Name: "basic.ru-2a", + Description: "", + IsPublic: true, + ExtraSpecs: map[string]interface{}{"volume_backend_name": "basic.ru-2a"}, + QosSpecsID: "", + Deleted: false, + DeletedAt: time.Time{}, + CreatedAt: time.Date(2016, 5, 4, 8, 54, 14, 0, time.UTC), + UpdatedAt: time.Date(2016, 5, 4, 9, 15, 33, 0, time.UTC), + }, + } + th.AssertDeepEquals(t, expected, actual) +} + +func TestReserve(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockReserveResponse(t) + + err := volumeactions.Reserve(client.ServiceClient(), "cd281d77-8217-4830-be95-9528227c105c").ExtractErr() + th.AssertNoErr(t, err) +} + +func TestUnreserve(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockUnreserveResponse(t) + + err := volumeactions.Unreserve(client.ServiceClient(), "cd281d77-8217-4830-be95-9528227c105c").ExtractErr() + th.AssertNoErr(t, err) +} + +func TestInitializeConnection(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockInitializeConnectionResponse(t) + + options := &volumeactions.InitializeConnectionOpts{ + IP: "127.0.0.1", + Host: "stack", + Initiator: "iqn.1994-05.com.redhat:17cf566367d2", + Multipath: gophercloud.Disabled, + Platform: "x86_64", + OSType: "linux2", + } + _, err := volumeactions.InitializeConnection(client.ServiceClient(), "cd281d77-8217-4830-be95-9528227c105c", options).Extract() + th.AssertNoErr(t, err) +} + +func TestTerminateConnection(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockTerminateConnectionResponse(t) + + options := &volumeactions.TerminateConnectionOpts{ + IP: "127.0.0.1", + Host: "stack", + Initiator: "iqn.1994-05.com.redhat:17cf566367d2", + Multipath: gophercloud.Enabled, + Platform: "x86_64", + OSType: "linux2", + } + err := volumeactions.TerminateConnection(client.ServiceClient(), "cd281d77-8217-4830-be95-9528227c105c", options).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestExtendSize(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockExtendSizeResponse(t) + + options := &volumeactions.ExtendSizeOpts{ + NewSize: 3, + } + + err := volumeactions.ExtendSize(client.ServiceClient(), "cd281d77-8217-4830-be95-9528227c105c", options).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/urls.go new file mode 100644 index 0000000000..5efd2b25c0 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/urls.go @@ -0,0 +1,39 @@ +package volumeactions + +import "github.com/gophercloud/gophercloud" + +func attachURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("volumes", id, "action") +} + +func beginDetachingURL(c *gophercloud.ServiceClient, id string) string { + return attachURL(c, id) +} + +func detachURL(c *gophercloud.ServiceClient, id string) string { + return attachURL(c, id) +} + +func uploadURL(c *gophercloud.ServiceClient, id string) string { + return attachURL(c, id) +} + +func reserveURL(c *gophercloud.ServiceClient, id string) string { + return attachURL(c, id) +} + +func unreserveURL(c *gophercloud.ServiceClient, id string) string { + return attachURL(c, id) +} + +func initializeConnectionURL(c *gophercloud.ServiceClient, id string) string { + return attachURL(c, id) +} + +func teminateConnectionURL(c *gophercloud.ServiceClient, id string) string { + return attachURL(c, id) +} + +func extendSizeURL(c *gophercloud.ServiceClient, id string) string { + return attachURL(c, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumetenants/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumetenants/results.go new file mode 100644 index 0000000000..b7d51c72b7 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumetenants/results.go @@ -0,0 +1,12 @@ +package volumetenants + +// VolumeExt is an extension to the base Volume object +type VolumeExt struct { + // TenantID is the id of the project that owns the volume. + TenantID string `json:"os-vol-tenant-attr:tenant_id"` +} + +// UnmarshalJSON to override default +func (r *VolumeExt) UnmarshalJSON(b []byte) error { + return nil +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/apiversions/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/apiversions/doc.go new file mode 100644 index 0000000000..e3af39f513 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/apiversions/doc.go @@ -0,0 +1,3 @@ +// Package apiversions provides information and interaction with the different +// API versions for the OpenStack Block Storage service, code-named Cinder. +package apiversions diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/apiversions/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/apiversions/requests.go new file mode 100644 index 0000000000..725c13a761 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/apiversions/requests.go @@ -0,0 +1,20 @@ +package apiversions + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// List lists all the Cinder API versions available to end-users. +func List(c *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(c, listURL(c), func(r pagination.PageResult) pagination.Page { + return APIVersionPage{pagination.SinglePageBase(r)} + }) +} + +// Get will retrieve the volume type with the provided ID. To extract the volume +// type from the result, call the Extract method on the GetResult. +func Get(client *gophercloud.ServiceClient, v string) (r GetResult) { + _, r.Err = client.Get(getURL(client, v), &r.Body, nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/apiversions/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/apiversions/results.go new file mode 100644 index 0000000000..f510c6d103 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/apiversions/results.go @@ -0,0 +1,49 @@ +package apiversions + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// APIVersion represents an API version for Cinder. +type APIVersion struct { + ID string `json:"id"` // unique identifier + Status string `json:"status"` // current status + Updated string `json:"updated"` // date last updated +} + +// APIVersionPage is the page returned by a pager when traversing over a +// collection of API versions. +type APIVersionPage struct { + pagination.SinglePageBase +} + +// IsEmpty checks whether an APIVersionPage struct is empty. +func (r APIVersionPage) IsEmpty() (bool, error) { + is, err := ExtractAPIVersions(r) + return len(is) == 0, err +} + +// ExtractAPIVersions takes a collection page, extracts all of the elements, +// and returns them a slice of APIVersion structs. It is effectively a cast. +func ExtractAPIVersions(r pagination.Page) ([]APIVersion, error) { + var s struct { + Versions []APIVersion `json:"versions"` + } + err := (r.(APIVersionPage)).ExtractInto(&s) + return s.Versions, err +} + +// GetResult represents the result of a get operation. +type GetResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts an API version resource. +func (r GetResult) Extract() (*APIVersion, error) { + var s struct { + Version *APIVersion `json:"version"` + } + err := r.ExtractInto(&s) + return s.Version, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/apiversions/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/apiversions/testing/doc.go new file mode 100644 index 0000000000..12e4bda0f9 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/apiversions/testing/doc.go @@ -0,0 +1,2 @@ +// apiversions_v1 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/apiversions/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/apiversions/testing/fixtures.go new file mode 100644 index 0000000000..885fdf659a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/apiversions/testing/fixtures.go @@ -0,0 +1,91 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func MockListResponse(t *testing.T) { + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "versions": [ + { + "status": "CURRENT", + "updated": "2012-01-04T11:33:21Z", + "id": "v1.0", + "links": [ + { + "href": "http://23.253.228.211:8776/v1/", + "rel": "self" + } + ] + }, + { + "status": "CURRENT", + "updated": "2012-11-21T11:33:21Z", + "id": "v2.0", + "links": [ + { + "href": "http://23.253.228.211:8776/v2/", + "rel": "self" + } + ] + } + ] + }`) + }) +} + +func MockGetResponse(t *testing.T) { + th.Mux.HandleFunc("/v1/", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "version": { + "status": "CURRENT", + "updated": "2012-01-04T11:33:21Z", + "media-types": [ + { + "base": "application/xml", + "type": "application/vnd.openstack.volume+xml;version=1" + }, + { + "base": "application/json", + "type": "application/vnd.openstack.volume+json;version=1" + } + ], + "id": "v1.0", + "links": [ + { + "href": "http://23.253.228.211:8776/v1/", + "rel": "self" + }, + { + "href": "http://jorgew.github.com/block-storage-api/content/os-block-storage-1.0.pdf", + "type": "application/pdf", + "rel": "describedby" + }, + { + "href": "http://docs.rackspacecloud.com/servers/api/v1.1/application.wadl", + "type": "application/vnd.sun.wadl+xml", + "rel": "describedby" + } + ] + } + }`) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/apiversions/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/apiversions/testing/requests_test.go new file mode 100644 index 0000000000..31034970cf --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/apiversions/testing/requests_test.go @@ -0,0 +1,64 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/blockstorage/v1/apiversions" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestListVersions(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockListResponse(t) + + count := 0 + + apiversions.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := apiversions.ExtractAPIVersions(page) + th.AssertNoErr(t, err) + + expected := []apiversions.APIVersion{ + { + ID: "v1.0", + Status: "CURRENT", + Updated: "2012-01-04T11:33:21Z", + }, + { + ID: "v2.0", + Status: "CURRENT", + Updated: "2012-11-21T11:33:21Z", + }, + } + + th.AssertDeepEquals(t, expected, actual) + + return true, nil + }) + + th.AssertEquals(t, 1, count) +} + +func TestAPIInfo(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockGetResponse(t) + + actual, err := apiversions.Get(client.ServiceClient(), "v1").Extract() + th.AssertNoErr(t, err) + + expected := apiversions.APIVersion{ + ID: "v1.0", + Status: "CURRENT", + Updated: "2012-01-04T11:33:21Z", + } + + th.AssertEquals(t, actual.ID, expected.ID) + th.AssertEquals(t, actual.Status, expected.Status) + th.AssertEquals(t, actual.Updated, expected.Updated) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/apiversions/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/apiversions/urls.go new file mode 100644 index 0000000000..d1861ac196 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/apiversions/urls.go @@ -0,0 +1,18 @@ +package apiversions + +import ( + "net/url" + "strings" + + "github.com/gophercloud/gophercloud" +) + +func getURL(c *gophercloud.ServiceClient, version string) string { + return c.ServiceURL(strings.TrimRight(version, "/") + "/") +} + +func listURL(c *gophercloud.ServiceClient) string { + u, _ := url.Parse(c.ServiceURL("")) + u.Path = "/" + return u.String() +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/snapshots/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/snapshots/doc.go new file mode 100644 index 0000000000..198f83077c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/snapshots/doc.go @@ -0,0 +1,5 @@ +// Package snapshots provides information and interaction with snapshots in the +// OpenStack Block Storage service. A snapshot is a point in time copy of the +// data contained in an external storage volume, and can be controlled +// programmatically. +package snapshots diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/snapshots/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/snapshots/requests.go new file mode 100644 index 0000000000..cb9d0d0e06 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/snapshots/requests.go @@ -0,0 +1,158 @@ +package snapshots + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToSnapshotCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains options for creating a Snapshot. This object is passed to +// the snapshots.Create function. For more information about these parameters, +// see the Snapshot object. +type CreateOpts struct { + VolumeID string `json:"volume_id" required:"true"` + Description string `json:"display_description,omitempty"` + Force bool `json:"force,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` + Name string `json:"display_name,omitempty"` +} + +// ToSnapshotCreateMap assembles a request body based on the contents of a +// CreateOpts. +func (opts CreateOpts) ToSnapshotCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "snapshot") +} + +// Create will create a new Snapshot based on the values in CreateOpts. To +// extract the Snapshot object from the response, call the Extract method on the +// CreateResult. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToSnapshotCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + return +} + +// Delete will delete the existing Snapshot with the provided ID. +func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} + +// Get retrieves the Snapshot with the provided ID. To extract the Snapshot +// object from the response, call the Extract method on the GetResult. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// ListOptsBuilder allows extensions to add additional parameters to the List +// request. +type ListOptsBuilder interface { + ToSnapshotListQuery() (string, error) +} + +// ListOpts hold options for listing Snapshots. It is passed to the +// snapshots.List function. +type ListOpts struct { + Name string `q:"display_name"` + Status string `q:"status"` + VolumeID string `q:"volume_id"` +} + +// ToSnapshotListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToSnapshotListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns Snapshots optionally limited by the conditions provided in +// ListOpts. +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToSnapshotListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return SnapshotPage{pagination.SinglePageBase(r)} + }) +} + +// UpdateMetadataOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateMetadataOptsBuilder interface { + ToSnapshotUpdateMetadataMap() (map[string]interface{}, error) +} + +// UpdateMetadataOpts contain options for updating an existing Snapshot. This +// object is passed to the snapshots.Update function. For more information +// about the parameters, see the Snapshot object. +type UpdateMetadataOpts struct { + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +// ToSnapshotUpdateMetadataMap assembles a request body based on the contents of +// an UpdateMetadataOpts. +func (opts UpdateMetadataOpts) ToSnapshotUpdateMetadataMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "") +} + +// UpdateMetadata will update the Snapshot with provided information. To +// extract the updated Snapshot from the response, call the ExtractMetadata +// method on the UpdateMetadataResult. +func UpdateMetadata(client *gophercloud.ServiceClient, id string, opts UpdateMetadataOptsBuilder) (r UpdateMetadataResult) { + b, err := opts.ToSnapshotUpdateMetadataMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(updateMetadataURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// IDFromName is a convienience function that returns a snapshot's ID given its name. +func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + pages, err := List(client, nil).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractSnapshots(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", gophercloud.ErrResourceNotFound{Name: name, ResourceType: "snapshot"} + case 1: + return id, nil + default: + return "", gophercloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "snapshot"} + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/snapshots/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/snapshots/results.go new file mode 100644 index 0000000000..5282509273 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/snapshots/results.go @@ -0,0 +1,130 @@ +package snapshots + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Snapshot contains all the information associated with an OpenStack Snapshot. +type Snapshot struct { + // Currect status of the Snapshot. + Status string `json:"status"` + + // Display name. + Name string `json:"display_name"` + + // Instances onto which the Snapshot is attached. + Attachments []string `json:"attachments"` + + // Logical group. + AvailabilityZone string `json:"availability_zone"` + + // Is the Snapshot bootable? + Bootable string `json:"bootable"` + + // Date created. + CreatedAt time.Time `json:"-"` + + // Display description. + Description string `json:"display_description"` + + // See VolumeType object for more information. + VolumeType string `json:"volume_type"` + + // ID of the Snapshot from which this Snapshot was created. + SnapshotID string `json:"snapshot_id"` + + // ID of the Volume from which this Snapshot was created. + VolumeID string `json:"volume_id"` + + // User-defined key-value pairs. + Metadata map[string]string `json:"metadata"` + + // Unique identifier. + ID string `json:"id"` + + // Size of the Snapshot, in GB. + Size int `json:"size"` +} + +func (r *Snapshot) UnmarshalJSON(b []byte) error { + type tmp Snapshot + var s struct { + tmp + CreatedAt gophercloud.JSONRFC3339MilliNoZ `json:"created_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Snapshot(s.tmp) + + r.CreatedAt = time.Time(s.CreatedAt) + + return err +} + +// CreateResult contains the response body and error from a Create request. +type CreateResult struct { + commonResult +} + +// GetResult contains the response body and error from a Get request. +type GetResult struct { + commonResult +} + +// DeleteResult contains the response body and error from a Delete request. +type DeleteResult struct { + gophercloud.ErrResult +} + +// SnapshotPage is a pagination.Pager that is returned from a call to the List function. +type SnapshotPage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a SnapshotPage contains no Snapshots. +func (r SnapshotPage) IsEmpty() (bool, error) { + volumes, err := ExtractSnapshots(r) + return len(volumes) == 0, err +} + +// ExtractSnapshots extracts and returns Snapshots. It is used while iterating over a snapshots.List call. +func ExtractSnapshots(r pagination.Page) ([]Snapshot, error) { + var s struct { + Snapshots []Snapshot `json:"snapshots"` + } + err := (r.(SnapshotPage)).ExtractInto(&s) + return s.Snapshots, err +} + +// UpdateMetadataResult contains the response body and error from an UpdateMetadata request. +type UpdateMetadataResult struct { + commonResult +} + +// ExtractMetadata returns the metadata from a response from snapshots.UpdateMetadata. +func (r UpdateMetadataResult) ExtractMetadata() (map[string]interface{}, error) { + if r.Err != nil { + return nil, r.Err + } + m := r.Body.(map[string]interface{})["metadata"] + return m.(map[string]interface{}), nil +} + +type commonResult struct { + gophercloud.Result +} + +// Extract will get the Snapshot object out of the commonResult object. +func (r commonResult) Extract() (*Snapshot, error) { + var s struct { + Snapshot *Snapshot `json:"snapshot"` + } + err := r.ExtractInto(&s) + return s.Snapshot, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/snapshots/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/snapshots/testing/doc.go new file mode 100644 index 0000000000..85c45f4078 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/snapshots/testing/doc.go @@ -0,0 +1,2 @@ +// snapshots_v1 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/snapshots/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/snapshots/testing/fixtures.go new file mode 100644 index 0000000000..21be6f90a0 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/snapshots/testing/fixtures.go @@ -0,0 +1,134 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +func MockListResponse(t *testing.T) { + th.Mux.HandleFunc("/snapshots", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` + { + "snapshots": [ + { + "id": "289da7f8-6440-407c-9fb4-7db01ec49164", + "display_name": "snapshot-001", + "volume_id": "521752a6-acf6-4b2d-bc7a-119f9148cd8c", + "display_description": "Daily Backup", + "status": "available", + "size": 30, + "created_at": "2012-02-14T20:53:07" + }, + { + "id": "96c3bda7-c82a-4f50-be73-ca7621794835", + "display_name": "snapshot-002", + "volume_id": "76b8950a-8594-4e5b-8dce-0dfa9c696358", + "display_description": "Weekly Backup", + "status": "available", + "size": 25, + "created_at": "2012-02-14T20:53:08" + } + ] + } + `) + }) +} + +func MockGetResponse(t *testing.T) { + th.Mux.HandleFunc("/snapshots/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` +{ + "snapshot": { + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "display_name": "snapshot-001", + "display_description": "Daily backup", + "volume_id": "521752a6-acf6-4b2d-bc7a-119f9148cd8c", + "status": "available", + "size": 30, + "created_at": "2012-02-29T03:50:07" + } +} + `) + }) +} + +func MockCreateResponse(t *testing.T) { + th.Mux.HandleFunc("/snapshots", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "snapshot": { + "volume_id": "1234", + "display_name": "snapshot-001" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "snapshot": { + "volume_id": "1234", + "display_name": "snapshot-001", + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "display_description": "Daily backup", + "volume_id": "1234", + "status": "available", + "size": 30, + "created_at": "2012-02-29T03:50:07" + } +} + `) + }) +} + +func MockUpdateMetadataResponse(t *testing.T) { + th.Mux.HandleFunc("/snapshots/123/metadata", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestJSONRequest(t, r, ` + { + "metadata": { + "key": "v1" + } + } + `) + + fmt.Fprintf(w, ` + { + "metadata": { + "key": "v1" + } + } + `) + }) +} + +func MockDeleteResponse(t *testing.T) { + th.Mux.HandleFunc("/snapshots/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/snapshots/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/snapshots/testing/requests_test.go new file mode 100644 index 0000000000..f4056b5b9f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/snapshots/testing/requests_test.go @@ -0,0 +1,116 @@ +package testing + +import ( + "testing" + "time" + + "github.com/gophercloud/gophercloud/openstack/blockstorage/v1/snapshots" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockListResponse(t) + + count := 0 + + snapshots.List(client.ServiceClient(), &snapshots.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := snapshots.ExtractSnapshots(page) + if err != nil { + t.Errorf("Failed to extract snapshots: %v", err) + return false, err + } + + expected := []snapshots.Snapshot{ + { + ID: "289da7f8-6440-407c-9fb4-7db01ec49164", + Name: "snapshot-001", + VolumeID: "521752a6-acf6-4b2d-bc7a-119f9148cd8c", + Status: "available", + Size: 30, + CreatedAt: time.Date(2012, 2, 14, 20, 53, 7, 0, time.UTC), + Description: "Daily Backup", + }, + { + ID: "96c3bda7-c82a-4f50-be73-ca7621794835", + Name: "snapshot-002", + VolumeID: "76b8950a-8594-4e5b-8dce-0dfa9c696358", + Status: "available", + Size: 25, + CreatedAt: time.Date(2012, 2, 14, 20, 53, 8, 0, time.UTC), + Description: "Weekly Backup", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockGetResponse(t) + + v, err := snapshots.Get(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, v.Name, "snapshot-001") + th.AssertEquals(t, v.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockCreateResponse(t) + + options := snapshots.CreateOpts{VolumeID: "1234", Name: "snapshot-001"} + n, err := snapshots.Create(client.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.VolumeID, "1234") + th.AssertEquals(t, n.Name, "snapshot-001") + th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestUpdateMetadata(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockUpdateMetadataResponse(t) + + expected := map[string]interface{}{"key": "v1"} + + options := &snapshots.UpdateMetadataOpts{ + Metadata: map[string]interface{}{ + "key": "v1", + }, + } + + actual, err := snapshots.UpdateMetadata(client.ServiceClient(), "123", options).ExtractMetadata() + + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, actual, expected) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockDeleteResponse(t) + + res := snapshots.Delete(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22") + th.AssertNoErr(t, res.Err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/snapshots/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/snapshots/urls.go new file mode 100644 index 0000000000..7780437493 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/snapshots/urls.go @@ -0,0 +1,27 @@ +package snapshots + +import "github.com/gophercloud/gophercloud" + +func createURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("snapshots") +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("snapshots", id) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return deleteURL(c, id) +} + +func listURL(c *gophercloud.ServiceClient) string { + return createURL(c) +} + +func metadataURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("snapshots", id, "metadata") +} + +func updateMetadataURL(c *gophercloud.ServiceClient, id string) string { + return metadataURL(c, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/snapshots/util.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/snapshots/util.go new file mode 100644 index 0000000000..40fbb827b8 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/snapshots/util.go @@ -0,0 +1,22 @@ +package snapshots + +import ( + "github.com/gophercloud/gophercloud" +) + +// WaitForStatus will continually poll the resource, checking for a particular +// status. It will do this for the amount of seconds defined. +func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error { + return gophercloud.WaitFor(secs, func() (bool, error) { + current, err := Get(c, id).Extract() + if err != nil { + return false, err + } + + if current.Status == status { + return true, nil + } + + return false, nil + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes/doc.go new file mode 100644 index 0000000000..307b8b12d2 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes/doc.go @@ -0,0 +1,5 @@ +// Package volumes provides information and interaction with volumes in the +// OpenStack Block Storage service. A volume is a detachable block storage +// device, akin to a USB hard drive. It can only be attached to one instance at +// a time. +package volumes diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes/requests.go new file mode 100644 index 0000000000..566def5181 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes/requests.go @@ -0,0 +1,167 @@ +package volumes + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToVolumeCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains options for creating a Volume. This object is passed to +// the volumes.Create function. For more information about these parameters, +// see the Volume object. +type CreateOpts struct { + Size int `json:"size" required:"true"` + AvailabilityZone string `json:"availability_zone,omitempty"` + Description string `json:"display_description,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` + Name string `json:"display_name,omitempty"` + SnapshotID string `json:"snapshot_id,omitempty"` + SourceVolID string `json:"source_volid,omitempty"` + ImageID string `json:"imageRef,omitempty"` + VolumeType string `json:"volume_type,omitempty"` +} + +// ToVolumeCreateMap assembles a request body based on the contents of a +// CreateOpts. +func (opts CreateOpts) ToVolumeCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "volume") +} + +// Create will create a new Volume based on the values in CreateOpts. To extract +// the Volume object from the response, call the Extract method on the +// CreateResult. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToVolumeCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + return +} + +// Delete will delete the existing Volume with the provided ID. +func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} + +// Get retrieves the Volume with the provided ID. To extract the Volume object +// from the response, call the Extract method on the GetResult. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// ListOptsBuilder allows extensions to add additional parameters to the List +// request. +type ListOptsBuilder interface { + ToVolumeListQuery() (string, error) +} + +// ListOpts holds options for listing Volumes. It is passed to the volumes.List +// function. +type ListOpts struct { + // admin-only option. Set it to true to see all tenant volumes. + AllTenants bool `q:"all_tenants"` + // List only volumes that contain Metadata. + Metadata map[string]string `q:"metadata"` + // List only volumes that have Name as the display name. + Name string `q:"display_name"` + // List only volumes that have a status of Status. + Status string `q:"status"` +} + +// ToVolumeListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToVolumeListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns Volumes optionally limited by the conditions provided in ListOpts. +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToVolumeListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return VolumePage{pagination.SinglePageBase(r)} + }) +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToVolumeUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contain options for updating an existing Volume. This object is passed +// to the volumes.Update function. For more information about the parameters, see +// the Volume object. +type UpdateOpts struct { + Name string `json:"display_name,omitempty"` + Description string `json:"display_description,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +// ToVolumeUpdateMap assembles a request body based on the contents of an +// UpdateOpts. +func (opts UpdateOpts) ToVolumeUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "volume") +} + +// Update will update the Volume with provided information. To extract the updated +// Volume from the response, call the Extract method on the UpdateResult. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToVolumeUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// IDFromName is a convienience function that returns a server's ID given its name. +func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + pages, err := List(client, nil).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractVolumes(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", gophercloud.ErrResourceNotFound{Name: name, ResourceType: "volume"} + case 1: + return id, nil + default: + return "", gophercloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "volume"} + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes/results.go new file mode 100644 index 0000000000..7f68d14863 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes/results.go @@ -0,0 +1,109 @@ +package volumes + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Volume contains all the information associated with an OpenStack Volume. +type Volume struct { + // Current status of the volume. + Status string `json:"status"` + // Human-readable display name for the volume. + Name string `json:"display_name"` + // Instances onto which the volume is attached. + Attachments []map[string]interface{} `json:"attachments"` + // This parameter is no longer used. + AvailabilityZone string `json:"availability_zone"` + // Indicates whether this is a bootable volume. + Bootable string `json:"bootable"` + // The date when this volume was created. + CreatedAt time.Time `json:"-"` + // Human-readable description for the volume. + Description string `json:"display_description"` + // The type of volume to create, either SATA or SSD. + VolumeType string `json:"volume_type"` + // The ID of the snapshot from which the volume was created + SnapshotID string `json:"snapshot_id"` + // The ID of another block storage volume from which the current volume was created + SourceVolID string `json:"source_volid"` + // Arbitrary key-value pairs defined by the user. + Metadata map[string]string `json:"metadata"` + // Unique identifier for the volume. + ID string `json:"id"` + // Size of the volume in GB. + Size int `json:"size"` +} + +func (r *Volume) UnmarshalJSON(b []byte) error { + type tmp Volume + var s struct { + tmp + CreatedAt gophercloud.JSONRFC3339MilliNoZ `json:"created_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Volume(s.tmp) + + r.CreatedAt = time.Time(s.CreatedAt) + + return err +} + +// CreateResult contains the response body and error from a Create request. +type CreateResult struct { + commonResult +} + +// GetResult contains the response body and error from a Get request. +type GetResult struct { + commonResult +} + +// DeleteResult contains the response body and error from a Delete request. +type DeleteResult struct { + gophercloud.ErrResult +} + +// VolumePage is a pagination.pager that is returned from a call to the List function. +type VolumePage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a VolumePage contains no Volumes. +func (r VolumePage) IsEmpty() (bool, error) { + volumes, err := ExtractVolumes(r) + return len(volumes) == 0, err +} + +// ExtractVolumes extracts and returns Volumes. It is used while iterating over a volumes.List call. +func ExtractVolumes(r pagination.Page) ([]Volume, error) { + var s struct { + Volumes []Volume `json:"volumes"` + } + err := (r.(VolumePage)).ExtractInto(&s) + return s.Volumes, err +} + +// UpdateResult contains the response body and error from an Update request. +type UpdateResult struct { + commonResult +} + +type commonResult struct { + gophercloud.Result +} + +// Extract will get the Volume object out of the commonResult object. +func (r commonResult) Extract() (*Volume, error) { + var s struct { + Volume *Volume `json:"volume"` + } + err := r.ExtractInto(&s) + return s.Volume, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes/testing/doc.go new file mode 100644 index 0000000000..088e43c57f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes/testing/doc.go @@ -0,0 +1,9 @@ +// volumes_v1 +package testing + +/* +This is package created is to hold fixtures (which imports testing), +so that importing volumes package does not inadvertently import testing into production code +More information here: +https://github.com/rackspace/gophercloud/issues/473 +*/ diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes/testing/fixtures.go new file mode 100644 index 0000000000..306901b048 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes/testing/fixtures.go @@ -0,0 +1,127 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +func MockListResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` + { + "volumes": [ + { + "id": "289da7f8-6440-407c-9fb4-7db01ec49164", + "display_name": "vol-001" + }, + { + "id": "96c3bda7-c82a-4f50-be73-ca7621794835", + "display_name": "vol-002" + } + ] + } + `) + }) +} + +func MockGetResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` + { + "volume": { + "id": "521752a6-acf6-4b2d-bc7a-119f9148cd8c", + "display_name": "vol-001", + "display_description": "Another volume.", + "status": "active", + "size": 30, + "volume_type": "289da7f8-6440-407c-9fb4-7db01ec49164", + "metadata": { + "contents": "junk" + }, + "availability_zone": "us-east1", + "bootable": "false", + "snapshot_id": null, + "attachments": [ + { + "attachment_id": "03987cd1-0ad5-40d1-9b2a-7cc48295d4fa", + "id": "47e9ecc5-4045-4ee3-9a4b-d859d546a0cf", + "volume_id": "6c80f8ac-e3e2-480c-8e6e-f1db92fe4bfe", + "server_id": "d1c4788b-9435-42e2-9b81-29f3be1cd01f", + "host_name": "mitaka", + "device": "/" + } + ], + "created_at": "2012-02-14T20:53:07" + } + } + `) + }) +} + +func MockCreateResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "volume": { + "size": 75, + "availability_zone": "us-east1" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "volume": { + "size": 4, + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22" + } +} + `) + }) +} + +func MockDeleteResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) +} + +func MockUpdateResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` + { + "volume": { + "display_name": "vol-002", + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22" + } + } + `) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes/testing/requests_test.go new file mode 100644 index 0000000000..c4ce23a75a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes/testing/requests_test.go @@ -0,0 +1,152 @@ +package testing + +import ( + "testing" + "time" + + "github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockListResponse(t) + + count := 0 + + volumes.List(client.ServiceClient(), &volumes.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := volumes.ExtractVolumes(page) + if err != nil { + t.Errorf("Failed to extract volumes: %v", err) + return false, err + } + + expected := []volumes.Volume{ + { + ID: "289da7f8-6440-407c-9fb4-7db01ec49164", + Name: "vol-001", + }, + { + ID: "96c3bda7-c82a-4f50-be73-ca7621794835", + Name: "vol-002", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestListAll(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockListResponse(t) + + allPages, err := volumes.List(client.ServiceClient(), &volumes.ListOpts{}).AllPages() + th.AssertNoErr(t, err) + actual, err := volumes.ExtractVolumes(allPages) + th.AssertNoErr(t, err) + + expected := []volumes.Volume{ + { + ID: "289da7f8-6440-407c-9fb4-7db01ec49164", + Name: "vol-001", + }, + { + ID: "96c3bda7-c82a-4f50-be73-ca7621794835", + Name: "vol-002", + }, + } + + th.CheckDeepEquals(t, expected, actual) + +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockGetResponse(t) + + actual, err := volumes.Get(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract() + th.AssertNoErr(t, err) + + expected := &volumes.Volume{ + Status: "active", + Name: "vol-001", + Attachments: []map[string]interface{}{ + { + "attachment_id": "03987cd1-0ad5-40d1-9b2a-7cc48295d4fa", + "id": "47e9ecc5-4045-4ee3-9a4b-d859d546a0cf", + "volume_id": "6c80f8ac-e3e2-480c-8e6e-f1db92fe4bfe", + "server_id": "d1c4788b-9435-42e2-9b81-29f3be1cd01f", + "host_name": "mitaka", + "device": "/", + }, + }, + AvailabilityZone: "us-east1", + Bootable: "false", + CreatedAt: time.Date(2012, 2, 14, 20, 53, 07, 0, time.UTC), + Description: "Another volume.", + VolumeType: "289da7f8-6440-407c-9fb4-7db01ec49164", + SnapshotID: "", + SourceVolID: "", + Metadata: map[string]string{ + "contents": "junk", + }, + ID: "521752a6-acf6-4b2d-bc7a-119f9148cd8c", + Size: 30, + } + + th.AssertDeepEquals(t, expected, actual) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockCreateResponse(t) + + options := &volumes.CreateOpts{ + Size: 75, + AvailabilityZone: "us-east1", + } + n, err := volumes.Create(client.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Size, 4) + th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockDeleteResponse(t) + + res := volumes.Delete(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22") + th.AssertNoErr(t, res.Err) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockUpdateResponse(t) + + options := volumes.UpdateOpts{Name: "vol-002"} + v, err := volumes.Update(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22", options).Extract() + th.AssertNoErr(t, err) + th.CheckEquals(t, "vol-002", v.Name) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes/urls.go new file mode 100644 index 0000000000..8a00f97e98 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes/urls.go @@ -0,0 +1,23 @@ +package volumes + +import "github.com/gophercloud/gophercloud" + +func createURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("volumes") +} + +func listURL(c *gophercloud.ServiceClient) string { + return createURL(c) +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("volumes", id) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return deleteURL(c, id) +} + +func updateURL(c *gophercloud.ServiceClient, id string) string { + return deleteURL(c, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes/util.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes/util.go new file mode 100644 index 0000000000..e86c1b4b4e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes/util.go @@ -0,0 +1,22 @@ +package volumes + +import ( + "github.com/gophercloud/gophercloud" +) + +// WaitForStatus will continually poll the resource, checking for a particular +// status. It will do this for the amount of seconds defined. +func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error { + return gophercloud.WaitFor(secs, func() (bool, error) { + current, err := Get(c, id).Extract() + if err != nil { + return false, err + } + + if current.Status == status { + return true, nil + } + + return false, nil + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumetypes/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumetypes/doc.go new file mode 100644 index 0000000000..793084f89b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumetypes/doc.go @@ -0,0 +1,9 @@ +// Package volumetypes provides information and interaction with volume types +// in the OpenStack Block Storage service. A volume type indicates the type of +// a block storage volume, such as SATA, SCSCI, SSD, etc. These can be +// customized or defined by the OpenStack admin. +// +// You can also define extra_specs associated with your volume types. For +// instance, you could have a VolumeType=SATA, with extra_specs (RPM=10000, +// RAID-Level=5) . Extra_specs are defined and customized by the admin. +package volumetypes diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumetypes/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumetypes/requests.go new file mode 100644 index 0000000000..b95c09addd --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumetypes/requests.go @@ -0,0 +1,59 @@ +package volumetypes + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToVolumeTypeCreateMap() (map[string]interface{}, error) +} + +// CreateOpts are options for creating a volume type. +type CreateOpts struct { + // See VolumeType. + ExtraSpecs map[string]interface{} `json:"extra_specs,omitempty"` + // See VolumeType. + Name string `json:"name,omitempty"` +} + +// ToVolumeTypeCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToVolumeTypeCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "volume_type") +} + +// Create will create a new volume. To extract the created volume type object, +// call the Extract method on the CreateResult. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToVolumeTypeCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + return +} + +// Delete will delete the volume type with the provided ID. +func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} + +// Get will retrieve the volume type with the provided ID. To extract the volume +// type from the result, call the Extract method on the GetResult. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// List returns all volume types. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page { + return VolumeTypePage{pagination.SinglePageBase(r)} + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumetypes/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumetypes/results.go new file mode 100644 index 0000000000..2c312385c6 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumetypes/results.go @@ -0,0 +1,61 @@ +package volumetypes + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// VolumeType contains all information associated with an OpenStack Volume Type. +type VolumeType struct { + ExtraSpecs map[string]interface{} `json:"extra_specs"` // user-defined metadata + ID string `json:"id"` // unique identifier + Name string `json:"name"` // display name +} + +// CreateResult contains the response body and error from a Create request. +type CreateResult struct { + commonResult +} + +// GetResult contains the response body and error from a Get request. +type GetResult struct { + commonResult +} + +// DeleteResult contains the response error from a Delete request. +type DeleteResult struct { + gophercloud.ErrResult +} + +// VolumeTypePage is a pagination.Pager that is returned from a call to the List function. +type VolumeTypePage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a VolumeTypePage contains no Volume Types. +func (r VolumeTypePage) IsEmpty() (bool, error) { + volumeTypes, err := ExtractVolumeTypes(r) + return len(volumeTypes) == 0, err +} + +// ExtractVolumeTypes extracts and returns Volume Types. +func ExtractVolumeTypes(r pagination.Page) ([]VolumeType, error) { + var s struct { + VolumeTypes []VolumeType `json:"volume_types"` + } + err := (r.(VolumeTypePage)).ExtractInto(&s) + return s.VolumeTypes, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract will get the Volume Type object out of the commonResult object. +func (r commonResult) Extract() (*VolumeType, error) { + var s struct { + VolumeType *VolumeType `json:"volume_type"` + } + err := r.ExtractInto(&s) + return s.VolumeType, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumetypes/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumetypes/testing/doc.go new file mode 100644 index 0000000000..73834ed731 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumetypes/testing/doc.go @@ -0,0 +1,2 @@ +// volumetypes_v1 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumetypes/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumetypes/testing/fixtures.go new file mode 100644 index 0000000000..0e2715a14f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumetypes/testing/fixtures.go @@ -0,0 +1,60 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +func MockListResponse(t *testing.T) { + th.Mux.HandleFunc("/types", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` + { + "volume_types": [ + { + "id": "289da7f8-6440-407c-9fb4-7db01ec49164", + "name": "vol-type-001", + "extra_specs": { + "capabilities": "gpu" + } + }, + { + "id": "96c3bda7-c82a-4f50-be73-ca7621794835", + "name": "vol-type-002", + "extra_specs": {} + } + ] + } + `) + }) +} + +func MockGetResponse(t *testing.T) { + th.Mux.HandleFunc("/types/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` +{ + "volume_type": { + "name": "vol-type-001", + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "extra_specs": { + "serverNumber": "2" + } + } +} + `) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumetypes/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumetypes/testing/requests_test.go new file mode 100644 index 0000000000..42446151b3 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumetypes/testing/requests_test.go @@ -0,0 +1,119 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumetypes" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockListResponse(t) + + count := 0 + + volumetypes.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := volumetypes.ExtractVolumeTypes(page) + if err != nil { + t.Errorf("Failed to extract volume types: %v", err) + return false, err + } + + expected := []volumetypes.VolumeType{ + { + ID: "289da7f8-6440-407c-9fb4-7db01ec49164", + Name: "vol-type-001", + ExtraSpecs: map[string]interface{}{ + "capabilities": "gpu", + }, + }, + { + ID: "96c3bda7-c82a-4f50-be73-ca7621794835", + Name: "vol-type-002", + ExtraSpecs: map[string]interface{}{}, + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockGetResponse(t) + + vt, err := volumetypes.Get(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract() + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, vt.ExtraSpecs, map[string]interface{}{"serverNumber": "2"}) + th.AssertEquals(t, vt.Name, "vol-type-001") + th.AssertEquals(t, vt.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/types", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "volume_type": { + "name": "vol-type-001" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "volume_type": { + "name": "vol-type-001", + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22" + } +} + `) + }) + + options := &volumetypes.CreateOpts{Name: "vol-type-001"} + n, err := volumetypes.Create(client.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Name, "vol-type-001") + th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/types/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + w.WriteHeader(http.StatusAccepted) + }) + + err := volumetypes.Delete(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumetypes/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumetypes/urls.go new file mode 100644 index 0000000000..822c7dd891 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumetypes/urls.go @@ -0,0 +1,19 @@ +package volumetypes + +import "github.com/gophercloud/gophercloud" + +func listURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("types") +} + +func createURL(c *gophercloud.ServiceClient) string { + return listURL(c) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("types", id) +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return getURL(c, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/snapshots/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/snapshots/doc.go new file mode 100644 index 0000000000..198f83077c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/snapshots/doc.go @@ -0,0 +1,5 @@ +// Package snapshots provides information and interaction with snapshots in the +// OpenStack Block Storage service. A snapshot is a point in time copy of the +// data contained in an external storage volume, and can be controlled +// programmatically. +package snapshots diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/snapshots/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/snapshots/requests.go new file mode 100644 index 0000000000..1f8f81b89a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/snapshots/requests.go @@ -0,0 +1,158 @@ +package snapshots + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToSnapshotCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains options for creating a Snapshot. This object is passed to +// the snapshots.Create function. For more information about these parameters, +// see the Snapshot object. +type CreateOpts struct { + VolumeID string `json:"volume_id" required:"true"` + Force bool `json:"force,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +// ToSnapshotCreateMap assembles a request body based on the contents of a +// CreateOpts. +func (opts CreateOpts) ToSnapshotCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "snapshot") +} + +// Create will create a new Snapshot based on the values in CreateOpts. To +// extract the Snapshot object from the response, call the Extract method on the +// CreateResult. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToSnapshotCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + return +} + +// Delete will delete the existing Snapshot with the provided ID. +func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} + +// Get retrieves the Snapshot with the provided ID. To extract the Snapshot +// object from the response, call the Extract method on the GetResult. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// ListOptsBuilder allows extensions to add additional parameters to the List +// request. +type ListOptsBuilder interface { + ToSnapshotListQuery() (string, error) +} + +// ListOpts hold options for listing Snapshots. It is passed to the +// snapshots.List function. +type ListOpts struct { + Name string `q:"name"` + Status string `q:"status"` + VolumeID string `q:"volume_id"` +} + +// ToSnapshotListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToSnapshotListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns Snapshots optionally limited by the conditions provided in +// ListOpts. +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToSnapshotListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return SnapshotPage{pagination.SinglePageBase(r)} + }) +} + +// UpdateMetadataOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateMetadataOptsBuilder interface { + ToSnapshotUpdateMetadataMap() (map[string]interface{}, error) +} + +// UpdateMetadataOpts contain options for updating an existing Snapshot. This +// object is passed to the snapshots.Update function. For more information +// about the parameters, see the Snapshot object. +type UpdateMetadataOpts struct { + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +// ToSnapshotUpdateMetadataMap assembles a request body based on the contents of +// an UpdateMetadataOpts. +func (opts UpdateMetadataOpts) ToSnapshotUpdateMetadataMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "") +} + +// UpdateMetadata will update the Snapshot with provided information. To +// extract the updated Snapshot from the response, call the ExtractMetadata +// method on the UpdateMetadataResult. +func UpdateMetadata(client *gophercloud.ServiceClient, id string, opts UpdateMetadataOptsBuilder) (r UpdateMetadataResult) { + b, err := opts.ToSnapshotUpdateMetadataMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(updateMetadataURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// IDFromName is a convienience function that returns a snapshot's ID given its name. +func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + pages, err := List(client, nil).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractSnapshots(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", gophercloud.ErrResourceNotFound{Name: name, ResourceType: "snapshot"} + case 1: + return id, nil + default: + return "", gophercloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "snapshot"} + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/snapshots/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/snapshots/results.go new file mode 100644 index 0000000000..0b444d08ad --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/snapshots/results.go @@ -0,0 +1,120 @@ +package snapshots + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Snapshot contains all the information associated with a Cinder Snapshot. +type Snapshot struct { + // Unique identifier. + ID string `json:"id"` + + // Date created. + CreatedAt time.Time `json:"-"` + + // Date updated. + UpdatedAt time.Time `json:"-"` + + // Display name. + Name string `json:"name"` + + // Display description. + Description string `json:"description"` + + // ID of the Volume from which this Snapshot was created. + VolumeID string `json:"volume_id"` + + // Currect status of the Snapshot. + Status string `json:"status"` + + // Size of the Snapshot, in GB. + Size int `json:"size"` + + // User-defined key-value pairs. + Metadata map[string]string `json:"metadata"` +} + +// CreateResult contains the response body and error from a Create request. +type CreateResult struct { + commonResult +} + +// GetResult contains the response body and error from a Get request. +type GetResult struct { + commonResult +} + +// DeleteResult contains the response body and error from a Delete request. +type DeleteResult struct { + gophercloud.ErrResult +} + +// SnapshotPage is a pagination.Pager that is returned from a call to the List function. +type SnapshotPage struct { + pagination.SinglePageBase +} + +func (r *Snapshot) UnmarshalJSON(b []byte) error { + type tmp Snapshot + var s struct { + tmp + CreatedAt gophercloud.JSONRFC3339MilliNoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339MilliNoZ `json:"updated_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Snapshot(s.tmp) + + r.CreatedAt = time.Time(s.CreatedAt) + r.UpdatedAt = time.Time(s.UpdatedAt) + + return err +} + +// IsEmpty returns true if a SnapshotPage contains no Snapshots. +func (r SnapshotPage) IsEmpty() (bool, error) { + volumes, err := ExtractSnapshots(r) + return len(volumes) == 0, err +} + +// ExtractSnapshots extracts and returns Snapshots. It is used while iterating over a snapshots.List call. +func ExtractSnapshots(r pagination.Page) ([]Snapshot, error) { + var s struct { + Snapshots []Snapshot `json:"snapshots"` + } + err := (r.(SnapshotPage)).ExtractInto(&s) + return s.Snapshots, err +} + +// UpdateMetadataResult contains the response body and error from an UpdateMetadata request. +type UpdateMetadataResult struct { + commonResult +} + +// ExtractMetadata returns the metadata from a response from snapshots.UpdateMetadata. +func (r UpdateMetadataResult) ExtractMetadata() (map[string]interface{}, error) { + if r.Err != nil { + return nil, r.Err + } + m := r.Body.(map[string]interface{})["metadata"] + return m.(map[string]interface{}), nil +} + +type commonResult struct { + gophercloud.Result +} + +// Extract will get the Snapshot object out of the commonResult object. +func (r commonResult) Extract() (*Snapshot, error) { + var s struct { + Snapshot *Snapshot `json:"snapshot"` + } + err := r.ExtractInto(&s) + return s.Snapshot, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/snapshots/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/snapshots/testing/doc.go new file mode 100644 index 0000000000..9702a25fb2 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/snapshots/testing/doc.go @@ -0,0 +1,2 @@ +// snapshots_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/snapshots/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/snapshots/testing/fixtures.go new file mode 100644 index 0000000000..9638fa5007 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/snapshots/testing/fixtures.go @@ -0,0 +1,134 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +func MockListResponse(t *testing.T) { + th.Mux.HandleFunc("/snapshots", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` + { + "snapshots": [ + { + "id": "289da7f8-6440-407c-9fb4-7db01ec49164", + "name": "snapshot-001", + "volume_id": "521752a6-acf6-4b2d-bc7a-119f9148cd8c", + "description": "Daily Backup", + "status": "available", + "size": 30, + "created_at": "2017-05-30T03:35:03.000000" + }, + { + "id": "96c3bda7-c82a-4f50-be73-ca7621794835", + "name": "snapshot-002", + "volume_id": "76b8950a-8594-4e5b-8dce-0dfa9c696358", + "description": "Weekly Backup", + "status": "available", + "size": 25, + "created_at": "2017-05-30T03:35:03.000000" + } + ] + } + `) + }) +} + +func MockGetResponse(t *testing.T) { + th.Mux.HandleFunc("/snapshots/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` +{ + "snapshot": { + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "name": "snapshot-001", + "description": "Daily backup", + "volume_id": "521752a6-acf6-4b2d-bc7a-119f9148cd8c", + "status": "available", + "size": 30, + "created_at": "2017-05-30T03:35:03.000000" + } +} + `) + }) +} + +func MockCreateResponse(t *testing.T) { + th.Mux.HandleFunc("/snapshots", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "snapshot": { + "volume_id": "1234", + "name": "snapshot-001" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, ` +{ + "snapshot": { + "volume_id": "1234", + "name": "snapshot-001", + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "description": "Daily backup", + "volume_id": "1234", + "status": "available", + "size": 30, + "created_at": "2017-05-30T03:35:03.000000" + } +} + `) + }) +} + +func MockUpdateMetadataResponse(t *testing.T) { + th.Mux.HandleFunc("/snapshots/123/metadata", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestJSONRequest(t, r, ` + { + "metadata": { + "key": "v1" + } + } + `) + + fmt.Fprintf(w, ` + { + "metadata": { + "key": "v1" + } + } + `) + }) +} + +func MockDeleteResponse(t *testing.T) { + th.Mux.HandleFunc("/snapshots/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/snapshots/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/snapshots/testing/requests_test.go new file mode 100644 index 0000000000..1c44e52c7e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/snapshots/testing/requests_test.go @@ -0,0 +1,116 @@ +package testing + +import ( + "testing" + "time" + + "github.com/gophercloud/gophercloud/openstack/blockstorage/v2/snapshots" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockListResponse(t) + + count := 0 + + snapshots.List(client.ServiceClient(), &snapshots.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := snapshots.ExtractSnapshots(page) + if err != nil { + t.Errorf("Failed to extract snapshots: %v", err) + return false, err + } + + expected := []snapshots.Snapshot{ + { + ID: "289da7f8-6440-407c-9fb4-7db01ec49164", + Name: "snapshot-001", + VolumeID: "521752a6-acf6-4b2d-bc7a-119f9148cd8c", + Status: "available", + Size: 30, + CreatedAt: time.Date(2017, 5, 30, 3, 35, 3, 0, time.UTC), + Description: "Daily Backup", + }, + { + ID: "96c3bda7-c82a-4f50-be73-ca7621794835", + Name: "snapshot-002", + VolumeID: "76b8950a-8594-4e5b-8dce-0dfa9c696358", + Status: "available", + Size: 25, + CreatedAt: time.Date(2017, 5, 30, 3, 35, 3, 0, time.UTC), + Description: "Weekly Backup", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockGetResponse(t) + + v, err := snapshots.Get(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, v.Name, "snapshot-001") + th.AssertEquals(t, v.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockCreateResponse(t) + + options := snapshots.CreateOpts{VolumeID: "1234", Name: "snapshot-001"} + n, err := snapshots.Create(client.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.VolumeID, "1234") + th.AssertEquals(t, n.Name, "snapshot-001") + th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestUpdateMetadata(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockUpdateMetadataResponse(t) + + expected := map[string]interface{}{"key": "v1"} + + options := &snapshots.UpdateMetadataOpts{ + Metadata: map[string]interface{}{ + "key": "v1", + }, + } + + actual, err := snapshots.UpdateMetadata(client.ServiceClient(), "123", options).ExtractMetadata() + + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, actual, expected) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockDeleteResponse(t) + + res := snapshots.Delete(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22") + th.AssertNoErr(t, res.Err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/snapshots/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/snapshots/urls.go new file mode 100644 index 0000000000..7780437493 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/snapshots/urls.go @@ -0,0 +1,27 @@ +package snapshots + +import "github.com/gophercloud/gophercloud" + +func createURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("snapshots") +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("snapshots", id) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return deleteURL(c, id) +} + +func listURL(c *gophercloud.ServiceClient) string { + return createURL(c) +} + +func metadataURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("snapshots", id, "metadata") +} + +func updateMetadataURL(c *gophercloud.ServiceClient, id string) string { + return metadataURL(c, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/snapshots/util.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/snapshots/util.go new file mode 100644 index 0000000000..40fbb827b8 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/snapshots/util.go @@ -0,0 +1,22 @@ +package snapshots + +import ( + "github.com/gophercloud/gophercloud" +) + +// WaitForStatus will continually poll the resource, checking for a particular +// status. It will do this for the amount of seconds defined. +func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error { + return gophercloud.WaitFor(secs, func() (bool, error) { + current, err := Get(c, id).Extract() + if err != nil { + return false, err + } + + if current.Status == status { + return true, nil + } + + return false, nil + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes/doc.go new file mode 100644 index 0000000000..307b8b12d2 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes/doc.go @@ -0,0 +1,5 @@ +// Package volumes provides information and interaction with volumes in the +// OpenStack Block Storage service. A volume is a detachable block storage +// device, akin to a USB hard drive. It can only be attached to one instance at +// a time. +package volumes diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes/requests.go new file mode 100644 index 0000000000..18c9cb272e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes/requests.go @@ -0,0 +1,182 @@ +package volumes + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToVolumeCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains options for creating a Volume. This object is passed to +// the volumes.Create function. For more information about these parameters, +// see the Volume object. +type CreateOpts struct { + // The size of the volume, in GB + Size int `json:"size" required:"true"` + // The availability zone + AvailabilityZone string `json:"availability_zone,omitempty"` + // ConsistencyGroupID is the ID of a consistency group + ConsistencyGroupID string `json:"consistencygroup_id,omitempty"` + // The volume description + Description string `json:"description,omitempty"` + // One or more metadata key and value pairs to associate with the volume + Metadata map[string]string `json:"metadata,omitempty"` + // The volume name + Name string `json:"name,omitempty"` + // the ID of the existing volume snapshot + SnapshotID string `json:"snapshot_id,omitempty"` + // SourceReplica is a UUID of an existing volume to replicate with + SourceReplica string `json:"source_replica,omitempty"` + // the ID of the existing volume + SourceVolID string `json:"source_volid,omitempty"` + // The ID of the image from which you want to create the volume. + // Required to create a bootable volume. + ImageID string `json:"imageRef,omitempty"` + // The associated volume type + VolumeType string `json:"volume_type,omitempty"` +} + +// ToVolumeCreateMap assembles a request body based on the contents of a +// CreateOpts. +func (opts CreateOpts) ToVolumeCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "volume") +} + +// Create will create a new Volume based on the values in CreateOpts. To extract +// the Volume object from the response, call the Extract method on the +// CreateResult. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToVolumeCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + return +} + +// Delete will delete the existing Volume with the provided ID. +func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} + +// Get retrieves the Volume with the provided ID. To extract the Volume object +// from the response, call the Extract method on the GetResult. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// ListOptsBuilder allows extensions to add additional parameters to the List +// request. +type ListOptsBuilder interface { + ToVolumeListQuery() (string, error) +} + +// ListOpts holds options for listing Volumes. It is passed to the volumes.List +// function. +type ListOpts struct { + // admin-only option. Set it to true to see all tenant volumes. + AllTenants bool `q:"all_tenants"` + // List only volumes that contain Metadata. + Metadata map[string]string `q:"metadata"` + // List only volumes that have Name as the display name. + Name string `q:"name"` + // List only volumes that have a status of Status. + Status string `q:"status"` +} + +// ToVolumeListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToVolumeListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns Volumes optionally limited by the conditions provided in ListOpts. +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToVolumeListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return VolumePage{pagination.SinglePageBase(r)} + }) +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToVolumeUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contain options for updating an existing Volume. This object is passed +// to the volumes.Update function. For more information about the parameters, see +// the Volume object. +type UpdateOpts struct { + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +// ToVolumeUpdateMap assembles a request body based on the contents of an +// UpdateOpts. +func (opts UpdateOpts) ToVolumeUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "volume") +} + +// Update will update the Volume with provided information. To extract the updated +// Volume from the response, call the Extract method on the UpdateResult. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToVolumeUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// IDFromName is a convienience function that returns a server's ID given its name. +func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + pages, err := List(client, nil).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractVolumes(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", gophercloud.ErrResourceNotFound{Name: name, ResourceType: "volume"} + case 1: + return id, nil + default: + return "", gophercloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "volume"} + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes/results.go new file mode 100644 index 0000000000..674ec34686 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes/results.go @@ -0,0 +1,154 @@ +package volumes + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +type Attachment struct { + AttachedAt time.Time `json:"-"` + AttachmentID string `json:"attachment_id"` + Device string `json:"device"` + HostName string `json:"host_name"` + ID string `json:"id"` + ServerID string `json:"server_id"` + VolumeID string `json:"volume_id"` +} + +func (r *Attachment) UnmarshalJSON(b []byte) error { + type tmp Attachment + var s struct { + tmp + AttachedAt gophercloud.JSONRFC3339MilliNoZ `json:"attached_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Attachment(s.tmp) + + r.AttachedAt = time.Time(s.AttachedAt) + + return err +} + +// Volume contains all the information associated with an OpenStack Volume. +type Volume struct { + // Unique identifier for the volume. + ID string `json:"id"` + // Current status of the volume. + Status string `json:"status"` + // Size of the volume in GB. + Size int `json:"size"` + // AvailabilityZone is which availability zone the volume is in. + AvailabilityZone string `json:"availability_zone"` + // The date when this volume was created. + CreatedAt time.Time `json:"-"` + // The date when this volume was last updated + UpdatedAt time.Time `json:"-"` + // Instances onto which the volume is attached. + Attachments []Attachment `json:"attachments"` + // Human-readable display name for the volume. + Name string `json:"name"` + // Human-readable description for the volume. + Description string `json:"description"` + // The type of volume to create, either SATA or SSD. + VolumeType string `json:"volume_type"` + // The ID of the snapshot from which the volume was created + SnapshotID string `json:"snapshot_id"` + // The ID of another block storage volume from which the current volume was created + SourceVolID string `json:"source_volid"` + // Arbitrary key-value pairs defined by the user. + Metadata map[string]string `json:"metadata"` + // UserID is the id of the user who created the volume. + UserID string `json:"user_id"` + // Indicates whether this is a bootable volume. + Bootable string `json:"bootable"` + // Encrypted denotes if the volume is encrypted. + Encrypted bool `json:"encrypted"` + // ReplicationStatus is the status of replication. + ReplicationStatus string `json:"replication_status"` + // ConsistencyGroupID is the consistency group ID. + ConsistencyGroupID string `json:"consistencygroup_id"` + // Multiattach denotes if the volume is multi-attach capable. + Multiattach bool `json:"multiattach"` +} + +func (r *Volume) UnmarshalJSON(b []byte) error { + type tmp Volume + var s struct { + tmp + CreatedAt gophercloud.JSONRFC3339MilliNoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339MilliNoZ `json:"updated_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Volume(s.tmp) + + r.CreatedAt = time.Time(s.CreatedAt) + r.UpdatedAt = time.Time(s.UpdatedAt) + + return err +} + +// VolumePage is a pagination.pager that is returned from a call to the List function. +type VolumePage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a ListResult contains no Volumes. +func (r VolumePage) IsEmpty() (bool, error) { + volumes, err := ExtractVolumes(r) + return len(volumes) == 0, err +} + +// ExtractVolumes extracts and returns Volumes. It is used while iterating over a volumes.List call. +func ExtractVolumes(r pagination.Page) ([]Volume, error) { + var s []Volume + err := ExtractVolumesInto(r, &s) + return s, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract will get the Volume object out of the commonResult object. +func (r commonResult) Extract() (*Volume, error) { + var s Volume + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "volume") +} + +func ExtractVolumesInto(r pagination.Page, v interface{}) error { + return r.(VolumePage).Result.ExtractIntoSlicePtr(v, "volumes") +} + +// CreateResult contains the response body and error from a Create request. +type CreateResult struct { + commonResult +} + +// GetResult contains the response body and error from a Get request. +type GetResult struct { + commonResult +} + +// UpdateResult contains the response body and error from an Update request. +type UpdateResult struct { + commonResult +} + +// DeleteResult contains the response body and error from a Delete request. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes/testing/doc.go new file mode 100644 index 0000000000..aa8351ab15 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes/testing/doc.go @@ -0,0 +1,2 @@ +// volumes_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes/testing/fixtures.go new file mode 100644 index 0000000000..44d2ca383c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes/testing/fixtures.go @@ -0,0 +1,203 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +func MockListResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes/detail", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` + { + "volumes": [ + { + "volume_type": "lvmdriver-1", + "created_at": "2015-09-17T03:35:03.000000", + "bootable": "false", + "name": "vol-001", + "os-vol-mig-status-attr:name_id": null, + "consistencygroup_id": null, + "source_volid": null, + "os-volume-replication:driver_data": null, + "multiattach": false, + "snapshot_id": null, + "replication_status": "disabled", + "os-volume-replication:extended_status": null, + "encrypted": false, + "os-vol-host-attr:host": null, + "availability_zone": "nova", + "attachments": [{ + "server_id": "83ec2e3b-4321-422b-8706-a84185f52a0a", + "attachment_id": "05551600-a936-4d4a-ba42-79a037c1-c91a", + "attached_at": "2016-08-06T14:48:20.000000", + "host_name": "foobar", + "volume_id": "d6cacb1a-8b59-4c88-ad90-d70ebb82bb75", + "device": "/dev/vdc", + "id": "d6cacb1a-8b59-4c88-ad90-d70ebb82bb75" + }], + "id": "289da7f8-6440-407c-9fb4-7db01ec49164", + "size": 75, + "user_id": "ff1ce52c03ab433aaba9108c2e3ef541", + "os-vol-tenant-attr:tenant_id": "304dc00909ac4d0da6c62d816bcb3459", + "os-vol-mig-status-attr:migstat": null, + "metadata": {"foo": "bar"}, + "status": "available", + "description": null + }, + { + "volume_type": "lvmdriver-1", + "created_at": "2015-09-17T03:32:29.000000", + "bootable": "false", + "name": "vol-002", + "os-vol-mig-status-attr:name_id": null, + "consistencygroup_id": null, + "source_volid": null, + "os-volume-replication:driver_data": null, + "multiattach": false, + "snapshot_id": null, + "replication_status": "disabled", + "os-volume-replication:extended_status": null, + "encrypted": false, + "os-vol-host-attr:host": null, + "availability_zone": "nova", + "attachments": [], + "id": "96c3bda7-c82a-4f50-be73-ca7621794835", + "size": 75, + "user_id": "ff1ce52c03ab433aaba9108c2e3ef541", + "os-vol-tenant-attr:tenant_id": "304dc00909ac4d0da6c62d816bcb3459", + "os-vol-mig-status-attr:migstat": null, + "metadata": {}, + "status": "available", + "description": null + } + ] +} + `) + }) +} + +func MockGetResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` +{ + "volume": { + "volume_type": "lvmdriver-1", + "created_at": "2015-09-17T03:32:29.000000", + "bootable": "false", + "name": "vol-001", + "os-vol-mig-status-attr:name_id": null, + "consistencygroup_id": null, + "source_volid": null, + "os-volume-replication:driver_data": null, + "multiattach": false, + "snapshot_id": null, + "replication_status": "disabled", + "os-volume-replication:extended_status": null, + "encrypted": false, + "os-vol-host-attr:host": null, + "availability_zone": "nova", + "attachments": [{ + "server_id": "83ec2e3b-4321-422b-8706-a84185f52a0a", + "attachment_id": "05551600-a936-4d4a-ba42-79a037c1-c91a", + "attached_at": "2016-08-06T14:48:20.000000", + "host_name": "foobar", + "volume_id": "d6cacb1a-8b59-4c88-ad90-d70ebb82bb75", + "device": "/dev/vdc", + "id": "d6cacb1a-8b59-4c88-ad90-d70ebb82bb75" + }], + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "size": 75, + "user_id": "ff1ce52c03ab433aaba9108c2e3ef541", + "os-vol-tenant-attr:tenant_id": "304dc00909ac4d0da6c62d816bcb3459", + "os-vol-mig-status-attr:migstat": null, + "metadata": {}, + "status": "available", + "description": null + } +} + `) + }) +} + +func MockCreateResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "volume": { + "name": "vol-001", + "size": 75 + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, ` +{ + "volume": { + "size": 75, + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "metadata": {}, + "created_at": "2015-09-17T03:32:29.044216", + "encrypted": false, + "bootable": "false", + "availability_zone": "nova", + "attachments": [], + "user_id": "ff1ce52c03ab433aaba9108c2e3ef541", + "status": "creating", + "description": null, + "volume_type": "lvmdriver-1", + "name": "vol-001", + "replication_status": "disabled", + "consistencygroup_id": null, + "source_volid": null, + "snapshot_id": null, + "multiattach": false + } +} + `) + }) +} + +func MockDeleteResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusAccepted) + }) +} + +func MockUpdateResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` +{ + "volume": { + "name": "vol-002" + } +} + `) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes/testing/requests_test.go new file mode 100644 index 0000000000..0a18544850 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes/testing/requests_test.go @@ -0,0 +1,257 @@ +package testing + +import ( + "testing" + "time" + + "github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumetenants" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestListWithExtensions(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockListResponse(t) + + count := 0 + + volumes.List(client.ServiceClient(), &volumes.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := volumes.ExtractVolumes(page) + if err != nil { + t.Errorf("Failed to extract volumes: %v", err) + return false, err + } + + expected := []volumes.Volume{ + { + ID: "289da7f8-6440-407c-9fb4-7db01ec49164", + Name: "vol-001", + Attachments: []volumes.Attachment{{ + ServerID: "83ec2e3b-4321-422b-8706-a84185f52a0a", + AttachmentID: "05551600-a936-4d4a-ba42-79a037c1-c91a", + AttachedAt: time.Date(2016, 8, 6, 14, 48, 20, 0, time.UTC), + HostName: "foobar", + VolumeID: "d6cacb1a-8b59-4c88-ad90-d70ebb82bb75", + Device: "/dev/vdc", + ID: "d6cacb1a-8b59-4c88-ad90-d70ebb82bb75", + }}, + AvailabilityZone: "nova", + Bootable: "false", + ConsistencyGroupID: "", + CreatedAt: time.Date(2015, 9, 17, 3, 35, 3, 0, time.UTC), + Description: "", + Encrypted: false, + Metadata: map[string]string{"foo": "bar"}, + Multiattach: false, + //TenantID: "304dc00909ac4d0da6c62d816bcb3459", + //ReplicationDriverData: "", + //ReplicationExtendedStatus: "", + ReplicationStatus: "disabled", + Size: 75, + SnapshotID: "", + SourceVolID: "", + Status: "available", + UserID: "ff1ce52c03ab433aaba9108c2e3ef541", + VolumeType: "lvmdriver-1", + }, + { + ID: "96c3bda7-c82a-4f50-be73-ca7621794835", + Name: "vol-002", + Attachments: []volumes.Attachment{}, + AvailabilityZone: "nova", + Bootable: "false", + ConsistencyGroupID: "", + CreatedAt: time.Date(2015, 9, 17, 3, 32, 29, 0, time.UTC), + Description: "", + Encrypted: false, + Metadata: map[string]string{}, + Multiattach: false, + //TenantID: "304dc00909ac4d0da6c62d816bcb3459", + //ReplicationDriverData: "", + //ReplicationExtendedStatus: "", + ReplicationStatus: "disabled", + Size: 75, + SnapshotID: "", + SourceVolID: "", + Status: "available", + UserID: "ff1ce52c03ab433aaba9108c2e3ef541", + VolumeType: "lvmdriver-1", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestListAllWithExtensions(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockListResponse(t) + + type VolumeWithExt struct { + volumes.Volume + volumetenants.VolumeExt + } + + allPages, err := volumes.List(client.ServiceClient(), &volumes.ListOpts{}).AllPages() + th.AssertNoErr(t, err) + + var actual []VolumeWithExt + err = volumes.ExtractVolumesInto(allPages, &actual) + th.AssertNoErr(t, err) + th.AssertEquals(t, 2, len(actual)) + th.AssertEquals(t, "304dc00909ac4d0da6c62d816bcb3459", actual[0].TenantID) +} + +func TestListAll(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockListResponse(t) + + allPages, err := volumes.List(client.ServiceClient(), &volumes.ListOpts{}).AllPages() + th.AssertNoErr(t, err) + actual, err := volumes.ExtractVolumes(allPages) + th.AssertNoErr(t, err) + + expected := []volumes.Volume{ + { + ID: "289da7f8-6440-407c-9fb4-7db01ec49164", + Name: "vol-001", + Attachments: []volumes.Attachment{{ + ServerID: "83ec2e3b-4321-422b-8706-a84185f52a0a", + AttachmentID: "05551600-a936-4d4a-ba42-79a037c1-c91a", + AttachedAt: time.Date(2016, 8, 6, 14, 48, 20, 0, time.UTC), + HostName: "foobar", + VolumeID: "d6cacb1a-8b59-4c88-ad90-d70ebb82bb75", + Device: "/dev/vdc", + ID: "d6cacb1a-8b59-4c88-ad90-d70ebb82bb75", + }}, + AvailabilityZone: "nova", + Bootable: "false", + ConsistencyGroupID: "", + CreatedAt: time.Date(2015, 9, 17, 3, 35, 3, 0, time.UTC), + Description: "", + Encrypted: false, + Metadata: map[string]string{"foo": "bar"}, + Multiattach: false, + //TenantID: "304dc00909ac4d0da6c62d816bcb3459", + //ReplicationDriverData: "", + //ReplicationExtendedStatus: "", + ReplicationStatus: "disabled", + Size: 75, + SnapshotID: "", + SourceVolID: "", + Status: "available", + UserID: "ff1ce52c03ab433aaba9108c2e3ef541", + VolumeType: "lvmdriver-1", + }, + { + ID: "96c3bda7-c82a-4f50-be73-ca7621794835", + Name: "vol-002", + Attachments: []volumes.Attachment{}, + AvailabilityZone: "nova", + Bootable: "false", + ConsistencyGroupID: "", + CreatedAt: time.Date(2015, 9, 17, 3, 32, 29, 0, time.UTC), + Description: "", + Encrypted: false, + Metadata: map[string]string{}, + Multiattach: false, + //TenantID: "304dc00909ac4d0da6c62d816bcb3459", + //ReplicationDriverData: "", + //ReplicationExtendedStatus: "", + ReplicationStatus: "disabled", + Size: 75, + SnapshotID: "", + SourceVolID: "", + Status: "available", + UserID: "ff1ce52c03ab433aaba9108c2e3ef541", + VolumeType: "lvmdriver-1", + }, + } + + th.CheckDeepEquals(t, expected, actual) + +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockGetResponse(t) + + v, err := volumes.Get(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, v.Name, "vol-001") + th.AssertEquals(t, v.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockCreateResponse(t) + + options := &volumes.CreateOpts{Size: 75, Name: "vol-001"} + n, err := volumes.Create(client.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Size, 75) + th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockDeleteResponse(t) + + res := volumes.Delete(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22") + th.AssertNoErr(t, res.Err) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockUpdateResponse(t) + + options := volumes.UpdateOpts{Name: "vol-002"} + v, err := volumes.Update(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22", options).Extract() + th.AssertNoErr(t, err) + th.CheckEquals(t, "vol-002", v.Name) +} + +func TestGetWithExtensions(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockGetResponse(t) + + var s struct { + volumes.Volume + volumetenants.VolumeExt + } + err := volumes.Get(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").ExtractInto(&s) + th.AssertNoErr(t, err) + th.AssertEquals(t, "304dc00909ac4d0da6c62d816bcb3459", s.TenantID) + + err = volumes.Get(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").ExtractInto(s) + if err == nil { + t.Errorf("Expected error when providing non-pointer struct") + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes/urls.go new file mode 100644 index 0000000000..170724905a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes/urls.go @@ -0,0 +1,23 @@ +package volumes + +import "github.com/gophercloud/gophercloud" + +func createURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("volumes") +} + +func listURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("volumes", "detail") +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("volumes", id) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return deleteURL(c, id) +} + +func updateURL(c *gophercloud.ServiceClient, id string) string { + return deleteURL(c, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes/util.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes/util.go new file mode 100644 index 0000000000..e86c1b4b4e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes/util.go @@ -0,0 +1,22 @@ +package volumes + +import ( + "github.com/gophercloud/gophercloud" +) + +// WaitForStatus will continually poll the resource, checking for a particular +// status. It will do this for the amount of seconds defined. +func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error { + return gophercloud.WaitFor(secs, func() (bool, error) { + current, err := Get(c, id).Extract() + if err != nil { + return false, err + } + + if current.Status == status { + return true, nil + } + + return false, nil + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/base/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/base/doc.go new file mode 100644 index 0000000000..f78d4f7355 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/base/doc.go @@ -0,0 +1,4 @@ +// Package base provides information and interaction with the base API +// resource in the OpenStack CDN service. This API resource allows for +// retrieving the Home Document and pinging the root URL. +package base diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/base/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/base/requests.go new file mode 100644 index 0000000000..34d3b724fb --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/base/requests.go @@ -0,0 +1,19 @@ +package base + +import "github.com/gophercloud/gophercloud" + +// Get retrieves the home document, allowing the user to discover the +// entire API. +func Get(c *gophercloud.ServiceClient) (r GetResult) { + _, r.Err = c.Get(getURL(c), &r.Body, nil) + return +} + +// Ping retrieves a ping to the server. +func Ping(c *gophercloud.ServiceClient) (r PingResult) { + _, r.Err = c.Get(pingURL(c), nil, &gophercloud.RequestOpts{ + OkCodes: []int{204}, + MoreHeaders: map[string]string{"Accept": ""}, + }) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/base/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/base/results.go new file mode 100644 index 0000000000..2dfde7dca3 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/base/results.go @@ -0,0 +1,23 @@ +package base + +import "github.com/gophercloud/gophercloud" + +// HomeDocument is a resource that contains all the resources for the CDN API. +type HomeDocument map[string]interface{} + +// GetResult represents the result of a Get operation. +type GetResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a home document resource. +func (r GetResult) Extract() (*HomeDocument, error) { + var s HomeDocument + err := r.ExtractInto(&s) + return &s, err +} + +// PingResult represents the result of a Ping operation. +type PingResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/base/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/base/testing/doc.go new file mode 100644 index 0000000000..891c69a215 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/base/testing/doc.go @@ -0,0 +1,2 @@ +// cdn_base_v1 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/base/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/base/testing/fixtures.go new file mode 100644 index 0000000000..f1f4ac0047 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/base/testing/fixtures.go @@ -0,0 +1,53 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +// HandleGetSuccessfully creates an HTTP handler at `/` on the test handler mux +// that responds with a `Get` response. +func HandleGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` + { + "resources": { + "rel/cdn": { + "href-template": "services{?marker,limit}", + "href-vars": { + "marker": "param/marker", + "limit": "param/limit" + }, + "hints": { + "allow": [ + "GET" + ], + "formats": { + "application/json": {} + } + } + } + } + } + `) + + }) +} + +// HandlePingSuccessfully creates an HTTP handler at `/ping` on the test handler +// mux that responds with a `Ping` response. +func HandlePingSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/base/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/base/testing/requests_test.go new file mode 100644 index 0000000000..9c9517e3a0 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/base/testing/requests_test.go @@ -0,0 +1,46 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/cdn/v1/base" + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestGetHomeDocument(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetSuccessfully(t) + + actual, err := base.Get(fake.ServiceClient()).Extract() + th.CheckNoErr(t, err) + + expected := base.HomeDocument{ + "resources": map[string]interface{}{ + "rel/cdn": map[string]interface{}{ + "href-template": "services{?marker,limit}", + "href-vars": map[string]interface{}{ + "marker": "param/marker", + "limit": "param/limit", + }, + "hints": map[string]interface{}{ + "allow": []interface{}{"GET"}, + "formats": map[string]interface{}{ + "application/json": map[string]interface{}{}, + }, + }, + }, + }, + } + th.CheckDeepEquals(t, expected, *actual) +} + +func TestPing(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandlePingSuccessfully(t) + + err := base.Ping(fake.ServiceClient()).ExtractErr() + th.CheckNoErr(t, err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/base/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/base/urls.go new file mode 100644 index 0000000000..07d892ba93 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/base/urls.go @@ -0,0 +1,11 @@ +package base + +import "github.com/gophercloud/gophercloud" + +func getURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL() +} + +func pingURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("ping") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/flavors/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/flavors/doc.go new file mode 100644 index 0000000000..d4066985cb --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/flavors/doc.go @@ -0,0 +1,6 @@ +// Package flavors provides information and interaction with the flavors API +// resource in the OpenStack CDN service. This API resource allows for +// listing flavors and retrieving a specific flavor. +// +// A flavor is a mapping configuration to a CDN provider. +package flavors diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/flavors/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/flavors/requests.go new file mode 100644 index 0000000000..1977fe365a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/flavors/requests.go @@ -0,0 +1,19 @@ +package flavors + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// List returns a single page of CDN flavors. +func List(c *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(c, listURL(c), func(r pagination.PageResult) pagination.Page { + return FlavorPage{pagination.SinglePageBase(r)} + }) +} + +// Get retrieves a specific flavor based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(getURL(c, id), &r.Body, nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/flavors/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/flavors/results.go new file mode 100644 index 0000000000..02c285134b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/flavors/results.go @@ -0,0 +1,60 @@ +package flavors + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Provider represents a provider for a particular flavor. +type Provider struct { + // Specifies the name of the provider. The name must not exceed 64 bytes in + // length and is limited to unicode, digits, underscores, and hyphens. + Provider string `json:"provider"` + // Specifies a list with an href where rel is provider_url. + Links []gophercloud.Link `json:"links"` +} + +// Flavor represents a mapping configuration to a CDN provider. +type Flavor struct { + // Specifies the name of the flavor. The name must not exceed 64 bytes in + // length and is limited to unicode, digits, underscores, and hyphens. + ID string `json:"id"` + // Specifies the list of providers mapped to this flavor. + Providers []Provider `json:"providers"` + // Specifies the self-navigating JSON document paths. + Links []gophercloud.Link `json:"links"` +} + +// FlavorPage is the page returned by a pager when traversing over a +// collection of CDN flavors. +type FlavorPage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a FlavorPage contains no Flavors. +func (r FlavorPage) IsEmpty() (bool, error) { + flavors, err := ExtractFlavors(r) + return len(flavors) == 0, err +} + +// ExtractFlavors extracts and returns Flavors. It is used while iterating over +// a flavors.List call. +func ExtractFlavors(r pagination.Page) ([]Flavor, error) { + var s struct { + Flavors []Flavor `json:"flavors"` + } + err := (r.(FlavorPage)).ExtractInto(&s) + return s.Flavors, err +} + +// GetResult represents the result of a get operation. +type GetResult struct { + gophercloud.Result +} + +// Extract is a function that extracts a flavor from a GetResult. +func (r GetResult) Extract() (*Flavor, error) { + var s *Flavor + err := r.ExtractInto(&s) + return s, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/flavors/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/flavors/testing/doc.go new file mode 100644 index 0000000000..567b67e237 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/flavors/testing/doc.go @@ -0,0 +1,2 @@ +// cdn_flavors_v1 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/flavors/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/flavors/testing/fixtures.go new file mode 100644 index 0000000000..ed97247e2e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/flavors/testing/fixtures.go @@ -0,0 +1,82 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +// HandleListCDNFlavorsSuccessfully creates an HTTP handler at `/flavors` on the test handler mux +// that responds with a `List` response. +func HandleListCDNFlavorsSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/flavors", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` + { + "flavors": [ + { + "id": "europe", + "providers": [ + { + "provider": "Fastly", + "links": [ + { + "href": "http://www.fastly.com", + "rel": "provider_url" + } + ] + } + ], + "links": [ + { + "href": "https://www.poppycdn.io/v1.0/flavors/europe", + "rel": "self" + } + ] + } + ] + } + `) + }) +} + +// HandleGetCDNFlavorSuccessfully creates an HTTP handler at `/flavors/{id}` on the test handler mux +// that responds with a `Get` response. +func HandleGetCDNFlavorSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/flavors/asia", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` + { + "id" : "asia", + "providers" : [ + { + "provider" : "ChinaCache", + "links": [ + { + "href": "http://www.chinacache.com", + "rel": "provider_url" + } + ] + } + ], + "links": [ + { + "href": "https://www.poppycdn.io/v1.0/flavors/asia", + "rel": "self" + } + ] + } + `) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/flavors/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/flavors/testing/requests_test.go new file mode 100644 index 0000000000..bc4b1a50e5 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/flavors/testing/requests_test.go @@ -0,0 +1,90 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/cdn/v1/flavors" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleListCDNFlavorsSuccessfully(t) + + count := 0 + + err := flavors.List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := flavors.ExtractFlavors(page) + if err != nil { + t.Errorf("Failed to extract flavors: %v", err) + return false, err + } + + expected := []flavors.Flavor{ + { + ID: "europe", + Providers: []flavors.Provider{ + { + Provider: "Fastly", + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "http://www.fastly.com", + Rel: "provider_url", + }, + }, + }, + }, + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "https://www.poppycdn.io/v1.0/flavors/europe", + Rel: "self", + }, + }, + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleGetCDNFlavorSuccessfully(t) + + expected := &flavors.Flavor{ + ID: "asia", + Providers: []flavors.Provider{ + { + Provider: "ChinaCache", + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "http://www.chinacache.com", + Rel: "provider_url", + }, + }, + }, + }, + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "https://www.poppycdn.io/v1.0/flavors/asia", + Rel: "self", + }, + }, + } + + actual, err := flavors.Get(fake.ServiceClient(), "asia").Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expected, actual) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/flavors/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/flavors/urls.go new file mode 100644 index 0000000000..a8540a2aed --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/flavors/urls.go @@ -0,0 +1,11 @@ +package flavors + +import "github.com/gophercloud/gophercloud" + +func listURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("flavors") +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("flavors", id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/serviceassets/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/serviceassets/doc.go new file mode 100644 index 0000000000..ceecaa5a5e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/serviceassets/doc.go @@ -0,0 +1,7 @@ +// Package serviceassets provides information and interaction with the +// serviceassets API resource in the OpenStack CDN service. This API resource +// allows for deleting cached assets. +// +// A service distributes assets across the network. Service assets let you +// interrogate properties about these assets and perform certain actions on them. +package serviceassets diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/serviceassets/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/serviceassets/requests.go new file mode 100644 index 0000000000..80c908fd9d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/serviceassets/requests.go @@ -0,0 +1,51 @@ +package serviceassets + +import ( + "strings" + + "github.com/gophercloud/gophercloud" +) + +// DeleteOptsBuilder allows extensions to add additional parameters to the Delete +// request. +type DeleteOptsBuilder interface { + ToCDNAssetDeleteParams() (string, error) +} + +// DeleteOpts is a structure that holds options for deleting CDN service assets. +type DeleteOpts struct { + // If all is set to true, specifies that the delete occurs against all of the + // assets for the service. + All bool `q:"all"` + // Specifies the relative URL of the asset to be deleted. + URL string `q:"url"` +} + +// ToCDNAssetDeleteParams formats a DeleteOpts into a query string. +func (opts DeleteOpts) ToCDNAssetDeleteParams() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// Delete accepts a unique service ID or URL and deletes the CDN service asset associated with +// it. For example, both "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" and +// "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" +// are valid options for idOrURL. +func Delete(c *gophercloud.ServiceClient, idOrURL string, opts DeleteOptsBuilder) (r DeleteResult) { + var url string + if strings.Contains(idOrURL, "/") { + url = idOrURL + } else { + url = deleteURL(c, idOrURL) + } + if opts != nil { + q, err := opts.ToCDNAssetDeleteParams() + if err != nil { + r.Err = err + return + } + url += q + } + _, r.Err = c.Delete(url, nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/serviceassets/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/serviceassets/results.go new file mode 100644 index 0000000000..b6114c6893 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/serviceassets/results.go @@ -0,0 +1,8 @@ +package serviceassets + +import "github.com/gophercloud/gophercloud" + +// DeleteResult represents the result of a Delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/serviceassets/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/serviceassets/testing/doc.go new file mode 100644 index 0000000000..1adb681a28 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/serviceassets/testing/doc.go @@ -0,0 +1,2 @@ +// cdn_serviceassets_v1 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/serviceassets/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/serviceassets/testing/fixtures.go new file mode 100644 index 0000000000..3172d30fd1 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/serviceassets/testing/fixtures.go @@ -0,0 +1,19 @@ +package testing + +import ( + "net/http" + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +// HandleDeleteCDNAssetSuccessfully creates an HTTP handler at `/services/{id}/assets` on the test handler mux +// that responds with a `Delete` response. +func HandleDeleteCDNAssetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0/assets", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/serviceassets/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/serviceassets/testing/requests_test.go new file mode 100644 index 0000000000..ff2073bd86 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/serviceassets/testing/requests_test.go @@ -0,0 +1,19 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/cdn/v1/serviceassets" + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleDeleteCDNAssetSuccessfully(t) + + err := serviceassets.Delete(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", nil).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/serviceassets/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/serviceassets/urls.go new file mode 100644 index 0000000000..ce1741826a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/serviceassets/urls.go @@ -0,0 +1,7 @@ +package serviceassets + +import "github.com/gophercloud/gophercloud" + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("services", id, "assets") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/services/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/services/doc.go new file mode 100644 index 0000000000..41f7c60dae --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/services/doc.go @@ -0,0 +1,7 @@ +// Package services provides information and interaction with the services API +// resource in the OpenStack CDN service. This API resource allows for +// listing, creating, updating, retrieving, and deleting services. +// +// A service represents an application that has its content cached to the edge +// nodes. +package services diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/services/errors.go b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/services/errors.go new file mode 100644 index 0000000000..359584c2a6 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/services/errors.go @@ -0,0 +1,7 @@ +package services + +import "fmt" + +func no(str string) error { + return fmt.Errorf("Required parameter %s not provided", str) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/services/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/services/requests.go new file mode 100644 index 0000000000..4c0c626606 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/services/requests.go @@ -0,0 +1,285 @@ +package services + +import ( + "fmt" + "strings" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToCDNServiceListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Marker and Limit are used for pagination. +type ListOpts struct { + Marker string `q:"marker"` + Limit int `q:"limit"` +} + +// ToCDNServiceListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToCDNServiceListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// CDN services. It accepts a ListOpts struct, which allows for pagination via +// marker and limit. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToCDNServiceListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + p := ServicePage{pagination.MarkerPageBase{PageResult: r}} + p.MarkerPageBase.Owner = p + return p + }) +} + +// CreateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Create operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type CreateOptsBuilder interface { + ToCDNServiceCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts struct { + // Specifies the name of the service. The minimum length for name is + // 3. The maximum length is 256. + Name string `json:"name" required:"true"` + // Specifies a list of domains used by users to access their website. + Domains []Domain `json:"domains" required:"true"` + // Specifies a list of origin domains or IP addresses where the + // original assets are stored. + Origins []Origin `json:"origins" required:"true"` + // Specifies the CDN provider flavor ID to use. For a list of + // flavors, see the operation to list the available flavors. The minimum + // length for flavor_id is 1. The maximum length is 256. + FlavorID string `json:"flavor_id" required:"true"` + // Specifies the TTL rules for the assets under this service. Supports wildcards for fine-grained control. + Caching []CacheRule `json:"caching,omitempty"` + // Specifies the restrictions that define who can access assets (content from the CDN cache). + Restrictions []Restriction `json:"restrictions,omitempty"` +} + +// ToCDNServiceCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToCDNServiceCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "") +} + +// Create accepts a CreateOpts struct and creates a new CDN service using the +// values provided. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToCDNServiceCreateMap() + if err != nil { + r.Err = err + return r + } + resp, err := c.Post(createURL(c), &b, nil, nil) + r.Header = resp.Header + r.Err = err + return +} + +// Get retrieves a specific service based on its URL or its unique ID. For +// example, both "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" and +// "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" +// are valid options for idOrURL. +func Get(c *gophercloud.ServiceClient, idOrURL string) (r GetResult) { + var url string + if strings.Contains(idOrURL, "/") { + url = idOrURL + } else { + url = getURL(c, idOrURL) + } + _, r.Err = c.Get(url, &r.Body, nil) + return +} + +// Path is a JSON pointer location that indicates which service parameter is being added, replaced, +// or removed. +type Path struct { + baseElement string +} + +func (p Path) renderRoot() string { + return "/" + p.baseElement +} + +func (p Path) renderDash() string { + return fmt.Sprintf("/%s/-", p.baseElement) +} + +func (p Path) renderIndex(index int64) string { + return fmt.Sprintf("/%s/%d", p.baseElement, index) +} + +var ( + // PathDomains indicates that an update operation is to be performed on a Domain. + PathDomains = Path{baseElement: "domains"} + + // PathOrigins indicates that an update operation is to be performed on an Origin. + PathOrigins = Path{baseElement: "origins"} + + // PathCaching indicates that an update operation is to be performed on a CacheRule. + PathCaching = Path{baseElement: "caching"} +) + +type value interface { + toPatchValue() interface{} + appropriatePath() Path + renderRootOr(func(p Path) string) string +} + +// Patch represents a single update to an existing Service. Multiple updates to a service can be +// submitted at the same time. +type Patch interface { + ToCDNServiceUpdateMap() map[string]interface{} +} + +// Insertion is a Patch that requests the addition of a value (Domain, Origin, or CacheRule) to +// a Service at a fixed index. Use an Append instead to append the new value to the end of its +// collection. Pass it to the Update function as part of the Patch slice. +type Insertion struct { + Index int64 + Value value +} + +// ToCDNServiceUpdateMap converts an Insertion into a request body fragment suitable for the +// Update call. +func (opts Insertion) ToCDNServiceUpdateMap() map[string]interface{} { + return map[string]interface{}{ + "op": "add", + "path": opts.Value.renderRootOr(func(p Path) string { return p.renderIndex(opts.Index) }), + "value": opts.Value.toPatchValue(), + } +} + +// Append is a Patch that requests the addition of a value (Domain, Origin, or CacheRule) to a +// Service at the end of its respective collection. Use an Insertion instead to insert the value +// at a fixed index within the collection. Pass this to the Update function as part of its +// Patch slice. +type Append struct { + Value value +} + +// ToCDNServiceUpdateMap converts an Append into a request body fragment suitable for the +// Update call. +func (a Append) ToCDNServiceUpdateMap() map[string]interface{} { + return map[string]interface{}{ + "op": "add", + "path": a.Value.renderRootOr(func(p Path) string { return p.renderDash() }), + "value": a.Value.toPatchValue(), + } +} + +// Replacement is a Patch that alters a specific service parameter (Domain, Origin, or CacheRule) +// in-place by index. Pass it to the Update function as part of the Patch slice. +type Replacement struct { + Value value + Index int64 +} + +// ToCDNServiceUpdateMap converts a Replacement into a request body fragment suitable for the +// Update call. +func (r Replacement) ToCDNServiceUpdateMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": r.Value.renderRootOr(func(p Path) string { return p.renderIndex(r.Index) }), + "value": r.Value.toPatchValue(), + } +} + +// NameReplacement specifically updates the Service name. Pass it to the Update function as part +// of the Patch slice. +type NameReplacement struct { + NewName string +} + +// ToCDNServiceUpdateMap converts a NameReplacement into a request body fragment suitable for the +// Update call. +func (r NameReplacement) ToCDNServiceUpdateMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/name", + "value": r.NewName, + } +} + +// Removal is a Patch that requests the removal of a service parameter (Domain, Origin, or +// CacheRule) by index. Pass it to the Update function as part of the Patch slice. +type Removal struct { + Path Path + Index int64 + All bool +} + +// ToCDNServiceUpdateMap converts a Removal into a request body fragment suitable for the +// Update call. +func (opts Removal) ToCDNServiceUpdateMap() map[string]interface{} { + b := map[string]interface{}{"op": "remove"} + if opts.All { + b["path"] = opts.Path.renderRoot() + } else { + b["path"] = opts.Path.renderIndex(opts.Index) + } + return b +} + +// UpdateOpts is a slice of Patches used to update a CDN service +type UpdateOpts []Patch + +// Update accepts a slice of Patch operations (Insertion, Append, Replacement or Removal) and +// updates an existing CDN service using the values provided. idOrURL can be either the service's +// URL or its ID. For example, both "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" and +// "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" +// are valid options for idOrURL. +func Update(c *gophercloud.ServiceClient, idOrURL string, opts UpdateOpts) (r UpdateResult) { + var url string + if strings.Contains(idOrURL, "/") { + url = idOrURL + } else { + url = updateURL(c, idOrURL) + } + + b := make([]map[string]interface{}, len(opts)) + for i, patch := range opts { + b[i] = patch.ToCDNServiceUpdateMap() + } + + resp, err := c.Request("PATCH", url, &gophercloud.RequestOpts{ + JSONBody: &b, + OkCodes: []int{202}, + }) + r.Header = resp.Header + r.Err = err + return +} + +// Delete accepts a service's ID or its URL and deletes the CDN service +// associated with it. For example, both "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" and +// "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" +// are valid options for idOrURL. +func Delete(c *gophercloud.ServiceClient, idOrURL string) (r DeleteResult) { + var url string + if strings.Contains(idOrURL, "/") { + url = idOrURL + } else { + url = deleteURL(c, idOrURL) + } + _, r.Err = c.Delete(url, nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/services/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/services/results.go new file mode 100644 index 0000000000..f9a1caae73 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/services/results.go @@ -0,0 +1,304 @@ +package services + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Domain represents a domain used by users to access their website. +type Domain struct { + // Specifies the domain used to access the assets on their website, for which + // a CNAME is given to the CDN provider. + Domain string `json:"domain" required:"true"` + // Specifies the protocol used to access the assets on this domain. Only "http" + // or "https" are currently allowed. The default is "http". + Protocol string `json:"protocol,omitempty"` +} + +func (d Domain) toPatchValue() interface{} { + r := make(map[string]interface{}) + r["domain"] = d.Domain + if d.Protocol != "" { + r["protocol"] = d.Protocol + } + return r +} + +func (d Domain) appropriatePath() Path { + return PathDomains +} + +func (d Domain) renderRootOr(render func(p Path) string) string { + return render(d.appropriatePath()) +} + +// DomainList provides a useful way to perform bulk operations in a single Patch. +type DomainList []Domain + +func (list DomainList) toPatchValue() interface{} { + r := make([]interface{}, len(list)) + for i, domain := range list { + r[i] = domain.toPatchValue() + } + return r +} + +func (list DomainList) appropriatePath() Path { + return PathDomains +} + +func (list DomainList) renderRootOr(_ func(p Path) string) string { + return list.appropriatePath().renderRoot() +} + +// OriginRule represents a rule that defines when an origin should be accessed. +type OriginRule struct { + // Specifies the name of this rule. + Name string `json:"name" required:"true"` + // Specifies the request URL this rule should match for this origin to be used. Regex is supported. + RequestURL string `json:"request_url" required:"true"` +} + +// Origin specifies a list of origin domains or IP addresses where the original assets are stored. +type Origin struct { + // Specifies the URL or IP address to pull origin content from. + Origin string `json:"origin" required:"true"` + // Specifies the port used to access the origin. The default is port 80. + Port int `json:"port,omitempty"` + // Specifies whether or not to use HTTPS to access the origin. The default + // is false. + SSL bool `json:"ssl"` + // Specifies a collection of rules that define the conditions when this origin + // should be accessed. If there is more than one origin, the rules parameter is required. + Rules []OriginRule `json:"rules,omitempty"` +} + +func (o Origin) toPatchValue() interface{} { + r := make(map[string]interface{}) + r["origin"] = o.Origin + r["port"] = o.Port + r["ssl"] = o.SSL + if len(o.Rules) > 0 { + r["rules"] = make([]map[string]interface{}, len(o.Rules)) + for index, rule := range o.Rules { + submap := r["rules"].([]map[string]interface{})[index] + submap["name"] = rule.Name + submap["request_url"] = rule.RequestURL + } + } + return r +} + +func (o Origin) appropriatePath() Path { + return PathOrigins +} + +func (o Origin) renderRootOr(render func(p Path) string) string { + return render(o.appropriatePath()) +} + +// OriginList provides a useful way to perform bulk operations in a single Patch. +type OriginList []Origin + +func (list OriginList) toPatchValue() interface{} { + r := make([]interface{}, len(list)) + for i, origin := range list { + r[i] = origin.toPatchValue() + } + return r +} + +func (list OriginList) appropriatePath() Path { + return PathOrigins +} + +func (list OriginList) renderRootOr(_ func(p Path) string) string { + return list.appropriatePath().renderRoot() +} + +// TTLRule specifies a rule that determines if a TTL should be applied to an asset. +type TTLRule struct { + // Specifies the name of this rule. + Name string `json:"name" required:"true"` + // Specifies the request URL this rule should match for this TTL to be used. Regex is supported. + RequestURL string `json:"request_url" required:"true"` +} + +// CacheRule specifies the TTL rules for the assets under this service. +type CacheRule struct { + // Specifies the name of this caching rule. Note: 'default' is a reserved name used for the default TTL setting. + Name string `json:"name" required:"true"` + // Specifies the TTL to apply. + TTL int `json:"ttl,omitempty"` + // Specifies a collection of rules that determine if this TTL should be applied to an asset. + Rules []TTLRule `json:"rules,omitempty"` +} + +func (c CacheRule) toPatchValue() interface{} { + r := make(map[string]interface{}) + r["name"] = c.Name + r["ttl"] = c.TTL + r["rules"] = make([]map[string]interface{}, len(c.Rules)) + for index, rule := range c.Rules { + submap := r["rules"].([]map[string]interface{})[index] + submap["name"] = rule.Name + submap["request_url"] = rule.RequestURL + } + return r +} + +func (c CacheRule) appropriatePath() Path { + return PathCaching +} + +func (c CacheRule) renderRootOr(render func(p Path) string) string { + return render(c.appropriatePath()) +} + +// CacheRuleList provides a useful way to perform bulk operations in a single Patch. +type CacheRuleList []CacheRule + +func (list CacheRuleList) toPatchValue() interface{} { + r := make([]interface{}, len(list)) + for i, rule := range list { + r[i] = rule.toPatchValue() + } + return r +} + +func (list CacheRuleList) appropriatePath() Path { + return PathCaching +} + +func (list CacheRuleList) renderRootOr(_ func(p Path) string) string { + return list.appropriatePath().renderRoot() +} + +// RestrictionRule specifies a rule that determines if this restriction should be applied to an asset. +type RestrictionRule struct { + // Specifies the name of this rule. + Name string `json:"name" required:"true"` + // Specifies the http host that requests must come from. + Referrer string `json:"referrer,omitempty"` +} + +// Restriction specifies a restriction that defines who can access assets (content from the CDN cache). +type Restriction struct { + // Specifies the name of this restriction. + Name string `json:"name" required:"true"` + // Specifies a collection of rules that determine if this TTL should be applied to an asset. + Rules []RestrictionRule `json:"rules,omitempty"` +} + +// Error specifies an error that occurred during the previous service action. +type Error struct { + // Specifies an error message detailing why there is an error. + Message string `json:"message"` +} + +// Service represents a CDN service resource. +type Service struct { + // Specifies the service ID that represents distributed content. The value is + // a UUID, such as 96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0, that is generated by the server. + ID string `json:"id"` + // Specifies the name of the service. + Name string `json:"name"` + // Specifies a list of domains used by users to access their website. + Domains []Domain `json:"domains"` + // Specifies a list of origin domains or IP addresses where the original assets are stored. + Origins []Origin `json:"origins"` + // Specifies the TTL rules for the assets under this service. Supports wildcards for fine grained control. + Caching []CacheRule `json:"caching"` + // Specifies the restrictions that define who can access assets (content from the CDN cache). + Restrictions []Restriction `json:"restrictions"` + // Specifies the CDN provider flavor ID to use. For a list of flavors, see the operation to list the available flavors. + FlavorID string `json:"flavor_id"` + // Specifies the current status of the service. + Status string `json:"status"` + // Specifies the list of errors that occurred during the previous service action. + Errors []Error `json:"errors"` + // Specifies the self-navigating JSON document paths. + Links []gophercloud.Link `json:"links"` +} + +// ServicePage is the page returned by a pager when traversing over a +// collection of CDN services. +type ServicePage struct { + pagination.MarkerPageBase +} + +// IsEmpty returns true if a ListResult contains no services. +func (r ServicePage) IsEmpty() (bool, error) { + services, err := ExtractServices(r) + return len(services) == 0, err +} + +// LastMarker returns the last service in a ListResult. +func (r ServicePage) LastMarker() (string, error) { + services, err := ExtractServices(r) + if err != nil { + return "", err + } + if len(services) == 0 { + return "", nil + } + return (services[len(services)-1]).ID, nil +} + +// ExtractServices is a function that takes a ListResult and returns the services' information. +func ExtractServices(r pagination.Page) ([]Service, error) { + var s struct { + Services []Service `json:"services"` + } + err := (r.(ServicePage)).ExtractInto(&s) + return s.Services, err +} + +// CreateResult represents the result of a Create operation. +type CreateResult struct { + gophercloud.Result +} + +// Extract is a method that extracts the location of a newly created service. +func (r CreateResult) Extract() (string, error) { + if r.Err != nil { + return "", r.Err + } + if l, ok := r.Header["Location"]; ok && len(l) > 0 { + return l[0], nil + } + return "", nil +} + +// GetResult represents the result of a get operation. +type GetResult struct { + gophercloud.Result +} + +// Extract is a function that extracts a service from a GetResult. +func (r GetResult) Extract() (*Service, error) { + var s Service + err := r.ExtractInto(&s) + return &s, err +} + +// UpdateResult represents the result of a Update operation. +type UpdateResult struct { + gophercloud.Result +} + +// Extract is a method that extracts the location of an updated service. +func (r UpdateResult) Extract() (string, error) { + if r.Err != nil { + return "", r.Err + } + if l, ok := r.Header["Location"]; ok && len(l) > 0 { + return l[0], nil + } + return "", nil +} + +// DeleteResult represents the result of a Delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/services/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/services/testing/doc.go new file mode 100644 index 0000000000..c72e391afe --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/services/testing/doc.go @@ -0,0 +1,2 @@ +// cdn_services_v1 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/services/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/services/testing/fixtures.go new file mode 100644 index 0000000000..d4093e0515 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/services/testing/fixtures.go @@ -0,0 +1,372 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +// HandleListCDNServiceSuccessfully creates an HTTP handler at `/services` on the test handler mux +// that responds with a `List` response. +func HandleListCDNServiceSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, ` + { + "links": [ + { + "rel": "next", + "href": "https://www.poppycdn.io/v1.0/services?marker=96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0&limit=20" + } + ], + "services": [ + { + "id": "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", + "name": "mywebsite.com", + "domains": [ + { + "domain": "www.mywebsite.com" + } + ], + "origins": [ + { + "origin": "mywebsite.com", + "port": 80, + "ssl": false + } + ], + "caching": [ + { + "name": "default", + "ttl": 3600 + }, + { + "name": "home", + "ttl": 17200, + "rules": [ + { + "name": "index", + "request_url": "/index.htm" + } + ] + }, + { + "name": "images", + "ttl": 12800, + "rules": [ + { + "name": "images", + "request_url": "*.png" + } + ] + } + ], + "restrictions": [ + { + "name": "website only", + "rules": [ + { + "name": "mywebsite.com", + "referrer": "www.mywebsite.com" + } + ] + } + ], + "flavor_id": "asia", + "status": "deployed", + "errors" : [], + "links": [ + { + "href": "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", + "rel": "self" + }, + { + "href": "mywebsite.com.cdn123.poppycdn.net", + "rel": "access_url" + }, + { + "href": "https://www.poppycdn.io/v1.0/flavors/asia", + "rel": "flavor" + } + ] + }, + { + "id": "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1", + "name": "myothersite.com", + "domains": [ + { + "domain": "www.myothersite.com" + } + ], + "origins": [ + { + "origin": "44.33.22.11", + "port": 80, + "ssl": false + }, + { + "origin": "77.66.55.44", + "port": 80, + "ssl": false, + "rules": [ + { + "name": "videos", + "request_url": "^/videos/*.m3u" + } + ] + } + ], + "caching": [ + { + "name": "default", + "ttl": 3600 + } + ], + "restrictions": [ + {} + ], + "flavor_id": "europe", + "status": "deployed", + "links": [ + { + "href": "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1", + "rel": "self" + }, + { + "href": "myothersite.com.poppycdn.net", + "rel": "access_url" + }, + { + "href": "https://www.poppycdn.io/v1.0/flavors/europe", + "rel": "flavor" + } + ] + } + ] + } + `) + case "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1": + fmt.Fprintf(w, `{ + "services": [] + }`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) +} + +// HandleCreateCDNServiceSuccessfully creates an HTTP handler at `/services` on the test handler mux +// that responds with a `Create` response. +func HandleCreateCDNServiceSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestJSONRequest(t, r, ` + { + "name": "mywebsite.com", + "domains": [ + { + "domain": "www.mywebsite.com" + }, + { + "domain": "blog.mywebsite.com" + } + ], + "origins": [ + { + "origin": "mywebsite.com", + "port": 80, + "ssl": false + } + ], + "restrictions": [ + { + "name": "website only", + "rules": [ + { + "name": "mywebsite.com", + "referrer": "www.mywebsite.com" + } + ] + } + ], + "caching": [ + { + "name": "default", + "ttl": 3600 + } + ], + + "flavor_id": "cdn" + } + `) + w.Header().Add("Location", "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0") + w.WriteHeader(http.StatusAccepted) + }) +} + +// HandleGetCDNServiceSuccessfully creates an HTTP handler at `/services/{id}` on the test handler mux +// that responds with a `Get` response. +func HandleGetCDNServiceSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` + { + "id": "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", + "name": "mywebsite.com", + "domains": [ + { + "domain": "www.mywebsite.com", + "protocol": "http" + } + ], + "origins": [ + { + "origin": "mywebsite.com", + "port": 80, + "ssl": false + } + ], + "caching": [ + { + "name": "default", + "ttl": 3600 + }, + { + "name": "home", + "ttl": 17200, + "rules": [ + { + "name": "index", + "request_url": "/index.htm" + } + ] + }, + { + "name": "images", + "ttl": 12800, + "rules": [ + { + "name": "images", + "request_url": "*.png" + } + ] + } + ], + "restrictions": [ + { + "name": "website only", + "rules": [ + { + "name": "mywebsite.com", + "referrer": "www.mywebsite.com" + } + ] + } + ], + "flavor_id": "cdn", + "status": "deployed", + "errors" : [], + "links": [ + { + "href": "https://global.cdn.api.rackspacecloud.com/v1.0/110011/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", + "rel": "self" + }, + { + "href": "blog.mywebsite.com.cdn1.raxcdn.com", + "rel": "access_url" + }, + { + "href": "https://global.cdn.api.rackspacecloud.com/v1.0/110011/flavors/cdn", + "rel": "flavor" + } + ] + } + `) + }) +} + +// HandleUpdateCDNServiceSuccessfully creates an HTTP handler at `/services/{id}` on the test handler mux +// that responds with a `Update` response. +func HandleUpdateCDNServiceSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PATCH") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestJSONRequest(t, r, ` + [ + { + "op": "add", + "path": "/domains/-", + "value": {"domain": "appended.mocksite4.com"} + }, + { + "op": "add", + "path": "/domains/4", + "value": {"domain": "inserted.mocksite4.com"} + }, + { + "op": "add", + "path": "/domains", + "value": [ + {"domain": "bulkadded1.mocksite4.com"}, + {"domain": "bulkadded2.mocksite4.com"} + ] + }, + { + "op": "replace", + "path": "/origins/2", + "value": {"origin": "44.33.22.11", "port": 80, "ssl": false} + }, + { + "op": "replace", + "path": "/origins", + "value": [ + {"origin": "44.33.22.11", "port": 80, "ssl": false}, + {"origin": "55.44.33.22", "port": 443, "ssl": true} + ] + }, + { + "op": "remove", + "path": "/caching/8" + }, + { + "op": "remove", + "path": "/caching" + }, + { + "op": "replace", + "path": "/name", + "value": "differentServiceName" + } + ] + `) + w.Header().Add("Location", "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0") + w.WriteHeader(http.StatusAccepted) + }) +} + +// HandleDeleteCDNServiceSuccessfully creates an HTTP handler at `/services/{id}` on the test handler mux +// that responds with a `Delete` response. +func HandleDeleteCDNServiceSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/services/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/services/testing/requests_test.go new file mode 100644 index 0000000000..0abc98ef3d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/services/testing/requests_test.go @@ -0,0 +1,359 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/cdn/v1/services" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleListCDNServiceSuccessfully(t) + + count := 0 + + err := services.List(fake.ServiceClient(), &services.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := services.ExtractServices(page) + if err != nil { + t.Errorf("Failed to extract services: %v", err) + return false, err + } + + expected := []services.Service{ + { + ID: "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", + Name: "mywebsite.com", + Domains: []services.Domain{ + { + Domain: "www.mywebsite.com", + }, + }, + Origins: []services.Origin{ + { + Origin: "mywebsite.com", + Port: 80, + SSL: false, + }, + }, + Caching: []services.CacheRule{ + { + Name: "default", + TTL: 3600, + }, + { + Name: "home", + TTL: 17200, + Rules: []services.TTLRule{ + { + Name: "index", + RequestURL: "/index.htm", + }, + }, + }, + { + Name: "images", + TTL: 12800, + Rules: []services.TTLRule{ + { + Name: "images", + RequestURL: "*.png", + }, + }, + }, + }, + Restrictions: []services.Restriction{ + { + Name: "website only", + Rules: []services.RestrictionRule{ + { + Name: "mywebsite.com", + Referrer: "www.mywebsite.com", + }, + }, + }, + }, + FlavorID: "asia", + Status: "deployed", + Errors: []services.Error{}, + Links: []gophercloud.Link{ + { + Href: "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", + Rel: "self", + }, + { + Href: "mywebsite.com.cdn123.poppycdn.net", + Rel: "access_url", + }, + { + Href: "https://www.poppycdn.io/v1.0/flavors/asia", + Rel: "flavor", + }, + }, + }, + { + ID: "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1", + Name: "myothersite.com", + Domains: []services.Domain{ + { + Domain: "www.myothersite.com", + }, + }, + Origins: []services.Origin{ + { + Origin: "44.33.22.11", + Port: 80, + SSL: false, + }, + { + Origin: "77.66.55.44", + Port: 80, + SSL: false, + Rules: []services.OriginRule{ + { + Name: "videos", + RequestURL: "^/videos/*.m3u", + }, + }, + }, + }, + Caching: []services.CacheRule{ + { + Name: "default", + TTL: 3600, + }, + }, + Restrictions: []services.Restriction{}, + FlavorID: "europe", + Status: "deployed", + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1", + Rel: "self", + }, + gophercloud.Link{ + Href: "myothersite.com.poppycdn.net", + Rel: "access_url", + }, + gophercloud.Link{ + Href: "https://www.poppycdn.io/v1.0/flavors/europe", + Rel: "flavor", + }, + }, + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleCreateCDNServiceSuccessfully(t) + + createOpts := services.CreateOpts{ + Name: "mywebsite.com", + Domains: []services.Domain{ + { + Domain: "www.mywebsite.com", + }, + { + Domain: "blog.mywebsite.com", + }, + }, + Origins: []services.Origin{ + { + Origin: "mywebsite.com", + Port: 80, + SSL: false, + }, + }, + Restrictions: []services.Restriction{ + { + Name: "website only", + Rules: []services.RestrictionRule{ + { + Name: "mywebsite.com", + Referrer: "www.mywebsite.com", + }, + }, + }, + }, + Caching: []services.CacheRule{ + { + Name: "default", + TTL: 3600, + }, + }, + FlavorID: "cdn", + } + + expected := "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" + actual, err := services.Create(fake.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, expected, actual) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleGetCDNServiceSuccessfully(t) + + expected := &services.Service{ + ID: "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", + Name: "mywebsite.com", + Domains: []services.Domain{ + { + Domain: "www.mywebsite.com", + Protocol: "http", + }, + }, + Origins: []services.Origin{ + { + Origin: "mywebsite.com", + Port: 80, + SSL: false, + }, + }, + Caching: []services.CacheRule{ + { + Name: "default", + TTL: 3600, + }, + { + Name: "home", + TTL: 17200, + Rules: []services.TTLRule{ + { + Name: "index", + RequestURL: "/index.htm", + }, + }, + }, + { + Name: "images", + TTL: 12800, + Rules: []services.TTLRule{ + { + Name: "images", + RequestURL: "*.png", + }, + }, + }, + }, + Restrictions: []services.Restriction{ + { + Name: "website only", + Rules: []services.RestrictionRule{ + { + Name: "mywebsite.com", + Referrer: "www.mywebsite.com", + }, + }, + }, + }, + FlavorID: "cdn", + Status: "deployed", + Errors: []services.Error{}, + Links: []gophercloud.Link{ + { + Href: "https://global.cdn.api.rackspacecloud.com/v1.0/110011/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", + Rel: "self", + }, + { + Href: "blog.mywebsite.com.cdn1.raxcdn.com", + Rel: "access_url", + }, + { + Href: "https://global.cdn.api.rackspacecloud.com/v1.0/110011/flavors/cdn", + Rel: "flavor", + }, + }, + } + + actual, err := services.Get(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0").Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expected, actual) +} + +func TestSuccessfulUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleUpdateCDNServiceSuccessfully(t) + + expected := "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" + ops := services.UpdateOpts{ + // Append a single Domain + services.Append{Value: services.Domain{Domain: "appended.mocksite4.com"}}, + // Insert a single Domain + services.Insertion{ + Index: 4, + Value: services.Domain{Domain: "inserted.mocksite4.com"}, + }, + // Bulk addition + services.Append{ + Value: services.DomainList{ + {Domain: "bulkadded1.mocksite4.com"}, + {Domain: "bulkadded2.mocksite4.com"}, + }, + }, + // Replace a single Origin + services.Replacement{ + Index: 2, + Value: services.Origin{Origin: "44.33.22.11", Port: 80, SSL: false}, + }, + // Bulk replace Origins + services.Replacement{ + Index: 0, // Ignored + Value: services.OriginList{ + {Origin: "44.33.22.11", Port: 80, SSL: false}, + {Origin: "55.44.33.22", Port: 443, SSL: true}, + }, + }, + // Remove a single CacheRule + services.Removal{ + Index: 8, + Path: services.PathCaching, + }, + // Bulk removal + services.Removal{ + All: true, + Path: services.PathCaching, + }, + // Service name replacement + services.NameReplacement{ + NewName: "differentServiceName", + }, + } + + actual, err := services.Update(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", ops).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, expected, actual) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleDeleteCDNServiceSuccessfully(t) + + err := services.Delete(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0").ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/services/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/services/urls.go new file mode 100644 index 0000000000..5bb3ca9d92 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/cdn/v1/services/urls.go @@ -0,0 +1,23 @@ +package services + +import "github.com/gophercloud/gophercloud" + +func listURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("services") +} + +func createURL(c *gophercloud.ServiceClient) string { + return listURL(c) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("services", id) +} + +func updateURL(c *gophercloud.ServiceClient, id string) string { + return getURL(c, id) +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return getURL(c, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/client.go b/vendor/github.com/gophercloud/gophercloud/openstack/client.go new file mode 100644 index 0000000000..09120e8faf --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/client.go @@ -0,0 +1,295 @@ +package openstack + +import ( + "fmt" + "net/url" + "reflect" + + "github.com/gophercloud/gophercloud" + tokens2 "github.com/gophercloud/gophercloud/openstack/identity/v2/tokens" + tokens3 "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens" + "github.com/gophercloud/gophercloud/openstack/utils" +) + +const ( + v20 = "v2.0" + v30 = "v3.0" +) + +// NewClient prepares an unauthenticated ProviderClient instance. +// Most users will probably prefer using the AuthenticatedClient function instead. +// This is useful if you wish to explicitly control the version of the identity service that's used for authentication explicitly, +// for example. +func NewClient(endpoint string) (*gophercloud.ProviderClient, error) { + u, err := url.Parse(endpoint) + if err != nil { + return nil, err + } + hadPath := u.Path != "" + u.Path, u.RawQuery, u.Fragment = "", "", "" + base := u.String() + + endpoint = gophercloud.NormalizeURL(endpoint) + base = gophercloud.NormalizeURL(base) + + if hadPath { + return &gophercloud.ProviderClient{ + IdentityBase: base, + IdentityEndpoint: endpoint, + }, nil + } + + return &gophercloud.ProviderClient{ + IdentityBase: base, + IdentityEndpoint: "", + }, nil +} + +// AuthenticatedClient logs in to an OpenStack cloud found at the identity endpoint specified by options, acquires a token, and +// returns a Client instance that's ready to operate. +// It first queries the root identity endpoint to determine which versions of the identity service are supported, then chooses +// the most recent identity service available to proceed. +func AuthenticatedClient(options gophercloud.AuthOptions) (*gophercloud.ProviderClient, error) { + client, err := NewClient(options.IdentityEndpoint) + if err != nil { + return nil, err + } + + err = Authenticate(client, options) + if err != nil { + return nil, err + } + return client, nil +} + +// Authenticate or re-authenticate against the most recent identity service supported at the provided endpoint. +func Authenticate(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error { + versions := []*utils.Version{ + {ID: v20, Priority: 20, Suffix: "/v2.0/"}, + {ID: v30, Priority: 30, Suffix: "/v3/"}, + } + + chosen, endpoint, err := utils.ChooseVersion(client, versions) + if err != nil { + return err + } + + switch chosen.ID { + case v20: + return v2auth(client, endpoint, options, gophercloud.EndpointOpts{}) + case v30: + return v3auth(client, endpoint, &options, gophercloud.EndpointOpts{}) + default: + // The switch statement must be out of date from the versions list. + return fmt.Errorf("Unrecognized identity version: %s", chosen.ID) + } +} + +// AuthenticateV2 explicitly authenticates against the identity v2 endpoint. +func AuthenticateV2(client *gophercloud.ProviderClient, options gophercloud.AuthOptions, eo gophercloud.EndpointOpts) error { + return v2auth(client, "", options, eo) +} + +func v2auth(client *gophercloud.ProviderClient, endpoint string, options gophercloud.AuthOptions, eo gophercloud.EndpointOpts) error { + v2Client, err := NewIdentityV2(client, eo) + if err != nil { + return err + } + + if endpoint != "" { + v2Client.Endpoint = endpoint + } + + v2Opts := tokens2.AuthOptions{ + IdentityEndpoint: options.IdentityEndpoint, + Username: options.Username, + Password: options.Password, + TenantID: options.TenantID, + TenantName: options.TenantName, + AllowReauth: options.AllowReauth, + TokenID: options.TokenID, + } + + result := tokens2.Create(v2Client, v2Opts) + + token, err := result.ExtractToken() + if err != nil { + return err + } + + catalog, err := result.ExtractServiceCatalog() + if err != nil { + return err + } + + if options.AllowReauth { + client.ReauthFunc = func() error { + client.TokenID = "" + return v2auth(client, endpoint, options, eo) + } + } + client.TokenID = token.ID + client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) { + return V2EndpointURL(catalog, opts) + } + + return nil +} + +// AuthenticateV3 explicitly authenticates against the identity v3 service. +func AuthenticateV3(client *gophercloud.ProviderClient, options tokens3.AuthOptionsBuilder, eo gophercloud.EndpointOpts) error { + return v3auth(client, "", options, eo) +} + +func v3auth(client *gophercloud.ProviderClient, endpoint string, opts tokens3.AuthOptionsBuilder, eo gophercloud.EndpointOpts) error { + // Override the generated service endpoint with the one returned by the version endpoint. + v3Client, err := NewIdentityV3(client, eo) + if err != nil { + return err + } + + if endpoint != "" { + v3Client.Endpoint = endpoint + } + + result := tokens3.Create(v3Client, opts) + + token, err := result.ExtractToken() + if err != nil { + return err + } + + catalog, err := result.ExtractServiceCatalog() + if err != nil { + return err + } + + client.TokenID = token.ID + + if opts.CanReauth() { + client.ReauthFunc = func() error { + client.TokenID = "" + return v3auth(client, endpoint, opts, eo) + } + } + client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) { + return V3EndpointURL(catalog, opts) + } + + return nil +} + +// NewIdentityV2 creates a ServiceClient that may be used to interact with the v2 identity service. +func NewIdentityV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + endpoint := client.IdentityBase + "v2.0/" + clientType := "identity" + var err error + if !reflect.DeepEqual(eo, gophercloud.EndpointOpts{}) { + eo.ApplyDefaults(clientType) + endpoint, err = client.EndpointLocator(eo) + if err != nil { + return nil, err + } + } + + return &gophercloud.ServiceClient{ + ProviderClient: client, + Endpoint: endpoint, + Type: clientType, + }, nil +} + +// NewIdentityV3 creates a ServiceClient that may be used to access the v3 identity service. +func NewIdentityV3(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + endpoint := client.IdentityBase + "v3/" + clientType := "identity" + var err error + if !reflect.DeepEqual(eo, gophercloud.EndpointOpts{}) { + eo.ApplyDefaults(clientType) + endpoint, err = client.EndpointLocator(eo) + if err != nil { + return nil, err + } + } + + return &gophercloud.ServiceClient{ + ProviderClient: client, + Endpoint: endpoint, + Type: clientType, + }, nil +} + +func initClientOpts(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts, clientType string) (*gophercloud.ServiceClient, error) { + sc := new(gophercloud.ServiceClient) + eo.ApplyDefaults(clientType) + url, err := client.EndpointLocator(eo) + if err != nil { + return sc, err + } + sc.ProviderClient = client + sc.Endpoint = url + sc.Type = clientType + return sc, nil +} + +// NewObjectStorageV1 creates a ServiceClient that may be used with the v1 object storage package. +func NewObjectStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "object-store") +} + +// NewComputeV2 creates a ServiceClient that may be used with the v2 compute package. +func NewComputeV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "compute") +} + +// NewNetworkV2 creates a ServiceClient that may be used with the v2 network package. +func NewNetworkV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "network") + sc.ResourceBase = sc.Endpoint + "v2.0/" + return sc, err +} + +// NewBlockStorageV1 creates a ServiceClient that may be used to access the v1 block storage service. +func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "volume") +} + +// NewBlockStorageV2 creates a ServiceClient that may be used to access the v2 block storage service. +func NewBlockStorageV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "volumev2") +} + +// NewSharedFileSystemV2 creates a ServiceClient that may be used to access the v2 shared file system service. +func NewSharedFileSystemV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "sharev2") +} + +// NewCDNV1 creates a ServiceClient that may be used to access the OpenStack v1 +// CDN service. +func NewCDNV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "cdn") +} + +// NewOrchestrationV1 creates a ServiceClient that may be used to access the v1 orchestration service. +func NewOrchestrationV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "orchestration") +} + +// NewDBV1 creates a ServiceClient that may be used to access the v1 DB service. +func NewDBV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "database") +} + +// NewDNSV2 creates a ServiceClient that may be used to access the v2 DNS service. +func NewDNSV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "dns") + sc.ResourceBase = sc.Endpoint + "v2/" + return sc, err +} + +// NewImageServiceV2 creates a ServiceClient that may be used to access the v2 image service. +func NewImageServiceV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "image") + sc.ResourceBase = sc.Endpoint + "v2/" + return sc, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/common/README.md b/vendor/github.com/gophercloud/gophercloud/openstack/common/README.md new file mode 100644 index 0000000000..7b55795d08 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/common/README.md @@ -0,0 +1,3 @@ +# Common Resources + +This directory is for resources that are shared by multiple services. diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/common/extensions/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/common/extensions/doc.go new file mode 100644 index 0000000000..4a168f4b2c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/common/extensions/doc.go @@ -0,0 +1,15 @@ +// Package extensions provides information and interaction with the different extensions available +// for an OpenStack service. +// +// The purpose of OpenStack API extensions is to: +// +// - Introduce new features in the API without requiring a version change. +// - Introduce vendor-specific niche functionality. +// - Act as a proving ground for experimental functionalities that might be included in a future +// version of the API. +// +// Extensions usually have tags that prevent conflicts with other extensions that define attributes +// or resources with the same names, and with core resources and attributes. +// Because an extension might not be supported by all plug-ins, its availability varies with deployments +// and the specific plug-in. +package extensions diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/common/extensions/errors.go b/vendor/github.com/gophercloud/gophercloud/openstack/common/extensions/errors.go new file mode 100755 index 0000000000..aeec0fa756 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/common/extensions/errors.go @@ -0,0 +1 @@ +package extensions diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/common/extensions/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/common/extensions/requests.go new file mode 100755 index 0000000000..46b7d60cd6 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/common/extensions/requests.go @@ -0,0 +1,20 @@ +package extensions + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Get retrieves information for a specific extension using its alias. +func Get(c *gophercloud.ServiceClient, alias string) (r GetResult) { + _, r.Err = c.Get(ExtensionURL(c, alias), &r.Body, nil) + return +} + +// List returns a Pager which allows you to iterate over the full collection of extensions. +// It does not accept query parameters. +func List(c *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(c, ListExtensionURL(c), func(r pagination.PageResult) pagination.Page { + return ExtensionPage{pagination.SinglePageBase(r)} + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/common/extensions/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/common/extensions/results.go new file mode 100755 index 0000000000..d5f8650913 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/common/extensions/results.go @@ -0,0 +1,53 @@ +package extensions + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// GetResult temporarily stores the result of a Get call. +// Use its Extract() method to interpret it as an Extension. +type GetResult struct { + gophercloud.Result +} + +// Extract interprets a GetResult as an Extension. +func (r GetResult) Extract() (*Extension, error) { + var s struct { + Extension *Extension `json:"extension"` + } + err := r.ExtractInto(&s) + return s.Extension, err +} + +// Extension is a struct that represents an OpenStack extension. +type Extension struct { + Updated string `json:"updated"` + Name string `json:"name"` + Links []interface{} `json:"links"` + Namespace string `json:"namespace"` + Alias string `json:"alias"` + Description string `json:"description"` +} + +// ExtensionPage is the page returned by a pager when traversing over a collection of extensions. +type ExtensionPage struct { + pagination.SinglePageBase +} + +// IsEmpty checks whether an ExtensionPage struct is empty. +func (r ExtensionPage) IsEmpty() (bool, error) { + is, err := ExtractExtensions(r) + return len(is) == 0, err +} + +// ExtractExtensions accepts a Page struct, specifically an ExtensionPage struct, and extracts the +// elements into a slice of Extension structs. +// In other words, a generic collection is mapped into a relevant slice. +func ExtractExtensions(r pagination.Page) ([]Extension, error) { + var s struct { + Extensions []Extension `json:"extensions"` + } + err := (r.(ExtensionPage)).ExtractInto(&s) + return s.Extensions, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/common/extensions/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/common/extensions/testing/doc.go new file mode 100644 index 0000000000..24b079593e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/common/extensions/testing/doc.go @@ -0,0 +1,2 @@ +// common_extensions +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/common/extensions/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/common/extensions/testing/fixtures.go new file mode 100644 index 0000000000..a986c950a2 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/common/extensions/testing/fixtures.go @@ -0,0 +1,90 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud/openstack/common/extensions" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +// ListOutput provides a single page of Extension results. +const ListOutput = ` +{ + "extensions": [ + { + "updated": "2013-01-20T00:00:00-00:00", + "name": "Neutron Service Type Management", + "links": [], + "namespace": "http://docs.openstack.org/ext/neutron/service-type/api/v1.0", + "alias": "service-type", + "description": "API for retrieving service providers for Neutron advanced services" + } + ] +}` + +// GetOutput provides a single Extension result. +const GetOutput = ` +{ + "extension": { + "updated": "2013-02-03T10:00:00-00:00", + "name": "agent", + "links": [], + "namespace": "http://docs.openstack.org/ext/agent/api/v2.0", + "alias": "agent", + "description": "The agent management extension." + } +} +` + +// ListedExtension is the Extension that should be parsed from ListOutput. +var ListedExtension = extensions.Extension{ + Updated: "2013-01-20T00:00:00-00:00", + Name: "Neutron Service Type Management", + Links: []interface{}{}, + Namespace: "http://docs.openstack.org/ext/neutron/service-type/api/v1.0", + Alias: "service-type", + Description: "API for retrieving service providers for Neutron advanced services", +} + +// ExpectedExtensions is a slice containing the Extension that should be parsed from ListOutput. +var ExpectedExtensions = []extensions.Extension{ListedExtension} + +// SingleExtension is the Extension that should be parsed from GetOutput. +var SingleExtension = &extensions.Extension{ + Updated: "2013-02-03T10:00:00-00:00", + Name: "agent", + Links: []interface{}{}, + Namespace: "http://docs.openstack.org/ext/agent/api/v2.0", + Alias: "agent", + Description: "The agent management extension.", +} + +// HandleListExtensionsSuccessfully creates an HTTP handler at `/extensions` on the test handler +// mux that response with a list containing a single tenant. +func HandleListExtensionsSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/extensions", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + + fmt.Fprintf(w, ListOutput) + }) +} + +// HandleGetExtensionSuccessfully creates an HTTP handler at `/extensions/agent` that responds with +// a JSON payload corresponding to SingleExtension. +func HandleGetExtensionSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/extensions/agent", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, GetOutput) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/common/extensions/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/common/extensions/testing/requests_test.go new file mode 100644 index 0000000000..fbaedfa0be --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/common/extensions/testing/requests_test.go @@ -0,0 +1,39 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/common/extensions" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListExtensionsSuccessfully(t) + + count := 0 + + extensions.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := extensions.ExtractExtensions(page) + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, ExpectedExtensions, actual) + + return true, nil + }) + + th.CheckEquals(t, 1, count) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetExtensionSuccessfully(t) + + actual, err := extensions.Get(client.ServiceClient(), "agent").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, SingleExtension, actual) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/common/extensions/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/common/extensions/urls.go new file mode 100644 index 0000000000..eaf38b2d19 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/common/extensions/urls.go @@ -0,0 +1,13 @@ +package extensions + +import "github.com/gophercloud/gophercloud" + +// ExtensionURL generates the URL for an extension resource by name. +func ExtensionURL(c *gophercloud.ServiceClient, name string) string { + return c.ServiceURL("extensions", name) +} + +// ListExtensionURL generates the URL for the extensions resource collection. +func ListExtensionURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("extensions") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/attachinterfaces/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/attachinterfaces/doc.go new file mode 100644 index 0000000000..2ef30db869 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/attachinterfaces/doc.go @@ -0,0 +1,3 @@ +// Package attachinterfaces provides the ability to manage network interfaces through +// nova-network +package attachinterfaces diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/attachinterfaces/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/attachinterfaces/requests.go new file mode 100644 index 0000000000..76cc879630 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/attachinterfaces/requests.go @@ -0,0 +1,13 @@ +package attachinterfaces + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// List makes a request against the nova API to list the servers interfaces. +func List(client *gophercloud.ServiceClient, serverID string) pagination.Pager { + return pagination.NewPager(client, listInterfaceURL(client, serverID), func(r pagination.PageResult) pagination.Page { + return InterfacePage{pagination.SinglePageBase(r)} + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/attachinterfaces/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/attachinterfaces/results.go new file mode 100644 index 0000000000..776ad8305b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/attachinterfaces/results.go @@ -0,0 +1,43 @@ +package attachinterfaces + +import ( + "github.com/gophercloud/gophercloud/pagination" +) + +// FixedIP represents a Fixed IP Address. +type FixedIP struct { + SubnetID string `json:"subnet_id"` + IPAddress string `json:"ip_address"` +} + +// Interface represents a network interface on an instance. +type Interface struct { + PortState string `json:"port_state"` + FixedIPs []FixedIP `json:"fixed_ips"` + PortID string `json:"port_id"` + NetID string `json:"net_id"` + MACAddr string `json:"mac_addr"` +} + +// InterfacePage abstracts the raw results of making a List() request against the API. +// As OpenStack extensions may freely alter the response bodies of structures returned +// to the client, you may only safely access the data provided through the ExtractInterfaces call. +type InterfacePage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if an InterfacePage contains no interfaces. +func (r InterfacePage) IsEmpty() (bool, error) { + interfaces, err := ExtractInterfaces(r) + return len(interfaces) == 0, err +} + +// ExtractInterfaces interprets the results of a single page from a List() call, +// producing a map of interfaces. +func ExtractInterfaces(r pagination.Page) ([]Interface, error) { + var s struct { + Interfaces []Interface `json:"interfaceAttachments"` + } + err := (r.(InterfacePage)).ExtractInto(&s) + return s.Interfaces, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/attachinterfaces/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/attachinterfaces/testing/doc.go new file mode 100644 index 0000000000..d9da11d008 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/attachinterfaces/testing/doc.go @@ -0,0 +1,2 @@ +// compute_extensions_attachinterfaces_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/attachinterfaces/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/attachinterfaces/testing/fixtures.go new file mode 100644 index 0000000000..017243f707 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/attachinterfaces/testing/fixtures.go @@ -0,0 +1,61 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/attachinterfaces" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +// ListInterfacesExpected represents an expected repsonse from a ListInterfaces request. +var ListInterfacesExpected = []attachinterfaces.Interface{ + { + PortState: "ACTIVE", + FixedIPs: []attachinterfaces.FixedIP{ + { + SubnetID: "d7906db4-a566-4546-b1f4-5c7fa70f0bf3", + IPAddress: "10.0.0.7", + }, + { + SubnetID: "45906d64-a548-4276-h1f8-kcffa80fjbnl", + IPAddress: "10.0.0.8", + }, + }, + PortID: "0dde1598-b374-474e-986f-5b8dd1df1d4e", + NetID: "8a5fe506-7e9f-4091-899b-96336909d93c", + MACAddr: "fa:16:3e:38:2d:80", + }, +} + +// HandleInterfaceListSuccessfully sets up the test server to respond to a ListInterfaces request. +func HandleInterfaceListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/b07e7a3b-d951-4efc-a4f9-ac9f001afb7f/os-interface", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "interfaceAttachments": [ + { + "port_state":"ACTIVE", + "fixed_ips": [ + { + "subnet_id": "d7906db4-a566-4546-b1f4-5c7fa70f0bf3", + "ip_address": "10.0.0.7" + }, + { + "subnet_id": "45906d64-a548-4276-h1f8-kcffa80fjbnl", + "ip_address": "10.0.0.8" + } + ], + "port_id": "0dde1598-b374-474e-986f-5b8dd1df1d4e", + "net_id": "8a5fe506-7e9f-4091-899b-96336909d93c", + "mac_addr": "fa:16:3e:38:2d:80" + } + ] + }`) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/attachinterfaces/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/attachinterfaces/testing/requests_test.go new file mode 100644 index 0000000000..d7eb511daa --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/attachinterfaces/testing/requests_test.go @@ -0,0 +1,45 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/attachinterfaces" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestListInterface(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleInterfaceListSuccessfully(t) + + expected := ListInterfacesExpected + pages := 0 + err := attachinterfaces.List(client.ServiceClient(), "b07e7a3b-d951-4efc-a4f9-ac9f001afb7f").EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := attachinterfaces.ExtractInterfaces(page) + th.AssertNoErr(t, err) + + if len(actual) != 1 { + t.Fatalf("Expected 1 interface, got %d", len(actual)) + } + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, pages) +} + +func TestListInterfacesAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleInterfaceListSuccessfully(t) + + allPages, err := attachinterfaces.List(client.ServiceClient(), "b07e7a3b-d951-4efc-a4f9-ac9f001afb7f").AllPages() + th.AssertNoErr(t, err) + _, err = attachinterfaces.ExtractInterfaces(allPages) + th.AssertNoErr(t, err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/attachinterfaces/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/attachinterfaces/urls.go new file mode 100644 index 0000000000..7d376f99bb --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/attachinterfaces/urls.go @@ -0,0 +1,7 @@ +package attachinterfaces + +import "github.com/gophercloud/gophercloud" + +func listInterfaceURL(client *gophercloud.ServiceClient, serverID string) string { + return client.ServiceURL("servers", serverID, "os-interface") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/availabilityzones/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/availabilityzones/results.go new file mode 100644 index 0000000000..96a6a50b3d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/availabilityzones/results.go @@ -0,0 +1,12 @@ +package availabilityzones + +// ServerExt is an extension to the base Server object +type ServerExt struct { + // AvailabilityZone is the availabilty zone the server is in. + AvailabilityZone string `json:"OS-EXT-AZ:availability_zone"` +} + +// UnmarshalJSON to override default +func (r *ServerExt) UnmarshalJSON(b []byte) error { + return nil +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests.go new file mode 100644 index 0000000000..9dae14c7a9 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests.go @@ -0,0 +1,120 @@ +package bootfromvolume + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" +) + +type ( + // DestinationType represents the type of medium being used as the + // destination of the bootable device. + DestinationType string + + // SourceType represents the type of medium being used as the source of the + // bootable device. + SourceType string +) + +const ( + // DestinationLocal DestinationType is for using an ephemeral disk as the + // destination. + DestinationLocal DestinationType = "local" + + // DestinationVolume DestinationType is for using a volume as the destination. + DestinationVolume DestinationType = "volume" + + // SourceBlank SourceType is for a "blank" or empty source. + SourceBlank SourceType = "blank" + + // SourceImage SourceType is for using images as the source of a block device. + SourceImage SourceType = "image" + + // SourceSnapshot SourceType is for using a volume snapshot as the source of + // a block device. + SourceSnapshot SourceType = "snapshot" + + // SourceVolume SourceType is for using a volume as the source of block + // device. + SourceVolume SourceType = "volume" +) + +// BlockDevice is a structure with options for creating block devices in a +// server. The block device may be created from an image, snapshot, new volume, +// or existing volume. The destination may be a new volume, existing volume +// which will be attached to the instance, ephemeral disk, or boot device. +type BlockDevice struct { + // SourceType must be one of: "volume", "snapshot", "image", or "blank". + SourceType SourceType `json:"source_type" required:"true"` + + // UUID is the unique identifier for the existing volume, snapshot, or + // image (see above). + UUID string `json:"uuid,omitempty"` + + // BootIndex is the boot index. It defaults to 0. + BootIndex int `json:"boot_index"` + + // DeleteOnTermination specifies whether or not to delete the attached volume + // when the server is deleted. Defaults to `false`. + DeleteOnTermination bool `json:"delete_on_termination"` + + // DestinationType is the type that gets created. Possible values are "volume" + // and "local". + DestinationType DestinationType `json:"destination_type,omitempty"` + + // GuestFormat specifies the format of the block device. + GuestFormat string `json:"guest_format,omitempty"` + + // VolumeSize is the size of the volume to create (in gigabytes). This can be + // omitted for existing volumes. + VolumeSize int `json:"volume_size,omitempty"` +} + +// CreateOptsExt is a structure that extends the server `CreateOpts` structure +// by allowing for a block device mapping. +type CreateOptsExt struct { + servers.CreateOptsBuilder + BlockDevice []BlockDevice `json:"block_device_mapping_v2,omitempty"` +} + +// ToServerCreateMap adds the block device mapping option to the base server +// creation options. +func (opts CreateOptsExt) ToServerCreateMap() (map[string]interface{}, error) { + base, err := opts.CreateOptsBuilder.ToServerCreateMap() + if err != nil { + return nil, err + } + + if len(opts.BlockDevice) == 0 { + err := gophercloud.ErrMissingInput{} + err.Argument = "bootfromvolume.CreateOptsExt.BlockDevice" + return nil, err + } + + serverMap := base["server"].(map[string]interface{}) + + blockDevice := make([]map[string]interface{}, len(opts.BlockDevice)) + + for i, bd := range opts.BlockDevice { + b, err := gophercloud.BuildRequestBody(bd, "") + if err != nil { + return nil, err + } + blockDevice[i] = b + } + serverMap["block_device_mapping_v2"] = blockDevice + + return base, nil +} + +// Create requests the creation of a server from the given block device mapping. +func Create(client *gophercloud.ServiceClient, opts servers.CreateOptsBuilder) (r servers.CreateResult) { + b, err := opts.ToServerCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 202}, + }) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume/results.go new file mode 100644 index 0000000000..3211fb1f3d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume/results.go @@ -0,0 +1,10 @@ +package bootfromvolume + +import ( + os "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" +) + +// CreateResult temporarily contains the response from a Create call. +type CreateResult struct { + os.CreateResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume/testing/doc.go new file mode 100644 index 0000000000..cb879d9257 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume/testing/doc.go @@ -0,0 +1,2 @@ +// compute_extensions_bootfromvolume_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume/testing/requests_test.go new file mode 100644 index 0000000000..7fd3e7d84a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume/testing/requests_test.go @@ -0,0 +1,327 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestBootFromNewVolume(t *testing.T) { + base := servers.CreateOpts{ + Name: "createdserver", + FlavorRef: "performance1-1", + } + + ext := bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: base, + BlockDevice: []bootfromvolume.BlockDevice{ + { + UUID: "123456", + SourceType: bootfromvolume.SourceImage, + DestinationType: bootfromvolume.DestinationVolume, + VolumeSize: 10, + DeleteOnTermination: true, + }, + }, + } + + expected := ` + { + "server": { + "name":"createdserver", + "flavorRef":"performance1-1", + "imageRef":"", + "block_device_mapping_v2":[ + { + "uuid":"123456", + "source_type":"image", + "destination_type":"volume", + "boot_index": 0, + "delete_on_termination": true, + "volume_size": 10 + } + ] + } + } + ` + actual, err := ext.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expected, actual) +} + +func TestBootFromExistingVolume(t *testing.T) { + base := servers.CreateOpts{ + Name: "createdserver", + FlavorRef: "performance1-1", + } + + ext := bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: base, + BlockDevice: []bootfromvolume.BlockDevice{ + { + UUID: "123456", + SourceType: bootfromvolume.SourceVolume, + DestinationType: bootfromvolume.DestinationVolume, + DeleteOnTermination: true, + }, + }, + } + + expected := ` + { + "server": { + "name":"createdserver", + "flavorRef":"performance1-1", + "imageRef":"", + "block_device_mapping_v2":[ + { + "uuid":"123456", + "source_type":"volume", + "destination_type":"volume", + "boot_index": 0, + "delete_on_termination": true + } + ] + } + } + ` + actual, err := ext.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expected, actual) +} + +func TestBootFromImage(t *testing.T) { + base := servers.CreateOpts{ + Name: "createdserver", + ImageRef: "asdfasdfasdf", + FlavorRef: "performance1-1", + } + + ext := bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: base, + BlockDevice: []bootfromvolume.BlockDevice{ + { + BootIndex: 0, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationLocal, + SourceType: bootfromvolume.SourceImage, + UUID: "asdfasdfasdf", + }, + }, + } + + expected := ` + { + "server": { + "name": "createdserver", + "imageRef": "asdfasdfasdf", + "flavorRef": "performance1-1", + "block_device_mapping_v2":[ + { + "boot_index": 0, + "delete_on_termination": true, + "destination_type":"local", + "source_type":"image", + "uuid":"asdfasdfasdf" + } + ] + } + } + ` + actual, err := ext.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expected, actual) +} + +func TestCreateMultiEphemeralOpts(t *testing.T) { + base := servers.CreateOpts{ + Name: "createdserver", + ImageRef: "asdfasdfasdf", + FlavorRef: "performance1-1", + } + + ext := bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: base, + BlockDevice: []bootfromvolume.BlockDevice{ + { + BootIndex: 0, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationLocal, + SourceType: bootfromvolume.SourceImage, + UUID: "asdfasdfasdf", + }, + { + BootIndex: -1, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationLocal, + GuestFormat: "ext4", + SourceType: bootfromvolume.SourceBlank, + VolumeSize: 1, + }, + { + BootIndex: -1, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationLocal, + GuestFormat: "ext4", + SourceType: bootfromvolume.SourceBlank, + VolumeSize: 1, + }, + }, + } + + expected := ` + { + "server": { + "name": "createdserver", + "imageRef": "asdfasdfasdf", + "flavorRef": "performance1-1", + "block_device_mapping_v2":[ + { + "boot_index": 0, + "delete_on_termination": true, + "destination_type":"local", + "source_type":"image", + "uuid":"asdfasdfasdf" + }, + { + "boot_index": -1, + "delete_on_termination": true, + "destination_type":"local", + "guest_format":"ext4", + "source_type":"blank", + "volume_size": 1 + }, + { + "boot_index": -1, + "delete_on_termination": true, + "destination_type":"local", + "guest_format":"ext4", + "source_type":"blank", + "volume_size": 1 + } + ] + } + } + ` + actual, err := ext.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expected, actual) +} + +func TestAttachNewVolume(t *testing.T) { + base := servers.CreateOpts{ + Name: "createdserver", + ImageRef: "asdfasdfasdf", + FlavorRef: "performance1-1", + } + + ext := bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: base, + BlockDevice: []bootfromvolume.BlockDevice{ + { + BootIndex: 0, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationLocal, + SourceType: bootfromvolume.SourceImage, + UUID: "asdfasdfasdf", + }, + { + BootIndex: 1, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationVolume, + SourceType: bootfromvolume.SourceBlank, + VolumeSize: 1, + }, + }, + } + + expected := ` + { + "server": { + "name": "createdserver", + "imageRef": "asdfasdfasdf", + "flavorRef": "performance1-1", + "block_device_mapping_v2":[ + { + "boot_index": 0, + "delete_on_termination": true, + "destination_type":"local", + "source_type":"image", + "uuid":"asdfasdfasdf" + }, + { + "boot_index": 1, + "delete_on_termination": true, + "destination_type":"volume", + "source_type":"blank", + "volume_size": 1 + } + ] + } + } + ` + actual, err := ext.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expected, actual) +} + +func TestAttachExistingVolume(t *testing.T) { + base := servers.CreateOpts{ + Name: "createdserver", + ImageRef: "asdfasdfasdf", + FlavorRef: "performance1-1", + } + + ext := bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: base, + BlockDevice: []bootfromvolume.BlockDevice{ + { + BootIndex: 0, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationLocal, + SourceType: bootfromvolume.SourceImage, + UUID: "asdfasdfasdf", + }, + { + BootIndex: 1, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationVolume, + SourceType: bootfromvolume.SourceVolume, + UUID: "123456", + VolumeSize: 1, + }, + }, + } + + expected := ` + { + "server": { + "name": "createdserver", + "imageRef": "asdfasdfasdf", + "flavorRef": "performance1-1", + "block_device_mapping_v2":[ + { + "boot_index": 0, + "delete_on_termination": true, + "destination_type":"local", + "source_type":"image", + "uuid":"asdfasdfasdf" + }, + { + "boot_index": 1, + "delete_on_termination": true, + "destination_type":"volume", + "source_type":"volume", + "uuid":"123456", + "volume_size": 1 + } + ] + } + } + ` + actual, err := ext.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expected, actual) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume/urls.go new file mode 100644 index 0000000000..dc007eadf8 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume/urls.go @@ -0,0 +1,7 @@ +package bootfromvolume + +import "github.com/gophercloud/gophercloud" + +func createURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("os-volumes_boot") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/defsecrules/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/defsecrules/doc.go new file mode 100644 index 0000000000..2571a1a5a7 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/defsecrules/doc.go @@ -0,0 +1 @@ +package defsecrules diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/defsecrules/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/defsecrules/requests.go new file mode 100644 index 0000000000..184fdc919c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/defsecrules/requests.go @@ -0,0 +1,70 @@ +package defsecrules + +import ( + "strings" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// List will return a collection of default rules. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, rootURL(client), func(r pagination.PageResult) pagination.Page { + return DefaultRulePage{pagination.SinglePageBase(r)} + }) +} + +// CreateOpts represents the configuration for adding a new default rule. +type CreateOpts struct { + // The lower bound of the port range that will be opened.s + FromPort int `json:"from_port"` + // The upper bound of the port range that will be opened. + ToPort int `json:"to_port"` + // The protocol type that will be allowed, e.g. TCP. + IPProtocol string `json:"ip_protocol" required:"true"` + // ONLY required if FromGroupID is blank. This represents the IP range that + // will be the source of network traffic to your security group. Use + // 0.0.0.0/0 to allow all IP addresses. + CIDR string `json:"cidr,omitempty"` +} + +// CreateOptsBuilder builds the create rule options into a serializable format. +type CreateOptsBuilder interface { + ToRuleCreateMap() (map[string]interface{}, error) +} + +// ToRuleCreateMap builds the create rule options into a serializable format. +func (opts CreateOpts) ToRuleCreateMap() (map[string]interface{}, error) { + if opts.FromPort == 0 && strings.ToUpper(opts.IPProtocol) != "ICMP" { + return nil, gophercloud.ErrMissingInput{Argument: "FromPort"} + } + if opts.ToPort == 0 && strings.ToUpper(opts.IPProtocol) != "ICMP" { + return nil, gophercloud.ErrMissingInput{Argument: "ToPort"} + } + return gophercloud.BuildRequestBody(opts, "security_group_default_rule") +} + +// Create is the operation responsible for creating a new default rule. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToRuleCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(rootURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Get will return details for a particular default rule. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(resourceURL(client, id), &r.Body, nil) + return +} + +// Delete will permanently delete a default rule from the project. +func Delete(client *gophercloud.ServiceClient, id string) (r gophercloud.ErrResult) { + _, r.Err = client.Delete(resourceURL(client, id), nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/defsecrules/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/defsecrules/results.go new file mode 100644 index 0000000000..f990c9991d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/defsecrules/results.go @@ -0,0 +1,67 @@ +package defsecrules + +import ( + "encoding/json" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups" + "github.com/gophercloud/gophercloud/pagination" +) + +// DefaultRule represents a default rule - which is identical to a +// normal security rule. +type DefaultRule secgroups.Rule + +func (r *DefaultRule) UnmarshalJSON(b []byte) error { + var s secgroups.Rule + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = DefaultRule(s) + return nil +} + +// DefaultRulePage is a single page of a DefaultRule collection. +type DefaultRulePage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a page of default rules contains any results. +func (page DefaultRulePage) IsEmpty() (bool, error) { + users, err := ExtractDefaultRules(page) + return len(users) == 0, err +} + +// ExtractDefaultRules returns a slice of DefaultRules contained in a single +// page of results. +func ExtractDefaultRules(r pagination.Page) ([]DefaultRule, error) { + var s struct { + DefaultRules []DefaultRule `json:"security_group_default_rules"` + } + err := (r.(DefaultRulePage)).ExtractInto(&s) + return s.DefaultRules, err +} + +type commonResult struct { + gophercloud.Result +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// Extract will extract a DefaultRule struct from most responses. +func (r commonResult) Extract() (*DefaultRule, error) { + var s struct { + DefaultRule DefaultRule `json:"security_group_default_rule"` + } + err := r.ExtractInto(&s) + return &s.DefaultRule, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/defsecrules/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/defsecrules/testing/doc.go new file mode 100644 index 0000000000..7e51c8f15a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/defsecrules/testing/doc.go @@ -0,0 +1,2 @@ +// compute_extensions_defsecrules_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/defsecrules/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/defsecrules/testing/fixtures.go new file mode 100644 index 0000000000..e4a62d4ecc --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/defsecrules/testing/fixtures.go @@ -0,0 +1,143 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +const rootPath = "/os-security-group-default-rules" + +func mockListRulesResponse(t *testing.T) { + th.Mux.HandleFunc(rootPath, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_group_default_rules": [ + { + "from_port": 80, + "id": "{ruleID}", + "ip_protocol": "TCP", + "ip_range": { + "cidr": "10.10.10.0/24" + }, + "to_port": 80 + } + ] +} + `) + }) +} + +func mockCreateRuleResponse(t *testing.T) { + th.Mux.HandleFunc(rootPath, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "security_group_default_rule": { + "ip_protocol": "TCP", + "from_port": 80, + "to_port": 80, + "cidr": "10.10.12.0/24" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_group_default_rule": { + "from_port": 80, + "id": "{ruleID}", + "ip_protocol": "TCP", + "ip_range": { + "cidr": "10.10.12.0/24" + }, + "to_port": 80 + } +} +`) + }) +} + +func mockCreateRuleResponseICMPZero(t *testing.T) { + th.Mux.HandleFunc(rootPath, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "security_group_default_rule": { + "ip_protocol": "ICMP", + "from_port": 0, + "to_port": 0, + "cidr": "10.10.12.0/24" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_group_default_rule": { + "from_port": 0, + "id": "{ruleID}", + "ip_protocol": "ICMP", + "ip_range": { + "cidr": "10.10.12.0/24" + }, + "to_port": 0 + } +} +`) + }) +} + +func mockGetRuleResponse(t *testing.T, ruleID string) { + url := rootPath + "/" + ruleID + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_group_default_rule": { + "id": "{ruleID}", + "from_port": 80, + "to_port": 80, + "ip_protocol": "TCP", + "ip_range": { + "cidr": "10.10.12.0/24" + } + } +} + `) + }) +} + +func mockDeleteRuleResponse(t *testing.T, ruleID string) { + url := rootPath + "/" + ruleID + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusNoContent) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/defsecrules/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/defsecrules/testing/requests_test.go new file mode 100644 index 0000000000..1f2fb8686a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/defsecrules/testing/requests_test.go @@ -0,0 +1,127 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/defsecrules" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +const ruleID = "{ruleID}" + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockListRulesResponse(t) + + count := 0 + + err := defsecrules.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := defsecrules.ExtractDefaultRules(page) + th.AssertNoErr(t, err) + + expected := []defsecrules.DefaultRule{ + { + FromPort: 80, + ID: ruleID, + IPProtocol: "TCP", + IPRange: secgroups.IPRange{CIDR: "10.10.10.0/24"}, + ToPort: 80, + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, count) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockCreateRuleResponse(t) + + opts := defsecrules.CreateOpts{ + IPProtocol: "TCP", + FromPort: 80, + ToPort: 80, + CIDR: "10.10.12.0/24", + } + + group, err := defsecrules.Create(client.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) + + expected := &defsecrules.DefaultRule{ + ID: ruleID, + FromPort: 80, + ToPort: 80, + IPProtocol: "TCP", + IPRange: secgroups.IPRange{CIDR: "10.10.12.0/24"}, + } + th.AssertDeepEquals(t, expected, group) +} + +func TestCreateICMPZero(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockCreateRuleResponseICMPZero(t) + + opts := defsecrules.CreateOpts{ + IPProtocol: "ICMP", + FromPort: 0, + ToPort: 0, + CIDR: "10.10.12.0/24", + } + + group, err := defsecrules.Create(client.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) + + expected := &defsecrules.DefaultRule{ + ID: ruleID, + FromPort: 0, + ToPort: 0, + IPProtocol: "ICMP", + IPRange: secgroups.IPRange{CIDR: "10.10.12.0/24"}, + } + th.AssertDeepEquals(t, expected, group) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockGetRuleResponse(t, ruleID) + + group, err := defsecrules.Get(client.ServiceClient(), ruleID).Extract() + th.AssertNoErr(t, err) + + expected := &defsecrules.DefaultRule{ + ID: ruleID, + FromPort: 80, + ToPort: 80, + IPProtocol: "TCP", + IPRange: secgroups.IPRange{CIDR: "10.10.12.0/24"}, + } + + th.AssertDeepEquals(t, expected, group) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDeleteRuleResponse(t, ruleID) + + err := defsecrules.Delete(client.ServiceClient(), ruleID).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/defsecrules/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/defsecrules/urls.go new file mode 100644 index 0000000000..e5fbf82454 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/defsecrules/urls.go @@ -0,0 +1,13 @@ +package defsecrules + +import "github.com/gophercloud/gophercloud" + +const rulepath = "os-security-group-default-rules" + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rulepath, id) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rulepath) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/delegate.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/delegate.go new file mode 100644 index 0000000000..00e7c3becf --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/delegate.go @@ -0,0 +1,23 @@ +package extensions + +import ( + "github.com/gophercloud/gophercloud" + common "github.com/gophercloud/gophercloud/openstack/common/extensions" + "github.com/gophercloud/gophercloud/pagination" +) + +// ExtractExtensions interprets a Page as a slice of Extensions. +func ExtractExtensions(page pagination.Page) ([]common.Extension, error) { + return common.ExtractExtensions(page) +} + +// Get retrieves information for a specific extension using its alias. +func Get(c *gophercloud.ServiceClient, alias string) common.GetResult { + return common.Get(c, alias) +} + +// List returns a Pager which allows you to iterate over the full collection of extensions. +// It does not accept query parameters. +func List(c *gophercloud.ServiceClient) pagination.Pager { + return common.List(c) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/diskconfig/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/diskconfig/doc.go new file mode 100644 index 0000000000..80785faca9 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/diskconfig/doc.go @@ -0,0 +1,3 @@ +// Package diskconfig provides information and interaction with the Disk +// Config extension that works with the OpenStack Compute service. +package diskconfig diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/diskconfig/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/diskconfig/requests.go new file mode 100644 index 0000000000..41d04b9baf --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/diskconfig/requests.go @@ -0,0 +1,103 @@ +package diskconfig + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" +) + +// DiskConfig represents one of the two possible settings for the DiskConfig option when creating, +// rebuilding, or resizing servers: Auto or Manual. +type DiskConfig string + +const ( + // Auto builds a server with a single partition the size of the target flavor disk and + // automatically adjusts the filesystem to fit the entire partition. Auto may only be used with + // images and servers that use a single EXT3 partition. + Auto DiskConfig = "AUTO" + + // Manual builds a server using whatever partition scheme and filesystem are present in the source + // image. If the target flavor disk is larger, the remaining space is left unpartitioned. This + // enables images to have non-EXT3 filesystems, multiple partitions, and so on, and enables you + // to manage the disk configuration. It also results in slightly shorter boot times. + Manual DiskConfig = "MANUAL" +) + +// CreateOptsExt adds a DiskConfig option to the base CreateOpts. +type CreateOptsExt struct { + servers.CreateOptsBuilder + + // DiskConfig [optional] controls how the created server's disk is partitioned. + DiskConfig DiskConfig `json:"OS-DCF:diskConfig,omitempty"` +} + +// ToServerCreateMap adds the diskconfig option to the base server creation options. +func (opts CreateOptsExt) ToServerCreateMap() (map[string]interface{}, error) { + base, err := opts.CreateOptsBuilder.ToServerCreateMap() + if err != nil { + return nil, err + } + + if string(opts.DiskConfig) == "" { + return base, nil + } + + serverMap := base["server"].(map[string]interface{}) + serverMap["OS-DCF:diskConfig"] = string(opts.DiskConfig) + + return base, nil +} + +// RebuildOptsExt adds a DiskConfig option to the base RebuildOpts. +type RebuildOptsExt struct { + servers.RebuildOptsBuilder + // DiskConfig controls how the rebuilt server's disk is partitioned. + DiskConfig DiskConfig `json:"OS-DCF:diskConfig,omitempty"` +} + +// ToServerRebuildMap adds the diskconfig option to the base server rebuild options. +func (opts RebuildOptsExt) ToServerRebuildMap() (map[string]interface{}, error) { + if opts.DiskConfig != Auto && opts.DiskConfig != Manual { + err := gophercloud.ErrInvalidInput{} + err.Argument = "diskconfig.RebuildOptsExt.DiskConfig" + err.Info = "Must be either diskconfig.Auto or diskconfig.Manual" + return nil, err + } + + base, err := opts.RebuildOptsBuilder.ToServerRebuildMap() + if err != nil { + return nil, err + } + + serverMap := base["rebuild"].(map[string]interface{}) + serverMap["OS-DCF:diskConfig"] = string(opts.DiskConfig) + + return base, nil +} + +// ResizeOptsExt adds a DiskConfig option to the base server resize options. +type ResizeOptsExt struct { + servers.ResizeOptsBuilder + + // DiskConfig [optional] controls how the resized server's disk is partitioned. + DiskConfig DiskConfig +} + +// ToServerResizeMap adds the diskconfig option to the base server creation options. +func (opts ResizeOptsExt) ToServerResizeMap() (map[string]interface{}, error) { + if opts.DiskConfig != Auto && opts.DiskConfig != Manual { + err := gophercloud.ErrInvalidInput{} + err.Argument = "diskconfig.ResizeOptsExt.DiskConfig" + err.Info = "Must be either diskconfig.Auto or diskconfig.Manual" + return nil, err + } + + base, err := opts.ResizeOptsBuilder.ToServerResizeMap() + if err != nil { + return nil, err + } + + serverMap := base["resize"].(map[string]interface{}) + serverMap["OS-DCF:diskConfig"] = string(opts.DiskConfig) + + return base, nil +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/diskconfig/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/diskconfig/results.go new file mode 100644 index 0000000000..3ba66f5196 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/diskconfig/results.go @@ -0,0 +1,17 @@ +package diskconfig + +import "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + +type ServerWithDiskConfig struct { + servers.Server + DiskConfig DiskConfig `json:"OS-DCF:diskConfig"` +} + +func (s ServerWithDiskConfig) ToServerCreateResult() (m map[string]interface{}) { + m["OS-DCF:diskConfig"] = s.DiskConfig + return +} + +type CreateServerResultBuilder interface { + ToServerCreateResult() map[string]interface{} +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/diskconfig/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/diskconfig/testing/doc.go new file mode 100644 index 0000000000..54c863b523 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/diskconfig/testing/doc.go @@ -0,0 +1,2 @@ +// compute_extensions_diskconfig_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/diskconfig/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/diskconfig/testing/requests_test.go new file mode 100644 index 0000000000..6ce560aa61 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/diskconfig/testing/requests_test.go @@ -0,0 +1,88 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/diskconfig" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestCreateOpts(t *testing.T) { + base := servers.CreateOpts{ + Name: "createdserver", + ImageRef: "asdfasdfasdf", + FlavorRef: "performance1-1", + } + + ext := diskconfig.CreateOptsExt{ + CreateOptsBuilder: base, + DiskConfig: diskconfig.Manual, + } + + expected := ` + { + "server": { + "name": "createdserver", + "imageRef": "asdfasdfasdf", + "flavorRef": "performance1-1", + "OS-DCF:diskConfig": "MANUAL" + } + } + ` + actual, err := ext.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expected, actual) +} + +func TestRebuildOpts(t *testing.T) { + base := servers.RebuildOpts{ + Name: "rebuiltserver", + AdminPass: "swordfish", + ImageID: "asdfasdfasdf", + } + + ext := diskconfig.RebuildOptsExt{ + RebuildOptsBuilder: base, + DiskConfig: diskconfig.Auto, + } + + actual, err := ext.ToServerRebuildMap() + th.AssertNoErr(t, err) + + expected := ` + { + "rebuild": { + "name": "rebuiltserver", + "imageRef": "asdfasdfasdf", + "adminPass": "swordfish", + "OS-DCF:diskConfig": "AUTO" + } + } + ` + th.CheckJSONEquals(t, expected, actual) +} + +func TestResizeOpts(t *testing.T) { + base := servers.ResizeOpts{ + FlavorRef: "performance1-8", + } + + ext := diskconfig.ResizeOptsExt{ + ResizeOptsBuilder: base, + DiskConfig: diskconfig.Auto, + } + + actual, err := ext.ToServerResizeMap() + th.AssertNoErr(t, err) + + expected := ` + { + "resize": { + "flavorRef": "performance1-8", + "OS-DCF:diskConfig": "AUTO" + } + } + ` + th.CheckJSONEquals(t, expected, actual) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/doc.go new file mode 100644 index 0000000000..2b447da1d6 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/doc.go @@ -0,0 +1,3 @@ +// Package extensions provides information and interaction with the +// different extensions available for the OpenStack Compute service. +package extensions diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips/doc.go new file mode 100644 index 0000000000..6682fa6290 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips/doc.go @@ -0,0 +1,3 @@ +// Package floatingips provides the ability to manage floating ips through +// nova-network +package floatingips diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips/requests.go new file mode 100644 index 0000000000..b36aeba59c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips/requests.go @@ -0,0 +1,112 @@ +package floatingips + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// List returns a Pager that allows you to iterate over a collection of FloatingIPs. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page { + return FloatingIPPage{pagination.SinglePageBase(r)} + }) +} + +// CreateOptsBuilder describes struct types that can be accepted by the Create call. Notable, the +// CreateOpts struct in this package does. +type CreateOptsBuilder interface { + ToFloatingIPCreateMap() (map[string]interface{}, error) +} + +// CreateOpts specifies a Floating IP allocation request +type CreateOpts struct { + // Pool is the pool of floating IPs to allocate one from + Pool string `json:"pool" required:"true"` +} + +// ToFloatingIPCreateMap constructs a request body from CreateOpts. +func (opts CreateOpts) ToFloatingIPCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "") +} + +// Create requests the creation of a new floating IP +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToFloatingIPCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Get returns data about a previously created FloatingIP. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// Delete requests the deletion of a previous allocated FloatingIP. +func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} + +// AssociateOptsBuilder is the interface types must satfisfy to be used as +// Associate options +type AssociateOptsBuilder interface { + ToFloatingIPAssociateMap() (map[string]interface{}, error) +} + +// AssociateOpts specifies the required information to associate a floating IP with an instance +type AssociateOpts struct { + // FloatingIP is the floating IP to associate with an instance + FloatingIP string `json:"address" required:"true"` + // FixedIP is an optional fixed IP address of the server + FixedIP string `json:"fixed_address,omitempty"` +} + +// ToFloatingIPAssociateMap constructs a request body from AssociateOpts. +func (opts AssociateOpts) ToFloatingIPAssociateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "addFloatingIp") +} + +// AssociateInstance pairs an allocated floating IP with an instance. +func AssociateInstance(client *gophercloud.ServiceClient, serverID string, opts AssociateOptsBuilder) (r AssociateResult) { + b, err := opts.ToFloatingIPAssociateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(associateURL(client, serverID), b, nil, nil) + return +} + +// DisassociateOptsBuilder is the interface types must satfisfy to be used as +// Disassociate options +type DisassociateOptsBuilder interface { + ToFloatingIPDisassociateMap() (map[string]interface{}, error) +} + +// DisassociateOpts specifies the required information to disassociate a floating IP with an instance +type DisassociateOpts struct { + FloatingIP string `json:"address" required:"true"` +} + +// ToFloatingIPDisassociateMap constructs a request body from AssociateOpts. +func (opts DisassociateOpts) ToFloatingIPDisassociateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "removeFloatingIp") +} + +// DisassociateInstance decouples an allocated floating IP from an instance +func DisassociateInstance(client *gophercloud.ServiceClient, serverID string, opts DisassociateOptsBuilder) (r DisassociateResult) { + b, err := opts.ToFloatingIPDisassociateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(disassociateURL(client, serverID), b, nil, nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips/results.go new file mode 100644 index 0000000000..2f5b33844e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips/results.go @@ -0,0 +1,117 @@ +package floatingips + +import ( + "encoding/json" + "strconv" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// A FloatingIP is an IP that can be associated with an instance +type FloatingIP struct { + // ID is a unique ID of the Floating IP + ID string `json:"-"` + + // FixedIP is the IP of the instance related to the Floating IP + FixedIP string `json:"fixed_ip,omitempty"` + + // InstanceID is the ID of the instance that is using the Floating IP + InstanceID string `json:"instance_id"` + + // IP is the actual Floating IP + IP string `json:"ip"` + + // Pool is the pool of floating IPs that this floating IP belongs to + Pool string `json:"pool"` +} + +func (r *FloatingIP) UnmarshalJSON(b []byte) error { + type tmp FloatingIP + var s struct { + tmp + ID interface{} `json:"id"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = FloatingIP(s.tmp) + + switch t := s.ID.(type) { + case float64: + r.ID = strconv.FormatFloat(t, 'f', -1, 64) + case string: + r.ID = t + } + + return err +} + +// FloatingIPPage stores a single, only page of FloatingIPs +// results from a List call. +type FloatingIPPage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a FloatingIPsPage is empty. +func (page FloatingIPPage) IsEmpty() (bool, error) { + va, err := ExtractFloatingIPs(page) + return len(va) == 0, err +} + +// ExtractFloatingIPs interprets a page of results as a slice of +// FloatingIPs. +func ExtractFloatingIPs(r pagination.Page) ([]FloatingIP, error) { + var s struct { + FloatingIPs []FloatingIP `json:"floating_ips"` + } + err := (r.(FloatingIPPage)).ExtractInto(&s) + return s.FloatingIPs, err +} + +// FloatingIPResult is the raw result from a FloatingIP request. +type FloatingIPResult struct { + gophercloud.Result +} + +// Extract is a method that attempts to interpret any FloatingIP resource +// response as a FloatingIP struct. +func (r FloatingIPResult) Extract() (*FloatingIP, error) { + var s struct { + FloatingIP *FloatingIP `json:"floating_ip"` + } + err := r.ExtractInto(&s) + return s.FloatingIP, err +} + +// CreateResult is the response from a Create operation. Call its Extract method to interpret it +// as a FloatingIP. +type CreateResult struct { + FloatingIPResult +} + +// GetResult is the response from a Get operation. Call its Extract method to interpret it +// as a FloatingIP. +type GetResult struct { + FloatingIPResult +} + +// DeleteResult is the response from a Delete operation. Call its Extract method to determine if +// the call succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// AssociateResult is the response from a Delete operation. Call its Extract method to determine if +// the call succeeded or failed. +type AssociateResult struct { + gophercloud.ErrResult +} + +// DisassociateResult is the response from a Delete operation. Call its Extract method to determine if +// the call succeeded or failed. +type DisassociateResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips/testing/doc.go new file mode 100644 index 0000000000..961aeee276 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips/testing/doc.go @@ -0,0 +1,2 @@ +// compute_extensions_floatingips_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips/testing/fixtures.go new file mode 100644 index 0000000000..6866e265de --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips/testing/fixtures.go @@ -0,0 +1,223 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +// ListOutput is a sample response to a List call. +const ListOutput = ` +{ + "floating_ips": [ + { + "fixed_ip": null, + "id": "1", + "instance_id": null, + "ip": "10.10.10.1", + "pool": "nova" + }, + { + "fixed_ip": "166.78.185.201", + "id": "2", + "instance_id": "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + "ip": "10.10.10.2", + "pool": "nova" + } + ] +} +` + +// GetOutput is a sample response to a Get call. +const GetOutput = ` +{ + "floating_ip": { + "fixed_ip": "166.78.185.201", + "id": "2", + "instance_id": "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + "ip": "10.10.10.2", + "pool": "nova" + } +} +` + +// CreateOutput is a sample response to a Post call +const CreateOutput = ` +{ + "floating_ip": { + "fixed_ip": null, + "id": "1", + "instance_id": null, + "ip": "10.10.10.1", + "pool": "nova" + } +} +` + +// CreateOutputWithNumericID is a sample response to a Post call +// with a legacy nova-network-based numeric ID. +const CreateOutputWithNumericID = ` +{ + "floating_ip": { + "fixed_ip": null, + "id": 1, + "instance_id": null, + "ip": "10.10.10.1", + "pool": "nova" + } +} +` + +// FirstFloatingIP is the first result in ListOutput. +var FirstFloatingIP = floatingips.FloatingIP{ + ID: "1", + IP: "10.10.10.1", + Pool: "nova", +} + +// SecondFloatingIP is the first result in ListOutput. +var SecondFloatingIP = floatingips.FloatingIP{ + FixedIP: "166.78.185.201", + ID: "2", + InstanceID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + IP: "10.10.10.2", + Pool: "nova", +} + +// ExpectedFloatingIPsSlice is the slice of results that should be parsed +// from ListOutput, in the expected order. +var ExpectedFloatingIPsSlice = []floatingips.FloatingIP{FirstFloatingIP, SecondFloatingIP} + +// CreatedFloatingIP is the parsed result from CreateOutput. +var CreatedFloatingIP = floatingips.FloatingIP{ + ID: "1", + IP: "10.10.10.1", + Pool: "nova", +} + +// HandleListSuccessfully configures the test server to respond to a List request. +func HandleListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-floating-ips", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ListOutput) + }) +} + +// HandleGetSuccessfully configures the test server to respond to a Get request +// for an existing floating ip +func HandleGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-floating-ips/2", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetOutput) + }) +} + +// HandleCreateSuccessfully configures the test server to respond to a Create request +// for a new floating ip +func HandleCreateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-floating-ips", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, ` +{ + "pool": "nova" +} +`) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, CreateOutput) + }) +} + +// HandleCreateWithNumericIDSuccessfully configures the test server to respond to a Create request +// for a new floating ip +func HandleCreateWithNumericIDSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-floating-ips", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, ` +{ + "pool": "nova" +} +`) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, CreateOutputWithNumericID) + }) +} + +// HandleDeleteSuccessfully configures the test server to respond to a Delete request for a +// an existing floating ip +func HandleDeleteSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-floating-ips/1", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusAccepted) + }) +} + +// HandleAssociateSuccessfully configures the test server to respond to a Post request +// to associate an allocated floating IP +func HandleAssociateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/4d8c3732-a248-40ed-bebc-539a6ffd25c0/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, ` +{ + "addFloatingIp": { + "address": "10.10.10.2" + } +} +`) + + w.WriteHeader(http.StatusAccepted) + }) +} + +// HandleFixedAssociateSucessfully configures the test server to respond to a Post request +// to associate an allocated floating IP with a specific fixed IP address +func HandleAssociateFixedSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/4d8c3732-a248-40ed-bebc-539a6ffd25c0/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, ` +{ + "addFloatingIp": { + "address": "10.10.10.2", + "fixed_address": "166.78.185.201" + } +} +`) + + w.WriteHeader(http.StatusAccepted) + }) +} + +// HandleDisassociateSuccessfully configures the test server to respond to a Post request +// to disassociate an allocated floating IP +func HandleDisassociateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/4d8c3732-a248-40ed-bebc-539a6ffd25c0/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, ` +{ + "removeFloatingIp": { + "address": "10.10.10.2" + } +} +`) + + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips/testing/requests_test.go new file mode 100644 index 0000000000..2356671e02 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips/testing/requests_test.go @@ -0,0 +1,111 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListSuccessfully(t) + + count := 0 + err := floatingips.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := floatingips.ExtractFloatingIPs(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedFloatingIPsSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateSuccessfully(t) + + actual, err := floatingips.Create(client.ServiceClient(), floatingips.CreateOpts{ + Pool: "nova", + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &CreatedFloatingIP, actual) +} + +func TestCreateWithNumericID(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateWithNumericIDSuccessfully(t) + + actual, err := floatingips.Create(client.ServiceClient(), floatingips.CreateOpts{ + Pool: "nova", + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &CreatedFloatingIP, actual) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetSuccessfully(t) + + actual, err := floatingips.Get(client.ServiceClient(), "2").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &SecondFloatingIP, actual) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteSuccessfully(t) + + err := floatingips.Delete(client.ServiceClient(), "1").ExtractErr() + th.AssertNoErr(t, err) +} + +func TestAssociate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleAssociateSuccessfully(t) + + associateOpts := floatingips.AssociateOpts{ + FloatingIP: "10.10.10.2", + } + + err := floatingips.AssociateInstance(client.ServiceClient(), "4d8c3732-a248-40ed-bebc-539a6ffd25c0", associateOpts).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestAssociateFixed(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleAssociateFixedSuccessfully(t) + + associateOpts := floatingips.AssociateOpts{ + FloatingIP: "10.10.10.2", + FixedIP: "166.78.185.201", + } + + err := floatingips.AssociateInstance(client.ServiceClient(), "4d8c3732-a248-40ed-bebc-539a6ffd25c0", associateOpts).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestDisassociateInstance(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDisassociateSuccessfully(t) + + disassociateOpts := floatingips.DisassociateOpts{ + FloatingIP: "10.10.10.2", + } + + err := floatingips.DisassociateInstance(client.ServiceClient(), "4d8c3732-a248-40ed-bebc-539a6ffd25c0", disassociateOpts).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips/urls.go new file mode 100644 index 0000000000..4768e5a897 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips/urls.go @@ -0,0 +1,37 @@ +package floatingips + +import "github.com/gophercloud/gophercloud" + +const resourcePath = "os-floating-ips" + +func resourceURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(resourcePath) +} + +func listURL(c *gophercloud.ServiceClient) string { + return resourceURL(c) +} + +func createURL(c *gophercloud.ServiceClient) string { + return resourceURL(c) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id) +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return getURL(c, id) +} + +func serverURL(c *gophercloud.ServiceClient, serverID string) string { + return c.ServiceURL("servers/" + serverID + "/action") +} + +func associateURL(c *gophercloud.ServiceClient, serverID string) string { + return serverURL(c, serverID) +} + +func disassociateURL(c *gophercloud.ServiceClient, serverID string) string { + return serverURL(c, serverID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors/doc.go new file mode 100644 index 0000000000..026f3ddf75 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors/doc.go @@ -0,0 +1,3 @@ +// Package hypervisors gives information and control of the os-hypervisors +// portion of the compute API +package hypervisors diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors/requests.go new file mode 100644 index 0000000000..57cc19a71f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors/requests.go @@ -0,0 +1,13 @@ +package hypervisors + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// List makes a request against the API to list hypervisors. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, hypervisorsListDetailURL(client), func(r pagination.PageResult) pagination.Page { + return HypervisorPage{pagination.SinglePageBase(r)} + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors/results.go new file mode 100644 index 0000000000..844aa65c50 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors/results.go @@ -0,0 +1,161 @@ +package hypervisors + +import ( + "encoding/json" + "fmt" + + "github.com/gophercloud/gophercloud/pagination" +) + +type Topology struct { + Sockets int `json:"sockets"` + Cores int `json:"cores"` + Threads int `json:"threads"` +} + +type CPUInfo struct { + Vendor string `json:"vendor"` + Arch string `json:"arch"` + Model string `json:"model"` + Features []string `json:"features"` + Topology Topology `json:"topology"` +} + +type Service struct { + Host string `json:"host"` + ID int `json:"id"` + DisabledReason string `json:"disabled_reason"` +} + +type Hypervisor struct { + // A structure that contains cpu information like arch, model, vendor, features and topology + CPUInfo CPUInfo `json:"-"` + // The current_workload is the number of tasks the hypervisor is responsible for. + // This will be equal or greater than the number of active VMs on the system + // (it can be greater when VMs are being deleted and the hypervisor is still cleaning up). + CurrentWorkload int `json:"current_workload"` + // Status of the hypervisor, either "enabled" or "disabled" + Status string `json:"status"` + // State of the hypervisor, either "up" or "down" + State string `json:"state"` + // Actual free disk on this hypervisor in GB + DiskAvailableLeast int `json:"disk_available_least"` + // The hypervisor's IP address + HostIP string `json:"host_ip"` + // The free disk remaining on this hypervisor in GB + FreeDiskGB int `json:"-"` + // The free RAM in this hypervisor in MB + FreeRamMB int `json:"free_ram_mb"` + // The hypervisor host name + HypervisorHostname string `json:"hypervisor_hostname"` + // The hypervisor type + HypervisorType string `json:"hypervisor_type"` + // The hypervisor version + HypervisorVersion int `json:"-"` + // Unique ID of the hypervisor + ID int `json:"id"` + // The disk in this hypervisor in GB + LocalGB int `json:"-"` + // The disk used in this hypervisor in GB + LocalGBUsed int `json:"local_gb_used"` + // The memory of this hypervisor in MB + MemoryMB int `json:"memory_mb"` + // The memory used in this hypervisor in MB + MemoryMBUsed int `json:"memory_mb_used"` + // The number of running vms on this hypervisor + RunningVMs int `json:"running_vms"` + // The hypervisor service object + Service Service `json:"service"` + // The number of vcpu in this hypervisor + VCPUs int `json:"vcpus"` + // The number of vcpu used in this hypervisor + VCPUsUsed int `json:"vcpus_used"` +} + +func (r *Hypervisor) UnmarshalJSON(b []byte) error { + + type tmp Hypervisor + var s struct { + tmp + CPUInfo interface{} `json:"cpu_info"` + HypervisorVersion interface{} `json:"hypervisor_version"` + FreeDiskGB interface{} `json:"free_disk_gb"` + LocalGB interface{} `json:"local_gb"` + } + + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = Hypervisor(s.tmp) + + // Newer versions pass the CPU into around as the correct types, this just needs + // converting and copying into place. Older versions pass CPU info around as a string + // and can simply be unmarshalled by the json parser + var tmpb []byte + + switch t := s.CPUInfo.(type) { + case string: + tmpb = []byte(t) + case map[string]interface{}: + tmpb, err = json.Marshal(t) + if err != nil { + return err + } + default: + return fmt.Errorf("CPUInfo has unexpected type: %T", t) + } + + err = json.Unmarshal(tmpb, &r.CPUInfo) + if err != nil { + return err + } + + // These fields may be passed in in scientific notation + switch t := s.HypervisorVersion.(type) { + case int: + r.HypervisorVersion = t + case float64: + r.HypervisorVersion = int(t) + default: + return fmt.Errorf("Hypervisor version of unexpected type") + } + + switch t := s.FreeDiskGB.(type) { + case int: + r.FreeDiskGB = t + case float64: + r.FreeDiskGB = int(t) + default: + return fmt.Errorf("Free disk GB of unexpected type") + } + + switch t := s.LocalGB.(type) { + case int: + r.LocalGB = t + case float64: + r.LocalGB = int(t) + default: + return fmt.Errorf("Local GB of unexpected type") + } + + return nil +} + +type HypervisorPage struct { + pagination.SinglePageBase +} + +func (page HypervisorPage) IsEmpty() (bool, error) { + va, err := ExtractHypervisors(page) + return len(va) == 0, err +} + +func ExtractHypervisors(p pagination.Page) ([]Hypervisor, error) { + var h struct { + Hypervisors []Hypervisor `json:"hypervisors"` + } + err := (p.(HypervisorPage)).ExtractInto(&h) + return h.Hypervisors, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors/testing/fixtures.go new file mode 100644 index 0000000000..45a32de18d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors/testing/fixtures.go @@ -0,0 +1,136 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors" + "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +// The first hypervisor represents what the specification says (~Newton) +// The second is exactly the same, but what you can get off a real system (~Kilo) +const HypervisorListBody = ` +{ + "hypervisors": [ + { + "cpu_info": { + "arch": "x86_64", + "model": "Nehalem", + "vendor": "Intel", + "features": [ + "pge", + "clflush" + ], + "topology": { + "cores": 1, + "threads": 1, + "sockets": 4 + } + }, + "current_workload": 0, + "status": "enabled", + "state": "up", + "disk_available_least": 0, + "host_ip": "1.1.1.1", + "free_disk_gb": 1028, + "free_ram_mb": 7680, + "hypervisor_hostname": "fake-mini", + "hypervisor_type": "fake", + "hypervisor_version": 2002000, + "id": 1, + "local_gb": 1028, + "local_gb_used": 0, + "memory_mb": 8192, + "memory_mb_used": 512, + "running_vms": 0, + "service": { + "host": "e6a37ee802d74863ab8b91ade8f12a67", + "id": 2, + "disabled_reason": null + }, + "vcpus": 1, + "vcpus_used": 0 + }, + { + "cpu_info": "{\"arch\": \"x86_64\", \"model\": \"Nehalem\", \"vendor\": \"Intel\", \"features\": [\"pge\", \"clflush\"], \"topology\": {\"cores\": 1, \"threads\": 1, \"sockets\": 4}}", + "current_workload": 0, + "status": "enabled", + "state": "up", + "disk_available_least": 0, + "host_ip": "1.1.1.1", + "free_disk_gb": 1028, + "free_ram_mb": 7680, + "hypervisor_hostname": "fake-mini", + "hypervisor_type": "fake", + "hypervisor_version": 2.002e+06, + "id": 1, + "local_gb": 1028, + "local_gb_used": 0, + "memory_mb": 8192, + "memory_mb_used": 512, + "running_vms": 0, + "service": { + "host": "e6a37ee802d74863ab8b91ade8f12a67", + "id": 2, + "disabled_reason": null + }, + "vcpus": 1, + "vcpus_used": 0 + } + ] +}` + +var ( + HypervisorFake = hypervisors.Hypervisor{ + CPUInfo: hypervisors.CPUInfo{ + Arch: "x86_64", + Model: "Nehalem", + Vendor: "Intel", + Features: []string{ + "pge", + "clflush", + }, + Topology: hypervisors.Topology{ + Cores: 1, + Threads: 1, + Sockets: 4, + }, + }, + CurrentWorkload: 0, + Status: "enabled", + State: "up", + DiskAvailableLeast: 0, + HostIP: "1.1.1.1", + FreeDiskGB: 1028, + FreeRamMB: 7680, + HypervisorHostname: "fake-mini", + HypervisorType: "fake", + HypervisorVersion: 2002000, + ID: 1, + LocalGB: 1028, + LocalGBUsed: 0, + MemoryMB: 8192, + MemoryMBUsed: 512, + RunningVMs: 0, + Service: hypervisors.Service{ + Host: "e6a37ee802d74863ab8b91ade8f12a67", + ID: 2, + DisabledReason: "", + }, + VCPUs: 1, + VCPUsUsed: 0, + } +) + +func HandleHypervisorListSuccessfully(t *testing.T) { + testhelper.Mux.HandleFunc("/os-hypervisors/detail", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "GET") + testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, HypervisorListBody) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors/testing/requests_test.go new file mode 100644 index 0000000000..1da3b1de50 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors/testing/requests_test.go @@ -0,0 +1,53 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors" + "github.com/gophercloud/gophercloud/pagination" + "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestListHypervisors(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + HandleHypervisorListSuccessfully(t) + + pages := 0 + err := hypervisors.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := hypervisors.ExtractHypervisors(page) + if err != nil { + return false, err + } + + if len(actual) != 2 { + t.Fatalf("Expected 2 hypervisors, got %d", len(actual)) + } + testhelper.CheckDeepEquals(t, HypervisorFake, actual[0]) + testhelper.CheckDeepEquals(t, HypervisorFake, actual[1]) + + return true, nil + }) + + testhelper.AssertNoErr(t, err) + + if pages != 1 { + t.Errorf("Expected 1 page, saw %d", pages) + } +} + +func TestListAllHypervisors(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + HandleHypervisorListSuccessfully(t) + + allPages, err := hypervisors.List(client.ServiceClient()).AllPages() + testhelper.AssertNoErr(t, err) + actual, err := hypervisors.ExtractHypervisors(allPages) + testhelper.AssertNoErr(t, err) + testhelper.CheckDeepEquals(t, HypervisorFake, actual[0]) + testhelper.CheckDeepEquals(t, HypervisorFake, actual[1]) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors/urls.go new file mode 100644 index 0000000000..5e6f679e96 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors/urls.go @@ -0,0 +1,7 @@ +package hypervisors + +import "github.com/gophercloud/gophercloud" + +func hypervisorsListDetailURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("os-hypervisors", "detail") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/doc.go new file mode 100644 index 0000000000..856f41bacc --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/doc.go @@ -0,0 +1,3 @@ +// Package keypairs provides information and interaction with the Keypairs +// extension for the OpenStack Compute service. +package keypairs diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/requests.go new file mode 100644 index 0000000000..adf1e5596f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/requests.go @@ -0,0 +1,84 @@ +package keypairs + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + "github.com/gophercloud/gophercloud/pagination" +) + +// CreateOptsExt adds a KeyPair option to the base CreateOpts. +type CreateOptsExt struct { + servers.CreateOptsBuilder + KeyName string `json:"key_name,omitempty"` +} + +// ToServerCreateMap adds the key_name and, optionally, key_data options to +// the base server creation options. +func (opts CreateOptsExt) ToServerCreateMap() (map[string]interface{}, error) { + base, err := opts.CreateOptsBuilder.ToServerCreateMap() + if err != nil { + return nil, err + } + + if opts.KeyName == "" { + return base, nil + } + + serverMap := base["server"].(map[string]interface{}) + serverMap["key_name"] = opts.KeyName + + return base, nil +} + +// List returns a Pager that allows you to iterate over a collection of KeyPairs. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page { + return KeyPairPage{pagination.SinglePageBase(r)} + }) +} + +// CreateOptsBuilder describes struct types that can be accepted by the Create call. Notable, the +// CreateOpts struct in this package does. +type CreateOptsBuilder interface { + ToKeyPairCreateMap() (map[string]interface{}, error) +} + +// CreateOpts specifies keypair creation or import parameters. +type CreateOpts struct { + // Name is a friendly name to refer to this KeyPair in other services. + Name string `json:"name" required:"true"` + // PublicKey [optional] is a pregenerated OpenSSH-formatted public key. If provided, this key + // will be imported and no new key will be created. + PublicKey string `json:"public_key,omitempty"` +} + +// ToKeyPairCreateMap constructs a request body from CreateOpts. +func (opts CreateOpts) ToKeyPairCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "keypair") +} + +// Create requests the creation of a new keypair on the server, or to import a pre-existing +// keypair. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToKeyPairCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Get returns public data about a previously uploaded KeyPair. +func Get(client *gophercloud.ServiceClient, name string) (r GetResult) { + _, r.Err = client.Get(getURL(client, name), &r.Body, nil) + return +} + +// Delete requests the deletion of a previous stored KeyPair from the server. +func Delete(client *gophercloud.ServiceClient, name string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, name), nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/results.go new file mode 100644 index 0000000000..4c785a24cd --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/results.go @@ -0,0 +1,86 @@ +package keypairs + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// KeyPair is an SSH key known to the OpenStack Cloud that is available to be injected into +// servers. +type KeyPair struct { + // Name is used to refer to this keypair from other services within this region. + Name string `json:"name"` + + // Fingerprint is a short sequence of bytes that can be used to authenticate or validate a longer + // public key. + Fingerprint string `json:"fingerprint"` + + // PublicKey is the public key from this pair, in OpenSSH format. "ssh-rsa AAAAB3Nz..." + PublicKey string `json:"public_key"` + + // PrivateKey is the private key from this pair, in PEM format. + // "-----BEGIN RSA PRIVATE KEY-----\nMIICXA..." It is only present if this keypair was just + // returned from a Create call + PrivateKey string `json:"private_key"` + + // UserID is the user who owns this keypair. + UserID string `json:"user_id"` +} + +// KeyPairPage stores a single, only page of KeyPair results from a List call. +type KeyPairPage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a KeyPairPage is empty. +func (page KeyPairPage) IsEmpty() (bool, error) { + ks, err := ExtractKeyPairs(page) + return len(ks) == 0, err +} + +// ExtractKeyPairs interprets a page of results as a slice of KeyPairs. +func ExtractKeyPairs(r pagination.Page) ([]KeyPair, error) { + type pair struct { + KeyPair KeyPair `json:"keypair"` + } + var s struct { + KeyPairs []pair `json:"keypairs"` + } + err := (r.(KeyPairPage)).ExtractInto(&s) + results := make([]KeyPair, len(s.KeyPairs)) + for i, pair := range s.KeyPairs { + results[i] = pair.KeyPair + } + return results, err +} + +type keyPairResult struct { + gophercloud.Result +} + +// Extract is a method that attempts to interpret any KeyPair resource response as a KeyPair struct. +func (r keyPairResult) Extract() (*KeyPair, error) { + var s struct { + KeyPair *KeyPair `json:"keypair"` + } + err := r.ExtractInto(&s) + return s.KeyPair, err +} + +// CreateResult is the response from a Create operation. Call its Extract method to interpret it +// as a KeyPair. +type CreateResult struct { + keyPairResult +} + +// GetResult is the response from a Get operation. Call its Extract method to interpret it +// as a KeyPair. +type GetResult struct { + keyPairResult +} + +// DeleteResult is the response from a Delete operation. Call its Extract method to determine if +// the call succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/testing/doc.go new file mode 100644 index 0000000000..8f8aaca7d8 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/testing/doc.go @@ -0,0 +1,2 @@ +// compute_extensions_keypairs_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/testing/fixtures.go new file mode 100644 index 0000000000..dc716d8db4 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/testing/fixtures.go @@ -0,0 +1,170 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +// ListOutput is a sample response to a List call. +const ListOutput = ` +{ + "keypairs": [ + { + "keypair": { + "fingerprint": "15:b0:f8:b3:f9:48:63:71:cf:7b:5b:38:6d:44:2d:4a", + "name": "firstkey", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC+Eo/RZRngaGTkFs7I62ZjsIlO79KklKbMXi8F+KITD4bVQHHn+kV+4gRgkgCRbdoDqoGfpaDFs877DYX9n4z6FrAIZ4PES8TNKhatifpn9NdQYWA+IkU8CuvlEKGuFpKRi/k7JLos/gHi2hy7QUwgtRvcefvD/vgQZOVw/mGR9Q== Generated by Nova\n" + } + }, + { + "keypair": { + "fingerprint": "35:9d:d0:c3:4a:80:d3:d8:86:f1:ca:f7:df:c4:f9:d8", + "name": "secondkey", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7DUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5Q== Generated by Nova\n" + } + } + ] +} +` + +// GetOutput is a sample response to a Get call. +const GetOutput = ` +{ + "keypair": { + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC+Eo/RZRngaGTkFs7I62ZjsIlO79KklKbMXi8F+KITD4bVQHHn+kV+4gRgkgCRbdoDqoGfpaDFs877DYX9n4z6FrAIZ4PES8TNKhatifpn9NdQYWA+IkU8CuvlEKGuFpKRi/k7JLos/gHi2hy7QUwgtRvcefvD/vgQZOVw/mGR9Q== Generated by Nova\n", + "name": "firstkey", + "fingerprint": "15:b0:f8:b3:f9:48:63:71:cf:7b:5b:38:6d:44:2d:4a" + } +} +` + +// CreateOutput is a sample response to a Create call. +const CreateOutput = ` +{ + "keypair": { + "fingerprint": "35:9d:d0:c3:4a:80:d3:d8:86:f1:ca:f7:df:c4:f9:d8", + "name": "createdkey", + "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIICXAIBAAKBgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7\nDUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ\n9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5QIDAQAB\nAoGAE5XO1mDhORy9COvsg+kYPUhB1GsCYxh+v88wG7HeFDKBY6KUc/Kxo6yoGn5T\nTjRjekyi2KoDZHz4VlIzyZPwFS4I1bf3oCunVoAKzgLdmnTtvRNMC5jFOGc2vUgP\n9bSyRj3S1R4ClVk2g0IDeagko/jc8zzLEYuIK+fbkds79YECQQDt3vcevgegnkga\ntF4NsDmmBPRkcSHCqrANP/7vFcBQN3czxeYYWX3DK07alu6GhH1Y4sHbdm616uU0\nll7xbDzxAkEAzAtN2IyftNygV2EGiaGgqLyo/tD9+Vui2qCQplqe4jvWh/5Sparl\nOjmKo+uAW+hLrLVMnHzRWxbWU8hirH5FNQJATO+ZxCK4etXXAnQmG41NCAqANWB2\nB+2HJbH2NcQ2QHvAHUm741JGn/KI/aBlo7KEjFRDWUVUB5ji64BbUwCsMQJBAIku\nLGcjnBf/oLk+XSPZC2eGd2Ph5G5qYmH0Q2vkTx+wtTn3DV+eNsDfgMtWAJVJ5t61\ngU1QSXyhLPVlKpnnxuUCQC+xvvWjWtsLaFtAsZywJiqLxQzHts8XLGZptYJ5tLWV\nrtmYtBcJCN48RrgQHry/xWYeA4K/AFQpXfNPgprQ96Q=\n-----END RSA PRIVATE KEY-----\n", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7DUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5Q== Generated by Nova\n", + "user_id": "fake" + } +} +` + +// ImportOutput is a sample response to a Create call that provides its own public key. +const ImportOutput = ` +{ + "keypair": { + "fingerprint": "1e:2c:9b:56:79:4b:45:77:f9:ca:7a:98:2c:b0:d5:3c", + "name": "importedkey", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova", + "user_id": "fake" + } +} +` + +// FirstKeyPair is the first result in ListOutput. +var FirstKeyPair = keypairs.KeyPair{ + Name: "firstkey", + Fingerprint: "15:b0:f8:b3:f9:48:63:71:cf:7b:5b:38:6d:44:2d:4a", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC+Eo/RZRngaGTkFs7I62ZjsIlO79KklKbMXi8F+KITD4bVQHHn+kV+4gRgkgCRbdoDqoGfpaDFs877DYX9n4z6FrAIZ4PES8TNKhatifpn9NdQYWA+IkU8CuvlEKGuFpKRi/k7JLos/gHi2hy7QUwgtRvcefvD/vgQZOVw/mGR9Q== Generated by Nova\n", +} + +// SecondKeyPair is the second result in ListOutput. +var SecondKeyPair = keypairs.KeyPair{ + Name: "secondkey", + Fingerprint: "35:9d:d0:c3:4a:80:d3:d8:86:f1:ca:f7:df:c4:f9:d8", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7DUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5Q== Generated by Nova\n", +} + +// ExpectedKeyPairSlice is the slice of results that should be parsed from ListOutput, in the expected +// order. +var ExpectedKeyPairSlice = []keypairs.KeyPair{FirstKeyPair, SecondKeyPair} + +// CreatedKeyPair is the parsed result from CreatedOutput. +var CreatedKeyPair = keypairs.KeyPair{ + Name: "createdkey", + Fingerprint: "35:9d:d0:c3:4a:80:d3:d8:86:f1:ca:f7:df:c4:f9:d8", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7DUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5Q== Generated by Nova\n", + PrivateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIICXAIBAAKBgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7\nDUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ\n9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5QIDAQAB\nAoGAE5XO1mDhORy9COvsg+kYPUhB1GsCYxh+v88wG7HeFDKBY6KUc/Kxo6yoGn5T\nTjRjekyi2KoDZHz4VlIzyZPwFS4I1bf3oCunVoAKzgLdmnTtvRNMC5jFOGc2vUgP\n9bSyRj3S1R4ClVk2g0IDeagko/jc8zzLEYuIK+fbkds79YECQQDt3vcevgegnkga\ntF4NsDmmBPRkcSHCqrANP/7vFcBQN3czxeYYWX3DK07alu6GhH1Y4sHbdm616uU0\nll7xbDzxAkEAzAtN2IyftNygV2EGiaGgqLyo/tD9+Vui2qCQplqe4jvWh/5Sparl\nOjmKo+uAW+hLrLVMnHzRWxbWU8hirH5FNQJATO+ZxCK4etXXAnQmG41NCAqANWB2\nB+2HJbH2NcQ2QHvAHUm741JGn/KI/aBlo7KEjFRDWUVUB5ji64BbUwCsMQJBAIku\nLGcjnBf/oLk+XSPZC2eGd2Ph5G5qYmH0Q2vkTx+wtTn3DV+eNsDfgMtWAJVJ5t61\ngU1QSXyhLPVlKpnnxuUCQC+xvvWjWtsLaFtAsZywJiqLxQzHts8XLGZptYJ5tLWV\nrtmYtBcJCN48RrgQHry/xWYeA4K/AFQpXfNPgprQ96Q=\n-----END RSA PRIVATE KEY-----\n", + UserID: "fake", +} + +// ImportedKeyPair is the parsed result from ImportOutput. +var ImportedKeyPair = keypairs.KeyPair{ + Name: "importedkey", + Fingerprint: "1e:2c:9b:56:79:4b:45:77:f9:ca:7a:98:2c:b0:d5:3c", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova", + UserID: "fake", +} + +// HandleListSuccessfully configures the test server to respond to a List request. +func HandleListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-keypairs", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ListOutput) + }) +} + +// HandleGetSuccessfully configures the test server to respond to a Get request for "firstkey". +func HandleGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-keypairs/firstkey", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetOutput) + }) +} + +// HandleCreateSuccessfully configures the test server to respond to a Create request for a new +// keypair called "createdkey". +func HandleCreateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-keypairs", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "keypair": { "name": "createdkey" } }`) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, CreateOutput) + }) +} + +// HandleImportSuccessfully configures the test server to respond to an Import request for an +// existing keypair called "importedkey". +func HandleImportSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-keypairs", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, ` + { + "keypair": { + "name": "importedkey", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova" + } + } + `) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ImportOutput) + }) +} + +// HandleDeleteSuccessfully configures the test server to respond to a Delete request for a +// keypair called "deletedkey". +func HandleDeleteSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-keypairs/deletedkey", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/testing/requests_test.go new file mode 100644 index 0000000000..1e05e6687e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/testing/requests_test.go @@ -0,0 +1,72 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListSuccessfully(t) + + count := 0 + err := keypairs.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := keypairs.ExtractKeyPairs(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedKeyPairSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateSuccessfully(t) + + actual, err := keypairs.Create(client.ServiceClient(), keypairs.CreateOpts{ + Name: "createdkey", + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &CreatedKeyPair, actual) +} + +func TestImport(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleImportSuccessfully(t) + + actual, err := keypairs.Create(client.ServiceClient(), keypairs.CreateOpts{ + Name: "importedkey", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova", + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &ImportedKeyPair, actual) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetSuccessfully(t) + + actual, err := keypairs.Get(client.ServiceClient(), "firstkey").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &FirstKeyPair, actual) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteSuccessfully(t) + + err := keypairs.Delete(client.ServiceClient(), "deletedkey").ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/urls.go new file mode 100644 index 0000000000..fec38f3679 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/urls.go @@ -0,0 +1,25 @@ +package keypairs + +import "github.com/gophercloud/gophercloud" + +const resourcePath = "os-keypairs" + +func resourceURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(resourcePath) +} + +func listURL(c *gophercloud.ServiceClient) string { + return resourceURL(c) +} + +func createURL(c *gophercloud.ServiceClient) string { + return resourceURL(c) +} + +func getURL(c *gophercloud.ServiceClient, name string) string { + return c.ServiceURL(resourcePath, name) +} + +func deleteURL(c *gophercloud.ServiceClient, name string) string { + return getURL(c, name) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/limits/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/limits/requests.go new file mode 100644 index 0000000000..70324b8356 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/limits/requests.go @@ -0,0 +1,39 @@ +package limits + +import ( + "github.com/gophercloud/gophercloud" +) + +// GetOptsBuilder allows extensions to add additional parameters to the +// Get request. +type GetOptsBuilder interface { + ToLimitsQuery() (string, error) +} + +// GetOpts enables retrieving limits by a specific tenant. +type GetOpts struct { + // The tenant ID to retrieve limits for + TenantID string `q:"tenant_id"` +} + +// ToLimitsQuery formats a GetOpts into a query string. +func (opts GetOpts) ToLimitsQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// Get returns the limits about the currently scoped tenant. +func Get(client *gophercloud.ServiceClient, opts GetOptsBuilder) (r GetResult) { + url := getURL(client) + if opts != nil { + query, err := opts.ToLimitsQuery() + if err != nil { + r.Err = err + return + } + url += query + } + + _, r.Err = client.Get(url, &r.Body, nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/limits/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/limits/results.go new file mode 100644 index 0000000000..b58f1ddd09 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/limits/results.go @@ -0,0 +1,90 @@ +package limits + +import ( + "github.com/gophercloud/gophercloud" +) + +// Limits is a struct that contains the response of a limit query. +type Limits struct { + // Absolute contains the limits and usage information. + Absolute Absolute `json:"absolute"` +} + +// Usage is a struct that contains the current resource usage and limits +// of a tenant. +type Absolute struct { + // MaxTotalCores is the number of cores available to a tenant. + MaxTotalCores int `json:"maxTotalCores"` + + // MaxImageMeta is the amount of image metadata available to a tenant. + MaxImageMeta int `json:"maxImageMeta"` + + // MaxServerMeta is the amount of server metadata available to a tenant. + MaxServerMeta int `json:"maxServerMeta"` + + // MaxPersonality is the amount of personality/files available to a tenant. + MaxPersonality int `json:"maxPersonality"` + + // MaxPersonalitySize is the personality file size available to a tenant. + MaxPersonalitySize int `json:"maxPersonalitySize"` + + // MaxTotalKeypairs is the total keypairs available to a tenant. + MaxTotalKeypairs int `json:"maxTotalKeypairs"` + + // MaxSecurityGroups is the number of security groups available to a tenant. + MaxSecurityGroups int `json:"maxSecurityGroups"` + + // MaxSecurityGroupRules is the number of security group rules available to + // a tenant. + MaxSecurityGroupRules int `json:"maxSecurityGroupRules"` + + // MaxServerGroups is the number of server groups available to a tenant. + MaxServerGroups int `json:"maxServerGroups"` + + // MaxServerGroupMembers is the number of server group members available + // to a tenant. + MaxServerGroupMembers int `json:"maxServerGroupMembers"` + + // MaxTotalFloatingIps is the number of floating IPs available to a tenant. + MaxTotalFloatingIps int `json:"maxTotalFloatingIps"` + + // MaxTotalInstances is the number of instances/servers available to a tenant. + MaxTotalInstances int `json:"maxTotalInstances"` + + // MaxTotalRAMSize is the total amount of RAM available to a tenant measured + // in megabytes (MB). + MaxTotalRAMSize int `json:"maxTotalRAMSize"` + + // TotalCoresUsed is the number of cores currently in use. + TotalCoresUsed int `json:"totalCoresUsed"` + + // TotalInstancesUsed is the number of instances/servers in use. + TotalInstancesUsed int `json:"totalInstancesUsed"` + + // TotalFloatingIpsUsed is the number of floating IPs in use. + TotalFloatingIpsUsed int `json:"totalFloatingIpsUsed"` + + // TotalRAMUsed is the total RAM/memory in use measured in megabytes (MB). + TotalRAMUsed int `json:"totalRAMUsed"` + + // TotalSecurityGroupsUsed is the total number of security groups in use. + TotalSecurityGroupsUsed int `json:"totalSecurityGroupsUsed"` + + // TotalServerGroupsUsed is the total number of server groups in use. + TotalServerGroupsUsed int `json:"totalServerGroupsUsed"` +} + +// Extract interprets a limits result as a Limits. +func (r GetResult) Extract() (*Limits, error) { + var s struct { + Limits *Limits `json:"limits"` + } + err := r.ExtractInto(&s) + return s.Limits, err +} + +// GetResult is the response from a Get operation. Call its ExtractAbsolute +// method to interpret it as an Absolute. +type GetResult struct { + gophercloud.Result +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/limits/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/limits/testing/fixtures.go new file mode 100644 index 0000000000..d4e52f7783 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/limits/testing/fixtures.go @@ -0,0 +1,80 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/limits" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +// GetOutput is a sample response to a Get call. +const GetOutput = ` +{ + "limits": { + "rate": [], + "absolute": { + "maxServerMeta": 128, + "maxPersonality": 5, + "totalServerGroupsUsed": 0, + "maxImageMeta": 128, + "maxPersonalitySize": 10240, + "maxTotalKeypairs": 100, + "maxSecurityGroupRules": 20, + "maxServerGroups": 10, + "totalCoresUsed": 1, + "totalRAMUsed": 2048, + "totalInstancesUsed": 1, + "maxSecurityGroups": 10, + "totalFloatingIpsUsed": 0, + "maxTotalCores": 20, + "maxServerGroupMembers": 10, + "maxTotalFloatingIps": 10, + "totalSecurityGroupsUsed": 1, + "maxTotalInstances": 10, + "maxTotalRAMSize": 51200 + } + } +} +` + +// LimitsResult is the result of the limits in GetOutput. +var LimitsResult = limits.Limits{ + Absolute: limits.Absolute{ + MaxServerMeta: 128, + MaxPersonality: 5, + TotalServerGroupsUsed: 0, + MaxImageMeta: 128, + MaxPersonalitySize: 10240, + MaxTotalKeypairs: 100, + MaxSecurityGroupRules: 20, + MaxServerGroups: 10, + TotalCoresUsed: 1, + TotalRAMUsed: 2048, + TotalInstancesUsed: 1, + MaxSecurityGroups: 10, + TotalFloatingIpsUsed: 0, + MaxTotalCores: 20, + MaxServerGroupMembers: 10, + MaxTotalFloatingIps: 10, + TotalSecurityGroupsUsed: 1, + MaxTotalInstances: 10, + MaxTotalRAMSize: 51200, + }, +} + +const TenantID = "555544443333222211110000ffffeeee" + +// HandleGetSuccessfully configures the test server to respond to a Get request +// for a limit. +func HandleGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/limits", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetOutput) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/limits/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/limits/testing/requests_test.go new file mode 100644 index 0000000000..9c8456c9dd --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/limits/testing/requests_test.go @@ -0,0 +1,23 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/limits" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetSuccessfully(t) + + getOpts := limits.GetOpts{ + TenantID: TenantID, + } + + actual, err := limits.Get(client.ServiceClient(), getOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &LimitsResult, actual) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/limits/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/limits/urls.go new file mode 100644 index 0000000000..edd97e4e0e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/limits/urls.go @@ -0,0 +1,11 @@ +package limits + +import ( + "github.com/gophercloud/gophercloud" +) + +const resourcePath = "limits" + +func getURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(resourcePath) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/networks/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/networks/doc.go new file mode 100644 index 0000000000..fafe4a04d7 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/networks/doc.go @@ -0,0 +1,2 @@ +// Package network provides the ability to manage nova-networks +package networks diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/networks/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/networks/requests.go new file mode 100644 index 0000000000..5432a1025c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/networks/requests.go @@ -0,0 +1,19 @@ +package networks + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// List returns a Pager that allows you to iterate over a collection of Network. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page { + return NetworkPage{pagination.SinglePageBase(r)} + }) +} + +// Get returns data about a previously created Network. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/networks/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/networks/results.go new file mode 100644 index 0000000000..cbcce31987 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/networks/results.go @@ -0,0 +1,134 @@ +package networks + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// A Network represents a nova-network that an instance communicates on +type Network struct { + // The Bridge that VIFs on this network are connected to + Bridge string `json:"bridge"` + + // BridgeInterface is what interface is connected to the Bridge + BridgeInterface string `json:"bridge_interface"` + + // The Broadcast address of the network. + Broadcast string `json:"broadcast"` + + // CIDR is the IPv4 subnet. + CIDR string `json:"cidr"` + + // CIDRv6 is the IPv6 subnet. + CIDRv6 string `json:"cidr_v6"` + + // CreatedAt is when the network was created.. + CreatedAt gophercloud.JSONRFC3339MilliNoZ `json:"created_at,omitempty"` + + // Deleted shows if the network has been deleted. + Deleted bool `json:"deleted"` + + // DeletedAt is the time when the network was deleted. + DeletedAt gophercloud.JSONRFC3339MilliNoZ `json:"deleted_at,omitempty"` + + // DHCPStart is the start of the DHCP address range. + DHCPStart string `json:"dhcp_start"` + + // DNS1 is the first DNS server to use through DHCP. + DNS1 string `json:"dns_1"` + + // DNS2 is the first DNS server to use through DHCP. + DNS2 string `json:"dns_2"` + + // Gateway is the network gateway. + Gateway string `json:"gateway"` + + // Gatewayv6 is the IPv6 network gateway. + Gatewayv6 string `json:"gateway_v6"` + + // Host is the host that the network service is running on. + Host string `json:"host"` + + // ID is the UUID of the network. + ID string `json:"id"` + + // Injected determines if network information is injected into the host. + Injected bool `json:"injected"` + + // Label is the common name that the network has.. + Label string `json:"label"` + + // MultiHost is if multi-host networking is enablec.. + MultiHost bool `json:"multi_host"` + + // Netmask is the network netmask. + Netmask string `json:"netmask"` + + // Netmaskv6 is the IPv6 netmask. + Netmaskv6 string `json:"netmask_v6"` + + // Priority is the network interface priority. + Priority int `json:"priority"` + + // ProjectID is the project associated with this network. + ProjectID string `json:"project_id"` + + // RXTXBase configures bandwidth entitlement. + RXTXBase int `json:"rxtx_base"` + + // UpdatedAt is the time when the network was last updated. + UpdatedAt gophercloud.JSONRFC3339MilliNoZ `json:"updated_at,omitempty"` + + // VLAN is the vlan this network runs on. + VLAN int `json:"vlan"` + + // VPNPrivateAddress is the private address of the CloudPipe VPN. + VPNPrivateAddress string `json:"vpn_private_address"` + + // VPNPublicAddress is the public address of the CloudPipe VPN. + VPNPublicAddress string `json:"vpn_public_address"` + + // VPNPublicPort is the port of the CloudPipe VPN. + VPNPublicPort int `json:"vpn_public_port"` +} + +// NetworkPage stores a single, only page of Networks +// results from a List call. +type NetworkPage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a NetworkPage is empty. +func (page NetworkPage) IsEmpty() (bool, error) { + va, err := ExtractNetworks(page) + return len(va) == 0, err +} + +// ExtractNetworks interprets a page of results as a slice of Networks +func ExtractNetworks(r pagination.Page) ([]Network, error) { + var s struct { + Networks []Network `json:"networks"` + } + err := (r.(NetworkPage)).ExtractInto(&s) + return s.Networks, err +} + +type NetworkResult struct { + gophercloud.Result +} + +// Extract is a method that attempts to interpret any Network resource +// response as a Network struct. +func (r NetworkResult) Extract() (*Network, error) { + var s struct { + Network *Network `json:"network"` + } + err := r.ExtractInto(&s) + return s.Network, err +} + +// GetResult is the response from a Get operation. Call its Extract method to interpret it +// as a Network. +type GetResult struct { + NetworkResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/networks/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/networks/testing/doc.go new file mode 100644 index 0000000000..76a18cdfa1 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/networks/testing/doc.go @@ -0,0 +1,2 @@ +// compute_extensions_networks_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/networks/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/networks/testing/fixtures.go new file mode 100644 index 0000000000..e2fa49b48a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/networks/testing/fixtures.go @@ -0,0 +1,204 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/networks" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +// ListOutput is a sample response to a List call. +const ListOutput = ` +{ + "networks": [ + { + "bridge": "br100", + "bridge_interface": "eth0", + "broadcast": "10.0.0.7", + "cidr": "10.0.0.0/29", + "cidr_v6": null, + "created_at": "2011-08-15T06:19:19.387525", + "deleted": false, + "dhcp_start": "10.0.0.3", + "dns1": null, + "dns2": null, + "gateway": "10.0.0.1", + "gateway_v6": null, + "host": "nsokolov-desktop", + "id": "20c8acc0-f747-4d71-a389-46d078ebf047", + "injected": false, + "label": "mynet_0", + "multi_host": false, + "netmask": "255.255.255.248", + "netmask_v6": null, + "priority": null, + "project_id": "1234", + "rxtx_base": null, + "updated_at": "2011-08-16T09:26:13.048257", + "vlan": 100, + "vpn_private_address": "10.0.0.2", + "vpn_public_address": "127.0.0.1", + "vpn_public_port": 1000 + }, + { + "bridge": "br101", + "bridge_interface": "eth0", + "broadcast": "10.0.0.15", + "cidr": "10.0.0.10/29", + "cidr_v6": null, + "created_at": "2011-08-15T06:19:19.387525", + "deleted": false, + "dhcp_start": "10.0.0.11", + "dns1": null, + "dns2": null, + "gateway": "10.0.0.9", + "gateway_v6": null, + "host": null, + "id": "20c8acc0-f747-4d71-a389-46d078ebf000", + "injected": false, + "label": "mynet_1", + "multi_host": false, + "netmask": "255.255.255.248", + "netmask_v6": null, + "priority": null, + "project_id": null, + "rxtx_base": null, + "vlan": 101, + "vpn_private_address": "10.0.0.10", + "vpn_public_address": null, + "vpn_public_port": 1001 + } + ] +} +` + +// GetOutput is a sample response to a Get call. +const GetOutput = ` +{ + "network": { + "bridge": "br101", + "bridge_interface": "eth0", + "broadcast": "10.0.0.15", + "cidr": "10.0.0.10/29", + "cidr_v6": null, + "created_at": "2011-08-15T06:19:19.387525", + "deleted": false, + "dhcp_start": "10.0.0.11", + "dns1": null, + "dns2": null, + "gateway": "10.0.0.9", + "gateway_v6": null, + "host": null, + "id": "20c8acc0-f747-4d71-a389-46d078ebf000", + "injected": false, + "label": "mynet_1", + "multi_host": false, + "netmask": "255.255.255.248", + "netmask_v6": null, + "priority": null, + "project_id": null, + "rxtx_base": null, + "vlan": 101, + "vpn_private_address": "10.0.0.10", + "vpn_public_address": null, + "vpn_public_port": 1001 + } +} +` + +// FirstNetwork is the first result in ListOutput. +var nilTime time.Time +var FirstNetwork = networks.Network{ + Bridge: "br100", + BridgeInterface: "eth0", + Broadcast: "10.0.0.7", + CIDR: "10.0.0.0/29", + CIDRv6: "", + CreatedAt: gophercloud.JSONRFC3339MilliNoZ(time.Date(2011, 8, 15, 6, 19, 19, 387525000, time.UTC)), + Deleted: false, + DeletedAt: gophercloud.JSONRFC3339MilliNoZ(nilTime), + DHCPStart: "10.0.0.3", + DNS1: "", + DNS2: "", + Gateway: "10.0.0.1", + Gatewayv6: "", + Host: "nsokolov-desktop", + ID: "20c8acc0-f747-4d71-a389-46d078ebf047", + Injected: false, + Label: "mynet_0", + MultiHost: false, + Netmask: "255.255.255.248", + Netmaskv6: "", + Priority: 0, + ProjectID: "1234", + RXTXBase: 0, + UpdatedAt: gophercloud.JSONRFC3339MilliNoZ(time.Date(2011, 8, 16, 9, 26, 13, 48257000, time.UTC)), + VLAN: 100, + VPNPrivateAddress: "10.0.0.2", + VPNPublicAddress: "127.0.0.1", + VPNPublicPort: 1000, +} + +// SecondNetwork is the second result in ListOutput. +var SecondNetwork = networks.Network{ + Bridge: "br101", + BridgeInterface: "eth0", + Broadcast: "10.0.0.15", + CIDR: "10.0.0.10/29", + CIDRv6: "", + CreatedAt: gophercloud.JSONRFC3339MilliNoZ(time.Date(2011, 8, 15, 6, 19, 19, 387525000, time.UTC)), + Deleted: false, + DeletedAt: gophercloud.JSONRFC3339MilliNoZ(nilTime), + DHCPStart: "10.0.0.11", + DNS1: "", + DNS2: "", + Gateway: "10.0.0.9", + Gatewayv6: "", + Host: "", + ID: "20c8acc0-f747-4d71-a389-46d078ebf000", + Injected: false, + Label: "mynet_1", + MultiHost: false, + Netmask: "255.255.255.248", + Netmaskv6: "", + Priority: 0, + ProjectID: "", + RXTXBase: 0, + UpdatedAt: gophercloud.JSONRFC3339MilliNoZ(nilTime), + VLAN: 101, + VPNPrivateAddress: "10.0.0.10", + VPNPublicAddress: "", + VPNPublicPort: 1001, +} + +// ExpectedNetworkSlice is the slice of results that should be parsed +// from ListOutput, in the expected order. +var ExpectedNetworkSlice = []networks.Network{FirstNetwork, SecondNetwork} + +// HandleListSuccessfully configures the test server to respond to a List request. +func HandleListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-networks", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ListOutput) + }) +} + +// HandleGetSuccessfully configures the test server to respond to a Get request +// for an existing network. +func HandleGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-networks/20c8acc0-f747-4d71-a389-46d078ebf000", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetOutput) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/networks/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/networks/testing/requests_test.go new file mode 100644 index 0000000000..36b5463e42 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/networks/testing/requests_test.go @@ -0,0 +1,38 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/networks" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListSuccessfully(t) + + count := 0 + err := networks.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := networks.ExtractNetworks(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedNetworkSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetSuccessfully(t) + + actual, err := networks.Get(client.ServiceClient(), "20c8acc0-f747-4d71-a389-46d078ebf000").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &SecondNetwork, actual) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/networks/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/networks/urls.go new file mode 100644 index 0000000000..491bde6f62 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/networks/urls.go @@ -0,0 +1,17 @@ +package networks + +import "github.com/gophercloud/gophercloud" + +const resourcePath = "os-networks" + +func resourceURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(resourcePath) +} + +func listURL(c *gophercloud.ServiceClient) string { + return resourceURL(c) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/pauseunpause/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/pauseunpause/doc.go new file mode 100644 index 0000000000..7a45a24c33 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/pauseunpause/doc.go @@ -0,0 +1,5 @@ +/* +Package pauseunpause provides functionality to pause and unpause servers that have +been provisioned by the OpenStack Compute service. +*/ +package pauseunpause diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/pauseunpause/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/pauseunpause/requests.go new file mode 100644 index 0000000000..a9e02d99e6 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/pauseunpause/requests.go @@ -0,0 +1,19 @@ +package pauseunpause + +import "github.com/gophercloud/gophercloud" + +func actionURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "action") +} + +// Pause is the operation responsible for pausing a Compute server. +func Pause(client *gophercloud.ServiceClient, id string) (r gophercloud.ErrResult) { + _, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"pause": nil}, nil, nil) + return +} + +// Unpause is the operation responsible for unpausing a Compute server. +func Unpause(client *gophercloud.ServiceClient, id string) (r gophercloud.ErrResult) { + _, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"unpause": nil}, nil, nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/pauseunpause/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/pauseunpause/testing/doc.go new file mode 100644 index 0000000000..104b80832e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/pauseunpause/testing/doc.go @@ -0,0 +1,2 @@ +// compute_extensions_pauseunpause_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/pauseunpause/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/pauseunpause/testing/fixtures.go new file mode 100644 index 0000000000..3723bb3360 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/pauseunpause/testing/fixtures.go @@ -0,0 +1,27 @@ +package testing + +import ( + "net/http" + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func mockPauseServerResponse(t *testing.T, id string) { + th.Mux.HandleFunc("/servers/"+id+"/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{"pause": null}`) + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockUnpauseServerResponse(t *testing.T, id string) { + th.Mux.HandleFunc("/servers/"+id+"/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{"unpause": null}`) + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/pauseunpause/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/pauseunpause/testing/requests_test.go new file mode 100644 index 0000000000..0433e8c482 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/pauseunpause/testing/requests_test.go @@ -0,0 +1,31 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/pauseunpause" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +const serverID = "{serverId}" + +func TestPause(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockPauseServerResponse(t, serverID) + + err := pauseunpause.Pause(client.ServiceClient(), serverID).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestUnpause(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockUnpauseServerResponse(t, serverID) + + err := pauseunpause.Unpause(client.ServiceClient(), serverID).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/doc.go new file mode 100644 index 0000000000..721024ec58 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/doc.go @@ -0,0 +1,3 @@ +// Package quotasets provides information and interaction with QuotaSet +// extension for the OpenStack Compute service. +package quotasets diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/requests.go new file mode 100644 index 0000000000..bb9cb22181 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/requests.go @@ -0,0 +1,76 @@ +package quotasets + +import ( + "github.com/gophercloud/gophercloud" +) + +// Get returns public data about a previously created QuotaSet. +func Get(client *gophercloud.ServiceClient, tenantID string) GetResult { + var res GetResult + _, res.Err = client.Get(getURL(client, tenantID), &res.Body, nil) + return res +} + +//Updates the quotas for the given tenantID and returns the new quota-set +func Update(client *gophercloud.ServiceClient, tenantID string, opts UpdateOptsBuilder) (res UpdateResult) { + reqBody, err := opts.ToComputeQuotaUpdateMap() + if err != nil { + res.Err = err + return + } + + _, res.Err = client.Put(updateURL(client, tenantID), reqBody, &res.Body, &gophercloud.RequestOpts{OkCodes: []int{200}}) + return res +} + +//Resets the uotas for the given tenant to their default values +func Delete(client *gophercloud.ServiceClient, tenantID string) (res DeleteResult) { + _, res.Err = client.Delete(deleteURL(client, tenantID), nil) + return +} + +//Options for Updating the quotas of a Tenant +//All int-values are pointers so they can be nil if they are not needed +//you can use gopercloud.IntToPointer() for convenience +type UpdateOpts struct { + //FixedIps is number of fixed ips alloted this quota_set + FixedIps *int `json:"fixed_ips,omitempty"` + // FloatingIps is number of floating ips alloted this quota_set + FloatingIps *int `json:"floating_ips,omitempty"` + // InjectedFileContentBytes is content bytes allowed for each injected file + InjectedFileContentBytes *int `json:"injected_file_content_bytes,omitempty"` + // InjectedFilePathBytes is allowed bytes for each injected file path + InjectedFilePathBytes *int `json:"injected_file_path_bytes,omitempty"` + // InjectedFiles is injected files allowed for each project + InjectedFiles *int `json:"injected_files,omitempty"` + // KeyPairs is number of ssh keypairs + KeyPairs *int `json:"key_pairs,omitempty"` + // MetadataItems is number of metadata items allowed for each instance + MetadataItems *int `json:"metadata_items,omitempty"` + // Ram is megabytes allowed for each instance + Ram *int `json:"ram,omitempty"` + // SecurityGroupRules is rules allowed for each security group + SecurityGroupRules *int `json:"security_group_rules,omitempty"` + // SecurityGroups security groups allowed for each project + SecurityGroups *int `json:"security_groups,omitempty"` + // Cores is number of instance cores allowed for each project + Cores *int `json:"cores,omitempty"` + // Instances is number of instances allowed for each project + Instances *int `json:"instances,omitempty"` + // Number of ServerGroups allowed for the project + ServerGroups *int `json:"server_groups,omitempty"` + // Max number of Members for each ServerGroup + ServerGroupMembers *int `json:"server_group_members,omitempty"` + //Users can force the update even if the quota has already been used and the reserved quota exceeds the new quota. + Force bool `json:"force,omitempty"` +} + +type UpdateOptsBuilder interface { + //Extra specific name to prevent collisions with interfaces for other quotas (e.g. neutron) + ToComputeQuotaUpdateMap() (map[string]interface{}, error) +} + +func (opts UpdateOpts) ToComputeQuotaUpdateMap() (map[string]interface{}, error) { + + return gophercloud.BuildRequestBody(opts, "quota_set") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/results.go new file mode 100644 index 0000000000..44e6b06028 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/results.go @@ -0,0 +1,91 @@ +package quotasets + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// QuotaSet is a set of operational limits that allow for control of compute usage. +type QuotaSet struct { + //ID is tenant associated with this quota_set + ID string `json:"id"` + //FixedIps is number of fixed ips alloted this quota_set + FixedIps int `json:"fixed_ips"` + // FloatingIps is number of floating ips alloted this quota_set + FloatingIps int `json:"floating_ips"` + // InjectedFileContentBytes is content bytes allowed for each injected file + InjectedFileContentBytes int `json:"injected_file_content_bytes"` + // InjectedFilePathBytes is allowed bytes for each injected file path + InjectedFilePathBytes int `json:"injected_file_path_bytes"` + // InjectedFiles is injected files allowed for each project + InjectedFiles int `json:"injected_files"` + // KeyPairs is number of ssh keypairs + KeyPairs int `json:"key_pairs"` + // MetadataItems is number of metadata items allowed for each instance + MetadataItems int `json:"metadata_items"` + // Ram is megabytes allowed for each instance + Ram int `json:"ram"` + // SecurityGroupRules is rules allowed for each security group + SecurityGroupRules int `json:"security_group_rules"` + // SecurityGroups security groups allowed for each project + SecurityGroups int `json:"security_groups"` + // Cores is number of instance cores allowed for each project + Cores int `json:"cores"` + // Instances is number of instances allowed for each project + Instances int `json:"instances"` + // Number of ServerGroups allowed for the project + ServerGroups int `json:"server_groups"` + // Max number of Members for each ServerGroup + ServerGroupMembers int `json:"server_group_members"` +} + +// QuotaSetPage stores a single, only page of QuotaSet results from a List call. +type QuotaSetPage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a QuotaSetsetPage is empty. +func (page QuotaSetPage) IsEmpty() (bool, error) { + ks, err := ExtractQuotaSets(page) + return len(ks) == 0, err +} + +// ExtractQuotaSets interprets a page of results as a slice of QuotaSets. +func ExtractQuotaSets(r pagination.Page) ([]QuotaSet, error) { + var s struct { + QuotaSets []QuotaSet `json:"quotas"` + } + err := (r.(QuotaSetPage)).ExtractInto(&s) + return s.QuotaSets, err +} + +type quotaResult struct { + gophercloud.Result +} + +// Extract is a method that attempts to interpret any QuotaSet resource response as a QuotaSet struct. +func (r quotaResult) Extract() (*QuotaSet, error) { + var s struct { + QuotaSet *QuotaSet `json:"quota_set"` + } + err := r.ExtractInto(&s) + return s.QuotaSet, err +} + +// GetResult is the response from a Get operation. Call its Extract method to interpret it +// as a QuotaSet. +type GetResult struct { + quotaResult +} + +// UpdateResult is the response from a Update operation. Call its Extract method to interpret it +// as a QuotaSet. +type UpdateResult struct { + quotaResult +} + +// DeleteResult is the response from a Delete operation. Call its Extract method to interpret it +// as a QuotaSet. +type DeleteResult struct { + quotaResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/testing/doc.go new file mode 100644 index 0000000000..19ad75d54b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/testing/doc.go @@ -0,0 +1,2 @@ +// compute_extensions_quotasets_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/testing/fixtures.go new file mode 100644 index 0000000000..004d7e8ae7 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/testing/fixtures.go @@ -0,0 +1,119 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +// GetOutput is a sample response to a Get call. +const GetOutput = ` +{ + "quota_set" : { + "instances" : 25, + "security_groups" : 10, + "security_group_rules" : 20, + "cores" : 200, + "injected_file_content_bytes" : 10240, + "injected_files" : 5, + "metadata_items" : 128, + "ram" : 200000, + "key_pairs" : 10, + "injected_file_path_bytes" : 255, + "server_groups" : 2, + "server_group_members" : 3 + } +} +` +const FirstTenantID = "555544443333222211110000ffffeeee" + +// FirstQuotaSet is the first result in ListOutput. +var FirstQuotaSet = quotasets.QuotaSet{ + FixedIps: 0, + FloatingIps: 0, + InjectedFileContentBytes: 10240, + InjectedFilePathBytes: 255, + InjectedFiles: 5, + KeyPairs: 10, + MetadataItems: 128, + Ram: 200000, + SecurityGroupRules: 20, + SecurityGroups: 10, + Cores: 200, + Instances: 25, + ServerGroups: 2, + ServerGroupMembers: 3, +} + +//The expected update Body. Is also returned by PUT request +const UpdateOutput = `{"quota_set":{"cores":200,"fixed_ips":0,"floating_ips":0,"injected_file_content_bytes":10240,"injected_file_path_bytes":255,"injected_files":5,"instances":25,"key_pairs":10,"metadata_items":128,"ram":200000,"security_group_rules":20,"security_groups":10,"server_groups":2,"server_group_members":3}}` + +//The expected partialupdate Body. Is also returned by PUT request +const PartialUpdateBody = `{"quota_set":{"cores":200, "force":true}}` + +//Result of Quota-update +var UpdatedQuotaSet = quotasets.UpdateOpts{ + FixedIps: gophercloud.IntToPointer(0), + FloatingIps: gophercloud.IntToPointer(0), + InjectedFileContentBytes: gophercloud.IntToPointer(10240), + InjectedFilePathBytes: gophercloud.IntToPointer(255), + InjectedFiles: gophercloud.IntToPointer(5), + KeyPairs: gophercloud.IntToPointer(10), + MetadataItems: gophercloud.IntToPointer(128), + Ram: gophercloud.IntToPointer(200000), + SecurityGroupRules: gophercloud.IntToPointer(20), + SecurityGroups: gophercloud.IntToPointer(10), + Cores: gophercloud.IntToPointer(200), + Instances: gophercloud.IntToPointer(25), + ServerGroups: gophercloud.IntToPointer(2), + ServerGroupMembers: gophercloud.IntToPointer(3), +} + +// HandleGetSuccessfully configures the test server to respond to a Get request for sample tenant +func HandleGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-quota-sets/"+FirstTenantID, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetOutput) + }) +} + +// HandlePutSuccessfully configures the test server to respond to a Put request for sample tenant +func HandlePutSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-quota-sets/"+FirstTenantID, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, UpdateOutput) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, UpdateOutput) + }) +} + +// HandlePartialPutSuccessfully configures the test server to respond to a Put request for sample tenant that only containes specific values +func HandlePartialPutSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-quota-sets/"+FirstTenantID, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, PartialUpdateBody) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, UpdateOutput) + }) +} + +// HandleDeleteSuccessfully configures the test server to respond to a Delete request for sample tenant +func HandleDeleteSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-quota-sets/"+FirstTenantID, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestBody(t, r, "") + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(202) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/testing/requests_test.go new file mode 100644 index 0000000000..dd45630b2b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/testing/requests_test.go @@ -0,0 +1,64 @@ +package testing + +import ( + "errors" + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetSuccessfully(t) + actual, err := quotasets.Get(client.ServiceClient(), FirstTenantID).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &FirstQuotaSet, actual) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandlePutSuccessfully(t) + actual, err := quotasets.Update(client.ServiceClient(), FirstTenantID, UpdatedQuotaSet).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &FirstQuotaSet, actual) +} + +func TestPartialUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandlePartialPutSuccessfully(t) + opts := quotasets.UpdateOpts{Cores: gophercloud.IntToPointer(200), Force: true} + actual, err := quotasets.Update(client.ServiceClient(), FirstTenantID, opts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &FirstQuotaSet, actual) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteSuccessfully(t) + _, err := quotasets.Delete(client.ServiceClient(), FirstTenantID).Extract() + th.AssertNoErr(t, err) +} + +type ErrorUpdateOpts quotasets.UpdateOpts + +func (opts ErrorUpdateOpts) ToComputeQuotaUpdateMap() (map[string]interface{}, error) { + return nil, errors.New("This is an error") +} + +func TestErrorInToComputeQuotaUpdateMap(t *testing.T) { + opts := &ErrorUpdateOpts{} + th.SetupHTTP() + defer th.TeardownHTTP() + HandlePutSuccessfully(t) + _, err := quotasets.Update(client.ServiceClient(), FirstTenantID, opts).Extract() + if err == nil { + t.Fatal("Error handling failed") + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/urls.go new file mode 100644 index 0000000000..64190e980b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/urls.go @@ -0,0 +1,21 @@ +package quotasets + +import "github.com/gophercloud/gophercloud" + +const resourcePath = "os-quota-sets" + +func resourceURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(resourcePath) +} + +func getURL(c *gophercloud.ServiceClient, tenantID string) string { + return c.ServiceURL(resourcePath, tenantID) +} + +func updateURL(c *gophercloud.ServiceClient, tenantID string) string { + return getURL(c, tenantID) +} + +func deleteURL(c *gophercloud.ServiceClient, tenantID string) string { + return getURL(c, tenantID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/schedulerhints/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/schedulerhints/doc.go new file mode 100644 index 0000000000..0bd45661b5 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/schedulerhints/doc.go @@ -0,0 +1,3 @@ +// Package schedulerhints enables instances to provide the OpenStack scheduler +// hints about where they should be placed in the cloud. +package schedulerhints diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/schedulerhints/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/schedulerhints/requests.go new file mode 100644 index 0000000000..a34263efcb --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/schedulerhints/requests.go @@ -0,0 +1,156 @@ +package schedulerhints + +import ( + "net" + "regexp" + "strings" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" +) + +// SchedulerHints represents a set of scheduling hints that are passed to the +// OpenStack scheduler +type SchedulerHints struct { + // Group specifies a Server Group to place the instance in. + Group string + // DifferentHost will place the instance on a compute node that does not + // host the given instances. + DifferentHost []string + // SameHost will place the instance on a compute node that hosts the given + // instances. + SameHost []string + // Query is a conditional statement that results in compute nodes able to + // host the instance. + Query []interface{} + // TargetCell specifies a cell name where the instance will be placed. + TargetCell string `json:"target_cell,omitempty"` + // BuildNearHostIP specifies a subnet of compute nodes to host the instance. + BuildNearHostIP string + // AdditionalProperies are arbitrary key/values that are not validated by nova. + AdditionalProperties map[string]interface{} +} + +// CreateOptsBuilder builds the scheduler hints into a serializable format. +type CreateOptsBuilder interface { + ToServerSchedulerHintsCreateMap() (map[string]interface{}, error) +} + +// ToServerSchedulerHintsMap builds the scheduler hints into a serializable format. +func (opts SchedulerHints) ToServerSchedulerHintsCreateMap() (map[string]interface{}, error) { + sh := make(map[string]interface{}) + + uuidRegex, _ := regexp.Compile("^[a-z0-9]{8}-[a-z0-9]{4}-[1-5][a-z0-9]{3}-[a-z0-9]{4}-[a-z0-9]{12}$") + + if opts.Group != "" { + if !uuidRegex.MatchString(opts.Group) { + err := gophercloud.ErrInvalidInput{} + err.Argument = "schedulerhints.SchedulerHints.Group" + err.Value = opts.Group + err.Info = "Group must be a UUID" + return nil, err + } + sh["group"] = opts.Group + } + + if len(opts.DifferentHost) > 0 { + for _, diffHost := range opts.DifferentHost { + if !uuidRegex.MatchString(diffHost) { + err := gophercloud.ErrInvalidInput{} + err.Argument = "schedulerhints.SchedulerHints.DifferentHost" + err.Value = opts.DifferentHost + err.Info = "The hosts must be in UUID format." + return nil, err + } + } + sh["different_host"] = opts.DifferentHost + } + + if len(opts.SameHost) > 0 { + for _, sameHost := range opts.SameHost { + if !uuidRegex.MatchString(sameHost) { + err := gophercloud.ErrInvalidInput{} + err.Argument = "schedulerhints.SchedulerHints.SameHost" + err.Value = opts.SameHost + err.Info = "The hosts must be in UUID format." + return nil, err + } + } + sh["same_host"] = opts.SameHost + } + + /* Query can be something simple like: + [">=", "$free_ram_mb", 1024] + + Or more complex like: + ['and', + ['>=', '$free_ram_mb', 1024], + ['>=', '$free_disk_mb', 200 * 1024] + ] + + Because of the possible complexity, just make sure the length is a minimum of 3. + */ + if len(opts.Query) > 0 { + if len(opts.Query) < 3 { + err := gophercloud.ErrInvalidInput{} + err.Argument = "schedulerhints.SchedulerHints.Query" + err.Value = opts.Query + err.Info = "Must be a conditional statement in the format of [op,variable,value]" + return nil, err + } + sh["query"] = opts.Query + } + + if opts.TargetCell != "" { + sh["target_cell"] = opts.TargetCell + } + + if opts.BuildNearHostIP != "" { + if _, _, err := net.ParseCIDR(opts.BuildNearHostIP); err != nil { + err := gophercloud.ErrInvalidInput{} + err.Argument = "schedulerhints.SchedulerHints.BuildNearHostIP" + err.Value = opts.BuildNearHostIP + err.Info = "Must be a valid subnet in the form 192.168.1.1/24" + return nil, err + } + ipParts := strings.Split(opts.BuildNearHostIP, "/") + sh["build_near_host_ip"] = ipParts[0] + sh["cidr"] = "/" + ipParts[1] + } + + if opts.AdditionalProperties != nil { + for k, v := range opts.AdditionalProperties { + sh[k] = v + } + } + + return sh, nil +} + +// CreateOptsExt adds a SchedulerHints option to the base CreateOpts. +type CreateOptsExt struct { + servers.CreateOptsBuilder + // SchedulerHints provides a set of hints to the scheduler. + SchedulerHints CreateOptsBuilder +} + +// ToServerCreateMap adds the SchedulerHints option to the base server creation options. +func (opts CreateOptsExt) ToServerCreateMap() (map[string]interface{}, error) { + base, err := opts.CreateOptsBuilder.ToServerCreateMap() + if err != nil { + return nil, err + } + + schedulerHints, err := opts.SchedulerHints.ToServerSchedulerHintsCreateMap() + if err != nil { + return nil, err + } + + if len(schedulerHints) == 0 { + return base, nil + } + + base["os:scheduler_hints"] = schedulerHints + + return base, nil +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/schedulerhints/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/schedulerhints/testing/doc.go new file mode 100644 index 0000000000..0640b5da57 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/schedulerhints/testing/doc.go @@ -0,0 +1,2 @@ +// compute_extensions_schedulerhints_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/schedulerhints/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/schedulerhints/testing/requests_test.go new file mode 100644 index 0000000000..c387eab860 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/schedulerhints/testing/requests_test.go @@ -0,0 +1,131 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/schedulerhints" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestCreateOpts(t *testing.T) { + base := servers.CreateOpts{ + Name: "createdserver", + ImageRef: "asdfasdfasdf", + FlavorRef: "performance1-1", + } + + schedulerHints := schedulerhints.SchedulerHints{ + Group: "101aed42-22d9-4a3e-9ba1-21103b0d1aba", + DifferentHost: []string{ + "a0cf03a5-d921-4877-bb5c-86d26cf818e1", + "8c19174f-4220-44f0-824a-cd1eeef10287", + }, + SameHost: []string{ + "a0cf03a5-d921-4877-bb5c-86d26cf818e1", + "8c19174f-4220-44f0-824a-cd1eeef10287", + }, + Query: []interface{}{">=", "$free_ram_mb", "1024"}, + TargetCell: "foobar", + BuildNearHostIP: "192.168.1.1/24", + AdditionalProperties: map[string]interface{}{"reservation": "a0cf03a5-d921-4877-bb5c-86d26cf818e1"}, + } + + ext := schedulerhints.CreateOptsExt{ + CreateOptsBuilder: base, + SchedulerHints: schedulerHints, + } + + expected := ` + { + "server": { + "name": "createdserver", + "imageRef": "asdfasdfasdf", + "flavorRef": "performance1-1" + }, + "os:scheduler_hints": { + "group": "101aed42-22d9-4a3e-9ba1-21103b0d1aba", + "different_host": [ + "a0cf03a5-d921-4877-bb5c-86d26cf818e1", + "8c19174f-4220-44f0-824a-cd1eeef10287" + ], + "same_host": [ + "a0cf03a5-d921-4877-bb5c-86d26cf818e1", + "8c19174f-4220-44f0-824a-cd1eeef10287" + ], + "query": [ + ">=", "$free_ram_mb", "1024" + ], + "target_cell": "foobar", + "build_near_host_ip": "192.168.1.1", + "cidr": "/24", + "reservation": "a0cf03a5-d921-4877-bb5c-86d26cf818e1" + } + } + ` + actual, err := ext.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expected, actual) +} + +func TestCreateOptsWithComplexQuery(t *testing.T) { + base := servers.CreateOpts{ + Name: "createdserver", + ImageRef: "asdfasdfasdf", + FlavorRef: "performance1-1", + } + + schedulerHints := schedulerhints.SchedulerHints{ + Group: "101aed42-22d9-4a3e-9ba1-21103b0d1aba", + DifferentHost: []string{ + "a0cf03a5-d921-4877-bb5c-86d26cf818e1", + "8c19174f-4220-44f0-824a-cd1eeef10287", + }, + SameHost: []string{ + "a0cf03a5-d921-4877-bb5c-86d26cf818e1", + "8c19174f-4220-44f0-824a-cd1eeef10287", + }, + Query: []interface{}{"and", []string{">=", "$free_ram_mb", "1024"}, []string{">=", "$free_disk_mb", "204800"}}, + TargetCell: "foobar", + BuildNearHostIP: "192.168.1.1/24", + AdditionalProperties: map[string]interface{}{"reservation": "a0cf03a5-d921-4877-bb5c-86d26cf818e1"}, + } + + ext := schedulerhints.CreateOptsExt{ + CreateOptsBuilder: base, + SchedulerHints: schedulerHints, + } + + expected := ` + { + "server": { + "name": "createdserver", + "imageRef": "asdfasdfasdf", + "flavorRef": "performance1-1" + }, + "os:scheduler_hints": { + "group": "101aed42-22d9-4a3e-9ba1-21103b0d1aba", + "different_host": [ + "a0cf03a5-d921-4877-bb5c-86d26cf818e1", + "8c19174f-4220-44f0-824a-cd1eeef10287" + ], + "same_host": [ + "a0cf03a5-d921-4877-bb5c-86d26cf818e1", + "8c19174f-4220-44f0-824a-cd1eeef10287" + ], + "query": [ + "and", + [">=", "$free_ram_mb", "1024"], + [">=", "$free_disk_mb", "204800"] + ], + "target_cell": "foobar", + "build_near_host_ip": "192.168.1.1", + "cidr": "/24", + "reservation": "a0cf03a5-d921-4877-bb5c-86d26cf818e1" + } + } + ` + actual, err := ext.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expected, actual) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/doc.go new file mode 100644 index 0000000000..702f32c985 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/doc.go @@ -0,0 +1 @@ +package secgroups diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/requests.go new file mode 100644 index 0000000000..ec8019f18c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/requests.go @@ -0,0 +1,171 @@ +package secgroups + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +func commonList(client *gophercloud.ServiceClient, url string) pagination.Pager { + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return SecurityGroupPage{pagination.SinglePageBase(r)} + }) +} + +// List will return a collection of all the security groups for a particular +// tenant. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return commonList(client, rootURL(client)) +} + +// ListByServer will return a collection of all the security groups which are +// associated with a particular server. +func ListByServer(client *gophercloud.ServiceClient, serverID string) pagination.Pager { + return commonList(client, listByServerURL(client, serverID)) +} + +// GroupOpts is the underlying struct responsible for creating or updating +// security groups. It therefore represents the mutable attributes of a +// security group. +type GroupOpts struct { + // the name of your security group. + Name string `json:"name" required:"true"` + // the description of your security group. + Description string `json:"description" required:"true"` +} + +// CreateOpts is the struct responsible for creating a security group. +type CreateOpts GroupOpts + +// CreateOptsBuilder builds the create options into a serializable format. +type CreateOptsBuilder interface { + ToSecGroupCreateMap() (map[string]interface{}, error) +} + +// ToSecGroupCreateMap builds the create options into a serializable format. +func (opts CreateOpts) ToSecGroupCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "security_group") +} + +// Create will create a new security group. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToSecGroupCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(rootURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// UpdateOpts is the struct responsible for updating an existing security group. +type UpdateOpts GroupOpts + +// UpdateOptsBuilder builds the update options into a serializable format. +type UpdateOptsBuilder interface { + ToSecGroupUpdateMap() (map[string]interface{}, error) +} + +// ToSecGroupUpdateMap builds the update options into a serializable format. +func (opts UpdateOpts) ToSecGroupUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "security_group") +} + +// Update will modify the mutable properties of a security group, notably its +// name and description. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToSecGroupUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(resourceURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Get will return details for a particular security group. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(resourceURL(client, id), &r.Body, nil) + return +} + +// Delete will permanently delete a security group from the project. +func Delete(client *gophercloud.ServiceClient, id string) (r gophercloud.ErrResult) { + _, r.Err = client.Delete(resourceURL(client, id), nil) + return +} + +// CreateRuleOpts represents the configuration for adding a new rule to an +// existing security group. +type CreateRuleOpts struct { + // the ID of the group that this rule will be added to. + ParentGroupID string `json:"parent_group_id" required:"true"` + // the lower bound of the port range that will be opened. + FromPort int `json:"from_port"` + // the upper bound of the port range that will be opened. + ToPort int `json:"to_port"` + // the protocol type that will be allowed, e.g. TCP. + IPProtocol string `json:"ip_protocol" required:"true"` + // ONLY required if FromGroupID is blank. This represents the IP range that + // will be the source of network traffic to your security group. Use + // 0.0.0.0/0 to allow all IP addresses. + CIDR string `json:"cidr,omitempty" or:"FromGroupID"` + // ONLY required if CIDR is blank. This value represents the ID of a group + // that forwards traffic to the parent group. So, instead of accepting + // network traffic from an entire IP range, you can instead refine the + // inbound source by an existing security group. + FromGroupID string `json:"group_id,omitempty" or:"CIDR"` +} + +// CreateRuleOptsBuilder builds the create rule options into a serializable format. +type CreateRuleOptsBuilder interface { + ToRuleCreateMap() (map[string]interface{}, error) +} + +// ToRuleCreateMap builds the create rule options into a serializable format. +func (opts CreateRuleOpts) ToRuleCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "security_group_rule") +} + +// CreateRule will add a new rule to an existing security group (whose ID is +// specified in CreateRuleOpts). You have the option of controlling inbound +// traffic from either an IP range (CIDR) or from another security group. +func CreateRule(client *gophercloud.ServiceClient, opts CreateRuleOptsBuilder) (r CreateRuleResult) { + b, err := opts.ToRuleCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(rootRuleURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// DeleteRule will permanently delete a rule from a security group. +func DeleteRule(client *gophercloud.ServiceClient, id string) (r gophercloud.ErrResult) { + _, r.Err = client.Delete(resourceRuleURL(client, id), nil) + return +} + +func actionMap(prefix, groupName string) map[string]map[string]string { + return map[string]map[string]string{ + prefix + "SecurityGroup": map[string]string{"name": groupName}, + } +} + +// AddServer will associate a server and a security group, enforcing the +// rules of the group on the server. +func AddServer(client *gophercloud.ServiceClient, serverID, groupName string) (r gophercloud.ErrResult) { + _, r.Err = client.Post(serverActionURL(client, serverID), actionMap("add", groupName), &r.Body, nil) + return +} + +// RemoveServer will disassociate a server from a security group. +func RemoveServer(client *gophercloud.ServiceClient, serverID, groupName string) (r gophercloud.ErrResult) { + _, r.Err = client.Post(serverActionURL(client, serverID), actionMap("remove", groupName), &r.Body, nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/results.go new file mode 100644 index 0000000000..f49338a1da --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/results.go @@ -0,0 +1,184 @@ +package secgroups + +import ( + "encoding/json" + "strconv" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// SecurityGroup represents a security group. +type SecurityGroup struct { + // The unique ID of the group. If Neutron is installed, this ID will be + // represented as a string UUID; if Neutron is not installed, it will be a + // numeric ID. For the sake of consistency, we always cast it to a string. + ID string `json:"-"` + + // The human-readable name of the group, which needs to be unique. + Name string `json:"name"` + + // The human-readable description of the group. + Description string `json:"description"` + + // The rules which determine how this security group operates. + Rules []Rule `json:"rules"` + + // The ID of the tenant to which this security group belongs. + TenantID string `json:"tenant_id"` +} + +func (r *SecurityGroup) UnmarshalJSON(b []byte) error { + type tmp SecurityGroup + var s struct { + tmp + ID interface{} `json:"id"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = SecurityGroup(s.tmp) + + switch t := s.ID.(type) { + case float64: + r.ID = strconv.FormatFloat(t, 'f', -1, 64) + case string: + r.ID = t + } + + return err +} + +// Rule represents a security group rule, a policy which determines how a +// security group operates and what inbound traffic it allows in. +type Rule struct { + // The unique ID. If Neutron is installed, this ID will be + // represented as a string UUID; if Neutron is not installed, it will be a + // numeric ID. For the sake of consistency, we always cast it to a string. + ID string `json:"-"` + + // The lower bound of the port range which this security group should open up + FromPort int `json:"from_port"` + + // The upper bound of the port range which this security group should open up + ToPort int `json:"to_port"` + + // The IP protocol (e.g. TCP) which the security group accepts + IPProtocol string `json:"ip_protocol"` + + // The CIDR IP range whose traffic can be received + IPRange IPRange `json:"ip_range"` + + // The security group ID to which this rule belongs + ParentGroupID string `json:"parent_group_id"` + + // Not documented. + Group Group +} + +func (r *Rule) UnmarshalJSON(b []byte) error { + type tmp Rule + var s struct { + tmp + ID interface{} `json:"id"` + ParentGroupID interface{} `json:"parent_group_id"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = Rule(s.tmp) + + switch t := s.ID.(type) { + case float64: + r.ID = strconv.FormatFloat(t, 'f', -1, 64) + case string: + r.ID = t + } + + switch t := s.ParentGroupID.(type) { + case float64: + r.ParentGroupID = strconv.FormatFloat(t, 'f', -1, 64) + case string: + r.ParentGroupID = t + } + + return err +} + +// IPRange represents the IP range whose traffic will be accepted by the +// security group. +type IPRange struct { + CIDR string +} + +// Group represents a group. +type Group struct { + TenantID string `json:"tenant_id"` + Name string +} + +// SecurityGroupPage is a single page of a SecurityGroup collection. +type SecurityGroupPage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a page of Security Groups contains any results. +func (page SecurityGroupPage) IsEmpty() (bool, error) { + users, err := ExtractSecurityGroups(page) + return len(users) == 0, err +} + +// ExtractSecurityGroups returns a slice of SecurityGroups contained in a single page of results. +func ExtractSecurityGroups(r pagination.Page) ([]SecurityGroup, error) { + var s struct { + SecurityGroups []SecurityGroup `json:"security_groups"` + } + err := (r.(SecurityGroupPage)).ExtractInto(&s) + return s.SecurityGroups, err +} + +type commonResult struct { + gophercloud.Result +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + commonResult +} + +// Extract will extract a SecurityGroup struct from most responses. +func (r commonResult) Extract() (*SecurityGroup, error) { + var s struct { + SecurityGroup *SecurityGroup `json:"security_group"` + } + err := r.ExtractInto(&s) + return s.SecurityGroup, err +} + +// CreateRuleResult represents the result when adding rules to a security group. +type CreateRuleResult struct { + gophercloud.Result +} + +// Extract will extract a Rule struct from a CreateRuleResult. +func (r CreateRuleResult) Extract() (*Rule, error) { + var s struct { + Rule *Rule `json:"security_group_rule"` + } + err := r.ExtractInto(&s) + return s.Rule, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/testing/doc.go new file mode 100644 index 0000000000..fbf46133e5 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/testing/doc.go @@ -0,0 +1,2 @@ +// compute_extensions_secgroups_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/testing/fixtures.go new file mode 100644 index 0000000000..536e7f8ea1 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/testing/fixtures.go @@ -0,0 +1,328 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +const rootPath = "/os-security-groups" + +const listGroupsJSON = ` +{ + "security_groups": [ + { + "description": "default", + "id": "{groupID}", + "name": "default", + "rules": [], + "tenant_id": "openstack" + } + ] +} +` + +func mockListGroupsResponse(t *testing.T) { + th.Mux.HandleFunc(rootPath, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, listGroupsJSON) + }) +} + +func mockListGroupsByServerResponse(t *testing.T, serverID string) { + url := fmt.Sprintf("/servers/%s%s", serverID, rootPath) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, listGroupsJSON) + }) +} + +func mockCreateGroupResponse(t *testing.T) { + th.Mux.HandleFunc(rootPath, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "security_group": { + "name": "test", + "description": "something" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_group": { + "description": "something", + "id": "{groupID}", + "name": "test", + "rules": [], + "tenant_id": "openstack" + } +} +`) + }) +} + +func mockUpdateGroupResponse(t *testing.T, groupID string) { + url := fmt.Sprintf("%s/%s", rootPath, groupID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "security_group": { + "name": "new_name", + "description": "new_desc" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_group": { + "description": "something", + "id": "{groupID}", + "name": "new_name", + "rules": [], + "tenant_id": "openstack" + } +} +`) + }) +} + +func mockGetGroupsResponse(t *testing.T, groupID string) { + url := fmt.Sprintf("%s/%s", rootPath, groupID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_group": { + "description": "default", + "id": "{groupID}", + "name": "default", + "rules": [ + { + "from_port": 80, + "group": { + "tenant_id": "openstack", + "name": "default" + }, + "ip_protocol": "TCP", + "to_port": 85, + "parent_group_id": "{groupID}", + "ip_range": { + "cidr": "0.0.0.0" + }, + "id": "{ruleID}" + } + ], + "tenant_id": "openstack" + } +} + `) + }) +} + +func mockGetNumericIDGroupResponse(t *testing.T, groupID int) { + url := fmt.Sprintf("%s/%d", rootPath, groupID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_group": { + "id": %d + } +} + `, groupID) + }) +} + +func mockGetNumericIDGroupRuleResponse(t *testing.T, groupID int) { + url := fmt.Sprintf("%s/%d", rootPath, groupID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_group": { + "id": %d, + "rules": [ + { + "parent_group_id": %d, + "id": %d + } + ] + } +} + `, groupID, groupID, groupID) + }) +} + +func mockDeleteGroupResponse(t *testing.T, groupID string) { + url := fmt.Sprintf("%s/%s", rootPath, groupID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockAddRuleResponse(t *testing.T) { + th.Mux.HandleFunc("/os-security-group-rules", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "security_group_rule": { + "from_port": 22, + "ip_protocol": "TCP", + "to_port": 22, + "parent_group_id": "{groupID}", + "cidr": "0.0.0.0/0" + } +} `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_group_rule": { + "from_port": 22, + "group": {}, + "ip_protocol": "TCP", + "to_port": 22, + "parent_group_id": "{groupID}", + "ip_range": { + "cidr": "0.0.0.0/0" + }, + "id": "{ruleID}" + } +}`) + }) +} + +func mockAddRuleResponseICMPZero(t *testing.T) { + th.Mux.HandleFunc("/os-security-group-rules", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "security_group_rule": { + "from_port": 0, + "ip_protocol": "ICMP", + "to_port": 0, + "parent_group_id": "{groupID}", + "cidr": "0.0.0.0/0" + } +} `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_group_rule": { + "from_port": 0, + "group": {}, + "ip_protocol": "ICMP", + "to_port": 0, + "parent_group_id": "{groupID}", + "ip_range": { + "cidr": "0.0.0.0/0" + }, + "id": "{ruleID}" + } +}`) + }) +} + +func mockDeleteRuleResponse(t *testing.T, ruleID string) { + url := fmt.Sprintf("/os-security-group-rules/%s", ruleID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockAddServerToGroupResponse(t *testing.T, serverID string) { + url := fmt.Sprintf("/servers/%s/action", serverID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "addSecurityGroup": { + "name": "test" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + fmt.Fprintf(w, `{}`) + }) +} + +func mockRemoveServerFromGroupResponse(t *testing.T, serverID string) { + url := fmt.Sprintf("/servers/%s/action", serverID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "removeSecurityGroup": { + "name": "test" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + fmt.Fprintf(w, `{}`) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/testing/requests_test.go new file mode 100644 index 0000000000..b5207646be --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/testing/requests_test.go @@ -0,0 +1,302 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +const ( + serverID = "{serverID}" + groupID = "{groupID}" + ruleID = "{ruleID}" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockListGroupsResponse(t) + + count := 0 + + err := secgroups.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := secgroups.ExtractSecurityGroups(page) + if err != nil { + t.Errorf("Failed to extract users: %v", err) + return false, err + } + + expected := []secgroups.SecurityGroup{ + { + ID: groupID, + Description: "default", + Name: "default", + Rules: []secgroups.Rule{}, + TenantID: "openstack", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, count) +} + +func TestListByServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockListGroupsByServerResponse(t, serverID) + + count := 0 + + err := secgroups.ListByServer(client.ServiceClient(), serverID).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := secgroups.ExtractSecurityGroups(page) + if err != nil { + t.Errorf("Failed to extract users: %v", err) + return false, err + } + + expected := []secgroups.SecurityGroup{ + { + ID: groupID, + Description: "default", + Name: "default", + Rules: []secgroups.Rule{}, + TenantID: "openstack", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, count) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockCreateGroupResponse(t) + + opts := secgroups.CreateOpts{ + Name: "test", + Description: "something", + } + + group, err := secgroups.Create(client.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) + + expected := &secgroups.SecurityGroup{ + ID: groupID, + Name: "test", + Description: "something", + TenantID: "openstack", + Rules: []secgroups.Rule{}, + } + th.AssertDeepEquals(t, expected, group) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockUpdateGroupResponse(t, groupID) + + opts := secgroups.UpdateOpts{ + Name: "new_name", + Description: "new_desc", + } + + group, err := secgroups.Update(client.ServiceClient(), groupID, opts).Extract() + th.AssertNoErr(t, err) + + expected := &secgroups.SecurityGroup{ + ID: groupID, + Name: "new_name", + Description: "something", + TenantID: "openstack", + Rules: []secgroups.Rule{}, + } + th.AssertDeepEquals(t, expected, group) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockGetGroupsResponse(t, groupID) + + group, err := secgroups.Get(client.ServiceClient(), groupID).Extract() + th.AssertNoErr(t, err) + + expected := &secgroups.SecurityGroup{ + ID: groupID, + Description: "default", + Name: "default", + TenantID: "openstack", + Rules: []secgroups.Rule{ + { + FromPort: 80, + ToPort: 85, + IPProtocol: "TCP", + IPRange: secgroups.IPRange{CIDR: "0.0.0.0"}, + Group: secgroups.Group{TenantID: "openstack", Name: "default"}, + ParentGroupID: groupID, + ID: ruleID, + }, + }, + } + + th.AssertDeepEquals(t, expected, group) +} + +func TestGetNumericID(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + numericGroupID := 12345 + + mockGetNumericIDGroupResponse(t, numericGroupID) + + group, err := secgroups.Get(client.ServiceClient(), "12345").Extract() + th.AssertNoErr(t, err) + + expected := &secgroups.SecurityGroup{ID: "12345"} + th.AssertDeepEquals(t, expected, group) +} + +func TestGetNumericRuleID(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + numericGroupID := 12345 + + mockGetNumericIDGroupRuleResponse(t, numericGroupID) + + group, err := secgroups.Get(client.ServiceClient(), "12345").Extract() + th.AssertNoErr(t, err) + + expected := &secgroups.SecurityGroup{ + ID: "12345", + Rules: []secgroups.Rule{ + { + ParentGroupID: "12345", + ID: "12345", + }, + }, + } + th.AssertDeepEquals(t, expected, group) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDeleteGroupResponse(t, groupID) + + err := secgroups.Delete(client.ServiceClient(), groupID).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestAddRule(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockAddRuleResponse(t) + + opts := secgroups.CreateRuleOpts{ + ParentGroupID: groupID, + FromPort: 22, + ToPort: 22, + IPProtocol: "TCP", + CIDR: "0.0.0.0/0", + } + + rule, err := secgroups.CreateRule(client.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) + + expected := &secgroups.Rule{ + FromPort: 22, + ToPort: 22, + Group: secgroups.Group{}, + IPProtocol: "TCP", + ParentGroupID: groupID, + IPRange: secgroups.IPRange{CIDR: "0.0.0.0/0"}, + ID: ruleID, + } + + th.AssertDeepEquals(t, expected, rule) +} + +func TestAddRuleICMPZero(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockAddRuleResponseICMPZero(t) + + opts := secgroups.CreateRuleOpts{ + ParentGroupID: groupID, + FromPort: 0, + ToPort: 0, + IPProtocol: "ICMP", + CIDR: "0.0.0.0/0", + } + + rule, err := secgroups.CreateRule(client.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) + + expected := &secgroups.Rule{ + FromPort: 0, + ToPort: 0, + Group: secgroups.Group{}, + IPProtocol: "ICMP", + ParentGroupID: groupID, + IPRange: secgroups.IPRange{CIDR: "0.0.0.0/0"}, + ID: ruleID, + } + + th.AssertDeepEquals(t, expected, rule) +} + +func TestDeleteRule(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDeleteRuleResponse(t, ruleID) + + err := secgroups.DeleteRule(client.ServiceClient(), ruleID).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestAddServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockAddServerToGroupResponse(t, serverID) + + err := secgroups.AddServer(client.ServiceClient(), serverID, "test").ExtractErr() + th.AssertNoErr(t, err) +} + +func TestRemoveServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockRemoveServerFromGroupResponse(t, serverID) + + err := secgroups.RemoveServer(client.ServiceClient(), serverID, "test").ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/urls.go new file mode 100644 index 0000000000..d99746cae9 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/urls.go @@ -0,0 +1,32 @@ +package secgroups + +import "github.com/gophercloud/gophercloud" + +const ( + secgrouppath = "os-security-groups" + rulepath = "os-security-group-rules" +) + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(secgrouppath, id) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(secgrouppath) +} + +func listByServerURL(c *gophercloud.ServiceClient, serverID string) string { + return c.ServiceURL("servers", serverID, secgrouppath) +} + +func rootRuleURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rulepath) +} + +func resourceRuleURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rulepath, id) +} + +func serverActionURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("servers", id, "action") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/doc.go new file mode 100644 index 0000000000..1e5ed568da --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/doc.go @@ -0,0 +1,2 @@ +// Package servergroups provides the ability to manage server groups +package servergroups diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/requests.go new file mode 100644 index 0000000000..ee98837074 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/requests.go @@ -0,0 +1,57 @@ +package servergroups + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// List returns a Pager that allows you to iterate over a collection of ServerGroups. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page { + return ServerGroupPage{pagination.SinglePageBase(r)} + }) +} + +// CreateOptsBuilder describes struct types that can be accepted by the Create call. Notably, the +// CreateOpts struct in this package does. +type CreateOptsBuilder interface { + ToServerGroupCreateMap() (map[string]interface{}, error) +} + +// CreateOpts specifies a Server Group allocation request +type CreateOpts struct { + // Name is the name of the server group + Name string `json:"name" required:"true"` + // Policies are the server group policies + Policies []string `json:"policies" required:"true"` +} + +// ToServerGroupCreateMap constructs a request body from CreateOpts. +func (opts CreateOpts) ToServerGroupCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "server_group") +} + +// Create requests the creation of a new Server Group +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToServerGroupCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Get returns data about a previously created ServerGroup. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// Delete requests the deletion of a previously allocated ServerGroup. +func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/results.go new file mode 100644 index 0000000000..ab49b35a44 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/results.go @@ -0,0 +1,78 @@ +package servergroups + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// A ServerGroup creates a policy for instance placement in the cloud +type ServerGroup struct { + // ID is the unique ID of the Server Group. + ID string `json:"id"` + + // Name is the common name of the server group. + Name string `json:"name"` + + // Polices are the group policies. + Policies []string `json:"policies"` + + // Members are the members of the server group. + Members []string `json:"members"` + + // Metadata includes a list of all user-specified key-value pairs attached to the Server Group. + Metadata map[string]interface{} +} + +// ServerGroupPage stores a single, only page of ServerGroups +// results from a List call. +type ServerGroupPage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a ServerGroupsPage is empty. +func (page ServerGroupPage) IsEmpty() (bool, error) { + va, err := ExtractServerGroups(page) + return len(va) == 0, err +} + +// ExtractServerGroups interprets a page of results as a slice of +// ServerGroups. +func ExtractServerGroups(r pagination.Page) ([]ServerGroup, error) { + var s struct { + ServerGroups []ServerGroup `json:"server_groups"` + } + err := (r.(ServerGroupPage)).ExtractInto(&s) + return s.ServerGroups, err +} + +type ServerGroupResult struct { + gophercloud.Result +} + +// Extract is a method that attempts to interpret any Server Group resource +// response as a ServerGroup struct. +func (r ServerGroupResult) Extract() (*ServerGroup, error) { + var s struct { + ServerGroup *ServerGroup `json:"server_group"` + } + err := r.ExtractInto(&s) + return s.ServerGroup, err +} + +// CreateResult is the response from a Create operation. Call its Extract method to interpret it +// as a ServerGroup. +type CreateResult struct { + ServerGroupResult +} + +// GetResult is the response from a Get operation. Call its Extract method to interpret it +// as a ServerGroup. +type GetResult struct { + ServerGroupResult +} + +// DeleteResult is the response from a Delete operation. Call its Extract method to determine if +// the call succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/testing/doc.go new file mode 100644 index 0000000000..65433f7362 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/testing/doc.go @@ -0,0 +1,2 @@ +// compute_extensions_servergroups_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/testing/fixtures.go new file mode 100644 index 0000000000..b53757a40f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/testing/fixtures.go @@ -0,0 +1,160 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +// ListOutput is a sample response to a List call. +const ListOutput = ` +{ + "server_groups": [ + { + "id": "616fb98f-46ca-475e-917e-2563e5a8cd19", + "name": "test", + "policies": [ + "anti-affinity" + ], + "members": [], + "metadata": {} + }, + { + "id": "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + "name": "test2", + "policies": [ + "affinity" + ], + "members": [], + "metadata": {} + } + ] +} +` + +// GetOutput is a sample response to a Get call. +const GetOutput = ` +{ + "server_group": { + "id": "616fb98f-46ca-475e-917e-2563e5a8cd19", + "name": "test", + "policies": [ + "anti-affinity" + ], + "members": [], + "metadata": {} + } +} +` + +// CreateOutput is a sample response to a Post call +const CreateOutput = ` +{ + "server_group": { + "id": "616fb98f-46ca-475e-917e-2563e5a8cd19", + "name": "test", + "policies": [ + "anti-affinity" + ], + "members": [], + "metadata": {} + } +} +` + +// FirstServerGroup is the first result in ListOutput. +var FirstServerGroup = servergroups.ServerGroup{ + ID: "616fb98f-46ca-475e-917e-2563e5a8cd19", + Name: "test", + Policies: []string{ + "anti-affinity", + }, + Members: []string{}, + Metadata: map[string]interface{}{}, +} + +// SecondServerGroup is the second result in ListOutput. +var SecondServerGroup = servergroups.ServerGroup{ + ID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + Name: "test2", + Policies: []string{ + "affinity", + }, + Members: []string{}, + Metadata: map[string]interface{}{}, +} + +// ExpectedServerGroupSlice is the slice of results that should be parsed +// from ListOutput, in the expected order. +var ExpectedServerGroupSlice = []servergroups.ServerGroup{FirstServerGroup, SecondServerGroup} + +// CreatedServerGroup is the parsed result from CreateOutput. +var CreatedServerGroup = servergroups.ServerGroup{ + ID: "616fb98f-46ca-475e-917e-2563e5a8cd19", + Name: "test", + Policies: []string{ + "anti-affinity", + }, + Members: []string{}, + Metadata: map[string]interface{}{}, +} + +// HandleListSuccessfully configures the test server to respond to a List request. +func HandleListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-server-groups", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ListOutput) + }) +} + +// HandleGetSuccessfully configures the test server to respond to a Get request +// for an existing server group +func HandleGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-server-groups/4d8c3732-a248-40ed-bebc-539a6ffd25c0", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetOutput) + }) +} + +// HandleCreateSuccessfully configures the test server to respond to a Create request +// for a new server group +func HandleCreateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-server-groups", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, ` +{ + "server_group": { + "name": "test", + "policies": [ + "anti-affinity" + ] + } +} +`) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, CreateOutput) + }) +} + +// HandleDeleteSuccessfully configures the test server to respond to a Delete request for a +// an existing server group +func HandleDeleteSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-server-groups/616fb98f-46ca-475e-917e-2563e5a8cd19", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/testing/requests_test.go new file mode 100644 index 0000000000..d86fa56861 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/testing/requests_test.go @@ -0,0 +1,60 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListSuccessfully(t) + + count := 0 + err := servergroups.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := servergroups.ExtractServerGroups(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedServerGroupSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateSuccessfully(t) + + actual, err := servergroups.Create(client.ServiceClient(), servergroups.CreateOpts{ + Name: "test", + Policies: []string{"anti-affinity"}, + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &CreatedServerGroup, actual) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetSuccessfully(t) + + actual, err := servergroups.Get(client.ServiceClient(), "4d8c3732-a248-40ed-bebc-539a6ffd25c0").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &FirstServerGroup, actual) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteSuccessfully(t) + + err := servergroups.Delete(client.ServiceClient(), "616fb98f-46ca-475e-917e-2563e5a8cd19").ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/urls.go new file mode 100644 index 0000000000..9a1f99b199 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/urls.go @@ -0,0 +1,25 @@ +package servergroups + +import "github.com/gophercloud/gophercloud" + +const resourcePath = "os-server-groups" + +func resourceURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(resourcePath) +} + +func listURL(c *gophercloud.ServiceClient) string { + return resourceURL(c) +} + +func createURL(c *gophercloud.ServiceClient) string { + return resourceURL(c) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id) +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return getURL(c, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/startstop/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/startstop/doc.go new file mode 100644 index 0000000000..d2729f8743 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/startstop/doc.go @@ -0,0 +1,5 @@ +/* +Package startstop provides functionality to start and stop servers that have +been provisioned by the OpenStack Compute service. +*/ +package startstop diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/startstop/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/startstop/requests.go new file mode 100644 index 0000000000..1d8a593b9f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/startstop/requests.go @@ -0,0 +1,19 @@ +package startstop + +import "github.com/gophercloud/gophercloud" + +func actionURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "action") +} + +// Start is the operation responsible for starting a Compute server. +func Start(client *gophercloud.ServiceClient, id string) (r gophercloud.ErrResult) { + _, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"os-start": nil}, nil, nil) + return +} + +// Stop is the operation responsible for stopping a Compute server. +func Stop(client *gophercloud.ServiceClient, id string) (r gophercloud.ErrResult) { + _, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"os-stop": nil}, nil, nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/startstop/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/startstop/testing/doc.go new file mode 100644 index 0000000000..6135475739 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/startstop/testing/doc.go @@ -0,0 +1,2 @@ +// compute_extensions_startstop_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/startstop/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/startstop/testing/fixtures.go new file mode 100644 index 0000000000..1086b0e341 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/startstop/testing/fixtures.go @@ -0,0 +1,27 @@ +package testing + +import ( + "net/http" + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func mockStartServerResponse(t *testing.T, id string) { + th.Mux.HandleFunc("/servers/"+id+"/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{"os-start": null}`) + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockStopServerResponse(t *testing.T, id string) { + th.Mux.HandleFunc("/servers/"+id+"/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{"os-stop": null}`) + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/startstop/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/startstop/testing/requests_test.go new file mode 100644 index 0000000000..be45bf5c71 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/startstop/testing/requests_test.go @@ -0,0 +1,31 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/startstop" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +const serverID = "{serverId}" + +func TestStart(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockStartServerResponse(t, serverID) + + err := startstop.Start(client.ServiceClient(), serverID).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestStop(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockStopServerResponse(t, serverID) + + err := startstop.Stop(client.ServiceClient(), serverID).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/suspendresume/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/suspendresume/doc.go new file mode 100644 index 0000000000..ad37c49915 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/suspendresume/doc.go @@ -0,0 +1,5 @@ +/* +Package suspendresume provides functionality to suspend and resume servers that have +been provisioned by the OpenStack Compute service. +*/ +package suspendresume diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/suspendresume/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/suspendresume/requests.go new file mode 100644 index 0000000000..d5f03fcdf1 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/suspendresume/requests.go @@ -0,0 +1,19 @@ +package suspendresume + +import "github.com/gophercloud/gophercloud" + +func actionURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "action") +} + +// Suspend is the operation responsible for suspending a Compute server. +func Suspend(client *gophercloud.ServiceClient, id string) (r gophercloud.ErrResult) { + _, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"suspend": nil}, nil, nil) + return +} + +// Resume is the operation responsible for resuming a Compute server. +func Resume(client *gophercloud.ServiceClient, id string) (r gophercloud.ErrResult) { + _, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"resume": nil}, nil, nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/suspendresume/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/suspendresume/testing/doc.go new file mode 100644 index 0000000000..b81051a39c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/suspendresume/testing/doc.go @@ -0,0 +1,2 @@ +// compute_extensions_suspendresume_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/suspendresume/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/suspendresume/testing/fixtures.go new file mode 100644 index 0000000000..5c6405deeb --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/suspendresume/testing/fixtures.go @@ -0,0 +1,27 @@ +package testing + +import ( + "net/http" + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func mockSuspendServerResponse(t *testing.T, id string) { + th.Mux.HandleFunc("/servers/"+id+"/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{"suspend": null}`) + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockResumeServerResponse(t *testing.T, id string) { + th.Mux.HandleFunc("/servers/"+id+"/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{"resume": null}`) + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/suspendresume/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/suspendresume/testing/requests_test.go new file mode 100644 index 0000000000..7c8e4ec6b1 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/suspendresume/testing/requests_test.go @@ -0,0 +1,31 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/suspendresume" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +const serverID = "{serverId}" + +func TestSuspend(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockSuspendServerResponse(t, serverID) + + err := suspendresume.Suspend(client.ServiceClient(), serverID).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestResume(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockResumeServerResponse(t, serverID) + + err := suspendresume.Resume(client.ServiceClient(), serverID).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/tenantnetworks/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/tenantnetworks/doc.go new file mode 100644 index 0000000000..65c46ff507 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/tenantnetworks/doc.go @@ -0,0 +1,2 @@ +// Package tenantnetworks provides the ability for tenants to see information about the networks they have access to +package tenantnetworks diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/tenantnetworks/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/tenantnetworks/requests.go new file mode 100644 index 0000000000..82836d4b8b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/tenantnetworks/requests.go @@ -0,0 +1,19 @@ +package tenantnetworks + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// List returns a Pager that allows you to iterate over a collection of Network. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page { + return NetworkPage{pagination.SinglePageBase(r)} + }) +} + +// Get returns data about a previously created Network. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/tenantnetworks/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/tenantnetworks/results.go new file mode 100644 index 0000000000..88cbc80ec2 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/tenantnetworks/results.go @@ -0,0 +1,59 @@ +package tenantnetworks + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// A Network represents a nova-network that an instance communicates on +type Network struct { + // CIDR is the IPv4 subnet. + CIDR string `json:"cidr"` + + // ID is the UUID of the network. + ID string `json:"id"` + + // Name is the common name that the network has. + Name string `json:"label"` +} + +// NetworkPage stores a single, only page of Networks +// results from a List call. +type NetworkPage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a NetworkPage is empty. +func (page NetworkPage) IsEmpty() (bool, error) { + va, err := ExtractNetworks(page) + return len(va) == 0, err +} + +// ExtractNetworks interprets a page of results as a slice of Networks +func ExtractNetworks(r pagination.Page) ([]Network, error) { + var s struct { + Networks []Network `json:"networks"` + } + err := (r.(NetworkPage)).ExtractInto(&s) + return s.Networks, err +} + +type NetworkResult struct { + gophercloud.Result +} + +// Extract is a method that attempts to interpret any Network resource +// response as a Network struct. +func (r NetworkResult) Extract() (*Network, error) { + var s struct { + Network *Network `json:"network"` + } + err := r.ExtractInto(&s) + return s.Network, err +} + +// GetResult is the response from a Get operation. Call its Extract method to interpret it +// as a Network. +type GetResult struct { + NetworkResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/tenantnetworks/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/tenantnetworks/testing/doc.go new file mode 100644 index 0000000000..7ed7ce3f99 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/tenantnetworks/testing/doc.go @@ -0,0 +1,2 @@ +// compute_extensions_tenantnetworks_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/tenantnetworks/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/tenantnetworks/testing/fixtures.go new file mode 100644 index 0000000000..ae679b46e4 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/tenantnetworks/testing/fixtures.go @@ -0,0 +1,83 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/tenantnetworks" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +// ListOutput is a sample response to a List call. +const ListOutput = ` +{ + "networks": [ + { + "cidr": "10.0.0.0/29", + "id": "20c8acc0-f747-4d71-a389-46d078ebf047", + "label": "mynet_0" + }, + { + "cidr": "10.0.0.10/29", + "id": "20c8acc0-f747-4d71-a389-46d078ebf000", + "label": "mynet_1" + } + ] +} +` + +// GetOutput is a sample response to a Get call. +const GetOutput = ` +{ + "network": { + "cidr": "10.0.0.10/29", + "id": "20c8acc0-f747-4d71-a389-46d078ebf000", + "label": "mynet_1" + } +} +` + +// FirstNetwork is the first result in ListOutput. +var nilTime time.Time +var FirstNetwork = tenantnetworks.Network{ + CIDR: "10.0.0.0/29", + ID: "20c8acc0-f747-4d71-a389-46d078ebf047", + Name: "mynet_0", +} + +// SecondNetwork is the second result in ListOutput. +var SecondNetwork = tenantnetworks.Network{ + CIDR: "10.0.0.10/29", + ID: "20c8acc0-f747-4d71-a389-46d078ebf000", + Name: "mynet_1", +} + +// ExpectedNetworkSlice is the slice of results that should be parsed +// from ListOutput, in the expected order. +var ExpectedNetworkSlice = []tenantnetworks.Network{FirstNetwork, SecondNetwork} + +// HandleListSuccessfully configures the test server to respond to a List request. +func HandleListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-tenant-networks", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ListOutput) + }) +} + +// HandleGetSuccessfully configures the test server to respond to a Get request +// for an existing network. +func HandleGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-tenant-networks/20c8acc0-f747-4d71-a389-46d078ebf000", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetOutput) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/tenantnetworks/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/tenantnetworks/testing/requests_test.go new file mode 100644 index 0000000000..703c8468b7 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/tenantnetworks/testing/requests_test.go @@ -0,0 +1,38 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/tenantnetworks" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListSuccessfully(t) + + count := 0 + err := tenantnetworks.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := tenantnetworks.ExtractNetworks(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedNetworkSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetSuccessfully(t) + + actual, err := tenantnetworks.Get(client.ServiceClient(), "20c8acc0-f747-4d71-a389-46d078ebf000").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &SecondNetwork, actual) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/tenantnetworks/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/tenantnetworks/urls.go new file mode 100644 index 0000000000..683041ded3 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/tenantnetworks/urls.go @@ -0,0 +1,17 @@ +package tenantnetworks + +import "github.com/gophercloud/gophercloud" + +const resourcePath = "os-tenant-networks" + +func resourceURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(resourcePath) +} + +func listURL(c *gophercloud.ServiceClient) string { + return resourceURL(c) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/testing/delegate_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/testing/delegate_test.go new file mode 100644 index 0000000000..822093ff47 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/testing/delegate_test.go @@ -0,0 +1,56 @@ +package testing + +import ( + "testing" + + common "github.com/gophercloud/gophercloud/openstack/common/extensions" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleListExtensionsSuccessfully(t) + + count := 0 + extensions.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := extensions.ExtractExtensions(page) + th.AssertNoErr(t, err) + + expected := []common.Extension{ + common.Extension{ + Updated: "2013-01-20T00:00:00-00:00", + Name: "Neutron Service Type Management", + Links: []interface{}{}, + Namespace: "http://docs.openstack.org/ext/neutron/service-type/api/v1.0", + Alias: "service-type", + Description: "API for retrieving service providers for Neutron advanced services", + }, + } + th.AssertDeepEquals(t, expected, actual) + + return true, nil + }) + th.CheckEquals(t, 1, count) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleGetExtensionsSuccessfully(t) + + ext, err := extensions.Get(client.ServiceClient(), "agent").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, ext.Updated, "2013-02-03T10:00:00-00:00") + th.AssertEquals(t, ext.Name, "agent") + th.AssertEquals(t, ext.Namespace, "http://docs.openstack.org/ext/agent/api/v2.0") + th.AssertEquals(t, ext.Alias, "agent") + th.AssertEquals(t, ext.Description, "The agent management extension.") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/testing/doc.go new file mode 100644 index 0000000000..5818711cfb --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/testing/doc.go @@ -0,0 +1,2 @@ +// compute_extensions_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/testing/fixtures.go new file mode 100644 index 0000000000..2a3fb69094 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/testing/fixtures.go @@ -0,0 +1,57 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func HandleListExtensionsSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/extensions", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + + fmt.Fprintf(w, ` +{ + "extensions": [ + { + "updated": "2013-01-20T00:00:00-00:00", + "name": "Neutron Service Type Management", + "links": [], + "namespace": "http://docs.openstack.org/ext/neutron/service-type/api/v1.0", + "alias": "service-type", + "description": "API for retrieving service providers for Neutron advanced services" + } + ] +} + `) + }) +} + +func HandleGetExtensionsSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/extensions/agent", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "extension": { + "updated": "2013-02-03T10:00:00-00:00", + "name": "agent", + "links": [], + "namespace": "http://docs.openstack.org/ext/agent/api/v2.0", + "alias": "agent", + "description": "The agent management extension." + } +} + `) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/doc.go new file mode 100644 index 0000000000..22f68d80e5 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/doc.go @@ -0,0 +1,3 @@ +// Package volumeattach provides the ability to attach and detach volumes +// to instances +package volumeattach diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/requests.go new file mode 100644 index 0000000000..ee4d62ddb0 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/requests.go @@ -0,0 +1,57 @@ +package volumeattach + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// List returns a Pager that allows you to iterate over a collection of VolumeAttachments. +func List(client *gophercloud.ServiceClient, serverID string) pagination.Pager { + return pagination.NewPager(client, listURL(client, serverID), func(r pagination.PageResult) pagination.Page { + return VolumeAttachmentPage{pagination.SinglePageBase(r)} + }) +} + +// CreateOptsBuilder describes struct types that can be accepted by the Create call. Notable, the +// CreateOpts struct in this package does. +type CreateOptsBuilder interface { + ToVolumeAttachmentCreateMap() (map[string]interface{}, error) +} + +// CreateOpts specifies volume attachment creation or import parameters. +type CreateOpts struct { + // Device is the device that the volume will attach to the instance as. Omit for "auto" + Device string `json:"device,omitempty"` + // VolumeID is the ID of the volume to attach to the instance + VolumeID string `json:"volumeId" required:"true"` +} + +// ToVolumeAttachmentCreateMap constructs a request body from CreateOpts. +func (opts CreateOpts) ToVolumeAttachmentCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "volumeAttachment") +} + +// Create requests the creation of a new volume attachment on the server +func Create(client *gophercloud.ServiceClient, serverID string, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToVolumeAttachmentCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client, serverID), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Get returns public data about a previously created VolumeAttachment. +func Get(client *gophercloud.ServiceClient, serverID, attachmentID string) (r GetResult) { + _, r.Err = client.Get(getURL(client, serverID, attachmentID), &r.Body, nil) + return +} + +// Delete requests the deletion of a previous stored VolumeAttachment from the server. +func Delete(client *gophercloud.ServiceClient, serverID, attachmentID string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, serverID, attachmentID), nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/results.go new file mode 100644 index 0000000000..53faf5d3af --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/results.go @@ -0,0 +1,76 @@ +package volumeattach + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// VolumeAttachment controls the attachment of a volume to an instance. +type VolumeAttachment struct { + // ID is a unique id of the attachment + ID string `json:"id"` + + // Device is what device the volume is attached as + Device string `json:"device"` + + // VolumeID is the ID of the attached volume + VolumeID string `json:"volumeId"` + + // ServerID is the ID of the instance that has the volume attached + ServerID string `json:"serverId"` +} + +// VolumeAttachmentPage stores a single, only page of VolumeAttachments +// results from a List call. +type VolumeAttachmentPage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a VolumeAttachmentsPage is empty. +func (page VolumeAttachmentPage) IsEmpty() (bool, error) { + va, err := ExtractVolumeAttachments(page) + return len(va) == 0, err +} + +// ExtractVolumeAttachments interprets a page of results as a slice of +// VolumeAttachments. +func ExtractVolumeAttachments(r pagination.Page) ([]VolumeAttachment, error) { + var s struct { + VolumeAttachments []VolumeAttachment `json:"volumeAttachments"` + } + err := (r.(VolumeAttachmentPage)).ExtractInto(&s) + return s.VolumeAttachments, err +} + +// VolumeAttachmentResult is the result from a volume attachment operation. +type VolumeAttachmentResult struct { + gophercloud.Result +} + +// Extract is a method that attempts to interpret any VolumeAttachment resource +// response as a VolumeAttachment struct. +func (r VolumeAttachmentResult) Extract() (*VolumeAttachment, error) { + var s struct { + VolumeAttachment *VolumeAttachment `json:"volumeAttachment"` + } + err := r.ExtractInto(&s) + return s.VolumeAttachment, err +} + +// CreateResult is the response from a Create operation. Call its Extract method to interpret it +// as a VolumeAttachment. +type CreateResult struct { + VolumeAttachmentResult +} + +// GetResult is the response from a Get operation. Call its Extract method to interpret it +// as a VolumeAttachment. +type GetResult struct { + VolumeAttachmentResult +} + +// DeleteResult is the response from a Delete operation. Call its Extract method to determine if +// the call succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/testing/doc.go new file mode 100644 index 0000000000..2cc0ab4af2 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/testing/doc.go @@ -0,0 +1,9 @@ +// compute_extensions_volumeattach_v2 +package testing + +/* +Package testing holds fixtures (which imports testing), +so that importing volumeattach package does not inadvertently import testing into production code +More information here: +https://github.com/gophercloud/gophercloud/issues/473 +*/ diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/testing/fixtures.go new file mode 100644 index 0000000000..4f996106e4 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/testing/fixtures.go @@ -0,0 +1,108 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +// ListOutput is a sample response to a List call. +const ListOutput = ` +{ + "volumeAttachments": [ + { + "device": "/dev/vdd", + "id": "a26887c6-c47b-4654-abb5-dfadf7d3f803", + "serverId": "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + "volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f803" + }, + { + "device": "/dev/vdc", + "id": "a26887c6-c47b-4654-abb5-dfadf7d3f804", + "serverId": "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + "volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f804" + } + ] +} +` + +// GetOutput is a sample response to a Get call. +const GetOutput = ` +{ + "volumeAttachment": { + "device": "/dev/vdc", + "id": "a26887c6-c47b-4654-abb5-dfadf7d3f804", + "serverId": "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + "volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f804" + } +} +` + +// CreateOutput is a sample response to a Create call. +const CreateOutput = ` +{ + "volumeAttachment": { + "device": "/dev/vdc", + "id": "a26887c6-c47b-4654-abb5-dfadf7d3f804", + "serverId": "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + "volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f804" + } +} +` + +// HandleListSuccessfully configures the test server to respond to a List request. +func HandleListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/4d8c3732-a248-40ed-bebc-539a6ffd25c0/os-volume_attachments", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ListOutput) + }) +} + +// HandleGetSuccessfully configures the test server to respond to a Get request +// for an existing attachment +func HandleGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/4d8c3732-a248-40ed-bebc-539a6ffd25c0/os-volume_attachments/a26887c6-c47b-4654-abb5-dfadf7d3f804", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetOutput) + }) +} + +// HandleCreateSuccessfully configures the test server to respond to a Create request +// for a new attachment +func HandleCreateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/4d8c3732-a248-40ed-bebc-539a6ffd25c0/os-volume_attachments", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, ` +{ + "volumeAttachment": { + "volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f804", + "device": "/dev/vdc" + } +} +`) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, CreateOutput) + }) +} + +// HandleDeleteSuccessfully configures the test server to respond to a Delete request for a +// an existing attachment +func HandleDeleteSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/4d8c3732-a248-40ed-bebc-539a6ffd25c0/os-volume_attachments/a26887c6-c47b-4654-abb5-dfadf7d3f804", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/testing/requests_test.go new file mode 100644 index 0000000000..9486f9bb56 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/testing/requests_test.go @@ -0,0 +1,102 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +// FirstVolumeAttachment is the first result in ListOutput. +var FirstVolumeAttachment = volumeattach.VolumeAttachment{ + Device: "/dev/vdd", + ID: "a26887c6-c47b-4654-abb5-dfadf7d3f803", + ServerID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f803", +} + +// SecondVolumeAttachment is the first result in ListOutput. +var SecondVolumeAttachment = volumeattach.VolumeAttachment{ + Device: "/dev/vdc", + ID: "a26887c6-c47b-4654-abb5-dfadf7d3f804", + ServerID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f804", +} + +// ExpectedVolumeAttachmentSlide is the slice of results that should be parsed +// from ListOutput, in the expected order. +var ExpectedVolumeAttachmentSlice = []volumeattach.VolumeAttachment{FirstVolumeAttachment, SecondVolumeAttachment} + +//CreatedVolumeAttachment is the parsed result from CreatedOutput. +var CreatedVolumeAttachment = volumeattach.VolumeAttachment{ + Device: "/dev/vdc", + ID: "a26887c6-c47b-4654-abb5-dfadf7d3f804", + ServerID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f804", +} + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleListSuccessfully(t) + + serverID := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" + + count := 0 + err := volumeattach.List(client.ServiceClient(), serverID).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := volumeattach.ExtractVolumeAttachments(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedVolumeAttachmentSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleCreateSuccessfully(t) + + serverID := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" + + actual, err := volumeattach.Create(client.ServiceClient(), serverID, volumeattach.CreateOpts{ + Device: "/dev/vdc", + VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f804", + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &CreatedVolumeAttachment, actual) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleGetSuccessfully(t) + + aID := "a26887c6-c47b-4654-abb5-dfadf7d3f804" + serverID := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" + + actual, err := volumeattach.Get(client.ServiceClient(), serverID, aID).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &SecondVolumeAttachment, actual) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleDeleteSuccessfully(t) + + aID := "a26887c6-c47b-4654-abb5-dfadf7d3f804" + serverID := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" + + err := volumeattach.Delete(client.ServiceClient(), serverID, aID).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/urls.go new file mode 100644 index 0000000000..083f8dc455 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/urls.go @@ -0,0 +1,25 @@ +package volumeattach + +import "github.com/gophercloud/gophercloud" + +const resourcePath = "os-volume_attachments" + +func resourceURL(c *gophercloud.ServiceClient, serverID string) string { + return c.ServiceURL("servers", serverID, resourcePath) +} + +func listURL(c *gophercloud.ServiceClient, serverID string) string { + return resourceURL(c, serverID) +} + +func createURL(c *gophercloud.ServiceClient, serverID string) string { + return resourceURL(c, serverID) +} + +func getURL(c *gophercloud.ServiceClient, serverID, aID string) string { + return c.ServiceURL("servers", serverID, resourcePath, aID) +} + +func deleteURL(c *gophercloud.ServiceClient, serverID, aID string) string { + return getURL(c, serverID, aID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/doc.go new file mode 100644 index 0000000000..5822e1bcf6 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/doc.go @@ -0,0 +1,7 @@ +// Package flavors provides information and interaction with the flavor API +// resource in the OpenStack Compute service. +// +// A flavor is an available hardware configuration for a server. Each flavor +// has a unique combination of disk space, memory capacity and priority for CPU +// time. +package flavors diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/requests.go new file mode 100644 index 0000000000..03d7e8724c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/requests.go @@ -0,0 +1,163 @@ +package flavors + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToFlavorListQuery() (string, error) +} + +// AccessType maps to OpenStack's Flavor.is_public field. Although the is_public field is boolean, the +// request options are ternary, which is why AccessType is a string. The following values are +// allowed: +// +// PublicAccess (the default): Returns public flavors and private flavors associated with that project. +// PrivateAccess (admin only): Returns private flavors, across all projects. +// AllAccess (admin only): Returns public and private flavors across all projects. +// +// The AccessType arguement is optional, and if it is not supplied, OpenStack returns the PublicAccess +// flavors. +type AccessType string + +const ( + PublicAccess AccessType = "true" + PrivateAccess AccessType = "false" + AllAccess AccessType = "None" +) + +// ListOpts helps control the results returned by the List() function. +// For example, a flavor with a minDisk field of 10 will not be returned if you specify MinDisk set to 20. +// Typically, software will use the last ID of the previous call to List to set the Marker for the current call. +type ListOpts struct { + + // ChangesSince, if provided, instructs List to return only those things which have changed since the timestamp provided. + ChangesSince string `q:"changes-since"` + + // MinDisk and MinRAM, if provided, elides flavors which do not meet your criteria. + MinDisk int `q:"minDisk"` + MinRAM int `q:"minRam"` + + // Marker and Limit control paging. + // Marker instructs List where to start listing from. + Marker string `q:"marker"` + + // Limit instructs List to refrain from sending excessively large lists of flavors. + Limit int `q:"limit"` + + // AccessType, if provided, instructs List which set of flavors to return. If IsPublic not provided, + // flavors for the current project are returned. + AccessType AccessType `q:"is_public"` +} + +// ToFlavorListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToFlavorListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// ListDetail instructs OpenStack to provide a list of flavors. +// You may provide criteria by which List curtails its results for easier processing. +// See ListOpts for more details. +func ListDetail(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToFlavorListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return FlavorPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +type CreateOptsBuilder interface { + ToFlavorCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is passed to Create to create a flavor +// Source: +// https://github.com/openstack/nova/blob/stable/newton/nova/api/openstack/compute/schemas/flavor_manage.py#L20 +type CreateOpts struct { + Name string `json:"name" required:"true"` + // memory size, in MBs + RAM int `json:"ram" required:"true"` + VCPUs int `json:"vcpus" required:"true"` + // disk size, in GBs + Disk *int `json:"disk" required:"true"` + ID string `json:"id,omitempty"` + // non-zero, positive + Swap *int `json:"swap,omitempty"` + RxTxFactor float64 `json:"rxtx_factor,omitempty"` + IsPublic *bool `json:"os-flavor-access:is_public,omitempty"` + // ephemeral disk size, in GBs, non-zero, positive + Ephemeral *int `json:"OS-FLV-EXT-DATA:ephemeral,omitempty"` +} + +// ToFlavorCreateMap satisfies the CreateOptsBuilder interface +func (opts *CreateOpts) ToFlavorCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "flavor") +} + +// Create a flavor +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToFlavorCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + return +} + +// Get instructs OpenStack to provide details on a single flavor, identified by its ID. +// Use ExtractFlavor to convert its result into a Flavor. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// IDFromName is a convienience function that returns a flavor's ID given its name. +func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + allPages, err := ListDetail(client, nil).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractFlavors(allPages) + if err != nil { + return "", err + } + + for _, f := range all { + if f.Name == name { + count++ + id = f.ID + } + } + + switch count { + case 0: + err := &gophercloud.ErrResourceNotFound{} + err.ResourceType = "flavor" + err.Name = name + return "", err + case 1: + return id, nil + default: + err := &gophercloud.ErrMultipleResourcesFound{} + err.ResourceType = "flavor" + err.Name = name + err.Count = count + return "", err + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/results.go new file mode 100644 index 0000000000..121abbb8d8 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/results.go @@ -0,0 +1,115 @@ +package flavors + +import ( + "encoding/json" + "strconv" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +type CreateResult struct { + commonResult +} + +// GetResult temporarily holds the response from a Get call. +type GetResult struct { + commonResult +} + +// Extract provides access to the individual Flavor returned by the Get and Create functions. +func (r commonResult) Extract() (*Flavor, error) { + var s struct { + Flavor *Flavor `json:"flavor"` + } + err := r.ExtractInto(&s) + return s.Flavor, err +} + +// Flavor records represent (virtual) hardware configurations for server resources in a region. +type Flavor struct { + // The Id field contains the flavor's unique identifier. + // For example, this identifier will be useful when specifying which hardware configuration to use for a new server instance. + ID string `json:"id"` + // The Disk and RA< fields provide a measure of storage space offered by the flavor, in GB and MB, respectively. + Disk int `json:"disk"` + RAM int `json:"ram"` + // The Name field provides a human-readable moniker for the flavor. + Name string `json:"name"` + RxTxFactor float64 `json:"rxtx_factor"` + // Swap indicates how much space is reserved for swap. + // If not provided, this field will be set to 0. + Swap int `json:"swap"` + // VCPUs indicates how many (virtual) CPUs are available for this flavor. + VCPUs int `json:"vcpus"` + // IsPublic indicates whether the flavor is public. + IsPublic bool `json:"is_public"` +} + +func (r *Flavor) UnmarshalJSON(b []byte) error { + type tmp Flavor + var s struct { + tmp + Swap interface{} `json:"swap"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = Flavor(s.tmp) + + switch t := s.Swap.(type) { + case float64: + r.Swap = int(t) + case string: + switch t { + case "": + r.Swap = 0 + default: + swap, err := strconv.ParseFloat(t, 64) + if err != nil { + return err + } + r.Swap = int(swap) + } + } + + return nil +} + +// FlavorPage contains a single page of the response from a List call. +type FlavorPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines if a page contains any results. +func (page FlavorPage) IsEmpty() (bool, error) { + flavors, err := ExtractFlavors(page) + return len(flavors) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to the next page of results. +func (page FlavorPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"flavors_links"` + } + err := page.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// ExtractFlavors provides access to the list of flavors in a page acquired from the List operation. +func ExtractFlavors(r pagination.Page) ([]Flavor, error) { + var s struct { + Flavors []Flavor `json:"flavors"` + } + err := (r.(FlavorPage)).ExtractInto(&s) + return s.Flavors, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/testing/doc.go new file mode 100644 index 0000000000..0d00761507 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/testing/doc.go @@ -0,0 +1,2 @@ +// compute_flavors_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/testing/requests_test.go new file mode 100644 index 0000000000..470a44454e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/testing/requests_test.go @@ -0,0 +1,199 @@ +package testing + +import ( + "fmt" + "net/http" + "reflect" + "testing" + + "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +const tokenID = "blerb" + +func TestListFlavors(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/flavors/detail", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, ` + { + "flavors": [ + { + "id": "1", + "name": "m1.tiny", + "vcpus": 1, + "disk": 1, + "ram": 512, + "swap":"", + "is_public": true + }, + { + "id": "2", + "name": "m1.small", + "vcpus": 1, + "disk": 20, + "ram": 2048, + "swap": 1000, + "is_public": true + }, + { + "id": "3", + "name": "m1.medium", + "vcpus": 2, + "disk": 40, + "ram": 4096, + "swap": 1000, + "is_public": false + } + ], + "flavors_links": [ + { + "href": "%s/flavors/detail?marker=2", + "rel": "next" + } + ] + } + `, th.Server.URL) + case "2": + fmt.Fprintf(w, `{ "flavors": [] }`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) + + pages := 0 + // Get public and private flavors + err := flavors.ListDetail(fake.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := flavors.ExtractFlavors(page) + if err != nil { + return false, err + } + + expected := []flavors.Flavor{ + {ID: "1", Name: "m1.tiny", VCPUs: 1, Disk: 1, RAM: 512, Swap: 0, IsPublic: true}, + {ID: "2", Name: "m1.small", VCPUs: 1, Disk: 20, RAM: 2048, Swap: 1000, IsPublic: true}, + {ID: "3", Name: "m1.medium", VCPUs: 2, Disk: 40, RAM: 4096, Swap: 1000, IsPublic: false}, + } + + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Expected %#v, but was %#v", expected, actual) + } + + return true, nil + }) + if err != nil { + t.Fatal(err) + } + if pages != 1 { + t.Errorf("Expected one page, got %d", pages) + } +} + +func TestGetFlavor(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/flavors/12345", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "flavor": { + "id": "1", + "name": "m1.tiny", + "disk": 1, + "ram": 512, + "vcpus": 1, + "rxtx_factor": 1, + "swap": "" + } + } + `) + }) + + actual, err := flavors.Get(fake.ServiceClient(), "12345").Extract() + if err != nil { + t.Fatalf("Unable to get flavor: %v", err) + } + + expected := &flavors.Flavor{ + ID: "1", + Name: "m1.tiny", + Disk: 1, + RAM: 512, + VCPUs: 1, + RxTxFactor: 1, + Swap: 0, + } + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Expected %#v, but was %#v", expected, actual) + } +} + +func TestCreateFlavor(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/flavors", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "flavor": { + "id": "1", + "name": "m1.tiny", + "disk": 1, + "ram": 512, + "vcpus": 1, + "rxtx_factor": 1, + "swap": "" + } + } + `) + }) + + disk := 1 + opts := &flavors.CreateOpts{ + ID: "1", + Name: "m1.tiny", + Disk: &disk, + RAM: 512, + VCPUs: 1, + RxTxFactor: 1.0, + } + actual, err := flavors.Create(fake.ServiceClient(), opts).Extract() + if err != nil { + t.Fatalf("Unable to create flavor: %v", err) + } + + expected := &flavors.Flavor{ + ID: "1", + Name: "m1.tiny", + Disk: 1, + RAM: 512, + VCPUs: 1, + RxTxFactor: 1, + Swap: 0, + } + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Expected %#v, but was %#v", expected, actual) + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/urls.go new file mode 100644 index 0000000000..2fc21796f7 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/urls.go @@ -0,0 +1,17 @@ +package flavors + +import ( + "github.com/gophercloud/gophercloud" +) + +func getURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id) +} + +func listURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("flavors", "detail") +} + +func createURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("flavors") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/doc.go new file mode 100644 index 0000000000..0edaa3f025 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/doc.go @@ -0,0 +1,7 @@ +// Package images provides information and interaction with the image API +// resource in the OpenStack Compute service. +// +// An image is a collection of files used to create or rebuild a server. +// Operators provide a number of pre-built OS images by default. You may also +// create custom images from cloud servers you have launched. +package images diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/requests.go new file mode 100644 index 0000000000..df9f1da8f6 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/requests.go @@ -0,0 +1,102 @@ +package images + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToImageListQuery() (string, error) +} + +// ListOpts contain options for limiting the number of Images returned from a call to ListDetail. +type ListOpts struct { + // When the image last changed status (in date-time format). + ChangesSince string `q:"changes-since"` + // The number of Images to return. + Limit int `q:"limit"` + // UUID of the Image at which to set a marker. + Marker string `q:"marker"` + // The name of the Image. + Name string `q:"name"` + // The name of the Server (in URL format). + Server string `q:"server"` + // The current status of the Image. + Status string `q:"status"` + // The value of the type of image (e.g. BASE, SERVER, ALL) + Type string `q:"type"` +} + +// ToImageListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToImageListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// ListDetail enumerates the available images. +func ListDetail(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listDetailURL(client) + if opts != nil { + query, err := opts.ToImageListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ImagePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get acquires additional detail about a specific image by ID. +// Use ExtractImage() to interpret the result as an openstack Image. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// Delete deletes the specified image ID. +func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} + +// IDFromName is a convienience function that returns an image's ID given its name. +func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + allPages, err := ListDetail(client, nil).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractImages(allPages) + if err != nil { + return "", err + } + + for _, f := range all { + if f.Name == name { + count++ + id = f.ID + } + } + + switch count { + case 0: + err := &gophercloud.ErrResourceNotFound{} + err.ResourceType = "image" + err.Name = name + return "", err + case 1: + return id, nil + default: + err := &gophercloud.ErrMultipleResourcesFound{} + err.ResourceType = "image" + err.Name = name + err.Count = count + return "", err + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/results.go new file mode 100644 index 0000000000..f9ebc69e98 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/results.go @@ -0,0 +1,83 @@ +package images + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// GetResult temporarily stores a Get response. +type GetResult struct { + gophercloud.Result +} + +// DeleteResult represents the result of an image.Delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// Extract interprets a GetResult as an Image. +func (r GetResult) Extract() (*Image, error) { + var s struct { + Image *Image `json:"image"` + } + err := r.ExtractInto(&s) + return s.Image, err +} + +// Image is used for JSON (un)marshalling. +// It provides a description of an OS image. +type Image struct { + // ID contains the image's unique identifier. + ID string + + Created string + + // MinDisk and MinRAM specify the minimum resources a server must provide to be able to install the image. + MinDisk int + MinRAM int + + // Name provides a human-readable moniker for the OS image. + Name string + + // The Progress and Status fields indicate image-creation status. + // Any usable image will have 100% progress. + Progress int + Status string + + Updated string + + Metadata map[string]interface{} +} + +// ImagePage contains a single page of results from a List operation. +// Use ExtractImages to convert it into a slice of usable structs. +type ImagePage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if a page contains no Image results. +func (page ImagePage) IsEmpty() (bool, error) { + images, err := ExtractImages(page) + return len(images) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to the next page of results. +func (page ImagePage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"images_links"` + } + err := page.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// ExtractImages converts a page of List results into a slice of usable Image structs. +func ExtractImages(r pagination.Page) ([]Image, error) { + var s struct { + Images []Image `json:"images"` + } + err := (r.(ImagePage)).ExtractInto(&s) + return s.Images, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/testing/doc.go new file mode 100644 index 0000000000..6f59ade68a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/testing/doc.go @@ -0,0 +1,2 @@ +// compute_images_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/testing/requests_test.go new file mode 100644 index 0000000000..1de030352e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/testing/requests_test.go @@ -0,0 +1,225 @@ +package testing + +import ( + "encoding/json" + "fmt" + "net/http" + "reflect" + "testing" + + "github.com/gophercloud/gophercloud/openstack/compute/v2/images" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestListImages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/images/detail", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, ` + { + "images": [ + { + "status": "ACTIVE", + "updated": "2014-09-23T12:54:56Z", + "id": "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7", + "OS-EXT-IMG-SIZE:size": 476704768, + "name": "F17-x86_64-cfntools", + "created": "2014-09-23T12:54:52Z", + "minDisk": 0, + "progress": 100, + "minRam": 0, + "metadata": { + "architecture": "x86_64", + "block_device_mapping": { + "guest_format": null, + "boot_index": 0, + "device_name": "/dev/vda", + "delete_on_termination": false + } + } + }, + { + "status": "ACTIVE", + "updated": "2014-09-23T12:51:43Z", + "id": "f90f6034-2570-4974-8351-6b49732ef2eb", + "OS-EXT-IMG-SIZE:size": 13167616, + "name": "cirros-0.3.2-x86_64-disk", + "created": "2014-09-23T12:51:42Z", + "minDisk": 0, + "progress": 100, + "minRam": 0 + } + ] + } + `) + case "2": + fmt.Fprintf(w, `{ "images": [] }`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) + + pages := 0 + options := &images.ListOpts{Limit: 2} + err := images.ListDetail(fake.ServiceClient(), options).EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := images.ExtractImages(page) + if err != nil { + return false, err + } + + expected := []images.Image{ + { + ID: "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7", + Name: "F17-x86_64-cfntools", + Created: "2014-09-23T12:54:52Z", + Updated: "2014-09-23T12:54:56Z", + MinDisk: 0, + MinRAM: 0, + Progress: 100, + Status: "ACTIVE", + Metadata: map[string]interface{}{ + "architecture": "x86_64", + "block_device_mapping": map[string]interface{}{ + "guest_format": interface{}(nil), + "boot_index": float64(0), + "device_name": "/dev/vda", + "delete_on_termination": false, + }, + }, + }, + { + ID: "f90f6034-2570-4974-8351-6b49732ef2eb", + Name: "cirros-0.3.2-x86_64-disk", + Created: "2014-09-23T12:51:42Z", + Updated: "2014-09-23T12:51:43Z", + MinDisk: 0, + MinRAM: 0, + Progress: 100, + Status: "ACTIVE", + }, + } + + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Unexpected page contents: expected %#v, got %#v", expected, actual) + } + + return false, nil + }) + + if err != nil { + t.Fatalf("EachPage error: %v", err) + } + if pages != 1 { + t.Errorf("Expected one page, got %d", pages) + } +} + +func TestGetImage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/images/12345678", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "image": { + "status": "ACTIVE", + "updated": "2014-09-23T12:54:56Z", + "id": "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7", + "OS-EXT-IMG-SIZE:size": 476704768, + "name": "F17-x86_64-cfntools", + "created": "2014-09-23T12:54:52Z", + "minDisk": 0, + "progress": 100, + "minRam": 0, + "metadata": { + "architecture": "x86_64", + "block_device_mapping": { + "guest_format": null, + "boot_index": 0, + "device_name": "/dev/vda", + "delete_on_termination": false + } + } + } + } + `) + }) + + actual, err := images.Get(fake.ServiceClient(), "12345678").Extract() + if err != nil { + t.Fatalf("Unexpected error from Get: %v", err) + } + + expected := &images.Image{ + Status: "ACTIVE", + Updated: "2014-09-23T12:54:56Z", + ID: "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7", + Name: "F17-x86_64-cfntools", + Created: "2014-09-23T12:54:52Z", + MinDisk: 0, + Progress: 100, + MinRAM: 0, + Metadata: map[string]interface{}{ + "architecture": "x86_64", + "block_device_mapping": map[string]interface{}{ + "guest_format": interface{}(nil), + "boot_index": float64(0), + "device_name": "/dev/vda", + "delete_on_termination": false, + }, + }, + } + + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Expected %#v, but got %#v", expected, actual) + } +} + +func TestNextPageURL(t *testing.T) { + var page images.ImagePage + var body map[string]interface{} + bodyString := []byte(`{"images":{"links":[{"href":"http://192.154.23.87/12345/images/image3","rel":"bookmark"}]}, "images_links":[{"href":"http://192.154.23.87/12345/images/image4","rel":"next"}]}`) + err := json.Unmarshal(bodyString, &body) + if err != nil { + t.Fatalf("Error unmarshaling data into page body: %v", err) + } + page.Body = body + + expected := "http://192.154.23.87/12345/images/image4" + actual, err := page.NextPageURL() + th.AssertNoErr(t, err) + th.CheckEquals(t, expected, actual) +} + +// Test Image delete +func TestDeleteImage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/images/12345678", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) + + res := images.Delete(fake.ServiceClient(), "12345678") + th.AssertNoErr(t, res.Err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/urls.go new file mode 100644 index 0000000000..57787fb725 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/urls.go @@ -0,0 +1,15 @@ +package images + +import "github.com/gophercloud/gophercloud" + +func listDetailURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("images", "detail") +} + +func getURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("images", id) +} + +func deleteURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("images", id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/doc.go new file mode 100644 index 0000000000..fe4567120c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/doc.go @@ -0,0 +1,6 @@ +// Package servers provides information and interaction with the server API +// resource in the OpenStack Compute service. +// +// A server is a virtual machine instance in the compute system. In order for +// one to be provisioned, a valid flavor and image are required. +package servers diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/errors.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/errors.go new file mode 100644 index 0000000000..c9f0e3c20b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/errors.go @@ -0,0 +1,71 @@ +package servers + +import ( + "fmt" + + "github.com/gophercloud/gophercloud" +) + +// ErrNeitherImageIDNorImageNameProvided is the error when neither the image +// ID nor the image name is provided for a server operation +type ErrNeitherImageIDNorImageNameProvided struct{ gophercloud.ErrMissingInput } + +func (e ErrNeitherImageIDNorImageNameProvided) Error() string { + return "One and only one of the image ID and the image name must be provided." +} + +// ErrNeitherFlavorIDNorFlavorNameProvided is the error when neither the flavor +// ID nor the flavor name is provided for a server operation +type ErrNeitherFlavorIDNorFlavorNameProvided struct{ gophercloud.ErrMissingInput } + +func (e ErrNeitherFlavorIDNorFlavorNameProvided) Error() string { + return "One and only one of the flavor ID and the flavor name must be provided." +} + +type ErrNoClientProvidedForIDByName struct{ gophercloud.ErrMissingInput } + +func (e ErrNoClientProvidedForIDByName) Error() string { + return "A service client must be provided to find a resource ID by name." +} + +// ErrInvalidHowParameterProvided is the error when an unknown value is given +// for the `how` argument +type ErrInvalidHowParameterProvided struct{ gophercloud.ErrInvalidInput } + +// ErrNoAdminPassProvided is the error when an administrative password isn't +// provided for a server operation +type ErrNoAdminPassProvided struct{ gophercloud.ErrMissingInput } + +// ErrNoImageIDProvided is the error when an image ID isn't provided for a server +// operation +type ErrNoImageIDProvided struct{ gophercloud.ErrMissingInput } + +// ErrNoIDProvided is the error when a server ID isn't provided for a server +// operation +type ErrNoIDProvided struct{ gophercloud.ErrMissingInput } + +// ErrServer is a generic error type for servers HTTP operations. +type ErrServer struct { + gophercloud.ErrUnexpectedResponseCode + ID string +} + +func (se ErrServer) Error() string { + return fmt.Sprintf("Error while executing HTTP request for server [%s]", se.ID) +} + +// Error404 overrides the generic 404 error message. +func (se ErrServer) Error404(e gophercloud.ErrUnexpectedResponseCode) error { + se.ErrUnexpectedResponseCode = e + return &ErrServerNotFound{se} +} + +// ErrServerNotFound is the error when a 404 is received during server HTTP +// operations. +type ErrServerNotFound struct { + ErrServer +} + +func (e ErrServerNotFound) Error() string { + return fmt.Sprintf("I couldn't find server [%s]", e.ID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/requests.go new file mode 100644 index 0000000000..1a7b3ec77e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/requests.go @@ -0,0 +1,744 @@ +package servers + +import ( + "encoding/base64" + "encoding/json" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors" + "github.com/gophercloud/gophercloud/openstack/compute/v2/images" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToServerListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the server attributes you want to see returned. Marker and Limit are used +// for pagination. +type ListOpts struct { + // A time/date stamp for when the server last changed status. + ChangesSince string `q:"changes-since"` + + // Name of the image in URL format. + Image string `q:"image"` + + // Name of the flavor in URL format. + Flavor string `q:"flavor"` + + // Name of the server as a string; can be queried with regular expressions. + // Realize that ?name=bob returns both bob and bobb. If you need to match bob + // only, you can use a regular expression matching the syntax of the + // underlying database server implemented for Compute. + Name string `q:"name"` + + // Value of the status of the server so that you can filter on "ACTIVE" for example. + Status string `q:"status"` + + // Name of the host as a string. + Host string `q:"host"` + + // UUID of the server at which you want to set a marker. + Marker string `q:"marker"` + + // Integer value for the limit of values to return. + Limit int `q:"limit"` + + // Bool to show all tenants + AllTenants bool `q:"all_tenants"` + + // List servers for a particular tenant. Setting "AllTenants = true" is required. + TenantID string `q:"tenant_id"` +} + +// ToServerListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToServerListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List makes a request against the API to list servers accessible to you. +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listDetailURL(client) + if opts != nil { + query, err := opts.ToServerListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ServerPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder describes struct types that can be accepted by the Create call. +// The CreateOpts struct in this package does. +type CreateOptsBuilder interface { + ToServerCreateMap() (map[string]interface{}, error) +} + +// Network is used within CreateOpts to control a new server's network attachments. +type Network struct { + // UUID of a nova-network to attach to the newly provisioned server. + // Required unless Port is provided. + UUID string + + // Port of a neutron network to attach to the newly provisioned server. + // Required unless UUID is provided. + Port string + + // FixedIP [optional] specifies a fixed IPv4 address to be used on this network. + FixedIP string +} + +// Personality is an array of files that are injected into the server at launch. +type Personality []*File + +// File is used within CreateOpts and RebuildOpts to inject a file into the server at launch. +// File implements the json.Marshaler interface, so when a Create or Rebuild operation is requested, +// json.Marshal will call File's MarshalJSON method. +type File struct { + // Path of the file + Path string + // Contents of the file. Maximum content size is 255 bytes. + Contents []byte +} + +// MarshalJSON marshals the escaped file, base64 encoding the contents. +func (f *File) MarshalJSON() ([]byte, error) { + file := struct { + Path string `json:"path"` + Contents string `json:"contents"` + }{ + Path: f.Path, + Contents: base64.StdEncoding.EncodeToString(f.Contents), + } + return json.Marshal(file) +} + +// CreateOpts specifies server creation parameters. +type CreateOpts struct { + // Name is the name to assign to the newly launched server. + Name string `json:"name" required:"true"` + + // ImageRef [optional; required if ImageName is not provided] is the ID or full + // URL to the image that contains the server's OS and initial state. + // Also optional if using the boot-from-volume extension. + ImageRef string `json:"imageRef"` + + // ImageName [optional; required if ImageRef is not provided] is the name of the + // image that contains the server's OS and initial state. + // Also optional if using the boot-from-volume extension. + ImageName string `json:"-"` + + // FlavorRef [optional; required if FlavorName is not provided] is the ID or + // full URL to the flavor that describes the server's specs. + FlavorRef string `json:"flavorRef"` + + // FlavorName [optional; required if FlavorRef is not provided] is the name of + // the flavor that describes the server's specs. + FlavorName string `json:"-"` + + // SecurityGroups lists the names of the security groups to which this server should belong. + SecurityGroups []string `json:"-"` + + // UserData contains configuration information or scripts to use upon launch. + // Create will base64-encode it for you, if it isn't already. + UserData []byte `json:"-"` + + // AvailabilityZone in which to launch the server. + AvailabilityZone string `json:"availability_zone,omitempty"` + + // Networks dictates how this server will be attached to available networks. + // By default, the server will be attached to all isolated networks for the tenant. + Networks []Network `json:"-"` + + // Metadata contains key-value pairs (up to 255 bytes each) to attach to the server. + Metadata map[string]string `json:"metadata,omitempty"` + + // Personality includes files to inject into the server at launch. + // Create will base64-encode file contents for you. + Personality Personality `json:"personality,omitempty"` + + // ConfigDrive enables metadata injection through a configuration drive. + ConfigDrive *bool `json:"config_drive,omitempty"` + + // AdminPass sets the root user password. If not set, a randomly-generated + // password will be created and returned in the rponse. + AdminPass string `json:"adminPass,omitempty"` + + // AccessIPv4 specifies an IPv4 address for the instance. + AccessIPv4 string `json:"accessIPv4,omitempty"` + + // AccessIPv6 pecifies an IPv6 address for the instance. + AccessIPv6 string `json:"accessIPv6,omitempty"` + + // ServiceClient will allow calls to be made to retrieve an image or + // flavor ID by name. + ServiceClient *gophercloud.ServiceClient `json:"-"` +} + +// ToServerCreateMap assembles a request body based on the contents of a CreateOpts. +func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) { + sc := opts.ServiceClient + opts.ServiceClient = nil + b, err := gophercloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + if opts.UserData != nil { + var userData string + if _, err := base64.StdEncoding.DecodeString(string(opts.UserData)); err != nil { + userData = base64.StdEncoding.EncodeToString(opts.UserData) + } else { + userData = string(opts.UserData) + } + b["user_data"] = &userData + } + + if len(opts.SecurityGroups) > 0 { + securityGroups := make([]map[string]interface{}, len(opts.SecurityGroups)) + for i, groupName := range opts.SecurityGroups { + securityGroups[i] = map[string]interface{}{"name": groupName} + } + b["security_groups"] = securityGroups + } + + if len(opts.Networks) > 0 { + networks := make([]map[string]interface{}, len(opts.Networks)) + for i, net := range opts.Networks { + networks[i] = make(map[string]interface{}) + if net.UUID != "" { + networks[i]["uuid"] = net.UUID + } + if net.Port != "" { + networks[i]["port"] = net.Port + } + if net.FixedIP != "" { + networks[i]["fixed_ip"] = net.FixedIP + } + } + b["networks"] = networks + } + + // If ImageRef isn't provided, check if ImageName was provided to ascertain + // the image ID. + if opts.ImageRef == "" { + if opts.ImageName != "" { + if sc == nil { + err := ErrNoClientProvidedForIDByName{} + err.Argument = "ServiceClient" + return nil, err + } + imageID, err := images.IDFromName(sc, opts.ImageName) + if err != nil { + return nil, err + } + b["imageRef"] = imageID + } + } + + // If FlavorRef isn't provided, use FlavorName to ascertain the flavor ID. + if opts.FlavorRef == "" { + if opts.FlavorName == "" { + err := ErrNeitherFlavorIDNorFlavorNameProvided{} + err.Argument = "FlavorRef/FlavorName" + return nil, err + } + if sc == nil { + err := ErrNoClientProvidedForIDByName{} + err.Argument = "ServiceClient" + return nil, err + } + flavorID, err := flavors.IDFromName(sc, opts.FlavorName) + if err != nil { + return nil, err + } + b["flavorRef"] = flavorID + } + + return map[string]interface{}{"server": b}, nil +} + +// Create requests a server to be provisioned to the user in the current tenant. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + reqBody, err := opts.ToServerCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(listURL(client), reqBody, &r.Body, nil) + return +} + +// Delete requests that a server previously provisioned be removed from your account. +func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} + +// ForceDelete forces the deletion of a server +func ForceDelete(client *gophercloud.ServiceClient, id string) (r ActionResult) { + _, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"forceDelete": ""}, nil, nil) + return +} + +// Get requests details on a single server, by ID. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 203}, + }) + return +} + +// UpdateOptsBuilder allows extensions to add additional attributes to the Update request. +type UpdateOptsBuilder interface { + ToServerUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts specifies the base attributes that may be updated on an existing server. +type UpdateOpts struct { + // Name changes the displayed name of the server. + // The server host name will *not* change. + // Server names are not constrained to be unique, even within the same tenant. + Name string `json:"name,omitempty"` + + // AccessIPv4 provides a new IPv4 address for the instance. + AccessIPv4 string `json:"accessIPv4,omitempty"` + + // AccessIPv6 provides a new IPv6 address for the instance. + AccessIPv6 string `json:"accessIPv6,omitempty"` +} + +// ToServerUpdateMap formats an UpdateOpts structure into a request body. +func (opts UpdateOpts) ToServerUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "server") +} + +// Update requests that various attributes of the indicated server be changed. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToServerUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// ChangeAdminPassword alters the administrator or root password for a specified server. +func ChangeAdminPassword(client *gophercloud.ServiceClient, id, newPassword string) (r ActionResult) { + b := map[string]interface{}{ + "changePassword": map[string]string{ + "adminPass": newPassword, + }, + } + _, r.Err = client.Post(actionURL(client, id), b, nil, nil) + return +} + +// RebootMethod describes the mechanisms by which a server reboot can be requested. +type RebootMethod string + +// These constants determine how a server should be rebooted. +// See the Reboot() function for further details. +const ( + SoftReboot RebootMethod = "SOFT" + HardReboot RebootMethod = "HARD" + OSReboot = SoftReboot + PowerCycle = HardReboot +) + +// RebootOptsBuilder is an interface that options must satisfy in order to be +// used when rebooting a server instance +type RebootOptsBuilder interface { + ToServerRebootMap() (map[string]interface{}, error) +} + +// RebootOpts satisfies the RebootOptsBuilder interface +type RebootOpts struct { + Type RebootMethod `json:"type" required:"true"` +} + +// ToServerRebootMap allows RebootOpts to satisfiy the RebootOptsBuilder +// interface +func (opts *RebootOpts) ToServerRebootMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "reboot") +} + +// Reboot requests that a given server reboot. +// Two methods exist for rebooting a server: +// +// HardReboot (aka PowerCycle) starts the server instance by physically cutting power to the machine, or if a VM, +// terminating it at the hypervisor level. +// It's done. Caput. Full stop. +// Then, after a brief while, power is rtored or the VM instance rtarted. +// +// SoftReboot (aka OSReboot) simply tells the OS to rtart under its own procedur. +// E.g., in Linux, asking it to enter runlevel 6, or executing "sudo shutdown -r now", or by asking Windows to rtart the machine. +func Reboot(client *gophercloud.ServiceClient, id string, opts RebootOptsBuilder) (r ActionResult) { + b, err := opts.ToServerRebootMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(actionURL(client, id), b, nil, nil) + return +} + +// RebuildOptsBuilder is an interface that allows extensions to override the +// default behaviour of rebuild options +type RebuildOptsBuilder interface { + ToServerRebuildMap() (map[string]interface{}, error) +} + +// RebuildOpts represents the configuration options used in a server rebuild +// operation +type RebuildOpts struct { + // The server's admin password + AdminPass string `json:"adminPass,omitempty"` + // The ID of the image you want your server to be provisioned on + ImageID string `json:"imageRef"` + ImageName string `json:"-"` + // Name to set the server to + Name string `json:"name,omitempty"` + // AccessIPv4 [optional] provides a new IPv4 address for the instance. + AccessIPv4 string `json:"accessIPv4,omitempty"` + // AccessIPv6 [optional] provides a new IPv6 address for the instance. + AccessIPv6 string `json:"accessIPv6,omitempty"` + // Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the server. + Metadata map[string]string `json:"metadata,omitempty"` + // Personality [optional] includes files to inject into the server at launch. + // Rebuild will base64-encode file contents for you. + Personality Personality `json:"personality,omitempty"` + ServiceClient *gophercloud.ServiceClient `json:"-"` +} + +// ToServerRebuildMap formats a RebuildOpts struct into a map for use in JSON +func (opts RebuildOpts) ToServerRebuildMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + // If ImageRef isn't provided, check if ImageName was provided to ascertain + // the image ID. + if opts.ImageID == "" { + if opts.ImageName != "" { + if opts.ServiceClient == nil { + err := ErrNoClientProvidedForIDByName{} + err.Argument = "ServiceClient" + return nil, err + } + imageID, err := images.IDFromName(opts.ServiceClient, opts.ImageName) + if err != nil { + return nil, err + } + b["imageRef"] = imageID + } + } + + return map[string]interface{}{"rebuild": b}, nil +} + +// Rebuild will reprovision the server according to the configuration options +// provided in the RebuildOpts struct. +func Rebuild(client *gophercloud.ServiceClient, id string, opts RebuildOptsBuilder) (r RebuildResult) { + b, err := opts.ToServerRebuildMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(actionURL(client, id), b, &r.Body, nil) + return +} + +// ResizeOptsBuilder is an interface that allows extensions to override the default structure of +// a Resize request. +type ResizeOptsBuilder interface { + ToServerResizeMap() (map[string]interface{}, error) +} + +// ResizeOpts represents the configuration options used to control a Resize operation. +type ResizeOpts struct { + // FlavorRef is the ID of the flavor you wish your server to become. + FlavorRef string `json:"flavorRef" required:"true"` +} + +// ToServerResizeMap formats a ResizeOpts as a map that can be used as a JSON request body for the +// Resize request. +func (opts ResizeOpts) ToServerResizeMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "resize") +} + +// Resize instructs the provider to change the flavor of the server. +// Note that this implies rebuilding it. +// Unfortunately, one cannot pass rebuild parameters to the resize function. +// When the resize completes, the server will be in RESIZE_VERIFY state. +// While in this state, you can explore the use of the new server's configuration. +// If you like it, call ConfirmResize() to commit the resize permanently. +// Otherwise, call RevertResize() to restore the old configuration. +func Resize(client *gophercloud.ServiceClient, id string, opts ResizeOptsBuilder) (r ActionResult) { + b, err := opts.ToServerResizeMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(actionURL(client, id), b, nil, nil) + return +} + +// ConfirmResize confirms a previous resize operation on a server. +// See Resize() for more details. +func ConfirmResize(client *gophercloud.ServiceClient, id string) (r ActionResult) { + _, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"confirmResize": nil}, nil, &gophercloud.RequestOpts{ + OkCodes: []int{201, 202, 204}, + }) + return +} + +// RevertResize cancels a previous resize operation on a server. +// See Resize() for more details. +func RevertResize(client *gophercloud.ServiceClient, id string) (r ActionResult) { + _, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"revertResize": nil}, nil, nil) + return +} + +// RescueOptsBuilder is an interface that allows extensions to override the +// default structure of a Rescue request. +type RescueOptsBuilder interface { + ToServerRescueMap() (map[string]interface{}, error) +} + +// RescueOpts represents the configuration options used to control a Rescue +// option. +type RescueOpts struct { + // AdminPass is the desired administrative password for the instance in + // RESCUE mode. If it's left blank, the server will generate a password. + AdminPass string `json:"adminPass,omitempty"` +} + +// ToServerRescueMap formats a RescueOpts as a map that can be used as a JSON +// request body for the Rescue request. +func (opts RescueOpts) ToServerRescueMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "rescue") +} + +// Rescue instructs the provider to place the server into RESCUE mode. +func Rescue(client *gophercloud.ServiceClient, id string, opts RescueOptsBuilder) (r RescueResult) { + b, err := opts.ToServerRescueMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(actionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// ResetMetadataOptsBuilder allows extensions to add additional parameters to the +// Reset request. +type ResetMetadataOptsBuilder interface { + ToMetadataResetMap() (map[string]interface{}, error) +} + +// MetadataOpts is a map that contains key-value pairs. +type MetadataOpts map[string]string + +// ToMetadataResetMap assembles a body for a Reset request based on the contents of a MetadataOpts. +func (opts MetadataOpts) ToMetadataResetMap() (map[string]interface{}, error) { + return map[string]interface{}{"metadata": opts}, nil +} + +// ToMetadataUpdateMap assembles a body for an Update request based on the contents of a MetadataOpts. +func (opts MetadataOpts) ToMetadataUpdateMap() (map[string]interface{}, error) { + return map[string]interface{}{"metadata": opts}, nil +} + +// ResetMetadata will create multiple new key-value pairs for the given server ID. +// Note: Using this operation will erase any already-existing metadata and create +// the new metadata provided. To keep any already-existing metadata, use the +// UpdateMetadatas or UpdateMetadata function. +func ResetMetadata(client *gophercloud.ServiceClient, id string, opts ResetMetadataOptsBuilder) (r ResetMetadataResult) { + b, err := opts.ToMetadataResetMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(metadataURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Metadata requests all the metadata for the given server ID. +func Metadata(client *gophercloud.ServiceClient, id string) (r GetMetadataResult) { + _, r.Err = client.Get(metadataURL(client, id), &r.Body, nil) + return +} + +// UpdateMetadataOptsBuilder allows extensions to add additional parameters to the +// Create request. +type UpdateMetadataOptsBuilder interface { + ToMetadataUpdateMap() (map[string]interface{}, error) +} + +// UpdateMetadata updates (or creates) all the metadata specified by opts for the given server ID. +// This operation does not affect already-existing metadata that is not specified +// by opts. +func UpdateMetadata(client *gophercloud.ServiceClient, id string, opts UpdateMetadataOptsBuilder) (r UpdateMetadataResult) { + b, err := opts.ToMetadataUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(metadataURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// MetadatumOptsBuilder allows extensions to add additional parameters to the +// Create request. +type MetadatumOptsBuilder interface { + ToMetadatumCreateMap() (map[string]interface{}, string, error) +} + +// MetadatumOpts is a map of length one that contains a key-value pair. +type MetadatumOpts map[string]string + +// ToMetadatumCreateMap assembles a body for a Create request based on the contents of a MetadataumOpts. +func (opts MetadatumOpts) ToMetadatumCreateMap() (map[string]interface{}, string, error) { + if len(opts) != 1 { + err := gophercloud.ErrInvalidInput{} + err.Argument = "servers.MetadatumOpts" + err.Info = "Must have 1 and only 1 key-value pair" + return nil, "", err + } + metadatum := map[string]interface{}{"meta": opts} + var key string + for k := range metadatum["meta"].(MetadatumOpts) { + key = k + } + return metadatum, key, nil +} + +// CreateMetadatum will create or update the key-value pair with the given key for the given server ID. +func CreateMetadatum(client *gophercloud.ServiceClient, id string, opts MetadatumOptsBuilder) (r CreateMetadatumResult) { + b, key, err := opts.ToMetadatumCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(metadatumURL(client, id, key), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Metadatum requests the key-value pair with the given key for the given server ID. +func Metadatum(client *gophercloud.ServiceClient, id, key string) (r GetMetadatumResult) { + _, r.Err = client.Get(metadatumURL(client, id, key), &r.Body, nil) + return +} + +// DeleteMetadatum will delete the key-value pair with the given key for the given server ID. +func DeleteMetadatum(client *gophercloud.ServiceClient, id, key string) (r DeleteMetadatumResult) { + _, r.Err = client.Delete(metadatumURL(client, id, key), nil) + return +} + +// ListAddresses makes a request against the API to list the servers IP addresses. +func ListAddresses(client *gophercloud.ServiceClient, id string) pagination.Pager { + return pagination.NewPager(client, listAddressesURL(client, id), func(r pagination.PageResult) pagination.Page { + return AddressPage{pagination.SinglePageBase(r)} + }) +} + +// ListAddressesByNetwork makes a request against the API to list the servers IP addresses +// for the given network. +func ListAddressesByNetwork(client *gophercloud.ServiceClient, id, network string) pagination.Pager { + return pagination.NewPager(client, listAddressesByNetworkURL(client, id, network), func(r pagination.PageResult) pagination.Page { + return NetworkAddressPage{pagination.SinglePageBase(r)} + }) +} + +// CreateImageOptsBuilder is the interface types must satisfy in order to be +// used as CreateImage options +type CreateImageOptsBuilder interface { + ToServerCreateImageMap() (map[string]interface{}, error) +} + +// CreateImageOpts satisfies the CreateImageOptsBuilder +type CreateImageOpts struct { + // Name of the image/snapshot + Name string `json:"name" required:"true"` + // Metadata contains key-value pairs (up to 255 bytes each) to attach to the created image. + Metadata map[string]string `json:"metadata,omitempty"` +} + +// ToServerCreateImageMap formats a CreateImageOpts structure into a request body. +func (opts CreateImageOpts) ToServerCreateImageMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "createImage") +} + +// CreateImage makes a request against the nova API to schedule an image to be created of the server +func CreateImage(client *gophercloud.ServiceClient, id string, opts CreateImageOptsBuilder) (r CreateImageResult) { + b, err := opts.ToServerCreateImageMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + r.Err = err + r.Header = resp.Header + return +} + +// IDFromName is a convienience function that returns a server's ID given its name. +func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + allPages, err := List(client, nil).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractServers(allPages) + if err != nil { + return "", err + } + + for _, f := range all { + if f.Name == name { + count++ + id = f.ID + } + } + + switch count { + case 0: + return "", gophercloud.ErrResourceNotFound{Name: name, ResourceType: "server"} + case 1: + return id, nil + default: + return "", gophercloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "server"} + } +} + +// GetPassword makes a request against the nova API to get the encrypted administrative password. +func GetPassword(client *gophercloud.ServiceClient, serverId string) (r GetPasswordResult) { + _, r.Err = client.Get(passwordURL(client, serverId), &r.Body, nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/results.go new file mode 100644 index 0000000000..1ae1e91c78 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/results.go @@ -0,0 +1,350 @@ +package servers + +import ( + "crypto/rsa" + "encoding/base64" + "encoding/json" + "fmt" + "net/url" + "path" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +type serverResult struct { + gophercloud.Result +} + +// Extract interprets any serverResult as a Server, if possible. +func (r serverResult) Extract() (*Server, error) { + var s Server + err := r.ExtractInto(&s) + return &s, err +} + +func (r serverResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "server") +} + +func ExtractServersInto(r pagination.Page, v interface{}) error { + return r.(ServerPage).Result.ExtractIntoSlicePtr(v, "servers") +} + +// CreateResult temporarily contains the response from a Create call. +type CreateResult struct { + serverResult +} + +// GetResult temporarily contains the response from a Get call. +type GetResult struct { + serverResult +} + +// UpdateResult temporarily contains the response from an Update call. +type UpdateResult struct { + serverResult +} + +// DeleteResult temporarily contains the response from a Delete call. +type DeleteResult struct { + gophercloud.ErrResult +} + +// RebuildResult temporarily contains the response from a Rebuild call. +type RebuildResult struct { + serverResult +} + +// ActionResult represents the result of server action operations, like reboot +type ActionResult struct { + gophercloud.ErrResult +} + +// RescueResult represents the result of a server rescue operation +type RescueResult struct { + ActionResult +} + +// CreateImageResult represents the result of an image creation operation +type CreateImageResult struct { + gophercloud.Result +} + +// GetPasswordResult represent the result of a get os-server-password operation. +type GetPasswordResult struct { + gophercloud.Result +} + +// ExtractPassword gets the encrypted password. +// If privateKey != nil the password is decrypted with the private key. +// If privateKey == nil the encrypted password is returned and can be decrypted with: +// echo '' | base64 -D | openssl rsautl -decrypt -inkey +func (r GetPasswordResult) ExtractPassword(privateKey *rsa.PrivateKey) (string, error) { + var s struct { + Password string `json:"password"` + } + err := r.ExtractInto(&s) + if err == nil && privateKey != nil && s.Password != "" { + return decryptPassword(s.Password, privateKey) + } + return s.Password, err +} + +func decryptPassword(encryptedPassword string, privateKey *rsa.PrivateKey) (string, error) { + b64EncryptedPassword := make([]byte, base64.StdEncoding.DecodedLen(len(encryptedPassword))) + + n, err := base64.StdEncoding.Decode(b64EncryptedPassword, []byte(encryptedPassword)) + if err != nil { + return "", fmt.Errorf("Failed to base64 decode encrypted password: %s", err) + } + password, err := rsa.DecryptPKCS1v15(nil, privateKey, b64EncryptedPassword[0:n]) + if err != nil { + return "", fmt.Errorf("Failed to decrypt password: %s", err) + } + + return string(password), nil +} + +// ExtractImageID gets the ID of the newly created server image from the header +func (r CreateImageResult) ExtractImageID() (string, error) { + if r.Err != nil { + return "", r.Err + } + // Get the image id from the header + u, err := url.ParseRequestURI(r.Header.Get("Location")) + if err != nil { + return "", err + } + imageID := path.Base(u.Path) + if imageID == "." || imageID == "/" { + return "", fmt.Errorf("Failed to parse the ID of newly created image: %s", u) + } + return imageID, nil +} + +// Extract interprets any RescueResult as an AdminPass, if possible. +func (r RescueResult) Extract() (string, error) { + var s struct { + AdminPass string `json:"adminPass"` + } + err := r.ExtractInto(&s) + return s.AdminPass, err +} + +// Server exposes only the standard OpenStack fields corresponding to a given server on the user's account. +type Server struct { + // ID uniquely identifies this server amongst all other servers, including those not accessible to the current tenant. + ID string `json:"id"` + // TenantID identifies the tenant owning this server resource. + TenantID string `json:"tenant_id"` + // UserID uniquely identifies the user account owning the tenant. + UserID string `json:"user_id"` + // Name contains the human-readable name for the server. + Name string `json:"name"` + // Updated and Created contain ISO-8601 timestamps of when the state of the server last changed, and when it was created. + Updated time.Time `json:"updated"` + Created time.Time `json:"created"` + HostID string `json:"hostid"` + // Status contains the current operational status of the server, such as IN_PROGRESS or ACTIVE. + Status string `json:"status"` + // Progress ranges from 0..100. + // A request made against the server completes only once Progress reaches 100. + Progress int `json:"progress"` + // AccessIPv4 and AccessIPv6 contain the IP addresses of the server, suitable for remote access for administration. + AccessIPv4 string `json:"accessIPv4"` + AccessIPv6 string `json:"accessIPv6"` + // Image refers to a JSON object, which itself indicates the OS image used to deploy the server. + Image map[string]interface{} `json:"-"` + // Flavor refers to a JSON object, which itself indicates the hardware configuration of the deployed server. + Flavor map[string]interface{} `json:"flavor"` + // Addresses includes a list of all IP addresses assigned to the server, keyed by pool. + Addresses map[string]interface{} `json:"addresses"` + // Metadata includes a list of all user-specified key-value pairs attached to the server. + Metadata map[string]string `json:"metadata"` + // Links includes HTTP references to the itself, useful for passing along to other APIs that might want a server reference. + Links []interface{} `json:"links"` + // KeyName indicates which public key was injected into the server on launch. + KeyName string `json:"key_name"` + // AdminPass will generally be empty (""). However, it will contain the administrative password chosen when provisioning a new server without a set AdminPass setting in the first place. + // Note that this is the ONLY time this field will be valid. + AdminPass string `json:"adminPass"` + // SecurityGroups includes the security groups that this instance has applied to it + SecurityGroups []map[string]interface{} `json:"security_groups"` +} + +func (r *Server) UnmarshalJSON(b []byte) error { + type tmp Server + var s struct { + tmp + Image interface{} `json:"image"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = Server(s.tmp) + + switch t := s.Image.(type) { + case map[string]interface{}: + r.Image = t + case string: + switch t { + case "": + r.Image = nil + } + } + + return err +} + +// ServerPage abstracts the raw results of making a List() request against the API. +// As OpenStack extensions may freely alter the response bodies of structures returned to the client, you may only safely access the +// data provided through the ExtractServers call. +type ServerPage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if a page contains no Server results. +func (r ServerPage) IsEmpty() (bool, error) { + s, err := ExtractServers(r) + return len(s) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to the next page of results. +func (r ServerPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"servers_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// ExtractServers interprets the results of a single page from a List() call, producing a slice of Server entities. +func ExtractServers(r pagination.Page) ([]Server, error) { + var s []Server + err := ExtractServersInto(r, &s) + return s, err +} + +// MetadataResult contains the result of a call for (potentially) multiple key-value pairs. +type MetadataResult struct { + gophercloud.Result +} + +// GetMetadataResult temporarily contains the response from a metadata Get call. +type GetMetadataResult struct { + MetadataResult +} + +// ResetMetadataResult temporarily contains the response from a metadata Reset call. +type ResetMetadataResult struct { + MetadataResult +} + +// UpdateMetadataResult temporarily contains the response from a metadata Update call. +type UpdateMetadataResult struct { + MetadataResult +} + +// MetadatumResult contains the result of a call for individual a single key-value pair. +type MetadatumResult struct { + gophercloud.Result +} + +// GetMetadatumResult temporarily contains the response from a metadatum Get call. +type GetMetadatumResult struct { + MetadatumResult +} + +// CreateMetadatumResult temporarily contains the response from a metadatum Create call. +type CreateMetadatumResult struct { + MetadatumResult +} + +// DeleteMetadatumResult temporarily contains the response from a metadatum Delete call. +type DeleteMetadatumResult struct { + gophercloud.ErrResult +} + +// Extract interprets any MetadataResult as a Metadata, if possible. +func (r MetadataResult) Extract() (map[string]string, error) { + var s struct { + Metadata map[string]string `json:"metadata"` + } + err := r.ExtractInto(&s) + return s.Metadata, err +} + +// Extract interprets any MetadatumResult as a Metadatum, if possible. +func (r MetadatumResult) Extract() (map[string]string, error) { + var s struct { + Metadatum map[string]string `json:"meta"` + } + err := r.ExtractInto(&s) + return s.Metadatum, err +} + +// Address represents an IP address. +type Address struct { + Version int `json:"version"` + Address string `json:"addr"` +} + +// AddressPage abstracts the raw results of making a ListAddresses() request against the API. +// As OpenStack extensions may freely alter the response bodies of structures returned +// to the client, you may only safely access the data provided through the ExtractAddresses call. +type AddressPage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if an AddressPage contains no networks. +func (r AddressPage) IsEmpty() (bool, error) { + addresses, err := ExtractAddresses(r) + return len(addresses) == 0, err +} + +// ExtractAddresses interprets the results of a single page from a ListAddresses() call, +// producing a map of addresses. +func ExtractAddresses(r pagination.Page) (map[string][]Address, error) { + var s struct { + Addresses map[string][]Address `json:"addresses"` + } + err := (r.(AddressPage)).ExtractInto(&s) + return s.Addresses, err +} + +// NetworkAddressPage abstracts the raw results of making a ListAddressesByNetwork() request against the API. +// As OpenStack extensions may freely alter the response bodies of structures returned +// to the client, you may only safely access the data provided through the ExtractAddresses call. +type NetworkAddressPage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a NetworkAddressPage contains no addresses. +func (r NetworkAddressPage) IsEmpty() (bool, error) { + addresses, err := ExtractNetworkAddresses(r) + return len(addresses) == 0, err +} + +// ExtractNetworkAddresses interprets the results of a single page from a ListAddressesByNetwork() call, +// producing a slice of addresses. +func ExtractNetworkAddresses(r pagination.Page) ([]Address, error) { + var s map[string][]Address + err := (r.(NetworkAddressPage)).ExtractInto(&s) + if err != nil { + return nil, err + } + + var key string + for k := range s { + key = k + } + + return s[key], err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/testing/doc.go new file mode 100644 index 0000000000..c7c598298b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/testing/doc.go @@ -0,0 +1,2 @@ +// compute_servers_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/testing/fixtures.go new file mode 100644 index 0000000000..f77dfb44ee --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/testing/fixtures.go @@ -0,0 +1,971 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +// ServerListBody contains the canned body of a servers.List response. +const ServerListBody = ` +{ + "servers": [ + { + "status": "ACTIVE", + "updated": "2014-09-25T13:10:10Z", + "hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362", + "OS-EXT-SRV-ATTR:host": "devstack", + "addresses": { + "private": [ + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:7c:1b:2b", + "version": 4, + "addr": "10.0.0.32", + "OS-EXT-IPS:type": "fixed" + } + ] + }, + "links": [ + { + "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5", + "rel": "self" + }, + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5", + "rel": "bookmark" + } + ], + "key_name": null, + "image": { + "id": "f90f6034-2570-4974-8351-6b49732ef2eb", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + "rel": "bookmark" + } + ] + }, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "OS-EXT-SRV-ATTR:instance_name": "instance-0000001e", + "OS-SRV-USG:launched_at": "2014-09-25T13:10:10.000000", + "OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack", + "flavor": { + "id": "1", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1", + "rel": "bookmark" + } + ] + }, + "id": "ef079b0c-e610-4dfb-b1aa-b49f07ac48e5", + "security_groups": [ + { + "name": "default" + } + ], + "OS-SRV-USG:terminated_at": null, + "OS-EXT-AZ:availability_zone": "nova", + "user_id": "9349aff8be7545ac9d2f1d00999a23cd", + "name": "herp", + "created": "2014-09-25T13:10:02Z", + "tenant_id": "fcad67a6189847c4aecfa3c81a05783b", + "OS-DCF:diskConfig": "MANUAL", + "os-extended-volumes:volumes_attached": [], + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "OS-EXT-STS:power_state": 1, + "config_drive": "", + "metadata": {} + }, + { + "status": "ACTIVE", + "updated": "2014-09-25T13:04:49Z", + "hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362", + "OS-EXT-SRV-ATTR:host": "devstack", + "addresses": { + "private": [ + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be", + "version": 4, + "addr": "10.0.0.31", + "OS-EXT-IPS:type": "fixed" + } + ] + }, + "links": [ + { + "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "self" + }, + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "bookmark" + } + ], + "key_name": null, + "image": { + "id": "f90f6034-2570-4974-8351-6b49732ef2eb", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + "rel": "bookmark" + } + ] + }, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "OS-EXT-SRV-ATTR:instance_name": "instance-0000001d", + "OS-SRV-USG:launched_at": "2014-09-25T13:04:49.000000", + "OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack", + "flavor": { + "id": "1", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1", + "rel": "bookmark" + } + ] + }, + "id": "9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "security_groups": [ + { + "name": "default" + } + ], + "OS-SRV-USG:terminated_at": null, + "OS-EXT-AZ:availability_zone": "nova", + "user_id": "9349aff8be7545ac9d2f1d00999a23cd", + "name": "derp", + "created": "2014-09-25T13:04:41Z", + "tenant_id": "fcad67a6189847c4aecfa3c81a05783b", + "OS-DCF:diskConfig": "MANUAL", + "os-extended-volumes:volumes_attached": [], + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "OS-EXT-STS:power_state": 1, + "config_drive": "", + "metadata": {} + }, + { + "status": "ACTIVE", + "updated": "2014-09-25T13:04:49Z", + "hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362", + "OS-EXT-SRV-ATTR:host": "devstack", + "addresses": { + "private": [ + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be", + "version": 4, + "addr": "10.0.0.31", + "OS-EXT-IPS:type": "fixed" + } + ] + }, + "links": [ + { + "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "self" + }, + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "bookmark" + } + ], + "key_name": null, + "image": "", + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "OS-EXT-SRV-ATTR:instance_name": "instance-0000001d", + "OS-SRV-USG:launched_at": "2014-09-25T13:04:49.000000", + "OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack", + "flavor": { + "id": "1", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1", + "rel": "bookmark" + } + ] + }, + "id": "9e5476bd-a4ec-4653-93d6-72c93aa682bb", + "security_groups": [ + { + "name": "default" + } + ], + "OS-SRV-USG:terminated_at": null, + "OS-EXT-AZ:availability_zone": "nova", + "user_id": "9349aff8be7545ac9d2f1d00999a23cd", + "name": "merp", + "created": "2014-09-25T13:04:41Z", + "tenant_id": "fcad67a6189847c4aecfa3c81a05783b", + "OS-DCF:diskConfig": "MANUAL", + "os-extended-volumes:volumes_attached": [], + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "OS-EXT-STS:power_state": 1, + "config_drive": "", + "metadata": {} + } + ] +} +` + +// SingleServerBody is the canned body of a Get request on an existing server. +const SingleServerBody = ` +{ + "server": { + "status": "ACTIVE", + "updated": "2014-09-25T13:04:49Z", + "hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362", + "OS-EXT-SRV-ATTR:host": "devstack", + "addresses": { + "private": [ + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be", + "version": 4, + "addr": "10.0.0.31", + "OS-EXT-IPS:type": "fixed" + } + ] + }, + "links": [ + { + "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "self" + }, + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "bookmark" + } + ], + "key_name": null, + "image": { + "id": "f90f6034-2570-4974-8351-6b49732ef2eb", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + "rel": "bookmark" + } + ] + }, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "OS-EXT-SRV-ATTR:instance_name": "instance-0000001d", + "OS-SRV-USG:launched_at": "2014-09-25T13:04:49.000000", + "OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack", + "flavor": { + "id": "1", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1", + "rel": "bookmark" + } + ] + }, + "id": "9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "security_groups": [ + { + "name": "default" + } + ], + "OS-SRV-USG:terminated_at": null, + "OS-EXT-AZ:availability_zone": "nova", + "user_id": "9349aff8be7545ac9d2f1d00999a23cd", + "name": "derp", + "created": "2014-09-25T13:04:41Z", + "tenant_id": "fcad67a6189847c4aecfa3c81a05783b", + "OS-DCF:diskConfig": "MANUAL", + "os-extended-volumes:volumes_attached": [], + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "OS-EXT-STS:power_state": 1, + "config_drive": "", + "metadata": {} + } +} +` + +const ServerPasswordBody = ` +{ + "password": "xlozO3wLCBRWAa2yDjCCVx8vwNPypxnypmRYDa/zErlQ+EzPe1S/Gz6nfmC52mOlOSCRuUOmG7kqqgejPof6M7bOezS387zjq4LSvvwp28zUknzy4YzfFGhnHAdai3TxUJ26pfQCYrq8UTzmKF2Bq8ioSEtVVzM0A96pDh8W2i7BOz6MdoiVyiev/I1K2LsuipfxSJR7Wdke4zNXJjHHP2RfYsVbZ/k9ANu+Nz4iIH8/7Cacud/pphH7EjrY6a4RZNrjQskrhKYed0YERpotyjYk1eDtRe72GrSiXteqCM4biaQ5w3ruS+AcX//PXk3uJ5kC7d67fPXaVz4WaQRYMg==" +} +` + +var ( + herpTimeCreated, _ = time.Parse(time.RFC3339, "2014-09-25T13:10:02Z") + herpTimeUpdated, _ = time.Parse(time.RFC3339, "2014-09-25T13:10:10Z") + // ServerHerp is a Server struct that should correspond to the first result in ServerListBody. + ServerHerp = servers.Server{ + Status: "ACTIVE", + Updated: herpTimeUpdated, + HostID: "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362", + Addresses: map[string]interface{}{ + "private": []interface{}{ + map[string]interface{}{ + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:7c:1b:2b", + "version": float64(4), + "addr": "10.0.0.32", + "OS-EXT-IPS:type": "fixed", + }, + }, + }, + Links: []interface{}{ + map[string]interface{}{ + "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5", + "rel": "self", + }, + map[string]interface{}{ + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5", + "rel": "bookmark", + }, + }, + Image: map[string]interface{}{ + "id": "f90f6034-2570-4974-8351-6b49732ef2eb", + "links": []interface{}{ + map[string]interface{}{ + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + "rel": "bookmark", + }, + }, + }, + Flavor: map[string]interface{}{ + "id": "1", + "links": []interface{}{ + map[string]interface{}{ + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1", + "rel": "bookmark", + }, + }, + }, + ID: "ef079b0c-e610-4dfb-b1aa-b49f07ac48e5", + UserID: "9349aff8be7545ac9d2f1d00999a23cd", + Name: "herp", + Created: herpTimeCreated, + TenantID: "fcad67a6189847c4aecfa3c81a05783b", + Metadata: map[string]string{}, + SecurityGroups: []map[string]interface{}{ + map[string]interface{}{ + "name": "default", + }, + }, + } + + derpTimeCreated, _ = time.Parse(time.RFC3339, "2014-09-25T13:04:41Z") + derpTimeUpdated, _ = time.Parse(time.RFC3339, "2014-09-25T13:04:49Z") + // ServerDerp is a Server struct that should correspond to the second server in ServerListBody. + ServerDerp = servers.Server{ + Status: "ACTIVE", + Updated: derpTimeUpdated, + HostID: "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362", + Addresses: map[string]interface{}{ + "private": []interface{}{ + map[string]interface{}{ + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be", + "version": float64(4), + "addr": "10.0.0.31", + "OS-EXT-IPS:type": "fixed", + }, + }, + }, + Links: []interface{}{ + map[string]interface{}{ + "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "self", + }, + map[string]interface{}{ + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "bookmark", + }, + }, + Image: map[string]interface{}{ + "id": "f90f6034-2570-4974-8351-6b49732ef2eb", + "links": []interface{}{ + map[string]interface{}{ + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + "rel": "bookmark", + }, + }, + }, + Flavor: map[string]interface{}{ + "id": "1", + "links": []interface{}{ + map[string]interface{}{ + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1", + "rel": "bookmark", + }, + }, + }, + ID: "9e5476bd-a4ec-4653-93d6-72c93aa682ba", + UserID: "9349aff8be7545ac9d2f1d00999a23cd", + Name: "derp", + Created: derpTimeCreated, + TenantID: "fcad67a6189847c4aecfa3c81a05783b", + Metadata: map[string]string{}, + SecurityGroups: []map[string]interface{}{ + map[string]interface{}{ + "name": "default", + }, + }, + } + + merpTimeCreated, _ = time.Parse(time.RFC3339, "2014-09-25T13:04:41Z") + merpTimeUpdated, _ = time.Parse(time.RFC3339, "2014-09-25T13:04:49Z") + // ServerMerp is a Server struct that should correspond to the second server in ServerListBody. + ServerMerp = servers.Server{ + Status: "ACTIVE", + Updated: merpTimeUpdated, + HostID: "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362", + Addresses: map[string]interface{}{ + "private": []interface{}{ + map[string]interface{}{ + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be", + "version": float64(4), + "addr": "10.0.0.31", + "OS-EXT-IPS:type": "fixed", + }, + }, + }, + Links: []interface{}{ + map[string]interface{}{ + "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "self", + }, + map[string]interface{}{ + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "bookmark", + }, + }, + Image: nil, + Flavor: map[string]interface{}{ + "id": "1", + "links": []interface{}{ + map[string]interface{}{ + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1", + "rel": "bookmark", + }, + }, + }, + ID: "9e5476bd-a4ec-4653-93d6-72c93aa682bb", + UserID: "9349aff8be7545ac9d2f1d00999a23cd", + Name: "merp", + Created: merpTimeCreated, + TenantID: "fcad67a6189847c4aecfa3c81a05783b", + Metadata: map[string]string{}, + SecurityGroups: []map[string]interface{}{ + map[string]interface{}{ + "name": "default", + }, + }, + } +) + +type CreateOptsWithCustomField struct { + servers.CreateOpts + Foo string `json:"foo,omitempty"` +} + +func (opts CreateOptsWithCustomField) ToServerCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "server") +} + +// HandleServerCreationSuccessfully sets up the test server to respond to a server creation request +// with a given response. +func HandleServerCreationSuccessfully(t *testing.T, response string) { + th.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ + "server": { + "name": "derp", + "imageRef": "f90f6034-2570-4974-8351-6b49732ef2eb", + "flavorRef": "1" + } + }`) + + w.WriteHeader(http.StatusAccepted) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, response) + }) + + th.Mux.HandleFunc("/images/detail", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, ` + { + "images": [ + { + "status": "ACTIVE", + "updated": "2014-09-23T12:54:56Z", + "id": "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7", + "OS-EXT-IMG-SIZE:size": 476704768, + "name": "F17-x86_64-cfntools", + "created": "2014-09-23T12:54:52Z", + "minDisk": 0, + "progress": 100, + "minRam": 0 + }, + { + "status": "ACTIVE", + "updated": "2014-09-23T12:51:43Z", + "id": "f90f6034-2570-4974-8351-6b49732ef2eb", + "OS-EXT-IMG-SIZE:size": 13167616, + "name": "cirros-0.3.2-x86_64-disk", + "created": "2014-09-23T12:51:42Z", + "minDisk": 0, + "progress": 100, + "minRam": 0 + } + ] + } + `) + case "2": + fmt.Fprintf(w, `{ "images": [] }`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) + + th.Mux.HandleFunc("/flavors/detail", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, ` + { + "flavors": [ + { + "id": "1", + "name": "m1.tiny", + "disk": 1, + "ram": 512, + "vcpus": 1, + "swap":"" + }, + { + "id": "2", + "name": "m2.small", + "disk": 10, + "ram": 1024, + "vcpus": 2, + "swap": 1000 + } + ], + "flavors_links": [ + { + "href": "%s/flavors/detail?marker=2", + "rel": "next" + } + ] + } + `, th.Server.URL) + case "2": + fmt.Fprintf(w, `{ "flavors": [] }`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) +} + +// HandleServerCreationWithCustomFieldSuccessfully sets up the test server to respond to a server creation request +// with a given response. +func HandleServerCreationWithCustomFieldSuccessfully(t *testing.T, response string) { + th.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ + "server": { + "name": "derp", + "imageRef": "f90f6034-2570-4974-8351-6b49732ef2eb", + "flavorRef": "1", + "foo": "bar" + } + }`) + + w.WriteHeader(http.StatusAccepted) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, response) + }) +} + +// HandleServerCreationWithUserdata sets up the test server to respond to a server creation request +// with a given response. +func HandleServerCreationWithUserdata(t *testing.T, response string) { + th.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ + "server": { + "name": "derp", + "imageRef": "f90f6034-2570-4974-8351-6b49732ef2eb", + "flavorRef": "1", + "user_data": "dXNlcmRhdGEgc3RyaW5n" + } + }`) + + w.WriteHeader(http.StatusAccepted) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, response) + }) +} + +// HandleServerCreationWithMetadata sets up the test server to respond to a server creation request +// with a given response. +func HandleServerCreationWithMetadata(t *testing.T, response string) { + th.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ + "server": { + "name": "derp", + "imageRef": "f90f6034-2570-4974-8351-6b49732ef2eb", + "flavorRef": "1", + "metadata": { + "abc": "def" + } + } + }`) + + w.WriteHeader(http.StatusAccepted) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, response) + }) +} + +// HandleServerListSuccessfully sets up the test server to respond to a server List request. +func HandleServerListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/detail", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, ServerListBody) + case "9e5476bd-a4ec-4653-93d6-72c93aa682ba": + fmt.Fprintf(w, `{ "servers": [] }`) + default: + t.Fatalf("/servers/detail invoked with unexpected marker=[%s]", marker) + } + }) +} + +// HandleServerDeletionSuccessfully sets up the test server to respond to a server deletion request. +func HandleServerDeletionSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/asdfasdfasdf", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleServerForceDeletionSuccessfully sets up the test server to respond to a server force deletion +// request. +func HandleServerForceDeletionSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/asdfasdfasdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "forceDelete": "" }`) + + w.WriteHeader(http.StatusAccepted) + }) +} + +// HandleServerGetSuccessfully sets up the test server to respond to a server Get request. +func HandleServerGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + fmt.Fprintf(w, SingleServerBody) + }) +} + +// HandleServerUpdateSuccessfully sets up the test server to respond to a server Update request. +func HandleServerUpdateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestJSONRequest(t, r, `{ "server": { "name": "new-name" } }`) + + fmt.Fprintf(w, SingleServerBody) + }) +} + +// HandleAdminPasswordChangeSuccessfully sets up the test server to respond to a server password +// change request. +func HandleAdminPasswordChangeSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "changePassword": { "adminPass": "new-password" } }`) + + w.WriteHeader(http.StatusAccepted) + }) +} + +// HandleRebootSuccessfully sets up the test server to respond to a reboot request with success. +func HandleRebootSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "reboot": { "type": "SOFT" } }`) + + w.WriteHeader(http.StatusAccepted) + }) +} + +// HandleRebuildSuccessfully sets up the test server to respond to a rebuild request with success. +func HandleRebuildSuccessfully(t *testing.T, response string) { + th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, ` + { + "rebuild": { + "name": "new-name", + "adminPass": "swordfish", + "imageRef": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + "accessIPv4": "1.2.3.4" + } + } + `) + + w.WriteHeader(http.StatusAccepted) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, response) + }) +} + +// HandleServerRescueSuccessfully sets up the test server to respond to a server Rescue request. +func HandleServerRescueSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "rescue": { "adminPass": "1234567890" } }`) + + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{ "adminPass": "1234567890" }`)) + }) +} + +// HandleMetadatumGetSuccessfully sets up the test server to respond to a metadatum Get request. +func HandleMetadatumGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/metadata/foo", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + w.Write([]byte(`{ "meta": {"foo":"bar"}}`)) + }) +} + +// HandleMetadatumCreateSuccessfully sets up the test server to respond to a metadatum Create request. +func HandleMetadatumCreateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/metadata/foo", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ + "meta": { + "foo": "bar" + } + }`) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + w.Write([]byte(`{ "meta": {"foo":"bar"}}`)) + }) +} + +// HandleMetadatumDeleteSuccessfully sets up the test server to respond to a metadatum Delete request. +func HandleMetadatumDeleteSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/metadata/foo", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleMetadataGetSuccessfully sets up the test server to respond to a metadata Get request. +func HandleMetadataGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/metadata", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{ "metadata": {"foo":"bar", "this":"that"}}`)) + }) +} + +// HandleMetadataResetSuccessfully sets up the test server to respond to a metadata Create request. +func HandleMetadataResetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/metadata", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ + "metadata": { + "foo": "bar", + "this": "that" + } + }`) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + w.Write([]byte(`{ "metadata": {"foo":"bar", "this":"that"}}`)) + }) +} + +// HandleMetadataUpdateSuccessfully sets up the test server to respond to a metadata Update request. +func HandleMetadataUpdateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/metadata", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ + "metadata": { + "foo": "baz", + "this": "those" + } + }`) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + w.Write([]byte(`{ "metadata": {"foo":"baz", "this":"those"}}`)) + }) +} + +// ListAddressesExpected represents an expected repsonse from a ListAddresses request. +var ListAddressesExpected = map[string][]servers.Address{ + "public": []servers.Address{ + { + Version: 4, + Address: "50.56.176.35", + }, + { + Version: 6, + Address: "2001:4800:790e:510:be76:4eff:fe04:84a8", + }, + }, + "private": []servers.Address{ + { + Version: 4, + Address: "10.180.3.155", + }, + }, +} + +// HandleAddressListSuccessfully sets up the test server to respond to a ListAddresses request. +func HandleAddressListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/asdfasdfasdf/ips", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "addresses": { + "public": [ + { + "version": 4, + "addr": "50.56.176.35" + }, + { + "version": 6, + "addr": "2001:4800:790e:510:be76:4eff:fe04:84a8" + } + ], + "private": [ + { + "version": 4, + "addr": "10.180.3.155" + } + ] + } + }`) + }) +} + +// ListNetworkAddressesExpected represents an expected repsonse from a ListAddressesByNetwork request. +var ListNetworkAddressesExpected = []servers.Address{ + { + Version: 4, + Address: "50.56.176.35", + }, + { + Version: 6, + Address: "2001:4800:790e:510:be76:4eff:fe04:84a8", + }, +} + +// HandleNetworkAddressListSuccessfully sets up the test server to respond to a ListAddressesByNetwork request. +func HandleNetworkAddressListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/asdfasdfasdf/ips/public", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "public": [ + { + "version": 4, + "addr": "50.56.176.35" + }, + { + "version": 6, + "addr": "2001:4800:790e:510:be76:4eff:fe04:84a8" + } + ] + }`) + }) +} + +// HandleCreateServerImageSuccessfully sets up the test server to respond to a TestCreateServerImage request. +func HandleCreateServerImageSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/serverimage/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + w.Header().Add("Location", "https://0.0.0.0/images/xxxx-xxxxx-xxxxx-xxxx") + w.WriteHeader(http.StatusAccepted) + }) +} + +// HandlePasswordGetSuccessfully sets up the test server to respond to a password Get request. +func HandlePasswordGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/os-server-password", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + fmt.Fprintf(w, ServerPasswordBody) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/testing/requests_test.go new file mode 100644 index 0000000000..05712f7b0f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/testing/requests_test.go @@ -0,0 +1,522 @@ +package testing + +import ( + "encoding/base64" + "encoding/json" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/availabilityzones" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestListServers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerListSuccessfully(t) + + pages := 0 + err := servers.List(client.ServiceClient(), servers.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := servers.ExtractServers(page) + if err != nil { + return false, err + } + + if len(actual) != 3 { + t.Fatalf("Expected 3 servers, got %d", len(actual)) + } + th.CheckDeepEquals(t, ServerHerp, actual[0]) + th.CheckDeepEquals(t, ServerDerp, actual[1]) + th.CheckDeepEquals(t, ServerMerp, actual[2]) + + return true, nil + }) + + th.AssertNoErr(t, err) + + if pages != 1 { + t.Errorf("Expected 1 page, saw %d", pages) + } +} + +func TestListAllServers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerListSuccessfully(t) + + allPages, err := servers.List(client.ServiceClient(), servers.ListOpts{}).AllPages() + th.AssertNoErr(t, err) + actual, err := servers.ExtractServers(allPages) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ServerHerp, actual[0]) + th.CheckDeepEquals(t, ServerDerp, actual[1]) +} + +func TestListAllServersWithExtensions(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerListSuccessfully(t) + + type ServerWithExt struct { + servers.Server + availabilityzones.ServerExt + } + + allPages, err := servers.List(client.ServiceClient(), servers.ListOpts{}).AllPages() + th.AssertNoErr(t, err) + + var actual []ServerWithExt + err = servers.ExtractServersInto(allPages, &actual) + th.AssertNoErr(t, err) + th.AssertEquals(t, 3, len(actual)) + th.AssertEquals(t, "nova", actual[0].AvailabilityZone) +} + +func TestCreateServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerCreationSuccessfully(t, SingleServerBody) + + actual, err := servers.Create(client.ServiceClient(), servers.CreateOpts{ + Name: "derp", + ImageRef: "f90f6034-2570-4974-8351-6b49732ef2eb", + FlavorRef: "1", + }).Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ServerDerp, *actual) +} + +func TestCreateServerWithCustomField(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerCreationWithCustomFieldSuccessfully(t, SingleServerBody) + + actual, err := servers.Create(client.ServiceClient(), CreateOptsWithCustomField{ + CreateOpts: servers.CreateOpts{ + Name: "derp", + ImageRef: "f90f6034-2570-4974-8351-6b49732ef2eb", + FlavorRef: "1", + }, + Foo: "bar", + }).Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ServerDerp, *actual) +} + +func TestCreateServerWithMetadata(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerCreationWithMetadata(t, SingleServerBody) + + actual, err := servers.Create(client.ServiceClient(), servers.CreateOpts{ + Name: "derp", + ImageRef: "f90f6034-2570-4974-8351-6b49732ef2eb", + FlavorRef: "1", + Metadata: map[string]string{ + "abc": "def", + }, + }).Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ServerDerp, *actual) +} + +func TestCreateServerWithUserdataString(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerCreationWithUserdata(t, SingleServerBody) + + actual, err := servers.Create(client.ServiceClient(), servers.CreateOpts{ + Name: "derp", + ImageRef: "f90f6034-2570-4974-8351-6b49732ef2eb", + FlavorRef: "1", + UserData: []byte("userdata string"), + }).Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ServerDerp, *actual) +} + +func TestCreateServerWithUserdataEncoded(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerCreationWithUserdata(t, SingleServerBody) + + encoded := base64.StdEncoding.EncodeToString([]byte("userdata string")) + + actual, err := servers.Create(client.ServiceClient(), servers.CreateOpts{ + Name: "derp", + ImageRef: "f90f6034-2570-4974-8351-6b49732ef2eb", + FlavorRef: "1", + UserData: []byte(encoded), + }).Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ServerDerp, *actual) +} + +func TestCreateServerWithImageNameAndFlavorName(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerCreationSuccessfully(t, SingleServerBody) + + actual, err := servers.Create(client.ServiceClient(), servers.CreateOpts{ + Name: "derp", + ImageName: "cirros-0.3.2-x86_64-disk", + FlavorName: "m1.tiny", + ServiceClient: client.ServiceClient(), + }).Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ServerDerp, *actual) +} + +func TestDeleteServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerDeletionSuccessfully(t) + + res := servers.Delete(client.ServiceClient(), "asdfasdfasdf") + th.AssertNoErr(t, res.Err) +} + +func TestForceDeleteServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerForceDeletionSuccessfully(t) + + res := servers.ForceDelete(client.ServiceClient(), "asdfasdfasdf") + th.AssertNoErr(t, res.Err) +} + +func TestGetServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerGetSuccessfully(t) + + client := client.ServiceClient() + actual, err := servers.Get(client, "1234asdf").Extract() + if err != nil { + t.Fatalf("Unexpected Get error: %v", err) + } + + th.CheckDeepEquals(t, ServerDerp, *actual) +} + +func TestGetServerWithExtensions(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerGetSuccessfully(t) + + var s struct { + servers.Server + availabilityzones.ServerExt + } + + err := servers.Get(client.ServiceClient(), "1234asdf").ExtractInto(&s) + th.AssertNoErr(t, err) + th.AssertEquals(t, "nova", s.AvailabilityZone) + + err = servers.Get(client.ServiceClient(), "1234asdf").ExtractInto(s) + if err == nil { + t.Errorf("Expected error when providing non-pointer struct") + } +} + +func TestUpdateServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerUpdateSuccessfully(t) + + client := client.ServiceClient() + actual, err := servers.Update(client, "1234asdf", servers.UpdateOpts{Name: "new-name"}).Extract() + if err != nil { + t.Fatalf("Unexpected Update error: %v", err) + } + + th.CheckDeepEquals(t, ServerDerp, *actual) +} + +func TestChangeServerAdminPassword(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleAdminPasswordChangeSuccessfully(t) + + res := servers.ChangeAdminPassword(client.ServiceClient(), "1234asdf", "new-password") + th.AssertNoErr(t, res.Err) +} + +func TestGetPassword(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandlePasswordGetSuccessfully(t) + + res := servers.GetPassword(client.ServiceClient(), "1234asdf") + th.AssertNoErr(t, res.Err) +} + +func TestRebootServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleRebootSuccessfully(t) + + res := servers.Reboot(client.ServiceClient(), "1234asdf", &servers.RebootOpts{ + Type: servers.SoftReboot, + }) + th.AssertNoErr(t, res.Err) +} + +func TestRebuildServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleRebuildSuccessfully(t, SingleServerBody) + + opts := servers.RebuildOpts{ + Name: "new-name", + AdminPass: "swordfish", + ImageID: "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + AccessIPv4: "1.2.3.4", + } + + actual, err := servers.Rebuild(client.ServiceClient(), "1234asdf", opts).Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ServerDerp, *actual) +} + +func TestResizeServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "resize": { "flavorRef": "2" } }`) + + w.WriteHeader(http.StatusAccepted) + }) + + res := servers.Resize(client.ServiceClient(), "1234asdf", servers.ResizeOpts{FlavorRef: "2"}) + th.AssertNoErr(t, res.Err) +} + +func TestConfirmResize(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "confirmResize": null }`) + + w.WriteHeader(http.StatusNoContent) + }) + + res := servers.ConfirmResize(client.ServiceClient(), "1234asdf") + th.AssertNoErr(t, res.Err) +} + +func TestRevertResize(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "revertResize": null }`) + + w.WriteHeader(http.StatusAccepted) + }) + + res := servers.RevertResize(client.ServiceClient(), "1234asdf") + th.AssertNoErr(t, res.Err) +} + +func TestRescue(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleServerRescueSuccessfully(t) + + res := servers.Rescue(client.ServiceClient(), "1234asdf", servers.RescueOpts{ + AdminPass: "1234567890", + }) + th.AssertNoErr(t, res.Err) + adminPass, _ := res.Extract() + th.AssertEquals(t, "1234567890", adminPass) +} + +func TestGetMetadatum(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleMetadatumGetSuccessfully(t) + + expected := map[string]string{"foo": "bar"} + actual, err := servers.Metadatum(client.ServiceClient(), "1234asdf", "foo").Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expected, actual) +} + +func TestCreateMetadatum(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleMetadatumCreateSuccessfully(t) + + expected := map[string]string{"foo": "bar"} + actual, err := servers.CreateMetadatum(client.ServiceClient(), "1234asdf", servers.MetadatumOpts{"foo": "bar"}).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expected, actual) +} + +func TestDeleteMetadatum(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleMetadatumDeleteSuccessfully(t) + + err := servers.DeleteMetadatum(client.ServiceClient(), "1234asdf", "foo").ExtractErr() + th.AssertNoErr(t, err) +} + +func TestGetMetadata(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleMetadataGetSuccessfully(t) + + expected := map[string]string{"foo": "bar", "this": "that"} + actual, err := servers.Metadata(client.ServiceClient(), "1234asdf").Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expected, actual) +} + +func TestResetMetadata(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleMetadataResetSuccessfully(t) + + expected := map[string]string{"foo": "bar", "this": "that"} + actual, err := servers.ResetMetadata(client.ServiceClient(), "1234asdf", servers.MetadataOpts{ + "foo": "bar", + "this": "that", + }).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expected, actual) +} + +func TestUpdateMetadata(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleMetadataUpdateSuccessfully(t) + + expected := map[string]string{"foo": "baz", "this": "those"} + actual, err := servers.UpdateMetadata(client.ServiceClient(), "1234asdf", servers.MetadataOpts{ + "foo": "baz", + "this": "those", + }).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expected, actual) +} + +func TestListAddresses(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleAddressListSuccessfully(t) + + expected := ListAddressesExpected + pages := 0 + err := servers.ListAddresses(client.ServiceClient(), "asdfasdfasdf").EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := servers.ExtractAddresses(page) + th.AssertNoErr(t, err) + + if len(actual) != 2 { + t.Fatalf("Expected 2 networks, got %d", len(actual)) + } + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, pages) +} + +func TestListAddressesByNetwork(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleNetworkAddressListSuccessfully(t) + + expected := ListNetworkAddressesExpected + pages := 0 + err := servers.ListAddressesByNetwork(client.ServiceClient(), "asdfasdfasdf", "public").EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := servers.ExtractNetworkAddresses(page) + th.AssertNoErr(t, err) + + if len(actual) != 2 { + t.Fatalf("Expected 2 addresses, got %d", len(actual)) + } + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, pages) +} + +func TestCreateServerImage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateServerImageSuccessfully(t) + + _, err := servers.CreateImage(client.ServiceClient(), "serverimage", servers.CreateImageOpts{Name: "test"}).ExtractImageID() + th.AssertNoErr(t, err) +} + +func TestMarshalPersonality(t *testing.T) { + name := "/etc/test" + contents := []byte("asdfasdf") + + personality := servers.Personality{ + &servers.File{ + Path: name, + Contents: contents, + }, + } + + data, err := json.Marshal(personality) + if err != nil { + t.Fatal(err) + } + + var actual []map[string]string + err = json.Unmarshal(data, &actual) + if err != nil { + t.Fatal(err) + } + + if len(actual) != 1 { + t.Fatal("expected personality length 1") + } + + if actual[0]["path"] != name { + t.Fatal("file path incorrect") + } + + if actual[0]["contents"] != base64.StdEncoding.EncodeToString(contents) { + t.Fatal("file contents incorrect") + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/testing/results_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/testing/results_test.go new file mode 100644 index 0000000000..d4773dc916 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/testing/results_test.go @@ -0,0 +1,110 @@ +package testing + +import ( + "crypto/rsa" + "encoding/json" + "fmt" + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" + "golang.org/x/crypto/ssh" +) + +// Fail - No password in JSON. +func TestExtractPassword_no_pwd_data(t *testing.T) { + + var dejson interface{} + err := json.Unmarshal([]byte(`{ "Crappy data": ".-.-." }`), &dejson) + if err != nil { + t.Fatalf("%s", err) + } + resp := servers.GetPasswordResult{Result: gophercloud.Result{Body: dejson}} + + pwd, err := resp.ExtractPassword(nil) + th.AssertEquals(t, pwd, "") +} + +// Ok - return encrypted password when no private key is given. +func TestExtractPassword_encrypted_pwd(t *testing.T) { + + var dejson interface{} + sejson := []byte(`{"password":"PP8EnwPO9DhEc8+O/6CKAkPF379mKsUsfFY6yyw0734XXvKsSdV9KbiHQ2hrBvzeZxtGMrlFaikVunCRizyLLWLMuOi4hoH+qy9F9sQid61gQIGkxwDAt85d/7Eau2/KzorFnZhgxArl7IiqJ67X6xjKkR3zur+Yp3V/mtVIehpPYIaAvPbcp2t4mQXl1I9J8yrQfEZOctLL1L4heDEVXnxvNihVLK6pivlVggp6SZCtjj9cduZGrYGsxsOCso1dqJQr7GCojfwvuLOoG0OYwEGuWVTZppxWxi/q1QgeHFhGKA5QUXlz7pS71oqpjYsTeViuHnfvlqb5TVYZpQ1haw=="}`) + + err := json.Unmarshal(sejson, &dejson) + fmt.Printf("%v\n", dejson) + if err != nil { + t.Fatalf("%s", err) + } + resp := servers.GetPasswordResult{Result: gophercloud.Result{Body: dejson}} + + pwd, err := resp.ExtractPassword(nil) + th.AssertNoErr(t, err) + th.AssertEquals(t, "PP8EnwPO9DhEc8+O/6CKAkPF379mKsUsfFY6yyw0734XXvKsSdV9KbiHQ2hrBvzeZxtGMrlFaikVunCRizyLLWLMuOi4hoH+qy9F9sQid61gQIGkxwDAt85d/7Eau2/KzorFnZhgxArl7IiqJ67X6xjKkR3zur+Yp3V/mtVIehpPYIaAvPbcp2t4mQXl1I9J8yrQfEZOctLL1L4heDEVXnxvNihVLK6pivlVggp6SZCtjj9cduZGrYGsxsOCso1dqJQr7GCojfwvuLOoG0OYwEGuWVTZppxWxi/q1QgeHFhGKA5QUXlz7pS71oqpjYsTeViuHnfvlqb5TVYZpQ1haw==", pwd) +} + +// Ok - return decrypted password when private key is given. +// Decrytion can be verified by: +// echo "" | base64 -D | openssl rsautl -decrypt -inkey +func TestExtractPassword_decrypted_pwd(t *testing.T) { + + privateKey, err := ssh.ParseRawPrivateKey([]byte(` +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAo1ODZgwMVdTJYim9UYuYhowoPMhGEuV5IRZjcJ315r7RBSC+ +yEiBb1V+jhf+P8fzAyU35lkBzZGDr7E3jxSesbOuYT8cItQS4ErUnI1LGuqvMxwv +X3GMyE/HmOcaiODF1XZN3Ur5pMJdVknnmczgUsW0hT98Udrh3MQn9WSuh/6LRy6+ +x1QsKHOCLFPnkhWa3LKyxmpQq/Gvhz+6NLe+gt8MFullA5mKQxBJ/K6laVHeaMlw +JG3GCX0EZhRlvzoV8koIBKZtbKFolFr8ZtxBm3R5LvnyrtOvp22sa+xeItUT5kG1 +ZnbGNdK87oYW+VigEUfzT/+8R1i6E2QIXoeZiQIDAQABAoIBAQCVZ70IqbbTAW8j +RAlyQh/J3Qal65LmkFJJKUDX8TfT1/Q/G6BKeMEmxm+Zrmsfj1pHI1HKftt+YEG1 +g4jOc09kQXkgbmnfll6aHPn3J+1vdwXD3GGdjrL5PrnYrngAhJWU2r8J0x8hT8ew +OrUJZXhDX6XuSpAAFRmOKUZgXbSmo4X+LZX76ACnarselJt5FL724ECvpWJ7xxC4 +FMzvp4RqMmNFvv/Uq9lE/EmoSk4dviYyIZZ16DbDNyc9k/sGqCAMktCEwZ3EQm// +S5bkNhgP6oUXjluWy53aPRgykEylgDWo5SSdSEyKnw/fciU0xdprA9JrBGIcTyHS +/k2kgD4xAoGBANTkJ88Q0YrxX3fZNZVqcn00XKTxPGmxN5LRs7eV743q30AxK5Db +QU8iwaAA1IKUWV5DLhgUTNsDCOPUPue4aOSBD3/sj+WEmvIhj7afDL5didkYHsqf +fDnhFHq7y/3i57d428C7BwwR79pGWVyi7vH3pfu9A1iwl1aNOae+zvbVAoGBAMRm +AmwQ9fJ3Qc44jysFK/yliLRGdShjkMMah5G3JlrelwfPtwPwEL2EHHhJB/C1acMs +n6Q6RaoF6WNSZUY65ksQg7aPOYf2X0FTFwQJvwDJ4qlWjmq7w+tQ0AoGJG+dVUmQ +zHZ/Y+HokSXzz9c4oevk4v/rMgAQ00WHrTdtIhnlAoGBALIJJ72D7CkNGHCq5qPQ +xHQukPejgolFGhufYXM7YX3GmPMe67cVlTVv9Isxhoa5N0+cUPT0LR3PGOUm/4Bb +eOT3hZXOqLwhvE6XgI8Rzd95bClwgXekDoh80dqeKMdmta961BQGlKskaPiacmsF +G1yhZV70P9Mwwy8vpbLB4GUNAoGAbTwbjsWkNfa0qCF3J8NZoszjCvnBQfSW2J1R +1+8ZKyNwt0yFi3Ajr3TibNiZzPzp1T9lj29FvfpJxA9Y+sXZvthxmcFxizix5GB1 +ha5yCNtA8VSOI7lJkAFDpL+j1lyYyjD6N9JE2KqEyKoh6J+8F7sXsqW7CqRRDfQX +mKNfey0CgYEAxcEoNoADN2hRl7qY9rbQfVvQb3RkoQkdHhl9gpLFCcV32IP8R4xg +09NbQK5OmgcIuZhLVNzTmUHJbabEGeXqIFIV0DsqECAt3WzbDyKQO23VJysFD46c +KSde3I0ybDz7iS2EtceKB7m4C0slYd+oBkm4efuF00rCOKDwpFq45m0= +-----END RSA PRIVATE KEY----- +`)) + if err != nil { + t.Fatalf("Error parsing private key: %s\n", err) + } + + var dejson interface{} + sejson := []byte(`{"password":"PP8EnwPO9DhEc8+O/6CKAkPF379mKsUsfFY6yyw0734XXvKsSdV9KbiHQ2hrBvzeZxtGMrlFaikVunCRizyLLWLMuOi4hoH+qy9F9sQid61gQIGkxwDAt85d/7Eau2/KzorFnZhgxArl7IiqJ67X6xjKkR3zur+Yp3V/mtVIehpPYIaAvPbcp2t4mQXl1I9J8yrQfEZOctLL1L4heDEVXnxvNihVLK6pivlVggp6SZCtjj9cduZGrYGsxsOCso1dqJQr7GCojfwvuLOoG0OYwEGuWVTZppxWxi/q1QgeHFhGKA5QUXlz7pS71oqpjYsTeViuHnfvlqb5TVYZpQ1haw=="}`) + + err = json.Unmarshal(sejson, &dejson) + fmt.Printf("%v\n", dejson) + if err != nil { + t.Fatalf("%s", err) + } + resp := servers.GetPasswordResult{Result: gophercloud.Result{Body: dejson}} + + pwd, err := resp.ExtractPassword(privateKey.(*rsa.PrivateKey)) + th.AssertNoErr(t, err) + th.AssertEquals(t, "ruZKK0tqxRfYm5t7lSJq", pwd) +} + +func TestListAddressesAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleAddressListSuccessfully(t) + + allPages, err := servers.ListAddresses(client.ServiceClient(), "asdfasdfasdf").AllPages() + th.AssertNoErr(t, err) + _, err = servers.ExtractAddresses(allPages) + th.AssertNoErr(t, err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/urls.go new file mode 100644 index 0000000000..e892e8d925 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/urls.go @@ -0,0 +1,51 @@ +package servers + +import "github.com/gophercloud/gophercloud" + +func createURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("servers") +} + +func listURL(client *gophercloud.ServiceClient) string { + return createURL(client) +} + +func listDetailURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("servers", "detail") +} + +func deleteURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id) +} + +func getURL(client *gophercloud.ServiceClient, id string) string { + return deleteURL(client, id) +} + +func updateURL(client *gophercloud.ServiceClient, id string) string { + return deleteURL(client, id) +} + +func actionURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "action") +} + +func metadatumURL(client *gophercloud.ServiceClient, id, key string) string { + return client.ServiceURL("servers", id, "metadata", key) +} + +func metadataURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "metadata") +} + +func listAddressesURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "ips") +} + +func listAddressesByNetworkURL(client *gophercloud.ServiceClient, id, network string) string { + return client.ServiceURL("servers", id, "ips", network) +} + +func passwordURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "os-server-password") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/util.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/util.go new file mode 100644 index 0000000000..494a0e4dc4 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/util.go @@ -0,0 +1,20 @@ +package servers + +import "github.com/gophercloud/gophercloud" + +// WaitForStatus will continually poll a server until it successfully transitions to a specified +// status. It will do this for at most the number of seconds specified. +func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error { + return gophercloud.WaitFor(secs, func() (bool, error) { + current, err := Get(c, id).Extract() + if err != nil { + return false, err + } + + if current.Status == status { + return true, nil + } + + return false, nil + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/configurations/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/configurations/doc.go new file mode 100644 index 0000000000..45b9cfb4e8 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/configurations/doc.go @@ -0,0 +1,11 @@ +// Package configurations provides information and interaction with the +// configuration API resource in the Rackspace Database service. +// +// A configuration group is a collection of key/value pairs which define how a +// particular database operates. These key/value pairs are specific to each +// datastore type and serve like settings. Some directives are capable of being +// applied dynamically, while other directives require a server restart to take +// effect. The configuration group can be applied to an instance at creation or +// applied to an existing instance to modify the behavior of the running +// datastore on the instance. +package configurations diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/configurations/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/configurations/requests.go new file mode 100644 index 0000000000..6851c58765 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/configurations/requests.go @@ -0,0 +1,167 @@ +package configurations + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/db/v1/instances" + "github.com/gophercloud/gophercloud/pagination" +) + +// List will list all of the available configurations. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, baseURL(client), func(r pagination.PageResult) pagination.Page { + return ConfigPage{pagination.SinglePageBase(r)} + }) +} + +// CreateOptsBuilder is a top-level interface which renders a JSON map. +type CreateOptsBuilder interface { + ToConfigCreateMap() (map[string]interface{}, error) +} + +// DatastoreOpts is the primary options struct for creating and modifying +// how configuration resources are associated with datastores. +type DatastoreOpts struct { + // The type of datastore. Defaults to "MySQL". + Type string `json:"type,omitempty"` + // The specific version of a datastore. Defaults to "5.6". + Version string `json:"version,omitempty"` +} + +// CreateOpts is the struct responsible for configuring new configurations. +type CreateOpts struct { + // The configuration group name + Name string `json:"name" required:"true"` + // A map of user-defined configuration settings that will define + // how each associated datastore works. Each key/value pair is specific to a + // datastore type. + Values map[string]interface{} `json:"values" required:"true"` + // Associates the configuration group with a particular datastore. + Datastore *DatastoreOpts `json:"datastore,omitempty"` + // A human-readable explanation for the group. + Description string `json:"description,omitempty"` +} + +// ToConfigCreateMap casts a CreateOpts struct into a JSON map. +func (opts CreateOpts) ToConfigCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "configuration") +} + +// Create will create a new configuration group. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToConfigCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(baseURL(client), &b, &r.Body, &gophercloud.RequestOpts{OkCodes: []int{200}}) + return +} + +// Get will retrieve the details for a specified configuration group. +func Get(client *gophercloud.ServiceClient, configID string) (r GetResult) { + _, r.Err = client.Get(resourceURL(client, configID), &r.Body, nil) + return +} + +// UpdateOptsBuilder is the top-level interface for casting update options into +// JSON maps. +type UpdateOptsBuilder interface { + ToConfigUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts is the struct responsible for modifying existing configurations. +type UpdateOpts struct { + // The configuration group name + Name string `json:"name,omitempty"` + // A map of user-defined configuration settings that will define + // how each associated datastore works. Each key/value pair is specific to a + // datastore type. + Values map[string]interface{} `json:"values,omitempty"` + // Associates the configuration group with a particular datastore. + Datastore *DatastoreOpts `json:"datastore,omitempty"` + // A human-readable explanation for the group. + Description string `json:"description,omitempty"` +} + +// ToConfigUpdateMap will cast an UpdateOpts struct into a JSON map. +func (opts UpdateOpts) ToConfigUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "configuration") +} + +// Update will modify an existing configuration group by performing a merge +// between new and existing values. If the key already exists, the new value +// will overwrite. All other keys will remain unaffected. +func Update(client *gophercloud.ServiceClient, configID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToConfigUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Patch(resourceURL(client, configID), &b, nil, nil) + return +} + +// Replace will modify an existing configuration group by overwriting the +// entire parameter group with the new values provided. Any existing keys not +// included in UpdateOptsBuilder will be deleted. +func Replace(client *gophercloud.ServiceClient, configID string, opts UpdateOptsBuilder) (r ReplaceResult) { + b, err := opts.ToConfigUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(resourceURL(client, configID), &b, nil, nil) + return +} + +// Delete will permanently delete a configuration group. Please note that +// config groups cannot be deleted whilst still attached to running instances - +// you must detach and then delete them. +func Delete(client *gophercloud.ServiceClient, configID string) (r DeleteResult) { + _, r.Err = client.Delete(resourceURL(client, configID), nil) + return +} + +// ListInstances will list all the instances associated with a particular +// configuration group. +func ListInstances(client *gophercloud.ServiceClient, configID string) pagination.Pager { + return pagination.NewPager(client, instancesURL(client, configID), func(r pagination.PageResult) pagination.Page { + return instances.InstancePage{LinkedPageBase: pagination.LinkedPageBase{PageResult: r}} + }) +} + +// ListDatastoreParams will list all the available and supported parameters +// that can be used for a particular datastore ID and a particular version. +// For example, if you are wondering how you can configure a MySQL 5.6 instance, +// you can use this operation (you will need to retrieve the MySQL datastore ID +// by using the datastores API). +func ListDatastoreParams(client *gophercloud.ServiceClient, datastoreID, versionID string) pagination.Pager { + return pagination.NewPager(client, listDSParamsURL(client, datastoreID, versionID), func(r pagination.PageResult) pagination.Page { + return ParamPage{pagination.SinglePageBase(r)} + }) +} + +// GetDatastoreParam will retrieve information about a specific configuration +// parameter. For example, you can use this operation to understand more about +// "innodb_file_per_table" configuration param for MySQL datastores. You will +// need the param's ID first, which can be attained by using the ListDatastoreParams +// operation. +func GetDatastoreParam(client *gophercloud.ServiceClient, datastoreID, versionID, paramID string) (r ParamResult) { + _, r.Err = client.Get(getDSParamURL(client, datastoreID, versionID, paramID), &r.Body, nil) + return +} + +// ListGlobalParams is similar to ListDatastoreParams but does not require a +// DatastoreID. +func ListGlobalParams(client *gophercloud.ServiceClient, versionID string) pagination.Pager { + return pagination.NewPager(client, listGlobalParamsURL(client, versionID), func(r pagination.PageResult) pagination.Page { + return ParamPage{pagination.SinglePageBase(r)} + }) +} + +// GetGlobalParam is similar to GetDatastoreParam but does not require a +// DatastoreID. +func GetGlobalParam(client *gophercloud.ServiceClient, versionID, paramID string) (r ParamResult) { + _, r.Err = client.Get(getGlobalParamURL(client, versionID, paramID), &r.Body, nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/configurations/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/configurations/results.go new file mode 100644 index 0000000000..c52a65417b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/configurations/results.go @@ -0,0 +1,121 @@ +package configurations + +import ( + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Config represents a configuration group API resource. +type Config struct { + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` + DatastoreName string `json:"datastore_name"` + DatastoreVersionID string `json:"datastore_version_id"` + DatastoreVersionName string `json:"datastore_version_name"` + Description string + ID string + Name string + Values map[string]interface{} +} + +// ConfigPage contains a page of Config resources in a paginated collection. +type ConfigPage struct { + pagination.SinglePageBase +} + +// IsEmpty indicates whether a ConfigPage is empty. +func (r ConfigPage) IsEmpty() (bool, error) { + is, err := ExtractConfigs(r) + return len(is) == 0, err +} + +// ExtractConfigs will retrieve a slice of Config structs from a page. +func ExtractConfigs(r pagination.Page) ([]Config, error) { + var s struct { + Configs []Config `json:"configurations"` + } + err := (r.(ConfigPage)).ExtractInto(&s) + return s.Configs, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract will retrieve a Config resource from an operation result. +func (r commonResult) Extract() (*Config, error) { + var s struct { + Config *Config `json:"configuration"` + } + err := r.ExtractInto(&s) + return s.Config, err +} + +// GetResult represents the result of a Get operation. +type GetResult struct { + commonResult +} + +// CreateResult represents the result of a Create operation. +type CreateResult struct { + commonResult +} + +// UpdateResult represents the result of an Update operation. +type UpdateResult struct { + gophercloud.ErrResult +} + +// ReplaceResult represents the result of a Replace operation. +type ReplaceResult struct { + gophercloud.ErrResult +} + +// DeleteResult represents the result of a Delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// Param represents a configuration parameter API resource. +type Param struct { + Max float64 + Min float64 + Name string + RestartRequired bool `json:"restart_required"` + Type string +} + +// ParamPage contains a page of Param resources in a paginated collection. +type ParamPage struct { + pagination.SinglePageBase +} + +// IsEmpty indicates whether a ParamPage is empty. +func (r ParamPage) IsEmpty() (bool, error) { + is, err := ExtractParams(r) + return len(is) == 0, err +} + +// ExtractParams will retrieve a slice of Param structs from a page. +func ExtractParams(r pagination.Page) ([]Param, error) { + var s struct { + Params []Param `json:"configuration-parameters"` + } + err := (r.(ParamPage)).ExtractInto(&s) + return s.Params, err +} + +// ParamResult represents the result of an operation which retrieves details +// about a particular configuration param. +type ParamResult struct { + gophercloud.Result +} + +// Extract will retrieve a param from an operation result. +func (r ParamResult) Extract() (*Param, error) { + var s *Param + err := r.ExtractInto(&s) + return s, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/configurations/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/configurations/testing/doc.go new file mode 100644 index 0000000000..60c997a2b3 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/configurations/testing/doc.go @@ -0,0 +1,2 @@ +// db_configurations_v1 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/configurations/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/configurations/testing/fixtures.go new file mode 100644 index 0000000000..3a9b562c4f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/configurations/testing/fixtures.go @@ -0,0 +1,159 @@ +package testing + +import ( + "fmt" + "time" + + "github.com/gophercloud/gophercloud/openstack/db/v1/configurations" +) + +var ( + timestamp = "2015-11-12T14:22:42Z" + timeVal, _ = time.Parse(time.RFC3339, timestamp) +) + +var singleConfigJSON = ` +{ + "created": "` + timestamp + `", + "datastore_name": "mysql", + "datastore_version_id": "b00000b0-00b0-0b00-00b0-000b000000bb", + "datastore_version_name": "5.6", + "description": "example_description", + "id": "005a8bb7-a8df-40ee-b0b7-fc144641abc2", + "name": "example-configuration-name", + "updated": "` + timestamp + `" +} +` + +var singleConfigWithValuesJSON = ` +{ + "created": "` + timestamp + `", + "datastore_name": "mysql", + "datastore_version_id": "b00000b0-00b0-0b00-00b0-000b000000bb", + "datastore_version_name": "5.6", + "description": "example description", + "id": "005a8bb7-a8df-40ee-b0b7-fc144641abc2", + "instance_count": 0, + "name": "example-configuration-name", + "updated": "` + timestamp + `", + "values": { + "collation_server": "latin1_swedish_ci", + "connect_timeout": 120 + } +} +` + +var ( + ListConfigsJSON = fmt.Sprintf(`{"configurations": [%s]}`, singleConfigJSON) + GetConfigJSON = fmt.Sprintf(`{"configuration": %s}`, singleConfigJSON) + CreateConfigJSON = fmt.Sprintf(`{"configuration": %s}`, singleConfigWithValuesJSON) +) + +var CreateReq = ` +{ + "configuration": { + "datastore": { + "type": "a00000a0-00a0-0a00-00a0-000a000000aa", + "version": "b00000b0-00b0-0b00-00b0-000b000000bb" + }, + "description": "example description", + "name": "example-configuration-name", + "values": { + "collation_server": "latin1_swedish_ci", + "connect_timeout": 120 + } + } +} +` + +var UpdateReq = ` +{ + "configuration": { + "values": { + "connect_timeout": 300 + } + } +} +` + +var ListInstancesJSON = ` +{ + "instances": [ + { + "id": "d4603f69-ec7e-4e9b-803f-600b9205576f", + "name": "json_rack_instance" + } + ] +} +` + +var ListParamsJSON = ` +{ + "configuration-parameters": [ + { + "max": 1, + "min": 0, + "name": "innodb_file_per_table", + "restart_required": true, + "type": "integer" + }, + { + "max": 4294967296, + "min": 0, + "name": "key_buffer_size", + "restart_required": false, + "type": "integer" + }, + { + "max": 65535, + "min": 2, + "name": "connect_timeout", + "restart_required": false, + "type": "integer" + }, + { + "max": 4294967296, + "min": 0, + "name": "join_buffer_size", + "restart_required": false, + "type": "integer" + } + ] +} +` + +var GetParamJSON = ` +{ + "max": 1, + "min": 0, + "name": "innodb_file_per_table", + "restart_required": true, + "type": "integer" +} +` + +var ExampleConfig = configurations.Config{ + Created: timeVal, + DatastoreName: "mysql", + DatastoreVersionID: "b00000b0-00b0-0b00-00b0-000b000000bb", + DatastoreVersionName: "5.6", + Description: "example_description", + ID: "005a8bb7-a8df-40ee-b0b7-fc144641abc2", + Name: "example-configuration-name", + Updated: timeVal, +} + +var ExampleConfigWithValues = configurations.Config{ + Created: timeVal, + DatastoreName: "mysql", + DatastoreVersionID: "b00000b0-00b0-0b00-00b0-000b000000bb", + DatastoreVersionName: "5.6", + Description: "example description", + ID: "005a8bb7-a8df-40ee-b0b7-fc144641abc2", + Name: "example-configuration-name", + Updated: timeVal, + Values: map[string]interface{}{ + "collation_server": "latin1_swedish_ci", + "connect_timeout": float64(120), + }, +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/configurations/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/configurations/testing/requests_test.go new file mode 100644 index 0000000000..643f363426 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/configurations/testing/requests_test.go @@ -0,0 +1,237 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/db/v1/configurations" + "github.com/gophercloud/gophercloud/openstack/db/v1/instances" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" + "github.com/gophercloud/gophercloud/testhelper/fixture" +) + +var ( + configID = "{configID}" + _baseURL = "/configurations" + resURL = _baseURL + "/" + configID + + dsID = "{datastoreID}" + versionID = "{versionID}" + paramID = "{paramID}" + dsParamListURL = "/datastores/" + dsID + "/versions/" + versionID + "/parameters" + dsParamGetURL = "/datastores/" + dsID + "/versions/" + versionID + "/parameters/" + paramID + globalParamListURL = "/datastores/versions/" + versionID + "/parameters" + globalParamGetURL = "/datastores/versions/" + versionID + "/parameters/" + paramID +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + fixture.SetupHandler(t, _baseURL, "GET", "", ListConfigsJSON, 200) + + count := 0 + err := configurations.List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := configurations.ExtractConfigs(page) + th.AssertNoErr(t, err) + + expected := []configurations.Config{ExampleConfig} + th.AssertDeepEquals(t, expected, actual) + + return true, nil + }) + + th.AssertEquals(t, 1, count) + th.AssertNoErr(t, err) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + fixture.SetupHandler(t, resURL, "GET", "", GetConfigJSON, 200) + + config, err := configurations.Get(fake.ServiceClient(), configID).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, &ExampleConfig, config) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + fixture.SetupHandler(t, _baseURL, "POST", CreateReq, CreateConfigJSON, 200) + + opts := configurations.CreateOpts{ + Datastore: &configurations.DatastoreOpts{ + Type: "a00000a0-00a0-0a00-00a0-000a000000aa", + Version: "b00000b0-00b0-0b00-00b0-000b000000bb", + }, + Description: "example description", + Name: "example-configuration-name", + Values: map[string]interface{}{ + "collation_server": "latin1_swedish_ci", + "connect_timeout": 120, + }, + } + + config, err := configurations.Create(fake.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, &ExampleConfigWithValues, config) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + fixture.SetupHandler(t, resURL, "PATCH", UpdateReq, "", 200) + + opts := configurations.UpdateOpts{ + Values: map[string]interface{}{ + "connect_timeout": 300, + }, + } + + err := configurations.Update(fake.ServiceClient(), configID, opts).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestReplace(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + fixture.SetupHandler(t, resURL, "PUT", UpdateReq, "", 202) + + opts := configurations.UpdateOpts{ + Values: map[string]interface{}{ + "connect_timeout": 300, + }, + } + + err := configurations.Replace(fake.ServiceClient(), configID, opts).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + fixture.SetupHandler(t, resURL, "DELETE", "", "", 202) + + err := configurations.Delete(fake.ServiceClient(), configID).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestListInstances(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + fixture.SetupHandler(t, resURL+"/instances", "GET", "", ListInstancesJSON, 200) + + expectedInstance := instances.Instance{ + ID: "d4603f69-ec7e-4e9b-803f-600b9205576f", + Name: "json_rack_instance", + } + + pages := 0 + err := configurations.ListInstances(fake.ServiceClient(), configID).EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := instances.ExtractInstances(page) + if err != nil { + return false, err + } + + th.AssertDeepEquals(t, actual, []instances.Instance{expectedInstance}) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, pages) +} + +func TestListDSParams(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + fixture.SetupHandler(t, dsParamListURL, "GET", "", ListParamsJSON, 200) + + pages := 0 + err := configurations.ListDatastoreParams(fake.ServiceClient(), dsID, versionID).EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := configurations.ExtractParams(page) + if err != nil { + return false, err + } + + expected := []configurations.Param{ + {Max: 1, Min: 0, Name: "innodb_file_per_table", RestartRequired: true, Type: "integer"}, + {Max: 4294967296, Min: 0, Name: "key_buffer_size", RestartRequired: false, Type: "integer"}, + {Max: 65535, Min: 2, Name: "connect_timeout", RestartRequired: false, Type: "integer"}, + {Max: 4294967296, Min: 0, Name: "join_buffer_size", RestartRequired: false, Type: "integer"}, + } + + th.AssertDeepEquals(t, actual, expected) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, pages) +} + +func TestGetDSParam(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + fixture.SetupHandler(t, dsParamGetURL, "GET", "", GetParamJSON, 200) + + param, err := configurations.GetDatastoreParam(fake.ServiceClient(), dsID, versionID, paramID).Extract() + th.AssertNoErr(t, err) + + expected := &configurations.Param{ + Max: 1, Min: 0, Name: "innodb_file_per_table", RestartRequired: true, Type: "integer", + } + + th.AssertDeepEquals(t, expected, param) +} + +func TestListGlobalParams(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + fixture.SetupHandler(t, globalParamListURL, "GET", "", ListParamsJSON, 200) + + pages := 0 + err := configurations.ListGlobalParams(fake.ServiceClient(), versionID).EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := configurations.ExtractParams(page) + if err != nil { + return false, err + } + + expected := []configurations.Param{ + {Max: 1, Min: 0, Name: "innodb_file_per_table", RestartRequired: true, Type: "integer"}, + {Max: 4294967296, Min: 0, Name: "key_buffer_size", RestartRequired: false, Type: "integer"}, + {Max: 65535, Min: 2, Name: "connect_timeout", RestartRequired: false, Type: "integer"}, + {Max: 4294967296, Min: 0, Name: "join_buffer_size", RestartRequired: false, Type: "integer"}, + } + + th.AssertDeepEquals(t, actual, expected) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, pages) +} + +func TestGetGlobalParam(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + fixture.SetupHandler(t, globalParamGetURL, "GET", "", GetParamJSON, 200) + + param, err := configurations.GetGlobalParam(fake.ServiceClient(), versionID, paramID).Extract() + th.AssertNoErr(t, err) + + expected := &configurations.Param{ + Max: 1, Min: 0, Name: "innodb_file_per_table", RestartRequired: true, Type: "integer", + } + + th.AssertDeepEquals(t, expected, param) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/configurations/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/configurations/urls.go new file mode 100644 index 0000000000..0a69253a72 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/configurations/urls.go @@ -0,0 +1,31 @@ +package configurations + +import "github.com/gophercloud/gophercloud" + +func baseURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("configurations") +} + +func resourceURL(c *gophercloud.ServiceClient, configID string) string { + return c.ServiceURL("configurations", configID) +} + +func instancesURL(c *gophercloud.ServiceClient, configID string) string { + return c.ServiceURL("configurations", configID, "instances") +} + +func listDSParamsURL(c *gophercloud.ServiceClient, datastoreID, versionID string) string { + return c.ServiceURL("datastores", datastoreID, "versions", versionID, "parameters") +} + +func getDSParamURL(c *gophercloud.ServiceClient, datastoreID, versionID, paramID string) string { + return c.ServiceURL("datastores", datastoreID, "versions", versionID, "parameters", paramID) +} + +func listGlobalParamsURL(c *gophercloud.ServiceClient, versionID string) string { + return c.ServiceURL("datastores", "versions", versionID, "parameters") +} + +func getGlobalParamURL(c *gophercloud.ServiceClient, versionID, paramID string) string { + return c.ServiceURL("datastores", "versions", versionID, "parameters", paramID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/databases/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/databases/doc.go new file mode 100644 index 0000000000..15275fe5d7 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/databases/doc.go @@ -0,0 +1,6 @@ +// Package flavors provides information and interaction with the database API +// resource in the OpenStack Database service. +// +// A database, when referred to here, refers to the database engine running on +// an instance. +package databases diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/databases/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/databases/requests.go new file mode 100644 index 0000000000..ef5394f9c6 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/databases/requests.go @@ -0,0 +1,89 @@ +package databases + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// CreateOptsBuilder builds create options +type CreateOptsBuilder interface { + ToDBCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is the struct responsible for configuring a database; often in +// the context of an instance. +type CreateOpts struct { + // Specifies the name of the database. Valid names can be composed + // of the following characters: letters (either case); numbers; these + // characters '@', '?', '#', ' ' but NEVER beginning a name string; '_' is + // permitted anywhere. Prohibited characters that are forbidden include: + // single quotes, double quotes, back quotes, semicolons, commas, backslashes, + // and forward slashes. + Name string `json:"name" required:"true"` + // Set of symbols and encodings. The default character set is + // "utf8". See http://dev.mysql.com/doc/refman/5.1/en/charset-mysql.html for + // supported character sets. + CharSet string `json:"character_set,omitempty"` + // Set of rules for comparing characters in a character set. The + // default value for collate is "utf8_general_ci". See + // http://dev.mysql.com/doc/refman/5.1/en/charset-mysql.html for supported + // collations. + Collate string `json:"collate,omitempty"` +} + +// ToMap is a helper function to convert individual DB create opt structures +// into sub-maps. +func (opts CreateOpts) ToMap() (map[string]interface{}, error) { + if len(opts.Name) > 64 { + err := gophercloud.ErrInvalidInput{} + err.Argument = "databases.CreateOpts.Name" + err.Value = opts.Name + err.Info = "Must be less than 64 chars long" + return nil, err + } + return gophercloud.BuildRequestBody(opts, "") +} + +// BatchCreateOpts allows for multiple databases to created and modified. +type BatchCreateOpts []CreateOpts + +// ToDBCreateMap renders a JSON map for creating DBs. +func (opts BatchCreateOpts) ToDBCreateMap() (map[string]interface{}, error) { + dbs := make([]map[string]interface{}, len(opts)) + for i, db := range opts { + dbMap, err := db.ToMap() + if err != nil { + return nil, err + } + dbs[i] = dbMap + } + return map[string]interface{}{"databases": dbs}, nil +} + +// Create will create a new database within the specified instance. If the +// specified instance does not exist, a 404 error will be returned. +func Create(client *gophercloud.ServiceClient, instanceID string, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToDBCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(baseURL(client, instanceID), &b, nil, nil) + return +} + +// List will list all of the databases for a specified instance. Note: this +// operation will only return user-defined databases; it will exclude system +// databases like "mysql", "information_schema", "lost+found" etc. +func List(client *gophercloud.ServiceClient, instanceID string) pagination.Pager { + return pagination.NewPager(client, baseURL(client, instanceID), func(r pagination.PageResult) pagination.Page { + return DBPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Delete will permanently delete the database within a specified instance. +// All contained data inside the database will also be permanently deleted. +func Delete(client *gophercloud.ServiceClient, instanceID, dbName string) (r DeleteResult) { + _, r.Err = client.Delete(dbURL(client, instanceID, dbName), nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/databases/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/databases/results.go new file mode 100644 index 0000000000..0479d0e6eb --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/databases/results.go @@ -0,0 +1,63 @@ +package databases + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Database represents a Database API resource. +type Database struct { + // Specifies the name of the MySQL database. + Name string + + // Set of symbols and encodings. The default character set is utf8. + CharSet string + + // Set of rules for comparing characters in a character set. The default + // value for collate is utf8_general_ci. + Collate string +} + +// CreateResult represents the result of a Create operation. +type CreateResult struct { + gophercloud.ErrResult +} + +// DeleteResult represents the result of a Delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// DBPage represents a single page of a paginated DB collection. +type DBPage struct { + pagination.LinkedPageBase +} + +// IsEmpty checks to see whether the collection is empty. +func (page DBPage) IsEmpty() (bool, error) { + dbs, err := ExtractDBs(page) + return len(dbs) == 0, err +} + +// NextPageURL will retrieve the next page URL. +func (page DBPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"databases_links"` + } + err := page.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// ExtractDBs will convert a generic pagination struct into a more +// relevant slice of DB structs. +func ExtractDBs(page pagination.Page) ([]Database, error) { + r := page.(DBPage) + var s struct { + Databases []Database `json:"databases"` + } + err := r.ExtractInto(&s) + return s.Databases, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/databases/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/databases/testing/doc.go new file mode 100644 index 0000000000..abdfab98b5 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/databases/testing/doc.go @@ -0,0 +1,2 @@ +// db_databases_v1 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/databases/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/databases/testing/fixtures.go new file mode 100644 index 0000000000..02b9ecc2a3 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/databases/testing/fixtures.go @@ -0,0 +1,61 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/testhelper/fixture" +) + +var ( + instanceID = "{instanceID}" + resURL = "/instances/" + instanceID + "/databases" +) + +var createDBsReq = ` +{ + "databases": [ + { + "character_set": "utf8", + "collate": "utf8_general_ci", + "name": "testingdb" + }, + { + "name": "sampledb" + } + ] +} +` + +var listDBsResp = ` +{ + "databases": [ + { + "name": "anotherexampledb" + }, + { + "name": "exampledb" + }, + { + "name": "nextround" + }, + { + "name": "sampledb" + }, + { + "name": "testingdb" + } + ] +} +` + +func HandleCreate(t *testing.T) { + fixture.SetupHandler(t, resURL, "POST", createDBsReq, "", 202) +} + +func HandleList(t *testing.T) { + fixture.SetupHandler(t, resURL, "GET", "", listDBsResp, 200) +} + +func HandleDelete(t *testing.T) { + fixture.SetupHandler(t, resURL+"/{dbName}", "DELETE", "", "", 202) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/databases/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/databases/testing/requests_test.go new file mode 100644 index 0000000000..a470ffa899 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/databases/testing/requests_test.go @@ -0,0 +1,67 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/db/v1/databases" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreate(t) + + opts := databases.BatchCreateOpts{ + databases.CreateOpts{Name: "testingdb", CharSet: "utf8", Collate: "utf8_general_ci"}, + databases.CreateOpts{Name: "sampledb"}, + } + + res := databases.Create(fake.ServiceClient(), instanceID, opts) + th.AssertNoErr(t, res.Err) +} + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleList(t) + + expectedDBs := []databases.Database{ + {Name: "anotherexampledb"}, + {Name: "exampledb"}, + {Name: "nextround"}, + {Name: "sampledb"}, + {Name: "testingdb"}, + } + + pages := 0 + err := databases.List(fake.ServiceClient(), instanceID).EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := databases.ExtractDBs(page) + if err != nil { + return false, err + } + + th.CheckDeepEquals(t, expectedDBs, actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + + if pages != 1 { + t.Errorf("Expected 1 page, saw %d", pages) + } +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDelete(t) + + err := databases.Delete(fake.ServiceClient(), instanceID, "{dbName}").ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/databases/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/databases/urls.go new file mode 100644 index 0000000000..aba42c9c87 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/databases/urls.go @@ -0,0 +1,11 @@ +package databases + +import "github.com/gophercloud/gophercloud" + +func baseURL(c *gophercloud.ServiceClient, instanceID string) string { + return c.ServiceURL("instances", instanceID, "databases") +} + +func dbURL(c *gophercloud.ServiceClient, instanceID, dbName string) string { + return c.ServiceURL("instances", instanceID, "databases", dbName) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/datastores/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/datastores/doc.go new file mode 100644 index 0000000000..ae14026b52 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/datastores/doc.go @@ -0,0 +1,3 @@ +// Package datastores provides information and interaction with the datastore +// API resource in the Rackspace Database service. +package datastores diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/datastores/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/datastores/requests.go new file mode 100644 index 0000000000..134e309076 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/datastores/requests.go @@ -0,0 +1,33 @@ +package datastores + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// List will list all available datastore types that instances can use. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, baseURL(client), func(r pagination.PageResult) pagination.Page { + return DatastorePage{pagination.SinglePageBase(r)} + }) +} + +// Get will retrieve the details of a specified datastore type. +func Get(client *gophercloud.ServiceClient, datastoreID string) (r GetResult) { + _, r.Err = client.Get(resourceURL(client, datastoreID), &r.Body, nil) + return +} + +// ListVersions will list all of the available versions for a specified +// datastore type. +func ListVersions(client *gophercloud.ServiceClient, datastoreID string) pagination.Pager { + return pagination.NewPager(client, versionsURL(client, datastoreID), func(r pagination.PageResult) pagination.Page { + return VersionPage{pagination.SinglePageBase(r)} + }) +} + +// GetVersion will retrieve the details of a specified datastore version. +func GetVersion(client *gophercloud.ServiceClient, datastoreID, versionID string) (r GetVersionResult) { + _, r.Err = client.Get(versionURL(client, datastoreID, versionID), &r.Body, nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/datastores/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/datastores/results.go new file mode 100644 index 0000000000..a6e27d2745 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/datastores/results.go @@ -0,0 +1,100 @@ +package datastores + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Version represents a version API resource. Multiple versions belong to a Datastore. +type Version struct { + ID string + Links []gophercloud.Link + Name string +} + +// Datastore represents a Datastore API resource. +type Datastore struct { + DefaultVersion string `json:"default_version"` + ID string + Links []gophercloud.Link + Name string + Versions []Version +} + +// DatastorePartial is a meta structure which is used in various API responses. +// It is a lightweight and truncated version of a full Datastore resource, +// offering details of the Version, Type and VersionID only. +type DatastorePartial struct { + Version string + Type string + VersionID string `json:"version_id"` +} + +// GetResult represents the result of a Get operation. +type GetResult struct { + gophercloud.Result +} + +// GetVersionResult represents the result of getting a version. +type GetVersionResult struct { + gophercloud.Result +} + +// DatastorePage represents a page of datastore resources. +type DatastorePage struct { + pagination.SinglePageBase +} + +// IsEmpty indicates whether a Datastore collection is empty. +func (r DatastorePage) IsEmpty() (bool, error) { + is, err := ExtractDatastores(r) + return len(is) == 0, err +} + +// ExtractDatastores retrieves a slice of datastore structs from a paginated +// collection. +func ExtractDatastores(r pagination.Page) ([]Datastore, error) { + var s struct { + Datastores []Datastore `json:"datastores"` + } + err := (r.(DatastorePage)).ExtractInto(&s) + return s.Datastores, err +} + +// Extract retrieves a single Datastore struct from an operation result. +func (r GetResult) Extract() (*Datastore, error) { + var s struct { + Datastore *Datastore `json:"datastore"` + } + err := r.ExtractInto(&s) + return s.Datastore, err +} + +// VersionPage represents a page of version resources. +type VersionPage struct { + pagination.SinglePageBase +} + +// IsEmpty indicates whether a collection of version resources is empty. +func (r VersionPage) IsEmpty() (bool, error) { + is, err := ExtractVersions(r) + return len(is) == 0, err +} + +// ExtractVersions retrieves a slice of versions from a paginated collection. +func ExtractVersions(r pagination.Page) ([]Version, error) { + var s struct { + Versions []Version `json:"versions"` + } + err := (r.(VersionPage)).ExtractInto(&s) + return s.Versions, err +} + +// Extract retrieves a single Version struct from an operation result. +func (r GetVersionResult) Extract() (*Version, error) { + var s struct { + Version *Version `json:"version"` + } + err := r.ExtractInto(&s) + return s.Version, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/datastores/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/datastores/testing/doc.go new file mode 100644 index 0000000000..8f06f86c1d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/datastores/testing/doc.go @@ -0,0 +1,2 @@ +// db_datastores_v1 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/datastores/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/datastores/testing/fixtures.go new file mode 100644 index 0000000000..3b82646342 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/datastores/testing/fixtures.go @@ -0,0 +1,101 @@ +package testing + +import ( + "fmt" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/db/v1/datastores" +) + +const version1JSON = ` +{ + "id": "b00000b0-00b0-0b00-00b0-000b000000bb", + "links": [ + { + "href": "https://10.240.28.38:8779/v1.0/1234/datastores/versions/b00000b0-00b0-0b00-00b0-000b000000bb", + "rel": "self" + }, + { + "href": "https://10.240.28.38:8779/datastores/versions/b00000b0-00b0-0b00-00b0-000b000000bb", + "rel": "bookmark" + } + ], + "name": "5.1" +} +` + +const version2JSON = ` +{ + "id": "c00000b0-00c0-0c00-00c0-000b000000cc", + "links": [ + { + "href": "https://10.240.28.38:8779/v1.0/1234/datastores/versions/c00000b0-00c0-0c00-00c0-000b000000cc", + "rel": "self" + }, + { + "href": "https://10.240.28.38:8779/datastores/versions/c00000b0-00c0-0c00-00c0-000b000000cc", + "rel": "bookmark" + } + ], + "name": "5.2" +} +` + +var versionsJSON = fmt.Sprintf(`"versions": [%s, %s]`, version1JSON, version2JSON) + +var singleDSJSON = fmt.Sprintf(` +{ + "default_version": "c00000b0-00c0-0c00-00c0-000b000000cc", + "id": "10000000-0000-0000-0000-000000000001", + "links": [ + { + "href": "https://10.240.28.38:8779/v1.0/1234/datastores/10000000-0000-0000-0000-000000000001", + "rel": "self" + }, + { + "href": "https://10.240.28.38:8779/datastores/10000000-0000-0000-0000-000000000001", + "rel": "bookmark" + } + ], + "name": "mysql", + %s +} +`, versionsJSON) + +var ( + ListDSResp = fmt.Sprintf(`{"datastores":[%s]}`, singleDSJSON) + GetDSResp = fmt.Sprintf(`{"datastore":%s}`, singleDSJSON) + ListVersionsResp = fmt.Sprintf(`{%s}`, versionsJSON) + GetVersionResp = fmt.Sprintf(`{"version":%s}`, version1JSON) +) + +var ExampleVersion1 = datastores.Version{ + ID: "b00000b0-00b0-0b00-00b0-000b000000bb", + Links: []gophercloud.Link{ + {Rel: "self", Href: "https://10.240.28.38:8779/v1.0/1234/datastores/versions/b00000b0-00b0-0b00-00b0-000b000000bb"}, + {Rel: "bookmark", Href: "https://10.240.28.38:8779/datastores/versions/b00000b0-00b0-0b00-00b0-000b000000bb"}, + }, + Name: "5.1", +} + +var exampleVersion2 = datastores.Version{ + ID: "c00000b0-00c0-0c00-00c0-000b000000cc", + Links: []gophercloud.Link{ + {Rel: "self", Href: "https://10.240.28.38:8779/v1.0/1234/datastores/versions/c00000b0-00c0-0c00-00c0-000b000000cc"}, + {Rel: "bookmark", Href: "https://10.240.28.38:8779/datastores/versions/c00000b0-00c0-0c00-00c0-000b000000cc"}, + }, + Name: "5.2", +} + +var ExampleVersions = []datastores.Version{ExampleVersion1, exampleVersion2} + +var ExampleDatastore = datastores.Datastore{ + DefaultVersion: "c00000b0-00c0-0c00-00c0-000b000000cc", + ID: "10000000-0000-0000-0000-000000000001", + Links: []gophercloud.Link{ + {Rel: "self", Href: "https://10.240.28.38:8779/v1.0/1234/datastores/10000000-0000-0000-0000-000000000001"}, + {Rel: "bookmark", Href: "https://10.240.28.38:8779/datastores/10000000-0000-0000-0000-000000000001"}, + }, + Name: "mysql", + Versions: ExampleVersions, +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/datastores/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/datastores/testing/requests_test.go new file mode 100644 index 0000000000..b505726d3f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/datastores/testing/requests_test.go @@ -0,0 +1,79 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/db/v1/datastores" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" + "github.com/gophercloud/gophercloud/testhelper/fixture" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + fixture.SetupHandler(t, "/datastores", "GET", "", ListDSResp, 200) + + pages := 0 + + err := datastores.List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := datastores.ExtractDatastores(page) + if err != nil { + return false, err + } + + th.CheckDeepEquals(t, []datastores.Datastore{ExampleDatastore}, actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, pages) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + fixture.SetupHandler(t, "/datastores/{dsID}", "GET", "", GetDSResp, 200) + + ds, err := datastores.Get(fake.ServiceClient(), "{dsID}").Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, &ExampleDatastore, ds) +} + +func TestListVersions(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + fixture.SetupHandler(t, "/datastores/{dsID}/versions", "GET", "", ListVersionsResp, 200) + + pages := 0 + + err := datastores.ListVersions(fake.ServiceClient(), "{dsID}").EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := datastores.ExtractVersions(page) + if err != nil { + return false, err + } + + th.CheckDeepEquals(t, ExampleVersions, actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, pages) +} + +func TestGetVersion(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + fixture.SetupHandler(t, "/datastores/{dsID}/versions/{versionID}", "GET", "", GetVersionResp, 200) + + ds, err := datastores.GetVersion(fake.ServiceClient(), "{dsID}", "{versionID}").Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, &ExampleVersion1, ds) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/datastores/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/datastores/urls.go new file mode 100644 index 0000000000..06d1b3daec --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/datastores/urls.go @@ -0,0 +1,19 @@ +package datastores + +import "github.com/gophercloud/gophercloud" + +func baseURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("datastores") +} + +func resourceURL(c *gophercloud.ServiceClient, dsID string) string { + return c.ServiceURL("datastores", dsID) +} + +func versionsURL(c *gophercloud.ServiceClient, dsID string) string { + return c.ServiceURL("datastores", dsID, "versions") +} + +func versionURL(c *gophercloud.ServiceClient, dsID, versionID string) string { + return c.ServiceURL("datastores", dsID, "versions", versionID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/flavors/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/flavors/doc.go new file mode 100644 index 0000000000..4d281d562f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/flavors/doc.go @@ -0,0 +1,7 @@ +// Package flavors provides information and interaction with the flavor API +// resource in the OpenStack Database service. +// +// A flavor is an available hardware configuration for a database instance. +// Each flavor has a unique combination of disk space, memory capacity and +// priority for CPU time. +package flavors diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/flavors/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/flavors/requests.go new file mode 100644 index 0000000000..7fac56ae60 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/flavors/requests.go @@ -0,0 +1,21 @@ +package flavors + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// List will list all available hardware flavors that an instance can use. The +// operation is identical to the one supported by the Nova API, but without the +// "disk" property. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page { + return FlavorPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get will retrieve information for a specified hardware flavor. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/flavors/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/flavors/results.go new file mode 100644 index 0000000000..0ba515ce3e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/flavors/results.go @@ -0,0 +1,71 @@ +package flavors + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// GetResult temporarily holds the response from a Get call. +type GetResult struct { + gophercloud.Result +} + +// Extract provides access to the individual Flavor returned by the Get function. +func (r GetResult) Extract() (*Flavor, error) { + var s struct { + Flavor *Flavor `json:"flavor"` + } + err := r.ExtractInto(&s) + return s.Flavor, err +} + +// Flavor records represent (virtual) hardware configurations for server resources in a region. +type Flavor struct { + // The flavor's unique identifier. + // Contains 0 if the ID is not an integer. + ID int `json:"id"` + + // The RAM capacity for the flavor. + RAM int `json:"ram"` + + // The Name field provides a human-readable moniker for the flavor. + Name string `json:"name"` + + // Links to access the flavor. + Links []gophercloud.Link + + // The flavor's unique identifier as a string + StrID string `json:"str_id"` +} + +// FlavorPage contains a single page of the response from a List call. +type FlavorPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines if a page contains any results. +func (page FlavorPage) IsEmpty() (bool, error) { + flavors, err := ExtractFlavors(page) + return len(flavors) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to the next page of results. +func (page FlavorPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"flavors_links"` + } + err := page.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// ExtractFlavors provides access to the list of flavors in a page acquired from the List operation. +func ExtractFlavors(r pagination.Page) ([]Flavor, error) { + var s struct { + Flavors []Flavor `json:"flavors"` + } + err := (r.(FlavorPage)).ExtractInto(&s) + return s.Flavors, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/flavors/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/flavors/testing/doc.go new file mode 100644 index 0000000000..08092661c5 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/flavors/testing/doc.go @@ -0,0 +1,2 @@ +// db_flavors_v1 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/flavors/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/flavors/testing/fixtures.go new file mode 100644 index 0000000000..9c323b80c7 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/flavors/testing/fixtures.go @@ -0,0 +1,52 @@ +package testing + +import ( + "fmt" + "testing" + + "github.com/gophercloud/gophercloud/testhelper/fixture" +) + +const flavor = ` +{ + "id": %s, + "links": [ + { + "href": "https://openstack.example.com/v1.0/1234/flavors/%s", + "rel": "self" + }, + { + "href": "https://openstack.example.com/flavors/%s", + "rel": "bookmark" + } + ], + "name": "%s", + "ram": %d, + "str_id": "%s" +} +` + +var ( + flavorID = "{flavorID}" + _baseURL = "/flavors" + resURL = "/flavors/" + flavorID +) + +var ( + flavor1 = fmt.Sprintf(flavor, "1", "1", "1", "m1.tiny", 512, "1") + flavor2 = fmt.Sprintf(flavor, "2", "2", "2", "m1.small", 1024, "2") + flavor3 = fmt.Sprintf(flavor, "3", "3", "3", "m1.medium", 2048, "3") + flavor4 = fmt.Sprintf(flavor, "4", "4", "4", "m1.large", 4096, "4") + flavor5 = fmt.Sprintf(flavor, "null", "d1", "d1", "ds512M", 512, "d1") + + listFlavorsResp = fmt.Sprintf(`{"flavors":[%s, %s, %s, %s, %s]}`, flavor1, flavor2, flavor3, flavor4, flavor5) + getFlavorResp = fmt.Sprintf(`{"flavor": %s}`, flavor1) +) + +func HandleList(t *testing.T) { + fixture.SetupHandler(t, _baseURL, "GET", "", listFlavorsResp, 200) +} + +func HandleGet(t *testing.T) { + fixture.SetupHandler(t, resURL, "GET", "", getFlavorResp, 200) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/flavors/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/flavors/testing/requests_test.go new file mode 100644 index 0000000000..e8b580aef6 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/flavors/testing/requests_test.go @@ -0,0 +1,107 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/db/v1/flavors" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestListFlavors(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleList(t) + + pages := 0 + err := flavors.List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := flavors.ExtractFlavors(page) + if err != nil { + return false, err + } + + expected := []flavors.Flavor{ + { + ID: 1, + Name: "m1.tiny", + RAM: 512, + Links: []gophercloud.Link{ + {Href: "https://openstack.example.com/v1.0/1234/flavors/1", Rel: "self"}, + {Href: "https://openstack.example.com/flavors/1", Rel: "bookmark"}, + }, + StrID: "1", + }, + { + ID: 2, + Name: "m1.small", + RAM: 1024, + Links: []gophercloud.Link{ + {Href: "https://openstack.example.com/v1.0/1234/flavors/2", Rel: "self"}, + {Href: "https://openstack.example.com/flavors/2", Rel: "bookmark"}, + }, + StrID: "2", + }, + { + ID: 3, + Name: "m1.medium", + RAM: 2048, + Links: []gophercloud.Link{ + {Href: "https://openstack.example.com/v1.0/1234/flavors/3", Rel: "self"}, + {Href: "https://openstack.example.com/flavors/3", Rel: "bookmark"}, + }, + StrID: "3", + }, + { + ID: 4, + Name: "m1.large", + RAM: 4096, + Links: []gophercloud.Link{ + {Href: "https://openstack.example.com/v1.0/1234/flavors/4", Rel: "self"}, + {Href: "https://openstack.example.com/flavors/4", Rel: "bookmark"}, + }, + StrID: "4", + }, + { + ID: 0, + Name: "ds512M", + RAM: 512, + Links: []gophercloud.Link{ + {Href: "https://openstack.example.com/v1.0/1234/flavors/d1", Rel: "self"}, + {Href: "https://openstack.example.com/flavors/d1", Rel: "bookmark"}, + }, + StrID: "d1", + }, + } + + th.AssertDeepEquals(t, expected, actual) + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, pages) +} + +func TestGetFlavor(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGet(t) + + actual, err := flavors.Get(fake.ServiceClient(), flavorID).Extract() + th.AssertNoErr(t, err) + + expected := &flavors.Flavor{ + ID: 1, + Name: "m1.tiny", + RAM: 512, + Links: []gophercloud.Link{ + {Href: "https://openstack.example.com/v1.0/1234/flavors/1", Rel: "self"}, + }, + StrID: "1", + } + + th.AssertDeepEquals(t, expected, actual) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/flavors/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/flavors/urls.go new file mode 100644 index 0000000000..a24301b186 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/flavors/urls.go @@ -0,0 +1,11 @@ +package flavors + +import "github.com/gophercloud/gophercloud" + +func getURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id) +} + +func listURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("flavors") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/instances/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/instances/doc.go new file mode 100644 index 0000000000..dc5c90f955 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/instances/doc.go @@ -0,0 +1,7 @@ +// Package instances provides information and interaction with the instance API +// resource in the OpenStack Database service. +// +// A database instance is an isolated database environment with compute and +// storage resources in a single tenant environment on a shared physical host +// machine. +package instances diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/instances/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/instances/requests.go new file mode 100644 index 0000000000..f8afb7364d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/instances/requests.go @@ -0,0 +1,205 @@ +package instances + +import ( + "github.com/gophercloud/gophercloud" + db "github.com/gophercloud/gophercloud/openstack/db/v1/databases" + "github.com/gophercloud/gophercloud/openstack/db/v1/users" + "github.com/gophercloud/gophercloud/pagination" +) + +// CreateOptsBuilder is the top-level interface for create options. +type CreateOptsBuilder interface { + ToInstanceCreateMap() (map[string]interface{}, error) +} + +// DatastoreOpts represents the configuration for how an instance stores data. +type DatastoreOpts struct { + Version string `json:"version"` + Type string `json:"type"` +} + +// ToMap converts a DatastoreOpts to a map[string]string (for a request body) +func (opts DatastoreOpts) ToMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "") +} + +// NetworkOpts is used within CreateOpts to control a new server's network attachments. +type NetworkOpts struct { + // UUID of a nova-network to attach to the newly provisioned server. + // Required unless Port is provided. + UUID string `json:"net-id,omitempty"` + + // Port of a neutron network to attach to the newly provisioned server. + // Required unless UUID is provided. + Port string `json:"port-id,omitempty"` + + // V4FixedIP [optional] specifies a fixed IPv4 address to be used on this network. + V4FixedIP string `json:"v4-fixed-ip,omitempty"` + + // V6FixedIP [optional] specifies a fixed IPv6 address to be used on this network. + V6FixedIP string `json:"v6-fixed-ip,omitempty"` +} + +// ToMap converts a NetworkOpts to a map[string]string (for a request body) +func (opts NetworkOpts) ToMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "") +} + +// CreateOpts is the struct responsible for configuring a new database instance. +type CreateOpts struct { + // Either the integer UUID (in string form) of the flavor, or its URI + // reference as specified in the response from the List() call. Required. + FlavorRef string + // Specifies the volume size in gigabytes (GB). The value must be between 1 + // and 300. Required. + Size int + // Name of the instance to create. The length of the name is limited to + // 255 characters and any characters are permitted. Optional. + Name string + // A slice of database information options. + Databases db.CreateOptsBuilder + // A slice of user information options. + Users users.CreateOptsBuilder + // Options to configure the type of datastore the instance will use. This is + // optional, and if excluded will default to MySQL. + Datastore *DatastoreOpts + // Networks dictates how this server will be attached to available networks. + Networks []NetworkOpts +} + +// ToInstanceCreateMap will render a JSON map. +func (opts CreateOpts) ToInstanceCreateMap() (map[string]interface{}, error) { + if opts.Size > 300 || opts.Size < 1 { + err := gophercloud.ErrInvalidInput{} + err.Argument = "instances.CreateOpts.Size" + err.Value = opts.Size + err.Info = "Size (GB) must be between 1-300" + return nil, err + } + + if opts.FlavorRef == "" { + return nil, gophercloud.ErrMissingInput{Argument: "instances.CreateOpts.FlavorRef"} + } + + instance := map[string]interface{}{ + "volume": map[string]int{"size": opts.Size}, + "flavorRef": opts.FlavorRef, + } + + if opts.Name != "" { + instance["name"] = opts.Name + } + if opts.Databases != nil { + dbs, err := opts.Databases.ToDBCreateMap() + if err != nil { + return nil, err + } + instance["databases"] = dbs["databases"] + } + if opts.Users != nil { + users, err := opts.Users.ToUserCreateMap() + if err != nil { + return nil, err + } + instance["users"] = users["users"] + } + if opts.Datastore != nil { + datastore, err := opts.Datastore.ToMap() + if err != nil { + return nil, err + } + instance["datastore"] = datastore + } + + if len(opts.Networks) > 0 { + networks := make([]map[string]interface{}, len(opts.Networks)) + for i, net := range opts.Networks { + var err error + networks[i], err = net.ToMap() + if err != nil { + return nil, err + } + } + instance["nics"] = networks + } + + return map[string]interface{}{"instance": instance}, nil +} + +// Create asynchronously provisions a new database instance. It requires the +// user to specify a flavor and a volume size. The API service then provisions +// the instance with the requested flavor and sets up a volume of the specified +// size, which is the storage for the database instance. +// +// Although this call only allows the creation of 1 instance per request, you +// can create an instance with multiple databases and users. The default +// binding for a MySQL instance is port 3306. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToInstanceCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(baseURL(client), &b, &r.Body, &gophercloud.RequestOpts{OkCodes: []int{200}}) + return +} + +// List retrieves the status and information for all database instances. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, baseURL(client), func(r pagination.PageResult) pagination.Page { + return InstancePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves the status and information for a specified database instance. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(resourceURL(client, id), &r.Body, nil) + return +} + +// Delete permanently destroys the database instance. +func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(resourceURL(client, id), nil) + return +} + +// EnableRootUser enables the login from any host for the root user and +// provides the user with a generated root password. +func EnableRootUser(client *gophercloud.ServiceClient, id string) (r EnableRootUserResult) { + _, r.Err = client.Post(userRootURL(client, id), nil, &r.Body, &gophercloud.RequestOpts{OkCodes: []int{200}}) + return +} + +// IsRootEnabled checks an instance to see if root access is enabled. It returns +// True if root user is enabled for the specified database instance or False +// otherwise. +func IsRootEnabled(client *gophercloud.ServiceClient, id string) (r IsRootEnabledResult) { + _, r.Err = client.Get(userRootURL(client, id), &r.Body, nil) + return +} + +// Restart will restart only the MySQL Instance. Restarting MySQL will +// erase any dynamic configuration settings that you have made within MySQL. +// The MySQL service will be unavailable until the instance restarts. +func Restart(client *gophercloud.ServiceClient, id string) (r ActionResult) { + b := map[string]interface{}{"restart": struct{}{}} + _, r.Err = client.Post(actionURL(client, id), &b, nil, nil) + return +} + +// Resize changes the memory size of the instance, assuming a valid +// flavorRef is provided. It will also restart the MySQL service. +func Resize(client *gophercloud.ServiceClient, id, flavorRef string) (r ActionResult) { + b := map[string]interface{}{"resize": map[string]string{"flavorRef": flavorRef}} + _, r.Err = client.Post(actionURL(client, id), &b, nil, nil) + return +} + +// ResizeVolume will resize the attached volume for an instance. It supports +// only increasing the volume size and does not support decreasing the size. +// The volume size is in gigabytes (GB) and must be an integer. +func ResizeVolume(client *gophercloud.ServiceClient, id string, size int) (r ActionResult) { + b := map[string]interface{}{"resize": map[string]interface{}{"volume": map[string]int{"size": size}}} + _, r.Err = client.Post(actionURL(client, id), &b, nil, nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/instances/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/instances/results.go new file mode 100644 index 0000000000..6bfde15030 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/instances/results.go @@ -0,0 +1,181 @@ +package instances + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/db/v1/datastores" + "github.com/gophercloud/gophercloud/openstack/db/v1/users" + "github.com/gophercloud/gophercloud/pagination" +) + +// Volume represents information about an attached volume for a database instance. +type Volume struct { + // The size in GB of the volume + Size int + + Used float64 +} + +// Flavor represents (virtual) hardware configurations for server resources in a region. +type Flavor struct { + // The flavor's unique identifier. + ID string + // Links to access the flavor. + Links []gophercloud.Link +} + +// Instance represents a remote MySQL instance. +type Instance struct { + // Indicates the datetime that the instance was created + Created time.Time `json:"-"` + + // Indicates the most recent datetime that the instance was updated. + Updated time.Time `json:"-"` + + // Indicates the hardware flavor the instance uses. + Flavor Flavor + + // A DNS-resolvable hostname associated with the database instance (rather + // than an IPv4 address). Since the hostname always resolves to the correct + // IP address of the database instance, this relieves the user from the task + // of maintaining the mapping. Note that although the IP address may likely + // change on resizing, migrating, and so forth, the hostname always resolves + // to the correct database instance. + Hostname string + + // The IP addresses associated with the database instance + // Is empty if the instance has a hostname + IP []string + + // Indicates the unique identifier for the instance resource. + ID string + + // Exposes various links that reference the instance resource. + Links []gophercloud.Link + + // The human-readable name of the instance. + Name string + + // The build status of the instance. + Status string + + // Information about the attached volume of the instance. + Volume Volume + + // Indicates how the instance stores data. + Datastore datastores.DatastorePartial +} + +func (r *Instance) UnmarshalJSON(b []byte) error { + type tmp Instance + var s struct { + tmp + Created gophercloud.JSONRFC3339NoZ `json:"created"` + Updated gophercloud.JSONRFC3339NoZ `json:"updated"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Instance(s.tmp) + + r.Created = time.Time(s.Created) + r.Updated = time.Time(s.Updated) + + return nil +} + +type commonResult struct { + gophercloud.Result +} + +// CreateResult represents the result of a Create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a Get operation. +type GetResult struct { + commonResult +} + +// DeleteResult represents the result of a Delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// Extract will extract an Instance from various result structs. +func (r commonResult) Extract() (*Instance, error) { + var s struct { + Instance *Instance `json:"instance"` + } + err := r.ExtractInto(&s) + return s.Instance, err +} + +// InstancePage represents a single page of a paginated instance collection. +type InstancePage struct { + pagination.LinkedPageBase +} + +// IsEmpty checks to see whether the collection is empty. +func (page InstancePage) IsEmpty() (bool, error) { + instances, err := ExtractInstances(page) + return len(instances) == 0, err +} + +// NextPageURL will retrieve the next page URL. +func (page InstancePage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"instances_links"` + } + err := page.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// ExtractInstances will convert a generic pagination struct into a more +// relevant slice of Instance structs. +func ExtractInstances(r pagination.Page) ([]Instance, error) { + var s struct { + Instances []Instance `json:"instances"` + } + err := (r.(InstancePage)).ExtractInto(&s) + return s.Instances, err +} + +// EnableRootUserResult represents the result of an operation to enable the root user. +type EnableRootUserResult struct { + gophercloud.Result +} + +// Extract will extract root user information from a UserRootResult. +func (r EnableRootUserResult) Extract() (*users.User, error) { + var s struct { + User *users.User `json:"user"` + } + err := r.ExtractInto(&s) + return s.User, err +} + +// ActionResult represents the result of action requests, such as: restarting +// an instance service, resizing its memory allocation, and resizing its +// attached volume size. +type ActionResult struct { + gophercloud.ErrResult +} + +// IsRootEnabledResult is the result of a call to IsRootEnabled. To see if +// root is enabled, call the type's Extract method. +type IsRootEnabledResult struct { + gophercloud.Result +} + +// Extract is used to extract the data from a IsRootEnabledResult. +func (r IsRootEnabledResult) Extract() (bool, error) { + return r.Body.(map[string]interface{})["rootEnabled"] == true, r.Err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/instances/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/instances/testing/doc.go new file mode 100644 index 0000000000..386ac58aef --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/instances/testing/doc.go @@ -0,0 +1,2 @@ +// db_instances_v1 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/instances/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/instances/testing/fixtures.go new file mode 100644 index 0000000000..9347ee15be --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/instances/testing/fixtures.go @@ -0,0 +1,169 @@ +package testing + +import ( + "fmt" + "testing" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/db/v1/datastores" + "github.com/gophercloud/gophercloud/openstack/db/v1/instances" + "github.com/gophercloud/gophercloud/testhelper/fixture" +) + +var ( + timestamp = "2015-11-12T14:22:42" + timeVal, _ = time.Parse(gophercloud.RFC3339NoZ, timestamp) +) + +var instance = ` +{ + "created": "` + timestamp + `", + "datastore": { + "type": "mysql", + "version": "5.6" + }, + "flavor": { + "id": "1", + "links": [ + { + "href": "https://openstack.example.com/v1.0/1234/flavors/1", + "rel": "self" + }, + { + "href": "https://openstack.example.com/v1.0/1234/flavors/1", + "rel": "bookmark" + } + ] + }, + "links": [ + { + "href": "https://openstack.example.com/v1.0/1234/instances/1", + "rel": "self" + } + ], + "hostname": "e09ad9a3f73309469cf1f43d11e79549caf9acf2.openstack.example.com", + "id": "{instanceID}", + "name": "json_rack_instance", + "status": "BUILD", + "updated": "` + timestamp + `", + "volume": { + "size": 2 + } +} +` + +var createReq = ` +{ + "instance": { + "databases": [ + { + "character_set": "utf8", + "collate": "utf8_general_ci", + "name": "sampledb" + }, + { + "name": "nextround" + } + ], + "flavorRef": "1", + "name": "json_rack_instance", + "users": [ + { + "databases": [ + { + "name": "sampledb" + } + ], + "name": "demouser", + "password": "demopassword" + } + ], + "volume": { + "size": 2 + } + } +} +` + +var ( + instanceID = "{instanceID}" + rootURL = "/instances" + resURL = rootURL + "/" + instanceID + uRootURL = resURL + "/root" + aURL = resURL + "/action" +) + +var ( + restartReq = `{"restart": {}}` + resizeReq = `{"resize": {"flavorRef": "2"}}` + resizeVolReq = `{"resize": {"volume": {"size": 4}}}` +) + +var ( + createResp = fmt.Sprintf(`{"instance": %s}`, instance) + listInstancesResp = fmt.Sprintf(`{"instances":[%s]}`, instance) + getInstanceResp = createResp + enableUserResp = `{"user":{"name":"root","password":"secretsecret"}}` + isUserEnabledResp = `{"rootEnabled":true}` +) + +var expectedInstance = instances.Instance{ + Created: timeVal, + Updated: timeVal, + Flavor: instances.Flavor{ + ID: "1", + Links: []gophercloud.Link{ + {Href: "https://openstack.example.com/v1.0/1234/flavors/1", Rel: "self"}, + {Href: "https://openstack.example.com/v1.0/1234/flavors/1", Rel: "bookmark"}, + }, + }, + Hostname: "e09ad9a3f73309469cf1f43d11e79549caf9acf2.openstack.example.com", + ID: instanceID, + Links: []gophercloud.Link{ + {Href: "https://openstack.example.com/v1.0/1234/instances/1", Rel: "self"}, + }, + Name: "json_rack_instance", + Status: "BUILD", + Volume: instances.Volume{Size: 2}, + Datastore: datastores.DatastorePartial{ + Type: "mysql", + Version: "5.6", + }, +} + +func HandleCreate(t *testing.T) { + fixture.SetupHandler(t, rootURL, "POST", createReq, createResp, 200) +} + +func HandleList(t *testing.T) { + fixture.SetupHandler(t, rootURL, "GET", "", listInstancesResp, 200) +} + +func HandleGet(t *testing.T) { + fixture.SetupHandler(t, resURL, "GET", "", getInstanceResp, 200) +} + +func HandleDelete(t *testing.T) { + fixture.SetupHandler(t, resURL, "DELETE", "", "", 202) +} + +func HandleEnableRoot(t *testing.T) { + fixture.SetupHandler(t, uRootURL, "POST", "", enableUserResp, 200) +} + +func HandleIsRootEnabled(t *testing.T) { + fixture.SetupHandler(t, uRootURL, "GET", "", isUserEnabledResp, 200) +} + +func HandleRestart(t *testing.T) { + fixture.SetupHandler(t, aURL, "POST", restartReq, "", 202) +} + +func HandleResize(t *testing.T) { + fixture.SetupHandler(t, aURL, "POST", resizeReq, "", 202) +} + +func HandleResizeVol(t *testing.T) { + fixture.SetupHandler(t, aURL, "POST", resizeVolReq, "", 202) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/instances/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/instances/testing/requests_test.go new file mode 100644 index 0000000000..e3c81e34c7 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/instances/testing/requests_test.go @@ -0,0 +1,134 @@ +package testing + +import ( + "testing" + + db "github.com/gophercloud/gophercloud/openstack/db/v1/databases" + "github.com/gophercloud/gophercloud/openstack/db/v1/instances" + "github.com/gophercloud/gophercloud/openstack/db/v1/users" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreate(t) + + opts := instances.CreateOpts{ + Name: "json_rack_instance", + FlavorRef: "1", + Databases: db.BatchCreateOpts{ + {CharSet: "utf8", Collate: "utf8_general_ci", Name: "sampledb"}, + {Name: "nextround"}, + }, + Users: users.BatchCreateOpts{ + { + Name: "demouser", + Password: "demopassword", + Databases: db.BatchCreateOpts{ + {Name: "sampledb"}, + }, + }, + }, + Size: 2, + } + + instance, err := instances.Create(fake.ServiceClient(), opts).Extract() + + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, &expectedInstance, instance) +} + +func TestInstanceList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleList(t) + + pages := 0 + err := instances.List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := instances.ExtractInstances(page) + if err != nil { + return false, err + } + + th.CheckDeepEquals(t, []instances.Instance{expectedInstance}, actual) + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, pages) +} + +func TestGetInstance(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGet(t) + + instance, err := instances.Get(fake.ServiceClient(), instanceID).Extract() + + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, &expectedInstance, instance) +} + +func TestDeleteInstance(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDelete(t) + + res := instances.Delete(fake.ServiceClient(), instanceID) + th.AssertNoErr(t, res.Err) +} + +func TestEnableRootUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleEnableRoot(t) + + expected := &users.User{Name: "root", Password: "secretsecret"} + user, err := instances.EnableRootUser(fake.ServiceClient(), instanceID).Extract() + + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expected, user) +} + +func TestIsRootEnabled(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleIsRootEnabled(t) + + isEnabled, err := instances.IsRootEnabled(fake.ServiceClient(), instanceID).Extract() + + th.AssertNoErr(t, err) + th.AssertEquals(t, true, isEnabled) +} + +func TestRestart(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleRestart(t) + + res := instances.Restart(fake.ServiceClient(), instanceID) + th.AssertNoErr(t, res.Err) +} + +func TestResize(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleResize(t) + + res := instances.Resize(fake.ServiceClient(), instanceID, "2") + th.AssertNoErr(t, res.Err) +} + +func TestResizeVolume(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleResizeVol(t) + + res := instances.ResizeVolume(fake.ServiceClient(), instanceID, 4) + th.AssertNoErr(t, res.Err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/instances/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/instances/urls.go new file mode 100644 index 0000000000..76d1ca56d8 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/instances/urls.go @@ -0,0 +1,19 @@ +package instances + +import "github.com/gophercloud/gophercloud" + +func baseURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("instances") +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("instances", id) +} + +func userRootURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("instances", id, "root") +} + +func actionURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("instances", id, "action") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/users/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/users/doc.go new file mode 100644 index 0000000000..cf07832f34 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/users/doc.go @@ -0,0 +1,3 @@ +// Package users provides information and interaction with the user API +// resource in the OpenStack Database service. +package users diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/users/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/users/requests.go new file mode 100644 index 0000000000..d342de3447 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/users/requests.go @@ -0,0 +1,91 @@ +package users + +import ( + "github.com/gophercloud/gophercloud" + db "github.com/gophercloud/gophercloud/openstack/db/v1/databases" + "github.com/gophercloud/gophercloud/pagination" +) + +// CreateOptsBuilder is the top-level interface for creating JSON maps. +type CreateOptsBuilder interface { + ToUserCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is the struct responsible for configuring a new user; often in the +// context of an instance. +type CreateOpts struct { + // Specifies a name for the user. Valid names can be composed + // of the following characters: letters (either case); numbers; these + // characters '@', '?', '#', ' ' but NEVER beginning a name string; '_' is + // permitted anywhere. Prohibited characters that are forbidden include: + // single quotes, double quotes, back quotes, semicolons, commas, backslashes, + // and forward slashes. Spaces at the front or end of a user name are also + // not permitted. + Name string `json:"name" required:"true"` + // Specifies a password for the user. + Password string `json:"password" required:"true"` + // An array of databases that this user will connect to. The + // "name" field is the only requirement for each option. + Databases db.BatchCreateOpts `json:"databases,omitempty"` + // Specifies the host from which a user is allowed to connect to + // the database. Possible values are a string containing an IPv4 address or + // "%" to allow connecting from any host. Optional; the default is "%". + Host string `json:"host,omitempty"` +} + +// ToMap is a convenience function for creating sub-maps for individual users. +func (opts CreateOpts) ToMap() (map[string]interface{}, error) { + if opts.Name == "root" { + err := gophercloud.ErrInvalidInput{} + err.Argument = "users.CreateOpts.Name" + err.Value = "root" + err.Info = "root is a reserved user name and cannot be used" + return nil, err + } + return gophercloud.BuildRequestBody(opts, "") +} + +// BatchCreateOpts allows multiple users to be created at once. +type BatchCreateOpts []CreateOpts + +// ToUserCreateMap will generate a JSON map. +func (opts BatchCreateOpts) ToUserCreateMap() (map[string]interface{}, error) { + users := make([]map[string]interface{}, len(opts)) + for i, opt := range opts { + user, err := opt.ToMap() + if err != nil { + return nil, err + } + users[i] = user + } + return map[string]interface{}{"users": users}, nil +} + +// Create asynchronously provisions a new user for the specified database +// instance based on the configuration defined in CreateOpts. If databases are +// assigned for a particular user, the user will be granted all privileges +// for those specified databases. "root" is a reserved name and cannot be used. +func Create(client *gophercloud.ServiceClient, instanceID string, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToUserCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(baseURL(client, instanceID), &b, nil, nil) + return +} + +// List will list all the users associated with a specified database instance, +// along with their associated databases. This operation will not return any +// system users or administrators for a database. +func List(client *gophercloud.ServiceClient, instanceID string) pagination.Pager { + return pagination.NewPager(client, baseURL(client, instanceID), func(r pagination.PageResult) pagination.Page { + return UserPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Delete will permanently delete a user from a specified database instance. +func Delete(client *gophercloud.ServiceClient, instanceID, userName string) (r DeleteResult) { + _, r.Err = client.Delete(userURL(client, instanceID, userName), nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/users/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/users/results.go new file mode 100644 index 0000000000..d12a681bdf --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/users/results.go @@ -0,0 +1,62 @@ +package users + +import ( + "github.com/gophercloud/gophercloud" + db "github.com/gophercloud/gophercloud/openstack/db/v1/databases" + "github.com/gophercloud/gophercloud/pagination" +) + +// User represents a database user +type User struct { + // The user name + Name string + + // The user password + Password string + + // The databases associated with this user + Databases []db.Database +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + gophercloud.ErrResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// UserPage represents a single page of a paginated user collection. +type UserPage struct { + pagination.LinkedPageBase +} + +// IsEmpty checks to see whether the collection is empty. +func (page UserPage) IsEmpty() (bool, error) { + users, err := ExtractUsers(page) + return len(users) == 0, err +} + +// NextPageURL will retrieve the next page URL. +func (page UserPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"users_links"` + } + err := page.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// ExtractUsers will convert a generic pagination struct into a more +// relevant slice of User structs. +func ExtractUsers(r pagination.Page) ([]User, error) { + var s struct { + Users []User `json:"users"` + } + err := (r.(UserPage)).ExtractInto(&s) + return s.Users, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/users/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/users/testing/doc.go new file mode 100644 index 0000000000..3c98966e3a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/users/testing/doc.go @@ -0,0 +1,2 @@ +// db_users_v1 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/users/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/users/testing/fixtures.go new file mode 100644 index 0000000000..f49f46f93c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/users/testing/fixtures.go @@ -0,0 +1,37 @@ +package testing + +import ( + "fmt" + "testing" + + "github.com/gophercloud/gophercloud/testhelper/fixture" +) + +const user1 = ` +{"databases": [{"name": "databaseA"}],"name": "dbuser3"%s} +` + +const user2 = ` +{"databases": [{"name": "databaseB"},{"name": "databaseC"}],"name": "dbuser4"%s} +` + +var ( + instanceID = "{instanceID}" + _rootURL = "/instances/" + instanceID + "/users" + pUser1 = fmt.Sprintf(user1, `,"password":"secretsecret"`) + pUser2 = fmt.Sprintf(user2, `,"password":"secretsecret"`) + createReq = fmt.Sprintf(`{"users":[%s, %s]}`, pUser1, pUser2) + listResp = fmt.Sprintf(`{"users":[%s, %s]}`, fmt.Sprintf(user1, ""), fmt.Sprintf(user2, "")) +) + +func HandleCreate(t *testing.T) { + fixture.SetupHandler(t, _rootURL, "POST", createReq, "", 202) +} + +func HandleList(t *testing.T) { + fixture.SetupHandler(t, _rootURL, "GET", "", listResp, 200) +} + +func HandleDelete(t *testing.T) { + fixture.SetupHandler(t, _rootURL+"/{userName}", "DELETE", "", "", 202) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/users/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/users/testing/requests_test.go new file mode 100644 index 0000000000..952f245eb7 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/users/testing/requests_test.go @@ -0,0 +1,85 @@ +package testing + +import ( + "testing" + + db "github.com/gophercloud/gophercloud/openstack/db/v1/databases" + "github.com/gophercloud/gophercloud/openstack/db/v1/users" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreate(t) + + opts := users.BatchCreateOpts{ + { + Databases: db.BatchCreateOpts{ + db.CreateOpts{Name: "databaseA"}, + }, + Name: "dbuser3", + Password: "secretsecret", + }, + { + Databases: db.BatchCreateOpts{ + {Name: "databaseB"}, + {Name: "databaseC"}, + }, + Name: "dbuser4", + Password: "secretsecret", + }, + } + + res := users.Create(fake.ServiceClient(), instanceID, opts) + th.AssertNoErr(t, res.Err) +} + +func TestUserList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleList(t) + + expectedUsers := []users.User{ + { + Databases: []db.Database{ + db.Database{Name: "databaseA"}, + }, + Name: "dbuser3", + }, + { + Databases: []db.Database{ + {Name: "databaseB"}, + {Name: "databaseC"}, + }, + Name: "dbuser4", + }, + } + + pages := 0 + err := users.List(fake.ServiceClient(), instanceID).EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := users.ExtractUsers(page) + if err != nil { + return false, err + } + + th.CheckDeepEquals(t, expectedUsers, actual) + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, pages) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDelete(t) + + res := users.Delete(fake.ServiceClient(), instanceID, "{userName}") + th.AssertNoErr(t, res.Err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/users/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/users/urls.go new file mode 100644 index 0000000000..8c36a39b30 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/db/v1/users/urls.go @@ -0,0 +1,11 @@ +package users + +import "github.com/gophercloud/gophercloud" + +func baseURL(c *gophercloud.ServiceClient, instanceID string) string { + return c.ServiceURL("instances", instanceID, "users") +} + +func userURL(c *gophercloud.ServiceClient, instanceID, userName string) string { + return c.ServiceURL("instances", instanceID, "users", userName) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets/doc.go new file mode 100644 index 0000000000..82a0aed5d7 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets/doc.go @@ -0,0 +1,6 @@ +// Package recordsets provides information and interaction with the zone API +// resource for the OpenStack DNS service. +// +// For more information, see: +// http://developer.openstack.org/api-ref/dns/#recordsets +package recordsets diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets/requests.go new file mode 100644 index 0000000000..eab6bc926b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets/requests.go @@ -0,0 +1,157 @@ +package recordsets + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToRecordSetListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the server attributes you want to see returned. Marker and Limit are used +// for pagination. +// https://developer.openstack.org/api-ref/dns/ +type ListOpts struct { + // Integer value for the limit of values to return. + Limit int `q:"limit"` + + // UUID of the recordset at which you want to set a marker. + Marker string `q:"marker"` + + Data string `q:"data"` + Description string `q:"description"` + Name string `q:"name"` + SortDir string `q:"sort_dir"` + SortKey string `q:"sort_key"` + Status string `q:"status"` + TTL int `q:"ttl"` + Type string `q:"type"` + ZoneID string `q:"zone_id"` +} + +// ToRecordSetListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToRecordSetListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// ListByZone implements the recordset list request. +func ListByZone(client *gophercloud.ServiceClient, zoneID string, opts ListOptsBuilder) pagination.Pager { + url := baseURL(client, zoneID) + if opts != nil { + query, err := opts.ToRecordSetListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return RecordSetPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get implements the recordset get request. +func Get(client *gophercloud.ServiceClient, zoneID string, rrsetID string) (r GetResult) { + _, r.Err = client.Get(rrsetURL(client, zoneID, rrsetID), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional attributes to the Create request. +type CreateOptsBuilder interface { + ToRecordSetCreateMap() (map[string]interface{}, error) +} + +// CreateOpts specifies the base attributes that may be used to create a RecordSet. +type CreateOpts struct { + // Name is the name of the RecordSet. + Name string `json:"name" required:"true"` + + // Description is a description of the RecordSet. + Description string `json:"description,omitempty"` + + // Records are the DNS records of the RecordSet. + Records []string `json:"records,omitempty"` + + // TTL is the time to live of the RecordSet. + TTL int `json:"ttl,omitempty"` + + // Type is the RRTYPE of the RecordSet. + Type string `json:"type,omitempty"` +} + +// ToRecordSetCreateMap formats an CreateOpts structure into a request body. +func (opts CreateOpts) ToRecordSetCreateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + return b, nil +} + +// Create creates a recordset in a given zone. +func Create(client *gophercloud.ServiceClient, zoneID string, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToRecordSetCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(baseURL(client, zoneID), &b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{201, 202}, + }) + return +} + +// UpdateOptsBuilder allows extensions to add additional attributes to the Update request. +type UpdateOptsBuilder interface { + ToRecordSetUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts specifies the base attributes that may be updated on an existing RecordSet. +type UpdateOpts struct { + Description string `json:"description,omitempty"` + TTL int `json:"ttl,omitempty"` + Records []string `json:"records,omitempty"` +} + +// ToRecordSetUpdateMap formats an UpdateOpts structure into a request body. +func (opts UpdateOpts) ToRecordSetUpdateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + if opts.TTL > 0 { + b["ttl"] = opts.TTL + } else { + b["ttl"] = nil + } + + return b, nil +} + +// Update updates a recordset in a given zone +func Update(client *gophercloud.ServiceClient, zoneID string, rrsetID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToRecordSetUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(rrsetURL(client, zoneID, rrsetID), &b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 202}, + }) + return +} + +// Delete removes an existing RecordSet. +func Delete(client *gophercloud.ServiceClient, zoneID string, rrsetID string) (r DeleteResult) { + _, r.Err = client.Delete(rrsetURL(client, zoneID, rrsetID), &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets/results.go new file mode 100644 index 0000000000..fc9d9554da --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets/results.go @@ -0,0 +1,141 @@ +package recordsets + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// Extract interprets a GetResult, CreateResult or UpdateResult as a concrete RecordSet. +// An error is returned if the original call or the extraction failed. +func (r commonResult) Extract() (*RecordSet, error) { + var s *RecordSet + err := r.ExtractInto(&s) + return s, err +} + +// CreateResult is the deferred result of a Create call. +type CreateResult struct { + commonResult +} + +// GetResult is the deferred result of a Get call. +type GetResult struct { + commonResult +} + +// RecordSetPage is a single page of RecordSet results. +type RecordSetPage struct { + pagination.LinkedPageBase +} + +// UpdateResult is the deferred result of an Update call. +type UpdateResult struct { + commonResult +} + +// DeleteResult is the deferred result of an Delete call. +type DeleteResult struct { + gophercloud.ErrResult +} + +// IsEmpty returns true if the page contains no results. +func (r RecordSetPage) IsEmpty() (bool, error) { + s, err := ExtractRecordSets(r) + return len(s) == 0, err +} + +// ExtractRecordSets extracts a slice of RecordSets from a Collection acquired from List. +func ExtractRecordSets(r pagination.Page) ([]RecordSet, error) { + var s struct { + RecordSets []RecordSet `json:"recordsets"` + } + err := (r.(RecordSetPage)).ExtractInto(&s) + return s.RecordSets, err +} + +type RecordSet struct { + // ID is the unique ID of the recordset + ID string `json:"id"` + + // ZoneID is the ID of the zone the recordset belongs to. + ZoneID string `json:"zone_id"` + + // ProjectID is the ID of the project that owns the recordset. + ProjectID string `json:"project_id"` + + // Name is the name of the recordset. + Name string `json:"name"` + + // ZoneName is the name of the zone the recordset belongs to. + ZoneName string `json:"zone_name"` + + // Type is the RRTYPE of the recordset. + Type string `json:"type"` + + // Records are the DNS records of the recordset. + Records []string `json:"records"` + + // TTL is the time to live of the recordset. + TTL int `json:"ttl"` + + // Status is the status of the recordset. + Status string `json:"status"` + + // Action is the current action in progress of the recordset. + Action string `json:"action"` + + // Description is the description of the recordset. + Description string `json:"description"` + + // Version is the revision of the recordset. + Version int `json:"version"` + + // CreatedAt is the date when the recordset was created. + CreatedAt time.Time `json:"-"` + + // UpdatedAt is the date when the recordset was updated. + UpdatedAt time.Time `json:"-"` + + // Links includes HTTP references to the itself, + // useful for passing along to other APIs that might want a recordset reference. + Links []gophercloud.Link `json:"-"` +} + +func (r *RecordSet) UnmarshalJSON(b []byte) error { + type tmp RecordSet + var s struct { + tmp + CreatedAt gophercloud.JSONRFC3339MilliNoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339MilliNoZ `json:"updated_at"` + Links map[string]interface{} `json:"links"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = RecordSet(s.tmp) + + r.CreatedAt = time.Time(s.CreatedAt) + r.UpdatedAt = time.Time(s.UpdatedAt) + + if s.Links != nil { + for rel, href := range s.Links { + if v, ok := href.(string); ok { + link := gophercloud.Link{ + Rel: rel, + Href: v, + } + r.Links = append(r.Links, link) + } + } + } + + return err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets/testing/doc.go new file mode 100644 index 0000000000..f1361e129b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets/testing/doc.go @@ -0,0 +1,2 @@ +// dns recordsets v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets/testing/fixtures.go new file mode 100644 index 0000000000..24ca89c518 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets/testing/fixtures.go @@ -0,0 +1,377 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +// ListByZoneOutput is a sample response to a ListByZone call. +const ListByZoneOutput = ` +{ + "recordsets": [ + { + "description": "This is an example record set.", + "links": { + "self": "https://127.0.0.1:9001/v2/zones/2150b1bf-dee2-4221-9d85-11f7886fb15f/recordsets/f7b10e9b-0cae-4a91-b162-562bc6096648" + }, + "updated_at": null, + "records": [ + "10.1.0.2" + ], + "ttl": 3600, + "id": "f7b10e9b-0cae-4a91-b162-562bc6096648", + "name": "example.org.", + "project_id": "4335d1f0-f793-11e2-b778-0800200c9a66", + "zone_id": "2150b1bf-dee2-4221-9d85-11f7886fb15f", + "zone_name": "example.com.", + "created_at": "2014-10-24T19:59:44.000000", + "version": 1, + "type": "A", + "status": "PENDING", + "action": "CREATE" + }, + { + "description": "This is another example record set.", + "links": { + "self": "https://127.0.0.1:9001/v2/zones/2150b1bf-dee2-4221-9d85-11f7886fb15f/recordsets/7423aeaf-b354-4bd7-8aba-2e831567b478" + }, + "updated_at": "2017-03-04T14:29:07.000000", + "records": [ + "10.1.0.3", + "10.1.0.4" + ], + "ttl": 3600, + "id": "7423aeaf-b354-4bd7-8aba-2e831567b478", + "name": "foo.example.org.", + "project_id": "4335d1f0-f793-11e2-b778-0800200c9a66", + "zone_id": "2150b1bf-dee2-4221-9d85-11f7886fb15f", + "zone_name": "example.com.", + "created_at": "2014-10-24T19:59:44.000000", + "version": 1, + "type": "A", + "status": "PENDING", + "action": "CREATE" + } + ], + "links": { + "self": "http://127.0.0.1:9001/v2/zones/2150b1bf-dee2-4221-9d85-11f7886fb15f/recordsets" + }, + "metadata": { + "total_count": 2 + } +} +` + +// ListByZoneOutputLimited is a sample response to a ListByZone call with a requested limit. +const ListByZoneOutputLimited = ` +{ + "recordsets": [ + { + "description": "This is another example record set.", + "links": { + "self": "https://127.0.0.1:9001/v2/zones/2150b1bf-dee2-4221-9d85-11f7886fb15f/recordsets/7423aeaf-b354-4bd7-8aba-2e831567b478" + }, + "updated_at": "2017-03-04T14:29:07.000000", + "records": [ + "10.1.0.3", + "10.1.0.4" + ], + "ttl": 3600, + "id": "7423aeaf-b354-4bd7-8aba-2e831567b478", + "name": "foo.example.org.", + "project_id": "4335d1f0-f793-11e2-b778-0800200c9a66", + "zone_id": "2150b1bf-dee2-4221-9d85-11f7886fb15f", + "zone_name": "example.com.", + "created_at": "2014-10-24T19:59:44.000000", + "version": 1, + "type": "A", + "status": "PENDING", + "action": "CREATE" + } + ], + "links": { + "self": "http://127.0.0.1:9001/v2/zones/2150b1bf-dee2-4221-9d85-11f7886fb15f/recordsets?limit=1" + }, + "metadata": { + "total_count": 1 + } +} +` + +// GetOutput is a sample response to a Get call. +const GetOutput = ` +{ + "description": "This is an example record set.", + "links": { + "self": "https://127.0.0.1:9001/v2/zones/2150b1bf-dee2-4221-9d85-11f7886fb15f/recordsets/f7b10e9b-0cae-4a91-b162-562bc6096648" + }, + "updated_at": null, + "records": [ + "10.1.0.2" + ], + "ttl": 3600, + "id": "f7b10e9b-0cae-4a91-b162-562bc6096648", + "name": "example.org.", + "project_id": "4335d1f0-f793-11e2-b778-0800200c9a66", + "zone_id": "2150b1bf-dee2-4221-9d85-11f7886fb15f", + "zone_name": "example.com.", + "created_at": "2014-10-24T19:59:44.000000", + "version": 1, + "type": "A", + "status": "PENDING", + "action": "CREATE" +} +` + +// NextPageRequest is a sample request to test pagination. +const NextPageRequest = ` +{ + "links": { + "self": "http://127.0.0.1:9001/v2/zones/2150b1bf-dee2-4221-9d85-11f7886fb15f/recordsets?limit=1", + "next": "http://127.0.0.1:9001/v2/zones/2150b1bf-dee2-4221-9d85-11f7886fb15f/recordsets?limit=1&marker=f7b10e9b-0cae-4a91-b162-562bc6096648" + } +} +` + +// FirstRecordSet is the first result in ListByZoneOutput +var FirstRecordSetCreatedAt, _ = time.Parse(gophercloud.RFC3339MilliNoZ, "2014-10-24T19:59:44.000000") +var FirstRecordSet = recordsets.RecordSet{ + ID: "f7b10e9b-0cae-4a91-b162-562bc6096648", + Description: "This is an example record set.", + UpdatedAt: time.Time{}, + Records: []string{"10.1.0.2"}, + TTL: 3600, + Name: "example.org.", + ProjectID: "4335d1f0-f793-11e2-b778-0800200c9a66", + ZoneID: "2150b1bf-dee2-4221-9d85-11f7886fb15f", + ZoneName: "example.com.", + CreatedAt: FirstRecordSetCreatedAt, + Version: 1, + Type: "A", + Status: "PENDING", + Action: "CREATE", + Links: []gophercloud.Link{ + { + Rel: "self", + Href: "https://127.0.0.1:9001/v2/zones/2150b1bf-dee2-4221-9d85-11f7886fb15f/recordsets/f7b10e9b-0cae-4a91-b162-562bc6096648", + }, + }, +} + +// SecondRecordSet is the first result in ListByZoneOutput +var SecondRecordSetCreatedAt, _ = time.Parse(gophercloud.RFC3339MilliNoZ, "2014-10-24T19:59:44.000000") +var SecondRecordSetUpdatedAt, _ = time.Parse(gophercloud.RFC3339MilliNoZ, "2017-03-04T14:29:07.000000") +var SecondRecordSet = recordsets.RecordSet{ + ID: "7423aeaf-b354-4bd7-8aba-2e831567b478", + Description: "This is another example record set.", + UpdatedAt: SecondRecordSetUpdatedAt, + Records: []string{"10.1.0.3", "10.1.0.4"}, + TTL: 3600, + Name: "foo.example.org.", + ProjectID: "4335d1f0-f793-11e2-b778-0800200c9a66", + ZoneID: "2150b1bf-dee2-4221-9d85-11f7886fb15f", + ZoneName: "example.com.", + CreatedAt: SecondRecordSetCreatedAt, + Version: 1, + Type: "A", + Status: "PENDING", + Action: "CREATE", + Links: []gophercloud.Link{ + { + Rel: "self", + Href: "https://127.0.0.1:9001/v2/zones/2150b1bf-dee2-4221-9d85-11f7886fb15f/recordsets/7423aeaf-b354-4bd7-8aba-2e831567b478", + }, + }, +} + +// ExpectedRecordSetSlice is the slice of results that should be parsed +// from ListByZoneOutput, in the expected order. +var ExpectedRecordSetSlice = []recordsets.RecordSet{FirstRecordSet, SecondRecordSet} + +// ExpectedRecordSetSliceLimited is the slice of limited results that should be parsed +// from ListByZoneOutput. +var ExpectedRecordSetSliceLimited = []recordsets.RecordSet{SecondRecordSet} + +// HandleListByZoneSuccessfully configures the test server to respond to a ListByZone request. +func HandleListByZoneSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/zones/2150b1bf-dee2-4221-9d85-11f7886fb15f/recordsets", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "f7b10e9b-0cae-4a91-b162-562bc6096648": + fmt.Fprintf(w, ListByZoneOutputLimited) + case "": + fmt.Fprintf(w, ListByZoneOutput) + } + }) +} + +// HandleGetSuccessfully configures the test server to respond to a Get request. +func HandleGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/zones/2150b1bf-dee2-4221-9d85-11f7886fb15f/recordsets/f7b10e9b-0cae-4a91-b162-562bc6096648", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetOutput) + }) +} + +// CreateRecordSetRequest is a sample request to create a resource record. +const CreateRecordSetRequest = ` +{ + "name" : "example.org.", + "description" : "This is an example record set.", + "type" : "A", + "ttl" : 3600, + "records" : [ + "10.1.0.2" + ] +} +` + +// CreateRecordSetResponse is a sample response to a create request. +const CreateRecordSetResponse = ` +{ + "description": "This is an example record set.", + "links": { + "self": "https://127.0.0.1:9001/v2/zones/2150b1bf-dee2-4221-9d85-11f7886fb15f/recordsets/f7b10e9b-0cae-4a91-b162-562bc6096648" + }, + "updated_at": null, + "records": [ + "10.1.0.2" + ], + "ttl": 3600, + "id": "f7b10e9b-0cae-4a91-b162-562bc6096648", + "name": "example.org.", + "project_id": "4335d1f0-f793-11e2-b778-0800200c9a66", + "zone_id": "2150b1bf-dee2-4221-9d85-11f7886fb15f", + "zone_name": "example.com.", + "created_at": "2014-10-24T19:59:44.000000", + "version": 1, + "type": "A", + "status": "PENDING", + "action": "CREATE" +} +` + +// CreatedRecordSet is the expected created resource record. +var CreatedRecordSet = FirstRecordSet + +// HandleZoneCreationSuccessfully configures the test server to respond to a Create request. +func HandleCreateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/zones/2150b1bf-dee2-4221-9d85-11f7886fb15f/recordsets", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, CreateRecordSetRequest) + + w.WriteHeader(http.StatusCreated) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, CreateRecordSetResponse) + }) +} + +// UpdateRecordSetRequest is a sample request to update a record set. +const UpdateRecordSetRequest = ` +{ + "description" : "Updated description", + "ttl" : null, + "records" : [ + "10.1.0.2", + "10.1.0.3" + ] +} +` + +// UpdateRecordSetResponse is a sample response to an update request. +const UpdateRecordSetResponse = ` +{ + "description": "Updated description", + "links": { + "self": "https://127.0.0.1:9001/v2/zones/2150b1bf-dee2-4221-9d85-11f7886fb15f/recordsets/f7b10e9b-0cae-4a91-b162-562bc6096648" + }, + "updated_at": null, + "records": [ + "10.1.0.2", + "10.1.0.3" + ], + "ttl": 3600, + "id": "f7b10e9b-0cae-4a91-b162-562bc6096648", + "name": "example.org.", + "project_id": "4335d1f0-f793-11e2-b778-0800200c9a66", + "zone_id": "2150b1bf-dee2-4221-9d85-11f7886fb15f", + "zone_name": "example.com.", + "created_at": "2014-10-24T19:59:44.000000", + "version": 2, + "type": "A", + "status": "PENDING", + "action": "UPDATE" +} +` + +// HandleUpdateSuccessfully configures the test server to respond to an Update request. +func HandleUpdateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/zones/2150b1bf-dee2-4221-9d85-11f7886fb15f/recordsets/f7b10e9b-0cae-4a91-b162-562bc6096648", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, UpdateRecordSetRequest) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, UpdateRecordSetResponse) + }) +} + +// DeleteRecordSetResponse is a sample response to a delete request. +const DeleteRecordSetResponse = ` +{ + "description": "Updated description", + "links": { + "self": "https://127.0.0.1:9001/v2/zones/2150b1bf-dee2-4221-9d85-11f7886fb15f/recordsets/f7b10e9b-0cae-4a91-b162-562bc6096648" + }, + "updated_at": null, + "records": [ + "10.1.0.2", + "10.1.0.3", + ], + "ttl": null, + "id": "f7b10e9b-0cae-4a91-b162-562bc6096648", + "name": "example.org.", + "project_id": "4335d1f0-f793-11e2-b778-0800200c9a66", + "zone_id": "2150b1bf-dee2-4221-9d85-11f7886fb15f", + "zone_name": "example.com.", + "created_at": "2014-10-24T19:59:44.000000", + "version": 2, + "type": "A", + "status": "PENDING", + "action": "UPDATE" +} +` + +// HandleDeleteSuccessfully configures the test server to respond to an Delete request. +func HandleDeleteSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/zones/2150b1bf-dee2-4221-9d85-11f7886fb15f/recordsets/f7b10e9b-0cae-4a91-b162-562bc6096648", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusAccepted) + //w.Header().Add("Content-Type", "application/json") + //fmt.Fprintf(w, DeleteZoneResponse) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets/testing/requests_test.go new file mode 100644 index 0000000000..21630152ff --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets/testing/requests_test.go @@ -0,0 +1,145 @@ +package testing + +import ( + "encoding/json" + "testing" + + "github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestListByZone(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListByZoneSuccessfully(t) + + count := 0 + err := recordsets.ListByZone(client.ServiceClient(), "2150b1bf-dee2-4221-9d85-11f7886fb15f", nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := recordsets.ExtractRecordSets(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedRecordSetSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestListByZoneLimited(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListByZoneSuccessfully(t) + + count := 0 + listOpts := recordsets.ListOpts{ + Limit: 1, + Marker: "f7b10e9b-0cae-4a91-b162-562bc6096648", + } + err := recordsets.ListByZone(client.ServiceClient(), "2150b1bf-dee2-4221-9d85-11f7886fb15f", listOpts).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := recordsets.ExtractRecordSets(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedRecordSetSliceLimited, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestListByZoneAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListByZoneSuccessfully(t) + + allPages, err := recordsets.ListByZone(client.ServiceClient(), "2150b1bf-dee2-4221-9d85-11f7886fb15f", nil).AllPages() + th.AssertNoErr(t, err) + allRecordSets, err := recordsets.ExtractRecordSets(allPages) + th.AssertNoErr(t, err) + th.CheckEquals(t, 2, len(allRecordSets)) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetSuccessfully(t) + + actual, err := recordsets.Get(client.ServiceClient(), "2150b1bf-dee2-4221-9d85-11f7886fb15f", "f7b10e9b-0cae-4a91-b162-562bc6096648").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &FirstRecordSet, actual) +} + +func TestNextPageURL(t *testing.T) { + var page recordsets.RecordSetPage + var body map[string]interface{} + err := json.Unmarshal([]byte(NextPageRequest), &body) + if err != nil { + t.Fatalf("Error unmarshaling data into page body: %v", err) + } + page.Body = body + expected := "http://127.0.0.1:9001/v2/zones/2150b1bf-dee2-4221-9d85-11f7886fb15f/recordsets?limit=1&marker=f7b10e9b-0cae-4a91-b162-562bc6096648" + actual, err := page.NextPageURL() + th.AssertNoErr(t, err) + th.CheckEquals(t, expected, actual) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateSuccessfully(t) + + createOpts := recordsets.CreateOpts{ + Name: "example.org.", + Type: "A", + TTL: 3600, + Description: "This is an example record set.", + Records: []string{"10.1.0.2"}, + } + + actual, err := recordsets.Create(client.ServiceClient(), "2150b1bf-dee2-4221-9d85-11f7886fb15f", createOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &CreatedRecordSet, actual) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleUpdateSuccessfully(t) + + updateOpts := recordsets.UpdateOpts{ + TTL: 0, + Description: "Updated description", + Records: []string{"10.1.0.2", "10.1.0.3"}, + } + + UpdatedRecordSet := CreatedRecordSet + UpdatedRecordSet.Status = "PENDING" + UpdatedRecordSet.Action = "UPDATE" + UpdatedRecordSet.Description = "Updated description" + UpdatedRecordSet.Records = []string{"10.1.0.2", "10.1.0.3"} + UpdatedRecordSet.Version = 2 + + actual, err := recordsets.Update(client.ServiceClient(), UpdatedRecordSet.ZoneID, UpdatedRecordSet.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &UpdatedRecordSet, actual) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteSuccessfully(t) + + DeletedRecordSet := CreatedRecordSet + DeletedRecordSet.Status = "PENDING" + DeletedRecordSet.Action = "UPDATE" + DeletedRecordSet.Description = "Updated description" + DeletedRecordSet.Records = []string{"10.1.0.2", "10.1.0.3"} + DeletedRecordSet.Version = 2 + + err := recordsets.Delete(client.ServiceClient(), DeletedRecordSet.ZoneID, DeletedRecordSet.ID).ExtractErr() + th.AssertNoErr(t, err) + //th.CheckDeepEquals(t, &DeletedZone, actual) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets/urls.go new file mode 100644 index 0000000000..5ec18d1bb7 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets/urls.go @@ -0,0 +1,11 @@ +package recordsets + +import "github.com/gophercloud/gophercloud" + +func baseURL(c *gophercloud.ServiceClient, zoneID string) string { + return c.ServiceURL("zones", zoneID, "recordsets") +} + +func rrsetURL(c *gophercloud.ServiceClient, zoneID string, rrsetID string) string { + return c.ServiceURL("zones", zoneID, "recordsets", rrsetID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/zones/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/zones/doc.go new file mode 100644 index 0000000000..1302cb93d4 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/zones/doc.go @@ -0,0 +1,6 @@ +// Package tokens provides information and interaction with the zone API +// resource for the OpenStack DNS service. +// +// For more information, see: +// http://developer.openstack.org/api-ref/dns/#zone +package zones diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/zones/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/zones/requests.go new file mode 100644 index 0000000000..4160f80a97 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/zones/requests.go @@ -0,0 +1,162 @@ +package zones + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +type ListOptsBuilder interface { + ToZoneListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the server attributes you want to see returned. Marker and Limit are used +// for pagination. +// https://developer.openstack.org/api-ref/dns/ +type ListOpts struct { + // Integer value for the limit of values to return. + Limit int `q:"limit"` + + // UUID of the zone at which you want to set a marker. + Marker string `q:"marker"` + + Description string `q:"description"` + Email string `q:"email"` + Name string `q:"name"` + SortDir string `q:"sort_dir"` + SortKey string `q:"sort_key"` + Status string `q:"status"` + TTL int `q:"ttl"` + Type string `q:"type"` +} + +func (opts ListOpts) ToZoneListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := baseURL(client) + if opts != nil { + query, err := opts.ToZoneListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ZonePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get returns additional information about a zone, given its ID. +func Get(client *gophercloud.ServiceClient, zoneID string) (r GetResult) { + _, r.Err = client.Get(zoneURL(client, zoneID), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional attributes to the Update request. +type CreateOptsBuilder interface { + ToZoneCreateMap() (map[string]interface{}, error) +} + +// CreateOpts specifies the base attributes used to create a zone. +type CreateOpts struct { + // Attributes are settings that supply hints and filters for the zone. + Attributes map[string]string `json:"attributes,omitempty"` + + // Email contact of the zone. + Email string `json:"email,omitempty"` + + // Description of the zone. + Description string `json:"description,omitempty"` + + // Name of the zone. + Name string `json:"name" required:"true"` + + // Masters specifies zone masters if this is a secondary zone. + Masters []string `json:"masters,omitempty"` + + // TTL is the time to live of the zone. + TTL int `json:"-"` + + // Type specifies if this is a primary or secondary zone. + Type string `json:"type,omitempty"` +} + +// ToZoneCreateMap formats an CreateOpts structure into a request body. +func (opts CreateOpts) ToZoneCreateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + if opts.TTL > 0 { + b["ttl"] = opts.TTL + } + + return b, nil +} + +// Create a zone +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToZoneCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(baseURL(client), &b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{201, 202}, + }) + return +} + +// UpdateOptsBuilder allows extensions to add additional attributes to the Update request. +type UpdateOptsBuilder interface { + ToZoneUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts specifies the base attributes to update a zone. +type UpdateOpts struct { + Email string `json:"email,omitempty"` + TTL int `json:"-"` + Masters []string `json:"masters,omitempty"` + Description string `json:"description,omitempty"` +} + +// ToZoneUpdateMap formats an UpdateOpts structure into a request body. +func (opts UpdateOpts) ToZoneUpdateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + if opts.TTL > 0 { + b["ttl"] = opts.TTL + } + + return b, nil +} + +// Update a zone. +func Update(client *gophercloud.ServiceClient, zoneID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToZoneUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Patch(zoneURL(client, zoneID), &b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 202}, + }) + return +} + +// Delete a zone. +func Delete(client *gophercloud.ServiceClient, zoneID string) (r DeleteResult) { + _, r.Err = client.Delete(zoneURL(client, zoneID), &gophercloud.RequestOpts{ + OkCodes: []int{202}, + JSONResponse: &r.Body, + }) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/zones/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/zones/results.go new file mode 100644 index 0000000000..de8069567e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/zones/results.go @@ -0,0 +1,159 @@ +package zones + +import ( + "encoding/json" + "strconv" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// Extract interprets a GetResult, CreateResult or UpdateResult as a concrete Zone. +// An error is returned if the original call or the extraction failed. +func (r commonResult) Extract() (*Zone, error) { + var s *Zone + err := r.ExtractInto(&s) + return s, err +} + +// CreateResult is the deferred result of a Create call. +type CreateResult struct { + commonResult +} + +// GetResult is the deferred result of a Get call. +type GetResult struct { + commonResult +} + +// UpdateResult is the deferred result of an Update call. +type UpdateResult struct { + commonResult +} + +// DeleteResult is the deferred result of an Delete call. +type DeleteResult struct { + commonResult +} + +// ZonePage is a single page of Zone results. +type ZonePage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if the page contains no results. +func (r ZonePage) IsEmpty() (bool, error) { + s, err := ExtractZones(r) + return len(s) == 0, err +} + +// ExtractZones extracts a slice of Services from a Collection acquired from List. +func ExtractZones(r pagination.Page) ([]Zone, error) { + var s struct { + Zones []Zone `json:"zones"` + } + err := (r.(ZonePage)).ExtractInto(&s) + return s.Zones, err +} + +// Zone represents a DNS zone. +type Zone struct { + // ID uniquely identifies this zone amongst all other zones, including those not accessible to the current tenant. + ID string `json:"id"` + + // PoolID is the ID for the pool hosting this zone. + PoolID string `json:"pool_id"` + + // ProjectID identifies the project/tenant owning this resource. + ProjectID string `json:"project_id"` + + // Name is the DNS Name for the zone. + Name string `json:"name"` + + // Email for the zone. Used in SOA records for the zone. + Email string `json:"email"` + + // Description for this zone. + Description string `json:"description"` + + // TTL is the Time to Live for the zone. + TTL int `json:"ttl"` + + // Serial is the current serial number for the zone. + Serial int `json:"-"` + + // Status is the status of the resource. + Status string `json:"status"` + + // Action is the current action in progress on the resource. + Action string `json:"action"` + + // Version of the resource. + Version int `json:"version"` + + // Attributes for the zone. + Attributes map[string]string `json:"attributes"` + + // Type of zone. Primary is controlled by Designate. + // Secondary zones are slaved from another DNS Server. + // Defaults to Primary. + Type string `json:"type"` + + // Masters is the servers for slave servers to get DNS information from. + Masters []string `json:"masters"` + + // CreatedAt is the date when the zone was created. + CreatedAt time.Time `json:"-"` + + // UpdatedAt is the date when the last change was made to the zone. + UpdatedAt time.Time `json:"-"` + + // TransferredAt is the last time an update was retrieved from the master servers. + TransferredAt time.Time `json:"-"` + + // Links includes HTTP references to the itself, useful for passing along to other APIs that might want a server reference. + Links map[string]interface{} `json:"links"` +} + +func (r *Zone) UnmarshalJSON(b []byte) error { + type tmp Zone + var s struct { + tmp + CreatedAt gophercloud.JSONRFC3339MilliNoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339MilliNoZ `json:"updated_at"` + TransferredAt gophercloud.JSONRFC3339MilliNoZ `json:"transferred_at"` + Serial interface{} `json:"serial"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Zone(s.tmp) + + r.CreatedAt = time.Time(s.CreatedAt) + r.UpdatedAt = time.Time(s.UpdatedAt) + r.TransferredAt = time.Time(s.TransferredAt) + + switch t := s.Serial.(type) { + case float64: + r.Serial = int(t) + case string: + switch t { + case "": + r.Serial = 0 + default: + serial, err := strconv.ParseFloat(t, 64) + if err != nil { + return err + } + r.Serial = int(serial) + } + } + + return err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/zones/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/zones/testing/doc.go new file mode 100644 index 0000000000..54a0d217e0 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/zones/testing/doc.go @@ -0,0 +1,2 @@ +// dns_zones_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/zones/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/zones/testing/fixtures.go new file mode 100644 index 0000000000..55e401306d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/zones/testing/fixtures.go @@ -0,0 +1,302 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/dns/v2/zones" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +// List Output is a sample response to a List call. +const ListOutput = ` +{ + "links": { + "self": "http://example.com:9001/v2/zones" + }, + "metadata": { + "total_count": 2 + }, + "zones": [ + { + "id": "a86dba58-0043-4cc6-a1bb-69d5e86f3ca3", + "pool_id": "572ba08c-d929-4c70-8e42-03824bb24ca2", + "project_id": "4335d1f0-f793-11e2-b778-0800200c9a66", + "name": "example.org.", + "email": "joe@example.org", + "ttl": 7200, + "serial": 1404757531, + "status": "ACTIVE", + "action": "CREATE", + "description": "This is an example zone.", + "masters": [], + "type": "PRIMARY", + "transferred_at": null, + "version": 1, + "created_at": "2014-07-07T18:25:31.275934", + "updated_at": null, + "links": { + "self": "https://127.0.0.1:9001/v2/zones/a86dba58-0043-4cc6-a1bb-69d5e86f3ca3" + } + }, + { + "id": "34c4561c-9205-4386-9df5-167436f5a222", + "pool_id": "572ba08c-d929-4c70-8e42-03824bb24ca2", + "project_id": "4335d1f0-f793-11e2-b778-0800200c9a66", + "name": "foo.example.com.", + "email": "joe@foo.example.com", + "ttl": 7200, + "serial": 1488053571, + "status": "ACTIVE", + "action": "CREATE", + "description": "This is another example zone.", + "masters": ["example.com."], + "type": "PRIMARY", + "transferred_at": null, + "version": 1, + "created_at": "2014-07-07T18:25:31.275934", + "updated_at": "2015-02-25T20:23:01.234567", + "links": { + "self": "https://127.0.0.1:9001/v2/zones/34c4561c-9205-4386-9df5-167436f5a222" + } + } + ] +} +` + +// GetOutput is a sample response to a Get call. +const GetOutput = ` +{ + "id": "a86dba58-0043-4cc6-a1bb-69d5e86f3ca3", + "pool_id": "572ba08c-d929-4c70-8e42-03824bb24ca2", + "project_id": "4335d1f0-f793-11e2-b778-0800200c9a66", + "name": "example.org.", + "email": "joe@example.org", + "ttl": 7200, + "serial": 1404757531, + "status": "ACTIVE", + "action": "CREATE", + "description": "This is an example zone.", + "masters": [], + "type": "PRIMARY", + "transferred_at": null, + "version": 1, + "created_at": "2014-07-07T18:25:31.275934", + "updated_at": null, + "links": { + "self": "https://127.0.0.1:9001/v2/zones/a86dba58-0043-4cc6-a1bb-69d5e86f3ca3" + } +} +` + +// FirstZone is the first result in ListOutput +var FirstZoneCreatedAt, _ = time.Parse(gophercloud.RFC3339MilliNoZ, "2014-07-07T18:25:31.275934") +var FirstZone = zones.Zone{ + ID: "a86dba58-0043-4cc6-a1bb-69d5e86f3ca3", + PoolID: "572ba08c-d929-4c70-8e42-03824bb24ca2", + ProjectID: "4335d1f0-f793-11e2-b778-0800200c9a66", + Name: "example.org.", + Email: "joe@example.org", + TTL: 7200, + Serial: 1404757531, + Status: "ACTIVE", + Action: "CREATE", + Description: "This is an example zone.", + Masters: []string{}, + Type: "PRIMARY", + Version: 1, + CreatedAt: FirstZoneCreatedAt, + Links: map[string]interface{}{ + "self": "https://127.0.0.1:9001/v2/zones/a86dba58-0043-4cc6-a1bb-69d5e86f3ca3", + }, +} + +var SecondZoneCreatedAt, _ = time.Parse(gophercloud.RFC3339MilliNoZ, "2014-07-07T18:25:31.275934") +var SecondZoneUpdatedAt, _ = time.Parse(gophercloud.RFC3339MilliNoZ, "2015-02-25T20:23:01.234567") +var SecondZone = zones.Zone{ + ID: "34c4561c-9205-4386-9df5-167436f5a222", + PoolID: "572ba08c-d929-4c70-8e42-03824bb24ca2", + ProjectID: "4335d1f0-f793-11e2-b778-0800200c9a66", + Name: "foo.example.com.", + Email: "joe@foo.example.com", + TTL: 7200, + Serial: 1488053571, + Status: "ACTIVE", + Action: "CREATE", + Description: "This is another example zone.", + Masters: []string{"example.com."}, + Type: "PRIMARY", + Version: 1, + CreatedAt: SecondZoneCreatedAt, + UpdatedAt: SecondZoneUpdatedAt, + Links: map[string]interface{}{ + "self": "https://127.0.0.1:9001/v2/zones/34c4561c-9205-4386-9df5-167436f5a222", + }, +} + +// ExpectedZonesSlice is the slice of results that should be parsed +// from ListOutput, in the expected order. +var ExpectedZonesSlice = []zones.Zone{FirstZone, SecondZone} + +// HandleListSuccessfully configures the test server to respond to a List request. +func HandleListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/zones", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ListOutput) + }) +} + +// HandleGetSuccessfully configures the test server to respond to a List request. +func HandleGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/zones/a86dba58-0043-4cc6-a1bb-69d5e86f3ca3", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetOutput) + }) +} + +// CreateZoneRequest is a sample request to create a zone. +const CreateZoneRequest = ` +{ + "name": "example.org.", + "email": "joe@example.org", + "type": "PRIMARY", + "ttl": 7200, + "description": "This is an example zone." +} +` + +// CreateZoneResponse is a sample response to a create request. +const CreateZoneResponse = ` +{ + "id": "a86dba58-0043-4cc6-a1bb-69d5e86f3ca3", + "pool_id": "572ba08c-d929-4c70-8e42-03824bb24ca2", + "project_id": "4335d1f0-f793-11e2-b778-0800200c9a66", + "name": "example.org.", + "email": "joe@example.org", + "ttl": 7200, + "serial": 1404757531, + "status": "ACTIVE", + "action": "CREATE", + "description": "This is an example zone.", + "masters": [], + "type": "PRIMARY", + "transferred_at": null, + "version": 1, + "created_at": "2014-07-07T18:25:31.275934", + "updated_at": null, + "links": { + "self": "https://127.0.0.1:9001/v2/zones/a86dba58-0043-4cc6-a1bb-69d5e86f3ca3" + } +} +` + +// CreatedZone is the expected created zone +var CreatedZone = FirstZone + +// HandleZoneCreationSuccessfully configures the test server to respond to a Create request. +func HandleCreateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/zones", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, CreateZoneRequest) + + w.WriteHeader(http.StatusCreated) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, CreateZoneResponse) + }) +} + +// UpdateZoneRequest is a sample request to update a zone. +const UpdateZoneRequest = ` +{ + "ttl": 600, + "description": "Updated Description" +} +` + +// UpdateZoneResponse is a sample response to update a zone. +const UpdateZoneResponse = ` +{ + "id": "a86dba58-0043-4cc6-a1bb-69d5e86f3ca3", + "pool_id": "572ba08c-d929-4c70-8e42-03824bb24ca2", + "project_id": "4335d1f0-f793-11e2-b778-0800200c9a66", + "name": "example.org.", + "email": "joe@example.org", + "ttl": 600, + "serial": 1404757531, + "status": "PENDING", + "action": "UPDATE", + "description": "Updated Description", + "masters": [], + "type": "PRIMARY", + "transferred_at": null, + "version": 1, + "created_at": "2014-07-07T18:25:31.275934", + "updated_at": null, + "links": { + "self": "https://127.0.0.1:9001/v2/zones/a86dba58-0043-4cc6-a1bb-69d5e86f3ca3" + } +} +` + +// HandleZoneUpdateSuccessfully configures the test server to respond to an Update request. +func HandleUpdateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/zones/a86dba58-0043-4cc6-a1bb-69d5e86f3ca3", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PATCH") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, UpdateZoneRequest) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, UpdateZoneResponse) + }) +} + +// DeleteZoneResponse is a sample response to update a zone. +const DeleteZoneResponse = ` +{ + "id": "a86dba58-0043-4cc6-a1bb-69d5e86f3ca3", + "pool_id": "572ba08c-d929-4c70-8e42-03824bb24ca2", + "project_id": "4335d1f0-f793-11e2-b778-0800200c9a66", + "name": "example.org.", + "email": "joe@example.org", + "ttl": 600, + "serial": 1404757531, + "status": "PENDING", + "action": "DELETE", + "description": "Updated Description", + "masters": [], + "type": "PRIMARY", + "transferred_at": null, + "version": 1, + "created_at": "2014-07-07T18:25:31.275934", + "updated_at": null, + "links": { + "self": "https://127.0.0.1:9001/v2/zones/a86dba58-0043-4cc6-a1bb-69d5e86f3ca3" + } +} +` + +// HandleZoneDeleteSuccessfully configures the test server to respond to an Delete request. +func HandleDeleteSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/zones/a86dba58-0043-4cc6-a1bb-69d5e86f3ca3", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusAccepted) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, DeleteZoneResponse) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/zones/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/zones/testing/requests_test.go new file mode 100644 index 0000000000..412b34933f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/zones/testing/requests_test.go @@ -0,0 +1,105 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/dns/v2/zones" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListSuccessfully(t) + + count := 0 + err := zones.List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := zones.ExtractZones(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedZonesSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestListAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListSuccessfully(t) + + allPages, err := zones.List(client.ServiceClient(), nil).AllPages() + th.AssertNoErr(t, err) + allZones, err := zones.ExtractZones(allPages) + th.AssertNoErr(t, err) + th.CheckEquals(t, 2, len(allZones)) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetSuccessfully(t) + + actual, err := zones.Get(client.ServiceClient(), "a86dba58-0043-4cc6-a1bb-69d5e86f3ca3").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &FirstZone, actual) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateSuccessfully(t) + + createOpts := zones.CreateOpts{ + Name: "example.org.", + Email: "joe@example.org", + Type: "PRIMARY", + TTL: 7200, + Description: "This is an example zone.", + } + + actual, err := zones.Create(client.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &CreatedZone, actual) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleUpdateSuccessfully(t) + + updateOpts := zones.UpdateOpts{ + TTL: 600, + Description: "Updated Description", + } + + UpdatedZone := CreatedZone + UpdatedZone.Status = "PENDING" + UpdatedZone.Action = "UPDATE" + UpdatedZone.TTL = 600 + UpdatedZone.Description = "Updated Description" + + actual, err := zones.Update(client.ServiceClient(), UpdatedZone.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &UpdatedZone, actual) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteSuccessfully(t) + + DeletedZone := CreatedZone + DeletedZone.Status = "PENDING" + DeletedZone.Action = "DELETE" + DeletedZone.TTL = 600 + DeletedZone.Description = "Updated Description" + + actual, err := zones.Delete(client.ServiceClient(), DeletedZone.ID).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &DeletedZone, actual) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/zones/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/zones/urls.go new file mode 100644 index 0000000000..9bef705809 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/zones/urls.go @@ -0,0 +1,11 @@ +package zones + +import "github.com/gophercloud/gophercloud" + +func baseURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("zones") +} + +func zoneURL(c *gophercloud.ServiceClient, zoneID string) string { + return c.ServiceURL("zones", zoneID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/endpoint_location.go b/vendor/github.com/gophercloud/gophercloud/openstack/endpoint_location.go new file mode 100644 index 0000000000..ea37f5b271 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/endpoint_location.go @@ -0,0 +1,99 @@ +package openstack + +import ( + "github.com/gophercloud/gophercloud" + tokens2 "github.com/gophercloud/gophercloud/openstack/identity/v2/tokens" + tokens3 "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens" +) + +// V2EndpointURL discovers the endpoint URL for a specific service from a ServiceCatalog acquired +// during the v2 identity service. The specified EndpointOpts are used to identify a unique, +// unambiguous endpoint to return. It's an error both when multiple endpoints match the provided +// criteria and when none do. The minimum that can be specified is a Type, but you will also often +// need to specify a Name and/or a Region depending on what's available on your OpenStack +// deployment. +func V2EndpointURL(catalog *tokens2.ServiceCatalog, opts gophercloud.EndpointOpts) (string, error) { + // Extract Endpoints from the catalog entries that match the requested Type, Name if provided, and Region if provided. + var endpoints = make([]tokens2.Endpoint, 0, 1) + for _, entry := range catalog.Entries { + if (entry.Type == opts.Type) && (opts.Name == "" || entry.Name == opts.Name) { + for _, endpoint := range entry.Endpoints { + if opts.Region == "" || endpoint.Region == opts.Region { + endpoints = append(endpoints, endpoint) + } + } + } + } + + // Report an error if the options were ambiguous. + if len(endpoints) > 1 { + err := &ErrMultipleMatchingEndpointsV2{} + err.Endpoints = endpoints + return "", err + } + + // Extract the appropriate URL from the matching Endpoint. + for _, endpoint := range endpoints { + switch opts.Availability { + case gophercloud.AvailabilityPublic: + return gophercloud.NormalizeURL(endpoint.PublicURL), nil + case gophercloud.AvailabilityInternal: + return gophercloud.NormalizeURL(endpoint.InternalURL), nil + case gophercloud.AvailabilityAdmin: + return gophercloud.NormalizeURL(endpoint.AdminURL), nil + default: + err := &ErrInvalidAvailabilityProvided{} + err.Argument = "Availability" + err.Value = opts.Availability + return "", err + } + } + + // Report an error if there were no matching endpoints. + err := &gophercloud.ErrEndpointNotFound{} + return "", err +} + +// V3EndpointURL discovers the endpoint URL for a specific service from a Catalog acquired +// during the v3 identity service. The specified EndpointOpts are used to identify a unique, +// unambiguous endpoint to return. It's an error both when multiple endpoints match the provided +// criteria and when none do. The minimum that can be specified is a Type, but you will also often +// need to specify a Name and/or a Region depending on what's available on your OpenStack +// deployment. +func V3EndpointURL(catalog *tokens3.ServiceCatalog, opts gophercloud.EndpointOpts) (string, error) { + // Extract Endpoints from the catalog entries that match the requested Type, Interface, + // Name if provided, and Region if provided. + var endpoints = make([]tokens3.Endpoint, 0, 1) + for _, entry := range catalog.Entries { + if (entry.Type == opts.Type) && (opts.Name == "" || entry.Name == opts.Name) { + for _, endpoint := range entry.Endpoints { + if opts.Availability != gophercloud.AvailabilityAdmin && + opts.Availability != gophercloud.AvailabilityPublic && + opts.Availability != gophercloud.AvailabilityInternal { + err := &ErrInvalidAvailabilityProvided{} + err.Argument = "Availability" + err.Value = opts.Availability + return "", err + } + if (opts.Availability == gophercloud.Availability(endpoint.Interface)) && + (opts.Region == "" || endpoint.Region == opts.Region) { + endpoints = append(endpoints, endpoint) + } + } + } + } + + // Report an error if the options were ambiguous. + if len(endpoints) > 1 { + return "", ErrMultipleMatchingEndpointsV3{Endpoints: endpoints} + } + + // Extract the URL from the matching Endpoint. + for _, endpoint := range endpoints { + return gophercloud.NormalizeURL(endpoint.URL), nil + } + + // Report an error if there were no matching endpoints. + err := &gophercloud.ErrEndpointNotFound{} + return "", err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/errors.go b/vendor/github.com/gophercloud/gophercloud/openstack/errors.go new file mode 100644 index 0000000000..df410b1c61 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/errors.go @@ -0,0 +1,71 @@ +package openstack + +import ( + "fmt" + + "github.com/gophercloud/gophercloud" + tokens2 "github.com/gophercloud/gophercloud/openstack/identity/v2/tokens" + tokens3 "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens" +) + +// ErrEndpointNotFound is the error when no suitable endpoint can be found +// in the user's catalog +type ErrEndpointNotFound struct{ gophercloud.BaseError } + +func (e ErrEndpointNotFound) Error() string { + return "No suitable endpoint could be found in the service catalog." +} + +// ErrInvalidAvailabilityProvided is the error when an invalid endpoint +// availability is provided +type ErrInvalidAvailabilityProvided struct{ gophercloud.ErrInvalidInput } + +func (e ErrInvalidAvailabilityProvided) Error() string { + return fmt.Sprintf("Unexpected availability in endpoint query: %s", e.Value) +} + +// ErrMultipleMatchingEndpointsV2 is the error when more than one endpoint +// for the given options is found in the v2 catalog +type ErrMultipleMatchingEndpointsV2 struct { + gophercloud.BaseError + Endpoints []tokens2.Endpoint +} + +func (e ErrMultipleMatchingEndpointsV2) Error() string { + return fmt.Sprintf("Discovered %d matching endpoints: %#v", len(e.Endpoints), e.Endpoints) +} + +// ErrMultipleMatchingEndpointsV3 is the error when more than one endpoint +// for the given options is found in the v3 catalog +type ErrMultipleMatchingEndpointsV3 struct { + gophercloud.BaseError + Endpoints []tokens3.Endpoint +} + +func (e ErrMultipleMatchingEndpointsV3) Error() string { + return fmt.Sprintf("Discovered %d matching endpoints: %#v", len(e.Endpoints), e.Endpoints) +} + +// ErrNoAuthURL is the error when the OS_AUTH_URL environment variable is not +// found +type ErrNoAuthURL struct{ gophercloud.ErrInvalidInput } + +func (e ErrNoAuthURL) Error() string { + return "Environment variable OS_AUTH_URL needs to be set." +} + +// ErrNoUsername is the error when the OS_USERNAME environment variable is not +// found +type ErrNoUsername struct{ gophercloud.ErrInvalidInput } + +func (e ErrNoUsername) Error() string { + return "Environment variable OS_USERNAME needs to be set." +} + +// ErrNoPassword is the error when the OS_PASSWORD environment variable is not +// found +type ErrNoPassword struct{ gophercloud.ErrInvalidInput } + +func (e ErrNoPassword) Error() string { + return "Environment variable OS_PASSWORD needs to be set." +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/admin/roles/docs.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/admin/roles/docs.go new file mode 100644 index 0000000000..8954178716 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/admin/roles/docs.go @@ -0,0 +1,16 @@ +// Package roles provides functionality to interact with and control roles on +// the API. +// +// A role represents a personality that a user can assume when performing a +// specific set of operations. If a role includes a set of rights and +// privileges, a user assuming that role inherits those rights and privileges. +// +// When a token is generated, the list of roles that user can assume is returned +// back to them. Services that are being called by that user determine how they +// interpret the set of roles a user has and to which operations or resources +// each role grants access. +// +// It is up to individual services such as Compute or Image to assign meaning +// to these roles. As far as the Identity service is concerned, a role is an +// arbitrary name assigned by the user. +package roles diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/admin/roles/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/admin/roles/requests.go new file mode 100644 index 0000000000..50228c9066 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/admin/roles/requests.go @@ -0,0 +1,32 @@ +package roles + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// List is the operation responsible for listing all available global roles +// that a user can adopt. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, rootURL(client), func(r pagination.PageResult) pagination.Page { + return RolePage{pagination.SinglePageBase(r)} + }) +} + +// AddUser is the operation responsible for assigning a particular role to +// a user. This is confined to the scope of the user's tenant - so the tenant +// ID is a required argument. +func AddUser(client *gophercloud.ServiceClient, tenantID, userID, roleID string) (r UserRoleResult) { + _, r.Err = client.Put(userRoleURL(client, tenantID, userID, roleID), nil, nil, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + return +} + +// DeleteUser is the operation responsible for deleting a particular role +// from a user. This is confined to the scope of the user's tenant - so the +// tenant ID is a required argument. +func DeleteUser(client *gophercloud.ServiceClient, tenantID, userID, roleID string) (r UserRoleResult) { + _, r.Err = client.Delete(userRoleURL(client, tenantID, userID, roleID), nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/admin/roles/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/admin/roles/results.go new file mode 100644 index 0000000000..28de6bb410 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/admin/roles/results.go @@ -0,0 +1,47 @@ +package roles + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Role represents an API role resource. +type Role struct { + // The unique ID for the role. + ID string + + // The human-readable name of the role. + Name string + + // The description of the role. + Description string + + // The associated service for this role. + ServiceID string +} + +// RolePage is a single page of a user Role collection. +type RolePage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a page of Tenants contains any results. +func (r RolePage) IsEmpty() (bool, error) { + users, err := ExtractRoles(r) + return len(users) == 0, err +} + +// ExtractRoles returns a slice of roles contained in a single page of results. +func ExtractRoles(r pagination.Page) ([]Role, error) { + var s struct { + Roles []Role `json:"roles"` + } + err := (r.(RolePage)).ExtractInto(&s) + return s.Roles, err +} + +// UserRoleResult represents the result of either an AddUserRole or +// a DeleteUserRole operation. +type UserRoleResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/admin/roles/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/admin/roles/testing/doc.go new file mode 100644 index 0000000000..70ba643262 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/admin/roles/testing/doc.go @@ -0,0 +1,2 @@ +// identity_extensions_admin_roles_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/admin/roles/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/admin/roles/testing/fixtures.go new file mode 100644 index 0000000000..498c1611d0 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/admin/roles/testing/fixtures.go @@ -0,0 +1,48 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +func MockListRoleResponse(t *testing.T) { + th.Mux.HandleFunc("/OS-KSADM/roles", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "roles": [ + { + "id": "123", + "name": "compute:admin", + "description": "Nova Administrator" + } + ] +} + `) + }) +} + +func MockAddUserRoleResponse(t *testing.T) { + th.Mux.HandleFunc("/tenants/{tenant_id}/users/{user_id}/roles/OS-KSADM/{role_id}", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusCreated) + }) +} + +func MockDeleteUserRoleResponse(t *testing.T) { + th.Mux.HandleFunc("/tenants/{tenant_id}/users/{user_id}/roles/OS-KSADM/{role_id}", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/admin/roles/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/admin/roles/testing/requests_test.go new file mode 100644 index 0000000000..8cf539557c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/admin/roles/testing/requests_test.go @@ -0,0 +1,65 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/admin/roles" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestRole(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockListRoleResponse(t) + + count := 0 + + err := roles.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := roles.ExtractRoles(page) + if err != nil { + t.Errorf("Failed to extract users: %v", err) + return false, err + } + + expected := []roles.Role{ + { + ID: "123", + Name: "compute:admin", + Description: "Nova Administrator", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, count) +} + +func TestAddUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockAddUserRoleResponse(t) + + err := roles.AddUser(client.ServiceClient(), "{tenant_id}", "{user_id}", "{role_id}").ExtractErr() + + th.AssertNoErr(t, err) +} + +func TestDeleteUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockDeleteUserRoleResponse(t) + + err := roles.DeleteUser(client.ServiceClient(), "{tenant_id}", "{user_id}", "{role_id}").ExtractErr() + + th.AssertNoErr(t, err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/admin/roles/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/admin/roles/urls.go new file mode 100644 index 0000000000..e4661e8bf1 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/admin/roles/urls.go @@ -0,0 +1,21 @@ +package roles + +import "github.com/gophercloud/gophercloud" + +const ( + ExtPath = "OS-KSADM" + RolePath = "roles" + UserPath = "users" +) + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(ExtPath, RolePath, id) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(ExtPath, RolePath) +} + +func userRoleURL(c *gophercloud.ServiceClient, tenantID, userID, roleID string) string { + return c.ServiceURL("tenants", tenantID, UserPath, userID, RolePath, ExtPath, roleID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/delegate.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/delegate.go new file mode 100644 index 0000000000..cf6cc816da --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/delegate.go @@ -0,0 +1,46 @@ +package extensions + +import ( + "github.com/gophercloud/gophercloud" + common "github.com/gophercloud/gophercloud/openstack/common/extensions" + "github.com/gophercloud/gophercloud/pagination" +) + +// ExtensionPage is a single page of Extension results. +type ExtensionPage struct { + common.ExtensionPage +} + +// IsEmpty returns true if the current page contains at least one Extension. +func (page ExtensionPage) IsEmpty() (bool, error) { + is, err := ExtractExtensions(page) + return len(is) == 0, err +} + +// ExtractExtensions accepts a Page struct, specifically an ExtensionPage struct, and extracts the +// elements into a slice of Extension structs. +func ExtractExtensions(page pagination.Page) ([]common.Extension, error) { + // Identity v2 adds an intermediate "values" object. + var s struct { + Extensions struct { + Values []common.Extension `json:"values"` + } `json:"extensions"` + } + err := page.(ExtensionPage).ExtractInto(&s) + return s.Extensions.Values, err +} + +// Get retrieves information for a specific extension using its alias. +func Get(c *gophercloud.ServiceClient, alias string) common.GetResult { + return common.Get(c, alias) +} + +// List returns a Pager which allows you to iterate over the full collection of extensions. +// It does not accept query parameters. +func List(c *gophercloud.ServiceClient) pagination.Pager { + return common.List(c).WithPageCreator(func(r pagination.PageResult) pagination.Page { + return ExtensionPage{ + ExtensionPage: common.ExtensionPage{SinglePageBase: pagination.SinglePageBase(r)}, + } + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/doc.go new file mode 100644 index 0000000000..791e4e391d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/doc.go @@ -0,0 +1,3 @@ +// Package extensions provides information and interaction with the +// different extensions available for the OpenStack Identity service. +package extensions diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/testing/delegate_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/testing/delegate_test.go new file mode 100644 index 0000000000..e7869d8d87 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/testing/delegate_test.go @@ -0,0 +1,39 @@ +package testing + +import ( + "testing" + + common "github.com/gophercloud/gophercloud/openstack/common/extensions/testing" + "github.com/gophercloud/gophercloud/openstack/identity/v2/extensions" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListExtensionsSuccessfully(t) + + count := 0 + err := extensions.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := extensions.ExtractExtensions(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, common.ExpectedExtensions, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + common.HandleGetExtensionSuccessfully(t) + + actual, err := extensions.Get(client.ServiceClient(), "agent").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, common.SingleExtension, actual) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/testing/doc.go new file mode 100644 index 0000000000..6d4b67d181 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/testing/doc.go @@ -0,0 +1,2 @@ +// identity_extensions_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/testing/fixtures.go new file mode 100644 index 0000000000..60afb747b6 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/testing/fixtures.go @@ -0,0 +1,58 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +// ListOutput provides a single Extension result. It differs from the delegated implementation +// by the introduction of an intermediate "values" member. +const ListOutput = ` +{ + "extensions": { + "values": [ + { + "updated": "2013-01-20T00:00:00-00:00", + "name": "Neutron Service Type Management", + "links": [], + "namespace": "http://docs.openstack.org/ext/neutron/service-type/api/v1.0", + "alias": "service-type", + "description": "API for retrieving service providers for Neutron advanced services" + } + ] + } +} +` + +// HandleListExtensionsSuccessfully creates an HTTP handler that returns ListOutput for a List +// call. +func HandleListExtensionsSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/extensions", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + + fmt.Fprintf(w, ` +{ + "extensions": { + "values": [ + { + "updated": "2013-01-20T00:00:00-00:00", + "name": "Neutron Service Type Management", + "links": [], + "namespace": "http://docs.openstack.org/ext/neutron/service-type/api/v1.0", + "alias": "service-type", + "description": "API for retrieving service providers for Neutron advanced services" + } + ] + } +} + `) + }) + +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/doc.go new file mode 100644 index 0000000000..0c2d49d567 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/doc.go @@ -0,0 +1,7 @@ +// Package tenants provides information and interaction with the +// tenants API resource for the OpenStack Identity service. +// +// See http://developer.openstack.org/api-ref-identity-v2.html#identity-auth-v2 +// and http://developer.openstack.org/api-ref-identity-v2.html#admin-tenants +// for more information. +package tenants diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/requests.go new file mode 100644 index 0000000000..b6550ce60d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/requests.go @@ -0,0 +1,107 @@ +package tenants + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOpts filters the Tenants that are returned by the List call. +type ListOpts struct { + // Marker is the ID of the last Tenant on the previous page. + Marker string `q:"marker"` + // Limit specifies the page size. + Limit int `q:"limit"` +} + +// List enumerates the Tenants to which the current token has access. +func List(client *gophercloud.ServiceClient, opts *ListOpts) pagination.Pager { + url := listURL(client) + if opts != nil { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return pagination.Pager{Err: err} + } + url += q.String() + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return TenantPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOpts represents the options needed when creating new tenant. +type CreateOpts struct { + // Name is the name of the tenant. + Name string `json:"name" required:"true"` + // Description is the description of the tenant. + Description string `json:"description,omitempty"` + // Enabled sets the tenant status to enabled or disabled. + Enabled *bool `json:"enabled,omitempty"` +} + +// CreateOptsBuilder describes struct types that can be accepted by the Create call. +type CreateOptsBuilder interface { + ToTenantCreateMap() (map[string]interface{}, error) +} + +// ToTenantCreateMap assembles a request body based on the contents of a CreateOpts. +func (opts CreateOpts) ToTenantCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "tenant") +} + +// Create is the operation responsible for creating new tenant. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToTenantCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + return +} + +// Get requests details on a single tenant by ID. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// UpdateOptsBuilder allows extensions to add additional attributes to the Update request. +type UpdateOptsBuilder interface { + ToTenantUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts specifies the base attributes that may be updated on an existing server. +type UpdateOpts struct { + // Name is the name of the tenant. + Name string `json:"name,omitempty"` + // Description is the description of the tenant. + Description string `json:"description,omitempty"` + // Enabled sets the tenant status to enabled or disabled. + Enabled *bool `json:"enabled,omitempty"` +} + +// ToTenantUpdateMap formats an UpdateOpts structure into a request body. +func (opts UpdateOpts) ToTenantUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "tenant") +} + +// Update is the operation responsible for updating exist tenants by their TenantID. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToTenantUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(updateURL(client, id), &b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete is the operation responsible for permanently deleting an API tenant. +func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/results.go new file mode 100644 index 0000000000..5a319de5cb --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/results.go @@ -0,0 +1,86 @@ +package tenants + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Tenant is a grouping of users in the identity service. +type Tenant struct { + // ID is a unique identifier for this tenant. + ID string `json:"id"` + + // Name is a friendlier user-facing name for this tenant. + Name string `json:"name"` + + // Description is a human-readable explanation of this Tenant's purpose. + Description string `json:"description"` + + // Enabled indicates whether or not a tenant is active. + Enabled bool `json:"enabled"` +} + +// TenantPage is a single page of Tenant results. +type TenantPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of Tenants contains any results. +func (r TenantPage) IsEmpty() (bool, error) { + tenants, err := ExtractTenants(r) + return len(tenants) == 0, err +} + +// NextPageURL extracts the "next" link from the tenants_links section of the result. +func (r TenantPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"tenants_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// ExtractTenants returns a slice of Tenants contained in a single page of results. +func ExtractTenants(r pagination.Page) ([]Tenant, error) { + var s struct { + Tenants []Tenant `json:"tenants"` + } + err := (r.(TenantPage)).ExtractInto(&s) + return s.Tenants, err +} + +type tenantResult struct { + gophercloud.Result +} + +// Extract interprets any tenantResults as a tenant. +func (r tenantResult) Extract() (*Tenant, error) { + var s struct { + Tenant *Tenant `json:"tenant"` + } + err := r.ExtractInto(&s) + return s.Tenant, err +} + +// GetResult temporarily contains the response from the Get call. +type GetResult struct { + tenantResult +} + +// CreateResult temporarily contains the reponse from the Create call. +type CreateResult struct { + tenantResult +} + +// DeleteResult temporarily contains the response from the Delete call. +type DeleteResult struct { + gophercloud.ErrResult +} + +// UpdateResult temporarily contains the response from the Update call. +type UpdateResult struct { + tenantResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/testing/doc.go new file mode 100644 index 0000000000..57aaa1f035 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/testing/doc.go @@ -0,0 +1,2 @@ +// identity_tenants_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/testing/fixtures.go new file mode 100644 index 0000000000..9a314704fa --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/testing/fixtures.go @@ -0,0 +1,155 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud/openstack/identity/v2/tenants" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +// ListOutput provides a single page of Tenant results. +const ListOutput = ` +{ + "tenants": [ + { + "id": "1234", + "name": "Red Team", + "description": "The team that is red", + "enabled": true + }, + { + "id": "9876", + "name": "Blue Team", + "description": "The team that is blue", + "enabled": false + } + ] +} +` + +// RedTeam is a Tenant fixture. +var RedTeam = tenants.Tenant{ + ID: "1234", + Name: "Red Team", + Description: "The team that is red", + Enabled: true, +} + +// BlueTeam is a Tenant fixture. +var BlueTeam = tenants.Tenant{ + ID: "9876", + Name: "Blue Team", + Description: "The team that is blue", + Enabled: false, +} + +// ExpectedTenantSlice is the slice of tenants expected to be returned from ListOutput. +var ExpectedTenantSlice = []tenants.Tenant{RedTeam, BlueTeam} + +// HandleListTenantsSuccessfully creates an HTTP handler at `/tenants` on the test handler mux that +// responds with a list of two tenants. +func HandleListTenantsSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/tenants", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ListOutput) + }) +} + +func mockCreateTenantResponse(t *testing.T) { + th.Mux.HandleFunc("/tenants", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "tenant": { + "name": "new_tenant", + "description": "This is new tenant", + "enabled": true + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "tenant": { + "name": "new_tenant", + "description": "This is new tenant", + "enabled": true, + "id": "5c62ef576dc7444cbb73b1fe84b97648" + } +} +`) + }) +} + +func mockDeleteTenantResponse(t *testing.T) { + th.Mux.HandleFunc("/tenants/2466f69cd4714d89a548a68ed97ffcd4", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + w.WriteHeader(http.StatusNoContent) + }) +} + +func mockUpdateTenantResponse(t *testing.T) { + th.Mux.HandleFunc("/tenants/5c62ef576dc7444cbb73b1fe84b97648", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "tenant": { + "name": "new_name", + "description": "This is new name", + "enabled": true + } +} +`) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "tenant": { + "name": "new_name", + "description": "This is new name", + "enabled": true, + "id": "5c62ef576dc7444cbb73b1fe84b97648" + } +} +`) + }) +} + +func mockGetTenantResponse(t *testing.T) { + th.Mux.HandleFunc("/tenants/5c62ef576dc7444cbb73b1fe84b97648", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "tenant": { + "name": "new_tenant", + "description": "This is new tenant", + "enabled": true, + "id": "5c62ef576dc7444cbb73b1fe84b97648" + } +} +`) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/testing/requests_test.go new file mode 100644 index 0000000000..86f2c94722 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/testing/requests_test.go @@ -0,0 +1,113 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/identity/v2/tenants" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestListTenants(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListTenantsSuccessfully(t) + + count := 0 + err := tenants.List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + + actual, err := tenants.ExtractTenants(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ExpectedTenantSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestCreateTenant(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockCreateTenantResponse(t) + + opts := tenants.CreateOpts{ + Name: "new_tenant", + Description: "This is new tenant", + Enabled: gophercloud.Enabled, + } + + tenant, err := tenants.Create(client.ServiceClient(), opts).Extract() + + th.AssertNoErr(t, err) + + expected := &tenants.Tenant{ + Name: "new_tenant", + Description: "This is new tenant", + Enabled: true, + ID: "5c62ef576dc7444cbb73b1fe84b97648", + } + + th.AssertDeepEquals(t, expected, tenant) +} + +func TestDeleteTenant(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDeleteTenantResponse(t) + + err := tenants.Delete(client.ServiceClient(), "2466f69cd4714d89a548a68ed97ffcd4").ExtractErr() + th.AssertNoErr(t, err) +} + +func TestUpdateTenant(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockUpdateTenantResponse(t) + + id := "5c62ef576dc7444cbb73b1fe84b97648" + opts := tenants.UpdateOpts{ + Name: "new_name", + Description: "This is new name", + Enabled: gophercloud.Enabled, + } + + tenant, err := tenants.Update(client.ServiceClient(), id, opts).Extract() + + th.AssertNoErr(t, err) + + expected := &tenants.Tenant{ + Name: "new_name", + ID: id, + Description: "This is new name", + Enabled: true, + } + + th.AssertDeepEquals(t, expected, tenant) +} + +func TestGetTenant(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockGetTenantResponse(t) + + tenant, err := tenants.Get(client.ServiceClient(), "5c62ef576dc7444cbb73b1fe84b97648").Extract() + th.AssertNoErr(t, err) + + expected := &tenants.Tenant{ + Name: "new_tenant", + ID: "5c62ef576dc7444cbb73b1fe84b97648", + Description: "This is new tenant", + Enabled: true, + } + + th.AssertDeepEquals(t, expected, tenant) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/urls.go new file mode 100644 index 0000000000..0f02669079 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/urls.go @@ -0,0 +1,23 @@ +package tenants + +import "github.com/gophercloud/gophercloud" + +func listURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("tenants") +} + +func getURL(client *gophercloud.ServiceClient, tenantID string) string { + return client.ServiceURL("tenants", tenantID) +} + +func createURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("tenants") +} + +func deleteURL(client *gophercloud.ServiceClient, tenantID string) string { + return client.ServiceURL("tenants", tenantID) +} + +func updateURL(client *gophercloud.ServiceClient, tenantID string) string { + return client.ServiceURL("tenants", tenantID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/doc.go new file mode 100644 index 0000000000..31cacc5e17 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/doc.go @@ -0,0 +1,5 @@ +// Package tokens provides information and interaction with the token API +// resource for the OpenStack Identity service. +// For more information, see: +// http://developer.openstack.org/api-ref-identity-v2.html#identity-auth-v2 +package tokens diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/requests.go new file mode 100644 index 0000000000..4983031e7f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/requests.go @@ -0,0 +1,99 @@ +package tokens + +import "github.com/gophercloud/gophercloud" + +type PasswordCredentialsV2 struct { + Username string `json:"username" required:"true"` + Password string `json:"password" required:"true"` +} + +type TokenCredentialsV2 struct { + ID string `json:"id,omitempty" required:"true"` +} + +// AuthOptionsV2 wraps a gophercloud AuthOptions in order to adhere to the AuthOptionsBuilder +// interface. +type AuthOptionsV2 struct { + PasswordCredentials *PasswordCredentialsV2 `json:"passwordCredentials,omitempty" xor:"TokenCredentials"` + + // The TenantID and TenantName fields are optional for the Identity V2 API. + // Some providers allow you to specify a TenantName instead of the TenantId. + // Some require both. Your provider's authentication policies will determine + // how these fields influence authentication. + TenantID string `json:"tenantId,omitempty"` + TenantName string `json:"tenantName,omitempty"` + + // TokenCredentials allows users to authenticate (possibly as another user) with an + // authentication token ID. + TokenCredentials *TokenCredentialsV2 `json:"token,omitempty" xor:"PasswordCredentials"` +} + +// AuthOptionsBuilder describes any argument that may be passed to the Create call. +type AuthOptionsBuilder interface { + // ToTokenCreateMap assembles the Create request body, returning an error if parameters are + // missing or inconsistent. + ToTokenV2CreateMap() (map[string]interface{}, error) +} + +// AuthOptions are the valid options for Openstack Identity v2 authentication. +// For field descriptions, see gophercloud.AuthOptions. +type AuthOptions struct { + IdentityEndpoint string `json:"-"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + TenantID string `json:"tenantId,omitempty"` + TenantName string `json:"tenantName,omitempty"` + AllowReauth bool `json:"-"` + TokenID string +} + +// ToTokenV2CreateMap allows AuthOptions to satisfy the AuthOptionsBuilder +// interface in the v2 tokens package +func (opts AuthOptions) ToTokenV2CreateMap() (map[string]interface{}, error) { + v2Opts := AuthOptionsV2{ + TenantID: opts.TenantID, + TenantName: opts.TenantName, + } + + if opts.Password != "" { + v2Opts.PasswordCredentials = &PasswordCredentialsV2{ + Username: opts.Username, + Password: opts.Password, + } + } else { + v2Opts.TokenCredentials = &TokenCredentialsV2{ + ID: opts.TokenID, + } + } + + b, err := gophercloud.BuildRequestBody(v2Opts, "auth") + if err != nil { + return nil, err + } + return b, nil +} + +// Create authenticates to the identity service and attempts to acquire a Token. +// If successful, the CreateResult +// Generally, rather than interact with this call directly, end users should call openstack.AuthenticatedClient(), +// which abstracts all of the gory details about navigating service catalogs and such. +func Create(client *gophercloud.ServiceClient, auth AuthOptionsBuilder) (r CreateResult) { + b, err := auth.ToTokenV2CreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(CreateURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 203}, + MoreHeaders: map[string]string{"X-Auth-Token": ""}, + }) + return +} + +// Get validates and retrieves information for user's token. +func Get(client *gophercloud.ServiceClient, token string) (r GetResult) { + _, r.Err = client.Get(GetURL(client, token), &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 203}, + }) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/results.go new file mode 100644 index 0000000000..6b36493706 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/results.go @@ -0,0 +1,144 @@ +package tokens + +import ( + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/identity/v2/tenants" +) + +// Token provides only the most basic information related to an authentication token. +type Token struct { + // ID provides the primary means of identifying a user to the OpenStack API. + // OpenStack defines this field as an opaque value, so do not depend on its content. + // It is safe, however, to compare for equality. + ID string + + // ExpiresAt provides a timestamp in ISO 8601 format, indicating when the authentication token becomes invalid. + // After this point in time, future API requests made using this authentication token will respond with errors. + // Either the caller will need to reauthenticate manually, or more preferably, the caller should exploit automatic re-authentication. + // See the AuthOptions structure for more details. + ExpiresAt time.Time + + // Tenant provides information about the tenant to which this token grants access. + Tenant tenants.Tenant +} + +// Role is a role for a user. +type Role struct { + Name string `json:"name"` +} + +// User is an OpenStack user. +type User struct { + ID string `json:"id"` + Name string `json:"name"` + UserName string `json:"username"` + Roles []Role `json:"roles"` +} + +// Endpoint represents a single API endpoint offered by a service. +// It provides the public and internal URLs, if supported, along with a region specifier, again if provided. +// The significance of the Region field will depend upon your provider. +// +// In addition, the interface offered by the service will have version information associated with it +// through the VersionId, VersionInfo, and VersionList fields, if provided or supported. +// +// In all cases, fields which aren't supported by the provider and service combined will assume a zero-value (""). +type Endpoint struct { + TenantID string `json:"tenantId"` + PublicURL string `json:"publicURL"` + InternalURL string `json:"internalURL"` + AdminURL string `json:"adminURL"` + Region string `json:"region"` + VersionID string `json:"versionId"` + VersionInfo string `json:"versionInfo"` + VersionList string `json:"versionList"` +} + +// CatalogEntry provides a type-safe interface to an Identity API V2 service catalog listing. +// Each class of service, such as cloud DNS or block storage services, will have a single +// CatalogEntry representing it. +// +// Note: when looking for the desired service, try, whenever possible, to key off the type field. +// Otherwise, you'll tie the representation of the service to a specific provider. +type CatalogEntry struct { + // Name will contain the provider-specified name for the service. + Name string `json:"name"` + + // Type will contain a type string if OpenStack defines a type for the service. + // Otherwise, for provider-specific services, the provider may assign their own type strings. + Type string `json:"type"` + + // Endpoints will let the caller iterate over all the different endpoints that may exist for + // the service. + Endpoints []Endpoint `json:"endpoints"` +} + +// ServiceCatalog provides a view into the service catalog from a previous, successful authentication. +type ServiceCatalog struct { + Entries []CatalogEntry +} + +// CreateResult defers the interpretation of a created token. +// Use ExtractToken() to interpret it as a Token, or ExtractServiceCatalog() to interpret it as a service catalog. +type CreateResult struct { + gophercloud.Result +} + +// GetResult is the deferred response from a Get call, which is the same with a Created token. +// Use ExtractUser() to interpret it as a User. +type GetResult struct { + CreateResult +} + +// ExtractToken returns the just-created Token from a CreateResult. +func (r CreateResult) ExtractToken() (*Token, error) { + var s struct { + Access struct { + Token struct { + Expires string `json:"expires"` + ID string `json:"id"` + Tenant tenants.Tenant `json:"tenant"` + } `json:"token"` + } `json:"access"` + } + + err := r.ExtractInto(&s) + if err != nil { + return nil, err + } + + expiresTs, err := time.Parse(gophercloud.RFC3339Milli, s.Access.Token.Expires) + if err != nil { + return nil, err + } + + return &Token{ + ID: s.Access.Token.ID, + ExpiresAt: expiresTs, + Tenant: s.Access.Token.Tenant, + }, nil +} + +// ExtractServiceCatalog returns the ServiceCatalog that was generated along with the user's Token. +func (r CreateResult) ExtractServiceCatalog() (*ServiceCatalog, error) { + var s struct { + Access struct { + Entries []CatalogEntry `json:"serviceCatalog"` + } `json:"access"` + } + err := r.ExtractInto(&s) + return &ServiceCatalog{Entries: s.Access.Entries}, err +} + +// ExtractUser returns the User from a GetResult. +func (r GetResult) ExtractUser() (*User, error) { + var s struct { + Access struct { + User User `json:"user"` + } `json:"access"` + } + err := r.ExtractInto(&s) + return &s.Access.User, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/testing/doc.go new file mode 100644 index 0000000000..f9767eb12f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/testing/doc.go @@ -0,0 +1,2 @@ +// identity_tokens_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/testing/fixtures.go new file mode 100644 index 0000000000..0a8544ba86 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/testing/fixtures.go @@ -0,0 +1,194 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/gophercloud/gophercloud/openstack/identity/v2/tenants" + "github.com/gophercloud/gophercloud/openstack/identity/v2/tokens" + th "github.com/gophercloud/gophercloud/testhelper" + thclient "github.com/gophercloud/gophercloud/testhelper/client" +) + +// ExpectedToken is the token that should be parsed from TokenCreationResponse. +var ExpectedToken = &tokens.Token{ + ID: "aaaabbbbccccdddd", + ExpiresAt: time.Date(2014, time.January, 31, 15, 30, 58, 0, time.UTC), + Tenant: tenants.Tenant{ + ID: "fc394f2ab2df4114bde39905f800dc57", + Name: "test", + Description: "There are many tenants. This one is yours.", + Enabled: true, + }, +} + +// ExpectedServiceCatalog is the service catalog that should be parsed from TokenCreationResponse. +var ExpectedServiceCatalog = &tokens.ServiceCatalog{ + Entries: []tokens.CatalogEntry{ + { + Name: "inscrutablewalrus", + Type: "something", + Endpoints: []tokens.Endpoint{ + { + PublicURL: "http://something0:1234/v2/", + Region: "region0", + }, + { + PublicURL: "http://something1:1234/v2/", + Region: "region1", + }, + }, + }, + { + Name: "arbitrarypenguin", + Type: "else", + Endpoints: []tokens.Endpoint{ + { + PublicURL: "http://else0:4321/v3/", + Region: "region0", + }, + }, + }, + }, +} + +// ExpectedUser is the token that should be parsed from TokenGetResponse. +var ExpectedUser = &tokens.User{ + ID: "a530fefc3d594c4ba2693a4ecd6be74e", + Name: "apiserver", + Roles: []tokens.Role{tokens.Role{Name: "member"}, tokens.Role{Name: "service"}}, + UserName: "apiserver", +} + +// TokenCreationResponse is a JSON response that contains ExpectedToken and ExpectedServiceCatalog. +const TokenCreationResponse = ` +{ + "access": { + "token": { + "issued_at": "2014-01-30T15:30:58.000000Z", + "expires": "2014-01-31T15:30:58Z", + "id": "aaaabbbbccccdddd", + "tenant": { + "description": "There are many tenants. This one is yours.", + "enabled": true, + "id": "fc394f2ab2df4114bde39905f800dc57", + "name": "test" + } + }, + "serviceCatalog": [ + { + "endpoints": [ + { + "publicURL": "http://something0:1234/v2/", + "region": "region0" + }, + { + "publicURL": "http://something1:1234/v2/", + "region": "region1" + } + ], + "type": "something", + "name": "inscrutablewalrus" + }, + { + "endpoints": [ + { + "publicURL": "http://else0:4321/v3/", + "region": "region0" + } + ], + "type": "else", + "name": "arbitrarypenguin" + } + ] + } +} +` + +// TokenGetResponse is a JSON response that contains ExpectedToken and ExpectedUser. +const TokenGetResponse = ` +{ + "access": { + "token": { + "issued_at": "2014-01-30T15:30:58.000000Z", + "expires": "2014-01-31T15:30:58Z", + "id": "aaaabbbbccccdddd", + "tenant": { + "description": "There are many tenants. This one is yours.", + "enabled": true, + "id": "fc394f2ab2df4114bde39905f800dc57", + "name": "test" + } + }, + "serviceCatalog": [], + "user": { + "id": "a530fefc3d594c4ba2693a4ecd6be74e", + "name": "apiserver", + "roles": [ + { + "name": "member" + }, + { + "name": "service" + } + ], + "roles_links": [], + "username": "apiserver" + } + } +}` + +// HandleTokenPost expects a POST against a /tokens handler, ensures that the request body has been +// constructed properly given certain auth options, and returns the result. +func HandleTokenPost(t *testing.T, requestJSON string) { + th.Mux.HandleFunc("/tokens", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + if requestJSON != "" { + th.TestJSONRequest(t, r, requestJSON) + } + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, TokenCreationResponse) + }) +} + +// HandleTokenGet expects a Get against a /tokens handler, ensures that the request body has been +// constructed properly given certain auth options, and returns the result. +func HandleTokenGet(t *testing.T, token string) { + th.Mux.HandleFunc("/tokens/"+token, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "X-Auth-Token", thclient.TokenID) + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, TokenGetResponse) + }) +} + +// IsSuccessful ensures that a CreateResult was successful and contains the correct token and +// service catalog. +func IsSuccessful(t *testing.T, result tokens.CreateResult) { + token, err := result.ExtractToken() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedToken, token) + + serviceCatalog, err := result.ExtractServiceCatalog() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedServiceCatalog, serviceCatalog) +} + +// GetIsSuccessful ensures that a GetResult was successful and contains the correct token and +// User Info. +func GetIsSuccessful(t *testing.T, result tokens.GetResult) { + token, err := result.ExtractToken() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedToken, token) + + user, err := result.ExtractUser() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedUser, user) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/testing/requests_test.go new file mode 100644 index 0000000000..b687a929e0 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/testing/requests_test.go @@ -0,0 +1,104 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/identity/v2/tokens" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func tokenPost(t *testing.T, options gophercloud.AuthOptions, requestJSON string) tokens.CreateResult { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleTokenPost(t, requestJSON) + + return tokens.Create(client.ServiceClient(), options) +} + +func tokenPostErr(t *testing.T, options gophercloud.AuthOptions, expectedErr error) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleTokenPost(t, "") + + actualErr := tokens.Create(client.ServiceClient(), options).Err + th.CheckDeepEquals(t, expectedErr, actualErr) +} + +func TestCreateWithPassword(t *testing.T) { + options := gophercloud.AuthOptions{ + Username: "me", + Password: "swordfish", + } + + IsSuccessful(t, tokenPost(t, options, ` + { + "auth": { + "passwordCredentials": { + "username": "me", + "password": "swordfish" + } + } + } + `)) +} + +func TestCreateTokenWithTenantID(t *testing.T) { + options := gophercloud.AuthOptions{ + Username: "me", + Password: "opensesame", + TenantID: "fc394f2ab2df4114bde39905f800dc57", + } + + IsSuccessful(t, tokenPost(t, options, ` + { + "auth": { + "tenantId": "fc394f2ab2df4114bde39905f800dc57", + "passwordCredentials": { + "username": "me", + "password": "opensesame" + } + } + } + `)) +} + +func TestCreateTokenWithTenantName(t *testing.T) { + options := gophercloud.AuthOptions{ + Username: "me", + Password: "opensesame", + TenantName: "demo", + } + + IsSuccessful(t, tokenPost(t, options, ` + { + "auth": { + "tenantName": "demo", + "passwordCredentials": { + "username": "me", + "password": "opensesame" + } + } + } + `)) +} + +func TestRequireUsername(t *testing.T) { + options := gophercloud.AuthOptions{ + Password: "thing", + } + + tokenPostErr(t, options, gophercloud.ErrMissingInput{Argument: "Username"}) +} + +func tokenGet(t *testing.T, tokenId string) tokens.GetResult { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleTokenGet(t, tokenId) + return tokens.Get(client.ServiceClient(), tokenId) +} + +func TestGetWithToken(t *testing.T) { + GetIsSuccessful(t, tokenGet(t, "db22caf43c934e6c829087c41ff8d8d6")) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/urls.go new file mode 100644 index 0000000000..ee0a28f200 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/urls.go @@ -0,0 +1,13 @@ +package tokens + +import "github.com/gophercloud/gophercloud" + +// CreateURL generates the URL used to create new Tokens. +func CreateURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("tokens") +} + +// GetURL generates the URL used to Validate Tokens. +func GetURL(client *gophercloud.ServiceClient, token string) string { + return client.ServiceURL("tokens", token) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/users/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/users/doc.go new file mode 100644 index 0000000000..82abcb9fcc --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/users/doc.go @@ -0,0 +1 @@ +package users diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/users/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/users/requests.go new file mode 100644 index 0000000000..37fcd38870 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/users/requests.go @@ -0,0 +1,106 @@ +package users + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// List lists the existing users. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, rootURL(client), func(r pagination.PageResult) pagination.Page { + return UserPage{pagination.SinglePageBase(r)} + }) +} + +// CommonOpts are the parameters that are shared between CreateOpts and +// UpdateOpts +type CommonOpts struct { + // Either a name or username is required. When provided, the value must be + // unique or a 409 conflict error will be returned. If you provide a name but + // omit a username, the latter will be set to the former; and vice versa. + Name string `json:"name,omitempty"` + Username string `json:"username,omitempty"` + // The ID of the tenant to which you want to assign this user. + TenantID string `json:"tenantId,omitempty"` + // Indicates whether this user is enabled or not. + Enabled *bool `json:"enabled,omitempty"` + // The email address of this user. + Email string `json:"email,omitempty"` +} + +// CreateOpts represents the options needed when creating new users. +type CreateOpts CommonOpts + +// CreateOptsBuilder describes struct types that can be accepted by the Create call. +type CreateOptsBuilder interface { + ToUserCreateMap() (map[string]interface{}, error) +} + +// ToUserCreateMap assembles a request body based on the contents of a CreateOpts. +func (opts CreateOpts) ToUserCreateMap() (map[string]interface{}, error) { + if opts.Name == "" && opts.Username == "" { + err := gophercloud.ErrMissingInput{} + err.Argument = "users.CreateOpts.Name/users.CreateOpts.Username" + err.Info = "Either a Name or Username must be provided" + return nil, err + } + return gophercloud.BuildRequestBody(opts, "user") +} + +// Create is the operation responsible for creating new users. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToUserCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(rootURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + return +} + +// Get requests details on a single user, either by ID. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(ResourceURL(client, id), &r.Body, nil) + return +} + +// UpdateOptsBuilder allows extensions to add additional attributes to the Update request. +type UpdateOptsBuilder interface { + ToUserUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts specifies the base attributes that may be updated on an existing server. +type UpdateOpts CommonOpts + +// ToUserUpdateMap formats an UpdateOpts structure into a request body. +func (opts UpdateOpts) ToUserUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "user") +} + +// Update is the operation responsible for updating exist users by their UUID. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToUserUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(ResourceURL(client, id), &b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete is the operation responsible for permanently deleting an API user. +func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(ResourceURL(client, id), nil) + return +} + +// ListRoles lists the existing roles that can be assigned to users. +func ListRoles(client *gophercloud.ServiceClient, tenantID, userID string) pagination.Pager { + return pagination.NewPager(client, listRolesURL(client, tenantID, userID), func(r pagination.PageResult) pagination.Page { + return RolePage{pagination.SinglePageBase(r)} + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/users/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/users/results.go new file mode 100644 index 0000000000..c49338357d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/users/results.go @@ -0,0 +1,110 @@ +package users + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// User represents a user resource that exists on the API. +type User struct { + // The UUID for this user. + ID string + + // The human name for this user. + Name string + + // The username for this user. + Username string + + // Indicates whether the user is enabled (true) or disabled (false). + Enabled bool + + // The email address for this user. + Email string + + // The ID of the tenant to which this user belongs. + TenantID string `json:"tenant_id"` +} + +// Role assigns specific responsibilities to users, allowing them to accomplish +// certain API operations whilst scoped to a service. +type Role struct { + // UUID of the role + ID string + + // Name of the role + Name string +} + +// UserPage is a single page of a User collection. +type UserPage struct { + pagination.SinglePageBase +} + +// RolePage is a single page of a user Role collection. +type RolePage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a page of Tenants contains any results. +func (r UserPage) IsEmpty() (bool, error) { + users, err := ExtractUsers(r) + return len(users) == 0, err +} + +// ExtractUsers returns a slice of Tenants contained in a single page of results. +func ExtractUsers(r pagination.Page) ([]User, error) { + var s struct { + Users []User `json:"users"` + } + err := (r.(UserPage)).ExtractInto(&s) + return s.Users, err +} + +// IsEmpty determines whether or not a page of Tenants contains any results. +func (r RolePage) IsEmpty() (bool, error) { + users, err := ExtractRoles(r) + return len(users) == 0, err +} + +// ExtractRoles returns a slice of Roles contained in a single page of results. +func ExtractRoles(r pagination.Page) ([]Role, error) { + var s struct { + Roles []Role `json:"roles"` + } + err := (r.(RolePage)).ExtractInto(&s) + return s.Roles, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract interprets any commonResult as a User, if possible. +func (r commonResult) Extract() (*User, error) { + var s struct { + User *User `json:"user"` + } + err := r.ExtractInto(&s) + return s.User, err +} + +// CreateResult represents the result of a Create operation +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a Get operation +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an Update operation +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a Delete operation +type DeleteResult struct { + commonResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/users/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/users/testing/doc.go new file mode 100644 index 0000000000..a007def604 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/users/testing/doc.go @@ -0,0 +1,2 @@ +// identity_users_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/users/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/users/testing/fixtures.go new file mode 100644 index 0000000000..8626da2af6 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/users/testing/fixtures.go @@ -0,0 +1,163 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +func MockListUserResponse(t *testing.T) { + th.Mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "users":[ + { + "id": "u1000", + "name": "John Smith", + "username": "jqsmith", + "email": "john.smith@example.org", + "enabled": true, + "tenant_id": "12345" + }, + { + "id": "u1001", + "name": "Jane Smith", + "username": "jqsmith", + "email": "jane.smith@example.org", + "enabled": true, + "tenant_id": "12345" + } + ] +} + `) + }) +} + +func mockCreateUserResponse(t *testing.T) { + th.Mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "user": { + "name": "new_user", + "tenantId": "12345", + "enabled": false, + "email": "new_user@foo.com" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "user": { + "name": "new_user", + "tenant_id": "12345", + "enabled": false, + "email": "new_user@foo.com", + "id": "c39e3de9be2d4c779f1dfd6abacc176d" + } +} +`) + }) +} + +func mockGetUserResponse(t *testing.T) { + th.Mux.HandleFunc("/users/new_user", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "user": { + "name": "new_user", + "tenant_id": "12345", + "enabled": false, + "email": "new_user@foo.com", + "id": "c39e3de9be2d4c779f1dfd6abacc176d" + } +} +`) + }) +} + +func mockUpdateUserResponse(t *testing.T) { + th.Mux.HandleFunc("/users/c39e3de9be2d4c779f1dfd6abacc176d", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "user": { + "name": "new_name", + "enabled": true, + "email": "new_email@foo.com" + } +} +`) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "user": { + "name": "new_name", + "tenant_id": "12345", + "enabled": true, + "email": "new_email@foo.com", + "id": "c39e3de9be2d4c779f1dfd6abacc176d" + } +} +`) + }) +} + +func mockDeleteUserResponse(t *testing.T) { + th.Mux.HandleFunc("/users/c39e3de9be2d4c779f1dfd6abacc176d", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) +} + +func mockListRolesResponse(t *testing.T) { + th.Mux.HandleFunc("/tenants/1d8b6120dcc640fda4fc9194ffc80273/users/c39e3de9be2d4c779f1dfd6abacc176d/roles", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "roles": [ + { + "id": "9fe2ff9ee4384b1894a90878d3e92bab", + "name": "foo_role" + }, + { + "id": "1ea3d56793574b668e85960fbf651e13", + "name": "admin" + } + ] +} + `) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/users/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/users/testing/requests_test.go new file mode 100644 index 0000000000..3cb047e2da --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/users/testing/requests_test.go @@ -0,0 +1,161 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/identity/v2/users" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockListUserResponse(t) + + count := 0 + + err := users.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := users.ExtractUsers(page) + th.AssertNoErr(t, err) + + expected := []users.User{ + { + ID: "u1000", + Name: "John Smith", + Username: "jqsmith", + Email: "john.smith@example.org", + Enabled: true, + TenantID: "12345", + }, + { + ID: "u1001", + Name: "Jane Smith", + Username: "jqsmith", + Email: "jane.smith@example.org", + Enabled: true, + TenantID: "12345", + }, + } + th.CheckDeepEquals(t, expected, actual) + return true, nil + }) + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, count) +} + +func TestCreateUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockCreateUserResponse(t) + + opts := users.CreateOpts{ + Name: "new_user", + TenantID: "12345", + Enabled: gophercloud.Disabled, + Email: "new_user@foo.com", + } + + user, err := users.Create(client.ServiceClient(), opts).Extract() + + th.AssertNoErr(t, err) + + expected := &users.User{ + Name: "new_user", + ID: "c39e3de9be2d4c779f1dfd6abacc176d", + Email: "new_user@foo.com", + Enabled: false, + TenantID: "12345", + } + + th.AssertDeepEquals(t, expected, user) +} + +func TestGetUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockGetUserResponse(t) + + user, err := users.Get(client.ServiceClient(), "new_user").Extract() + th.AssertNoErr(t, err) + + expected := &users.User{ + Name: "new_user", + ID: "c39e3de9be2d4c779f1dfd6abacc176d", + Email: "new_user@foo.com", + Enabled: false, + TenantID: "12345", + } + + th.AssertDeepEquals(t, expected, user) +} + +func TestUpdateUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockUpdateUserResponse(t) + + id := "c39e3de9be2d4c779f1dfd6abacc176d" + opts := users.UpdateOpts{ + Name: "new_name", + Enabled: gophercloud.Enabled, + Email: "new_email@foo.com", + } + + user, err := users.Update(client.ServiceClient(), id, opts).Extract() + + th.AssertNoErr(t, err) + + expected := &users.User{ + Name: "new_name", + ID: id, + Email: "new_email@foo.com", + Enabled: true, + TenantID: "12345", + } + + th.AssertDeepEquals(t, expected, user) +} + +func TestDeleteUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDeleteUserResponse(t) + + res := users.Delete(client.ServiceClient(), "c39e3de9be2d4c779f1dfd6abacc176d") + th.AssertNoErr(t, res.Err) +} + +func TestListingUserRoles(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockListRolesResponse(t) + + tenantID := "1d8b6120dcc640fda4fc9194ffc80273" + userID := "c39e3de9be2d4c779f1dfd6abacc176d" + + err := users.ListRoles(client.ServiceClient(), tenantID, userID).EachPage(func(page pagination.Page) (bool, error) { + actual, err := users.ExtractRoles(page) + th.AssertNoErr(t, err) + + expected := []users.Role{ + {ID: "9fe2ff9ee4384b1894a90878d3e92bab", Name: "foo_role"}, + {ID: "1ea3d56793574b668e85960fbf651e13", Name: "admin"}, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + th.AssertNoErr(t, err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/users/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/users/urls.go new file mode 100644 index 0000000000..89f66f2799 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/users/urls.go @@ -0,0 +1,21 @@ +package users + +import "github.com/gophercloud/gophercloud" + +const ( + tenantPath = "tenants" + userPath = "users" + rolePath = "roles" +) + +func ResourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(userPath, id) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(userPath) +} + +func listRolesURL(c *gophercloud.ServiceClient, tenantID, userID string) string { + return c.ServiceURL(tenantPath, tenantID, userPath, userID, rolePath) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/endpoints/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/endpoints/doc.go new file mode 100644 index 0000000000..85163949a8 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/endpoints/doc.go @@ -0,0 +1,6 @@ +// Package endpoints provides information and interaction with the service +// endpoints API resource in the OpenStack Identity service. +// +// For more information, see: +// http://developer.openstack.org/api-ref-identity-v3.html#endpoints-v3 +package endpoints diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/endpoints/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/endpoints/requests.go new file mode 100644 index 0000000000..fc4436587c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/endpoints/requests.go @@ -0,0 +1,102 @@ +package endpoints + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +type CreateOptsBuilder interface { + ToEndpointCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains the subset of Endpoint attributes that should be used to create an Endpoint. +type CreateOpts struct { + Availability gophercloud.Availability `json:"interface" required:"true"` + Name string `json:"name" required:"true"` + Region string `json:"region,omitempty"` + URL string `json:"url" required:"true"` + ServiceID string `json:"service_id" required:"true"` +} + +func (opts CreateOpts) ToEndpointCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "endpoint") +} + +// Create inserts a new Endpoint into the service catalog. +// Within EndpointOpts, Region may be omitted by being left as "", but all other fields are required. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToEndpointCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(listURL(client), &b, &r.Body, nil) + return +} + +type ListOptsBuilder interface { + ToEndpointListParams() (string, error) +} + +// ListOpts allows finer control over the endpoints returned by a List call. +// All fields are optional. +type ListOpts struct { + Availability gophercloud.Availability `q:"interface"` + ServiceID string `q:"service_id"` + Page int `q:"page"` + PerPage int `q:"per_page"` +} + +func (opts ListOpts) ToEndpointListParams() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List enumerates endpoints in a paginated collection, optionally filtered by ListOpts criteria. +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + u := listURL(client) + if opts != nil { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return pagination.Pager{Err: err} + } + u += q.String() + } + return pagination.NewPager(client, u, func(r pagination.PageResult) pagination.Page { + return EndpointPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +type UpdateOptsBuilder interface { + ToEndpointUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contains the subset of Endpoint attributes that should be used to update an Endpoint. +type UpdateOpts struct { + Availability gophercloud.Availability `json:"interface,omitempty"` + Name string `json:"name,omitempty"` + Region string `json:"region,omitempty"` + URL string `json:"url,omitempty"` + ServiceID string `json:"service_id,omitempty"` +} + +func (opts UpdateOpts) ToEndpointUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "endpoint") +} + +// Update changes an existing endpoint with new data. +func Update(client *gophercloud.ServiceClient, endpointID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToEndpointUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Patch(endpointURL(client, endpointID), &b, &r.Body, nil) + return +} + +// Delete removes an endpoint from the service catalog. +func Delete(client *gophercloud.ServiceClient, endpointID string) (r DeleteResult) { + _, r.Err = client.Delete(endpointURL(client, endpointID), nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/endpoints/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/endpoints/results.go new file mode 100644 index 0000000000..f769881670 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/endpoints/results.go @@ -0,0 +1,65 @@ +package endpoints + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// Extract interprets a GetResult, CreateResult or UpdateResult as a concrete Endpoint. +// An error is returned if the original call or the extraction failed. +func (r commonResult) Extract() (*Endpoint, error) { + var s struct { + Endpoint *Endpoint `json:"endpoint"` + } + err := r.ExtractInto(&s) + return s.Endpoint, err +} + +// CreateResult is the deferred result of a Create call. +type CreateResult struct { + commonResult +} + +// UpdateResult is the deferred result of an Update call. +type UpdateResult struct { + commonResult +} + +// DeleteResult is the deferred result of an Delete call. +type DeleteResult struct { + gophercloud.ErrResult +} + +// Endpoint describes the entry point for another service's API. +type Endpoint struct { + ID string `json:"id"` + Availability gophercloud.Availability `json:"interface"` + Name string `json:"name"` + Region string `json:"region"` + ServiceID string `json:"service_id"` + URL string `json:"url"` +} + +// EndpointPage is a single page of Endpoint results. +type EndpointPage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if no Endpoints were returned. +func (r EndpointPage) IsEmpty() (bool, error) { + es, err := ExtractEndpoints(r) + return len(es) == 0, err +} + +// ExtractEndpoints extracts an Endpoint slice from a Page. +func ExtractEndpoints(r pagination.Page) ([]Endpoint, error) { + var s struct { + Endpoints []Endpoint `json:"endpoints"` + } + err := (r.(EndpointPage)).ExtractInto(&s) + return s.Endpoints, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/endpoints/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/endpoints/testing/doc.go new file mode 100644 index 0000000000..b6e89eff98 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/endpoints/testing/doc.go @@ -0,0 +1,2 @@ +// identity_endpoints_v3 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/endpoints/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/endpoints/testing/requests_test.go new file mode 100644 index 0000000000..53d8488896 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/endpoints/testing/requests_test.go @@ -0,0 +1,214 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/identity/v3/endpoints" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestCreateSuccessful(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/endpoints", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, ` + { + "endpoint": { + "interface": "public", + "name": "the-endiest-of-points", + "region": "underground", + "url": "https://1.2.3.4:9000/", + "service_id": "asdfasdfasdfasdf" + } + } + `) + + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, ` + { + "endpoint": { + "id": "12", + "interface": "public", + "links": { + "self": "https://localhost:5000/v3/endpoints/12" + }, + "name": "the-endiest-of-points", + "region": "underground", + "service_id": "asdfasdfasdfasdf", + "url": "https://1.2.3.4:9000/" + } + } + `) + }) + + actual, err := endpoints.Create(client.ServiceClient(), endpoints.CreateOpts{ + Availability: gophercloud.AvailabilityPublic, + Name: "the-endiest-of-points", + Region: "underground", + URL: "https://1.2.3.4:9000/", + ServiceID: "asdfasdfasdfasdf", + }).Extract() + th.AssertNoErr(t, err) + + expected := &endpoints.Endpoint{ + ID: "12", + Availability: gophercloud.AvailabilityPublic, + Name: "the-endiest-of-points", + Region: "underground", + ServiceID: "asdfasdfasdfasdf", + URL: "https://1.2.3.4:9000/", + } + + th.AssertDeepEquals(t, expected, actual) +} + +func TestListEndpoints(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/endpoints", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "endpoints": [ + { + "id": "12", + "interface": "public", + "links": { + "self": "https://localhost:5000/v3/endpoints/12" + }, + "name": "the-endiest-of-points", + "region": "underground", + "service_id": "asdfasdfasdfasdf", + "url": "https://1.2.3.4:9000/" + }, + { + "id": "13", + "interface": "internal", + "links": { + "self": "https://localhost:5000/v3/endpoints/13" + }, + "name": "shhhh", + "region": "underground", + "service_id": "asdfasdfasdfasdf", + "url": "https://1.2.3.4:9001/" + } + ], + "links": { + "next": null, + "previous": null + } + } + `) + }) + + count := 0 + endpoints.List(client.ServiceClient(), endpoints.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := endpoints.ExtractEndpoints(page) + if err != nil { + t.Errorf("Failed to extract endpoints: %v", err) + return false, err + } + + expected := []endpoints.Endpoint{ + { + ID: "12", + Availability: gophercloud.AvailabilityPublic, + Name: "the-endiest-of-points", + Region: "underground", + ServiceID: "asdfasdfasdfasdf", + URL: "https://1.2.3.4:9000/", + }, + { + ID: "13", + Availability: gophercloud.AvailabilityInternal, + Name: "shhhh", + Region: "underground", + ServiceID: "asdfasdfasdfasdf", + URL: "https://1.2.3.4:9001/", + }, + } + th.AssertDeepEquals(t, expected, actual) + return true, nil + }) + th.AssertEquals(t, 1, count) +} + +func TestUpdateEndpoint(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/endpoints/12", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PATCH") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, ` + { + "endpoint": { + "name": "renamed", + "region": "somewhere-else" + } + } + `) + + fmt.Fprintf(w, ` + { + "endpoint": { + "id": "12", + "interface": "public", + "links": { + "self": "https://localhost:5000/v3/endpoints/12" + }, + "name": "renamed", + "region": "somewhere-else", + "service_id": "asdfasdfasdfasdf", + "url": "https://1.2.3.4:9000/" + } + } + `) + }) + + actual, err := endpoints.Update(client.ServiceClient(), "12", endpoints.UpdateOpts{ + Name: "renamed", + Region: "somewhere-else", + }).Extract() + if err != nil { + t.Fatalf("Unexpected error from Update: %v", err) + } + + expected := &endpoints.Endpoint{ + ID: "12", + Availability: gophercloud.AvailabilityPublic, + Name: "renamed", + Region: "somewhere-else", + ServiceID: "asdfasdfasdfasdf", + URL: "https://1.2.3.4:9000/", + } + th.AssertDeepEquals(t, expected, actual) +} + +func TestDeleteEndpoint(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/endpoints/34", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) + + res := endpoints.Delete(client.ServiceClient(), "34") + th.AssertNoErr(t, res.Err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/endpoints/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/endpoints/urls.go new file mode 100644 index 0000000000..80cf57eb35 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/endpoints/urls.go @@ -0,0 +1,11 @@ +package endpoints + +import "github.com/gophercloud/gophercloud" + +func listURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("endpoints") +} + +func endpointURL(client *gophercloud.ServiceClient, endpointID string) string { + return client.ServiceURL("endpoints", endpointID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/trusts/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/trusts/requests.go new file mode 100644 index 0000000000..999dd73deb --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/trusts/requests.go @@ -0,0 +1,34 @@ +package trusts + +import "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens" + +type AuthOptsExt struct { + tokens.AuthOptionsBuilder + TrustID string `json:"id"` +} + +func (opts AuthOptsExt) ToTokenV3CreateMap(scope map[string]interface{}) (map[string]interface{}, error) { + return opts.AuthOptionsBuilder.ToTokenV3CreateMap(scope) +} + +func (opts AuthOptsExt) ToTokenV3ScopeMap() (map[string]interface{}, error) { + b, err := opts.AuthOptionsBuilder.ToTokenV3ScopeMap() + if err != nil { + return nil, err + } + + if opts.TrustID != "" { + if b == nil { + b = make(map[string]interface{}) + } + b["OS-TRUST:trust"] = map[string]interface{}{ + "id": opts.TrustID, + } + } + + return b, nil +} + +func (opts AuthOptsExt) CanReauth() bool { + return opts.AuthOptionsBuilder.CanReauth() +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/trusts/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/trusts/results.go new file mode 100644 index 0000000000..bdd8e8479e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/trusts/results.go @@ -0,0 +1,22 @@ +package trusts + +type TrusteeUser struct { + ID string `json:"id"` +} + +type TrustorUser struct { + ID string `json:"id"` +} + +type Trust struct { + ID string `json:"id"` + Impersonation bool `json:"impersonation"` + TrusteeUser TrusteeUser `json:"trustee_user"` + TrustorUser TrustorUser `json:"trustor_user"` + RedelegatedTrustID string `json:"redelegated_trust_id"` + RedelegationCount int `json:"redelegation_count"` +} + +type TokenExt struct { + Trust Trust `json:"OS-TRUST:trust"` +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/trusts/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/trusts/testing/doc.go new file mode 100644 index 0000000000..e660e2039e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/trusts/testing/doc.go @@ -0,0 +1,2 @@ +// identity_extensions_trusts_v3 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/trusts/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/trusts/testing/fixtures.go new file mode 100644 index 0000000000..e3115264b0 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/trusts/testing/fixtures.go @@ -0,0 +1,67 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens" + "github.com/gophercloud/gophercloud/testhelper" +) + +// HandleCreateTokenWithTrustID verifies that providing certain AuthOptions and Scope results in an expected JSON structure. +func HandleCreateTokenWithTrustID(t *testing.T, options tokens.AuthOptionsBuilder, requestJSON string) { + testhelper.Mux.HandleFunc("/auth/tokens", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "POST") + testhelper.TestHeader(t, r, "Content-Type", "application/json") + testhelper.TestHeader(t, r, "Accept", "application/json") + testhelper.TestJSONRequest(t, r, requestJSON) + + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `{ + "token": { + "expires_at": "2013-02-27T18:30:59.999999Z", + "issued_at": "2013-02-27T16:30:59.999999Z", + "methods": [ + "password" + ], + "OS-TRUST:trust": { + "id": "fe0aef", + "impersonation": false, + "redelegated_trust_id": "3ba234", + "redelegation_count": 2, + "links": { + "self": "http://example.com/identity/v3/trusts/fe0aef" + }, + "trustee_user": { + "id": "0ca8f6", + "links": { + "self": "http://example.com/identity/v3/users/0ca8f6" + } + }, + "trustor_user": { + "id": "bd263c", + "links": { + "self": "http://example.com/identity/v3/users/bd263c" + } + } + }, + "user": { + "domain": { + "id": "1789d1", + "links": { + "self": "http://example.com/identity/v3/domains/1789d1" + }, + "name": "example.com" + }, + "email": "joe@example.com", + "id": "0ca8f6", + "links": { + "self": "http://example.com/identity/v3/users/0ca8f6" + }, + "name": "Joe" + } + } +}`) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/trusts/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/trusts/testing/requests_test.go new file mode 100644 index 0000000000..f8a65adb5c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/trusts/testing/requests_test.go @@ -0,0 +1,74 @@ +package testing + +import ( + "testing" + "time" + + "github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/trusts" + "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestCreateUserIDPasswordTrustID(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + ao := trusts.AuthOptsExt{ + TrustID: "de0945a", + AuthOptionsBuilder: &tokens.AuthOptions{ + UserID: "me", + Password: "squirrel!", + }, + } + HandleCreateTokenWithTrustID(t, ao, ` + { + "auth": { + "identity": { + "methods": ["password"], + "password": { + "user": { "id": "me", "password": "squirrel!" } + } + }, + "scope": { + "OS-TRUST:trust": { + "id": "de0945a" + } + } + } + } + `) + + var actual struct { + tokens.Token + trusts.TokenExt + } + err := tokens.Create(client.ServiceClient(), ao).ExtractInto(&actual) + if err != nil { + t.Errorf("Create returned an error: %v", err) + } + expected := struct { + tokens.Token + trusts.TokenExt + }{ + tokens.Token{ + ExpiresAt: time.Date(2013, 02, 27, 18, 30, 59, 999999000, time.UTC), + }, + trusts.TokenExt{ + Trust: trusts.Trust{ + ID: "fe0aef", + Impersonation: false, + TrusteeUser: trusts.TrusteeUser{ + ID: "0ca8f6", + }, + TrustorUser: trusts.TrustorUser{ + ID: "bd263c", + }, + RedelegatedTrustID: "3ba234", + RedelegationCount: 2, + }, + }, + } + + th.AssertDeepEquals(t, expected, actual) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/groups/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/groups/results.go new file mode 100644 index 0000000000..ad25d6c8c1 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/groups/results.go @@ -0,0 +1,108 @@ +package groups + +import ( + "encoding/json" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/internal" + "github.com/gophercloud/gophercloud/pagination" +) + +// Group helps manage related users. +type Group struct { + // Description describes the group purpose. + Description string `json:"description"` + + // DomainID is the domain ID the group belongs to. + DomainID string `json:"domain_id"` + + // ID is the unique ID of the group. + ID string `json:"id"` + + // Extra is a collection of miscellaneous key/values. + Extra map[string]interface{} `json:"-"` + + // Links contains referencing links to the group. + Links map[string]interface{} `json:"links"` + + // Name is the name of the group. + Name string `json:"name"` +} + +func (r *Group) UnmarshalJSON(b []byte) error { + type tmp Group + var s struct { + tmp + Extra map[string]interface{} `json:"extra"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Group(s.tmp) + + // Collect other fields and bundle them into Extra + // but only if a field titled "extra" wasn't sent. + if s.Extra != nil { + r.Extra = s.Extra + } else { + var result interface{} + err := json.Unmarshal(b, &result) + if err != nil { + return err + } + if resultMap, ok := result.(map[string]interface{}); ok { + r.Extra = internal.RemainingKeys(Group{}, resultMap) + } + } + + return err +} + +type groupResult struct { + gophercloud.Result +} + +// GroupPage is a single page of Group results. +type GroupPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of Groups contains any results. +func (r GroupPage) IsEmpty() (bool, error) { + groups, err := ExtractGroups(r) + return len(groups) == 0, err +} + +// NextPageURL extracts the "next" link from the links section of the result. +func (r GroupPage) NextPageURL() (string, error) { + var s struct { + Links struct { + Next string `json:"next"` + Previous string `json:"previous"` + } `json:"links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return s.Links.Next, err +} + +// ExtractGroups returns a slice of Groups contained in a single page of results. +func ExtractGroups(r pagination.Page) ([]Group, error) { + var s struct { + Groups []Group `json:"groups"` + } + err := (r.(GroupPage)).ExtractInto(&s) + return s.Groups, err +} + +// Extract interprets any group results as a Group. +func (r groupResult) Extract() (*Group, error) { + var s struct { + Group *Group `json:"group"` + } + err := r.ExtractInto(&s) + return s.Group, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/projects/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/projects/requests.go new file mode 100644 index 0000000000..af21de39e1 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/projects/requests.go @@ -0,0 +1,152 @@ +package projects + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to +// the List request +type ListOptsBuilder interface { + ToProjectListQuery() (string, error) +} + +// ListOpts allows you to query the List method. +type ListOpts struct { + // DomainID filters the response by a domain ID. + DomainID string `q:"domain_id"` + + // Enabled filters the response by enabled projects. + Enabled *bool `q:"enabled"` + + // IsDomain filters the response by projects that are domains. + // Setting this to true is effectively listing domains. + IsDomain *bool `q:"is_domain"` + + // Name filters the response by project name. + Name string `q:"name"` + + // ParentID filters the response by projects of a given parent project. + ParentID string `q:"parent_id"` +} + +// ToProjectListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToProjectListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List enumerats the Projects to which the current token has access. +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToProjectListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ProjectPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves details on a single project, by ID. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to +// the Create request. +type CreateOptsBuilder interface { + ToProjectCreateMap() (map[string]interface{}, error) +} + +// CreateOpts allows you to modify the details included in the Create request. +type CreateOpts struct { + // DomainID is the ID this project will belong under. + DomainID string `json:"domain_id,omitempty"` + + // Enabled sets the project status to enabled or disabled. + Enabled *bool `json:"enabled,omitempty"` + + // IsDomain indicates if this project is a domain. + IsDomain *bool `json:"is_domain,omitempty"` + + // Name is the name of the project. + Name string `json:"name" required:"true"` + + // ParentID specifies the parent project of this new project. + ParentID string `json:"parent_id,omitempty"` + + // Description is the description of the project. + Description string `json:"description,omitempty"` +} + +// ToProjectCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToProjectCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "project") +} + +// Create creates a new Project. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToProjectCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, nil) + return +} + +// Delete deletes a project. +func Delete(client *gophercloud.ServiceClient, projectID string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, projectID), nil) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateOptsBuilder interface { + ToProjectUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts allows you to modify the details included in the Update request. +type UpdateOpts struct { + // DomainID is the ID this project will belong under. + DomainID string `json:"domain_id,omitempty"` + + // Enabled sets the project status to enabled or disabled. + Enabled *bool `json:"enabled,omitempty"` + + // IsDomain indicates if this project is a domain. + IsDomain *bool `json:"is_domain,omitempty"` + + // Name is the name of the project. + Name string `json:"name,omitempty"` + + // ParentID specifies the parent project of this new project. + ParentID string `json:"parent_id,omitempty"` + + // Description is the description of the project. + Description string `json:"description,omitempty"` +} + +// ToUpdateCreateMap formats a UpdateOpts into an update request. +func (opts UpdateOpts) ToProjectUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "project") +} + +// Update modifies the attributes of a project. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToProjectUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Patch(updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/projects/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/projects/results.go new file mode 100644 index 0000000000..a441e7f0f1 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/projects/results.go @@ -0,0 +1,98 @@ +package projects + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +type projectResult struct { + gophercloud.Result +} + +// GetResult temporarily contains the response from the Get call. +type GetResult struct { + projectResult +} + +// CreateResult temporarily contains the reponse from the Create call. +type CreateResult struct { + projectResult +} + +// DeleteResult temporarily contains the response from the Delete call. +type DeleteResult struct { + gophercloud.ErrResult +} + +// UpdateResult temporarily contains the response from the Update call. +type UpdateResult struct { + projectResult +} + +// Project is a base unit of ownership. +type Project struct { + // IsDomain indicates whether the project is a domain. + IsDomain bool `json:"is_domain"` + + // Description is the description of the project. + Description string `json:"description"` + + // DomainID is the domain ID the project belongs to. + DomainID string `json:"domain_id"` + + // Enabled is whether or not the project is enabled. + Enabled bool `json:"enabled"` + + // ID is the unique ID of the project. + ID string `json:"id"` + + // Name is the name of the project. + Name string `json:"name"` + + // ParentID is the parent_id of the project. + ParentID string `json:"parent_id"` +} + +// ProjectPage is a single page of Project results. +type ProjectPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of Projects contains any results. +func (r ProjectPage) IsEmpty() (bool, error) { + projects, err := ExtractProjects(r) + return len(projects) == 0, err +} + +// NextPageURL extracts the "next" link from the links section of the result. +func (r ProjectPage) NextPageURL() (string, error) { + var s struct { + Links struct { + Next string `json:"next"` + Previous string `json:"previous"` + } `json:"links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return s.Links.Next, err +} + +// ExtractProjects returns a slice of Projects contained in a single page of results. +func ExtractProjects(r pagination.Page) ([]Project, error) { + var s struct { + Projects []Project `json:"projects"` + } + err := (r.(ProjectPage)).ExtractInto(&s) + return s.Projects, err +} + +// Extract interprets any projectResults as a Project. +func (r projectResult) Extract() (*Project, error) { + var s struct { + Project *Project `json:"project"` + } + err := r.ExtractInto(&s) + return s.Project, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/projects/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/projects/testing/fixtures.go new file mode 100644 index 0000000000..caa55679a8 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/projects/testing/fixtures.go @@ -0,0 +1,192 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud/openstack/identity/v3/projects" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +// ListOutput provides a single page of Project results. +const ListOutput = ` +{ + "projects": [ + { + "is_domain": false, + "description": "The team that is red", + "domain_id": "default", + "enabled": true, + "id": "1234", + "name": "Red Team", + "parent_id": null + }, + { + "is_domain": false, + "description": "The team that is blue", + "domain_id": "default", + "enabled": true, + "id": "9876", + "name": "Blue Team", + "parent_id": null + } + ], + "links": { + "next": null, + "previous": null + } +} +` + +// GetOutput provides a Get result. +const GetOutput = ` +{ + "project": { + "is_domain": false, + "description": "The team that is red", + "domain_id": "default", + "enabled": true, + "id": "1234", + "name": "Red Team", + "parent_id": null + } +} +` + +// CreateRequest provides the input to a Create request. +const CreateRequest = ` +{ + "project": { + "description": "The team that is red", + "name": "Red Team" + } +} +` + +// UpdateRequest provides the input to an Update request. +const UpdateRequest = ` +{ + "project": { + "description": "The team that is bright red", + "name": "Bright Red Team" + } +} +` + +// UpdateOutput provides an Update response. +const UpdateOutput = ` +{ + "project": { + "is_domain": false, + "description": "The team that is bright red", + "domain_id": "default", + "enabled": true, + "id": "1234", + "name": "Bright Red Team", + "parent_id": null + } +} +` + +// RedTeam is a Project fixture. +var RedTeam = projects.Project{ + IsDomain: false, + Description: "The team that is red", + DomainID: "default", + Enabled: true, + ID: "1234", + Name: "Red Team", + ParentID: "", +} + +// BlueTeam is a Project fixture. +var BlueTeam = projects.Project{ + IsDomain: false, + Description: "The team that is blue", + DomainID: "default", + Enabled: true, + ID: "9876", + Name: "Blue Team", + ParentID: "", +} + +// UpdatedRedTeam is a Project Fixture. +var UpdatedRedTeam = projects.Project{ + IsDomain: false, + Description: "The team that is bright red", + DomainID: "default", + Enabled: true, + ID: "1234", + Name: "Bright Red Team", + ParentID: "", +} + +// ExpectedProjectSlice is the slice of projects expected to be returned from ListOutput. +var ExpectedProjectSlice = []projects.Project{RedTeam, BlueTeam} + +// HandleListProjectsSuccessfully creates an HTTP handler at `/projects` on the +// test handler mux that responds with a list of two tenants. +func HandleListProjectsSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/projects", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ListOutput) + }) +} + +// HandleGetProjectSuccessfully creates an HTTP handler at `/projects` on the +// test handler mux that responds with a single project. +func HandleGetProjectSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/projects/1234", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, GetOutput) + }) +} + +// HandleCreateProjectSuccessfully creates an HTTP handler at `/projects` on the +// test handler mux that tests project creation. +func HandleCreateProjectSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/projects", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, CreateRequest) + + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, GetOutput) + }) +} + +// HandleDeleteProjectSuccessfully creates an HTTP handler at `/projects` on the +// test handler mux that tests project deletion. +func HandleDeleteProjectSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/projects/1234", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleUpdateProjectSuccessfully creates an HTTP handler at `/projects` on the +// test handler mux that tests project updates. +func HandleUpdateProjectSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/projects/1234", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PATCH") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, UpdateRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, UpdateOutput) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/projects/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/projects/testing/requests_test.go new file mode 100644 index 0000000000..4b8af26a9c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/projects/testing/requests_test.go @@ -0,0 +1,79 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/identity/v3/projects" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestListProjects(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListProjectsSuccessfully(t) + + count := 0 + err := projects.List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + + actual, err := projects.ExtractProjects(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ExpectedProjectSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestGetProject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetProjectSuccessfully(t) + + actual, err := projects.Get(client.ServiceClient(), "1234").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, RedTeam, *actual) +} + +func TestCreateProject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateProjectSuccessfully(t) + + createOpts := projects.CreateOpts{ + Name: "Red Team", + Description: "The team that is red", + } + + actual, err := projects.Create(client.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, RedTeam, *actual) +} + +func TestDeleteProject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteProjectSuccessfully(t) + + res := projects.Delete(client.ServiceClient(), "1234") + th.AssertNoErr(t, res.Err) +} + +func TestUpdateProject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleUpdateProjectSuccessfully(t) + + updateOpts := projects.UpdateOpts{ + Name: "Bright Red Team", + Description: "The team that is bright red", + } + + actual, err := projects.Update(client.ServiceClient(), "1234", updateOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, UpdatedRedTeam, *actual) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/projects/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/projects/urls.go new file mode 100644 index 0000000000..e26cf3684c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/projects/urls.go @@ -0,0 +1,23 @@ +package projects + +import "github.com/gophercloud/gophercloud" + +func listURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("projects") +} + +func getURL(client *gophercloud.ServiceClient, projectID string) string { + return client.ServiceURL("projects", projectID) +} + +func createURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("projects") +} + +func deleteURL(client *gophercloud.ServiceClient, projectID string) string { + return client.ServiceURL("projects", projectID) +} + +func updateURL(client *gophercloud.ServiceClient, projectID string) string { + return client.ServiceURL("projects", projectID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/roles/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/roles/doc.go new file mode 100644 index 0000000000..bdbc674d65 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/roles/doc.go @@ -0,0 +1,3 @@ +// Package roles provides information and interaction with the roles API +// resource for the OpenStack Identity service. +package roles diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/roles/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/roles/requests.go new file mode 100644 index 0000000000..de65c51a78 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/roles/requests.go @@ -0,0 +1,47 @@ +package roles + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListAssignmentsOptsBuilder allows extensions to add additional parameters to +// the ListAssignments request. +type ListAssignmentsOptsBuilder interface { + ToRolesListAssignmentsQuery() (string, error) +} + +// ListAssignmentsOpts allows you to query the ListAssignments method. +// Specify one of or a combination of GroupId, RoleId, ScopeDomainId, ScopeProjectId, +// and/or UserId to search for roles assigned to corresponding entities. +// Effective lists effective assignments at the user, project, and domain level, +// allowing for the effects of group membership. +type ListAssignmentsOpts struct { + GroupID string `q:"group.id"` + RoleID string `q:"role.id"` + ScopeDomainID string `q:"scope.domain.id"` + ScopeProjectID string `q:"scope.project.id"` + UserID string `q:"user.id"` + Effective *bool `q:"effective"` +} + +// ToRolesListAssignmentsQuery formats a ListAssignmentsOpts into a query string. +func (opts ListAssignmentsOpts) ToRolesListAssignmentsQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// ListAssignments enumerates the roles assigned to a specified resource. +func ListAssignments(client *gophercloud.ServiceClient, opts ListAssignmentsOptsBuilder) pagination.Pager { + url := listAssignmentsURL(client) + if opts != nil { + query, err := opts.ToRolesListAssignmentsQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return RoleAssignmentPage{pagination.LinkedPageBase{PageResult: r}} + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/roles/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/roles/results.go new file mode 100644 index 0000000000..e8a3aa9a90 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/roles/results.go @@ -0,0 +1,67 @@ +package roles + +import "github.com/gophercloud/gophercloud/pagination" + +// RoleAssignment is the result of a role assignments query. +type RoleAssignment struct { + Role Role `json:"role,omitempty"` + Scope Scope `json:"scope,omitempty"` + User User `json:"user,omitempty"` + Group Group `json:"group,omitempty"` +} + +type Role struct { + ID string `json:"id,omitempty"` +} + +type Scope struct { + Domain Domain `json:"domain,omitempty"` + Project Project `json:"project,omitempty"` +} + +type Domain struct { + ID string `json:"id,omitempty"` +} + +type Project struct { + ID string `json:"id,omitempty"` +} + +type User struct { + ID string `json:"id,omitempty"` +} + +type Group struct { + ID string `json:"id,omitempty"` +} + +// RoleAssignmentPage is a single page of RoleAssignments results. +type RoleAssignmentPage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if the page contains no results. +func (r RoleAssignmentPage) IsEmpty() (bool, error) { + roleAssignments, err := ExtractRoleAssignments(r) + return len(roleAssignments) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to the next page of results. +func (r RoleAssignmentPage) NextPageURL() (string, error) { + var s struct { + Links struct { + Next string `json:"next"` + } `json:"links"` + } + err := r.ExtractInto(&s) + return s.Links.Next, err +} + +// ExtractRoleAssignments extracts a slice of RoleAssignments from a Collection acquired from List. +func ExtractRoleAssignments(r pagination.Page) ([]RoleAssignment, error) { + var s struct { + RoleAssignments []RoleAssignment `json:"role_assignments"` + } + err := (r.(RoleAssignmentPage)).ExtractInto(&s) + return s.RoleAssignments, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/roles/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/roles/testing/doc.go new file mode 100644 index 0000000000..37bcb85a59 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/roles/testing/doc.go @@ -0,0 +1,2 @@ +// identity_roles_v3 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/roles/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/roles/testing/requests_test.go new file mode 100644 index 0000000000..dd9b704d8d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/roles/testing/requests_test.go @@ -0,0 +1,105 @@ +package testing + +import ( + "fmt" + "net/http" + "reflect" + "testing" + + "github.com/gophercloud/gophercloud/openstack/identity/v3/roles" + "github.com/gophercloud/gophercloud/pagination" + "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestListSinglePage(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + testhelper.Mux.HandleFunc("/role_assignments", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "GET") + testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "role_assignments": [ + { + "links": { + "assignment": "http://identity:35357/v3/domains/161718/users/313233/roles/123456" + }, + "role": { + "id": "123456" + }, + "scope": { + "domain": { + "id": "161718" + } + }, + "user": { + "id": "313233" + } + }, + { + "links": { + "assignment": "http://identity:35357/v3/projects/456789/groups/101112/roles/123456", + "membership": "http://identity:35357/v3/groups/101112/users/313233" + }, + "role": { + "id": "123456" + }, + "scope": { + "project": { + "id": "456789" + } + }, + "user": { + "id": "313233" + } + } + ], + "links": { + "self": "http://identity:35357/v3/role_assignments?effective", + "previous": null, + "next": null + } + } + `) + }) + + count := 0 + err := roles.ListAssignments(client.ServiceClient(), roles.ListAssignmentsOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := roles.ExtractRoleAssignments(page) + if err != nil { + return false, err + } + + expected := []roles.RoleAssignment{ + { + Role: roles.Role{ID: "123456"}, + Scope: roles.Scope{Domain: roles.Domain{ID: "161718"}}, + User: roles.User{ID: "313233"}, + Group: roles.Group{}, + }, + { + Role: roles.Role{ID: "123456"}, + Scope: roles.Scope{Project: roles.Project{ID: "456789"}}, + User: roles.User{ID: "313233"}, + Group: roles.Group{}, + }, + } + + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Expected %#v, got %#v", expected, actual) + } + + return true, nil + }) + if err != nil { + t.Errorf("Unexpected error while paging: %v", err) + } + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/roles/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/roles/urls.go new file mode 100644 index 0000000000..8d87b6e7d4 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/roles/urls.go @@ -0,0 +1,7 @@ +package roles + +import "github.com/gophercloud/gophercloud" + +func listAssignmentsURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("role_assignments") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/services/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/services/doc.go new file mode 100644 index 0000000000..fa56411856 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/services/doc.go @@ -0,0 +1,3 @@ +// Package services provides information and interaction with the services API +// resource for the OpenStack Identity service. +package services diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/services/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/services/requests.go new file mode 100644 index 0000000000..bb7bb04761 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/services/requests.go @@ -0,0 +1,64 @@ +package services + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Create adds a new service of the requested type to the catalog. +func Create(client *gophercloud.ServiceClient, serviceType string) (r CreateResult) { + b := map[string]string{"type": serviceType} + _, r.Err = client.Post(listURL(client), b, &r.Body, nil) + return +} + +type ListOptsBuilder interface { + ToServiceListMap() (string, error) +} + +// ListOpts allows you to query the List method. +type ListOpts struct { + ServiceType string `q:"type"` + PerPage int `q:"perPage"` + Page int `q:"page"` +} + +func (opts ListOpts) ToServiceListMap() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List enumerates the services available to a specific user. +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + u := listURL(client) + if opts != nil { + q, err := opts.ToServiceListMap() + if err != nil { + return pagination.Pager{Err: err} + } + u += q + } + return pagination.NewPager(client, u, func(r pagination.PageResult) pagination.Page { + return ServicePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get returns additional information about a service, given its ID. +func Get(client *gophercloud.ServiceClient, serviceID string) (r GetResult) { + _, r.Err = client.Get(serviceURL(client, serviceID), &r.Body, nil) + return +} + +// Update changes the service type of an existing service. +func Update(client *gophercloud.ServiceClient, serviceID string, serviceType string) (r UpdateResult) { + b := map[string]string{"type": serviceType} + _, r.Err = client.Patch(serviceURL(client, serviceID), &b, &r.Body, nil) + return +} + +// Delete removes an existing service. +// It either deletes all associated endpoints, or fails until all endpoints are deleted. +func Delete(client *gophercloud.ServiceClient, serviceID string) (r DeleteResult) { + _, r.Err = client.Delete(serviceURL(client, serviceID), nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/services/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/services/results.go new file mode 100644 index 0000000000..3942f5e4d1 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/services/results.go @@ -0,0 +1,68 @@ +package services + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// Extract interprets a GetResult, CreateResult or UpdateResult as a concrete Service. +// An error is returned if the original call or the extraction failed. +func (r commonResult) Extract() (*Service, error) { + var s struct { + Service *Service `json:"service"` + } + err := r.ExtractInto(&s) + return s.Service, err +} + +// CreateResult is the deferred result of a Create call. +type CreateResult struct { + commonResult +} + +// GetResult is the deferred result of a Get call. +type GetResult struct { + commonResult +} + +// UpdateResult is the deferred result of an Update call. +type UpdateResult struct { + commonResult +} + +// DeleteResult is the deferred result of an Delete call. +type DeleteResult struct { + gophercloud.ErrResult +} + +// Service is the result of a list or information query. +type Service struct { + Description string `json:"description"` + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` +} + +// ServicePage is a single page of Service results. +type ServicePage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if the page contains no results. +func (p ServicePage) IsEmpty() (bool, error) { + services, err := ExtractServices(p) + return len(services) == 0, err +} + +// ExtractServices extracts a slice of Services from a Collection acquired from List. +func ExtractServices(r pagination.Page) ([]Service, error) { + var s struct { + Services []Service `json:"services"` + } + err := (r.(ServicePage)).ExtractInto(&s) + return s.Services, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/services/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/services/testing/doc.go new file mode 100644 index 0000000000..e4f1167ebc --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/services/testing/doc.go @@ -0,0 +1,2 @@ +// identity_services_v3 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/services/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/services/testing/requests_test.go new file mode 100644 index 0000000000..0a065a2afc --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/services/testing/requests_test.go @@ -0,0 +1,187 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud/openstack/identity/v3/services" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestCreateSuccessful(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "type": "compute" }`) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `{ + "service": { + "description": "Here's your service", + "id": "1234", + "name": "InscrutableOpenStackProjectName", + "type": "compute" + } + }`) + }) + + expected := &services.Service{ + Description: "Here's your service", + ID: "1234", + Name: "InscrutableOpenStackProjectName", + Type: "compute", + } + + actual, err := services.Create(client.ServiceClient(), "compute").Extract() + if err != nil { + t.Fatalf("Unexpected error from Create: %v", err) + } + th.AssertDeepEquals(t, expected, actual) +} + +func TestListSinglePage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "links": { + "next": null, + "previous": null + }, + "services": [ + { + "description": "Service One", + "id": "1234", + "name": "service-one", + "type": "identity" + }, + { + "description": "Service Two", + "id": "9876", + "name": "service-two", + "type": "compute" + } + ] + } + `) + }) + + count := 0 + err := services.List(client.ServiceClient(), services.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := services.ExtractServices(page) + if err != nil { + return false, err + } + + expected := []services.Service{ + { + Description: "Service One", + ID: "1234", + Name: "service-one", + Type: "identity", + }, + { + Description: "Service Two", + ID: "9876", + Name: "service-two", + Type: "compute", + }, + } + th.AssertDeepEquals(t, expected, actual) + return true, nil + }) + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, count) +} + +func TestGetSuccessful(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/services/12345", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "service": { + "description": "Service One", + "id": "12345", + "name": "service-one", + "type": "identity" + } + } + `) + }) + + actual, err := services.Get(client.ServiceClient(), "12345").Extract() + th.AssertNoErr(t, err) + + expected := &services.Service{ + ID: "12345", + Description: "Service One", + Name: "service-one", + Type: "identity", + } + + th.AssertDeepEquals(t, expected, actual) +} + +func TestUpdateSuccessful(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/services/12345", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PATCH") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "type": "lasermagic" }`) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "service": { + "id": "12345", + "type": "lasermagic" + } + } + `) + }) + + expected := &services.Service{ + ID: "12345", + Type: "lasermagic", + } + + actual, err := services.Update(client.ServiceClient(), "12345", "lasermagic").Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expected, actual) +} + +func TestDeleteSuccessful(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/services/12345", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := services.Delete(client.ServiceClient(), "12345") + th.AssertNoErr(t, res.Err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/services/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/services/urls.go new file mode 100644 index 0000000000..c5ae268379 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/services/urls.go @@ -0,0 +1,11 @@ +package services + +import "github.com/gophercloud/gophercloud" + +func listURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("services") +} + +func serviceURL(client *gophercloud.ServiceClient, serviceID string) string { + return client.ServiceURL("services", serviceID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/doc.go new file mode 100644 index 0000000000..76ff5f4738 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/doc.go @@ -0,0 +1,6 @@ +// Package tokens provides information and interaction with the token API +// resource for the OpenStack Identity service. +// +// For more information, see: +// http://developer.openstack.org/api-ref-identity-v3.html#tokens-v3 +package tokens diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/requests.go new file mode 100644 index 0000000000..39c19aee3c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/requests.go @@ -0,0 +1,200 @@ +package tokens + +import "github.com/gophercloud/gophercloud" + +// Scope allows a created token to be limited to a specific domain or project. +type Scope struct { + ProjectID string + ProjectName string + DomainID string + DomainName string +} + +// AuthOptionsBuilder describes any argument that may be passed to the Create call. +type AuthOptionsBuilder interface { + // ToTokenV3CreateMap assembles the Create request body, returning an error if parameters are + // missing or inconsistent. + ToTokenV3CreateMap(map[string]interface{}) (map[string]interface{}, error) + ToTokenV3ScopeMap() (map[string]interface{}, error) + CanReauth() bool +} + +type AuthOptions struct { + // IdentityEndpoint specifies the HTTP endpoint that is required to work with + // the Identity API of the appropriate version. While it's ultimately needed by + // all of the identity services, it will often be populated by a provider-level + // function. + IdentityEndpoint string `json:"-"` + + // Username is required if using Identity V2 API. Consult with your provider's + // control panel to discover your account's username. In Identity V3, either + // UserID or a combination of Username and DomainID or DomainName are needed. + Username string `json:"username,omitempty"` + UserID string `json:"id,omitempty"` + + Password string `json:"password,omitempty"` + + // At most one of DomainID and DomainName must be provided if using Username + // with Identity V3. Otherwise, either are optional. + DomainID string `json:"-"` + DomainName string `json:"name,omitempty"` + + // AllowReauth should be set to true if you grant permission for Gophercloud to + // cache your credentials in memory, and to allow Gophercloud to attempt to + // re-authenticate automatically if/when your token expires. If you set it to + // false, it will not cache these settings, but re-authentication will not be + // possible. This setting defaults to false. + AllowReauth bool `json:"-"` + + // TokenID allows users to authenticate (possibly as another user) with an + // authentication token ID. + TokenID string `json:"-"` + + Scope Scope `json:"-"` +} + +func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[string]interface{}, error) { + gophercloudAuthOpts := gophercloud.AuthOptions{ + Username: opts.Username, + UserID: opts.UserID, + Password: opts.Password, + DomainID: opts.DomainID, + DomainName: opts.DomainName, + AllowReauth: opts.AllowReauth, + TokenID: opts.TokenID, + } + + return gophercloudAuthOpts.ToTokenV3CreateMap(scope) +} + +func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) { + if opts.Scope.ProjectName != "" { + // ProjectName provided: either DomainID or DomainName must also be supplied. + // ProjectID may not be supplied. + if opts.Scope.DomainID == "" && opts.Scope.DomainName == "" { + return nil, gophercloud.ErrScopeDomainIDOrDomainName{} + } + if opts.Scope.ProjectID != "" { + return nil, gophercloud.ErrScopeProjectIDOrProjectName{} + } + + if opts.Scope.DomainID != "" { + // ProjectName + DomainID + return map[string]interface{}{ + "project": map[string]interface{}{ + "name": &opts.Scope.ProjectName, + "domain": map[string]interface{}{"id": &opts.Scope.DomainID}, + }, + }, nil + } + + if opts.Scope.DomainName != "" { + // ProjectName + DomainName + return map[string]interface{}{ + "project": map[string]interface{}{ + "name": &opts.Scope.ProjectName, + "domain": map[string]interface{}{"name": &opts.Scope.DomainName}, + }, + }, nil + } + } else if opts.Scope.ProjectID != "" { + // ProjectID provided. ProjectName, DomainID, and DomainName may not be provided. + if opts.Scope.DomainID != "" { + return nil, gophercloud.ErrScopeProjectIDAlone{} + } + if opts.Scope.DomainName != "" { + return nil, gophercloud.ErrScopeProjectIDAlone{} + } + + // ProjectID + return map[string]interface{}{ + "project": map[string]interface{}{ + "id": &opts.Scope.ProjectID, + }, + }, nil + } else if opts.Scope.DomainID != "" { + // DomainID provided. ProjectID, ProjectName, and DomainName may not be provided. + if opts.Scope.DomainName != "" { + return nil, gophercloud.ErrScopeDomainIDOrDomainName{} + } + + // DomainID + return map[string]interface{}{ + "domain": map[string]interface{}{ + "id": &opts.Scope.DomainID, + }, + }, nil + } else if opts.Scope.DomainName != "" { + return nil, gophercloud.ErrScopeDomainName{} + } + + return nil, nil +} + +func (opts *AuthOptions) CanReauth() bool { + return opts.AllowReauth +} + +func subjectTokenHeaders(c *gophercloud.ServiceClient, subjectToken string) map[string]string { + return map[string]string{ + "X-Subject-Token": subjectToken, + } +} + +// Create authenticates and either generates a new token, or changes the Scope of an existing token. +func Create(c *gophercloud.ServiceClient, opts AuthOptionsBuilder) (r CreateResult) { + scope, err := opts.ToTokenV3ScopeMap() + if err != nil { + r.Err = err + return + } + + b, err := opts.ToTokenV3CreateMap(scope) + if err != nil { + r.Err = err + return + } + + resp, err := c.Post(tokenURL(c), b, &r.Body, &gophercloud.RequestOpts{ + MoreHeaders: map[string]string{"X-Auth-Token": ""}, + }) + r.Err = err + if resp != nil { + r.Header = resp.Header + } + return +} + +// Get validates and retrieves information about another token. +func Get(c *gophercloud.ServiceClient, token string) (r GetResult) { + resp, err := c.Get(tokenURL(c), &r.Body, &gophercloud.RequestOpts{ + MoreHeaders: subjectTokenHeaders(c, token), + OkCodes: []int{200, 203}, + }) + if resp != nil { + r.Err = err + r.Header = resp.Header + } + return +} + +// Validate determines if a specified token is valid or not. +func Validate(c *gophercloud.ServiceClient, token string) (bool, error) { + resp, err := c.Request("HEAD", tokenURL(c), &gophercloud.RequestOpts{ + MoreHeaders: subjectTokenHeaders(c, token), + OkCodes: []int{200, 204, 404}, + }) + if err != nil { + return false, err + } + + return resp.StatusCode == 200 || resp.StatusCode == 204, nil +} + +// Revoke immediately makes specified token invalid. +func Revoke(c *gophercloud.ServiceClient, token string) (r RevokeResult) { + _, r.Err = c.Delete(tokenURL(c), &gophercloud.RequestOpts{ + MoreHeaders: subjectTokenHeaders(c, token), + }) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/results.go new file mode 100644 index 0000000000..7c306e83fe --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/results.go @@ -0,0 +1,156 @@ +package tokens + +import ( + "time" + + "github.com/gophercloud/gophercloud" +) + +// Endpoint represents a single API endpoint offered by a service. +// It matches either a public, internal or admin URL. +// If supported, it contains a region specifier, again if provided. +// The significance of the Region field will depend upon your provider. +type Endpoint struct { + ID string `json:"id"` + Region string `json:"region"` + Interface string `json:"interface"` + URL string `json:"url"` +} + +// CatalogEntry provides a type-safe interface to an Identity API V3 service catalog listing. +// Each class of service, such as cloud DNS or block storage services, could have multiple +// CatalogEntry representing it (one by interface type, e.g public, admin or internal). +// +// Note: when looking for the desired service, try, whenever possible, to key off the type field. +// Otherwise, you'll tie the representation of the service to a specific provider. +type CatalogEntry struct { + // Service ID + ID string `json:"id"` + // Name will contain the provider-specified name for the service. + Name string `json:"name"` + // Type will contain a type string if OpenStack defines a type for the service. + // Otherwise, for provider-specific services, the provider may assign their own type strings. + Type string `json:"type"` + // Endpoints will let the caller iterate over all the different endpoints that may exist for + // the service. + Endpoints []Endpoint `json:"endpoints"` +} + +// ServiceCatalog provides a view into the service catalog from a previous, successful authentication. +type ServiceCatalog struct { + Entries []CatalogEntry `json:"catalog"` +} + +// Domain provides information about the domain to which this token grants access. +type Domain struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// User represents a user resource that exists on the API. +type User struct { + Domain Domain `json:"domain"` + ID string `json:"id"` + Name string `json:"name"` +} + +// Role provides information about roles to which User is authorized. +type Role struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// Project provides information about project to which User is authorized. +type Project struct { + Domain Domain `json:"domain"` + ID string `json:"id"` + Name string `json:"name"` +} + +// commonResult is the deferred result of a Create or a Get call. +type commonResult struct { + gophercloud.Result +} + +// Extract is a shortcut for ExtractToken. +// This function is deprecated and still present for backward compatibility. +func (r commonResult) Extract() (*Token, error) { + return r.ExtractToken() +} + +// ExtractToken interprets a commonResult as a Token. +func (r commonResult) ExtractToken() (*Token, error) { + var s Token + err := r.ExtractInto(&s) + if err != nil { + return nil, err + } + + // Parse the token itself from the stored headers. + s.ID = r.Header.Get("X-Subject-Token") + + return &s, err +} + +// ExtractServiceCatalog returns the ServiceCatalog that was generated along with the user's Token. +func (r commonResult) ExtractServiceCatalog() (*ServiceCatalog, error) { + var s ServiceCatalog + err := r.ExtractInto(&s) + return &s, err +} + +// ExtractUser returns the User that is the owner of the Token. +func (r commonResult) ExtractUser() (*User, error) { + var s struct { + User *User `json:"user"` + } + err := r.ExtractInto(&s) + return s.User, err +} + +// ExtractRoles returns Roles to which User is authorized. +func (r commonResult) ExtractRoles() ([]Role, error) { + var s struct { + Roles []Role `json:"roles"` + } + err := r.ExtractInto(&s) + return s.Roles, err +} + +// ExtractProject returns Project to which User is authorized. +func (r commonResult) ExtractProject() (*Project, error) { + var s struct { + Project *Project `json:"project"` + } + err := r.ExtractInto(&s) + return s.Project, err +} + +// CreateResult defers the interpretation of a created token. +// Use ExtractToken() to interpret it as a Token, or ExtractServiceCatalog() to interpret it as a service catalog. +type CreateResult struct { + commonResult +} + +// GetResult is the deferred response from a Get call. +type GetResult struct { + commonResult +} + +// RevokeResult is the deferred response from a Revoke call. +type RevokeResult struct { + commonResult +} + +// Token is a string that grants a user access to a controlled set of services in an OpenStack provider. +// Each Token is valid for a set length of time. +type Token struct { + // ID is the issued token. + ID string `json:"id"` + // ExpiresAt is the timestamp at which this token will no longer be accepted. + ExpiresAt time.Time `json:"expires_at"` +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.ExtractIntoStructPtr(v, "token") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/testing/doc.go new file mode 100644 index 0000000000..ad1d35de9b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/testing/doc.go @@ -0,0 +1,2 @@ +// identity_tokens_v3 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/testing/fixtures.go new file mode 100644 index 0000000000..a475acb1b7 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/testing/fixtures.go @@ -0,0 +1,211 @@ +package testing + +import ( + "encoding/json" + "net/http" + "testing" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens" + "github.com/gophercloud/gophercloud/testhelper" +) + +const testTokenID = "130f6c17-420e-4a0b-97b0-0c9cf2a05f30" + +// TokenOutput is a sample response to a Token call. +const TokenOutput = ` +{ + "token":{ + "is_domain":false, + "methods":[ + "password" + ], + "roles":[ + { + "id":"434426788d5a451faf763b0e6db5aefb", + "name":"admin" + } + ], + "expires_at":"2017-06-03T02:19:49.000000Z", + "project":{ + "domain":{ + "id":"default", + "name":"Default" + }, + "id":"a99e9b4e620e4db09a2dfb6e42a01e66", + "name":"admin" + }, + "catalog":[ + { + "endpoints":[ + { + "url":"http://127.0.0.1:8774/v2.1/a99e9b4e620e4db09a2dfb6e42a01e66", + "interface":"admin", + "region":"RegionOne", + "region_id":"RegionOne", + "id":"3eac9e7588eb4eb2a4650cf5e079505f" + }, + { + "url":"http://127.0.0.1:8774/v2.1/a99e9b4e620e4db09a2dfb6e42a01e66", + "interface":"internal", + "region":"RegionOne", + "region_id":"RegionOne", + "id":"6b33fabc69c34ea782a3f6282582b59f" + }, + { + "url":"http://127.0.0.1:8774/v2.1/a99e9b4e620e4db09a2dfb6e42a01e66", + "interface":"public", + "region":"RegionOne", + "region_id":"RegionOne", + "id":"dae63c71bee24070a71f5425e7a916b5" + } + ], + "type":"compute", + "id":"17e0fa04647d4155a7933ee624dd66da", + "name":"nova" + }, + { + "endpoints":[ + { + "url":"http://127.0.0.1:35357/v3", + "interface":"admin", + "region":"RegionOne", + "region_id":"RegionOne", + "id":"0539aeff80954a0bb756cec496768d3d" + }, + { + "url":"http://127.0.0.1:5000/v3", + "interface":"public", + "region":"RegionOne", + "region_id":"RegionOne", + "id":"15bdf2d0853e4c939993d29548b1b56f" + }, + { + "url":"http://127.0.0.1:5000/v3", + "interface":"internal", + "region":"RegionOne", + "region_id":"RegionOne", + "id":"3b4423c54ba343c58226bc424cb11c4b" + } + ], + "type":"identity", + "id":"1cde0ea8cb3c49d8928cb172ca825ca5", + "name":"keystone" + } + ], + "user":{ + "domain":{ + "id":"default", + "name":"Default" + }, + "password_expires_at":null, + "name":"admin", + "id":"0fe36e73809d46aeae6705c39077b1b3" + }, + "audit_ids":[ + "ysSI0bEWR0Gmrp4LHL9LFw" + ], + "issued_at":"2017-06-03T01:19:49.000000Z" + } +}` + +var expectedTokenTime, _ = time.Parse(gophercloud.RFC3339Milli, + "2017-06-03T02:19:49.000000Z") +var ExpectedToken = tokens.Token{ + ID: testTokenID, + ExpiresAt: expectedTokenTime, +} + +var catalogEntry1 = tokens.CatalogEntry{ + ID: "17e0fa04647d4155a7933ee624dd66da", + Name: "nova", + Type: "compute", + Endpoints: []tokens.Endpoint{ + tokens.Endpoint{ + ID: "3eac9e7588eb4eb2a4650cf5e079505f", + Region: "RegionOne", + Interface: "admin", + URL: "http://127.0.0.1:8774/v2.1/a99e9b4e620e4db09a2dfb6e42a01e66", + }, + tokens.Endpoint{ + ID: "6b33fabc69c34ea782a3f6282582b59f", + Region: "RegionOne", + Interface: "internal", + URL: "http://127.0.0.1:8774/v2.1/a99e9b4e620e4db09a2dfb6e42a01e66", + }, + tokens.Endpoint{ + ID: "dae63c71bee24070a71f5425e7a916b5", + Region: "RegionOne", + Interface: "public", + URL: "http://127.0.0.1:8774/v2.1/a99e9b4e620e4db09a2dfb6e42a01e66", + }, + }, +} +var catalogEntry2 = tokens.CatalogEntry{ + ID: "1cde0ea8cb3c49d8928cb172ca825ca5", + Name: "keystone", + Type: "identity", + Endpoints: []tokens.Endpoint{ + tokens.Endpoint{ + ID: "0539aeff80954a0bb756cec496768d3d", + Region: "RegionOne", + Interface: "admin", + URL: "http://127.0.0.1:35357/v3", + }, + tokens.Endpoint{ + ID: "15bdf2d0853e4c939993d29548b1b56f", + Region: "RegionOne", + Interface: "public", + URL: "http://127.0.0.1:5000/v3", + }, + tokens.Endpoint{ + ID: "3b4423c54ba343c58226bc424cb11c4b", + Region: "RegionOne", + Interface: "internal", + URL: "http://127.0.0.1:5000/v3", + }, + }, +} + +// ExpectedServiceCatalog contains expected service extracted from token response. +var ExpectedServiceCatalog = tokens.ServiceCatalog{ + Entries: []tokens.CatalogEntry{catalogEntry1, catalogEntry2}, +} + +var domain = tokens.Domain{ + ID: "default", + Name: "Default", +} + +// ExpectedUser contains expected user extracted from token response. +var ExpectedUser = tokens.User{ + Domain: domain, + ID: "0fe36e73809d46aeae6705c39077b1b3", + Name: "admin", +} + +var role = tokens.Role{ + ID: "434426788d5a451faf763b0e6db5aefb", + Name: "admin", +} + +// ExpectedRoles contains expected roles extracted from token response. +var ExpectedRoles = []tokens.Role{role} + +// ExpectedProject contains expected project extracted from token response. +var ExpectedProject = tokens.Project{ + Domain: domain, + ID: "a99e9b4e620e4db09a2dfb6e42a01e66", + Name: "admin", +} + +func getGetResult(t *testing.T) tokens.GetResult { + result := tokens.GetResult{} + result.Header = http.Header{ + "X-Subject-Token": []string{testTokenID}, + } + err := json.Unmarshal([]byte(TokenOutput), &result.Body) + testhelper.AssertNoErr(t, err) + return result +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/testing/requests_test.go new file mode 100644 index 0000000000..41f116218e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/testing/requests_test.go @@ -0,0 +1,532 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens" + "github.com/gophercloud/gophercloud/testhelper" +) + +// authTokenPost verifies that providing certain AuthOptions and Scope results in an expected JSON structure. +func authTokenPost(t *testing.T, options tokens.AuthOptions, scope *tokens.Scope, requestJSON string) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + client := gophercloud.ServiceClient{ + ProviderClient: &gophercloud.ProviderClient{}, + Endpoint: testhelper.Endpoint(), + } + + testhelper.Mux.HandleFunc("/auth/tokens", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "POST") + testhelper.TestHeader(t, r, "Content-Type", "application/json") + testhelper.TestHeader(t, r, "Accept", "application/json") + testhelper.TestJSONRequest(t, r, requestJSON) + + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `{ + "token": { + "expires_at": "2014-10-02T13:45:00.000000Z" + } + }`) + }) + + if scope != nil { + options.Scope = *scope + } + + expected := &tokens.Token{ + ExpiresAt: time.Date(2014, 10, 2, 13, 45, 0, 0, time.UTC), + } + actual, err := tokens.Create(&client, &options).Extract() + testhelper.AssertNoErr(t, err) + testhelper.CheckDeepEquals(t, expected, actual) +} + +func authTokenPostErr(t *testing.T, options tokens.AuthOptions, scope *tokens.Scope, includeToken bool, expectedErr error) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + client := gophercloud.ServiceClient{ + ProviderClient: &gophercloud.ProviderClient{}, + Endpoint: testhelper.Endpoint(), + } + if includeToken { + client.TokenID = "abcdef123456" + } + + if scope != nil { + options.Scope = *scope + } + + _, err := tokens.Create(&client, &options).Extract() + if err == nil { + t.Errorf("Create did NOT return an error") + } + if err != expectedErr { + t.Errorf("Create returned an unexpected error: wanted %v, got %v", expectedErr, err) + } +} + +func TestCreateUserIDAndPassword(t *testing.T) { + authTokenPost(t, tokens.AuthOptions{UserID: "me", Password: "squirrel!"}, nil, ` + { + "auth": { + "identity": { + "methods": ["password"], + "password": { + "user": { "id": "me", "password": "squirrel!" } + } + } + } + } + `) +} + +func TestCreateUsernameDomainIDPassword(t *testing.T) { + authTokenPost(t, tokens.AuthOptions{Username: "fakey", Password: "notpassword", DomainID: "abc123"}, nil, ` + { + "auth": { + "identity": { + "methods": ["password"], + "password": { + "user": { + "domain": { + "id": "abc123" + }, + "name": "fakey", + "password": "notpassword" + } + } + } + } + } + `) +} + +func TestCreateUsernameDomainNamePassword(t *testing.T) { + authTokenPost(t, tokens.AuthOptions{Username: "frank", Password: "swordfish", DomainName: "spork.net"}, nil, ` + { + "auth": { + "identity": { + "methods": ["password"], + "password": { + "user": { + "domain": { + "name": "spork.net" + }, + "name": "frank", + "password": "swordfish" + } + } + } + } + } + `) +} + +func TestCreateTokenID(t *testing.T) { + authTokenPost(t, tokens.AuthOptions{TokenID: "12345abcdef"}, nil, ` + { + "auth": { + "identity": { + "methods": ["token"], + "token": { + "id": "12345abcdef" + } + } + } + } + `) +} + +func TestCreateProjectIDScope(t *testing.T) { + options := tokens.AuthOptions{UserID: "fenris", Password: "g0t0h311"} + scope := &tokens.Scope{ProjectID: "123456"} + authTokenPost(t, options, scope, ` + { + "auth": { + "identity": { + "methods": ["password"], + "password": { + "user": { + "id": "fenris", + "password": "g0t0h311" + } + } + }, + "scope": { + "project": { + "id": "123456" + } + } + } + } + `) +} + +func TestCreateDomainIDScope(t *testing.T) { + options := tokens.AuthOptions{UserID: "fenris", Password: "g0t0h311"} + scope := &tokens.Scope{DomainID: "1000"} + authTokenPost(t, options, scope, ` + { + "auth": { + "identity": { + "methods": ["password"], + "password": { + "user": { + "id": "fenris", + "password": "g0t0h311" + } + } + }, + "scope": { + "domain": { + "id": "1000" + } + } + } + } + `) +} + +func TestCreateProjectNameAndDomainIDScope(t *testing.T) { + options := tokens.AuthOptions{UserID: "fenris", Password: "g0t0h311"} + scope := &tokens.Scope{ProjectName: "world-domination", DomainID: "1000"} + authTokenPost(t, options, scope, ` + { + "auth": { + "identity": { + "methods": ["password"], + "password": { + "user": { + "id": "fenris", + "password": "g0t0h311" + } + } + }, + "scope": { + "project": { + "domain": { + "id": "1000" + }, + "name": "world-domination" + } + } + } + } + `) +} + +func TestCreateProjectNameAndDomainNameScope(t *testing.T) { + options := tokens.AuthOptions{UserID: "fenris", Password: "g0t0h311"} + scope := &tokens.Scope{ProjectName: "world-domination", DomainName: "evil-plans"} + authTokenPost(t, options, scope, ` + { + "auth": { + "identity": { + "methods": ["password"], + "password": { + "user": { + "id": "fenris", + "password": "g0t0h311" + } + } + }, + "scope": { + "project": { + "domain": { + "name": "evil-plans" + }, + "name": "world-domination" + } + } + } + } + `) +} + +func TestCreateExtractsTokenFromResponse(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + client := gophercloud.ServiceClient{ + ProviderClient: &gophercloud.ProviderClient{}, + Endpoint: testhelper.Endpoint(), + } + + testhelper.Mux.HandleFunc("/auth/tokens", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("X-Subject-Token", "aaa111") + + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `{ + "token": { + "expires_at": "2014-10-02T13:45:00.000000Z" + } + }`) + }) + + options := tokens.AuthOptions{UserID: "me", Password: "shhh"} + token, err := tokens.Create(&client, &options).Extract() + if err != nil { + t.Fatalf("Create returned an error: %v", err) + } + + if token.ID != "aaa111" { + t.Errorf("Expected token to be aaa111, but was %s", token.ID) + } +} + +func TestCreateFailureEmptyAuth(t *testing.T) { + authTokenPostErr(t, tokens.AuthOptions{}, nil, false, gophercloud.ErrMissingPassword{}) +} + +func TestCreateFailureTokenIDUsername(t *testing.T) { + authTokenPostErr(t, tokens.AuthOptions{Username: "something", TokenID: "12345"}, nil, true, gophercloud.ErrUsernameWithToken{}) +} + +func TestCreateFailureTokenIDUserID(t *testing.T) { + authTokenPostErr(t, tokens.AuthOptions{UserID: "something", TokenID: "12345"}, nil, true, gophercloud.ErrUserIDWithToken{}) +} + +func TestCreateFailureTokenIDDomainID(t *testing.T) { + authTokenPostErr(t, tokens.AuthOptions{DomainID: "something", TokenID: "12345"}, nil, true, gophercloud.ErrDomainIDWithToken{}) +} + +func TestCreateFailureTokenIDDomainName(t *testing.T) { + authTokenPostErr(t, tokens.AuthOptions{DomainName: "something", TokenID: "12345"}, nil, true, gophercloud.ErrDomainNameWithToken{}) +} + +func TestCreateFailureMissingUser(t *testing.T) { + options := tokens.AuthOptions{Password: "supersecure"} + authTokenPostErr(t, options, nil, false, gophercloud.ErrUsernameOrUserID{}) +} + +func TestCreateFailureBothUser(t *testing.T) { + options := tokens.AuthOptions{ + Password: "supersecure", + Username: "oops", + UserID: "redundancy", + } + authTokenPostErr(t, options, nil, false, gophercloud.ErrUsernameOrUserID{}) +} + +func TestCreateFailureMissingDomain(t *testing.T) { + options := tokens.AuthOptions{ + Password: "supersecure", + Username: "notuniqueenough", + } + authTokenPostErr(t, options, nil, false, gophercloud.ErrDomainIDOrDomainName{}) +} + +func TestCreateFailureBothDomain(t *testing.T) { + options := tokens.AuthOptions{ + Password: "supersecure", + Username: "someone", + DomainID: "hurf", + DomainName: "durf", + } + authTokenPostErr(t, options, nil, false, gophercloud.ErrDomainIDOrDomainName{}) +} + +func TestCreateFailureUserIDDomainID(t *testing.T) { + options := tokens.AuthOptions{ + UserID: "100", + Password: "stuff", + DomainID: "oops", + } + authTokenPostErr(t, options, nil, false, gophercloud.ErrDomainIDWithUserID{}) +} + +func TestCreateFailureUserIDDomainName(t *testing.T) { + options := tokens.AuthOptions{ + UserID: "100", + Password: "sssh", + DomainName: "oops", + } + authTokenPostErr(t, options, nil, false, gophercloud.ErrDomainNameWithUserID{}) +} + +func TestCreateFailureScopeProjectNameAlone(t *testing.T) { + options := tokens.AuthOptions{UserID: "myself", Password: "swordfish"} + scope := &tokens.Scope{ProjectName: "notenough"} + authTokenPostErr(t, options, scope, false, gophercloud.ErrScopeDomainIDOrDomainName{}) +} + +func TestCreateFailureScopeProjectNameAndID(t *testing.T) { + options := tokens.AuthOptions{UserID: "myself", Password: "swordfish"} + scope := &tokens.Scope{ProjectName: "whoops", ProjectID: "toomuch", DomainID: "1234"} + authTokenPostErr(t, options, scope, false, gophercloud.ErrScopeProjectIDOrProjectName{}) +} + +func TestCreateFailureScopeProjectIDAndDomainID(t *testing.T) { + options := tokens.AuthOptions{UserID: "myself", Password: "swordfish"} + scope := &tokens.Scope{ProjectID: "toomuch", DomainID: "notneeded"} + authTokenPostErr(t, options, scope, false, gophercloud.ErrScopeProjectIDAlone{}) +} + +func TestCreateFailureScopeProjectIDAndDomainNAme(t *testing.T) { + options := tokens.AuthOptions{UserID: "myself", Password: "swordfish"} + scope := &tokens.Scope{ProjectID: "toomuch", DomainName: "notneeded"} + authTokenPostErr(t, options, scope, false, gophercloud.ErrScopeProjectIDAlone{}) +} + +func TestCreateFailureScopeDomainIDAndDomainName(t *testing.T) { + options := tokens.AuthOptions{UserID: "myself", Password: "swordfish"} + scope := &tokens.Scope{DomainID: "toomuch", DomainName: "notneeded"} + authTokenPostErr(t, options, scope, false, gophercloud.ErrScopeDomainIDOrDomainName{}) +} + +func TestCreateFailureScopeDomainNameAlone(t *testing.T) { + options := tokens.AuthOptions{UserID: "myself", Password: "swordfish"} + scope := &tokens.Scope{DomainName: "notenough"} + authTokenPostErr(t, options, scope, false, gophercloud.ErrScopeDomainName{}) +} + +/* +func TestCreateFailureEmptyScope(t *testing.T) { + options := tokens.AuthOptions{UserID: "myself", Password: "swordfish"} + scope := &tokens.Scope{} + authTokenPostErr(t, options, scope, false, gophercloud.ErrScopeEmpty{}) +} +*/ + +func TestGetRequest(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + client := gophercloud.ServiceClient{ + ProviderClient: &gophercloud.ProviderClient{ + TokenID: "12345abcdef", + }, + Endpoint: testhelper.Endpoint(), + } + + testhelper.Mux.HandleFunc("/auth/tokens", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "GET") + testhelper.TestHeader(t, r, "Content-Type", "") + testhelper.TestHeader(t, r, "Accept", "application/json") + testhelper.TestHeader(t, r, "X-Auth-Token", "12345abcdef") + testhelper.TestHeader(t, r, "X-Subject-Token", "abcdef12345") + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` + { "token": { "expires_at": "2014-08-29T13:10:01.000000Z" } } + `) + }) + + token, err := tokens.Get(&client, "abcdef12345").Extract() + if err != nil { + t.Errorf("Info returned an error: %v", err) + } + + expected, _ := time.Parse(time.UnixDate, "Fri Aug 29 13:10:01 UTC 2014") + if token.ExpiresAt != expected { + t.Errorf("Expected expiration time %s, but was %s", expected.Format(time.UnixDate), time.Time(token.ExpiresAt).Format(time.UnixDate)) + } +} + +func prepareAuthTokenHandler(t *testing.T, expectedMethod string, status int) gophercloud.ServiceClient { + client := gophercloud.ServiceClient{ + ProviderClient: &gophercloud.ProviderClient{ + TokenID: "12345abcdef", + }, + Endpoint: testhelper.Endpoint(), + } + + testhelper.Mux.HandleFunc("/auth/tokens", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, expectedMethod) + testhelper.TestHeader(t, r, "Content-Type", "") + testhelper.TestHeader(t, r, "Accept", "application/json") + testhelper.TestHeader(t, r, "X-Auth-Token", "12345abcdef") + testhelper.TestHeader(t, r, "X-Subject-Token", "abcdef12345") + + w.WriteHeader(status) + }) + + return client +} + +func TestValidateRequestSuccessful(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + client := prepareAuthTokenHandler(t, "HEAD", http.StatusNoContent) + + ok, err := tokens.Validate(&client, "abcdef12345") + if err != nil { + t.Errorf("Unexpected error from Validate: %v", err) + } + + if !ok { + t.Errorf("Validate returned false for a valid token") + } +} + +func TestValidateRequestFailure(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + client := prepareAuthTokenHandler(t, "HEAD", http.StatusNotFound) + + ok, err := tokens.Validate(&client, "abcdef12345") + if err != nil { + t.Errorf("Unexpected error from Validate: %v", err) + } + + if ok { + t.Errorf("Validate returned true for an invalid token") + } +} + +func TestValidateRequestError(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + client := prepareAuthTokenHandler(t, "HEAD", http.StatusMethodNotAllowed) + + _, err := tokens.Validate(&client, "abcdef12345") + if err == nil { + t.Errorf("Missing expected error from Validate") + } +} + +func TestRevokeRequestSuccessful(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + client := prepareAuthTokenHandler(t, "DELETE", http.StatusNoContent) + + res := tokens.Revoke(&client, "abcdef12345") + testhelper.AssertNoErr(t, res.Err) +} + +func TestRevokeRequestError(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + client := prepareAuthTokenHandler(t, "DELETE", http.StatusNotFound) + + res := tokens.Revoke(&client, "abcdef12345") + if res.Err == nil { + t.Errorf("Missing expected error from Revoke") + } +} + +func TestNoTokenInResponse(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + client := gophercloud.ServiceClient{ + ProviderClient: &gophercloud.ProviderClient{}, + Endpoint: testhelper.Endpoint(), + } + + testhelper.Mux.HandleFunc("/auth/tokens", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `{}`) + }) + + options := tokens.AuthOptions{UserID: "me", Password: "squirrel!"} + _, err := tokens.Create(&client, &options).Extract() + testhelper.AssertNoErr(t, err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/testing/results_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/testing/results_test.go new file mode 100644 index 0000000000..d55a538bc1 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/testing/results_test.go @@ -0,0 +1,52 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/testhelper" +) + +func TestExtractToken(t *testing.T) { + result := getGetResult(t) + + token, err := result.ExtractToken() + testhelper.AssertNoErr(t, err) + + testhelper.CheckDeepEquals(t, &ExpectedToken, token) +} + +func TestExtractCatalog(t *testing.T) { + result := getGetResult(t) + + catalog, err := result.ExtractServiceCatalog() + testhelper.AssertNoErr(t, err) + + testhelper.CheckDeepEquals(t, &ExpectedServiceCatalog, catalog) +} + +func TestExtractUser(t *testing.T) { + result := getGetResult(t) + + user, err := result.ExtractUser() + testhelper.AssertNoErr(t, err) + + testhelper.CheckDeepEquals(t, &ExpectedUser, user) +} + +func TestExtractRoles(t *testing.T) { + result := getGetResult(t) + + roles, err := result.ExtractRoles() + testhelper.AssertNoErr(t, err) + + testhelper.CheckDeepEquals(t, ExpectedRoles, roles) +} + +func TestExtractProject(t *testing.T) { + result := getGetResult(t) + + project, err := result.ExtractProject() + testhelper.AssertNoErr(t, err) + + testhelper.CheckDeepEquals(t, &ExpectedProject, project) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/urls.go new file mode 100644 index 0000000000..2f864a31c8 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/urls.go @@ -0,0 +1,7 @@ +package tokens + +import "github.com/gophercloud/gophercloud" + +func tokenURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("auth", "tokens") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/users/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/users/requests.go new file mode 100644 index 0000000000..eea19197c0 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/users/requests.go @@ -0,0 +1,218 @@ +package users + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/identity/v3/groups" + "github.com/gophercloud/gophercloud/pagination" +) + +// Option is a specific option defined at the API to enable features +// on a user account. +type Option string + +const ( + IgnoreChangePasswordUponFirstUse Option = "ignore_change_password_upon_first_use" + IgnorePasswordExpiry Option = "ignore_password_expiry" + IgnoreLockoutFailureAttempts Option = "ignore_lockout_failure_attempts" + MultiFactorAuthRules Option = "multi_factor_auth_rules" + MultiFactorAuthEnabled Option = "multi_factor_auth_enabled" +) + +// ListOptsBuilder allows extensions to add additional parameters to +// the List request +type ListOptsBuilder interface { + ToUserListQuery() (string, error) +} + +// ListOpts allows you to query the List method. +type ListOpts struct { + // DomainID filters the response by a domain ID. + DomainID string `q:"domain_id"` + + // Enabled filters the response by enabled users. + Enabled *bool `q:"enabled"` + + // IdpID filters the response by an Identity Provider ID. + IdPID string `q:"idp_id"` + + // Name filters the response by username. + Name string `q:"name"` + + // PasswordExpiresAt filters the response based on expiring passwords. + PasswordExpiresAt string `q:"password_expires_at"` + + // ProtocolID filters the response by protocol ID. + ProtocolID string `q:"protocol_id"` + + // UniqueID filters the response by unique ID. + UniqueID string `q:"unique_id"` +} + +// ToUserListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToUserListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List enumerates the Users to which the current token has access. +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToUserListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return UserPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves details on a single user, by ID. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to +// the Create request. +type CreateOptsBuilder interface { + ToUserCreateMap() (map[string]interface{}, error) +} + +// CreateOpts implements CreateOptsBuilder +type CreateOpts struct { + // Name is the name of the new user. + Name string `json:"name" required:"true"` + + // DefaultProjectID is the ID of the default project of the user. + DefaultProjectID string `json:"default_project_id,omitempty"` + + // Description is a description of the user. + Description string `json:"description,omitempty"` + + // DomainID is the ID of the domain the user belongs to. + DomainID string `json:"domain_id,omitempty"` + + // Enabled sets the user status to enabled or disabled. + Enabled *bool `json:"enabled,omitempty"` + + // Extra is free-form extra key/value pairs to describe the user. + Extra map[string]interface{} `json:"-"` + + // Options are defined options in the API to enable certain features. + Options map[Option]interface{} `json:"options,omitempty"` + + // Password is the password of the new user. + Password string `json:"password,omitempty"` +} + +// ToUserCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToUserCreateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "user") + if err != nil { + return nil, err + } + + if opts.Extra != nil { + if v, ok := b["user"].(map[string]interface{}); ok { + for key, value := range opts.Extra { + v[key] = value + } + } + } + + return b, nil +} + +// Create creates a new User. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToUserCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{201}, + }) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateOptsBuilder interface { + ToUserUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts implements UpdateOptsBuilder +type UpdateOpts struct { + // Name is the name of the new user. + Name string `json:"name,omitempty"` + + // DefaultProjectID is the ID of the default project of the user. + DefaultProjectID string `json:"default_project_id,omitempty"` + + // Description is a description of the user. + Description string `json:"description,omitempty"` + + // DomainID is the ID of the domain the user belongs to. + DomainID string `json:"domain_id,omitempty"` + + // Enabled sets the user status to enabled or disabled. + Enabled *bool `json:"enabled,omitempty"` + + // Extra is free-form extra key/value pairs to describe the user. + Extra map[string]interface{} `json:"-"` + + // Options are defined options in the API to enable certain features. + Options map[Option]interface{} `json:"options,omitempty"` + + // Password is the password of the new user. + Password string `json:"password,omitempty"` +} + +// ToUserUpdateMap formats a UpdateOpts into an update request. +func (opts UpdateOpts) ToUserUpdateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "user") + if err != nil { + return nil, err + } + + if opts.Extra != nil { + if v, ok := b["user"].(map[string]interface{}); ok { + for key, value := range opts.Extra { + v[key] = value + } + } + } + + return b, nil +} + +// Update updates an existing User. +func Update(client *gophercloud.ServiceClient, userID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToUserUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Patch(updateURL(client, userID), &b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete deletes a user. +func Delete(client *gophercloud.ServiceClient, userID string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, userID), nil) + return +} + +// ListGroups enumerates groups user belongs to. +func ListGroups(client *gophercloud.ServiceClient, userID string) pagination.Pager { + url := listGroupsURL(client, userID) + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return groups.GroupPage{pagination.LinkedPageBase{PageResult: r}} + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/users/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/users/results.go new file mode 100644 index 0000000000..414bbf80df --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/users/results.go @@ -0,0 +1,145 @@ +package users + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/internal" + "github.com/gophercloud/gophercloud/pagination" +) + +// User is a base unit of ownership. +type User struct { + // DefaultProjectID is the ID of the default project of the user. + DefaultProjectID string `json:"default_project_id"` + + // Description is the description of the user. + Description string `json:"description"` + + // DomainID is the domain ID the user belongs to. + DomainID string `json:"domain_id"` + + // Enabled is whether or not the user is enabled. + Enabled bool `json:"enabled"` + + // Extra is a collection of miscellaneous key/values. + Extra map[string]interface{} `json:"-"` + + // ID is the unique ID of the user. + ID string `json:"id"` + + // Links contains referencing links to the user. + Links map[string]interface{} `json:"links"` + + // Name is the name of the user. + Name string `json:"name"` + + // Options are a set of defined options of the user. + Options map[string]interface{} `json:"options"` + + // PasswordExpiresAt is the timestamp when the user's password expires. + PasswordExpiresAt time.Time `json:"-"` +} + +func (r *User) UnmarshalJSON(b []byte) error { + type tmp User + var s struct { + tmp + Extra map[string]interface{} `json:"extra"` + PasswordExpiresAt gophercloud.JSONRFC3339MilliNoZ `json:"password_expires_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = User(s.tmp) + + r.PasswordExpiresAt = time.Time(s.PasswordExpiresAt) + + // Collect other fields and bundle them into Extra + // but only if a field titled "extra" wasn't sent. + if s.Extra != nil { + r.Extra = s.Extra + } else { + var result interface{} + err := json.Unmarshal(b, &result) + if err != nil { + return err + } + if resultMap, ok := result.(map[string]interface{}); ok { + delete(resultMap, "password_expires_at") + r.Extra = internal.RemainingKeys(User{}, resultMap) + } + } + + return err +} + +type userResult struct { + gophercloud.Result +} + +// GetResult temporarily contains the response from the Get call. +type GetResult struct { + userResult +} + +// CreateResult temporarily contains the response from the Create call. +type CreateResult struct { + userResult +} + +// UpdateResult temporarily contains the response from the Update call. +type UpdateResult struct { + userResult +} + +// DeleteResult temporarily contains the response from the Delete call. +type DeleteResult struct { + gophercloud.ErrResult +} + +// UserPage is a single page of User results. +type UserPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of Users contains any results. +func (r UserPage) IsEmpty() (bool, error) { + users, err := ExtractUsers(r) + return len(users) == 0, err +} + +// NextPageURL extracts the "next" link from the links section of the result. +func (r UserPage) NextPageURL() (string, error) { + var s struct { + Links struct { + Next string `json:"next"` + Previous string `json:"previous"` + } `json:"links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return s.Links.Next, err +} + +// ExtractUsers returns a slice of Users contained in a single page of results. +func ExtractUsers(r pagination.Page) ([]User, error) { + var s struct { + Users []User `json:"users"` + } + err := (r.(UserPage)).ExtractInto(&s) + return s.Users, err +} + +// Extract interprets any user results as a User. +func (r userResult) Extract() (*User, error) { + var s struct { + User *User `json:"user"` + } + err := r.ExtractInto(&s) + return s.User, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/users/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/users/testing/fixtures.go new file mode 100644 index 0000000000..00a0e3493f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/users/testing/fixtures.go @@ -0,0 +1,393 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/identity/v3/groups" + "github.com/gophercloud/gophercloud/openstack/identity/v3/users" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +// ListOutput provides a single page of User results. +const ListOutput = ` +{ + "links": { + "next": null, + "previous": null, + "self": "http://example.com/identity/v3/users" + }, + "users": [ + { + "domain_id": "default", + "enabled": true, + "id": "2844b2a08be147a08ef58317d6471f1f", + "links": { + "self": "http://example.com/identity/v3/users/2844b2a08be147a08ef58317d6471f1f" + }, + "name": "glance", + "password_expires_at": null, + "description": "some description", + "extra": { + "email": "glance@localhost" + } + }, + { + "default_project_id": "263fd9", + "domain_id": "1789d1", + "enabled": true, + "id": "9fe1d3", + "links": { + "self": "https://example.com/identity/v3/users/9fe1d3" + }, + "name": "jsmith", + "password_expires_at": "2016-11-06T15:32:17.000000", + "email": "jsmith@example.com", + "options": { + "ignore_password_expiry": true, + "multi_factor_auth_rules": [["password", "totp"], ["password", "custom-auth-method"]] + } + } + ] +} +` + +// GetOutput provides a Get result. +const GetOutput = ` +{ + "user": { + "default_project_id": "263fd9", + "domain_id": "1789d1", + "enabled": true, + "id": "9fe1d3", + "links": { + "self": "https://example.com/identity/v3/users/9fe1d3" + }, + "name": "jsmith", + "password_expires_at": "2016-11-06T15:32:17.000000", + "email": "jsmith@example.com", + "options": { + "ignore_password_expiry": true, + "multi_factor_auth_rules": [["password", "totp"], ["password", "custom-auth-method"]] + } + } +} +` + +// GetOutputNoOptions provides a Get result of a user with no options. +const GetOutputNoOptions = ` +{ + "user": { + "default_project_id": "263fd9", + "domain_id": "1789d1", + "enabled": true, + "id": "9fe1d3", + "links": { + "self": "https://example.com/identity/v3/users/9fe1d3" + }, + "name": "jsmith", + "password_expires_at": "2016-11-06T15:32:17.000000", + "email": "jsmith@example.com" + } +} +` + +// CreateRequest provides the input to a Create request. +const CreateRequest = ` +{ + "user": { + "default_project_id": "263fd9", + "domain_id": "1789d1", + "enabled": true, + "name": "jsmith", + "password": "secretsecret", + "email": "jsmith@example.com", + "options": { + "ignore_password_expiry": true, + "multi_factor_auth_rules": [["password", "totp"], ["password", "custom-auth-method"]] + } + } +} +` + +// CreateNoOptionsRequest provides the input to a Create request with no Options. +const CreateNoOptionsRequest = ` +{ + "user": { + "default_project_id": "263fd9", + "domain_id": "1789d1", + "enabled": true, + "name": "jsmith", + "password": "secretsecret", + "email": "jsmith@example.com" + } +} +` + +// UpdateRequest provides the input to as Update request. +const UpdateRequest = ` +{ + "user": { + "enabled": false, + "disabled_reason": "DDOS", + "options": { + "multi_factor_auth_rules": null + } + } +} +` + +// UpdateOutput provides an update result. +const UpdateOutput = ` +{ + "user": { + "default_project_id": "263fd9", + "domain_id": "1789d1", + "enabled": false, + "id": "9fe1d3", + "links": { + "self": "https://example.com/identity/v3/users/9fe1d3" + }, + "name": "jsmith", + "password_expires_at": "2016-11-06T15:32:17.000000", + "email": "jsmith@example.com", + "disabled_reason": "DDOS", + "options": { + "ignore_password_expiry": true + } + } +} +` + +// ListGroupsOutput provides a ListGroups result. +const ListGroupsOutput = ` +{ + "groups": [ + { + "description": "Developers cleared for work on all general projects", + "domain_id": "1789d1", + "id": "ea167b", + "links": { + "self": "https://example.com/identity/v3/groups/ea167b" + }, + "building": "Hilltop A", + "name": "Developers" + }, + { + "description": "Developers cleared for work on secret projects", + "domain_id": "1789d1", + "id": "a62db1", + "links": { + "self": "https://example.com/identity/v3/groups/a62db1" + }, + "name": "Secure Developers" + } + ], + "links": { + "self": "http://example.com/identity/v3/users/9fe1d3/groups", + "previous": null, + "next": null + } +} +` + +// FirstUser is the first user in the List request. +var nilTime time.Time +var FirstUser = users.User{ + DomainID: "default", + Enabled: true, + ID: "2844b2a08be147a08ef58317d6471f1f", + Links: map[string]interface{}{ + "self": "http://example.com/identity/v3/users/2844b2a08be147a08ef58317d6471f1f", + }, + Name: "glance", + PasswordExpiresAt: nilTime, + Description: "some description", + Extra: map[string]interface{}{ + "email": "glance@localhost", + }, +} + +// SecondUser is the second user in the List request. +var SecondUserPasswordExpiresAt, _ = time.Parse(gophercloud.RFC3339MilliNoZ, "2016-11-06T15:32:17.000000") +var SecondUser = users.User{ + DefaultProjectID: "263fd9", + DomainID: "1789d1", + Enabled: true, + ID: "9fe1d3", + Links: map[string]interface{}{ + "self": "https://example.com/identity/v3/users/9fe1d3", + }, + Name: "jsmith", + PasswordExpiresAt: SecondUserPasswordExpiresAt, + Extra: map[string]interface{}{ + "email": "jsmith@example.com", + }, + Options: map[string]interface{}{ + "ignore_password_expiry": true, + "multi_factor_auth_rules": []interface{}{ + []string{"password", "totp"}, + []string{"password", "custom-auth-method"}, + }, + }, +} + +var SecondUserNoOptions = users.User{ + DefaultProjectID: "263fd9", + DomainID: "1789d1", + Enabled: true, + ID: "9fe1d3", + Links: map[string]interface{}{ + "self": "https://example.com/identity/v3/users/9fe1d3", + }, + Name: "jsmith", + PasswordExpiresAt: SecondUserPasswordExpiresAt, + Extra: map[string]interface{}{ + "email": "jsmith@example.com", + }, +} + +// SecondUserUpdated is how SecondUser should look after an Update. +var SecondUserUpdated = users.User{ + DefaultProjectID: "263fd9", + DomainID: "1789d1", + Enabled: false, + ID: "9fe1d3", + Links: map[string]interface{}{ + "self": "https://example.com/identity/v3/users/9fe1d3", + }, + Name: "jsmith", + PasswordExpiresAt: SecondUserPasswordExpiresAt, + Extra: map[string]interface{}{ + "email": "jsmith@example.com", + "disabled_reason": "DDOS", + }, + Options: map[string]interface{}{ + "ignore_password_expiry": true, + }, +} + +// ExpectedUsersSlice is the slice of users expected to be returned from ListOutput. +var ExpectedUsersSlice = []users.User{FirstUser, SecondUser} + +var FirstGroup = groups.Group{ + Description: "Developers cleared for work on all general projects", + DomainID: "1789d1", + ID: "ea167b", + Links: map[string]interface{}{ + "self": "https://example.com/identity/v3/groups/ea167b", + }, + Extra: map[string]interface{}{ + "building": "Hilltop A", + }, + Name: "Developers", +} + +var SecondGroup = groups.Group{ + Description: "Developers cleared for work on secret projects", + DomainID: "1789d1", + ID: "a62db1", + Links: map[string]interface{}{ + "self": "https://example.com/identity/v3/groups/a62db1", + }, + Extra: map[string]interface{}{}, + Name: "Secure Developers", +} + +var ExpectedGroupsSlice = []groups.Group{FirstGroup, SecondGroup} + +// HandleListUsersSuccessfully creates an HTTP handler at `/users` on the +// test handler mux that responds with a list of two users. +func HandleListUsersSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ListOutput) + }) +} + +// HandleGetUserSuccessfully creates an HTTP handler at `/users` on the +// test handler mux that responds with a single user. +func HandleGetUserSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/users/9fe1d3", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, GetOutput) + }) +} + +// HandleCreateUserSuccessfully creates an HTTP handler at `/users` on the +// test handler mux that tests user creation. +func HandleCreateUserSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, CreateRequest) + + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, GetOutput) + }) +} + +// HandleCreateNoOptionsUserSuccessfully creates an HTTP handler at `/users` on the +// test handler mux that tests user creation. +func HandleCreateNoOptionsUserSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, CreateNoOptionsRequest) + + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, GetOutputNoOptions) + }) +} + +// HandleUpdateUserSuccessfully creates an HTTP handler at `/users` on the +// test handler mux that tests user update. +func HandleUpdateUserSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/users/9fe1d3", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PATCH") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, UpdateRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, UpdateOutput) + }) +} + +// HandleDeleteUserSuccessfully creates an HTTP handler at `/users` on the +// test handler mux that tests user deletion. +func HandleDeleteUserSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/users/9fe1d3", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleListUserGroupsSuccessfully creates an HTTP handler at /users/{userID}/groups +// on the test handler mux that respons wit a list of two groups +func HandleListUserGroupsSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/users/9fe1d3/groups", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ListGroupsOutput) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/users/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/users/testing/requests_test.go new file mode 100644 index 0000000000..5ba21f9de6 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/users/testing/requests_test.go @@ -0,0 +1,148 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/identity/v3/groups" + "github.com/gophercloud/gophercloud/openstack/identity/v3/users" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestListUsers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListUsersSuccessfully(t) + + count := 0 + err := users.List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + + actual, err := users.ExtractUsers(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ExpectedUsersSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestListUsersAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListUsersSuccessfully(t) + + allPages, err := users.List(client.ServiceClient(), nil).AllPages() + th.AssertNoErr(t, err) + actual, err := users.ExtractUsers(allPages) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedUsersSlice, actual) + th.AssertEquals(t, ExpectedUsersSlice[0].Extra["email"], "glance@localhost") + th.AssertEquals(t, ExpectedUsersSlice[1].Extra["email"], "jsmith@example.com") +} + +func TestGetUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetUserSuccessfully(t) + + actual, err := users.Get(client.ServiceClient(), "9fe1d3").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, SecondUser, *actual) + th.AssertEquals(t, SecondUser.Extra["email"], "jsmith@example.com") +} + +func TestCreateUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateUserSuccessfully(t) + + iTrue := true + createOpts := users.CreateOpts{ + Name: "jsmith", + DomainID: "1789d1", + Enabled: &iTrue, + Password: "secretsecret", + DefaultProjectID: "263fd9", + Options: map[users.Option]interface{}{ + users.IgnorePasswordExpiry: true, + users.MultiFactorAuthRules: []interface{}{ + []string{"password", "totp"}, + []string{"password", "custom-auth-method"}, + }, + }, + Extra: map[string]interface{}{ + "email": "jsmith@example.com", + }, + } + + actual, err := users.Create(client.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, SecondUser, *actual) +} + +func TestCreateNoOptionsUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateNoOptionsUserSuccessfully(t) + + iTrue := true + createOpts := users.CreateOpts{ + Name: "jsmith", + DomainID: "1789d1", + Enabled: &iTrue, + Password: "secretsecret", + DefaultProjectID: "263fd9", + Extra: map[string]interface{}{ + "email": "jsmith@example.com", + }, + } + + actual, err := users.Create(client.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, SecondUserNoOptions, *actual) +} + +func TestUpdateUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleUpdateUserSuccessfully(t) + + iFalse := false + updateOpts := users.UpdateOpts{ + Enabled: &iFalse, + Options: map[users.Option]interface{}{ + users.MultiFactorAuthRules: nil, + }, + Extra: map[string]interface{}{ + "disabled_reason": "DDOS", + }, + } + + actual, err := users.Update(client.ServiceClient(), "9fe1d3", updateOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, SecondUserUpdated, *actual) +} + +func TestDeleteUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteUserSuccessfully(t) + + res := users.Delete(client.ServiceClient(), "9fe1d3") + th.AssertNoErr(t, res.Err) +} + +func TestListUserGroups(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListUserGroupsSuccessfully(t) + allPages, err := users.ListGroups(client.ServiceClient(), "9fe1d3").AllPages() + th.AssertNoErr(t, err) + actual, err := groups.ExtractGroups(allPages) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedGroupsSlice, actual) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/users/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/users/urls.go new file mode 100644 index 0000000000..9ac605ae42 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/users/urls.go @@ -0,0 +1,27 @@ +package users + +import "github.com/gophercloud/gophercloud" + +func listURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("users") +} + +func getURL(client *gophercloud.ServiceClient, userID string) string { + return client.ServiceURL("users", userID) +} + +func createURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("users") +} + +func updateURL(client *gophercloud.ServiceClient, userID string) string { + return client.ServiceURL("users", userID) +} + +func deleteURL(client *gophercloud.ServiceClient, userID string) string { + return client.ServiceURL("users", userID) +} + +func listGroupsURL(client *gophercloud.ServiceClient, userID string) string { + return client.ServiceURL("users", userID, "groups") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/README.md b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/README.md new file mode 100644 index 0000000000..05c19befe8 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/README.md @@ -0,0 +1 @@ +This provides a Go API which wraps any service implementing the [OpenStack Image Service API, version 2](http://developer.openstack.org/api-ref-image-v2.html). diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/imagedata/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/imagedata/requests.go new file mode 100644 index 0000000000..7fd6951d3b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/imagedata/requests.go @@ -0,0 +1,28 @@ +package imagedata + +import ( + "io" + "net/http" + + "github.com/gophercloud/gophercloud" +) + +// Upload uploads image file +func Upload(client *gophercloud.ServiceClient, id string, data io.Reader) (r UploadResult) { + _, r.Err = client.Put(uploadURL(client, id), data, nil, &gophercloud.RequestOpts{ + MoreHeaders: map[string]string{"Content-Type": "application/octet-stream"}, + OkCodes: []int{204}, + }) + return +} + +// Download retrieves file +func Download(client *gophercloud.ServiceClient, id string) (r DownloadResult) { + var resp *http.Response + resp, r.Err = client.Get(downloadURL(client, id), nil, nil) + if resp != nil { + r.Body = resp.Body + r.Header = resp.Header + } + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/imagedata/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/imagedata/results.go new file mode 100644 index 0000000000..970b226f2a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/imagedata/results.go @@ -0,0 +1,26 @@ +package imagedata + +import ( + "fmt" + "io" + + "github.com/gophercloud/gophercloud" +) + +// UploadResult is the result of an upload image operation +type UploadResult struct { + gophercloud.ErrResult +} + +// DownloadResult is the result of a download image operation +type DownloadResult struct { + gophercloud.Result +} + +// Extract builds images model from io.Reader +func (r DownloadResult) Extract() (io.Reader, error) { + if r, ok := r.Body.(io.Reader); ok { + return r, nil + } + return nil, fmt.Errorf("Expected io.Reader but got: %T(%#v)", r.Body, r.Body) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/imagedata/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/imagedata/testing/fixtures.go new file mode 100644 index 0000000000..fe93fc9730 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/imagedata/testing/fixtures.go @@ -0,0 +1,40 @@ +package testing + +import ( + "io/ioutil" + "net/http" + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" + fakeclient "github.com/gophercloud/gophercloud/testhelper/client" +) + +// HandlePutImageDataSuccessfully setup +func HandlePutImageDataSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea/file", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + b, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Errorf("Unable to read request body: %v", err) + } + + th.AssertByteArrayEquals(t, []byte{5, 3, 7, 24}, b) + + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleGetImageDataSuccessfully setup +func HandleGetImageDataSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea/file", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.WriteHeader(http.StatusOK) + + _, err := w.Write([]byte{34, 87, 0, 23, 23, 23, 56, 255, 254, 0}) + th.AssertNoErr(t, err) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/imagedata/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/imagedata/testing/requests_test.go new file mode 100644 index 0000000000..4ac42d0e73 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/imagedata/testing/requests_test.go @@ -0,0 +1,87 @@ +package testing + +import ( + "fmt" + "io" + "io/ioutil" + "testing" + + "github.com/gophercloud/gophercloud/openstack/imageservice/v2/imagedata" + th "github.com/gophercloud/gophercloud/testhelper" + fakeclient "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestUpload(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandlePutImageDataSuccessfully(t) + + err := imagedata.Upload( + fakeclient.ServiceClient(), + "da3b75d9-3f4a-40e7-8a2c-bfab23927dea", + readSeekerOfBytes([]byte{5, 3, 7, 24})).ExtractErr() + + th.AssertNoErr(t, err) +} + +func readSeekerOfBytes(bs []byte) io.ReadSeeker { + return &RS{bs: bs} +} + +// implements io.ReadSeeker +type RS struct { + bs []byte + offset int +} + +func (rs *RS) Read(p []byte) (int, error) { + leftToRead := len(rs.bs) - rs.offset + + if 0 < leftToRead { + bytesToWrite := min(leftToRead, len(p)) + for i := 0; i < bytesToWrite; i++ { + p[i] = rs.bs[rs.offset] + rs.offset++ + } + return bytesToWrite, nil + } + return 0, io.EOF +} + +func min(a int, b int) int { + if a < b { + return a + } + return b +} + +func (rs *RS) Seek(offset int64, whence int) (int64, error) { + var offsetInt = int(offset) + if whence == 0 { + rs.offset = offsetInt + } else if whence == 1 { + rs.offset = rs.offset + offsetInt + } else if whence == 2 { + rs.offset = len(rs.bs) - offsetInt + } else { + return 0, fmt.Errorf("For parameter `whence`, expected value in {0,1,2} but got: %#v", whence) + } + + return int64(rs.offset), nil +} + +func TestDownload(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleGetImageDataSuccessfully(t) + + rdr, err := imagedata.Download(fakeclient.ServiceClient(), "da3b75d9-3f4a-40e7-8a2c-bfab23927dea").Extract() + th.AssertNoErr(t, err) + + bs, err := ioutil.ReadAll(rdr) + th.AssertNoErr(t, err) + + th.AssertByteArrayEquals(t, []byte{34, 87, 0, 23, 23, 23, 56, 255, 254, 0}, bs) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/imagedata/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/imagedata/urls.go new file mode 100644 index 0000000000..ccd6416e53 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/imagedata/urls.go @@ -0,0 +1,13 @@ +package imagedata + +import "github.com/gophercloud/gophercloud" + +// `imageDataURL(c,i)` is the URL for the binary image data for the +// image identified by ID `i` in the service `c`. +func uploadURL(c *gophercloud.ServiceClient, imageID string) string { + return c.ServiceURL("images", imageID, "file") +} + +func downloadURL(c *gophercloud.ServiceClient, imageID string) string { + return uploadURL(c, imageID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/requests.go new file mode 100644 index 0000000000..044b5cb95f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/requests.go @@ -0,0 +1,238 @@ +package images + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToImageListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the server attributes you want to see returned. Marker and Limit are used +// for pagination. +//http://developer.openstack.org/api-ref-image-v2.html +type ListOpts struct { + // Integer value for the limit of values to return. + Limit int `q:"limit"` + + // UUID of the server at which you want to set a marker. + Marker string `q:"marker"` + + Name string `q:"name"` + Visibility ImageVisibility `q:"visibility"` + MemberStatus ImageMemberStatus `q:"member_status"` + Owner string `q:"owner"` + Status ImageStatus `q:"status"` + SizeMin int64 `q:"size_min"` + SizeMax int64 `q:"size_max"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` + Tag string `q:"tag"` +} + +// ToImageListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToImageListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List implements image list request +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToImageListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return ImagePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder describes struct types that can be accepted by the Create call. +// The CreateOpts struct in this package does. +type CreateOptsBuilder interface { + // Returns value that can be passed to json.Marshal + ToImageCreateMap() (map[string]interface{}, error) +} + +// CreateOpts implements CreateOptsBuilder +type CreateOpts struct { + // Name is the name of the new image. + Name string `json:"name" required:"true"` + + // Id is the the image ID. + ID string `json:"id,omitempty"` + + // Visibility defines who can see/use the image. + Visibility *ImageVisibility `json:"visibility,omitempty"` + + // Tags is a set of image tags. + Tags []string `json:"tags,omitempty"` + + // ContainerFormat is the format of the + // container. Valid values are ami, ari, aki, bare, and ovf. + ContainerFormat string `json:"container_format,omitempty"` + + // DiskFormat is the format of the disk. If set, + // valid values are ami, ari, aki, vhd, vmdk, raw, qcow2, vdi, + // and iso. + DiskFormat string `json:"disk_format,omitempty"` + + // MinDisk is the amount of disk space in + // GB that is required to boot the image. + MinDisk int `json:"min_disk,omitempty"` + + // MinRAM is the amount of RAM in MB that + // is required to boot the image. + MinRAM int `json:"min_ram,omitempty"` + + // protected is whether the image is not deletable. + Protected *bool `json:"protected,omitempty"` + + // properties is a set of properties, if any, that + // are associated with the image. + Properties map[string]string `json:"-"` +} + +// ToImageCreateMap assembles a request body based on the contents of +// a CreateOpts. +func (opts CreateOpts) ToImageCreateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + if opts.Properties != nil { + for k, v := range opts.Properties { + b[k] = v + } + } + return b, nil +} + +// Create implements create image request +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToImageCreateMap() + if err != nil { + r.Err = err + return r + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{OkCodes: []int{201}}) + return +} + +// Delete implements image delete request +func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} + +// Get implements image get request +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// Update implements image updated request +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToImageUpdateMap() + if err != nil { + r.Err = err + return r + } + _, r.Err = client.Patch(updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + MoreHeaders: map[string]string{"Content-Type": "application/openstack-images-v2.1-json-patch"}, + }) + return +} + +// UpdateOptsBuilder implements UpdateOptsBuilder +type UpdateOptsBuilder interface { + // returns value implementing json.Marshaler which when marshaled matches the patch schema: + // http://specs.openstack.org/openstack/glance-specs/specs/api/v2/http-patch-image-api-v2.html + ToImageUpdateMap() ([]interface{}, error) +} + +// UpdateOpts implements UpdateOpts +type UpdateOpts []Patch + +// ToImageUpdateMap builder +func (opts UpdateOpts) ToImageUpdateMap() ([]interface{}, error) { + m := make([]interface{}, len(opts)) + for i, patch := range opts { + patchJSON := patch.ToImagePatchMap() + m[i] = patchJSON + } + return m, nil +} + +// Patch represents a single update to an existing image. Multiple updates to an image can be +// submitted at the same time. +type Patch interface { + ToImagePatchMap() map[string]interface{} +} + +// UpdateVisibility updated visibility +type UpdateVisibility struct { + Visibility ImageVisibility +} + +// ToImagePatchMap builder +func (u UpdateVisibility) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/visibility", + "value": u.Visibility, + } +} + +// ReplaceImageName implements Patch +type ReplaceImageName struct { + NewName string +} + +// ToImagePatchMap builder +func (r ReplaceImageName) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/name", + "value": r.NewName, + } +} + +// ReplaceImageChecksum implements Patch +type ReplaceImageChecksum struct { + Checksum string +} + +// ReplaceImageChecksum builder +func (rc ReplaceImageChecksum) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/checksum", + "value": rc.Checksum, + } +} + +// ReplaceImageTags implements Patch +type ReplaceImageTags struct { + NewTags []string +} + +// ToImagePatchMap builder +func (r ReplaceImageTags) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/tags", + "value": r.NewTags, + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/results.go new file mode 100644 index 0000000000..632186b728 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/results.go @@ -0,0 +1,189 @@ +package images + +import ( + "encoding/json" + "fmt" + "reflect" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/internal" + "github.com/gophercloud/gophercloud/pagination" +) + +// Image model +// Does not include the literal image data; just metadata. +// returned by listing images, and by fetching a specific image. +type Image struct { + // ID is the image UUID + ID string `json:"id"` + + // Name is the human-readable display name for the image. + Name string `json:"name"` + + // Status is the image status. It can be "queued" or "active" + // See imageservice/v2/images/type.go + Status ImageStatus `json:"status"` + + // Tags is a list of image tags. Tags are arbitrarily defined strings + // attached to an image. + Tags []string `json:"tags"` + + // ContainerFormat is the format of the container. + // Valid values are ami, ari, aki, bare, and ovf. + ContainerFormat string `json:"container_format"` + + // DiskFormat is the format of the disk. + // If set, valid values are ami, ari, aki, vhd, vmdk, raw, qcow2, vdi, and iso. + DiskFormat string `json:"disk_format"` + + // MinDiskGigabytes is the amount of disk space in GB that is required to boot the image. + MinDiskGigabytes int `json:"min_disk"` + + // MinRAMMegabytes [optional] is the amount of RAM in MB that is required to boot the image. + MinRAMMegabytes int `json:"min_ram"` + + // Owner is the tenant the image belongs to. + Owner string `json:"owner"` + + // Protected is whether the image is deletable or not. + Protected bool `json:"protected"` + + // Visibility defines who can see/use the image. + Visibility ImageVisibility `json:"visibility"` + + // Checksum is the checksum of the data that's associated with the image + Checksum string `json:"checksum"` + + // SizeBytes is the size of the data that's associated with the image. + SizeBytes int64 `json:"size"` + + // Metadata is a set of metadata associated with the image. + // Image metadata allow for meaningfully define the image properties + // and tags. See http://docs.openstack.org/developer/glance/metadefs-concepts.html. + Metadata map[string]string `json:"metadata"` + + // Properties is a set of key-value pairs, if any, that are associated with the image. + Properties map[string]interface{} `json:"-"` + + // CreatedAt is the date when the image has been created. + CreatedAt time.Time `json:"created_at"` + + // UpdatedAt is the date when the last change has been made to the image or it's properties. + UpdatedAt time.Time `json:"updated_at"` + + // File is the trailing path after the glance endpoint that represent the location + // of the image or the path to retrieve it. + File string `json:"file"` + + // Schema is the path to the JSON-schema that represent the image or image entity. + Schema string `json:"schema"` + + // VirtualSize is the virtual size of the image + VirtualSize int64 `json:"virtual_size"` +} + +func (r *Image) UnmarshalJSON(b []byte) error { + type tmp Image + var s struct { + tmp + SizeBytes interface{} `json:"size"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Image(s.tmp) + + switch t := s.SizeBytes.(type) { + case nil: + return nil + case float32: + r.SizeBytes = int64(t) + case float64: + r.SizeBytes = int64(t) + default: + return fmt.Errorf("Unknown type for SizeBytes: %v (value: %v)", reflect.TypeOf(t), t) + } + + // Bundle all other fields into Properties + var result interface{} + err = json.Unmarshal(b, &result) + if err != nil { + return err + } + if resultMap, ok := result.(map[string]interface{}); ok { + delete(resultMap, "self") + r.Properties = internal.RemainingKeys(Image{}, resultMap) + } + + return err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract interprets any commonResult as an Image. +func (r commonResult) Extract() (*Image, error) { + var s *Image + err := r.ExtractInto(&s) + return s, err +} + +// CreateResult represents the result of a Create operation +type CreateResult struct { + commonResult +} + +// UpdateResult represents the result of an Update operation +type UpdateResult struct { + commonResult +} + +// GetResult represents the result of a Get operation +type GetResult struct { + commonResult +} + +//DeleteResult model +type DeleteResult struct { + gophercloud.ErrResult +} + +// ImagePage represents page +type ImagePage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if a page contains no Images results. +func (r ImagePage) IsEmpty() (bool, error) { + images, err := ExtractImages(r) + return len(images) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to the next page of results. +func (r ImagePage) NextPageURL() (string, error) { + var s struct { + Next string `json:"next"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + + if s.Next == "" { + return "", nil + } + + return nextPageURL(r.URL.String(), s.Next) +} + +// ExtractImages interprets the results of a single page from a List() call, producing a slice of Image entities. +func ExtractImages(r pagination.Page) ([]Image, error) { + var s struct { + Images []Image `json:"images"` + } + err := (r.(ImagePage)).ExtractInto(&s) + return s.Images, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/testing/fixtures.go new file mode 100644 index 0000000000..33177c23f4 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/testing/fixtures.go @@ -0,0 +1,347 @@ +package testing + +import ( + "fmt" + "net/http" + "strconv" + "strings" + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" + fakeclient "github.com/gophercloud/gophercloud/testhelper/client" +) + +type imageEntry struct { + ID string + JSON string +} + +// HandleImageListSuccessfully test setup +func HandleImageListSuccessfully(t *testing.T) { + + images := make([]imageEntry, 3) + + images[0] = imageEntry{"cirros-0.3.4-x86_64-uec", + `{ + "status": "active", + "name": "cirros-0.3.4-x86_64-uec", + "tags": [], + "kernel_id": "e1b6edd4-bd9b-40ac-b010-8a6c16de4ba4", + "container_format": "ami", + "created_at": "2015-07-15T11:43:35Z", + "ramdisk_id": "8c64f48a-45a3-4eaa-adff-a8106b6c005b", + "disk_format": "ami", + "updated_at": "2015-07-15T11:43:35Z", + "visibility": "public", + "self": "/v2/images/07aa21a9-fa1a-430e-9a33-185be5982431", + "min_disk": 0, + "protected": false, + "id": "07aa21a9-fa1a-430e-9a33-185be5982431", + "size": 25165824, + "file": "/v2/images/07aa21a9-fa1a-430e-9a33-185be5982431/file", + "checksum": "eb9139e4942121f22bbc2afc0400b2a4", + "owner": "cba624273b8344e59dd1fd18685183b0", + "virtual_size": null, + "min_ram": 0, + "schema": "/v2/schemas/image", + "hw_disk_bus": "scsi", + "hw_disk_bus_model": "virtio-scsi", + "hw_scsi_model": "virtio-scsi" + }`} + images[1] = imageEntry{"cirros-0.3.4-x86_64-uec-ramdisk", + `{ + "status": "active", + "name": "cirros-0.3.4-x86_64-uec-ramdisk", + "tags": [], + "container_format": "ari", + "created_at": "2015-07-15T11:43:32Z", + "size": 3740163, + "disk_format": "ari", + "updated_at": "2015-07-15T11:43:32Z", + "visibility": "public", + "self": "/v2/images/8c64f48a-45a3-4eaa-adff-a8106b6c005b", + "min_disk": 0, + "protected": false, + "id": "8c64f48a-45a3-4eaa-adff-a8106b6c005b", + "file": "/v2/images/8c64f48a-45a3-4eaa-adff-a8106b6c005b/file", + "checksum": "be575a2b939972276ef675752936977f", + "owner": "cba624273b8344e59dd1fd18685183b0", + "virtual_size": null, + "min_ram": 0, + "schema": "/v2/schemas/image", + "hw_disk_bus": "scsi", + "hw_disk_bus_model": "virtio-scsi", + "hw_scsi_model": "virtio-scsi" + }`} + images[2] = imageEntry{"cirros-0.3.4-x86_64-uec-kernel", + `{ + "status": "active", + "name": "cirros-0.3.4-x86_64-uec-kernel", + "tags": [], + "container_format": "aki", + "created_at": "2015-07-15T11:43:29Z", + "size": 4979632, + "disk_format": "aki", + "updated_at": "2015-07-15T11:43:30Z", + "visibility": "public", + "self": "/v2/images/e1b6edd4-bd9b-40ac-b010-8a6c16de4ba4", + "min_disk": 0, + "protected": false, + "id": "e1b6edd4-bd9b-40ac-b010-8a6c16de4ba4", + "file": "/v2/images/e1b6edd4-bd9b-40ac-b010-8a6c16de4ba4/file", + "checksum": "8a40c862b5735975d82605c1dd395796", + "owner": "cba624273b8344e59dd1fd18685183b0", + "virtual_size": null, + "min_ram": 0, + "schema": "/v2/schemas/image", + "hw_disk_bus": "scsi", + "hw_disk_bus_model": "virtio-scsi", + "hw_scsi_model": "virtio-scsi" + }`} + + th.Mux.HandleFunc("/images", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + + w.WriteHeader(http.StatusOK) + + limit := 10 + var err error + if r.FormValue("limit") != "" { + limit, err = strconv.Atoi(r.FormValue("limit")) + if err != nil { + t.Errorf("Error value for 'limit' parameter %v (error: %v)", r.FormValue("limit"), err) + } + + } + + marker := "" + newMarker := "" + + if r.Form["marker"] != nil { + marker = r.Form["marker"][0] + } + + t.Logf("limit = %v marker = %v", limit, marker) + + selected := 0 + addNext := false + var imageJSON []string + + fmt.Fprintf(w, `{"images": [`) + + for _, i := range images { + if marker == "" || addNext { + t.Logf("Adding image %v to page", i.ID) + imageJSON = append(imageJSON, i.JSON) + newMarker = i.ID + selected++ + } else { + if strings.Contains(i.JSON, marker) { + addNext = true + } + } + + if selected == limit { + break + } + } + t.Logf("Writing out %v image(s)", len(imageJSON)) + fmt.Fprintf(w, strings.Join(imageJSON, ",")) + + fmt.Fprintf(w, `], + "next": "/images?marker=%s&limit=%v", + "schema": "/schemas/images", + "first": "/images?limit=%v"}`, newMarker, limit, limit) + + }) +} + +// HandleImageCreationSuccessfully test setup +func HandleImageCreationSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/images", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, `{ + "id": "e7db3b45-8db7-47ad-8109-3fb55c2c24fd", + "name": "Ubuntu 12.10", + "architecture": "x86_64", + "tags": [ + "ubuntu", + "quantal" + ] + }`) + + w.WriteHeader(http.StatusCreated) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "status": "queued", + "name": "Ubuntu 12.10", + "protected": false, + "tags": ["ubuntu","quantal"], + "container_format": "bare", + "created_at": "2014-11-11T20:47:55Z", + "disk_format": "qcow2", + "updated_at": "2014-11-11T20:47:55Z", + "visibility": "private", + "self": "/v2/images/e7db3b45-8db7-47ad-8109-3fb55c2c24fd", + "min_disk": 0, + "protected": false, + "id": "e7db3b45-8db7-47ad-8109-3fb55c2c24fd", + "file": "/v2/images/e7db3b45-8db7-47ad-8109-3fb55c2c24fd/file", + "owner": "b4eedccc6fb74fa8a7ad6b08382b852b", + "min_ram": 0, + "schema": "/v2/schemas/image", + "size": 0, + "checksum": "", + "virtual_size": 0, + "hw_disk_bus": "scsi", + "hw_disk_bus_model": "virtio-scsi", + "hw_scsi_model": "virtio-scsi" + }`) + }) +} + +// HandleImageCreationSuccessfullyNulls test setup +// JSON null values could be also returned according to behaviour https://bugs.launchpad.net/glance/+bug/1481512 +func HandleImageCreationSuccessfullyNulls(t *testing.T) { + th.Mux.HandleFunc("/images", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + th.TestJSONRequest(t, r, `{ + "id": "e7db3b45-8db7-47ad-8109-3fb55c2c24fd", + "name": "Ubuntu 12.10", + "tags": [ + "ubuntu", + "quantal" + ] + }`) + + w.WriteHeader(http.StatusCreated) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "status": "queued", + "name": "Ubuntu 12.10", + "protected": false, + "tags": ["ubuntu","quantal"], + "container_format": "bare", + "created_at": "2014-11-11T20:47:55Z", + "disk_format": "qcow2", + "updated_at": "2014-11-11T20:47:55Z", + "visibility": "private", + "self": "/v2/images/e7db3b45-8db7-47ad-8109-3fb55c2c24fd", + "min_disk": 0, + "protected": false, + "id": "e7db3b45-8db7-47ad-8109-3fb55c2c24fd", + "file": "/v2/images/e7db3b45-8db7-47ad-8109-3fb55c2c24fd/file", + "owner": "b4eedccc6fb74fa8a7ad6b08382b852b", + "min_ram": 0, + "schema": "/v2/schemas/image", + "size": null, + "checksum": null, + "virtual_size": null + }`) + }) +} + +// HandleImageGetSuccessfully test setup +func HandleImageGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "status": "active", + "name": "cirros-0.3.2-x86_64-disk", + "tags": [], + "container_format": "bare", + "created_at": "2014-05-05T17:15:10Z", + "disk_format": "qcow2", + "updated_at": "2014-05-05T17:15:11Z", + "visibility": "public", + "self": "/v2/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27", + "min_disk": 0, + "protected": false, + "id": "1bea47ed-f6a9-463b-b423-14b9cca9ad27", + "file": "/v2/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27/file", + "checksum": "64d7c1cd2b6f60c92c14662941cb7913", + "owner": "5ef70662f8b34079a6eddb8da9d75fe8", + "size": 13167616, + "min_ram": 0, + "schema": "/v2/schemas/image", + "virtual_size": null, + "hw_disk_bus": "scsi", + "hw_disk_bus_model": "virtio-scsi", + "hw_scsi_model": "virtio-scsi" + }`) + }) +} + +// HandleImageDeleteSuccessfully test setup +func HandleImageDeleteSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleImageUpdateSuccessfully setup +func HandleImageUpdateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PATCH") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + th.TestJSONRequest(t, r, `[ + { + "op": "replace", + "path": "/name", + "value": "Fedora 17" + }, + { + "op": "replace", + "path": "/tags", + "value": [ + "fedora", + "beefy" + ] + } + ]`) + + th.AssertEquals(t, "application/openstack-images-v2.1-json-patch", r.Header.Get("Content-Type")) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "id": "da3b75d9-3f4a-40e7-8a2c-bfab23927dea", + "name": "Fedora 17", + "status": "active", + "visibility": "public", + "size": 2254249, + "checksum": "2cec138d7dae2aa59038ef8c9aec2390", + "tags": [ + "fedora", + "beefy" + ], + "created_at": "2012-08-10T19:23:50Z", + "updated_at": "2012-08-12T11:11:33Z", + "self": "/v2/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea", + "file": "/v2/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea/file", + "schema": "/v2/schemas/image", + "owner": "", + "min_ram": 0, + "min_disk": 0, + "disk_format": "", + "virtual_size": 0, + "container_format": "", + "hw_disk_bus": "scsi", + "hw_disk_bus_model": "virtio-scsi", + "hw_scsi_model": "virtio-scsi" + }`) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/testing/requests_test.go new file mode 100644 index 0000000000..d1f0966a42 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/testing/requests_test.go @@ -0,0 +1,293 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + fakeclient "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestListImage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleImageListSuccessfully(t) + + t.Logf("Test setup %+v\n", th.Server) + + t.Logf("Id\tName\tOwner\tChecksum\tSizeBytes") + + pager := images.List(fakeclient.ServiceClient(), images.ListOpts{Limit: 1}) + t.Logf("Pager state %v", pager) + count, pages := 0, 0 + err := pager.EachPage(func(page pagination.Page) (bool, error) { + pages++ + t.Logf("Page %v", page) + images, err := images.ExtractImages(page) + if err != nil { + return false, err + } + + for _, i := range images { + t.Logf("%s\t%s\t%s\t%s\t%v\t\n", i.ID, i.Name, i.Owner, i.Checksum, i.SizeBytes) + count++ + } + + return true, nil + }) + th.AssertNoErr(t, err) + + t.Logf("--------\n%d images listed on %d pages.\n", count, pages) + th.AssertEquals(t, 3, pages) + th.AssertEquals(t, 3, count) +} + +func TestAllPagesImage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleImageListSuccessfully(t) + + pages, err := images.List(fakeclient.ServiceClient(), nil).AllPages() + th.AssertNoErr(t, err) + images, err := images.ExtractImages(pages) + th.AssertNoErr(t, err) + th.AssertEquals(t, 3, len(images)) +} + +func TestCreateImage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleImageCreationSuccessfully(t) + + id := "e7db3b45-8db7-47ad-8109-3fb55c2c24fd" + name := "Ubuntu 12.10" + + actualImage, err := images.Create(fakeclient.ServiceClient(), images.CreateOpts{ + ID: id, + Name: name, + Properties: map[string]string{ + "architecture": "x86_64", + }, + Tags: []string{"ubuntu", "quantal"}, + }).Extract() + + th.AssertNoErr(t, err) + + containerFormat := "bare" + diskFormat := "qcow2" + owner := "b4eedccc6fb74fa8a7ad6b08382b852b" + minDiskGigabytes := 0 + minRAMMegabytes := 0 + file := actualImage.File + createdDate := actualImage.CreatedAt + lastUpdate := actualImage.UpdatedAt + schema := "/v2/schemas/image" + + expectedImage := images.Image{ + ID: "e7db3b45-8db7-47ad-8109-3fb55c2c24fd", + Name: "Ubuntu 12.10", + Tags: []string{"ubuntu", "quantal"}, + + Status: images.ImageStatusQueued, + + ContainerFormat: containerFormat, + DiskFormat: diskFormat, + + MinDiskGigabytes: minDiskGigabytes, + MinRAMMegabytes: minRAMMegabytes, + + Owner: owner, + + Visibility: images.ImageVisibilityPrivate, + File: file, + CreatedAt: createdDate, + UpdatedAt: lastUpdate, + Schema: schema, + VirtualSize: 0, + Properties: map[string]interface{}{ + "hw_disk_bus": "scsi", + "hw_disk_bus_model": "virtio-scsi", + "hw_scsi_model": "virtio-scsi", + }, + } + + th.AssertDeepEquals(t, &expectedImage, actualImage) +} + +func TestCreateImageNulls(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleImageCreationSuccessfullyNulls(t) + + id := "e7db3b45-8db7-47ad-8109-3fb55c2c24fd" + name := "Ubuntu 12.10" + + actualImage, err := images.Create(fakeclient.ServiceClient(), images.CreateOpts{ + ID: id, + Name: name, + Tags: []string{"ubuntu", "quantal"}, + }).Extract() + + th.AssertNoErr(t, err) + + containerFormat := "bare" + diskFormat := "qcow2" + owner := "b4eedccc6fb74fa8a7ad6b08382b852b" + minDiskGigabytes := 0 + minRAMMegabytes := 0 + file := actualImage.File + createdDate := actualImage.CreatedAt + lastUpdate := actualImage.UpdatedAt + schema := "/v2/schemas/image" + + expectedImage := images.Image{ + ID: "e7db3b45-8db7-47ad-8109-3fb55c2c24fd", + Name: "Ubuntu 12.10", + Tags: []string{"ubuntu", "quantal"}, + + Status: images.ImageStatusQueued, + + ContainerFormat: containerFormat, + DiskFormat: diskFormat, + + MinDiskGigabytes: minDiskGigabytes, + MinRAMMegabytes: minRAMMegabytes, + + Owner: owner, + + Visibility: images.ImageVisibilityPrivate, + File: file, + CreatedAt: createdDate, + UpdatedAt: lastUpdate, + Schema: schema, + } + + th.AssertDeepEquals(t, &expectedImage, actualImage) +} + +func TestGetImage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleImageGetSuccessfully(t) + + actualImage, err := images.Get(fakeclient.ServiceClient(), "1bea47ed-f6a9-463b-b423-14b9cca9ad27").Extract() + + th.AssertNoErr(t, err) + + checksum := "64d7c1cd2b6f60c92c14662941cb7913" + sizeBytes := int64(13167616) + containerFormat := "bare" + diskFormat := "qcow2" + minDiskGigabytes := 0 + minRAMMegabytes := 0 + owner := "5ef70662f8b34079a6eddb8da9d75fe8" + file := actualImage.File + createdDate := actualImage.CreatedAt + lastUpdate := actualImage.UpdatedAt + schema := "/v2/schemas/image" + + expectedImage := images.Image{ + ID: "1bea47ed-f6a9-463b-b423-14b9cca9ad27", + Name: "cirros-0.3.2-x86_64-disk", + Tags: []string{}, + + Status: images.ImageStatusActive, + + ContainerFormat: containerFormat, + DiskFormat: diskFormat, + + MinDiskGigabytes: minDiskGigabytes, + MinRAMMegabytes: minRAMMegabytes, + + Owner: owner, + + Protected: false, + Visibility: images.ImageVisibilityPublic, + + Checksum: checksum, + SizeBytes: sizeBytes, + File: file, + CreatedAt: createdDate, + UpdatedAt: lastUpdate, + Schema: schema, + VirtualSize: 0, + Properties: map[string]interface{}{ + "hw_disk_bus": "scsi", + "hw_disk_bus_model": "virtio-scsi", + "hw_scsi_model": "virtio-scsi", + }, + } + + th.AssertDeepEquals(t, &expectedImage, actualImage) +} + +func TestDeleteImage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleImageDeleteSuccessfully(t) + + result := images.Delete(fakeclient.ServiceClient(), "1bea47ed-f6a9-463b-b423-14b9cca9ad27") + th.AssertNoErr(t, result.Err) +} + +func TestUpdateImage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleImageUpdateSuccessfully(t) + + actualImage, err := images.Update(fakeclient.ServiceClient(), "da3b75d9-3f4a-40e7-8a2c-bfab23927dea", images.UpdateOpts{ + images.ReplaceImageName{NewName: "Fedora 17"}, + images.ReplaceImageTags{NewTags: []string{"fedora", "beefy"}}, + }).Extract() + + th.AssertNoErr(t, err) + + sizebytes := int64(2254249) + checksum := "2cec138d7dae2aa59038ef8c9aec2390" + file := actualImage.File + createdDate := actualImage.CreatedAt + lastUpdate := actualImage.UpdatedAt + schema := "/v2/schemas/image" + + expectedImage := images.Image{ + ID: "da3b75d9-3f4a-40e7-8a2c-bfab23927dea", + Name: "Fedora 17", + Status: images.ImageStatusActive, + Visibility: images.ImageVisibilityPublic, + + SizeBytes: sizebytes, + Checksum: checksum, + + Tags: []string{ + "fedora", + "beefy", + }, + + Owner: "", + MinRAMMegabytes: 0, + MinDiskGigabytes: 0, + + DiskFormat: "", + ContainerFormat: "", + File: file, + CreatedAt: createdDate, + UpdatedAt: lastUpdate, + Schema: schema, + VirtualSize: 0, + Properties: map[string]interface{}{ + "hw_disk_bus": "scsi", + "hw_disk_bus_model": "virtio-scsi", + "hw_scsi_model": "virtio-scsi", + }, + } + + th.AssertDeepEquals(t, &expectedImage, actualImage) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/types.go b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/types.go new file mode 100644 index 0000000000..086e7e5d57 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/types.go @@ -0,0 +1,75 @@ +package images + +// ImageStatus image statuses +// http://docs.openstack.org/developer/glance/statuses.html +type ImageStatus string + +const ( + // ImageStatusQueued is a status for an image which identifier has + // been reserved for an image in the image registry. + ImageStatusQueued ImageStatus = "queued" + + // ImageStatusSaving denotes that an image’s raw data is currently being uploaded to Glance + ImageStatusSaving ImageStatus = "saving" + + // ImageStatusActive denotes an image that is fully available in Glance. + ImageStatusActive ImageStatus = "active" + + // ImageStatusKilled denotes that an error occurred during the uploading + // of an image’s data, and that the image is not readable. + ImageStatusKilled ImageStatus = "killed" + + // ImageStatusDeleted is used for an image that is no longer available to use. + // The image information is retained in the image registry. + ImageStatusDeleted ImageStatus = "deleted" + + // ImageStatusPendingDelete is similar to Delete, but the image is not yet deleted. + ImageStatusPendingDelete ImageStatus = "pending_delete" + + // ImageStatusDeactivated denotes that access to image data is not allowed to any non-admin user. + ImageStatusDeactivated ImageStatus = "deactivated" +) + +// ImageVisibility denotes an image that is fully available in Glance. +// This occurs when the image data is uploaded, or the image size +// is explicitly set to zero on creation. +// According to design +// https://wiki.openstack.org/wiki/Glance-v2-community-image-visibility-design +type ImageVisibility string + +const ( + // ImageVisibilityPublic all users + ImageVisibilityPublic ImageVisibility = "public" + + // ImageVisibilityPrivate users with tenantId == tenantId(owner) + ImageVisibilityPrivate ImageVisibility = "private" + + // ImageVisibilityShared images are visible to: + // - users with tenantId == tenantId(owner) + // - users with tenantId in the member-list of the image + // - users with tenantId in the member-list with member_status == 'accepted' + ImageVisibilityShared ImageVisibility = "shared" + + // ImageVisibilityCommunity images: + // - all users can see and boot it + // - users with tenantId in the member-list of the image with member_status == 'accepted' + // have this image in their default image-list + ImageVisibilityCommunity ImageVisibility = "community" +) + +// MemberStatus is a status for adding a new member (tenant) to an image member list. +type ImageMemberStatus string + +const ( + // ImageMemberStatusAccepted is the status for an accepted image member. + ImageMemberStatusAccepted ImageMemberStatus = "accepted" + + // ImageMemberStatusPending shows that the member addition is pending + ImageMemberStatusPending ImageMemberStatus = "pending" + + // ImageMemberStatusAccepted is the status for a rejected image member + ImageMemberStatusRejected ImageMemberStatus = "rejected" + + // ImageMemberStatusAll + ImageMemberStatusAll ImageMemberStatus = "all" +) diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/urls.go new file mode 100644 index 0000000000..bf7cea1ef8 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/urls.go @@ -0,0 +1,51 @@ +package images + +import ( + "net/url" + + "github.com/gophercloud/gophercloud" +) + +// `listURL` is a pure function. `listURL(c)` is a URL for which a GET +// request will respond with a list of images in the service `c`. +func listURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("images") +} + +func createURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("images") +} + +// `imageURL(c,i)` is the URL for the image identified by ID `i` in +// the service `c`. +func imageURL(c *gophercloud.ServiceClient, imageID string) string { + return c.ServiceURL("images", imageID) +} + +// `getURL(c,i)` is a URL for which a GET request will respond with +// information about the image identified by ID `i` in the service +// `c`. +func getURL(c *gophercloud.ServiceClient, imageID string) string { + return imageURL(c, imageID) +} + +func updateURL(c *gophercloud.ServiceClient, imageID string) string { + return imageURL(c, imageID) +} + +func deleteURL(c *gophercloud.ServiceClient, imageID string) string { + return imageURL(c, imageID) +} + +// builds next page full url based on current url +func nextPageURL(currentURL string, next string) (string, error) { + base, err := url.Parse(currentURL) + if err != nil { + return "", err + } + rel, err := url.Parse(next) + if err != nil { + return "", err + } + return base.ResolveReference(rel).String(), nil +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/members/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/members/requests.go new file mode 100644 index 0000000000..b16fb82d43 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/members/requests.go @@ -0,0 +1,77 @@ +package members + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Create member for specific image +// +// Preconditions +// The specified images must exist. +// You can only add a new member to an image which 'visibility' attribute is private. +// You must be the owner of the specified image. +// Synchronous Postconditions +// With correct permissions, you can see the member status of the image as pending through API calls. +// +// More details here: http://developer.openstack.org/api-ref-image-v2.html#createImageMember-v2 +func Create(client *gophercloud.ServiceClient, id string, member string) (r CreateResult) { + b := map[string]interface{}{"member": member} + _, r.Err = client.Post(createMemberURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// List members returns list of members for specifed image id +// More details: http://developer.openstack.org/api-ref-image-v2.html#listImageMembers-v2 +func List(client *gophercloud.ServiceClient, id string) pagination.Pager { + return pagination.NewPager(client, listMembersURL(client, id), func(r pagination.PageResult) pagination.Page { + return MemberPage{pagination.SinglePageBase(r)} + }) +} + +// Get image member details. +// More details: http://developer.openstack.org/api-ref-image-v2.html#getImageMember-v2 +func Get(client *gophercloud.ServiceClient, imageID string, memberID string) (r DetailsResult) { + _, r.Err = client.Get(getMemberURL(client, imageID, memberID), &r.Body, &gophercloud.RequestOpts{OkCodes: []int{200}}) + return +} + +// Delete membership for given image. +// Callee should be image owner +// More details: http://developer.openstack.org/api-ref-image-v2.html#deleteImageMember-v2 +func Delete(client *gophercloud.ServiceClient, imageID string, memberID string) (r DeleteResult) { + _, r.Err = client.Delete(deleteMemberURL(client, imageID, memberID), &gophercloud.RequestOpts{OkCodes: []int{204}}) + return +} + +// UpdateOptsBuilder allows extensions to add additional attributes to the Update request. +type UpdateOptsBuilder interface { + ToImageMemberUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts implements UpdateOptsBuilder +type UpdateOpts struct { + Status string +} + +// ToMemberUpdateMap formats an UpdateOpts structure into a request body. +func (opts UpdateOpts) ToImageMemberUpdateMap() (map[string]interface{}, error) { + return map[string]interface{}{ + "status": opts.Status, + }, nil +} + +// Update function updates member +// More details: http://developer.openstack.org/api-ref-image-v2.html#updateImageMember-v2 +func Update(client *gophercloud.ServiceClient, imageID string, memberID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToImageMemberUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(updateMemberURL(client, imageID, memberID), b, &r.Body, + &gophercloud.RequestOpts{OkCodes: []int{200}}) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/members/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/members/results.go new file mode 100644 index 0000000000..d3cc1ceaf7 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/members/results.go @@ -0,0 +1,69 @@ +package members + +import ( + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Member model +type Member struct { + CreatedAt time.Time `json:"created_at"` + ImageID string `json:"image_id"` + MemberID string `json:"member_id"` + Schema string `json:"schema"` + Status string `json:"status"` + UpdatedAt time.Time `json:"updated_at"` +} + +// Extract Member model from request if possible +func (r commonResult) Extract() (*Member, error) { + var s *Member + err := r.ExtractInto(&s) + return s, err +} + +// MemberPage is a single page of Members results. +type MemberPage struct { + pagination.SinglePageBase +} + +// ExtractMembers returns a slice of Members contained in a single page of results. +func ExtractMembers(r pagination.Page) ([]Member, error) { + var s struct { + Members []Member `json:"members"` + } + err := r.(MemberPage).ExtractInto(&s) + return s.Members, err +} + +// IsEmpty determines whether or not a page of Members contains any results. +func (r MemberPage) IsEmpty() (bool, error) { + members, err := ExtractMembers(r) + return len(members) == 0, err +} + +type commonResult struct { + gophercloud.Result +} + +// CreateResult result model +type CreateResult struct { + commonResult +} + +// DetailsResult model +type DetailsResult struct { + commonResult +} + +// UpdateResult model +type UpdateResult struct { + commonResult +} + +// DeleteResult model +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/members/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/members/testing/fixtures.go new file mode 100644 index 0000000000..c08fc5ebaa --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/members/testing/fixtures.go @@ -0,0 +1,138 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" + fakeclient "github.com/gophercloud/gophercloud/testhelper/client" +) + +// HandleCreateImageMemberSuccessfully setup +func HandleCreateImageMemberSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea/members", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + th.TestJSONRequest(t, r, `{"member": "8989447062e04a818baf9e073fd04fa7"}`) + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "created_at": "2013-09-20T19:22:19Z", + "image_id": "da3b75d9-3f4a-40e7-8a2c-bfab23927dea", + "member_id": "8989447062e04a818baf9e073fd04fa7", + "schema": "/v2/schemas/member", + "status": "pending", + "updated_at": "2013-09-20T19:25:31Z" + }`) + + }) +} + +// HandleImageMemberList happy path setup +func HandleImageMemberList(t *testing.T) { + th.Mux.HandleFunc("/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea/members", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "members": [ + { + "created_at": "2013-10-07T17:58:03Z", + "image_id": "da3b75d9-3f4a-40e7-8a2c-bfab23927dea", + "member_id": "123456789", + "schema": "/v2/schemas/member", + "status": "pending", + "updated_at": "2013-10-07T17:58:03Z" + }, + { + "created_at": "2013-10-07T17:58:55Z", + "image_id": "da3b75d9-3f4a-40e7-8a2c-bfab23927dea", + "member_id": "987654321", + "schema": "/v2/schemas/member", + "status": "accepted", + "updated_at": "2013-10-08T12:08:55Z" + } + ], + "schema": "/v2/schemas/members" + }`) + }) +} + +// HandleImageMemberEmptyList happy path setup +func HandleImageMemberEmptyList(t *testing.T) { + th.Mux.HandleFunc("/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea/members", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "members": [], + "schema": "/v2/schemas/members" + }`) + }) +} + +// HandleImageMemberDetails setup +func HandleImageMemberDetails(t *testing.T) { + th.Mux.HandleFunc("/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea/members/8989447062e04a818baf9e073fd04fa7", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "status": "pending", + "created_at": "2013-11-26T07:21:21Z", + "updated_at": "2013-11-26T07:21:21Z", + "image_id": "da3b75d9-3f4a-40e7-8a2c-bfab23927dea", + "member_id": "8989447062e04a818baf9e073fd04fa7", + "schema": "/v2/schemas/member" + }`) + }) +} + +// HandleImageMemberDeleteSuccessfully setup +func HandleImageMemberDeleteSuccessfully(t *testing.T) *CallsCounter { + var counter CallsCounter + th.Mux.HandleFunc("/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea/members/8989447062e04a818baf9e073fd04fa7", func(w http.ResponseWriter, r *http.Request) { + counter.Counter = counter.Counter + 1 + + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) + return &counter +} + +// HandleImageMemberUpdate setup +func HandleImageMemberUpdate(t *testing.T) *CallsCounter { + var counter CallsCounter + th.Mux.HandleFunc("/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea/members/8989447062e04a818baf9e073fd04fa7", func(w http.ResponseWriter, r *http.Request) { + counter.Counter = counter.Counter + 1 + + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID) + + th.TestJSONRequest(t, r, `{"status": "accepted"}`) + + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "status": "accepted", + "created_at": "2013-11-26T07:21:21Z", + "updated_at": "2013-11-26T07:21:21Z", + "image_id": "da3b75d9-3f4a-40e7-8a2c-bfab23927dea", + "member_id": "8989447062e04a818baf9e073fd04fa7", + "schema": "/v2/schemas/member" + }`) + }) + return &counter +} + +// CallsCounter for checking if request handler was called at all +type CallsCounter struct { + Counter int +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/members/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/members/testing/requests_test.go new file mode 100644 index 0000000000..04624c9937 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/members/testing/requests_test.go @@ -0,0 +1,172 @@ +package testing + +import ( + "testing" + "time" + + "github.com/gophercloud/gophercloud/openstack/imageservice/v2/members" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + fakeclient "github.com/gophercloud/gophercloud/testhelper/client" +) + +const createdAtString = "2013-09-20T19:22:19Z" +const updatedAtString = "2013-09-20T19:25:31Z" + +func TestCreateMemberSuccessfully(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleCreateImageMemberSuccessfully(t) + im, err := members.Create(fakeclient.ServiceClient(), "da3b75d9-3f4a-40e7-8a2c-bfab23927dea", + "8989447062e04a818baf9e073fd04fa7").Extract() + th.AssertNoErr(t, err) + + createdAt, err := time.Parse(time.RFC3339, createdAtString) + th.AssertNoErr(t, err) + + updatedAt, err := time.Parse(time.RFC3339, updatedAtString) + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, members.Member{ + CreatedAt: createdAt, + ImageID: "da3b75d9-3f4a-40e7-8a2c-bfab23927dea", + MemberID: "8989447062e04a818baf9e073fd04fa7", + Schema: "/v2/schemas/member", + Status: "pending", + UpdatedAt: updatedAt, + }, *im) + +} + +func TestMemberListSuccessfully(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleImageMemberList(t) + + pager := members.List(fakeclient.ServiceClient(), "da3b75d9-3f4a-40e7-8a2c-bfab23927dea") + t.Logf("Pager state %v", pager) + count, pages := 0, 0 + err := pager.EachPage(func(page pagination.Page) (bool, error) { + pages++ + t.Logf("Page %v", page) + members, err := members.ExtractMembers(page) + if err != nil { + return false, err + } + + for _, i := range members { + t.Logf("%s\t%s\t%s\t%s\t\n", i.ImageID, i.MemberID, i.Status, i.Schema) + count++ + } + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, pages) + th.AssertEquals(t, 2, count) +} + +func TestMemberListEmpty(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleImageMemberEmptyList(t) + + pager := members.List(fakeclient.ServiceClient(), "da3b75d9-3f4a-40e7-8a2c-bfab23927dea") + t.Logf("Pager state %v", pager) + count, pages := 0, 0 + err := pager.EachPage(func(page pagination.Page) (bool, error) { + pages++ + t.Logf("Page %v", page) + members, err := members.ExtractMembers(page) + if err != nil { + return false, err + } + + for _, i := range members { + t.Logf("%s\t%s\t%s\t%s\t\n", i.ImageID, i.MemberID, i.Status, i.Schema) + count++ + } + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 0, pages) + th.AssertEquals(t, 0, count) +} + +func TestShowMemberDetails(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleImageMemberDetails(t) + md, err := members.Get(fakeclient.ServiceClient(), + "da3b75d9-3f4a-40e7-8a2c-bfab23927dea", + "8989447062e04a818baf9e073fd04fa7").Extract() + + th.AssertNoErr(t, err) + if md == nil { + t.Errorf("Expected non-nil value for md") + } + + createdAt, err := time.Parse(time.RFC3339, "2013-11-26T07:21:21Z") + th.AssertNoErr(t, err) + + updatedAt, err := time.Parse(time.RFC3339, "2013-11-26T07:21:21Z") + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, members.Member{ + CreatedAt: createdAt, + ImageID: "da3b75d9-3f4a-40e7-8a2c-bfab23927dea", + MemberID: "8989447062e04a818baf9e073fd04fa7", + Schema: "/v2/schemas/member", + Status: "pending", + UpdatedAt: updatedAt, + }, *md) +} + +func TestDeleteMember(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + counter := HandleImageMemberDeleteSuccessfully(t) + + result := members.Delete(fakeclient.ServiceClient(), "da3b75d9-3f4a-40e7-8a2c-bfab23927dea", + "8989447062e04a818baf9e073fd04fa7") + th.AssertEquals(t, 1, counter.Counter) + th.AssertNoErr(t, result.Err) +} + +func TestMemberUpdateSuccessfully(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + counter := HandleImageMemberUpdate(t) + im, err := members.Update(fakeclient.ServiceClient(), "da3b75d9-3f4a-40e7-8a2c-bfab23927dea", + "8989447062e04a818baf9e073fd04fa7", + members.UpdateOpts{ + Status: "accepted", + }).Extract() + th.AssertEquals(t, 1, counter.Counter) + th.AssertNoErr(t, err) + + createdAt, err := time.Parse(time.RFC3339, "2013-11-26T07:21:21Z") + th.AssertNoErr(t, err) + + updatedAt, err := time.Parse(time.RFC3339, "2013-11-26T07:21:21Z") + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, members.Member{ + CreatedAt: createdAt, + ImageID: "da3b75d9-3f4a-40e7-8a2c-bfab23927dea", + MemberID: "8989447062e04a818baf9e073fd04fa7", + Schema: "/v2/schemas/member", + Status: "accepted", + UpdatedAt: updatedAt, + }, *im) + +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/members/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/members/urls.go new file mode 100644 index 0000000000..0898364e7d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/members/urls.go @@ -0,0 +1,31 @@ +package members + +import "github.com/gophercloud/gophercloud" + +func imageMembersURL(c *gophercloud.ServiceClient, imageID string) string { + return c.ServiceURL("images", imageID, "members") +} + +func listMembersURL(c *gophercloud.ServiceClient, imageID string) string { + return imageMembersURL(c, imageID) +} + +func createMemberURL(c *gophercloud.ServiceClient, imageID string) string { + return imageMembersURL(c, imageID) +} + +func imageMemberURL(c *gophercloud.ServiceClient, imageID string, memberID string) string { + return c.ServiceURL("images", imageID, "members", memberID) +} + +func getMemberURL(c *gophercloud.ServiceClient, imageID string, memberID string) string { + return imageMemberURL(c, imageID, memberID) +} + +func updateMemberURL(c *gophercloud.ServiceClient, imageID string, memberID string) string { + return imageMemberURL(c, imageID, memberID) +} + +func deleteMemberURL(c *gophercloud.ServiceClient, imageID string, memberID string) string { + return imageMemberURL(c, imageID, memberID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/apiversions/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/apiversions/doc.go new file mode 100644 index 0000000000..0208ee20ec --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/apiversions/doc.go @@ -0,0 +1,4 @@ +// Package apiversions provides information and interaction with the different +// API versions for the OpenStack Neutron service. This functionality is not +// restricted to this particular version. +package apiversions diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/apiversions/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/apiversions/requests.go new file mode 100644 index 0000000000..59ece85090 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/apiversions/requests.go @@ -0,0 +1,21 @@ +package apiversions + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListVersions lists all the Neutron API versions available to end-users +func ListVersions(c *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(c, apiVersionsURL(c), func(r pagination.PageResult) pagination.Page { + return APIVersionPage{pagination.SinglePageBase(r)} + }) +} + +// ListVersionResources lists all of the different API resources for a particular +// API versions. Typical resources for Neutron might be: networks, subnets, etc. +func ListVersionResources(c *gophercloud.ServiceClient, v string) pagination.Pager { + return pagination.NewPager(c, apiInfoURL(c, v), func(r pagination.PageResult) pagination.Page { + return APIVersionResourcePage{pagination.SinglePageBase(r)} + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/apiversions/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/apiversions/results.go new file mode 100644 index 0000000000..eff44855d7 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/apiversions/results.go @@ -0,0 +1,66 @@ +package apiversions + +import ( + "github.com/gophercloud/gophercloud/pagination" +) + +// APIVersion represents an API version for Neutron. It contains the status of +// the API, and its unique ID. +type APIVersion struct { + Status string `son:"status"` + ID string `json:"id"` +} + +// APIVersionPage is the page returned by a pager when traversing over a +// collection of API versions. +type APIVersionPage struct { + pagination.SinglePageBase +} + +// IsEmpty checks whether an APIVersionPage struct is empty. +func (r APIVersionPage) IsEmpty() (bool, error) { + is, err := ExtractAPIVersions(r) + return len(is) == 0, err +} + +// ExtractAPIVersions takes a collection page, extracts all of the elements, +// and returns them a slice of APIVersion structs. It is effectively a cast. +func ExtractAPIVersions(r pagination.Page) ([]APIVersion, error) { + var s struct { + Versions []APIVersion `json:"versions"` + } + err := (r.(APIVersionPage)).ExtractInto(&s) + return s.Versions, err +} + +// APIVersionResource represents a generic API resource. It contains the name +// of the resource and its plural collection name. +type APIVersionResource struct { + Name string `json:"name"` + Collection string `json:"collection"` +} + +// APIVersionResourcePage is a concrete type which embeds the common +// SinglePageBase struct, and is used when traversing API versions collections. +type APIVersionResourcePage struct { + pagination.SinglePageBase +} + +// IsEmpty is a concrete function which indicates whether an +// APIVersionResourcePage is empty or not. +func (r APIVersionResourcePage) IsEmpty() (bool, error) { + is, err := ExtractVersionResources(r) + return len(is) == 0, err +} + +// ExtractVersionResources accepts a Page struct, specifically a +// APIVersionResourcePage struct, and extracts the elements into a slice of +// APIVersionResource structs. In other words, the collection is mapped into +// a relevant slice. +func ExtractVersionResources(r pagination.Page) ([]APIVersionResource, error) { + var s struct { + APIVersionResources []APIVersionResource `json:"resources"` + } + err := (r.(APIVersionResourcePage)).ExtractInto(&s) + return s.APIVersionResources, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/apiversions/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/apiversions/testing/doc.go new file mode 100644 index 0000000000..0accd9911c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/apiversions/testing/doc.go @@ -0,0 +1,2 @@ +// networking_apiversions_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/apiversions/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/apiversions/testing/requests_test.go new file mode 100644 index 0000000000..5a66a2a09a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/apiversions/testing/requests_test.go @@ -0,0 +1,183 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud/openstack/networking/v2/apiversions" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestListVersions(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "versions": [ + { + "status": "CURRENT", + "id": "v2.0", + "links": [ + { + "href": "http://23.253.228.211:9696/v2.0", + "rel": "self" + } + ] + } + ] +}`) + }) + + count := 0 + + apiversions.ListVersions(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := apiversions.ExtractAPIVersions(page) + if err != nil { + t.Errorf("Failed to extract API versions: %v", err) + return false, err + } + + expected := []apiversions.APIVersion{ + { + Status: "CURRENT", + ID: "v2.0", + }, + } + + th.AssertDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestNonJSONCannotBeExtractedIntoAPIVersions(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + apiversions.ListVersions(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + if _, err := apiversions.ExtractAPIVersions(page); err == nil { + t.Fatalf("Expected error, got nil") + } + return true, nil + }) +} + +func TestAPIInfo(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "resources": [ + { + "links": [ + { + "href": "http://23.253.228.211:9696/v2.0/subnets", + "rel": "self" + } + ], + "name": "subnet", + "collection": "subnets" + }, + { + "links": [ + { + "href": "http://23.253.228.211:9696/v2.0/networks", + "rel": "self" + } + ], + "name": "network", + "collection": "networks" + }, + { + "links": [ + { + "href": "http://23.253.228.211:9696/v2.0/ports", + "rel": "self" + } + ], + "name": "port", + "collection": "ports" + } + ] +} + `) + }) + + count := 0 + + apiversions.ListVersionResources(fake.ServiceClient(), "v2.0").EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := apiversions.ExtractVersionResources(page) + if err != nil { + t.Errorf("Failed to extract version resources: %v", err) + return false, err + } + + expected := []apiversions.APIVersionResource{ + { + Name: "subnet", + Collection: "subnets", + }, + { + Name: "network", + Collection: "networks", + }, + { + Name: "port", + Collection: "ports", + }, + } + + th.AssertDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestNonJSONCannotBeExtractedIntoAPIVersionResources(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + apiversions.ListVersionResources(fake.ServiceClient(), "v2.0").EachPage(func(page pagination.Page) (bool, error) { + if _, err := apiversions.ExtractVersionResources(page); err == nil { + t.Fatalf("Expected error, got nil") + } + return true, nil + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/apiversions/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/apiversions/urls.go new file mode 100644 index 0000000000..0fa743776d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/apiversions/urls.go @@ -0,0 +1,15 @@ +package apiversions + +import ( + "strings" + + "github.com/gophercloud/gophercloud" +) + +func apiVersionsURL(c *gophercloud.ServiceClient) string { + return c.Endpoint +} + +func apiInfoURL(c *gophercloud.ServiceClient, version string) string { + return c.Endpoint + strings.TrimRight(version, "/") + "/" +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/common/common_tests.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/common/common_tests.go new file mode 100644 index 0000000000..7e1d917280 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/common/common_tests.go @@ -0,0 +1,14 @@ +package common + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +const TokenID = client.TokenID + +func ServiceClient() *gophercloud.ServiceClient { + sc := client.ServiceClient() + sc.ResourceBase = sc.Endpoint + "v2.0/" + return sc +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/delegate.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/delegate.go new file mode 100644 index 0000000000..0c43689bb8 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/delegate.go @@ -0,0 +1,41 @@ +package extensions + +import ( + "github.com/gophercloud/gophercloud" + common "github.com/gophercloud/gophercloud/openstack/common/extensions" + "github.com/gophercloud/gophercloud/pagination" +) + +// Extension is a single OpenStack extension. +type Extension struct { + common.Extension +} + +// GetResult wraps a GetResult from common. +type GetResult struct { + common.GetResult +} + +// ExtractExtensions interprets a Page as a slice of Extensions. +func ExtractExtensions(page pagination.Page) ([]Extension, error) { + inner, err := common.ExtractExtensions(page) + if err != nil { + return nil, err + } + outer := make([]Extension, len(inner)) + for index, ext := range inner { + outer[index] = Extension{ext} + } + return outer, nil +} + +// Get retrieves information for a specific extension using its alias. +func Get(c *gophercloud.ServiceClient, alias string) GetResult { + return GetResult{common.Get(c, alias)} +} + +// List returns a Pager which allows you to iterate over the full collection of extensions. +// It does not accept query parameters. +func List(c *gophercloud.ServiceClient) pagination.Pager { + return common.List(c) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/external/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/external/doc.go new file mode 100755 index 0000000000..dad3a844f7 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/external/doc.go @@ -0,0 +1,3 @@ +// Package external provides information and interaction with the external +// extension for the OpenStack Networking service. +package external diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/external/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/external/requests.go new file mode 100644 index 0000000000..1ee39d2bee --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/external/requests.go @@ -0,0 +1,32 @@ +package external + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/networking/v2/networks" +) + +// CreateOpts is the structure used when creating new external network +// resources. It embeds networks.CreateOpts and so inherits all of its required +// and optional fields, with the addition of the External field. +type CreateOpts struct { + networks.CreateOpts + External *bool `json:"router:external,omitempty"` +} + +// ToNetworkCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToNetworkCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "network") +} + +// UpdateOpts is the structure used when updating existing external network +// resources. It embeds networks.UpdateOpts and so inherits all of its required +// and optional fields, with the addition of the External field. +type UpdateOpts struct { + networks.UpdateOpts + External *bool `json:"router:external,omitempty"` +} + +// ToNetworkUpdateMap casts an UpdateOpts struct to a map. +func (opts UpdateOpts) ToNetworkUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "network") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/external/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/external/results.go new file mode 100644 index 0000000000..7e10c6d299 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/external/results.go @@ -0,0 +1,76 @@ +package external + +import ( + "github.com/gophercloud/gophercloud/openstack/networking/v2/networks" + "github.com/gophercloud/gophercloud/pagination" +) + +// NetworkExternal represents a decorated form of a Network with based on the +// "external-net" extension. +type NetworkExternal struct { + // UUID for the network + ID string `json:"id"` + + // Human-readable name for the network. Might not be unique. + Name string `json:"name"` + + // The administrative state of network. If false (down), the network does not forward packets. + AdminStateUp bool `json:"admin_state_up"` + + // Indicates whether network is currently operational. Possible values include + // `ACTIVE', `DOWN', `BUILD', or `ERROR'. Plug-ins might define additional values. + Status string `json:"status"` + + // Subnets associated with this network. + Subnets []string `json:"subnets"` + + // Owner of network. Only admin users can specify a tenant_id other than its own. + TenantID string `json:"tenant_id"` + + // Specifies whether the network resource can be accessed by any tenant or not. + Shared bool `json:"shared"` + + // Specifies whether the network is an external network or not. + External bool `json:"router:external"` +} + +// ExtractGet decorates a GetResult struct returned from a networks.Get() +// function with extended attributes. +func ExtractGet(r networks.GetResult) (*NetworkExternal, error) { + var s struct { + Network *NetworkExternal `json:"network"` + } + err := r.ExtractInto(&s) + return s.Network, err +} + +// ExtractCreate decorates a CreateResult struct returned from a networks.Create() +// function with extended attributes. +func ExtractCreate(r networks.CreateResult) (*NetworkExternal, error) { + var s struct { + Network *NetworkExternal `json:"network"` + } + err := r.ExtractInto(&s) + return s.Network, err +} + +// ExtractUpdate decorates a UpdateResult struct returned from a +// networks.Update() function with extended attributes. +func ExtractUpdate(r networks.UpdateResult) (*NetworkExternal, error) { + var s struct { + Network *NetworkExternal `json:"network"` + } + err := r.ExtractInto(&s) + return s.Network, err +} + +// ExtractList accepts a Page struct, specifically a NetworkPage struct, and +// extracts the elements into a slice of NetworkExternal structs. In other +// words, a generic collection is mapped into a relevant slice. +func ExtractList(r pagination.Page) ([]NetworkExternal, error) { + var s struct { + Networks []NetworkExternal `json:"networks" json:"networks"` + } + err := (r.(networks.NetworkPage)).ExtractInto(&s) + return s.Networks, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/external/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/external/testing/doc.go new file mode 100644 index 0000000000..8a30f6b9f9 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/external/testing/doc.go @@ -0,0 +1,2 @@ +// networking_extensions_external_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/external/testing/results_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/external/testing/results_test.go new file mode 100644 index 0000000000..82a420ede5 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/external/testing/results_test.go @@ -0,0 +1,262 @@ +package testing + +import ( + "errors" + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/external" + "github.com/gophercloud/gophercloud/openstack/networking/v2/networks" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/networks", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "networks": [ + { + "admin_state_up": true, + "id": "0f38d5ad-10a6-428f-a5fc-825cfe0f1970", + "name": "net1", + "router:external": false, + "shared": false, + "status": "ACTIVE", + "subnets": [ + "25778974-48a8-46e7-8998-9dc8c70d2f06" + ], + "tenant_id": "b575417a6c444a6eb5cc3a58eb4f714a" + }, + { + "admin_state_up": true, + "id": "8d05a1b1-297a-46ca-8974-17debf51ca3c", + "name": "ext_net", + "router:external": true, + "shared": false, + "status": "ACTIVE", + "subnets": [ + "2f1fb918-9b0e-4bf9-9a50-6cebbb4db2c5" + ], + "tenant_id": "5eb8995cf717462c9df8d1edfa498010" + } + ] +} + `) + }) + + count := 0 + + networks.List(fake.ServiceClient(), networks.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := external.ExtractList(page) + if err != nil { + t.Errorf("Failed to extract networks: %v", err) + return false, err + } + + expected := []external.NetworkExternal{ + { + Status: "ACTIVE", + Subnets: []string{"25778974-48a8-46e7-8998-9dc8c70d2f06"}, + Name: "net1", + AdminStateUp: true, + TenantID: "b575417a6c444a6eb5cc3a58eb4f714a", + Shared: false, + ID: "0f38d5ad-10a6-428f-a5fc-825cfe0f1970", + External: false, + }, + { + Status: "ACTIVE", + Subnets: []string{"2f1fb918-9b0e-4bf9-9a50-6cebbb4db2c5"}, + Name: "ext_net", + AdminStateUp: true, + TenantID: "5eb8995cf717462c9df8d1edfa498010", + Shared: false, + ID: "8d05a1b1-297a-46ca-8974-17debf51ca3c", + External: true, + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/networks/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "network": { + "admin_state_up": true, + "id": "8d05a1b1-297a-46ca-8974-17debf51ca3c", + "name": "ext_net", + "router:external": true, + "shared": false, + "status": "ACTIVE", + "subnets": [ + "2f1fb918-9b0e-4bf9-9a50-6cebbb4db2c5" + ], + "tenant_id": "5eb8995cf717462c9df8d1edfa498010" + } +} + `) + }) + + res := networks.Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22") + n, err := external.ExtractGet(res) + + th.AssertNoErr(t, err) + th.AssertEquals(t, true, n.External) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/networks", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "network": { + "admin_state_up": true, + "name": "ext_net", + "router:external": true + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "network": { + "admin_state_up": true, + "id": "8d05a1b1-297a-46ca-8974-17debf51ca3c", + "name": "ext_net", + "router:external": true, + "shared": false, + "status": "ACTIVE", + "subnets": [ + "2f1fb918-9b0e-4bf9-9a50-6cebbb4db2c5" + ], + "tenant_id": "5eb8995cf717462c9df8d1edfa498010" + } +} + `) + }) + + options := external.CreateOpts{ + CreateOpts: networks.CreateOpts{Name: "ext_net", AdminStateUp: gophercloud.Enabled}, + External: gophercloud.Enabled, + } + res := networks.Create(fake.ServiceClient(), options) + + n, err := external.ExtractCreate(res) + + th.AssertNoErr(t, err) + th.AssertEquals(t, true, n.External) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "network": { + "router:external": true, + "name": "new_name" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "network": { + "admin_state_up": true, + "id": "8d05a1b1-297a-46ca-8974-17debf51ca3c", + "name": "new_name", + "router:external": true, + "shared": false, + "status": "ACTIVE", + "subnets": [ + "2f1fb918-9b0e-4bf9-9a50-6cebbb4db2c5" + ], + "tenant_id": "5eb8995cf717462c9df8d1edfa498010" + } +} + `) + }) + + options := external.UpdateOpts{ + UpdateOpts: networks.UpdateOpts{Name: "new_name"}, + External: gophercloud.Enabled, + } + res := networks.Update(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", options) + n, err := external.ExtractUpdate(res) + + th.AssertNoErr(t, err) + th.AssertEquals(t, true, n.External) +} + +func TestExtractFnsReturnsErrWhenResultContainsErr(t *testing.T) { + gr := networks.GetResult{} + gr.Err = errors.New("") + + if _, err := external.ExtractGet(gr); err == nil { + t.Fatalf("Expected error, got one") + } + + ur := networks.UpdateResult{} + ur.Err = errors.New("") + + if _, err := external.ExtractUpdate(ur); err == nil { + t.Fatalf("Expected error, got one") + } + + cr := networks.CreateResult{} + cr.Err = errors.New("") + + if _, err := external.ExtractCreate(cr); err == nil { + t.Fatalf("Expected error, got one") + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/doc.go new file mode 100644 index 0000000000..3ec450a7b3 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/doc.go @@ -0,0 +1,3 @@ +// Package fwaas provides information and interaction with the Firewall +// as a Service extension for the OpenStack Networking service. +package fwaas diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/errors.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/errors.go new file mode 100644 index 0000000000..dd92bb20db --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/errors.go @@ -0,0 +1,11 @@ +package firewalls + +import "fmt" + +func err(str string) error { + return fmt.Errorf("%s", str) +} + +var ( + errPolicyRequired = err("A policy ID is required") +) diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/requests.go new file mode 100644 index 0000000000..21ceb4ea18 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/requests.go @@ -0,0 +1,140 @@ +package firewalls + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToFirewallListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the firewall attributes you want to see returned. SortKey allows you to sort +// by a particular firewall attribute. SortDir sets the direction, and is either +// `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + TenantID string `q:"tenant_id"` + Name string `q:"name"` + Description string `q:"description"` + AdminStateUp bool `q:"admin_state_up"` + Shared bool `q:"shared"` + PolicyID string `q:"firewall_policy_id"` + ID string `q:"id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToFirewallListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToFirewallListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// firewalls. It accepts a ListOpts struct, which allows you to filter +// and sort the returned collection for greater efficiency. +// +// Default policy settings return only those firewalls that are owned by the +// tenant who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := rootURL(c) + if opts != nil { + query, err := opts.ToFirewallListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return FirewallPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Create operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type CreateOptsBuilder interface { + ToFirewallCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains all the values needed to create a new firewall. +type CreateOpts struct { + PolicyID string `json:"firewall_policy_id" required:"true"` + // Only required if the caller has an admin role and wants to create a firewall + // for another tenant. + TenantID string `json:"tenant_id,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + AdminStateUp *bool `json:"admin_state_up,omitempty"` + Shared *bool `json:"shared,omitempty"` +} + +// ToFirewallCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToFirewallCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "firewall") +} + +// Create accepts a CreateOpts struct and uses the values to create a new firewall +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToFirewallCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(rootURL(c), b, &r.Body, nil) + return +} + +// Get retrieves a particular firewall based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(resourceURL(c, id), &r.Body, nil) + return +} + +// UpdateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Update operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type UpdateOptsBuilder interface { + ToFirewallUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contains the values used when updating a firewall. +type UpdateOpts struct { + PolicyID string `json:"firewall_policy_id" required:"true"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + AdminStateUp *bool `json:"admin_state_up,omitempty"` + Shared *bool `json:"shared,omitempty"` +} + +// ToFirewallUpdateMap casts a CreateOpts struct to a map. +func (opts UpdateOpts) ToFirewallUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "firewall") +} + +// Update allows firewalls to be updated. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToFirewallUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete will permanently delete a particular firewall based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(resourceURL(c, id), nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/results.go new file mode 100644 index 0000000000..1403ced2ba --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/results.go @@ -0,0 +1,91 @@ +package firewalls + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Firewall is an OpenStack firewall. +type Firewall struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + AdminStateUp bool `json:"admin_state_up"` + Status string `json:"status"` + PolicyID string `json:"firewall_policy_id"` + TenantID string `json:"tenant_id"` +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a firewall. +func (r commonResult) Extract() (*Firewall, error) { + var s Firewall + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "firewall") +} + +func ExtractFirewallsInto(r pagination.Page, v interface{}) error { + return r.(FirewallPage).Result.ExtractIntoSlicePtr(v, "firewalls") +} + +// FirewallPage is the page returned by a pager when traversing over a +// collection of firewalls. +type FirewallPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of firewalls has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r FirewallPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"firewalls_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a FirewallPage struct is empty. +func (r FirewallPage) IsEmpty() (bool, error) { + is, err := ExtractFirewalls(r) + return len(is) == 0, err +} + +// ExtractFirewalls accepts a Page struct, specifically a RouterPage struct, +// and extracts the elements into a slice of Router structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractFirewalls(r pagination.Page) ([]Firewall, error) { + var s []Firewall + err := ExtractFirewallsInto(r, &s) + return s, err +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/testing/doc.go new file mode 100644 index 0000000000..6b46bba2b9 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/testing/doc.go @@ -0,0 +1,2 @@ +// networking_extensions_fwaas_firewalls_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/testing/requests_test.go new file mode 100644 index 0000000000..13eca65b69 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/testing/requests_test.go @@ -0,0 +1,341 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud" + fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/routerinsertion" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewalls", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "firewalls":[ + { + "status": "ACTIVE", + "name": "fw1", + "admin_state_up": false, + "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b", + "firewall_policy_id": "34be8c83-4d42-4dca-a74e-b77fffb8e28a", + "id": "fb5b5315-64f6-4ea3-8e58-981cc37c6f61", + "description": "OpenStack firewall 1" + }, + { + "status": "PENDING_UPDATE", + "name": "fw2", + "admin_state_up": true, + "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b", + "firewall_policy_id": "34be8c83-4d42-4dca-a74e-b77fffb8e299", + "id": "fb5b5315-64f6-4ea3-8e58-981cc37c6f99", + "description": "OpenStack firewall 2" + } + ] +} + `) + }) + + count := 0 + + firewalls.List(fake.ServiceClient(), firewalls.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := firewalls.ExtractFirewalls(page) + if err != nil { + t.Errorf("Failed to extract members: %v", err) + return false, err + } + + expected := []firewalls.Firewall{ + { + Status: "ACTIVE", + Name: "fw1", + AdminStateUp: false, + TenantID: "b4eedccc6fb74fa8a7ad6b08382b852b", + PolicyID: "34be8c83-4d42-4dca-a74e-b77fffb8e28a", + ID: "fb5b5315-64f6-4ea3-8e58-981cc37c6f61", + Description: "OpenStack firewall 1", + }, + { + Status: "PENDING_UPDATE", + Name: "fw2", + AdminStateUp: true, + TenantID: "b4eedccc6fb74fa8a7ad6b08382b852b", + PolicyID: "34be8c83-4d42-4dca-a74e-b77fffb8e299", + ID: "fb5b5315-64f6-4ea3-8e58-981cc37c6f99", + Description: "OpenStack firewall 2", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestListWithExtensions(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewalls", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "firewalls":[ + { + "status": "ACTIVE", + "name": "fw1", + "admin_state_up": false, + "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b", + "firewall_policy_id": "34be8c83-4d42-4dca-a74e-b77fffb8e28a", + "id": "fb5b5315-64f6-4ea3-8e58-981cc37c6f61", + "description": "OpenStack firewall 1", + "router_ids": ["abcd1234"] + }, + { + "status": "PENDING_UPDATE", + "name": "fw2", + "admin_state_up": true, + "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b", + "firewall_policy_id": "34be8c83-4d42-4dca-a74e-b77fffb8e299", + "id": "fb5b5315-64f6-4ea3-8e58-981cc37c6f99", + "description": "OpenStack firewall 2" + } + ] +} + `) + }) + + type FirewallsWithExt struct { + firewalls.Firewall + routerinsertion.FirewallExt + } + + allPages, err := firewalls.List(fake.ServiceClient(), nil).AllPages() + th.AssertNoErr(t, err) + + var actual []FirewallsWithExt + err = firewalls.ExtractFirewallsInto(allPages, &actual) + th.AssertNoErr(t, err) + th.AssertEquals(t, 2, len(actual)) + th.AssertEquals(t, "fb5b5315-64f6-4ea3-8e58-981cc37c6f61", actual[0].ID) + th.AssertEquals(t, "abcd1234", actual[0].RouterIDs[0]) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewalls", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "firewall":{ + "name": "fw", + "description": "OpenStack firewall", + "admin_state_up": true, + "firewall_policy_id": "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c", + "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "firewall":{ + "status": "PENDING_CREATE", + "name": "fw", + "description": "OpenStack firewall", + "admin_state_up": true, + "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b", + "firewall_policy_id": "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c" + } +} + `) + }) + + options := firewalls.CreateOpts{ + TenantID: "b4eedccc6fb74fa8a7ad6b08382b852b", + Name: "fw", + Description: "OpenStack firewall", + AdminStateUp: gophercloud.Enabled, + PolicyID: "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c", + } + _, err := firewalls.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewalls/fb5b5315-64f6-4ea3-8e58-981cc37c6f61", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "firewall": { + "status": "ACTIVE", + "name": "fw", + "admin_state_up": true, + "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b", + "firewall_policy_id": "34be8c83-4d42-4dca-a74e-b77fffb8e28a", + "id": "fb5b5315-64f6-4ea3-8e58-981cc37c6f61", + "description": "OpenStack firewall" + } +} + `) + }) + + fw, err := firewalls.Get(fake.ServiceClient(), "fb5b5315-64f6-4ea3-8e58-981cc37c6f61").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "ACTIVE", fw.Status) + th.AssertEquals(t, "fw", fw.Name) + th.AssertEquals(t, "OpenStack firewall", fw.Description) + th.AssertEquals(t, true, fw.AdminStateUp) + th.AssertEquals(t, "34be8c83-4d42-4dca-a74e-b77fffb8e28a", fw.PolicyID) + th.AssertEquals(t, "fb5b5315-64f6-4ea3-8e58-981cc37c6f61", fw.ID) + th.AssertEquals(t, "b4eedccc6fb74fa8a7ad6b08382b852b", fw.TenantID) +} + +func TestGetWithExtensions(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewalls/fb5b5315-64f6-4ea3-8e58-981cc37c6f61", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "firewall": { + "status": "ACTIVE", + "name": "fw", + "admin_state_up": true, + "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b", + "firewall_policy_id": "34be8c83-4d42-4dca-a74e-b77fffb8e28a", + "id": "fb5b5315-64f6-4ea3-8e58-981cc37c6f61", + "description": "OpenStack firewall", + "router_ids": ["abcd1234"] + } +} + `) + }) + + var fw struct { + firewalls.Firewall + routerinsertion.FirewallExt + } + + err := firewalls.Get(fake.ServiceClient(), "fb5b5315-64f6-4ea3-8e58-981cc37c6f61").ExtractInto(&fw) + th.AssertNoErr(t, err) + + th.AssertEquals(t, "ACTIVE", fw.Status) + th.AssertEquals(t, "fw", fw.Name) + th.AssertEquals(t, "OpenStack firewall", fw.Description) + th.AssertEquals(t, true, fw.AdminStateUp) + th.AssertEquals(t, "34be8c83-4d42-4dca-a74e-b77fffb8e28a", fw.PolicyID) + th.AssertEquals(t, "fb5b5315-64f6-4ea3-8e58-981cc37c6f61", fw.ID) + th.AssertEquals(t, "b4eedccc6fb74fa8a7ad6b08382b852b", fw.TenantID) + th.AssertEquals(t, "abcd1234", fw.RouterIDs[0]) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewalls/ea5b5315-64f6-4ea3-8e58-981cc37c6576", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "firewall":{ + "name": "fw", + "description": "updated fw", + "admin_state_up":false, + "firewall_policy_id": "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "firewall": { + "status": "ACTIVE", + "name": "fw", + "admin_state_up": false, + "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b", + "firewall_policy_id": "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c", + "id": "ea5b5315-64f6-4ea3-8e58-981cc37c6576", + "description": "OpenStack firewall" + } +} + `) + }) + + options := firewalls.UpdateOpts{ + Name: "fw", + Description: "updated fw", + AdminStateUp: gophercloud.Disabled, + PolicyID: "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c", + } + + _, err := firewalls.Update(fake.ServiceClient(), "ea5b5315-64f6-4ea3-8e58-981cc37c6576", options).Extract() + th.AssertNoErr(t, err) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewalls/4ec89087-d057-4e2c-911f-60a3b47ee304", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := firewalls.Delete(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304") + th.AssertNoErr(t, res.Err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/urls.go new file mode 100644 index 0000000000..807ea1ab65 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/urls.go @@ -0,0 +1,16 @@ +package firewalls + +import "github.com/gophercloud/gophercloud" + +const ( + rootPath = "fw" + resourcePath = "firewalls" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/policies/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/policies/requests.go new file mode 100644 index 0000000000..437d1248b7 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/policies/requests.go @@ -0,0 +1,173 @@ +package policies + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToPolicyListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the firewall policy attributes you want to see returned. SortKey allows you +// to sort by a particular firewall policy attribute. SortDir sets the direction, +// and is either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + TenantID string `q:"tenant_id"` + Name string `q:"name"` + Description string `q:"description"` + Shared *bool `q:"shared"` + Audited *bool `q:"audited"` + ID string `q:"id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToPolicyListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToPolicyListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// firewall policies. It accepts a ListOpts struct, which allows you to filter +// and sort the returned collection for greater efficiency. +// +// Default policy settings return only those firewall policies that are owned by the +// tenant who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := rootURL(c) + if opts != nil { + query, err := opts.ToPolicyListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return PolicyPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Create operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type CreateOptsBuilder interface { + ToFirewallPolicyCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains all the values needed to create a new firewall policy. +type CreateOpts struct { + // Only required if the caller has an admin role and wants to create a firewall policy + // for another tenant. + TenantID string `json:"tenant_id,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Shared *bool `json:"shared,omitempty"` + Audited *bool `json:"audited,omitempty"` + Rules []string `json:"firewall_rules,omitempty"` +} + +// ToFirewallPolicyCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToFirewallPolicyCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "firewall_policy") +} + +// Create accepts a CreateOpts struct and uses the values to create a new firewall policy +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToFirewallPolicyCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(rootURL(c), b, &r.Body, nil) + return +} + +// Get retrieves a particular firewall policy based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(resourceURL(c, id), &r.Body, nil) + return +} + +// UpdateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Update operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type UpdateOptsBuilder interface { + ToFirewallPolicyUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contains the values used when updating a firewall policy. +type UpdateOpts struct { + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Shared *bool `json:"shared,omitempty"` + Audited *bool `json:"audited,omitempty"` + Rules []string `json:"firewall_rules,omitempty"` +} + +// ToFirewallPolicyUpdateMap casts a CreateOpts struct to a map. +func (opts UpdateOpts) ToFirewallPolicyUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "firewall_policy") +} + +// Update allows firewall policies to be updated. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToFirewallPolicyUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete will permanently delete a particular firewall policy based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(resourceURL(c, id), nil) + return +} + +type InsertRuleOptsBuilder interface { + ToFirewallPolicyInsertRuleMap() (map[string]interface{}, error) +} + +type InsertRuleOpts struct { + ID string `json:"firewall_rule_id" required:"true"` + BeforeRuleID string `json:"insert_before,omitempty"` + AfterRuleID string `json:"insert_after,omitempty"` +} + +func (opts InsertRuleOpts) ToFirewallPolicyInsertRuleMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "") +} + +func AddRule(c *gophercloud.ServiceClient, id string, opts InsertRuleOptsBuilder) (r InsertRuleResult) { + b, err := opts.ToFirewallPolicyInsertRuleMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(insertURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +func RemoveRule(c *gophercloud.ServiceClient, id, ruleID string) (r RemoveRuleResult) { + b := map[string]interface{}{"firewall_rule_id": ruleID} + _, r.Err = c.Put(removeURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/policies/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/policies/results.go new file mode 100644 index 0000000000..9c5b1861e3 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/policies/results.go @@ -0,0 +1,97 @@ +package policies + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Policy is a firewall policy. +type Policy struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + TenantID string `json:"tenant_id"` + Audited bool `json:"audited"` + Shared bool `json:"shared"` + Rules []string `json:"firewall_rules,omitempty"` +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a firewall policy. +func (r commonResult) Extract() (*Policy, error) { + var s struct { + Policy *Policy `json:"firewall_policy"` + } + err := r.ExtractInto(&s) + return s.Policy, err +} + +// PolicyPage is the page returned by a pager when traversing over a +// collection of firewall policies. +type PolicyPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of firewall policies has +// reached the end of a page and the pager seeks to traverse over a new one. +// In order to do this, it needs to construct the next page's URL. +func (r PolicyPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"firewall_policies_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a PolicyPage struct is empty. +func (r PolicyPage) IsEmpty() (bool, error) { + is, err := ExtractPolicies(r) + return len(is) == 0, err +} + +// ExtractPolicies accepts a Page struct, specifically a RouterPage struct, +// and extracts the elements into a slice of Router structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractPolicies(r pagination.Page) ([]Policy, error) { + var s struct { + Policies []Policy `json:"firewall_policies"` + } + err := (r.(PolicyPage)).ExtractInto(&s) + return s.Policies, err +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// InsertRuleResult represents the result of an InsertRule operation. +type InsertRuleResult struct { + commonResult +} + +// RemoveRuleResult represents the result of a RemoveRule operation. +type RemoveRuleResult struct { + commonResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/policies/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/policies/testing/doc.go new file mode 100644 index 0000000000..d2707f0c19 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/policies/testing/doc.go @@ -0,0 +1,2 @@ +// networking_extensions_fwaas_policies_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/policies/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/policies/testing/requests_test.go new file mode 100644 index 0000000000..11b9848f52 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/policies/testing/requests_test.go @@ -0,0 +1,274 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud" + fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/policies" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewall_policies", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "firewall_policies": [ + { + "name": "policy1", + "firewall_rules": [ + "75452b36-268e-4e75-aaf4-f0e7ed50bc97", + "c9e77ca0-1bc8-497d-904d-948107873dc6" + ], + "tenant_id": "9145d91459d248b1b02fdaca97c6a75d", + "audited": true, + "shared": false, + "id": "f2b08c1e-aa81-4668-8ae1-1401bcb0576c", + "description": "Firewall policy 1" + }, + { + "name": "policy2", + "firewall_rules": [ + "03d2a6ad-633f-431a-8463-4370d06a22c8" + ], + "tenant_id": "9145d91459d248b1b02fdaca97c6a75d", + "audited": false, + "shared": true, + "id": "c854fab5-bdaf-4a86-9359-78de93e5df01", + "description": "Firewall policy 2" + } + ] +} + `) + }) + + count := 0 + + policies.List(fake.ServiceClient(), policies.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := policies.ExtractPolicies(page) + if err != nil { + t.Errorf("Failed to extract members: %v", err) + return false, err + } + + expected := []policies.Policy{ + { + Name: "policy1", + Rules: []string{ + "75452b36-268e-4e75-aaf4-f0e7ed50bc97", + "c9e77ca0-1bc8-497d-904d-948107873dc6", + }, + TenantID: "9145d91459d248b1b02fdaca97c6a75d", + Audited: true, + Shared: false, + ID: "f2b08c1e-aa81-4668-8ae1-1401bcb0576c", + Description: "Firewall policy 1", + }, + { + Name: "policy2", + Rules: []string{ + "03d2a6ad-633f-431a-8463-4370d06a22c8", + }, + TenantID: "9145d91459d248b1b02fdaca97c6a75d", + Audited: false, + Shared: true, + ID: "c854fab5-bdaf-4a86-9359-78de93e5df01", + Description: "Firewall policy 2", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewall_policies", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "firewall_policy":{ + "name": "policy", + "firewall_rules": [ + "98a58c87-76be-ae7c-a74e-b77fffb88d95", + "11a58c87-76be-ae7c-a74e-b77fffb88a32" + ], + "description": "Firewall policy", + "tenant_id": "9145d91459d248b1b02fdaca97c6a75d", + "audited": true, + "shared": false + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "firewall_policy":{ + "name": "policy", + "firewall_rules": [ + "98a58c87-76be-ae7c-a74e-b77fffb88d95", + "11a58c87-76be-ae7c-a74e-b77fffb88a32" + ], + "tenant_id": "9145d91459d248b1b02fdaca97c6a75d", + "audited": false, + "id": "f2b08c1e-aa81-4668-8ae1-1401bcb0576c", + "description": "Firewall policy" + } +} + `) + }) + + options := policies.CreateOpts{ + TenantID: "9145d91459d248b1b02fdaca97c6a75d", + Name: "policy", + Description: "Firewall policy", + Shared: gophercloud.Disabled, + Audited: gophercloud.Enabled, + Rules: []string{ + "98a58c87-76be-ae7c-a74e-b77fffb88d95", + "11a58c87-76be-ae7c-a74e-b77fffb88a32", + }, + } + + _, err := policies.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewall_policies/bcab5315-64f6-4ea3-8e58-981cc37c6f61", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "firewall_policy":{ + "name": "www", + "firewall_rules": [ + "75452b36-268e-4e75-aaf4-f0e7ed50bc97", + "c9e77ca0-1bc8-497d-904d-948107873dc6", + "03d2a6ad-633f-431a-8463-4370d06a22c8" + ], + "tenant_id": "9145d91459d248b1b02fdaca97c6a75d", + "audited": false, + "id": "f2b08c1e-aa81-4668-8ae1-1401bcb0576c", + "description": "Firewall policy web" + } +} + `) + }) + + policy, err := policies.Get(fake.ServiceClient(), "bcab5315-64f6-4ea3-8e58-981cc37c6f61").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "www", policy.Name) + th.AssertEquals(t, "f2b08c1e-aa81-4668-8ae1-1401bcb0576c", policy.ID) + th.AssertEquals(t, "Firewall policy web", policy.Description) + th.AssertEquals(t, 3, len(policy.Rules)) + th.AssertEquals(t, "75452b36-268e-4e75-aaf4-f0e7ed50bc97", policy.Rules[0]) + th.AssertEquals(t, "c9e77ca0-1bc8-497d-904d-948107873dc6", policy.Rules[1]) + th.AssertEquals(t, "03d2a6ad-633f-431a-8463-4370d06a22c8", policy.Rules[2]) + th.AssertEquals(t, "9145d91459d248b1b02fdaca97c6a75d", policy.TenantID) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewall_policies/f2b08c1e-aa81-4668-8ae1-1401bcb0576c", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "firewall_policy":{ + "name": "policy", + "firewall_rules": [ + "98a58c87-76be-ae7c-a74e-b77fffb88d95", + "11a58c87-76be-ae7c-a74e-b77fffb88a32" + ], + "description": "Firewall policy" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "firewall_policy":{ + "name": "policy", + "firewall_rules": [ + "75452b36-268e-4e75-aaf4-f0e7ed50bc97", + "c9e77ca0-1bc8-497d-904d-948107873dc6", + "03d2a6ad-633f-431a-8463-4370d06a22c8" + ], + "tenant_id": "9145d91459d248b1b02fdaca97c6a75d", + "audited": false, + "id": "f2b08c1e-aa81-4668-8ae1-1401bcb0576c", + "description": "Firewall policy" + } +} + `) + }) + + options := policies.UpdateOpts{ + Name: "policy", + Description: "Firewall policy", + Rules: []string{ + "98a58c87-76be-ae7c-a74e-b77fffb88d95", + "11a58c87-76be-ae7c-a74e-b77fffb88a32", + }, + } + + _, err := policies.Update(fake.ServiceClient(), "f2b08c1e-aa81-4668-8ae1-1401bcb0576c", options).Extract() + th.AssertNoErr(t, err) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewall_policies/4ec89077-d057-4a2b-911f-60a3b47ee304", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := policies.Delete(fake.ServiceClient(), "4ec89077-d057-4a2b-911f-60a3b47ee304") + th.AssertNoErr(t, res.Err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/policies/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/policies/urls.go new file mode 100644 index 0000000000..c252b79dd0 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/policies/urls.go @@ -0,0 +1,26 @@ +package policies + +import "github.com/gophercloud/gophercloud" + +const ( + rootPath = "fw" + resourcePath = "firewall_policies" + insertPath = "insert_rule" + removePath = "remove_rule" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} + +func insertURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id, insertPath) +} + +func removeURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id, removePath) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/routerinsertion/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/routerinsertion/doc.go new file mode 100644 index 0000000000..9b847e2f5e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/routerinsertion/doc.go @@ -0,0 +1,2 @@ +// Package routerinsertion implements the fwaasrouterinsertion FWaaS extension. +package routerinsertion diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/routerinsertion/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/routerinsertion/requests.go new file mode 100644 index 0000000000..fce100f87e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/routerinsertion/requests.go @@ -0,0 +1,43 @@ +package routerinsertion + +import ( + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls" +) + +// CreateOptsExt adds a RouterIDs option to the base CreateOpts. +type CreateOptsExt struct { + firewalls.CreateOptsBuilder + RouterIDs []string `json:"router_ids"` +} + +// ToFirewallCreateMap adds router_ids to the base firewall creation options. +func (opts CreateOptsExt) ToFirewallCreateMap() (map[string]interface{}, error) { + base, err := opts.CreateOptsBuilder.ToFirewallCreateMap() + if err != nil { + return nil, err + } + + firewallMap := base["firewall"].(map[string]interface{}) + firewallMap["router_ids"] = opts.RouterIDs + + return base, nil +} + +// UpdateOptsExt updates a RouterIDs option to the base UpdateOpts. +type UpdateOptsExt struct { + firewalls.UpdateOptsBuilder + RouterIDs []string `json:"router_ids"` +} + +// ToFirewallUpdateMap adds router_ids to the base firewall update options. +func (opts UpdateOptsExt) ToFirewallUpdateMap() (map[string]interface{}, error) { + base, err := opts.UpdateOptsBuilder.ToFirewallUpdateMap() + if err != nil { + return nil, err + } + + firewallMap := base["firewall"].(map[string]interface{}) + firewallMap["router_ids"] = opts.RouterIDs + + return base, nil +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/routerinsertion/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/routerinsertion/results.go new file mode 100644 index 0000000000..85c288e51e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/routerinsertion/results.go @@ -0,0 +1,7 @@ +package routerinsertion + +// FirewallExt is an extension to the base Firewall object +type FirewallExt struct { + // RouterIDs are the routers that the firewall is attached to. + RouterIDs []string `json:"router_ids"` +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/routerinsertion/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/routerinsertion/testing/doc.go new file mode 100644 index 0000000000..36a6c1c434 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/routerinsertion/testing/doc.go @@ -0,0 +1,2 @@ +// networking_extensions_fwaas_extensions_routerinsertion_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/routerinsertion/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/routerinsertion/testing/requests_test.go new file mode 100644 index 0000000000..ac7a2be8d2 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/routerinsertion/testing/requests_test.go @@ -0,0 +1,235 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud" + fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/routerinsertion" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewalls", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "firewall":{ + "name": "fw", + "description": "OpenStack firewall", + "admin_state_up": true, + "firewall_policy_id": "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c", + "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b", + "router_ids": [ + "8a3a0d6a-34b5-4a92-b65d-6375a4c1e9e8" + ] + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "firewall":{ + "status": "PENDING_CREATE", + "name": "fw", + "description": "OpenStack firewall", + "admin_state_up": true, + "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b", + "firewall_policy_id": "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c" + } +} + `) + }) + + firewallCreateOpts := firewalls.CreateOpts{ + TenantID: "b4eedccc6fb74fa8a7ad6b08382b852b", + Name: "fw", + Description: "OpenStack firewall", + AdminStateUp: gophercloud.Enabled, + PolicyID: "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c", + } + createOpts := routerinsertion.CreateOptsExt{ + CreateOptsBuilder: firewallCreateOpts, + RouterIDs: []string{"8a3a0d6a-34b5-4a92-b65d-6375a4c1e9e8"}, + } + + _, err := firewalls.Create(fake.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) +} + +func TestCreateWithNoRouters(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewalls", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "firewall":{ + "name": "fw", + "description": "OpenStack firewall", + "admin_state_up": true, + "firewall_policy_id": "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c", + "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b", + "router_ids": [] + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "firewall":{ + "status": "PENDING_CREATE", + "name": "fw", + "description": "OpenStack firewall", + "admin_state_up": true, + "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b", + "firewall_policy_id": "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c" + } +} + `) + }) + + firewallCreateOpts := firewalls.CreateOpts{ + TenantID: "b4eedccc6fb74fa8a7ad6b08382b852b", + Name: "fw", + Description: "OpenStack firewall", + AdminStateUp: gophercloud.Enabled, + PolicyID: "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c", + } + createOpts := routerinsertion.CreateOptsExt{ + CreateOptsBuilder: firewallCreateOpts, + RouterIDs: []string{}, + } + + _, err := firewalls.Create(fake.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewalls/ea5b5315-64f6-4ea3-8e58-981cc37c6576", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "firewall":{ + "name": "fw", + "description": "updated fw", + "admin_state_up":false, + "firewall_policy_id": "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c", + "router_ids": [ + "8a3a0d6a-34b5-4a92-b65d-6375a4c1e9e8" + ] + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "firewall": { + "status": "ACTIVE", + "name": "fw", + "admin_state_up": false, + "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b", + "firewall_policy_id": "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c", + "id": "ea5b5315-64f6-4ea3-8e58-981cc37c6576", + "description": "OpenStack firewall" + } +} + `) + }) + + firewallUpdateOpts := firewalls.UpdateOpts{ + Name: "fw", + Description: "updated fw", + AdminStateUp: gophercloud.Disabled, + PolicyID: "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c", + } + updateOpts := routerinsertion.UpdateOptsExt{ + UpdateOptsBuilder: firewallUpdateOpts, + RouterIDs: []string{"8a3a0d6a-34b5-4a92-b65d-6375a4c1e9e8"}, + } + + _, err := firewalls.Update(fake.ServiceClient(), "ea5b5315-64f6-4ea3-8e58-981cc37c6576", updateOpts).Extract() + th.AssertNoErr(t, err) +} + +func TestUpdateWithNoRouters(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewalls/ea5b5315-64f6-4ea3-8e58-981cc37c6576", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "firewall":{ + "name": "fw", + "description": "updated fw", + "admin_state_up":false, + "firewall_policy_id": "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c", + "router_ids": [] + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "firewall": { + "status": "ACTIVE", + "name": "fw", + "admin_state_up": false, + "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b", + "firewall_policy_id": "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c", + "id": "ea5b5315-64f6-4ea3-8e58-981cc37c6576", + "description": "OpenStack firewall" + } +} + `) + }) + + firewallUpdateOpts := firewalls.UpdateOpts{ + Name: "fw", + Description: "updated fw", + AdminStateUp: gophercloud.Disabled, + PolicyID: "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c", + } + updateOpts := routerinsertion.UpdateOptsExt{ + UpdateOptsBuilder: firewallUpdateOpts, + RouterIDs: []string{}, + } + + _, err := firewalls.Update(fake.ServiceClient(), "ea5b5315-64f6-4ea3-8e58-981cc37c6576", updateOpts).Extract() + th.AssertNoErr(t, err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/rules/errors.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/rules/errors.go new file mode 100644 index 0000000000..0b29d39fd9 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/rules/errors.go @@ -0,0 +1,12 @@ +package rules + +import "fmt" + +func err(str string) error { + return fmt.Errorf("%s", str) +} + +var ( + errProtocolRequired = err("A protocol is required (tcp, udp, icmp or any)") + errActionRequired = err("An action is required (allow or deny)") +) diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/rules/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/rules/requests.go new file mode 100644 index 0000000000..c1784b7325 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/rules/requests.go @@ -0,0 +1,188 @@ +package rules + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +type ( + // Protocol represents a valid rule protocol + Protocol string +) + +const ( + // ProtocolAny is to allow any protocol + ProtocolAny Protocol = "any" + + // ProtocolICMP is to allow the ICMP protocol + ProtocolICMP Protocol = "icmp" + + // ProtocolTCP is to allow the TCP protocol + ProtocolTCP Protocol = "tcp" + + // ProtocolUDP is to allow the UDP protocol + ProtocolUDP Protocol = "udp" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToRuleListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the Firewall rule attributes you want to see returned. SortKey allows you to +// sort by a particular firewall rule attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + TenantID string `q:"tenant_id"` + Name string `q:"name"` + Description string `q:"description"` + Protocol string `q:"protocol"` + Action string `q:"action"` + IPVersion int `q:"ip_version"` + SourceIPAddress string `q:"source_ip_address"` + DestinationIPAddress string `q:"destination_ip_address"` + SourcePort string `q:"source_port"` + DestinationPort string `q:"destination_port"` + Enabled bool `q:"enabled"` + ID string `q:"id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToRuleListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToRuleListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// List returns a Pager which allows you to iterate over a collection of +// firewall rules. It accepts a ListOpts struct, which allows you to filter +// and sort the returned collection for greater efficiency. +// +// Default policy settings return only those firewall rules that are owned by the +// tenant who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := rootURL(c) + + if opts != nil { + query, err := opts.ToRuleListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return RulePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Create operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type CreateOptsBuilder interface { + ToRuleCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains all the values needed to create a new firewall rule. +type CreateOpts struct { + Protocol Protocol `json:"protocol" required:"true"` + Action string `json:"action" required:"true"` + TenantID string `json:"tenant_id,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + IPVersion gophercloud.IPVersion `json:"ip_version,omitempty"` + SourceIPAddress string `json:"source_ip_address,omitempty"` + DestinationIPAddress string `json:"destination_ip_address,omitempty"` + SourcePort string `json:"source_port,omitempty"` + DestinationPort string `json:"destination_port,omitempty"` + Shared *bool `json:"shared,omitempty"` + Enabled *bool `json:"enabled,omitempty"` +} + +// ToRuleCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToRuleCreateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "firewall_rule") + if err != nil { + return nil, err + } + + if m := b["firewall_rule"].(map[string]interface{}); m["protocol"] == "any" { + m["protocol"] = nil + } + + return b, nil +} + +// Create accepts a CreateOpts struct and uses the values to create a new firewall rule +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToRuleCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(rootURL(c), b, &r.Body, nil) + return +} + +// Get retrieves a particular firewall rule based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(resourceURL(c, id), &r.Body, nil) + return +} + +// UpdateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Update operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type UpdateOptsBuilder interface { + ToRuleUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contains the values used when updating a firewall rule. +type UpdateOpts struct { + Protocol *string `json:"protocol,omitempty"` + Action *string `json:"action,omitempty"` + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + IPVersion *gophercloud.IPVersion `json:"ip_version,omitempty"` + SourceIPAddress *string `json:"source_ip_address,omitempty"` + DestinationIPAddress *string `json:"destination_ip_address,omitempty"` + SourcePort *string `json:"source_port,omitempty"` + DestinationPort *string `json:"destination_port,omitempty"` + Shared *bool `json:"shared,omitempty"` + Enabled *bool `json:"enabled,omitempty"` +} + +// ToRuleUpdateMap casts a UpdateOpts struct to a map. +func (opts UpdateOpts) ToRuleUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "firewall_rule") +} + +// Update allows firewall policies to be updated. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToRuleUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete will permanently delete a particular firewall rule based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(resourceURL(c, id), nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/rules/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/rules/results.go new file mode 100644 index 0000000000..c44e5a9108 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/rules/results.go @@ -0,0 +1,95 @@ +package rules + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Rule represents a firewall rule +type Rule struct { + ID string `json:"id"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Protocol string `json:"protocol"` + Action string `json:"action"` + IPVersion int `json:"ip_version,omitempty"` + SourceIPAddress string `json:"source_ip_address,omitempty"` + DestinationIPAddress string `json:"destination_ip_address,omitempty"` + SourcePort string `json:"source_port,omitempty"` + DestinationPort string `json:"destination_port,omitempty"` + Shared bool `json:"shared,omitempty"` + Enabled bool `json:"enabled,omitempty"` + PolicyID string `json:"firewall_policy_id"` + Position int `json:"position"` + TenantID string `json:"tenant_id"` +} + +// RulePage is the page returned by a pager when traversing over a +// collection of firewall rules. +type RulePage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of firewall rules has +// reached the end of a page and the pager seeks to traverse over a new one. +// In order to do this, it needs to construct the next page's URL. +func (r RulePage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"firewall_rules_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a RulePage struct is empty. +func (r RulePage) IsEmpty() (bool, error) { + is, err := ExtractRules(r) + return len(is) == 0, err +} + +// ExtractRules accepts a Page struct, specifically a RouterPage struct, +// and extracts the elements into a slice of Router structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractRules(r pagination.Page) ([]Rule, error) { + var s struct { + Rules []Rule `json:"firewall_rules"` + } + err := (r.(RulePage)).ExtractInto(&s) + return s.Rules, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a firewall rule. +func (r commonResult) Extract() (*Rule, error) { + var s struct { + Rule *Rule `json:"firewall_rule"` + } + err := r.ExtractInto(&s) + return s.Rule, err +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/rules/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/rules/testing/doc.go new file mode 100644 index 0000000000..481ae2e5e9 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/rules/testing/doc.go @@ -0,0 +1,2 @@ +// networking_extensions_fwaas_rules_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/rules/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/rules/testing/requests_test.go new file mode 100644 index 0000000000..2fedfa8ac7 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/rules/testing/requests_test.go @@ -0,0 +1,381 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud" + fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/rules" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewall_rules", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "firewall_rules": [ + { + "protocol": "tcp", + "description": "ssh rule", + "source_port": null, + "source_ip_address": null, + "destination_ip_address": "192.168.1.0/24", + "firewall_policy_id": "e2a5fb51-698c-4898-87e8-f1eee6b50919", + "position": 2, + "destination_port": "22", + "id": "f03bd950-6c56-4f5e-a307-45967078f507", + "name": "ssh_form_any", + "tenant_id": "80cf934d6ffb4ef5b244f1c512ad1e61", + "enabled": true, + "action": "allow", + "ip_version": 4, + "shared": false + }, + { + "protocol": "udp", + "description": "udp rule", + "source_port": null, + "source_ip_address": null, + "destination_ip_address": null, + "firewall_policy_id": "98d7fb51-698c-4123-87e8-f1eee6b5ab7e", + "position": 1, + "destination_port": null, + "id": "ab7bd950-6c56-4f5e-a307-45967078f890", + "name": "deny_all_udp", + "tenant_id": "80cf934d6ffb4ef5b244f1c512ad1e61", + "enabled": true, + "action": "deny", + "ip_version": 4, + "shared": false + } + ] +} + `) + }) + + count := 0 + + rules.List(fake.ServiceClient(), rules.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := rules.ExtractRules(page) + if err != nil { + t.Errorf("Failed to extract members: %v", err) + return false, err + } + + expected := []rules.Rule{ + { + Protocol: "tcp", + Description: "ssh rule", + SourcePort: "", + SourceIPAddress: "", + DestinationIPAddress: "192.168.1.0/24", + PolicyID: "e2a5fb51-698c-4898-87e8-f1eee6b50919", + Position: 2, + DestinationPort: "22", + ID: "f03bd950-6c56-4f5e-a307-45967078f507", + Name: "ssh_form_any", + TenantID: "80cf934d6ffb4ef5b244f1c512ad1e61", + Enabled: true, + Action: "allow", + IPVersion: 4, + Shared: false, + }, + { + Protocol: "udp", + Description: "udp rule", + SourcePort: "", + SourceIPAddress: "", + DestinationIPAddress: "", + PolicyID: "98d7fb51-698c-4123-87e8-f1eee6b5ab7e", + Position: 1, + DestinationPort: "", + ID: "ab7bd950-6c56-4f5e-a307-45967078f890", + Name: "deny_all_udp", + TenantID: "80cf934d6ffb4ef5b244f1c512ad1e61", + Enabled: true, + Action: "deny", + IPVersion: 4, + Shared: false, + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewall_rules", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "firewall_rule": { + "protocol": "tcp", + "description": "ssh rule", + "destination_ip_address": "192.168.1.0/24", + "destination_port": "22", + "name": "ssh_form_any", + "action": "allow", + "tenant_id": "80cf934d6ffb4ef5b244f1c512ad1e61" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "firewall_rule":{ + "protocol": "tcp", + "description": "ssh rule", + "source_port": null, + "source_ip_address": null, + "destination_ip_address": "192.168.1.0/24", + "firewall_policy_id": "e2a5fb51-698c-4898-87e8-f1eee6b50919", + "position": 2, + "destination_port": "22", + "id": "f03bd950-6c56-4f5e-a307-45967078f507", + "name": "ssh_form_any", + "tenant_id": "80cf934d6ffb4ef5b244f1c512ad1e61", + "enabled": true, + "action": "allow", + "ip_version": 4, + "shared": false + } +} + `) + }) + + options := rules.CreateOpts{ + TenantID: "80cf934d6ffb4ef5b244f1c512ad1e61", + Protocol: rules.ProtocolTCP, + Description: "ssh rule", + DestinationIPAddress: "192.168.1.0/24", + DestinationPort: "22", + Name: "ssh_form_any", + Action: "allow", + } + + _, err := rules.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) +} + +func TestCreateAnyProtocol(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewall_rules", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "firewall_rule": { + "protocol": null, + "description": "any to 192.168.1.0/24", + "destination_ip_address": "192.168.1.0/24", + "name": "any_to_192.168.1.0/24", + "action": "allow", + "tenant_id": "80cf934d6ffb4ef5b244f1c512ad1e61" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "firewall_rule":{ + "protocol": null, + "description": "any to 192.168.1.0/24", + "source_port": null, + "source_ip_address": null, + "destination_ip_address": "192.168.1.0/24", + "firewall_policy_id": "e2a5fb51-698c-4898-87e8-f1eee6b50919", + "position": 2, + "destination_port": null, + "id": "f03bd950-6c56-4f5e-a307-45967078f507", + "name": "any_to_192.168.1.0/24", + "tenant_id": "80cf934d6ffb4ef5b244f1c512ad1e61", + "enabled": true, + "action": "allow", + "ip_version": 4, + "shared": false + } +} + `) + }) + + options := rules.CreateOpts{ + TenantID: "80cf934d6ffb4ef5b244f1c512ad1e61", + Protocol: rules.ProtocolAny, + Description: "any to 192.168.1.0/24", + DestinationIPAddress: "192.168.1.0/24", + Name: "any_to_192.168.1.0/24", + Action: "allow", + } + + _, err := rules.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewall_rules/f03bd950-6c56-4f5e-a307-45967078f507", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "firewall_rule":{ + "protocol": "tcp", + "description": "ssh rule", + "source_port": null, + "source_ip_address": null, + "destination_ip_address": "192.168.1.0/24", + "firewall_policy_id": "e2a5fb51-698c-4898-87e8-f1eee6b50919", + "position": 2, + "destination_port": "22", + "id": "f03bd950-6c56-4f5e-a307-45967078f507", + "name": "ssh_form_any", + "tenant_id": "80cf934d6ffb4ef5b244f1c512ad1e61", + "enabled": true, + "action": "allow", + "ip_version": 4, + "shared": false + } +} + `) + }) + + rule, err := rules.Get(fake.ServiceClient(), "f03bd950-6c56-4f5e-a307-45967078f507").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "tcp", rule.Protocol) + th.AssertEquals(t, "ssh rule", rule.Description) + th.AssertEquals(t, "192.168.1.0/24", rule.DestinationIPAddress) + th.AssertEquals(t, "e2a5fb51-698c-4898-87e8-f1eee6b50919", rule.PolicyID) + th.AssertEquals(t, 2, rule.Position) + th.AssertEquals(t, "22", rule.DestinationPort) + th.AssertEquals(t, "f03bd950-6c56-4f5e-a307-45967078f507", rule.ID) + th.AssertEquals(t, "ssh_form_any", rule.Name) + th.AssertEquals(t, "80cf934d6ffb4ef5b244f1c512ad1e61", rule.TenantID) + th.AssertEquals(t, true, rule.Enabled) + th.AssertEquals(t, "allow", rule.Action) + th.AssertEquals(t, 4, rule.IPVersion) + th.AssertEquals(t, false, rule.Shared) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewall_rules/f03bd950-6c56-4f5e-a307-45967078f507", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "firewall_rule":{ + "protocol": "tcp", + "description": "ssh rule", + "destination_ip_address": "192.168.1.0/24", + "destination_port": "22", + "name": "ssh_form_any", + "action": "allow", + "enabled": false + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "firewall_rule":{ + "protocol": "tcp", + "description": "ssh rule", + "destination_ip_address": "192.168.1.0/24", + "firewall_policy_id": "e2a5fb51-698c-4898-87e8-f1eee6b50919", + "position": 2, + "destination_port": "22", + "id": "f03bd950-6c56-4f5e-a307-45967078f507", + "name": "ssh_form_any", + "tenant_id": "80cf934d6ffb4ef5b244f1c512ad1e61", + "enabled": false, + "action": "allow", + "ip_version": 4, + "shared": false + } +} + `) + }) + + newProtocol := "tcp" + newDescription := "ssh rule" + newDestinationIP := "192.168.1.0/24" + newDestintionPort := "22" + newName := "ssh_form_any" + newAction := "allow" + + options := rules.UpdateOpts{ + Protocol: &newProtocol, + Description: &newDescription, + DestinationIPAddress: &newDestinationIP, + DestinationPort: &newDestintionPort, + Name: &newName, + Action: &newAction, + Enabled: gophercloud.Disabled, + } + + _, err := rules.Update(fake.ServiceClient(), "f03bd950-6c56-4f5e-a307-45967078f507", options).Extract() + th.AssertNoErr(t, err) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewall_rules/4ec89077-d057-4a2b-911f-60a3b47ee304", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := rules.Delete(fake.ServiceClient(), "4ec89077-d057-4a2b-911f-60a3b47ee304") + th.AssertNoErr(t, res.Err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/rules/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/rules/urls.go new file mode 100644 index 0000000000..79654be73e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/rules/urls.go @@ -0,0 +1,16 @@ +package rules + +import "github.com/gophercloud/gophercloud" + +const ( + rootPath = "fw" + resourcePath = "firewall_rules" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/doc.go new file mode 100644 index 0000000000..d533458267 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/doc.go @@ -0,0 +1,5 @@ +// Package layer3 provides access to the Layer-3 networking extension for the +// OpenStack Neutron service. This extension allows API users to route packets +// between subnets, forward packets from internal networks to external ones, +// and access instances from external networks through floating IPs. +package layer3 diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/requests.go new file mode 100644 index 0000000000..0c628426c0 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/requests.go @@ -0,0 +1,147 @@ +package floatingips + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the floating IP attributes you want to see returned. SortKey allows you to +// sort by a particular network attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + ID string `q:"id"` + FloatingNetworkID string `q:"floating_network_id"` + PortID string `q:"port_id"` + FixedIP string `q:"fixed_ip_address"` + FloatingIP string `q:"floating_ip_address"` + TenantID string `q:"tenant_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` + RouterID string `q:"router_id"` + Status string `q:"status"` +} + +// List returns a Pager which allows you to iterate over a collection of +// floating IP resources. It accepts a ListOpts struct, which allows you to +// filter and sort the returned collection for greater efficiency. +func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + q, err := gophercloud.BuildQueryString(&opts) + if err != nil { + return pagination.Pager{Err: err} + } + u := rootURL(c) + q.String() + return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page { + return FloatingIPPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder is the interface type must satisfy to be used as Create +// options. +type CreateOptsBuilder interface { + ToFloatingIPCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains all the values needed to create a new floating IP +// resource. The only required fields are FloatingNetworkID and PortID which +// refer to the external network and internal port respectively. +type CreateOpts struct { + FloatingNetworkID string `json:"floating_network_id" required:"true"` + FloatingIP string `json:"floating_ip_address,omitempty"` + PortID string `json:"port_id,omitempty"` + FixedIP string `json:"fixed_ip_address,omitempty"` + TenantID string `json:"tenant_id,omitempty"` +} + +// ToFloatingIPCreateMap allows CreateOpts to satisfy the CreateOptsBuilder +// interface +func (opts CreateOpts) ToFloatingIPCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "floatingip") +} + +// Create accepts a CreateOpts struct and uses the values provided to create a +// new floating IP resource. You can create floating IPs on external networks +// only. If you provide a FloatingNetworkID which refers to a network that is +// not external (i.e. its `router:external' attribute is False), the operation +// will fail and return a 400 error. +// +// If you do not specify a FloatingIP address value, the operation will +// automatically allocate an available address for the new resource. If you do +// choose to specify one, it must fall within the subnet range for the external +// network - otherwise the operation returns a 400 error. If the FloatingIP +// address is already in use, the operation returns a 409 error code. +// +// You can associate the new resource with an internal port by using the PortID +// field. If you specify a PortID that is not valid, the operation will fail and +// return 404 error code. +// +// You must also configure an IP address for the port associated with the PortID +// you have provided - this is what the FixedIP refers to: an IP fixed to a port. +// Because a port might be associated with multiple IP addresses, you can use +// the FixedIP field to associate a particular IP address rather than have the +// API assume for you. If you specify an IP address that is not valid, the +// operation will fail and return a 400 error code. If the PortID and FixedIP +// are already associated with another resource, the operation will fail and +// returns a 409 error code. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToFloatingIPCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(rootURL(c), b, &r.Body, nil) + return +} + +// Get retrieves a particular floating IP resource based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(resourceURL(c, id), &r.Body, nil) + return +} + +// UpdateOptsBuilder is the interface type must satisfy to be used as Update +// options. +type UpdateOptsBuilder interface { + ToFloatingIPUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contains the values used when updating a floating IP resource. The +// only value that can be updated is which internal port the floating IP is +// linked to. To associate the floating IP with a new internal port, provide its +// ID. To disassociate the floating IP from all ports, provide an empty string. +type UpdateOpts struct { + PortID *string `json:"port_id"` +} + +// ToFloatingIPUpdateMap allows UpdateOpts to satisfy the UpdateOptsBuilder +// interface +func (opts UpdateOpts) ToFloatingIPUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "floatingip") +} + +// Update allows floating IP resources to be updated. Currently, the only way to +// "update" a floating IP is to associate it with a new internal port, or +// disassociated it from all ports. See UpdateOpts for instructions of how to +// do this. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToFloatingIPUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete will permanently delete a particular floating IP resource. Please +// ensure this is what you want - you can also disassociate the IP from existing +// internal ports. +func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(resourceURL(c, id), nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/results.go new file mode 100644 index 0000000000..29d5b5662b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/results.go @@ -0,0 +1,110 @@ +package floatingips + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// FloatingIP represents a floating IP resource. A floating IP is an external +// IP address that is mapped to an internal port and, optionally, a specific +// IP address on a private network. In other words, it enables access to an +// instance on a private network from an external network. For this reason, +// floating IPs can only be defined on networks where the `router:external' +// attribute (provided by the external network extension) is set to True. +type FloatingIP struct { + // Unique identifier for the floating IP instance. + ID string `json:"id"` + + // UUID of the external network where the floating IP is to be created. + FloatingNetworkID string `json:"floating_network_id"` + + // Address of the floating IP on the external network. + FloatingIP string `json:"floating_ip_address"` + + // UUID of the port on an internal network that is associated with the floating IP. + PortID string `json:"port_id"` + + // The specific IP address of the internal port which should be associated + // with the floating IP. + FixedIP string `json:"fixed_ip_address"` + + // Owner of the floating IP. Only admin users can specify a tenant identifier + // other than its own. + TenantID string `json:"tenant_id"` + + // The condition of the API resource. + Status string `json:"status"` + + //The ID of the router used for this Floating-IP + RouterID string `json:"router_id"` +} + +type commonResult struct { + gophercloud.Result +} + +// Extract a result and extracts a FloatingIP resource. +func (r commonResult) Extract() (*FloatingIP, error) { + var s struct { + FloatingIP *FloatingIP `json:"floatingip"` + } + err := r.ExtractInto(&s) + return s.FloatingIP, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of an update operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// FloatingIPPage is the page returned by a pager when traversing over a +// collection of floating IPs. +type FloatingIPPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of floating IPs has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r FloatingIPPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"floatingips_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a NetworkPage struct is empty. +func (r FloatingIPPage) IsEmpty() (bool, error) { + is, err := ExtractFloatingIPs(r) + return len(is) == 0, err +} + +// ExtractFloatingIPs accepts a Page struct, specifically a FloatingIPPage struct, +// and extracts the elements into a slice of FloatingIP structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractFloatingIPs(r pagination.Page) ([]FloatingIP, error) { + var s struct { + FloatingIPs []FloatingIP `json:"floatingips"` + } + err := (r.(FloatingIPPage)).ExtractInto(&s) + return s.FloatingIPs, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/testing/doc.go new file mode 100644 index 0000000000..aa133389c8 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/testing/doc.go @@ -0,0 +1,2 @@ +// networking_extensions_layer3_floatingips_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/testing/requests_test.go new file mode 100644 index 0000000000..c665a2ef18 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/testing/requests_test.go @@ -0,0 +1,363 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/floatingips", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "floatingips": [ + { + "floating_network_id": "6d67c30a-ddb4-49a1-bec3-a65b286b4170", + "router_id": null, + "fixed_ip_address": null, + "floating_ip_address": "192.0.0.4", + "tenant_id": "017d8de156df4177889f31a9bd6edc00", + "status": "DOWN", + "port_id": null, + "id": "2f95fd2b-9f6a-4e8e-9e9a-2cbe286cbf9e", + "router_id": "1117c30a-ddb4-49a1-bec3-a65b286b4170" + }, + { + "floating_network_id": "90f742b1-6d17-487b-ba95-71881dbc0b64", + "router_id": "0a24cb83-faf5-4d7f-b723-3144ed8a2167", + "fixed_ip_address": "192.0.0.2", + "floating_ip_address": "10.0.0.3", + "tenant_id": "017d8de156df4177889f31a9bd6edc00", + "status": "DOWN", + "port_id": "74a342ce-8e07-4e91-880c-9f834b68fa25", + "id": "ada25a95-f321-4f59-b0e0-f3a970dd3d63", + "router_id": "2227c30a-ddb4-49a1-bec3-a65b286b4170" + } + ] +} + `) + }) + + count := 0 + + floatingips.List(fake.ServiceClient(), floatingips.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := floatingips.ExtractFloatingIPs(page) + if err != nil { + t.Errorf("Failed to extract floating IPs: %v", err) + return false, err + } + + expected := []floatingips.FloatingIP{ + { + FloatingNetworkID: "6d67c30a-ddb4-49a1-bec3-a65b286b4170", + FixedIP: "", + FloatingIP: "192.0.0.4", + TenantID: "017d8de156df4177889f31a9bd6edc00", + Status: "DOWN", + PortID: "", + ID: "2f95fd2b-9f6a-4e8e-9e9a-2cbe286cbf9e", + RouterID: "1117c30a-ddb4-49a1-bec3-a65b286b4170", + }, + { + FloatingNetworkID: "90f742b1-6d17-487b-ba95-71881dbc0b64", + FixedIP: "192.0.0.2", + FloatingIP: "10.0.0.3", + TenantID: "017d8de156df4177889f31a9bd6edc00", + Status: "DOWN", + PortID: "74a342ce-8e07-4e91-880c-9f834b68fa25", + ID: "ada25a95-f321-4f59-b0e0-f3a970dd3d63", + RouterID: "2227c30a-ddb4-49a1-bec3-a65b286b4170", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestInvalidNextPageURLs(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/floatingips", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{"floatingips": [{}], "floatingips_links": {}}`) + }) + + floatingips.List(fake.ServiceClient(), floatingips.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + floatingips.ExtractFloatingIPs(page) + return true, nil + }) +} + +func TestRequiredFieldsForCreate(t *testing.T) { + res1 := floatingips.Create(fake.ServiceClient(), floatingips.CreateOpts{FloatingNetworkID: ""}) + if res1.Err == nil { + t.Fatalf("Expected error, got none") + } + + res2 := floatingips.Create(fake.ServiceClient(), floatingips.CreateOpts{FloatingNetworkID: "foo", PortID: ""}) + if res2.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/floatingips", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "floatingip": { + "floating_network_id": "376da547-b977-4cfe-9cba-275c80debf57", + "port_id": "ce705c24-c1ef-408a-bda3-7bbd946164ab" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "floatingip": { + "router_id": "d23abc8d-2991-4a55-ba98-2aaea84cc72f", + "tenant_id": "4969c491a3c74ee4af974e6d800c62de", + "floating_network_id": "376da547-b977-4cfe-9cba-275c80debf57", + "fixed_ip_address": "10.0.0.3", + "floating_ip_address": "", + "port_id": "ce705c24-c1ef-408a-bda3-7bbd946164ab", + "id": "2f245a7b-796b-4f26-9cf9-9e82d248fda7" + } +} + `) + }) + + options := floatingips.CreateOpts{ + FloatingNetworkID: "376da547-b977-4cfe-9cba-275c80debf57", + PortID: "ce705c24-c1ef-408a-bda3-7bbd946164ab", + } + + ip, err := floatingips.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "2f245a7b-796b-4f26-9cf9-9e82d248fda7", ip.ID) + th.AssertEquals(t, "4969c491a3c74ee4af974e6d800c62de", ip.TenantID) + th.AssertEquals(t, "376da547-b977-4cfe-9cba-275c80debf57", ip.FloatingNetworkID) + th.AssertEquals(t, "", ip.FloatingIP) + th.AssertEquals(t, "ce705c24-c1ef-408a-bda3-7bbd946164ab", ip.PortID) + th.AssertEquals(t, "10.0.0.3", ip.FixedIP) +} + +func TestCreateEmptyPort(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/floatingips", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` + { + "floatingip": { + "floating_network_id": "376da547-b977-4cfe-9cba-275c80debf57" + } + } + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` + { + "floatingip": { + "router_id": "d23abc8d-2991-4a55-ba98-2aaea84cc72f", + "tenant_id": "4969c491a3c74ee4af974e6d800c62de", + "floating_network_id": "376da547-b977-4cfe-9cba-275c80debf57", + "fixed_ip_address": "10.0.0.3", + "floating_ip_address": "", + "id": "2f245a7b-796b-4f26-9cf9-9e82d248fda7" + } + } + `) + }) + + options := floatingips.CreateOpts{ + FloatingNetworkID: "376da547-b977-4cfe-9cba-275c80debf57", + } + + ip, err := floatingips.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "2f245a7b-796b-4f26-9cf9-9e82d248fda7", ip.ID) + th.AssertEquals(t, "4969c491a3c74ee4af974e6d800c62de", ip.TenantID) + th.AssertEquals(t, "376da547-b977-4cfe-9cba-275c80debf57", ip.FloatingNetworkID) + th.AssertEquals(t, "", ip.FloatingIP) + th.AssertEquals(t, "", ip.PortID) + th.AssertEquals(t, "10.0.0.3", ip.FixedIP) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/floatingips/2f245a7b-796b-4f26-9cf9-9e82d248fda7", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "floatingip": { + "floating_network_id": "90f742b1-6d17-487b-ba95-71881dbc0b64", + "fixed_ip_address": "192.0.0.2", + "floating_ip_address": "10.0.0.3", + "tenant_id": "017d8de156df4177889f31a9bd6edc00", + "status": "DOWN", + "port_id": "74a342ce-8e07-4e91-880c-9f834b68fa25", + "id": "2f245a7b-796b-4f26-9cf9-9e82d248fda7", + "router_id": "1117c30a-ddb4-49a1-bec3-a65b286b4170" + } +} + `) + }) + + ip, err := floatingips.Get(fake.ServiceClient(), "2f245a7b-796b-4f26-9cf9-9e82d248fda7").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "90f742b1-6d17-487b-ba95-71881dbc0b64", ip.FloatingNetworkID) + th.AssertEquals(t, "10.0.0.3", ip.FloatingIP) + th.AssertEquals(t, "74a342ce-8e07-4e91-880c-9f834b68fa25", ip.PortID) + th.AssertEquals(t, "192.0.0.2", ip.FixedIP) + th.AssertEquals(t, "017d8de156df4177889f31a9bd6edc00", ip.TenantID) + th.AssertEquals(t, "DOWN", ip.Status) + th.AssertEquals(t, "2f245a7b-796b-4f26-9cf9-9e82d248fda7", ip.ID) + th.AssertEquals(t, "1117c30a-ddb4-49a1-bec3-a65b286b4170", ip.RouterID) +} + +func TestAssociate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/floatingips/2f245a7b-796b-4f26-9cf9-9e82d248fda7", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "floatingip": { + "port_id": "423abc8d-2991-4a55-ba98-2aaea84cc72e" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "floatingip": { + "router_id": "d23abc8d-2991-4a55-ba98-2aaea84cc72f", + "tenant_id": "4969c491a3c74ee4af974e6d800c62de", + "floating_network_id": "376da547-b977-4cfe-9cba-275c80debf57", + "fixed_ip_address": null, + "floating_ip_address": "172.24.4.228", + "port_id": "423abc8d-2991-4a55-ba98-2aaea84cc72e", + "id": "2f245a7b-796b-4f26-9cf9-9e82d248fda7" + } +} + `) + }) + + portID := "423abc8d-2991-4a55-ba98-2aaea84cc72e" + ip, err := floatingips.Update(fake.ServiceClient(), "2f245a7b-796b-4f26-9cf9-9e82d248fda7", floatingips.UpdateOpts{PortID: &portID}).Extract() + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, portID, ip.PortID) +} + +func TestDisassociate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/floatingips/2f245a7b-796b-4f26-9cf9-9e82d248fda7", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "floatingip": { + "port_id": null + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "floatingip": { + "router_id": "d23abc8d-2991-4a55-ba98-2aaea84cc72f", + "tenant_id": "4969c491a3c74ee4af974e6d800c62de", + "floating_network_id": "376da547-b977-4cfe-9cba-275c80debf57", + "fixed_ip_address": null, + "floating_ip_address": "172.24.4.228", + "port_id": null, + "id": "2f245a7b-796b-4f26-9cf9-9e82d248fda7" + } +} + `) + }) + + ip, err := floatingips.Update(fake.ServiceClient(), "2f245a7b-796b-4f26-9cf9-9e82d248fda7", floatingips.UpdateOpts{PortID: nil}).Extract() + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, "", ip.FixedIP) + th.AssertDeepEquals(t, "", ip.PortID) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/floatingips/2f245a7b-796b-4f26-9cf9-9e82d248fda7", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := floatingips.Delete(fake.ServiceClient(), "2f245a7b-796b-4f26-9cf9-9e82d248fda7") + th.AssertNoErr(t, res.Err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/urls.go new file mode 100644 index 0000000000..1318a184ca --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/urls.go @@ -0,0 +1,13 @@ +package floatingips + +import "github.com/gophercloud/gophercloud" + +const resourcePath = "floatingips" + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers/requests.go new file mode 100644 index 0000000000..71b2f627d5 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers/requests.go @@ -0,0 +1,223 @@ +package routers + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the floating IP attributes you want to see returned. SortKey allows you to +// sort by a particular network attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + ID string `q:"id"` + Name string `q:"name"` + AdminStateUp *bool `q:"admin_state_up"` + Distributed *bool `q:"distributed"` + Status string `q:"status"` + TenantID string `q:"tenant_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// List returns a Pager which allows you to iterate over a collection of +// routers. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those routers that are owned by the +// tenant who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + q, err := gophercloud.BuildQueryString(&opts) + if err != nil { + return pagination.Pager{Err: err} + } + u := rootURL(c) + q.String() + return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page { + return RouterPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Create operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type CreateOptsBuilder interface { + ToRouterCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains all the values needed to create a new router. There are +// no required values. +type CreateOpts struct { + Name string `json:"name,omitempty"` + AdminStateUp *bool `json:"admin_state_up,omitempty"` + Distributed *bool `json:"distributed,omitempty"` + TenantID string `json:"tenant_id,omitempty"` + GatewayInfo *GatewayInfo `json:"external_gateway_info,omitempty"` +} + +func (opts CreateOpts) ToRouterCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "router") +} + +// Create accepts a CreateOpts struct and uses the values to create a new +// logical router. When it is created, the router does not have an internal +// interface - it is not associated to any subnet. +// +// You can optionally specify an external gateway for a router using the +// GatewayInfo struct. The external gateway for the router must be plugged into +// an external network (it is external if its `router:external' field is set to +// true). +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToRouterCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(rootURL(c), b, &r.Body, nil) + return +} + +// Get retrieves a particular router based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(resourceURL(c, id), &r.Body, nil) + return +} + +type UpdateOptsBuilder interface { + ToRouterUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contains the values used when updating a router. +type UpdateOpts struct { + Name string `json:"name,omitempty"` + AdminStateUp *bool `json:"admin_state_up,omitempty"` + Distributed *bool `json:"distributed,omitempty"` + GatewayInfo *GatewayInfo `json:"external_gateway_info,omitempty"` + Routes []Route `json:"routes"` +} + +func (opts UpdateOpts) ToRouterUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "router") +} + +// Update allows routers to be updated. You can update the name, administrative +// state, and the external gateway. For more information about how to set the +// external gateway for a router, see Create. This operation does not enable +// the update of router interfaces. To do this, use the AddInterface and +// RemoveInterface functions. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToRouterUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete will permanently delete a particular router based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(resourceURL(c, id), nil) + return +} + +// AddInterfaceOptsBuilder is what types must satisfy to be used as AddInterface +// options. +type AddInterfaceOptsBuilder interface { + ToRouterAddInterfaceMap() (map[string]interface{}, error) +} + +// AddInterfaceOpts allow you to work with operations that either add +// an internal interface from a router. +type AddInterfaceOpts struct { + SubnetID string `json:"subnet_id,omitempty" xor:"PortID"` + PortID string `json:"port_id,omitempty" xor:"SubnetID"` +} + +// ToRouterAddInterfaceMap allows InterfaceOpts to satisfy the InterfaceOptsBuilder +// interface +func (opts AddInterfaceOpts) ToRouterAddInterfaceMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "") +} + +// AddInterface attaches a subnet to an internal router interface. You must +// specify either a SubnetID or PortID in the request body. If you specify both, +// the operation will fail and an error will be returned. +// +// If you specify a SubnetID, the gateway IP address for that particular subnet +// is used to create the router interface. Alternatively, if you specify a +// PortID, the IP address associated with the port is used to create the router +// interface. +// +// If you reference a port that is associated with multiple IP addresses, or +// if the port is associated with zero IP addresses, the operation will fail and +// a 400 Bad Request error will be returned. +// +// If you reference a port already in use, the operation will fail and a 409 +// Conflict error will be returned. +// +// The PortID that is returned after using Extract() on the result of this +// operation can either be the same PortID passed in or, on the other hand, the +// identifier of a new port created by this operation. After the operation +// completes, the device ID of the port is set to the router ID, and the +// device owner attribute is set to `network:router_interface'. +func AddInterface(c *gophercloud.ServiceClient, id string, opts AddInterfaceOptsBuilder) (r InterfaceResult) { + b, err := opts.ToRouterAddInterfaceMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(addInterfaceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// RemoveInterfaceOptsBuilder is what types must satisfy to be used as RemoveInterface +// options. +type RemoveInterfaceOptsBuilder interface { + ToRouterRemoveInterfaceMap() (map[string]interface{}, error) +} + +// RemoveInterfaceOpts allow you to work with operations that either add or remote +// an internal interface from a router. +type RemoveInterfaceOpts struct { + SubnetID string `json:"subnet_id,omitempty" or:"PortID"` + PortID string `json:"port_id,omitempty" or:"SubnetID"` +} + +// ToRouterRemoveInterfaceMap allows RemoveInterfaceOpts to satisfy the RemoveInterfaceOptsBuilder +// interface +func (opts RemoveInterfaceOpts) ToRouterRemoveInterfaceMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "") +} + +// RemoveInterface removes an internal router interface, which detaches a +// subnet from the router. You must specify either a SubnetID or PortID, since +// these values are used to identify the router interface to remove. +// +// Unlike AddInterface, you can also specify both a SubnetID and PortID. If you +// choose to specify both, the subnet ID must correspond to the subnet ID of +// the first IP address on the port specified by the port ID. Otherwise, the +// operation will fail and return a 409 Conflict error. +// +// If the router, subnet or port which are referenced do not exist or are not +// visible to you, the operation will fail and a 404 Not Found error will be +// returned. After this operation completes, the port connecting the router +// with the subnet is removed from the subnet for the network. +func RemoveInterface(c *gophercloud.ServiceClient, id string, opts RemoveInterfaceOptsBuilder) (r InterfaceResult) { + b, err := opts.ToRouterRemoveInterfaceMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(removeInterfaceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers/results.go new file mode 100644 index 0000000000..d849d457ad --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers/results.go @@ -0,0 +1,152 @@ +package routers + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// GatewayInfo represents the information of an external gateway for any +// particular network router. +type GatewayInfo struct { + NetworkID string `json:"network_id"` +} + +// Route is a possible route in a router. +type Route struct { + NextHop string `json:"nexthop"` + DestinationCIDR string `json:"destination"` +} + +// Router represents a Neutron router. A router is a logical entity that +// forwards packets across internal subnets and NATs (network address +// translation) them on external networks through an appropriate gateway. +// +// A router has an interface for each subnet with which it is associated. By +// default, the IP address of such interface is the subnet's gateway IP. Also, +// whenever a router is associated with a subnet, a port for that router +// interface is added to the subnet's network. +type Router struct { + // Indicates whether or not a router is currently operational. + Status string `json:"status"` + + // Information on external gateway for the router. + GatewayInfo GatewayInfo `json:"external_gateway_info"` + + // Administrative state of the router. + AdminStateUp bool `json:"admin_state_up"` + + // Whether router is disitrubted or not.. + Distributed bool `json:"distributed"` + + // Human readable name for the router. Does not have to be unique. + Name string `json:"name"` + + // Unique identifier for the router. + ID string `json:"id"` + + // Owner of the router. Only admin users can specify a tenant identifier + // other than its own. + TenantID string `json:"tenant_id"` + + Routes []Route `json:"routes"` +} + +// RouterPage is the page returned by a pager when traversing over a +// collection of routers. +type RouterPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of routers has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r RouterPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"routers_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a RouterPage struct is empty. +func (r RouterPage) IsEmpty() (bool, error) { + is, err := ExtractRouters(r) + return len(is) == 0, err +} + +// ExtractRouters accepts a Page struct, specifically a RouterPage struct, +// and extracts the elements into a slice of Router structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractRouters(r pagination.Page) ([]Router, error) { + var s struct { + Routers []Router `json:"routers"` + } + err := (r.(RouterPage)).ExtractInto(&s) + return s.Routers, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a router. +func (r commonResult) Extract() (*Router, error) { + var s struct { + Router *Router `json:"router"` + } + err := r.ExtractInto(&s) + return s.Router, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// InterfaceInfo represents information about a particular router interface. As +// mentioned above, in order for a router to forward to a subnet, it needs an +// interface. +type InterfaceInfo struct { + // The ID of the subnet which this interface is associated with. + SubnetID string `json:"subnet_id"` + + // The ID of the port that is a part of the subnet. + PortID string `json:"port_id"` + + // The UUID of the interface. + ID string `json:"id"` + + // Owner of the interface. + TenantID string `json:"tenant_id"` +} + +// InterfaceResult represents the result of interface operations, such as +// AddInterface() and RemoveInterface(). +type InterfaceResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts an information struct. +func (r InterfaceResult) Extract() (*InterfaceInfo, error) { + var s InterfaceInfo + err := r.ExtractInto(&s) + return &s, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers/testing/doc.go new file mode 100644 index 0000000000..ef44c2e5c3 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers/testing/doc.go @@ -0,0 +1,2 @@ +// networking_extensions_layer3_routers_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers/testing/requests_test.go new file mode 100644 index 0000000000..bf7f35e54c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers/testing/requests_test.go @@ -0,0 +1,407 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/routers", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "routers": [ + { + "status": "ACTIVE", + "external_gateway_info": null, + "name": "second_routers", + "admin_state_up": true, + "tenant_id": "6b96ff0cb17a4b859e1e575d221683d3", + "distributed": false, + "id": "7177abc4-5ae9-4bb7-b0d4-89e94a4abf3b" + }, + { + "status": "ACTIVE", + "external_gateway_info": { + "network_id": "3c5bcddd-6af9-4e6b-9c3e-c153e521cab8" + }, + "name": "router1", + "admin_state_up": true, + "tenant_id": "33a40233088643acb66ff6eb0ebea679", + "distributed": false, + "id": "a9254bdb-2613-4a13-ac4c-adc581fba50d" + } + ] +} + `) + }) + + count := 0 + + routers.List(fake.ServiceClient(), routers.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := routers.ExtractRouters(page) + if err != nil { + t.Errorf("Failed to extract routers: %v", err) + return false, err + } + + expected := []routers.Router{ + { + Status: "ACTIVE", + GatewayInfo: routers.GatewayInfo{NetworkID: ""}, + AdminStateUp: true, + Distributed: false, + Name: "second_routers", + ID: "7177abc4-5ae9-4bb7-b0d4-89e94a4abf3b", + TenantID: "6b96ff0cb17a4b859e1e575d221683d3", + }, + { + Status: "ACTIVE", + GatewayInfo: routers.GatewayInfo{NetworkID: "3c5bcddd-6af9-4e6b-9c3e-c153e521cab8"}, + AdminStateUp: true, + Distributed: false, + Name: "router1", + ID: "a9254bdb-2613-4a13-ac4c-adc581fba50d", + TenantID: "33a40233088643acb66ff6eb0ebea679", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/routers", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "router":{ + "name": "foo_router", + "admin_state_up": false, + "external_gateway_info":{ + "network_id":"8ca37218-28ff-41cb-9b10-039601ea7e6b" + } + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "router": { + "status": "ACTIVE", + "external_gateway_info": { + "network_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b" + }, + "name": "foo_router", + "admin_state_up": false, + "tenant_id": "6b96ff0cb17a4b859e1e575d221683d3", + "distributed": false, + "id": "8604a0de-7f6b-409a-a47c-a1cc7bc77b2e" + } +} + `) + }) + + asu := false + gwi := routers.GatewayInfo{NetworkID: "8ca37218-28ff-41cb-9b10-039601ea7e6b"} + + options := routers.CreateOpts{ + Name: "foo_router", + AdminStateUp: &asu, + GatewayInfo: &gwi, + } + r, err := routers.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "foo_router", r.Name) + th.AssertEquals(t, false, r.AdminStateUp) + th.AssertDeepEquals(t, routers.GatewayInfo{NetworkID: "8ca37218-28ff-41cb-9b10-039601ea7e6b"}, r.GatewayInfo) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/routers/a07eea83-7710-4860-931b-5fe220fae533", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "router": { + "status": "ACTIVE", + "external_gateway_info": { + "network_id": "85d76829-6415-48ff-9c63-5c5ca8c61ac6" + }, + "routes": [ + { + "nexthop": "10.1.0.10", + "destination": "40.0.1.0/24" + } + ], + "name": "router1", + "admin_state_up": true, + "tenant_id": "d6554fe62e2f41efbb6e026fad5c1542", + "distributed": false, + "id": "a07eea83-7710-4860-931b-5fe220fae533" + } +} + `) + }) + + n, err := routers.Get(fake.ServiceClient(), "a07eea83-7710-4860-931b-5fe220fae533").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Status, "ACTIVE") + th.AssertDeepEquals(t, n.GatewayInfo, routers.GatewayInfo{NetworkID: "85d76829-6415-48ff-9c63-5c5ca8c61ac6"}) + th.AssertEquals(t, n.Name, "router1") + th.AssertEquals(t, n.AdminStateUp, true) + th.AssertEquals(t, n.TenantID, "d6554fe62e2f41efbb6e026fad5c1542") + th.AssertEquals(t, n.ID, "a07eea83-7710-4860-931b-5fe220fae533") + th.AssertDeepEquals(t, n.Routes, []routers.Route{{DestinationCIDR: "40.0.1.0/24", NextHop: "10.1.0.10"}}) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/routers/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "router": { + "name": "new_name", + "external_gateway_info": { + "network_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b" + }, + "routes": [ + { + "nexthop": "10.1.0.10", + "destination": "40.0.1.0/24" + } + ] + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "router": { + "status": "ACTIVE", + "external_gateway_info": { + "network_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b" + }, + "name": "new_name", + "admin_state_up": true, + "tenant_id": "6b96ff0cb17a4b859e1e575d221683d3", + "distributed": false, + "id": "8604a0de-7f6b-409a-a47c-a1cc7bc77b2e", + "routes": [ + { + "nexthop": "10.1.0.10", + "destination": "40.0.1.0/24" + } + ] + } +} + `) + }) + + gwi := routers.GatewayInfo{NetworkID: "8ca37218-28ff-41cb-9b10-039601ea7e6b"} + r := []routers.Route{{DestinationCIDR: "40.0.1.0/24", NextHop: "10.1.0.10"}} + options := routers.UpdateOpts{Name: "new_name", GatewayInfo: &gwi, Routes: r} + + n, err := routers.Update(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Name, "new_name") + th.AssertDeepEquals(t, n.GatewayInfo, routers.GatewayInfo{NetworkID: "8ca37218-28ff-41cb-9b10-039601ea7e6b"}) + th.AssertDeepEquals(t, n.Routes, []routers.Route{{DestinationCIDR: "40.0.1.0/24", NextHop: "10.1.0.10"}}) +} + +func TestAllRoutesRemoved(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/routers/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "router": { + "routes": [] + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "router": { + "status": "ACTIVE", + "external_gateway_info": { + "network_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b" + }, + "name": "name", + "admin_state_up": true, + "tenant_id": "6b96ff0cb17a4b859e1e575d221683d3", + "distributed": false, + "id": "8604a0de-7f6b-409a-a47c-a1cc7bc77b2e", + "routes": [] + } +} + `) + }) + + r := []routers.Route{} + options := routers.UpdateOpts{Routes: r} + + n, err := routers.Update(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", options).Extract() + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, n.Routes, []routers.Route{}) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/routers/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := routers.Delete(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c") + th.AssertNoErr(t, res.Err) +} + +func TestAddInterface(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/routers/4e8e5957-649f-477b-9e5b-f1f75b21c03c/add_router_interface", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "subnet_id": "a2f1f29d-571b-4533-907f-5803ab96ead1" +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "subnet_id": "0d32a837-8069-4ec3-84c4-3eef3e10b188", + "tenant_id": "017d8de156df4177889f31a9bd6edc00", + "port_id": "3f990102-4485-4df1-97a0-2c35bdb85b31", + "id": "9a83fa11-8da5-436e-9afe-3d3ac5ce7770" +} +`) + }) + + opts := routers.AddInterfaceOpts{SubnetID: "a2f1f29d-571b-4533-907f-5803ab96ead1"} + res, err := routers.AddInterface(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", opts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "0d32a837-8069-4ec3-84c4-3eef3e10b188", res.SubnetID) + th.AssertEquals(t, "017d8de156df4177889f31a9bd6edc00", res.TenantID) + th.AssertEquals(t, "3f990102-4485-4df1-97a0-2c35bdb85b31", res.PortID) + th.AssertEquals(t, "9a83fa11-8da5-436e-9afe-3d3ac5ce7770", res.ID) +} + +func TestAddInterfaceRequiredOpts(t *testing.T) { + _, err := routers.AddInterface(fake.ServiceClient(), "foo", routers.AddInterfaceOpts{}).Extract() + if err == nil { + t.Fatalf("Expected error, got none") + } + _, err = routers.AddInterface(fake.ServiceClient(), "foo", routers.AddInterfaceOpts{SubnetID: "bar", PortID: "baz"}).Extract() + if err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestRemoveInterface(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/routers/4e8e5957-649f-477b-9e5b-f1f75b21c03c/remove_router_interface", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "subnet_id": "a2f1f29d-571b-4533-907f-5803ab96ead1" +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "subnet_id": "0d32a837-8069-4ec3-84c4-3eef3e10b188", + "tenant_id": "017d8de156df4177889f31a9bd6edc00", + "port_id": "3f990102-4485-4df1-97a0-2c35bdb85b31", + "id": "9a83fa11-8da5-436e-9afe-3d3ac5ce7770" +} +`) + }) + + opts := routers.RemoveInterfaceOpts{SubnetID: "a2f1f29d-571b-4533-907f-5803ab96ead1"} + res, err := routers.RemoveInterface(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", opts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "0d32a837-8069-4ec3-84c4-3eef3e10b188", res.SubnetID) + th.AssertEquals(t, "017d8de156df4177889f31a9bd6edc00", res.TenantID) + th.AssertEquals(t, "3f990102-4485-4df1-97a0-2c35bdb85b31", res.PortID) + th.AssertEquals(t, "9a83fa11-8da5-436e-9afe-3d3ac5ce7770", res.ID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers/urls.go new file mode 100644 index 0000000000..f9e9da3211 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers/urls.go @@ -0,0 +1,21 @@ +package routers + +import "github.com/gophercloud/gophercloud" + +const resourcePath = "routers" + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id) +} + +func addInterfaceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id, "add_router_interface") +} + +func removeInterfaceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id, "remove_router_interface") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/doc.go new file mode 100644 index 0000000000..bc1fc282f4 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/doc.go @@ -0,0 +1,3 @@ +// Package lbaas provides information and interaction with the Load Balancer +// as a Service extension for the OpenStack Networking service. +package lbaas diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/members/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/members/requests.go new file mode 100644 index 0000000000..7e7b76885f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/members/requests.go @@ -0,0 +1,115 @@ +package members + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the floating IP attributes you want to see returned. SortKey allows you to +// sort by a particular network attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Status string `q:"status"` + Weight int `q:"weight"` + AdminStateUp *bool `q:"admin_state_up"` + TenantID string `q:"tenant_id"` + PoolID string `q:"pool_id"` + Address string `q:"address"` + ProtocolPort int `q:"protocol_port"` + ID string `q:"id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// List returns a Pager which allows you to iterate over a collection of +// pools. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those pools that are owned by the +// tenant who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + q, err := gophercloud.BuildQueryString(&opts) + if err != nil { + return pagination.Pager{Err: err} + } + u := rootURL(c) + q.String() + return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page { + return MemberPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +type CreateOptsBuilder interface { + ToLBMemberCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains all the values needed to create a new pool member. +type CreateOpts struct { + // The IP address of the member. + Address string `json:"address" required:"true"` + // The port on which the application is hosted. + ProtocolPort int `json:"protocol_port" required:"true"` + // The pool to which this member will belong. + PoolID string `json:"pool_id" required:"true"` + // Only required if the caller has an admin role and wants to create a pool + // for another tenant. + TenantID string `json:"tenant_id,omitempty"` +} + +func (opts CreateOpts) ToLBMemberCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "member") +} + +// Create accepts a CreateOpts struct and uses the values to create a new +// load balancer pool member. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToLBMemberCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(rootURL(c), b, &r.Body, nil) + return +} + +// Get retrieves a particular pool member based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(resourceURL(c, id), &r.Body, nil) + return +} + +type UpdateOptsBuilder interface { + ToLBMemberUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contains the values used when updating a pool member. +type UpdateOpts struct { + // The administrative state of the member, which is up (true) or down (false). + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +func (opts UpdateOpts) ToLBMemberUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "member") +} + +// Update allows members to be updated. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToLBMemberUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201, 202}, + }) + return +} + +// Delete will permanently delete a particular member based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(resourceURL(c, id), nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/members/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/members/results.go new file mode 100644 index 0000000000..933e1aedbf --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/members/results.go @@ -0,0 +1,104 @@ +package members + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Member represents the application running on a backend server. +type Member struct { + // The status of the member. Indicates whether the member is operational. + Status string + + // Weight of member. + Weight int + + // The administrative state of the member, which is up (true) or down (false). + AdminStateUp bool `json:"admin_state_up"` + + // Owner of the member. Only an administrative user can specify a tenant ID + // other than its own. + TenantID string `json:"tenant_id"` + + // The pool to which the member belongs. + PoolID string `json:"pool_id"` + + // The IP address of the member. + Address string + + // The port on which the application is hosted. + ProtocolPort int `json:"protocol_port"` + + // The unique ID for the member. + ID string +} + +// MemberPage is the page returned by a pager when traversing over a +// collection of pool members. +type MemberPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of members has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r MemberPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"members_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a MemberPage struct is empty. +func (r MemberPage) IsEmpty() (bool, error) { + is, err := ExtractMembers(r) + return len(is) == 0, err +} + +// ExtractMembers accepts a Page struct, specifically a MemberPage struct, +// and extracts the elements into a slice of Member structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractMembers(r pagination.Page) ([]Member, error) { + var s struct { + Members []Member `json:"members"` + } + err := (r.(MemberPage)).ExtractInto(&s) + return s.Members, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a router. +func (r commonResult) Extract() (*Member, error) { + var s struct { + Member *Member `json:"member"` + } + err := r.ExtractInto(&s) + return s.Member, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/members/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/members/testing/doc.go new file mode 100644 index 0000000000..3878904e82 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/members/testing/doc.go @@ -0,0 +1,2 @@ +// networking_extensions_lbaas_members_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/members/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/members/testing/requests_test.go new file mode 100644 index 0000000000..3e4f1d43f6 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/members/testing/requests_test.go @@ -0,0 +1,238 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud" + fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/members" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/members", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "members":[ + { + "status":"ACTIVE", + "weight":1, + "admin_state_up":true, + "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea", + "pool_id":"72741b06-df4d-4715-b142-276b6bce75ab", + "address":"10.0.0.4", + "protocol_port":80, + "id":"701b531b-111a-4f21-ad85-4795b7b12af6" + }, + { + "status":"ACTIVE", + "weight":1, + "admin_state_up":true, + "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea", + "pool_id":"72741b06-df4d-4715-b142-276b6bce75ab", + "address":"10.0.0.3", + "protocol_port":80, + "id":"beb53b4d-230b-4abd-8118-575b8fa006ef" + } + ] +} + `) + }) + + count := 0 + + members.List(fake.ServiceClient(), members.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := members.ExtractMembers(page) + if err != nil { + t.Errorf("Failed to extract members: %v", err) + return false, err + } + + expected := []members.Member{ + { + Status: "ACTIVE", + Weight: 1, + AdminStateUp: true, + TenantID: "83657cfcdfe44cd5920adaf26c48ceea", + PoolID: "72741b06-df4d-4715-b142-276b6bce75ab", + Address: "10.0.0.4", + ProtocolPort: 80, + ID: "701b531b-111a-4f21-ad85-4795b7b12af6", + }, + { + Status: "ACTIVE", + Weight: 1, + AdminStateUp: true, + TenantID: "83657cfcdfe44cd5920adaf26c48ceea", + PoolID: "72741b06-df4d-4715-b142-276b6bce75ab", + Address: "10.0.0.3", + ProtocolPort: 80, + ID: "beb53b4d-230b-4abd-8118-575b8fa006ef", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/members", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "member": { + "tenant_id": "453105b9-1754-413f-aab1-55f1af620750", + "pool_id": "foo", + "address": "192.0.2.14", + "protocol_port":8080 + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "member": { + "id": "975592ca-e308-48ad-8298-731935ee9f45", + "address": "192.0.2.14", + "protocol_port": 8080, + "tenant_id": "453105b9-1754-413f-aab1-55f1af620750", + "admin_state_up":true, + "weight": 1, + "status": "DOWN" + } +} + `) + }) + + options := members.CreateOpts{ + TenantID: "453105b9-1754-413f-aab1-55f1af620750", + Address: "192.0.2.14", + ProtocolPort: 8080, + PoolID: "foo", + } + _, err := members.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/members/975592ca-e308-48ad-8298-731935ee9f45", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "member":{ + "id":"975592ca-e308-48ad-8298-731935ee9f45", + "address":"192.0.2.14", + "protocol_port":8080, + "tenant_id":"453105b9-1754-413f-aab1-55f1af620750", + "admin_state_up":true, + "weight":1, + "status":"DOWN" + } +} + `) + }) + + m, err := members.Get(fake.ServiceClient(), "975592ca-e308-48ad-8298-731935ee9f45").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "975592ca-e308-48ad-8298-731935ee9f45", m.ID) + th.AssertEquals(t, "192.0.2.14", m.Address) + th.AssertEquals(t, 8080, m.ProtocolPort) + th.AssertEquals(t, "453105b9-1754-413f-aab1-55f1af620750", m.TenantID) + th.AssertEquals(t, true, m.AdminStateUp) + th.AssertEquals(t, 1, m.Weight) + th.AssertEquals(t, "DOWN", m.Status) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/members/332abe93-f488-41ba-870b-2ac66be7f853", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "member":{ + "admin_state_up":false + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "member":{ + "status":"PENDING_UPDATE", + "protocol_port":8080, + "weight":1, + "admin_state_up":false, + "tenant_id":"4fd44f30292945e481c7b8a0c8908869", + "pool_id":"7803631d-f181-4500-b3a2-1b68ba2a75fd", + "address":"10.0.0.5", + "status_description":null, + "id":"48a471ea-64f1-4eb6-9be7-dae6bbe40a0f" + } +} + `) + }) + + options := members.UpdateOpts{AdminStateUp: gophercloud.Disabled} + + _, err := members.Update(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", options).Extract() + th.AssertNoErr(t, err) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/members/332abe93-f488-41ba-870b-2ac66be7f853", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := members.Delete(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853") + th.AssertNoErr(t, res.Err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/members/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/members/urls.go new file mode 100644 index 0000000000..e2248f81f4 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/members/urls.go @@ -0,0 +1,16 @@ +package members + +import "github.com/gophercloud/gophercloud" + +const ( + rootPath = "lb" + resourcePath = "members" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/requests.go new file mode 100644 index 0000000000..f1b964b65e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/requests.go @@ -0,0 +1,210 @@ +package monitors + +import ( + "fmt" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the floating IP attributes you want to see returned. SortKey allows you to +// sort by a particular network attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + ID string `q:"id"` + TenantID string `q:"tenant_id"` + Type string `q:"type"` + Delay int `q:"delay"` + Timeout int `q:"timeout"` + MaxRetries int `q:"max_retries"` + HTTPMethod string `q:"http_method"` + URLPath string `q:"url_path"` + ExpectedCodes string `q:"expected_codes"` + AdminStateUp *bool `q:"admin_state_up"` + Status string `q:"status"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// List returns a Pager which allows you to iterate over a collection of +// routers. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those routers that are owned by the +// tenant who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + q, err := gophercloud.BuildQueryString(&opts) + if err != nil { + return pagination.Pager{Err: err} + } + u := rootURL(c) + q.String() + return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page { + return MonitorPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// MonitorType is the type for all the types of LB monitors +type MonitorType string + +// Constants that represent approved monitoring types. +const ( + TypePING MonitorType = "PING" + TypeTCP MonitorType = "TCP" + TypeHTTP MonitorType = "HTTP" + TypeHTTPS MonitorType = "HTTPS" +) + +// CreateOptsBuilder is what types must satisfy to be used as Create +// options. +type CreateOptsBuilder interface { + ToLBMonitorCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains all the values needed to create a new health monitor. +type CreateOpts struct { + // Required. The type of probe, which is PING, TCP, HTTP, or HTTPS, that is + // sent by the load balancer to verify the member state. + Type MonitorType `json:"type" required:"true"` + // Required. The time, in seconds, between sending probes to members. + Delay int `json:"delay" required:"true"` + // Required. Maximum number of seconds for a monitor to wait for a ping reply + // before it times out. The value must be less than the delay value. + Timeout int `json:"timeout" required:"true"` + // Required. Number of permissible ping failures before changing the member's + // status to INACTIVE. Must be a number between 1 and 10. + MaxRetries int `json:"max_retries" required:"true"` + // Required for HTTP(S) types. URI path that will be accessed if monitor type + // is HTTP or HTTPS. + URLPath string `json:"url_path,omitempty"` + // Required for HTTP(S) types. The HTTP method used for requests by the + // monitor. If this attribute is not specified, it defaults to "GET". + HTTPMethod string `json:"http_method,omitempty"` + // Required for HTTP(S) types. Expected HTTP codes for a passing HTTP(S) + // monitor. You can either specify a single status like "200", or a range + // like "200-202". + ExpectedCodes string `json:"expected_codes,omitempty"` + // Required for admins. Indicates the owner of the VIP. + TenantID string `json:"tenant_id,omitempty"` + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +// ToLBMonitorCreateMap allows CreateOpts to satisfy the CreateOptsBuilder +// interface +func (opts CreateOpts) ToLBMonitorCreateMap() (map[string]interface{}, error) { + if opts.Type == TypeHTTP || opts.Type == TypeHTTPS { + if opts.URLPath == "" { + err := gophercloud.ErrMissingInput{} + err.Argument = "monitors.CreateOpts.URLPath" + return nil, err + } + if opts.ExpectedCodes == "" { + err := gophercloud.ErrMissingInput{} + err.Argument = "monitors.CreateOpts.ExpectedCodes" + return nil, err + } + } + if opts.Delay < opts.Timeout { + err := gophercloud.ErrInvalidInput{} + err.Argument = "monitors.CreateOpts.Delay/monitors.CreateOpts.Timeout" + err.Info = "Delay must be greater than or equal to timeout" + return nil, err + } + return gophercloud.BuildRequestBody(opts, "health_monitor") +} + +// Create is an operation which provisions a new health monitor. There are +// different types of monitor you can provision: PING, TCP or HTTP(S). Below +// are examples of how to create each one. +// +// Here is an example config struct to use when creating a PING or TCP monitor: +// +// CreateOpts{Type: TypePING, Delay: 20, Timeout: 10, MaxRetries: 3} +// CreateOpts{Type: TypeTCP, Delay: 20, Timeout: 10, MaxRetries: 3} +// +// Here is an example config struct to use when creating a HTTP(S) monitor: +// +// CreateOpts{Type: TypeHTTP, Delay: 20, Timeout: 10, MaxRetries: 3, +// HttpMethod: "HEAD", ExpectedCodes: "200"} +// +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToLBMonitorCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(rootURL(c), b, &r.Body, nil) + return +} + +// Get retrieves a particular health monitor based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(resourceURL(c, id), &r.Body, nil) + return +} + +// UpdateOptsBuilder is what types must satisfy to be used as Update +// options. +type UpdateOptsBuilder interface { + ToLBMonitorUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contains all the values needed to update an existing virtual IP. +// Attributes not listed here but appear in CreateOpts are immutable and cannot +// be updated. +type UpdateOpts struct { + // The time, in seconds, between sending probes to members. + Delay int `json:"delay,omitempty"` + // Maximum number of seconds for a monitor to wait for a ping reply + // before it times out. The value must be less than the delay value. + Timeout int `json:"timeout,omitempty"` + // Number of permissible ping failures before changing the member's + // status to INACTIVE. Must be a number between 1 and 10. + MaxRetries int `json:"max_retries,omitempty"` + // URI path that will be accessed if monitor type + // is HTTP or HTTPS. + URLPath string `json:"url_path,omitempty"` + // The HTTP method used for requests by the + // monitor. If this attribute is not specified, it defaults to "GET". + HTTPMethod string `json:"http_method,omitempty"` + // Expected HTTP codes for a passing HTTP(S) + // monitor. You can either specify a single status like "200", or a range + // like "200-202". + ExpectedCodes string `json:"expected_codes,omitempty"` + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +// ToLBMonitorUpdateMap allows UpdateOpts to satisfy the UpdateOptsBuilder +// interface +func (opts UpdateOpts) ToLBMonitorUpdateMap() (map[string]interface{}, error) { + if opts.Delay > 0 && opts.Timeout > 0 && opts.Delay < opts.Timeout { + err := gophercloud.ErrInvalidInput{} + err.Argument = "monitors.CreateOpts.Delay/monitors.CreateOpts.Timeout" + err.Value = fmt.Sprintf("%d/%d", opts.Delay, opts.Timeout) + err.Info = "Delay must be greater than or equal to timeout" + return nil, err + } + return gophercloud.BuildRequestBody(opts, "health_monitor") +} + +// Update is an operation which modifies the attributes of the specified monitor. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToLBMonitorUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 202}, + }) + return +} + +// Delete will permanently delete a particular monitor based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(resourceURL(c, id), nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/results.go new file mode 100644 index 0000000000..0385942c80 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/results.go @@ -0,0 +1,136 @@ +package monitors + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Monitor represents a load balancer health monitor. A health monitor is used +// to determine whether or not back-end members of the VIP's pool are usable +// for processing a request. A pool can have several health monitors associated +// with it. There are different types of health monitors supported: +// +// PING: used to ping the members using ICMP. +// TCP: used to connect to the members using TCP. +// HTTP: used to send an HTTP request to the member. +// HTTPS: used to send a secure HTTP request to the member. +// +// When a pool has several monitors associated with it, each member of the pool +// is monitored by all these monitors. If any monitor declares the member as +// unhealthy, then the member status is changed to INACTIVE and the member +// won't participate in its pool's load balancing. In other words, ALL monitors +// must declare the member to be healthy for it to stay ACTIVE. +type Monitor struct { + // The unique ID for the VIP. + ID string + + // Monitor name. Does not have to be unique. + Name string + + // Owner of the VIP. Only an administrative user can specify a tenant ID + // other than its own. + TenantID string `json:"tenant_id"` + + // The type of probe sent by the load balancer to verify the member state, + // which is PING, TCP, HTTP, or HTTPS. + Type string + + // The time, in seconds, between sending probes to members. + Delay int + + // The maximum number of seconds for a monitor to wait for a connection to be + // established before it times out. This value must be less than the delay value. + Timeout int + + // Number of allowed connection failures before changing the status of the + // member to INACTIVE. A valid value is from 1 to 10. + MaxRetries int `json:"max_retries"` + + // The HTTP method that the monitor uses for requests. + HTTPMethod string `json:"http_method"` + + // The HTTP path of the request sent by the monitor to test the health of a + // member. Must be a string beginning with a forward slash (/). + URLPath string `json:"url_path"` + + // Expected HTTP codes for a passing HTTP(S) monitor. + ExpectedCodes string `json:"expected_codes"` + + // The administrative state of the health monitor, which is up (true) or down (false). + AdminStateUp bool `json:"admin_state_up"` + + // The status of the health monitor. Indicates whether the health monitor is + // operational. + Status string +} + +// MonitorPage is the page returned by a pager when traversing over a +// collection of health monitors. +type MonitorPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of monitors has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r MonitorPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"health_monitors_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a PoolPage struct is empty. +func (r MonitorPage) IsEmpty() (bool, error) { + is, err := ExtractMonitors(r) + return len(is) == 0, err +} + +// ExtractMonitors accepts a Page struct, specifically a MonitorPage struct, +// and extracts the elements into a slice of Monitor structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractMonitors(r pagination.Page) ([]Monitor, error) { + var s struct { + Monitors []Monitor `json:"health_monitors"` + } + err := (r.(MonitorPage)).ExtractInto(&s) + return s.Monitors, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a monitor. +func (r commonResult) Extract() (*Monitor, error) { + var s struct { + Monitor *Monitor `json:"health_monitor"` + } + err := r.ExtractInto(&s) + return s.Monitor, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/testing/doc.go new file mode 100644 index 0000000000..5ee866bbf2 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/testing/doc.go @@ -0,0 +1,2 @@ +// networking_extensions_lbaas_monitors_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/testing/requests_test.go new file mode 100644 index 0000000000..f7360743b0 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/testing/requests_test.go @@ -0,0 +1,310 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud" + fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/monitors" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/health_monitors", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "health_monitors":[ + { + "admin_state_up":true, + "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea", + "delay":10, + "max_retries":1, + "timeout":1, + "type":"PING", + "id":"466c8345-28d8-4f84-a246-e04380b0461d" + }, + { + "admin_state_up":true, + "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea", + "delay":5, + "expected_codes":"200", + "max_retries":2, + "http_method":"GET", + "timeout":2, + "url_path":"/", + "type":"HTTP", + "id":"5d4b5228-33b0-4e60-b225-9b727c1a20e7" + } + ] +} + `) + }) + + count := 0 + + monitors.List(fake.ServiceClient(), monitors.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := monitors.ExtractMonitors(page) + if err != nil { + t.Errorf("Failed to extract monitors: %v", err) + return false, err + } + + expected := []monitors.Monitor{ + { + AdminStateUp: true, + TenantID: "83657cfcdfe44cd5920adaf26c48ceea", + Delay: 10, + MaxRetries: 1, + Timeout: 1, + Type: "PING", + ID: "466c8345-28d8-4f84-a246-e04380b0461d", + }, + { + AdminStateUp: true, + TenantID: "83657cfcdfe44cd5920adaf26c48ceea", + Delay: 5, + ExpectedCodes: "200", + MaxRetries: 2, + Timeout: 2, + URLPath: "/", + Type: "HTTP", + HTTPMethod: "GET", + ID: "5d4b5228-33b0-4e60-b225-9b727c1a20e7", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestDelayMustBeGreaterOrEqualThanTimeout(t *testing.T) { + _, err := monitors.Create(fake.ServiceClient(), monitors.CreateOpts{ + Type: "HTTP", + Delay: 1, + Timeout: 10, + MaxRetries: 5, + URLPath: "/check", + ExpectedCodes: "200-299", + }).Extract() + + if err == nil { + t.Fatalf("Expected error, got none") + } + + _, err = monitors.Update(fake.ServiceClient(), "453105b9-1754-413f-aab1-55f1af620750", monitors.UpdateOpts{ + Delay: 1, + Timeout: 10, + }).Extract() + + if err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/health_monitors", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "health_monitor":{ + "type":"HTTP", + "tenant_id":"453105b9-1754-413f-aab1-55f1af620750", + "delay":20, + "timeout":10, + "max_retries":5, + "url_path":"/check", + "expected_codes":"200-299" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "health_monitor":{ + "id":"f3eeab00-8367-4524-b662-55e64d4cacb5", + "tenant_id":"453105b9-1754-413f-aab1-55f1af620750", + "type":"HTTP", + "delay":20, + "timeout":10, + "max_retries":5, + "http_method":"GET", + "url_path":"/check", + "expected_codes":"200-299", + "admin_state_up":true, + "status":"ACTIVE" + } +} + `) + }) + + _, err := monitors.Create(fake.ServiceClient(), monitors.CreateOpts{ + Type: "HTTP", + TenantID: "453105b9-1754-413f-aab1-55f1af620750", + Delay: 20, + Timeout: 10, + MaxRetries: 5, + URLPath: "/check", + ExpectedCodes: "200-299", + }).Extract() + + th.AssertNoErr(t, err) +} + +func TestRequiredCreateOpts(t *testing.T) { + res := monitors.Create(fake.ServiceClient(), monitors.CreateOpts{}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = monitors.Create(fake.ServiceClient(), monitors.CreateOpts{Type: monitors.TypeHTTP}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/health_monitors/f3eeab00-8367-4524-b662-55e64d4cacb5", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "health_monitor":{ + "id":"f3eeab00-8367-4524-b662-55e64d4cacb5", + "tenant_id":"453105b9-1754-413f-aab1-55f1af620750", + "type":"HTTP", + "delay":20, + "timeout":10, + "max_retries":5, + "http_method":"GET", + "url_path":"/check", + "expected_codes":"200-299", + "admin_state_up":true, + "status":"ACTIVE" + } +} + `) + }) + + hm, err := monitors.Get(fake.ServiceClient(), "f3eeab00-8367-4524-b662-55e64d4cacb5").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "f3eeab00-8367-4524-b662-55e64d4cacb5", hm.ID) + th.AssertEquals(t, "453105b9-1754-413f-aab1-55f1af620750", hm.TenantID) + th.AssertEquals(t, "HTTP", hm.Type) + th.AssertEquals(t, 20, hm.Delay) + th.AssertEquals(t, 10, hm.Timeout) + th.AssertEquals(t, 5, hm.MaxRetries) + th.AssertEquals(t, "GET", hm.HTTPMethod) + th.AssertEquals(t, "/check", hm.URLPath) + th.AssertEquals(t, "200-299", hm.ExpectedCodes) + th.AssertEquals(t, true, hm.AdminStateUp) + th.AssertEquals(t, "ACTIVE", hm.Status) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/health_monitors/b05e44b5-81f9-4551-b474-711a722698f7", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "health_monitor":{ + "delay": 30, + "timeout": 20, + "max_retries": 10, + "url_path": "/another_check", + "expected_codes": "301", + "admin_state_up": true + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, ` +{ + "health_monitor": { + "admin_state_up": true, + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "delay": 30, + "max_retries": 10, + "http_method": "GET", + "timeout": 20, + "pools": [ + { + "status": "PENDING_CREATE", + "status_description": null, + "pool_id": "6e55751f-6ad4-4e53-b8d4-02e442cd21df" + } + ], + "type": "PING", + "id": "b05e44b5-81f9-4551-b474-711a722698f7" + } +} + `) + }) + + _, err := monitors.Update(fake.ServiceClient(), "b05e44b5-81f9-4551-b474-711a722698f7", monitors.UpdateOpts{ + Delay: 30, + Timeout: 20, + MaxRetries: 10, + URLPath: "/another_check", + ExpectedCodes: "301", + AdminStateUp: gophercloud.Enabled, + }).Extract() + + th.AssertNoErr(t, err) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/health_monitors/b05e44b5-81f9-4551-b474-711a722698f7", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := monitors.Delete(fake.ServiceClient(), "b05e44b5-81f9-4551-b474-711a722698f7") + th.AssertNoErr(t, res.Err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/urls.go new file mode 100644 index 0000000000..e9d90fcc56 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/urls.go @@ -0,0 +1,16 @@ +package monitors + +import "github.com/gophercloud/gophercloud" + +const ( + rootPath = "lb" + resourcePath = "health_monitors" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/pools/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/pools/requests.go new file mode 100644 index 0000000000..2a75737a8c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/pools/requests.go @@ -0,0 +1,170 @@ +package pools + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the floating IP attributes you want to see returned. SortKey allows you to +// sort by a particular network attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Status string `q:"status"` + LBMethod string `q:"lb_method"` + Protocol string `q:"protocol"` + SubnetID string `q:"subnet_id"` + TenantID string `q:"tenant_id"` + AdminStateUp *bool `q:"admin_state_up"` + Name string `q:"name"` + ID string `q:"id"` + VIPID string `q:"vip_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// List returns a Pager which allows you to iterate over a collection of +// pools. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those pools that are owned by the +// tenant who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + q, err := gophercloud.BuildQueryString(&opts) + if err != nil { + return pagination.Pager{Err: err} + } + u := rootURL(c) + q.String() + return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page { + return PoolPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// LBMethod is a type used for possible load balancing methods +type LBMethod string + +// LBProtocol is a type used for possible load balancing protocols +type LBProtocol string + +// Supported attributes for create/update operations. +const ( + LBMethodRoundRobin LBMethod = "ROUND_ROBIN" + LBMethodLeastConnections LBMethod = "LEAST_CONNECTIONS" + + ProtocolTCP LBProtocol = "TCP" + ProtocolHTTP LBProtocol = "HTTP" + ProtocolHTTPS LBProtocol = "HTTPS" +) + +// CreateOptsBuilder is the interface types must satisfy to be used as options +// for the Create function +type CreateOptsBuilder interface { + ToLBPoolCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains all the values needed to create a new pool. +type CreateOpts struct { + // Name of the pool. + Name string `json:"name" required:"true"` + // The protocol used by the pool members, you can use either + // ProtocolTCP, ProtocolHTTP, or ProtocolHTTPS. + Protocol LBProtocol `json:"protocol" required:"true"` + // Only required if the caller has an admin role and wants to create a pool + // for another tenant. + TenantID string `json:"tenant_id,omitempty"` + // The network on which the members of the pool will be located. Only members + // that are on this network can be added to the pool. + SubnetID string `json:"subnet_id,omitempty"` + // The algorithm used to distribute load between the members of the pool. The + // current specification supports LBMethodRoundRobin and + // LBMethodLeastConnections as valid values for this attribute. + LBMethod LBMethod `json:"lb_method" required:"true"` + + // The provider of the pool + Provider string `json:"provider,omitempty"` +} + +// ToLBPoolCreateMap allows CreateOpts to satisfy the CreateOptsBuilder interface +func (opts CreateOpts) ToLBPoolCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "pool") +} + +// Create accepts a CreateOptsBuilder and uses the values to create a new +// load balancer pool. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToLBPoolCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(rootURL(c), b, &r.Body, nil) + return +} + +// Get retrieves a particular pool based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(resourceURL(c, id), &r.Body, nil) + return +} + +// UpdateOptsBuilder is the interface types must satisfy to be used as options +// for the Update function +type UpdateOptsBuilder interface { + ToLBPoolUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contains the values used when updating a pool. +type UpdateOpts struct { + // Name of the pool. + Name string `json:"name,omitempty"` + // The algorithm used to distribute load between the members of the pool. The + // current specification supports LBMethodRoundRobin and + // LBMethodLeastConnections as valid values for this attribute. + LBMethod LBMethod `json:"lb_method,omitempty"` +} + +// ToLBPoolUpdateMap allows UpdateOpts to satisfy the UpdateOptsBuilder interface +func (opts UpdateOpts) ToLBPoolUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "pool") +} + +// Update allows pools to be updated. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToLBPoolUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete will permanently delete a particular pool based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(resourceURL(c, id), nil) + return +} + +// AssociateMonitor will associate a health monitor with a particular pool. +// Once associated, the health monitor will start monitoring the members of the +// pool and will deactivate these members if they are deemed unhealthy. A +// member can be deactivated (status set to INACTIVE) if any of health monitors +// finds it unhealthy. +func AssociateMonitor(c *gophercloud.ServiceClient, poolID, monitorID string) (r AssociateResult) { + b := map[string]interface{}{"health_monitor": map[string]string{"id": monitorID}} + _, r.Err = c.Post(associateURL(c, poolID), b, &r.Body, nil) + return +} + +// DisassociateMonitor will disassociate a health monitor with a particular +// pool. When dissociation is successful, the health monitor will no longer +// check for the health of the members of the pool. +func DisassociateMonitor(c *gophercloud.ServiceClient, poolID, monitorID string) (r AssociateResult) { + _, r.Err = c.Delete(disassociateURL(c, poolID, monitorID), nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/pools/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/pools/results.go new file mode 100644 index 0000000000..2ca1963f27 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/pools/results.go @@ -0,0 +1,131 @@ +package pools + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Pool represents a logical set of devices, such as web servers, that you +// group together to receive and process traffic. The load balancing function +// chooses a member of the pool according to the configured load balancing +// method to handle the new requests or connections received on the VIP address. +// There is only one pool per virtual IP. +type Pool struct { + // The status of the pool. Indicates whether the pool is operational. + Status string + + // The load-balancer algorithm, which is round-robin, least-connections, and + // so on. This value, which must be supported, is dependent on the provider. + // Round-robin must be supported. + LBMethod string `json:"lb_method"` + + // The protocol of the pool, which is TCP, HTTP, or HTTPS. + Protocol string + + // Description for the pool. + Description string + + // The IDs of associated monitors which check the health of the pool members. + MonitorIDs []string `json:"health_monitors"` + + // The network on which the members of the pool will be located. Only members + // that are on this network can be added to the pool. + SubnetID string `json:"subnet_id"` + + // Owner of the pool. Only an administrative user can specify a tenant ID + // other than its own. + TenantID string `json:"tenant_id"` + + // The administrative state of the pool, which is up (true) or down (false). + AdminStateUp bool `json:"admin_state_up"` + + // Pool name. Does not have to be unique. + Name string + + // List of member IDs that belong to the pool. + MemberIDs []string `json:"members"` + + // The unique ID for the pool. + ID string + + // The ID of the virtual IP associated with this pool + VIPID string `json:"vip_id"` + + // The provider + Provider string +} + +// PoolPage is the page returned by a pager when traversing over a +// collection of pools. +type PoolPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of pools has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r PoolPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"pools_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a PoolPage struct is empty. +func (r PoolPage) IsEmpty() (bool, error) { + is, err := ExtractPools(r) + return len(is) == 0, err +} + +// ExtractPools accepts a Page struct, specifically a RouterPage struct, +// and extracts the elements into a slice of Router structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractPools(r pagination.Page) ([]Pool, error) { + var s struct { + Pools []Pool `json:"pools"` + } + err := (r.(PoolPage)).ExtractInto(&s) + return s.Pools, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a router. +func (r commonResult) Extract() (*Pool, error) { + var s struct { + Pool *Pool `json:"pool"` + } + err := r.ExtractInto(&s) + return s.Pool, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// AssociateResult represents the result of an association operation. +type AssociateResult struct { + commonResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/pools/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/pools/testing/doc.go new file mode 100644 index 0000000000..415dd2c93c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/pools/testing/doc.go @@ -0,0 +1,2 @@ +// networking_extensions_lbaas_pools_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/pools/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/pools/testing/requests_test.go new file mode 100644 index 0000000000..de038cb8cd --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/pools/testing/requests_test.go @@ -0,0 +1,316 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/pools" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/pools", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "pools":[ + { + "status":"ACTIVE", + "lb_method":"ROUND_ROBIN", + "protocol":"HTTP", + "description":"", + "health_monitors":[ + "466c8345-28d8-4f84-a246-e04380b0461d", + "5d4b5228-33b0-4e60-b225-9b727c1a20e7" + ], + "members":[ + "701b531b-111a-4f21-ad85-4795b7b12af6", + "beb53b4d-230b-4abd-8118-575b8fa006ef" + ], + "status_description": null, + "id":"72741b06-df4d-4715-b142-276b6bce75ab", + "vip_id":"4ec89087-d057-4e2c-911f-60a3b47ee304", + "name":"app_pool", + "admin_state_up":true, + "subnet_id":"8032909d-47a1-4715-90af-5153ffe39861", + "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea", + "health_monitors_status": [], + "provider": "haproxy" + } + ] +} + `) + }) + + count := 0 + + pools.List(fake.ServiceClient(), pools.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := pools.ExtractPools(page) + if err != nil { + t.Errorf("Failed to extract pools: %v", err) + return false, err + } + + expected := []pools.Pool{ + { + Status: "ACTIVE", + LBMethod: "ROUND_ROBIN", + Protocol: "HTTP", + Description: "", + MonitorIDs: []string{ + "466c8345-28d8-4f84-a246-e04380b0461d", + "5d4b5228-33b0-4e60-b225-9b727c1a20e7", + }, + SubnetID: "8032909d-47a1-4715-90af-5153ffe39861", + TenantID: "83657cfcdfe44cd5920adaf26c48ceea", + AdminStateUp: true, + Name: "app_pool", + MemberIDs: []string{ + "701b531b-111a-4f21-ad85-4795b7b12af6", + "beb53b4d-230b-4abd-8118-575b8fa006ef", + }, + ID: "72741b06-df4d-4715-b142-276b6bce75ab", + VIPID: "4ec89087-d057-4e2c-911f-60a3b47ee304", + Provider: "haproxy", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/pools", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "pool": { + "lb_method": "ROUND_ROBIN", + "protocol": "HTTP", + "name": "Example pool", + "subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9", + "tenant_id": "2ffc6e22aae24e4795f87155d24c896f", + "provider": "haproxy" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "pool": { + "status": "PENDING_CREATE", + "lb_method": "ROUND_ROBIN", + "protocol": "HTTP", + "description": "", + "health_monitors": [], + "members": [], + "status_description": null, + "id": "69055154-f603-4a28-8951-7cc2d9e54a9a", + "vip_id": null, + "name": "Example pool", + "admin_state_up": true, + "subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9", + "tenant_id": "2ffc6e22aae24e4795f87155d24c896f", + "health_monitors_status": [], + "provider": "haproxy" + } +} + `) + }) + + options := pools.CreateOpts{ + LBMethod: pools.LBMethodRoundRobin, + Protocol: "HTTP", + Name: "Example pool", + SubnetID: "1981f108-3c48-48d2-b908-30f7d28532c9", + TenantID: "2ffc6e22aae24e4795f87155d24c896f", + Provider: "haproxy", + } + p, err := pools.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "PENDING_CREATE", p.Status) + th.AssertEquals(t, "ROUND_ROBIN", p.LBMethod) + th.AssertEquals(t, "HTTP", p.Protocol) + th.AssertEquals(t, "", p.Description) + th.AssertDeepEquals(t, []string{}, p.MonitorIDs) + th.AssertDeepEquals(t, []string{}, p.MemberIDs) + th.AssertEquals(t, "69055154-f603-4a28-8951-7cc2d9e54a9a", p.ID) + th.AssertEquals(t, "Example pool", p.Name) + th.AssertEquals(t, "1981f108-3c48-48d2-b908-30f7d28532c9", p.SubnetID) + th.AssertEquals(t, "2ffc6e22aae24e4795f87155d24c896f", p.TenantID) + th.AssertEquals(t, "haproxy", p.Provider) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/pools/332abe93-f488-41ba-870b-2ac66be7f853", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "pool":{ + "id":"332abe93-f488-41ba-870b-2ac66be7f853", + "tenant_id":"19eaa775-cf5d-49bc-902e-2f85f668d995", + "name":"Example pool", + "description":"", + "protocol":"tcp", + "lb_algorithm":"ROUND_ROBIN", + "session_persistence":{ + }, + "healthmonitor_id":null, + "members":[ + ], + "admin_state_up":true, + "status":"ACTIVE" + } +} + `) + }) + + n, err := pools.Get(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.ID, "332abe93-f488-41ba-870b-2ac66be7f853") +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/pools/332abe93-f488-41ba-870b-2ac66be7f853", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "pool":{ + "name":"SuperPool", + "lb_method": "LEAST_CONNECTIONS" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "pool":{ + "status":"PENDING_UPDATE", + "lb_method":"LEAST_CONNECTIONS", + "protocol":"TCP", + "description":"", + "health_monitors":[ + + ], + "subnet_id":"8032909d-47a1-4715-90af-5153ffe39861", + "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea", + "admin_state_up":true, + "name":"SuperPool", + "members":[ + + ], + "id":"61b1f87a-7a21-4ad3-9dda-7f81d249944f", + "vip_id":null + } +} + `) + }) + + options := pools.UpdateOpts{Name: "SuperPool", LBMethod: pools.LBMethodLeastConnections} + + n, err := pools.Update(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "SuperPool", n.Name) + th.AssertDeepEquals(t, "LEAST_CONNECTIONS", n.LBMethod) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/pools/332abe93-f488-41ba-870b-2ac66be7f853", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := pools.Delete(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853") + th.AssertNoErr(t, res.Err) +} + +func TestAssociateHealthMonitor(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/pools/332abe93-f488-41ba-870b-2ac66be7f853/health_monitors", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "health_monitor":{ + "id":"b624decf-d5d3-4c66-9a3d-f047e7786181" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `{}`) + }) + + _, err := pools.AssociateMonitor(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", "b624decf-d5d3-4c66-9a3d-f047e7786181").Extract() + th.AssertNoErr(t, err) +} + +func TestDisassociateHealthMonitor(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/pools/332abe93-f488-41ba-870b-2ac66be7f853/health_monitors/b624decf-d5d3-4c66-9a3d-f047e7786181", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := pools.DisassociateMonitor(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", "b624decf-d5d3-4c66-9a3d-f047e7786181") + th.AssertNoErr(t, res.Err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/pools/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/pools/urls.go new file mode 100644 index 0000000000..fe3601bbec --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/pools/urls.go @@ -0,0 +1,25 @@ +package pools + +import "github.com/gophercloud/gophercloud" + +const ( + rootPath = "lb" + resourcePath = "pools" + monitorPath = "health_monitors" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} + +func associateURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id, monitorPath) +} + +func disassociateURL(c *gophercloud.ServiceClient, poolID, monitorID string) string { + return c.ServiceURL(rootPath, resourcePath, poolID, monitorPath, monitorID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/vips/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/vips/requests.go new file mode 100644 index 0000000000..f89d769adc --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/vips/requests.go @@ -0,0 +1,163 @@ +package vips + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the floating IP attributes you want to see returned. SortKey allows you to +// sort by a particular network attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + ID string `q:"id"` + Name string `q:"name"` + AdminStateUp *bool `q:"admin_state_up"` + Status string `q:"status"` + TenantID string `q:"tenant_id"` + SubnetID string `q:"subnet_id"` + Address string `q:"address"` + PortID string `q:"port_id"` + Protocol string `q:"protocol"` + ProtocolPort int `q:"protocol_port"` + ConnectionLimit int `q:"connection_limit"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// List returns a Pager which allows you to iterate over a collection of +// routers. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those routers that are owned by the +// tenant who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + q, err := gophercloud.BuildQueryString(&opts) + if err != nil { + return pagination.Pager{Err: err} + } + u := rootURL(c) + q.String() + return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page { + return VIPPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder is what types must satisfy to be used as Create +// options. +type CreateOptsBuilder interface { + ToVIPCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains all the values needed to create a new virtual IP. +type CreateOpts struct { + // Human-readable name for the VIP. Does not have to be unique. + Name string `json:"name" required:"true"` + // The network on which to allocate the VIP's address. A tenant can + // only create VIPs on networks authorized by policy (e.g. networks that + // belong to them or networks that are shared). + SubnetID string `json:"subnet_id" required:"true"` + // The protocol - can either be TCP, HTTP or HTTPS. + Protocol string `json:"protocol" required:"true"` + // The port on which to listen for client traffic. + ProtocolPort int `json:"protocol_port" required:"true"` + // The ID of the pool with which the VIP is associated. + PoolID string `json:"pool_id" required:"true"` + // Required for admins. Indicates the owner of the VIP. + TenantID string `json:"tenant_id,omitempty"` + // The IP address of the VIP. + Address string `json:"address,omitempty"` + // Human-readable description for the VIP. + Description string `json:"description,omitempty"` + // Omit this field to prevent session persistence. + Persistence *SessionPersistence `json:"session_persistence,omitempty"` + // The maximum number of connections allowed for the VIP. + ConnLimit *int `json:"connection_limit,omitempty"` + // The administrative state of the VIP. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +// ToVIPCreateMap allows CreateOpts to satisfy the CreateOptsBuilder +// interface +func (opts CreateOpts) ToVIPCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "vip") +} + +// Create is an operation which provisions a new virtual IP based on the +// configuration defined in the CreateOpts struct. Once the request is +// validated and progress has started on the provisioning process, a +// CreateResult will be returned. +// +// Please note that the PoolID should refer to a pool that is not already +// associated with another vip. If the pool is already used by another vip, +// then the operation will fail with a 409 Conflict error will be returned. +// +// Users with an admin role can create VIPs on behalf of other tenants by +// specifying a TenantID attribute different than their own. +func Create(c *gophercloud.ServiceClient, opts CreateOpts) (r CreateResult) { + b, err := opts.ToVIPCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(rootURL(c), b, &r.Body, nil) + return +} + +// Get retrieves a particular virtual IP based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(resourceURL(c, id), &r.Body, nil) + return +} + +// UpdateOptsBuilder is what types must satisfy to be used as Update +// options. +type UpdateOptsBuilder interface { + ToVIPUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contains all the values needed to update an existing virtual IP. +// Attributes not listed here but appear in CreateOpts are immutable and cannot +// be updated. +type UpdateOpts struct { + // Human-readable name for the VIP. Does not have to be unique. + Name *string `json:"name,omitempty"` + // The ID of the pool with which the VIP is associated. + PoolID *string `json:"pool_id,omitempty"` + // Human-readable description for the VIP. + Description *string `json:"description,omitempty"` + // Omit this field to prevent session persistence. + Persistence *SessionPersistence `json:"session_persistence,omitempty"` + // The maximum number of connections allowed for the VIP. + ConnLimit *int `json:"connection_limit,omitempty"` + // The administrative state of the VIP. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +// ToVIPUpdateMap allows UpdateOpts to satisfy the UpdateOptsBuilder interface +func (opts UpdateOpts) ToVIPUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "vip") +} + +// Update is an operation which modifies the attributes of the specified VIP. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToVIPUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 202}, + }) + return +} + +// Delete will permanently delete a particular virtual IP based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(resourceURL(c, id), nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/vips/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/vips/results.go new file mode 100644 index 0000000000..7ac7e79be7 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/vips/results.go @@ -0,0 +1,151 @@ +package vips + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// SessionPersistence represents the session persistence feature of the load +// balancing service. It attempts to force connections or requests in the same +// session to be processed by the same member as long as it is ative. Three +// types of persistence are supported: +// +// SOURCE_IP: With this mode, all connections originating from the same source +// IP address, will be handled by the same member of the pool. +// HTTP_COOKIE: With this persistence mode, the load balancing function will +// create a cookie on the first request from a client. Subsequent +// requests containing the same cookie value will be handled by +// the same member of the pool. +// APP_COOKIE: With this persistence mode, the load balancing function will +// rely on a cookie established by the backend application. All +// requests carrying the same cookie value will be handled by the +// same member of the pool. +type SessionPersistence struct { + // The type of persistence mode + Type string `json:"type"` + + // Name of cookie if persistence mode is set appropriately + CookieName string `json:"cookie_name,omitempty"` +} + +// VirtualIP is the primary load balancing configuration object that specifies +// the virtual IP address and port on which client traffic is received, as well +// as other details such as the load balancing method to be use, protocol, etc. +// This entity is sometimes known in LB products under the name of a "virtual +// server", a "vserver" or a "listener". +type VirtualIP struct { + // The unique ID for the VIP. + ID string `json:"id"` + + // Owner of the VIP. Only an admin user can specify a tenant ID other than its own. + TenantID string `json:"tenant_id"` + + // Human-readable name for the VIP. Does not have to be unique. + Name string `json:"name"` + + // Human-readable description for the VIP. + Description string `json:"description"` + + // The ID of the subnet on which to allocate the VIP address. + SubnetID string `json:"subnet_id"` + + // The IP address of the VIP. + Address string `json:"address"` + + // The protocol of the VIP address. A valid value is TCP, HTTP, or HTTPS. + Protocol string `json:"protocol"` + + // The port on which to listen to client traffic that is associated with the + // VIP address. A valid value is from 0 to 65535. + ProtocolPort int `json:"protocol_port"` + + // The ID of the pool with which the VIP is associated. + PoolID string `json:"pool_id"` + + // The ID of the port which belongs to the load balancer + PortID string `json:"port_id"` + + // Indicates whether connections in the same session will be processed by the + // same pool member or not. + Persistence SessionPersistence `json:"session_persistence"` + + // The maximum number of connections allowed for the VIP. Default is -1, + // meaning no limit. + ConnLimit int `json:"connection_limit"` + + // The administrative state of the VIP. A valid value is true (UP) or false (DOWN). + AdminStateUp bool `json:"admin_state_up"` + + // The status of the VIP. Indicates whether the VIP is operational. + Status string `json:"status"` +} + +// VIPPage is the page returned by a pager when traversing over a +// collection of routers. +type VIPPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of routers has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r VIPPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"vips_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a VIPPage struct is empty. +func (r VIPPage) IsEmpty() (bool, error) { + is, err := ExtractVIPs(r) + return len(is) == 0, err +} + +// ExtractVIPs accepts a Page struct, specifically a VIPPage struct, +// and extracts the elements into a slice of VirtualIP structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractVIPs(r pagination.Page) ([]VirtualIP, error) { + var s struct { + VIPs []VirtualIP `json:"vips"` + } + err := (r.(VIPPage)).ExtractInto(&s) + return s.VIPs, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a router. +func (r commonResult) Extract() (*VirtualIP, error) { + var s struct { + VirtualIP *VirtualIP `json:"vip" json:"vip"` + } + err := r.ExtractInto(&s) + return s.VirtualIP, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/vips/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/vips/testing/doc.go new file mode 100644 index 0000000000..8e91e78bc5 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/vips/testing/doc.go @@ -0,0 +1,2 @@ +// networking_extensions_lbaas_vips_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/vips/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/vips/testing/requests_test.go new file mode 100644 index 0000000000..7f9b6ddb78 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/vips/testing/requests_test.go @@ -0,0 +1,330 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud" + fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/vips" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/vips", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "vips":[ + { + "id": "db902c0c-d5ff-4753-b465-668ad9656918", + "tenant_id": "310df60f-2a10-4ee5-9554-98393092194c", + "name": "web_vip", + "description": "lb config for the web tier", + "subnet_id": "96a4386a-f8c3-42ed-afce-d7954eee77b3", + "address" : "10.30.176.47", + "port_id" : "cd1f7a47-4fa6-449c-9ee7-632838aedfea", + "protocol": "HTTP", + "protocol_port": 80, + "pool_id" : "cfc6589d-f949-4c66-99d2-c2da56ef3764", + "admin_state_up": true, + "status": "ACTIVE" + }, + { + "id": "36e08a3e-a78f-4b40-a229-1e7e23eee1ab", + "tenant_id": "310df60f-2a10-4ee5-9554-98393092194c", + "name": "db_vip", + "description": "lb config for the db tier", + "subnet_id": "9cedb85d-0759-4898-8a4b-fa5a5ea10086", + "address" : "10.30.176.48", + "port_id" : "cd1f7a47-4fa6-449c-9ee7-632838aedfea", + "protocol": "TCP", + "protocol_port": 3306, + "pool_id" : "41efe233-7591-43c5-9cf7-923964759f9e", + "session_persistence" : {"type" : "SOURCE_IP"}, + "connection_limit" : 2000, + "admin_state_up": true, + "status": "INACTIVE" + } + ] +} + `) + }) + + count := 0 + + vips.List(fake.ServiceClient(), vips.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := vips.ExtractVIPs(page) + if err != nil { + t.Errorf("Failed to extract LBs: %v", err) + return false, err + } + + expected := []vips.VirtualIP{ + { + ID: "db902c0c-d5ff-4753-b465-668ad9656918", + TenantID: "310df60f-2a10-4ee5-9554-98393092194c", + Name: "web_vip", + Description: "lb config for the web tier", + SubnetID: "96a4386a-f8c3-42ed-afce-d7954eee77b3", + Address: "10.30.176.47", + PortID: "cd1f7a47-4fa6-449c-9ee7-632838aedfea", + Protocol: "HTTP", + ProtocolPort: 80, + PoolID: "cfc6589d-f949-4c66-99d2-c2da56ef3764", + Persistence: vips.SessionPersistence{}, + ConnLimit: 0, + AdminStateUp: true, + Status: "ACTIVE", + }, + { + ID: "36e08a3e-a78f-4b40-a229-1e7e23eee1ab", + TenantID: "310df60f-2a10-4ee5-9554-98393092194c", + Name: "db_vip", + Description: "lb config for the db tier", + SubnetID: "9cedb85d-0759-4898-8a4b-fa5a5ea10086", + Address: "10.30.176.48", + PortID: "cd1f7a47-4fa6-449c-9ee7-632838aedfea", + Protocol: "TCP", + ProtocolPort: 3306, + PoolID: "41efe233-7591-43c5-9cf7-923964759f9e", + Persistence: vips.SessionPersistence{Type: "SOURCE_IP"}, + ConnLimit: 2000, + AdminStateUp: true, + Status: "INACTIVE", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/vips", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "vip": { + "protocol": "HTTP", + "name": "NewVip", + "admin_state_up": true, + "subnet_id": "8032909d-47a1-4715-90af-5153ffe39861", + "pool_id": "61b1f87a-7a21-4ad3-9dda-7f81d249944f", + "protocol_port": 80, + "session_persistence": {"type": "SOURCE_IP"} + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "vip": { + "status": "PENDING_CREATE", + "protocol": "HTTP", + "description": "", + "admin_state_up": true, + "subnet_id": "8032909d-47a1-4715-90af-5153ffe39861", + "tenant_id": "83657cfcdfe44cd5920adaf26c48ceea", + "connection_limit": -1, + "pool_id": "61b1f87a-7a21-4ad3-9dda-7f81d249944f", + "address": "10.0.0.11", + "protocol_port": 80, + "port_id": "f7e6fe6a-b8b5-43a8-8215-73456b32e0f5", + "id": "c987d2be-9a3c-4ac9-a046-e8716b1350e2", + "name": "NewVip" + } +} + `) + }) + + opts := vips.CreateOpts{ + Protocol: "HTTP", + Name: "NewVip", + AdminStateUp: gophercloud.Enabled, + SubnetID: "8032909d-47a1-4715-90af-5153ffe39861", + PoolID: "61b1f87a-7a21-4ad3-9dda-7f81d249944f", + ProtocolPort: 80, + Persistence: &vips.SessionPersistence{Type: "SOURCE_IP"}, + } + + r, err := vips.Create(fake.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "PENDING_CREATE", r.Status) + th.AssertEquals(t, "HTTP", r.Protocol) + th.AssertEquals(t, "", r.Description) + th.AssertEquals(t, true, r.AdminStateUp) + th.AssertEquals(t, "8032909d-47a1-4715-90af-5153ffe39861", r.SubnetID) + th.AssertEquals(t, "83657cfcdfe44cd5920adaf26c48ceea", r.TenantID) + th.AssertEquals(t, -1, r.ConnLimit) + th.AssertEquals(t, "61b1f87a-7a21-4ad3-9dda-7f81d249944f", r.PoolID) + th.AssertEquals(t, "10.0.0.11", r.Address) + th.AssertEquals(t, 80, r.ProtocolPort) + th.AssertEquals(t, "f7e6fe6a-b8b5-43a8-8215-73456b32e0f5", r.PortID) + th.AssertEquals(t, "c987d2be-9a3c-4ac9-a046-e8716b1350e2", r.ID) + th.AssertEquals(t, "NewVip", r.Name) +} + +func TestRequiredCreateOpts(t *testing.T) { + res := vips.Create(fake.ServiceClient(), vips.CreateOpts{}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = vips.Create(fake.ServiceClient(), vips.CreateOpts{Name: "foo"}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = vips.Create(fake.ServiceClient(), vips.CreateOpts{Name: "foo", SubnetID: "bar"}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = vips.Create(fake.ServiceClient(), vips.CreateOpts{Name: "foo", SubnetID: "bar", Protocol: "bar"}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = vips.Create(fake.ServiceClient(), vips.CreateOpts{Name: "foo", SubnetID: "bar", Protocol: "bar", ProtocolPort: 80}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/vips/4ec89087-d057-4e2c-911f-60a3b47ee304", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "vip": { + "status": "ACTIVE", + "protocol": "HTTP", + "description": "", + "admin_state_up": true, + "subnet_id": "8032909d-47a1-4715-90af-5153ffe39861", + "tenant_id": "83657cfcdfe44cd5920adaf26c48ceea", + "connection_limit": 1000, + "pool_id": "72741b06-df4d-4715-b142-276b6bce75ab", + "session_persistence": { + "cookie_name": "MyAppCookie", + "type": "APP_COOKIE" + }, + "address": "10.0.0.10", + "protocol_port": 80, + "port_id": "b5a743d6-056b-468b-862d-fb13a9aa694e", + "id": "4ec89087-d057-4e2c-911f-60a3b47ee304", + "name": "my-vip" + } +} + `) + }) + + vip, err := vips.Get(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "ACTIVE", vip.Status) + th.AssertEquals(t, "HTTP", vip.Protocol) + th.AssertEquals(t, "", vip.Description) + th.AssertEquals(t, true, vip.AdminStateUp) + th.AssertEquals(t, 1000, vip.ConnLimit) + th.AssertEquals(t, vips.SessionPersistence{Type: "APP_COOKIE", CookieName: "MyAppCookie"}, vip.Persistence) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/vips/4ec89087-d057-4e2c-911f-60a3b47ee304", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "vip": { + "connection_limit": 1000, + "session_persistence": {"type": "SOURCE_IP"} + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, ` +{ + "vip": { + "status": "PENDING_UPDATE", + "protocol": "HTTP", + "description": "", + "admin_state_up": true, + "subnet_id": "8032909d-47a1-4715-90af-5153ffe39861", + "tenant_id": "83657cfcdfe44cd5920adaf26c48ceea", + "connection_limit": 1000, + "pool_id": "61b1f87a-7a21-4ad3-9dda-7f81d249944f", + "address": "10.0.0.11", + "protocol_port": 80, + "port_id": "f7e6fe6a-b8b5-43a8-8215-73456b32e0f5", + "id": "c987d2be-9a3c-4ac9-a046-e8716b1350e2", + "name": "NewVip" + } +} + `) + }) + + i1000 := 1000 + options := vips.UpdateOpts{ + ConnLimit: &i1000, + Persistence: &vips.SessionPersistence{Type: "SOURCE_IP"}, + } + vip, err := vips.Update(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304", options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "PENDING_UPDATE", vip.Status) + th.AssertEquals(t, 1000, vip.ConnLimit) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/vips/4ec89087-d057-4e2c-911f-60a3b47ee304", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := vips.Delete(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304") + th.AssertNoErr(t, res.Err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/vips/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/vips/urls.go new file mode 100644 index 0000000000..584a1cf680 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/vips/urls.go @@ -0,0 +1,16 @@ +package vips + +import "github.com/gophercloud/gophercloud" + +const ( + rootPath = "lb" + resourcePath = "vips" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/doc.go new file mode 100644 index 0000000000..247a75facb --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/doc.go @@ -0,0 +1,5 @@ +// Package lbaas_v2 provides information and interaction with the Load Balancer +// as a Service v2 extension for the OpenStack Networking service. +// lbaas v2 api docs: http://developer.openstack.org/api-ref-networking-v2-ext.html#lbaas-v2.0 +// lbaas v2 api schema: https://github.com/openstack/neutron-lbaas/blob/master/neutron_lbaas/extensions/loadbalancerv2.py +package lbaas_v2 diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners/requests.go new file mode 100644 index 0000000000..4a78447902 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners/requests.go @@ -0,0 +1,182 @@ +package listeners + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +type Protocol string + +// Supported attributes for create/update operations. +const ( + ProtocolTCP Protocol = "TCP" + ProtocolHTTP Protocol = "HTTP" + ProtocolHTTPS Protocol = "HTTPS" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToListenerListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the floating IP attributes you want to see returned. SortKey allows you to +// sort by a particular listener attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + ID string `q:"id"` + Name string `q:"name"` + AdminStateUp *bool `q:"admin_state_up"` + TenantID string `q:"tenant_id"` + LoadbalancerID string `q:"loadbalancer_id"` + DefaultPoolID string `q:"default_pool_id"` + Protocol string `q:"protocol"` + ProtocolPort int `q:"protocol_port"` + ConnectionLimit int `q:"connection_limit"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToListenerListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToListenerListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// routers. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those routers that are owned by the +// tenant who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := rootURL(c) + if opts != nil { + query, err := opts.ToListenerListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return ListenerPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Create operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type CreateOptsBuilder interface { + ToListenerCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts struct { + // The load balancer on which to provision this listener. + LoadbalancerID string `json:"loadbalancer_id" required:"true"` + // The protocol - can either be TCP, HTTP or HTTPS. + Protocol Protocol `json:"protocol" required:"true"` + // The port on which to listen for client traffic. + ProtocolPort int `json:"protocol_port" required:"true"` + // Indicates the owner of the Listener. Required for admins. + TenantID string `json:"tenant_id,omitempty"` + // Human-readable name for the Listener. Does not have to be unique. + Name string `json:"name,omitempty"` + // The ID of the default pool with which the Listener is associated. + DefaultPoolID string `json:"default_pool_id,omitempty"` + // Human-readable description for the Listener. + Description string `json:"description,omitempty"` + // The maximum number of connections allowed for the Listener. + ConnLimit *int `json:"connection_limit,omitempty"` + // A reference to a container of TLS secrets. + DefaultTlsContainerRef string `json:"default_tls_container_ref,omitempty"` + // A list of references to TLS secrets. + SniContainerRefs []string `json:"sni_container_refs,omitempty"` + // The administrative state of the Listener. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +// ToListenerCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToListenerCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "listener") +} + +// Create is an operation which provisions a new Listeners based on the +// configuration defined in the CreateOpts struct. Once the request is +// validated and progress has started on the provisioning process, a +// CreateResult will be returned. +// +// Users with an admin role can create Listeners on behalf of other tenants by +// specifying a TenantID attribute different than their own. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToListenerCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(rootURL(c), b, &r.Body, nil) + return +} + +// Get retrieves a particular Listeners based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(resourceURL(c, id), &r.Body, nil) + return +} + +// UpdateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Update operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type UpdateOptsBuilder interface { + ToListenerUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts is the common options struct used in this package's Update +// operation. +type UpdateOpts struct { + // Human-readable name for the Listener. Does not have to be unique. + Name string `json:"name,omitempty"` + // Human-readable description for the Listener. + Description string `json:"description,omitempty"` + // The maximum number of connections allowed for the Listener. + ConnLimit *int `json:"connection_limit,omitempty"` + // A reference to a container of TLS secrets. + DefaultTlsContainerRef string `json:"default_tls_container_ref,omitempty"` + // A list of references to TLS secrets. + SniContainerRefs []string `json:"sni_container_refs,omitempty"` + // The administrative state of the Listener. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +// ToListenerUpdateMap casts a UpdateOpts struct to a map. +func (opts UpdateOpts) ToListenerUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "listener") +} + +// Update is an operation which modifies the attributes of the specified Listener. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) (r UpdateResult) { + b, err := opts.ToListenerUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 202}, + }) + return +} + +// Delete will permanently delete a particular Listeners based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(resourceURL(c, id), nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners/results.go new file mode 100644 index 0000000000..aa8ed1bde5 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners/results.go @@ -0,0 +1,114 @@ +package listeners + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools" + "github.com/gophercloud/gophercloud/pagination" +) + +type LoadBalancerID struct { + ID string `json:"id"` +} + +// Listener is the primary load balancing configuration object that specifies +// the loadbalancer and port on which client traffic is received, as well +// as other details such as the load balancing method to be use, protocol, etc. +type Listener struct { + // The unique ID for the Listener. + ID string `json:"id"` + // Owner of the Listener. Only an admin user can specify a tenant ID other than its own. + TenantID string `json:"tenant_id"` + // Human-readable name for the Listener. Does not have to be unique. + Name string `json:"name"` + // Human-readable description for the Listener. + Description string `json:"description"` + // The protocol to loadbalance. A valid value is TCP, HTTP, or HTTPS. + Protocol string `json:"protocol"` + // The port on which to listen to client traffic that is associated with the + // Loadbalancer. A valid value is from 0 to 65535. + ProtocolPort int `json:"protocol_port"` + // The UUID of default pool. Must have compatible protocol with listener. + DefaultPoolID string `json:"default_pool_id"` + // A list of load balancer IDs. + Loadbalancers []LoadBalancerID `json:"loadbalancers"` + // The maximum number of connections allowed for the Loadbalancer. Default is -1, + // meaning no limit. + ConnLimit int `json:"connection_limit"` + // The list of references to TLS secrets. + SniContainerRefs []string `json:"sni_container_refs"` + // Optional. A reference to a container of TLS secrets. + DefaultTlsContainerRef string `json:"default_tls_container_ref"` + // The administrative state of the Listener. A valid value is true (UP) or false (DOWN). + AdminStateUp bool `json:"admin_state_up"` + Pools []pools.Pool `json:"pools"` +} + +// ListenerPage is the page returned by a pager when traversing over a +// collection of routers. +type ListenerPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of routers has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r ListenerPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"listeners_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a RouterPage struct is empty. +func (r ListenerPage) IsEmpty() (bool, error) { + is, err := ExtractListeners(r) + return len(is) == 0, err +} + +// ExtractListeners accepts a Page struct, specifically a ListenerPage struct, +// and extracts the elements into a slice of Listener structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractListeners(r pagination.Page) ([]Listener, error) { + var s struct { + Listeners []Listener `json:"listeners"` + } + err := (r.(ListenerPage)).ExtractInto(&s) + return s.Listeners, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a router. +func (r commonResult) Extract() (*Listener, error) { + var s struct { + Listener *Listener `json:"listener"` + } + err := r.ExtractInto(&s) + return s.Listener, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners/testing/doc.go new file mode 100644 index 0000000000..c74a4de741 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners/testing/doc.go @@ -0,0 +1,2 @@ +// networking_extensions_lbaas_v2_listeners_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners/testing/fixtures.go new file mode 100644 index 0000000000..fa4fa25c18 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners/testing/fixtures.go @@ -0,0 +1,213 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +// ListenersListBody contains the canned body of a listeners list response. +const ListenersListBody = ` +{ + "listeners":[ + { + "id": "db902c0c-d5ff-4753-b465-668ad9656918", + "tenant_id": "310df60f-2a10-4ee5-9554-98393092194c", + "name": "web", + "description": "listener config for the web tier", + "loadbalancers": [{"id": "53306cda-815d-4354-9444-59e09da9c3c5"}], + "protocol": "HTTP", + "protocol_port": 80, + "default_pool_id": "fad389a3-9a4a-4762-a365-8c7038508b5d", + "admin_state_up": true, + "default_tls_container_ref": "2c433435-20de-4411-84ae-9cc8917def76", + "sni_container_refs": ["3d328d82-2547-4921-ac2f-61c3b452b5ff", "b3cfd7e3-8c19-455c-8ebb-d78dfd8f7e7d"] + }, + { + "id": "36e08a3e-a78f-4b40-a229-1e7e23eee1ab", + "tenant_id": "310df60f-2a10-4ee5-9554-98393092194c", + "name": "db", + "description": "listener config for the db tier", + "loadbalancers": [{"id": "79e05663-7f03-45d2-a092-8b94062f22ab"}], + "protocol": "TCP", + "protocol_port": 3306, + "default_pool_id": "41efe233-7591-43c5-9cf7-923964759f9e", + "connection_limit": 2000, + "admin_state_up": true, + "default_tls_container_ref": "2c433435-20de-4411-84ae-9cc8917def76", + "sni_container_refs": ["3d328d82-2547-4921-ac2f-61c3b452b5ff", "b3cfd7e3-8c19-455c-8ebb-d78dfd8f7e7d"] + } + ] +} +` + +// SingleServerBody is the canned body of a Get request on an existing listener. +const SingleListenerBody = ` +{ + "listener": { + "id": "36e08a3e-a78f-4b40-a229-1e7e23eee1ab", + "tenant_id": "310df60f-2a10-4ee5-9554-98393092194c", + "name": "db", + "description": "listener config for the db tier", + "loadbalancers": [{"id": "79e05663-7f03-45d2-a092-8b94062f22ab"}], + "protocol": "TCP", + "protocol_port": 3306, + "default_pool_id": "41efe233-7591-43c5-9cf7-923964759f9e", + "connection_limit": 2000, + "admin_state_up": true, + "default_tls_container_ref": "2c433435-20de-4411-84ae-9cc8917def76", + "sni_container_refs": ["3d328d82-2547-4921-ac2f-61c3b452b5ff", "b3cfd7e3-8c19-455c-8ebb-d78dfd8f7e7d"] + } +} +` + +// PostUpdateListenerBody is the canned response body of a Update request on an existing listener. +const PostUpdateListenerBody = ` +{ + "listener": { + "id": "36e08a3e-a78f-4b40-a229-1e7e23eee1ab", + "tenant_id": "310df60f-2a10-4ee5-9554-98393092194c", + "name": "NewListenerName", + "description": "listener config for the db tier", + "loadbalancers": [{"id": "79e05663-7f03-45d2-a092-8b94062f22ab"}], + "protocol": "TCP", + "protocol_port": 3306, + "default_pool_id": "41efe233-7591-43c5-9cf7-923964759f9e", + "connection_limit": 1000, + "admin_state_up": true, + "default_tls_container_ref": "2c433435-20de-4411-84ae-9cc8917def76", + "sni_container_refs": ["3d328d82-2547-4921-ac2f-61c3b452b5ff", "b3cfd7e3-8c19-455c-8ebb-d78dfd8f7e7d"] + } +} +` + +var ( + ListenerWeb = listeners.Listener{ + ID: "db902c0c-d5ff-4753-b465-668ad9656918", + TenantID: "310df60f-2a10-4ee5-9554-98393092194c", + Name: "web", + Description: "listener config for the web tier", + Loadbalancers: []listeners.LoadBalancerID{{ID: "53306cda-815d-4354-9444-59e09da9c3c5"}}, + Protocol: "HTTP", + ProtocolPort: 80, + DefaultPoolID: "fad389a3-9a4a-4762-a365-8c7038508b5d", + AdminStateUp: true, + DefaultTlsContainerRef: "2c433435-20de-4411-84ae-9cc8917def76", + SniContainerRefs: []string{"3d328d82-2547-4921-ac2f-61c3b452b5ff", "b3cfd7e3-8c19-455c-8ebb-d78dfd8f7e7d"}, + } + ListenerDb = listeners.Listener{ + ID: "36e08a3e-a78f-4b40-a229-1e7e23eee1ab", + TenantID: "310df60f-2a10-4ee5-9554-98393092194c", + Name: "db", + Description: "listener config for the db tier", + Loadbalancers: []listeners.LoadBalancerID{{ID: "79e05663-7f03-45d2-a092-8b94062f22ab"}}, + Protocol: "TCP", + ProtocolPort: 3306, + DefaultPoolID: "41efe233-7591-43c5-9cf7-923964759f9e", + ConnLimit: 2000, + AdminStateUp: true, + DefaultTlsContainerRef: "2c433435-20de-4411-84ae-9cc8917def76", + SniContainerRefs: []string{"3d328d82-2547-4921-ac2f-61c3b452b5ff", "b3cfd7e3-8c19-455c-8ebb-d78dfd8f7e7d"}, + } + ListenerUpdated = listeners.Listener{ + ID: "36e08a3e-a78f-4b40-a229-1e7e23eee1ab", + TenantID: "310df60f-2a10-4ee5-9554-98393092194c", + Name: "NewListenerName", + Description: "listener config for the db tier", + Loadbalancers: []listeners.LoadBalancerID{{ID: "79e05663-7f03-45d2-a092-8b94062f22ab"}}, + Protocol: "TCP", + ProtocolPort: 3306, + DefaultPoolID: "41efe233-7591-43c5-9cf7-923964759f9e", + ConnLimit: 1000, + AdminStateUp: true, + DefaultTlsContainerRef: "2c433435-20de-4411-84ae-9cc8917def76", + SniContainerRefs: []string{"3d328d82-2547-4921-ac2f-61c3b452b5ff", "b3cfd7e3-8c19-455c-8ebb-d78dfd8f7e7d"}, + } +) + +// HandleListenerListSuccessfully sets up the test server to respond to a listener List request. +func HandleListenerListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/v2.0/lbaas/listeners", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, ListenersListBody) + case "45e08a3e-a78f-4b40-a229-1e7e23eee1ab": + fmt.Fprintf(w, `{ "listeners": [] }`) + default: + t.Fatalf("/v2.0/lbaas/listeners invoked with unexpected marker=[%s]", marker) + } + }) +} + +// HandleListenerCreationSuccessfully sets up the test server to respond to a listener creation request +// with a given response. +func HandleListenerCreationSuccessfully(t *testing.T, response string) { + th.Mux.HandleFunc("/v2.0/lbaas/listeners", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ + "listener": { + "loadbalancer_id": "79e05663-7f03-45d2-a092-8b94062f22ab", + "protocol": "TCP", + "name": "db", + "admin_state_up": true, + "default_tls_container_ref": "2c433435-20de-4411-84ae-9cc8917def76", + "default_pool_id": "41efe233-7591-43c5-9cf7-923964759f9e", + "protocol_port": 3306 + } + }`) + + w.WriteHeader(http.StatusAccepted) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, response) + }) +} + +// HandleListenerGetSuccessfully sets up the test server to respond to a listener Get request. +func HandleListenerGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/v2.0/lbaas/listeners/4ec89087-d057-4e2c-911f-60a3b47ee304", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + fmt.Fprintf(w, SingleListenerBody) + }) +} + +// HandleListenerDeletionSuccessfully sets up the test server to respond to a listener deletion request. +func HandleListenerDeletionSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/v2.0/lbaas/listeners/4ec89087-d057-4e2c-911f-60a3b47ee304", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleListenerUpdateSuccessfully sets up the test server to respond to a listener Update request. +func HandleListenerUpdateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/v2.0/lbaas/listeners/4ec89087-d057-4e2c-911f-60a3b47ee304", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestJSONRequest(t, r, `{ + "listener": { + "name": "NewListenerName", + "connection_limit": 1001 + } + }`) + + fmt.Fprintf(w, PostUpdateListenerBody) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners/testing/requests_test.go new file mode 100644 index 0000000000..d463f6e859 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners/testing/requests_test.go @@ -0,0 +1,137 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud" + fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestListListeners(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListenerListSuccessfully(t) + + pages := 0 + err := listeners.List(fake.ServiceClient(), listeners.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := listeners.ExtractListeners(page) + if err != nil { + return false, err + } + + if len(actual) != 2 { + t.Fatalf("Expected 2 listeners, got %d", len(actual)) + } + th.CheckDeepEquals(t, ListenerWeb, actual[0]) + th.CheckDeepEquals(t, ListenerDb, actual[1]) + + return true, nil + }) + + th.AssertNoErr(t, err) + + if pages != 1 { + t.Errorf("Expected 1 page, saw %d", pages) + } +} + +func TestListAllListeners(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListenerListSuccessfully(t) + + allPages, err := listeners.List(fake.ServiceClient(), listeners.ListOpts{}).AllPages() + th.AssertNoErr(t, err) + actual, err := listeners.ExtractListeners(allPages) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ListenerWeb, actual[0]) + th.CheckDeepEquals(t, ListenerDb, actual[1]) +} + +func TestCreateListener(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListenerCreationSuccessfully(t, SingleListenerBody) + + actual, err := listeners.Create(fake.ServiceClient(), listeners.CreateOpts{ + Protocol: "TCP", + Name: "db", + LoadbalancerID: "79e05663-7f03-45d2-a092-8b94062f22ab", + AdminStateUp: gophercloud.Enabled, + DefaultTlsContainerRef: "2c433435-20de-4411-84ae-9cc8917def76", + DefaultPoolID: "41efe233-7591-43c5-9cf7-923964759f9e", + ProtocolPort: 3306, + }).Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ListenerDb, *actual) +} + +func TestRequiredCreateOpts(t *testing.T) { + res := listeners.Create(fake.ServiceClient(), listeners.CreateOpts{}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = listeners.Create(fake.ServiceClient(), listeners.CreateOpts{Name: "foo"}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = listeners.Create(fake.ServiceClient(), listeners.CreateOpts{Name: "foo", TenantID: "bar"}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = listeners.Create(fake.ServiceClient(), listeners.CreateOpts{Name: "foo", TenantID: "bar", Protocol: "bar"}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = listeners.Create(fake.ServiceClient(), listeners.CreateOpts{Name: "foo", TenantID: "bar", Protocol: "bar", ProtocolPort: 80}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestGetListener(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListenerGetSuccessfully(t) + + client := fake.ServiceClient() + actual, err := listeners.Get(client, "4ec89087-d057-4e2c-911f-60a3b47ee304").Extract() + if err != nil { + t.Fatalf("Unexpected Get error: %v", err) + } + + th.CheckDeepEquals(t, ListenerDb, *actual) +} + +func TestDeleteListener(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListenerDeletionSuccessfully(t) + + res := listeners.Delete(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304") + th.AssertNoErr(t, res.Err) +} + +func TestUpdateListener(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListenerUpdateSuccessfully(t) + + client := fake.ServiceClient() + i1001 := 1001 + actual, err := listeners.Update(client, "4ec89087-d057-4e2c-911f-60a3b47ee304", listeners.UpdateOpts{ + Name: "NewListenerName", + ConnLimit: &i1001, + }).Extract() + if err != nil { + t.Fatalf("Unexpected Update error: %v", err) + } + + th.CheckDeepEquals(t, ListenerUpdated, *actual) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners/urls.go new file mode 100644 index 0000000000..02fb1eb39e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners/urls.go @@ -0,0 +1,16 @@ +package listeners + +import "github.com/gophercloud/gophercloud" + +const ( + rootPath = "lbaas" + resourcePath = "listeners" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/requests.go new file mode 100644 index 0000000000..bc4a3c69a1 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/requests.go @@ -0,0 +1,172 @@ +package loadbalancers + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToLoadBalancerListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the Loadbalancer attributes you want to see returned. SortKey allows you to +// sort by a particular attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Description string `q:"description"` + AdminStateUp *bool `q:"admin_state_up"` + TenantID string `q:"tenant_id"` + ProvisioningStatus string `q:"provisioning_status"` + VipAddress string `q:"vip_address"` + VipPortID string `q:"vip_port_id"` + VipSubnetID string `q:"vip_subnet_id"` + ID string `q:"id"` + OperatingStatus string `q:"operating_status"` + Name string `q:"name"` + Flavor string `q:"flavor"` + Provider string `q:"provider"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToLoadbalancerListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToLoadBalancerListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// routers. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those routers that are owned by the +// tenant who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := rootURL(c) + if opts != nil { + query, err := opts.ToLoadBalancerListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return LoadBalancerPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Create operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type CreateOptsBuilder interface { + ToLoadBalancerCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts struct { + // Optional. Human-readable name for the Loadbalancer. Does not have to be unique. + Name string `json:"name,omitempty"` + // Optional. Human-readable description for the Loadbalancer. + Description string `json:"description,omitempty"` + // Required. The network on which to allocate the Loadbalancer's address. A tenant can + // only create Loadbalancers on networks authorized by policy (e.g. networks that + // belong to them or networks that are shared). + VipSubnetID string `json:"vip_subnet_id" required:"true"` + // Required for admins. The UUID of the tenant who owns the Loadbalancer. + // Only administrative users can specify a tenant UUID other than their own. + TenantID string `json:"tenant_id,omitempty"` + // Optional. The IP address of the Loadbalancer. + VipAddress string `json:"vip_address,omitempty"` + // Optional. The administrative state of the Loadbalancer. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` + // Optional. The UUID of a flavor. + Flavor string `json:"flavor,omitempty"` + // Optional. The name of the provider. + Provider string `json:"provider,omitempty"` +} + +// ToLoadBalancerCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToLoadBalancerCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "loadbalancer") +} + +// Create is an operation which provisions a new loadbalancer based on the +// configuration defined in the CreateOpts struct. Once the request is +// validated and progress has started on the provisioning process, a +// CreateResult will be returned. +// +// Users with an admin role can create loadbalancers on behalf of other tenants by +// specifying a TenantID attribute different than their own. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToLoadBalancerCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(rootURL(c), b, &r.Body, nil) + return +} + +// Get retrieves a particular Loadbalancer based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(resourceURL(c, id), &r.Body, nil) + return +} + +// UpdateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Update operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type UpdateOptsBuilder interface { + ToLoadBalancerUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts is the common options struct used in this package's Update +// operation. +type UpdateOpts struct { + // Optional. Human-readable name for the Loadbalancer. Does not have to be unique. + Name string `json:"name,omitempty"` + // Optional. Human-readable description for the Loadbalancer. + Description string `json:"description,omitempty"` + // Optional. The administrative state of the Loadbalancer. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +// ToLoadBalancerUpdateMap casts a UpdateOpts struct to a map. +func (opts UpdateOpts) ToLoadBalancerUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "loadbalancer") +} + +// Update is an operation which modifies the attributes of the specified LoadBalancer. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) (r UpdateResult) { + b, err := opts.ToLoadBalancerUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 202}, + }) + return +} + +// Delete will permanently delete a particular LoadBalancer based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(resourceURL(c, id), nil) + return +} + +func GetStatuses(c *gophercloud.ServiceClient, id string) (r GetStatusesResult) { + _, r.Err = c.Get(statusRootURL(c, id), &r.Body, nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/results.go new file mode 100644 index 0000000000..4423c2460c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/results.go @@ -0,0 +1,125 @@ +package loadbalancers + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners" + "github.com/gophercloud/gophercloud/pagination" +) + +// LoadBalancer is the primary load balancing configuration object that specifies +// the virtual IP address on which client traffic is received, as well +// as other details such as the load balancing method to be use, protocol, etc. +type LoadBalancer struct { + // Human-readable description for the Loadbalancer. + Description string `json:"description"` + // The administrative state of the Loadbalancer. A valid value is true (UP) or false (DOWN). + AdminStateUp bool `json:"admin_state_up"` + // Owner of the LoadBalancer. Only an admin user can specify a tenant ID other than its own. + TenantID string `json:"tenant_id"` + // The provisioning status of the LoadBalancer. This value is ACTIVE, PENDING_CREATE or ERROR. + ProvisioningStatus string `json:"provisioning_status"` + // The IP address of the Loadbalancer. + VipAddress string `json:"vip_address"` + // The UUID of the port associated with the IP address. + VipPortID string `json:"vip_port_id"` + // The UUID of the subnet on which to allocate the virtual IP for the Loadbalancer address. + VipSubnetID string `json:"vip_subnet_id"` + // The unique ID for the LoadBalancer. + ID string `json:"id"` + // The operating status of the LoadBalancer. This value is ONLINE or OFFLINE. + OperatingStatus string `json:"operating_status"` + // Human-readable name for the LoadBalancer. Does not have to be unique. + Name string `json:"name"` + // The UUID of a flavor if set. + Flavor string `json:"flavor"` + // The name of the provider. + Provider string `json:"provider"` + Listeners []listeners.Listener `json:"listeners"` +} + +type StatusTree struct { + Loadbalancer *LoadBalancer `json:"loadbalancer"` +} + +// LoadBalancerPage is the page returned by a pager when traversing over a +// collection of routers. +type LoadBalancerPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of routers has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r LoadBalancerPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"loadbalancers_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a LoadBalancerPage struct is empty. +func (p LoadBalancerPage) IsEmpty() (bool, error) { + is, err := ExtractLoadBalancers(p) + return len(is) == 0, err +} + +// ExtractLoadBalancers accepts a Page struct, specifically a LoadbalancerPage struct, +// and extracts the elements into a slice of LoadBalancer structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractLoadBalancers(r pagination.Page) ([]LoadBalancer, error) { + var s struct { + LoadBalancers []LoadBalancer `json:"loadbalancers"` + } + err := (r.(LoadBalancerPage)).ExtractInto(&s) + return s.LoadBalancers, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a router. +func (r commonResult) Extract() (*LoadBalancer, error) { + var s struct { + LoadBalancer *LoadBalancer `json:"loadbalancer"` + } + err := r.ExtractInto(&s) + return s.LoadBalancer, err +} + +type GetStatusesResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a Loadbalancer. +func (r GetStatusesResult) Extract() (*StatusTree, error) { + var s struct { + Statuses *StatusTree `json:"statuses"` + } + err := r.ExtractInto(&s) + return s.Statuses, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/testing/doc.go new file mode 100644 index 0000000000..b06352ec4a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/testing/doc.go @@ -0,0 +1,2 @@ +// networking_extensions_lbaas_v2_loadbalancers_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/testing/fixtures.go new file mode 100644 index 0000000000..a452236566 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/testing/fixtures.go @@ -0,0 +1,284 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" + + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools" +) + +// LoadbalancersListBody contains the canned body of a loadbalancer list response. +const LoadbalancersListBody = ` +{ + "loadbalancers":[ + { + "id": "c331058c-6a40-4144-948e-b9fb1df9db4b", + "tenant_id": "54030507-44f7-473c-9342-b4d14a95f692", + "name": "web_lb", + "description": "lb config for the web tier", + "vip_subnet_id": "8a49c438-848f-467b-9655-ea1548708154", + "vip_address": "10.30.176.47", + "vip_port_id": "2a22e552-a347-44fd-b530-1f2b1b2a6735", + "flavor": "small", + "provider": "haproxy", + "admin_state_up": true, + "provisioning_status": "ACTIVE", + "operating_status": "ONLINE" + }, + { + "id": "36e08a3e-a78f-4b40-a229-1e7e23eee1ab", + "tenant_id": "54030507-44f7-473c-9342-b4d14a95f692", + "name": "db_lb", + "description": "lb config for the db tier", + "vip_subnet_id": "9cedb85d-0759-4898-8a4b-fa5a5ea10086", + "vip_address": "10.30.176.48", + "vip_port_id": "2bf413c8-41a9-4477-b505-333d5cbe8b55", + "flavor": "medium", + "provider": "haproxy", + "admin_state_up": true, + "provisioning_status": "PENDING_CREATE", + "operating_status": "OFFLINE" + } + ] +} +` + +// SingleLoadbalancerBody is the canned body of a Get request on an existing loadbalancer. +const SingleLoadbalancerBody = ` +{ + "loadbalancer": { + "id": "36e08a3e-a78f-4b40-a229-1e7e23eee1ab", + "tenant_id": "54030507-44f7-473c-9342-b4d14a95f692", + "name": "db_lb", + "description": "lb config for the db tier", + "vip_subnet_id": "9cedb85d-0759-4898-8a4b-fa5a5ea10086", + "vip_address": "10.30.176.48", + "vip_port_id": "2bf413c8-41a9-4477-b505-333d5cbe8b55", + "flavor": "medium", + "provider": "haproxy", + "admin_state_up": true, + "provisioning_status": "PENDING_CREATE", + "operating_status": "OFFLINE" + } +} +` + +// PostUpdateLoadbalancerBody is the canned response body of a Update request on an existing loadbalancer. +const PostUpdateLoadbalancerBody = ` +{ + "loadbalancer": { + "id": "36e08a3e-a78f-4b40-a229-1e7e23eee1ab", + "tenant_id": "54030507-44f7-473c-9342-b4d14a95f692", + "name": "NewLoadbalancerName", + "description": "lb config for the db tier", + "vip_subnet_id": "9cedb85d-0759-4898-8a4b-fa5a5ea10086", + "vip_address": "10.30.176.48", + "vip_port_id": "2bf413c8-41a9-4477-b505-333d5cbe8b55", + "flavor": "medium", + "provider": "haproxy", + "admin_state_up": true, + "provisioning_status": "PENDING_CREATE", + "operating_status": "OFFLINE" + } +} +` + +// SingleLoadbalancerBody is the canned body of a Get request on an existing loadbalancer. +const LoadbalancerStatuesesTree = ` +{ + "statuses" : { + "loadbalancer": { + "id": "36e08a3e-a78f-4b40-a229-1e7e23eee1ab", + "name": "db_lb", + "provisioning_status": "PENDING_UPDATE", + "operating_status": "ACTIVE", + "listeners": [{ + "id": "db902c0c-d5ff-4753-b465-668ad9656918", + "name": "db", + "pools": [{ + "id": "fad389a3-9a4a-4762-a365-8c7038508b5d", + "name": "db", + "healthmonitor": { + "id": "67306cda-815d-4354-9fe4-59e09da9c3c5", + "type":"PING" + }, + "members":[{ + "id": "2a280670-c202-4b0b-a562-34077415aabf", + "name": "db", + "address": "10.0.2.11", + "protocol_port": 80 + }] + }] + }] + } + } +} +` + +var ( + LoadbalancerWeb = loadbalancers.LoadBalancer{ + ID: "c331058c-6a40-4144-948e-b9fb1df9db4b", + TenantID: "54030507-44f7-473c-9342-b4d14a95f692", + Name: "web_lb", + Description: "lb config for the web tier", + VipSubnetID: "8a49c438-848f-467b-9655-ea1548708154", + VipAddress: "10.30.176.47", + VipPortID: "2a22e552-a347-44fd-b530-1f2b1b2a6735", + Flavor: "small", + Provider: "haproxy", + AdminStateUp: true, + ProvisioningStatus: "ACTIVE", + OperatingStatus: "ONLINE", + } + LoadbalancerDb = loadbalancers.LoadBalancer{ + ID: "36e08a3e-a78f-4b40-a229-1e7e23eee1ab", + TenantID: "54030507-44f7-473c-9342-b4d14a95f692", + Name: "db_lb", + Description: "lb config for the db tier", + VipSubnetID: "9cedb85d-0759-4898-8a4b-fa5a5ea10086", + VipAddress: "10.30.176.48", + VipPortID: "2bf413c8-41a9-4477-b505-333d5cbe8b55", + Flavor: "medium", + Provider: "haproxy", + AdminStateUp: true, + ProvisioningStatus: "PENDING_CREATE", + OperatingStatus: "OFFLINE", + } + LoadbalancerUpdated = loadbalancers.LoadBalancer{ + ID: "36e08a3e-a78f-4b40-a229-1e7e23eee1ab", + TenantID: "54030507-44f7-473c-9342-b4d14a95f692", + Name: "NewLoadbalancerName", + Description: "lb config for the db tier", + VipSubnetID: "9cedb85d-0759-4898-8a4b-fa5a5ea10086", + VipAddress: "10.30.176.48", + VipPortID: "2bf413c8-41a9-4477-b505-333d5cbe8b55", + Flavor: "medium", + Provider: "haproxy", + AdminStateUp: true, + ProvisioningStatus: "PENDING_CREATE", + OperatingStatus: "OFFLINE", + } + LoadbalancerStatusesTree = loadbalancers.LoadBalancer{ + ID: "36e08a3e-a78f-4b40-a229-1e7e23eee1ab", + Name: "db_lb", + ProvisioningStatus: "PENDING_UPDATE", + OperatingStatus: "ACTIVE", + Listeners: []listeners.Listener{{ + ID: "db902c0c-d5ff-4753-b465-668ad9656918", + Name: "db", + Pools: []pools.Pool{{ + ID: "fad389a3-9a4a-4762-a365-8c7038508b5d", + Name: "db", + Monitor: monitors.Monitor{ + ID: "67306cda-815d-4354-9fe4-59e09da9c3c5", + Type: "PING", + }, + Members: []pools.Member{{ + ID: "2a280670-c202-4b0b-a562-34077415aabf", + Name: "db", + Address: "10.0.2.11", + ProtocolPort: 80, + }}, + }}, + }}, + } +) + +// HandleLoadbalancerListSuccessfully sets up the test server to respond to a loadbalancer List request. +func HandleLoadbalancerListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/v2.0/lbaas/loadbalancers", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, LoadbalancersListBody) + case "45e08a3e-a78f-4b40-a229-1e7e23eee1ab": + fmt.Fprintf(w, `{ "loadbalancers": [] }`) + default: + t.Fatalf("/v2.0/lbaas/loadbalancers invoked with unexpected marker=[%s]", marker) + } + }) +} + +// HandleLoadbalancerCreationSuccessfully sets up the test server to respond to a loadbalancer creation request +// with a given response. +func HandleLoadbalancerCreationSuccessfully(t *testing.T, response string) { + th.Mux.HandleFunc("/v2.0/lbaas/loadbalancers", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ + "loadbalancer": { + "name": "db_lb", + "vip_subnet_id": "9cedb85d-0759-4898-8a4b-fa5a5ea10086", + "vip_address": "10.30.176.48", + "flavor": "medium", + "provider": "haproxy", + "admin_state_up": true + } + }`) + + w.WriteHeader(http.StatusAccepted) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, response) + }) +} + +// HandleLoadbalancerGetSuccessfully sets up the test server to respond to a loadbalancer Get request. +func HandleLoadbalancerGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/v2.0/lbaas/loadbalancers/36e08a3e-a78f-4b40-a229-1e7e23eee1ab", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + fmt.Fprintf(w, SingleLoadbalancerBody) + }) +} + +// HandleLoadbalancerGetStatusesTree sets up the test server to respond to a loadbalancer Get statuses tree request. +func HandleLoadbalancerGetStatusesTree(t *testing.T) { + th.Mux.HandleFunc("/v2.0/lbaas/loadbalancers/36e08a3e-a78f-4b40-a229-1e7e23eee1ab/statuses", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + fmt.Fprintf(w, LoadbalancerStatuesesTree) + }) +} + +// HandleLoadbalancerDeletionSuccessfully sets up the test server to respond to a loadbalancer deletion request. +func HandleLoadbalancerDeletionSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/v2.0/lbaas/loadbalancers/36e08a3e-a78f-4b40-a229-1e7e23eee1ab", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleLoadbalancerUpdateSuccessfully sets up the test server to respond to a loadbalancer Update request. +func HandleLoadbalancerUpdateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/v2.0/lbaas/loadbalancers/36e08a3e-a78f-4b40-a229-1e7e23eee1ab", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestJSONRequest(t, r, `{ + "loadbalancer": { + "name": "NewLoadbalancerName" + } + }`) + + fmt.Fprintf(w, PostUpdateLoadbalancerBody) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/testing/requests_test.go new file mode 100644 index 0000000000..270bdf5a66 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/testing/requests_test.go @@ -0,0 +1,144 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud" + fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestListLoadbalancers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleLoadbalancerListSuccessfully(t) + + pages := 0 + err := loadbalancers.List(fake.ServiceClient(), loadbalancers.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := loadbalancers.ExtractLoadBalancers(page) + if err != nil { + return false, err + } + + if len(actual) != 2 { + t.Fatalf("Expected 2 loadbalancers, got %d", len(actual)) + } + th.CheckDeepEquals(t, LoadbalancerWeb, actual[0]) + th.CheckDeepEquals(t, LoadbalancerDb, actual[1]) + + return true, nil + }) + + th.AssertNoErr(t, err) + + if pages != 1 { + t.Errorf("Expected 1 page, saw %d", pages) + } +} + +func TestListAllLoadbalancers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleLoadbalancerListSuccessfully(t) + + allPages, err := loadbalancers.List(fake.ServiceClient(), loadbalancers.ListOpts{}).AllPages() + th.AssertNoErr(t, err) + actual, err := loadbalancers.ExtractLoadBalancers(allPages) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, LoadbalancerWeb, actual[0]) + th.CheckDeepEquals(t, LoadbalancerDb, actual[1]) +} + +func TestCreateLoadbalancer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleLoadbalancerCreationSuccessfully(t, SingleLoadbalancerBody) + + actual, err := loadbalancers.Create(fake.ServiceClient(), loadbalancers.CreateOpts{ + Name: "db_lb", + AdminStateUp: gophercloud.Enabled, + VipSubnetID: "9cedb85d-0759-4898-8a4b-fa5a5ea10086", + VipAddress: "10.30.176.48", + Flavor: "medium", + Provider: "haproxy", + }).Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, LoadbalancerDb, *actual) +} + +func TestRequiredCreateOpts(t *testing.T) { + res := loadbalancers.Create(fake.ServiceClient(), loadbalancers.CreateOpts{}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = loadbalancers.Create(fake.ServiceClient(), loadbalancers.CreateOpts{Name: "foo"}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = loadbalancers.Create(fake.ServiceClient(), loadbalancers.CreateOpts{Name: "foo", Description: "bar"}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = loadbalancers.Create(fake.ServiceClient(), loadbalancers.CreateOpts{Name: "foo", Description: "bar", VipAddress: "bar"}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestGetLoadbalancer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleLoadbalancerGetSuccessfully(t) + + client := fake.ServiceClient() + actual, err := loadbalancers.Get(client, "36e08a3e-a78f-4b40-a229-1e7e23eee1ab").Extract() + if err != nil { + t.Fatalf("Unexpected Get error: %v", err) + } + + th.CheckDeepEquals(t, LoadbalancerDb, *actual) +} + +func TestGetLoadbalancerStatusesTree(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleLoadbalancerGetStatusesTree(t) + + client := fake.ServiceClient() + actual, err := loadbalancers.GetStatuses(client, "36e08a3e-a78f-4b40-a229-1e7e23eee1ab").Extract() + if err != nil { + t.Fatalf("Unexpected Get error: %v", err) + } + + th.CheckDeepEquals(t, LoadbalancerStatusesTree, *(actual.Loadbalancer)) +} + +func TestDeleteLoadbalancer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleLoadbalancerDeletionSuccessfully(t) + + res := loadbalancers.Delete(fake.ServiceClient(), "36e08a3e-a78f-4b40-a229-1e7e23eee1ab") + th.AssertNoErr(t, res.Err) +} + +func TestUpdateLoadbalancer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleLoadbalancerUpdateSuccessfully(t) + + client := fake.ServiceClient() + actual, err := loadbalancers.Update(client, "36e08a3e-a78f-4b40-a229-1e7e23eee1ab", loadbalancers.UpdateOpts{ + Name: "NewLoadbalancerName", + }).Extract() + if err != nil { + t.Fatalf("Unexpected Update error: %v", err) + } + + th.CheckDeepEquals(t, LoadbalancerUpdated, *actual) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/urls.go new file mode 100644 index 0000000000..73cf5dc126 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/urls.go @@ -0,0 +1,21 @@ +package loadbalancers + +import "github.com/gophercloud/gophercloud" + +const ( + rootPath = "lbaas" + resourcePath = "loadbalancers" + statusPath = "statuses" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} + +func statusRootURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id, statusPath) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors/requests.go new file mode 100644 index 0000000000..1e776bfeed --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors/requests.go @@ -0,0 +1,233 @@ +package monitors + +import ( + "fmt" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToMonitorListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the Monitor attributes you want to see returned. SortKey allows you to +// sort by a particular Monitor attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + ID string `q:"id"` + Name string `q:"name"` + TenantID string `q:"tenant_id"` + PoolID string `q:"pool_id"` + Type string `q:"type"` + Delay int `q:"delay"` + Timeout int `q:"timeout"` + MaxRetries int `q:"max_retries"` + HTTPMethod string `q:"http_method"` + URLPath string `q:"url_path"` + ExpectedCodes string `q:"expected_codes"` + AdminStateUp *bool `q:"admin_state_up"` + Status string `q:"status"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToMonitorListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToMonitorListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// List returns a Pager which allows you to iterate over a collection of +// health monitors. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those health monitors that are owned by the +// tenant who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := rootURL(c) + if opts != nil { + query, err := opts.ToMonitorListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return MonitorPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Constants that represent approved monitoring types. +const ( + TypePING = "PING" + TypeTCP = "TCP" + TypeHTTP = "HTTP" + TypeHTTPS = "HTTPS" +) + +var ( + errDelayMustGETimeout = fmt.Errorf("Delay must be greater than or equal to timeout") +) + +// CreateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Create operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type CreateOptsBuilder interface { + ToMonitorCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts struct { + // Required. The Pool to Monitor. + PoolID string `json:"pool_id" required:"true"` + // Required. The type of probe, which is PING, TCP, HTTP, or HTTPS, that is + // sent by the load balancer to verify the member state. + Type string `json:"type" required:"true"` + // Required. The time, in seconds, between sending probes to members. + Delay int `json:"delay" required:"true"` + // Required. Maximum number of seconds for a Monitor to wait for a ping reply + // before it times out. The value must be less than the delay value. + Timeout int `json:"timeout" required:"true"` + // Required. Number of permissible ping failures before changing the member's + // status to INACTIVE. Must be a number between 1 and 10. + MaxRetries int `json:"max_retries" required:"true"` + // Required for HTTP(S) types. URI path that will be accessed if Monitor type + // is HTTP or HTTPS. + URLPath string `json:"url_path,omitempty"` + // Required for HTTP(S) types. The HTTP method used for requests by the + // Monitor. If this attribute is not specified, it defaults to "GET". + HTTPMethod string `json:"http_method,omitempty"` + // Required for HTTP(S) types. Expected HTTP codes for a passing HTTP(S) + // Monitor. You can either specify a single status like "200", or a range + // like "200-202". + ExpectedCodes string `json:"expected_codes,omitempty"` + // Indicates the owner of the Loadbalancer. Required for admins. + TenantID string `json:"tenant_id,omitempty"` + // Optional. The Name of the Monitor. + Name string `json:"name,omitempty"` + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +// ToMonitorCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToMonitorCreateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "healthmonitor") + if err != nil { + return nil, err + } + + switch opts.Type { + case TypeHTTP, TypeHTTPS: + switch opts.URLPath { + case "": + return nil, fmt.Errorf("URLPath must be provided for HTTP and HTTPS") + } + switch opts.ExpectedCodes { + case "": + return nil, fmt.Errorf("ExpectedCodes must be provided for HTTP and HTTPS") + } + } + + return b, nil +} + +/* + Create is an operation which provisions a new Health Monitor. There are + different types of Monitor you can provision: PING, TCP or HTTP(S). Below + are examples of how to create each one. + + Here is an example config struct to use when creating a PING or TCP Monitor: + + CreateOpts{Type: TypePING, Delay: 20, Timeout: 10, MaxRetries: 3} + CreateOpts{Type: TypeTCP, Delay: 20, Timeout: 10, MaxRetries: 3} + + Here is an example config struct to use when creating a HTTP(S) Monitor: + + CreateOpts{Type: TypeHTTP, Delay: 20, Timeout: 10, MaxRetries: 3, + HttpMethod: "HEAD", ExpectedCodes: "200", PoolID: "2c946bfc-1804-43ab-a2ff-58f6a762b505"} +*/ +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToMonitorCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(rootURL(c), b, &r.Body, nil) + return +} + +// Get retrieves a particular Health Monitor based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(resourceURL(c, id), &r.Body, nil) + return +} + +// UpdateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Update operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type UpdateOptsBuilder interface { + ToMonitorUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts is the common options struct used in this package's Update +// operation. +type UpdateOpts struct { + // Required. The time, in seconds, between sending probes to members. + Delay int `json:"delay,omitempty"` + // Required. Maximum number of seconds for a Monitor to wait for a ping reply + // before it times out. The value must be less than the delay value. + Timeout int `json:"timeout,omitempty"` + // Required. Number of permissible ping failures before changing the member's + // status to INACTIVE. Must be a number between 1 and 10. + MaxRetries int `json:"max_retries,omitempty"` + // Required for HTTP(S) types. URI path that will be accessed if Monitor type + // is HTTP or HTTPS. + URLPath string `json:"url_path,omitempty"` + // Required for HTTP(S) types. The HTTP method used for requests by the + // Monitor. If this attribute is not specified, it defaults to "GET". + HTTPMethod string `json:"http_method,omitempty"` + // Required for HTTP(S) types. Expected HTTP codes for a passing HTTP(S) + // Monitor. You can either specify a single status like "200", or a range + // like "200-202". + ExpectedCodes string `json:"expected_codes,omitempty"` + // Optional. The Name of the Monitor. + Name string `json:"name,omitempty"` + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +// ToMonitorUpdateMap casts a UpdateOpts struct to a map. +func (opts UpdateOpts) ToMonitorUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "healthmonitor") +} + +// Update is an operation which modifies the attributes of the specified Monitor. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToMonitorUpdateMap() + if err != nil { + r.Err = err + return + } + + _, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 202}, + }) + return +} + +// Delete will permanently delete a particular Monitor based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(resourceURL(c, id), nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors/results.go new file mode 100644 index 0000000000..05dcf477bb --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors/results.go @@ -0,0 +1,144 @@ +package monitors + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +type PoolID struct { + ID string `json:"id"` +} + +// Monitor represents a load balancer health monitor. A health monitor is used +// to determine whether or not back-end members of the VIP's pool are usable +// for processing a request. A pool can have several health monitors associated +// with it. There are different types of health monitors supported: +// +// PING: used to ping the members using ICMP. +// TCP: used to connect to the members using TCP. +// HTTP: used to send an HTTP request to the member. +// HTTPS: used to send a secure HTTP request to the member. +// +// When a pool has several monitors associated with it, each member of the pool +// is monitored by all these monitors. If any monitor declares the member as +// unhealthy, then the member status is changed to INACTIVE and the member +// won't participate in its pool's load balancing. In other words, ALL monitors +// must declare the member to be healthy for it to stay ACTIVE. +type Monitor struct { + // The unique ID for the Monitor. + ID string `json:"id"` + + // The Name of the Monitor. + Name string `json:"name"` + + // Only an administrative user can specify a tenant ID + // other than its own. + TenantID string `json:"tenant_id"` + + // The type of probe sent by the load balancer to verify the member state, + // which is PING, TCP, HTTP, or HTTPS. + Type string `json:"type"` + + // The time, in seconds, between sending probes to members. + Delay int `json:"delay"` + + // The maximum number of seconds for a monitor to wait for a connection to be + // established before it times out. This value must be less than the delay value. + Timeout int `json:"timeout"` + + // Number of allowed connection failures before changing the status of the + // member to INACTIVE. A valid value is from 1 to 10. + MaxRetries int `json:"max_retries"` + + // The HTTP method that the monitor uses for requests. + HTTPMethod string `json:"http_method"` + + // The HTTP path of the request sent by the monitor to test the health of a + // member. Must be a string beginning with a forward slash (/). + URLPath string `json:"url_path" ` + + // Expected HTTP codes for a passing HTTP(S) monitor. + ExpectedCodes string `json:"expected_codes"` + + // The administrative state of the health monitor, which is up (true) or down (false). + AdminStateUp bool `json:"admin_state_up"` + + // The status of the health monitor. Indicates whether the health monitor is + // operational. + Status string `json:"status"` + + // List of pools that are associated with the health monitor. + Pools []PoolID `json:"pools"` +} + +// MonitorPage is the page returned by a pager when traversing over a +// collection of health monitors. +type MonitorPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of monitors has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r MonitorPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"healthmonitors_links"` + } + + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a MonitorPage struct is empty. +func (r MonitorPage) IsEmpty() (bool, error) { + is, err := ExtractMonitors(r) + return len(is) == 0, err +} + +// ExtractMonitors accepts a Page struct, specifically a MonitorPage struct, +// and extracts the elements into a slice of Monitor structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractMonitors(r pagination.Page) ([]Monitor, error) { + var s struct { + Monitors []Monitor `json:"healthmonitors"` + } + err := (r.(MonitorPage)).ExtractInto(&s) + return s.Monitors, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a monitor. +func (r commonResult) Extract() (*Monitor, error) { + var s struct { + Monitor *Monitor `json:"healthmonitor"` + } + err := r.ExtractInto(&s) + return s.Monitor, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors/testing/doc.go new file mode 100644 index 0000000000..443f9ade3d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors/testing/doc.go @@ -0,0 +1,2 @@ +// networking_extensions_lbaas_v2_monitors_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors/testing/fixtures.go new file mode 100644 index 0000000000..6d3eb01ee4 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors/testing/fixtures.go @@ -0,0 +1,215 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +// HealthmonitorsListBody contains the canned body of a healthmonitor list response. +const HealthmonitorsListBody = ` +{ + "healthmonitors":[ + { + "admin_state_up":true, + "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea", + "delay":10, + "name":"web", + "max_retries":1, + "timeout":1, + "type":"PING", + "pools": [{"id": "84f1b61f-58c4-45bf-a8a9-2dafb9e5214d"}], + "id":"466c8345-28d8-4f84-a246-e04380b0461d" + }, + { + "admin_state_up":true, + "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea", + "delay":5, + "name":"db", + "expected_codes":"200", + "max_retries":2, + "http_method":"GET", + "timeout":2, + "url_path":"/", + "type":"HTTP", + "pools": [{"id": "d459f7d8-c6ee-439d-8713-d3fc08aeed8d"}], + "id":"5d4b5228-33b0-4e60-b225-9b727c1a20e7" + } + ] +} +` + +// SingleHealthmonitorBody is the canned body of a Get request on an existing healthmonitor. +const SingleHealthmonitorBody = ` +{ + "healthmonitor": { + "admin_state_up":true, + "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea", + "delay":5, + "name":"db", + "expected_codes":"200", + "max_retries":2, + "http_method":"GET", + "timeout":2, + "url_path":"/", + "type":"HTTP", + "pools": [{"id": "d459f7d8-c6ee-439d-8713-d3fc08aeed8d"}], + "id":"5d4b5228-33b0-4e60-b225-9b727c1a20e7" + } +} +` + +// PostUpdateHealthmonitorBody is the canned response body of a Update request on an existing healthmonitor. +const PostUpdateHealthmonitorBody = ` +{ + "healthmonitor": { + "admin_state_up":true, + "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea", + "delay":3, + "name":"NewHealthmonitorName", + "expected_codes":"301", + "max_retries":10, + "http_method":"GET", + "timeout":20, + "url_path":"/another_check", + "type":"HTTP", + "pools": [{"id": "d459f7d8-c6ee-439d-8713-d3fc08aeed8d"}], + "id":"5d4b5228-33b0-4e60-b225-9b727c1a20e7" + } +} +` + +var ( + HealthmonitorWeb = monitors.Monitor{ + AdminStateUp: true, + Name: "web", + TenantID: "83657cfcdfe44cd5920adaf26c48ceea", + Delay: 10, + MaxRetries: 1, + Timeout: 1, + Type: "PING", + ID: "466c8345-28d8-4f84-a246-e04380b0461d", + Pools: []monitors.PoolID{{ID: "84f1b61f-58c4-45bf-a8a9-2dafb9e5214d"}}, + } + HealthmonitorDb = monitors.Monitor{ + AdminStateUp: true, + Name: "db", + TenantID: "83657cfcdfe44cd5920adaf26c48ceea", + Delay: 5, + ExpectedCodes: "200", + MaxRetries: 2, + Timeout: 2, + URLPath: "/", + Type: "HTTP", + HTTPMethod: "GET", + ID: "5d4b5228-33b0-4e60-b225-9b727c1a20e7", + Pools: []monitors.PoolID{{ID: "d459f7d8-c6ee-439d-8713-d3fc08aeed8d"}}, + } + HealthmonitorUpdated = monitors.Monitor{ + AdminStateUp: true, + Name: "NewHealthmonitorName", + TenantID: "83657cfcdfe44cd5920adaf26c48ceea", + Delay: 3, + ExpectedCodes: "301", + MaxRetries: 10, + Timeout: 20, + URLPath: "/another_check", + Type: "HTTP", + HTTPMethod: "GET", + ID: "5d4b5228-33b0-4e60-b225-9b727c1a20e7", + Pools: []monitors.PoolID{{ID: "d459f7d8-c6ee-439d-8713-d3fc08aeed8d"}}, + } +) + +// HandleHealthmonitorListSuccessfully sets up the test server to respond to a healthmonitor List request. +func HandleHealthmonitorListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/v2.0/lbaas/healthmonitors", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, HealthmonitorsListBody) + case "556c8345-28d8-4f84-a246-e04380b0461d": + fmt.Fprintf(w, `{ "healthmonitors": [] }`) + default: + t.Fatalf("/v2.0/lbaas/healthmonitors invoked with unexpected marker=[%s]", marker) + } + }) +} + +// HandleHealthmonitorCreationSuccessfully sets up the test server to respond to a healthmonitor creation request +// with a given response. +func HandleHealthmonitorCreationSuccessfully(t *testing.T, response string) { + th.Mux.HandleFunc("/v2.0/lbaas/healthmonitors", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ + "healthmonitor": { + "type":"HTTP", + "pool_id":"84f1b61f-58c4-45bf-a8a9-2dafb9e5214d", + "tenant_id":"453105b9-1754-413f-aab1-55f1af620750", + "delay":20, + "name":"db", + "timeout":10, + "max_retries":5, + "url_path":"/check", + "expected_codes":"200-299" + } + }`) + + w.WriteHeader(http.StatusAccepted) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, response) + }) +} + +// HandleHealthmonitorGetSuccessfully sets up the test server to respond to a healthmonitor Get request. +func HandleHealthmonitorGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/v2.0/lbaas/healthmonitors/5d4b5228-33b0-4e60-b225-9b727c1a20e7", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + fmt.Fprintf(w, SingleHealthmonitorBody) + }) +} + +// HandleHealthmonitorDeletionSuccessfully sets up the test server to respond to a healthmonitor deletion request. +func HandleHealthmonitorDeletionSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/v2.0/lbaas/healthmonitors/5d4b5228-33b0-4e60-b225-9b727c1a20e7", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleHealthmonitorUpdateSuccessfully sets up the test server to respond to a healthmonitor Update request. +func HandleHealthmonitorUpdateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/v2.0/lbaas/healthmonitors/5d4b5228-33b0-4e60-b225-9b727c1a20e7", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestJSONRequest(t, r, `{ + "healthmonitor": { + "name": "NewHealthmonitorName", + "delay": 3, + "timeout": 20, + "max_retries": 10, + "url_path": "/another_check", + "expected_codes": "301" + } + }`) + + fmt.Fprintf(w, PostUpdateHealthmonitorBody) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors/testing/requests_test.go new file mode 100644 index 0000000000..743d9c1c6b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors/testing/requests_test.go @@ -0,0 +1,154 @@ +package testing + +import ( + "testing" + + fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestListHealthmonitors(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleHealthmonitorListSuccessfully(t) + + pages := 0 + err := monitors.List(fake.ServiceClient(), monitors.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := monitors.ExtractMonitors(page) + if err != nil { + return false, err + } + + if len(actual) != 2 { + t.Fatalf("Expected 2 healthmonitors, got %d", len(actual)) + } + th.CheckDeepEquals(t, HealthmonitorWeb, actual[0]) + th.CheckDeepEquals(t, HealthmonitorDb, actual[1]) + + return true, nil + }) + + th.AssertNoErr(t, err) + + if pages != 1 { + t.Errorf("Expected 1 page, saw %d", pages) + } +} + +func TestListAllHealthmonitors(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleHealthmonitorListSuccessfully(t) + + allPages, err := monitors.List(fake.ServiceClient(), monitors.ListOpts{}).AllPages() + th.AssertNoErr(t, err) + actual, err := monitors.ExtractMonitors(allPages) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, HealthmonitorWeb, actual[0]) + th.CheckDeepEquals(t, HealthmonitorDb, actual[1]) +} + +func TestCreateHealthmonitor(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleHealthmonitorCreationSuccessfully(t, SingleHealthmonitorBody) + + actual, err := monitors.Create(fake.ServiceClient(), monitors.CreateOpts{ + Type: "HTTP", + Name: "db", + PoolID: "84f1b61f-58c4-45bf-a8a9-2dafb9e5214d", + TenantID: "453105b9-1754-413f-aab1-55f1af620750", + Delay: 20, + Timeout: 10, + MaxRetries: 5, + URLPath: "/check", + ExpectedCodes: "200-299", + }).Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, HealthmonitorDb, *actual) +} + +func TestRequiredCreateOpts(t *testing.T) { + res := monitors.Create(fake.ServiceClient(), monitors.CreateOpts{}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = monitors.Create(fake.ServiceClient(), monitors.CreateOpts{Type: monitors.TypeHTTP}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestGetHealthmonitor(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleHealthmonitorGetSuccessfully(t) + + client := fake.ServiceClient() + actual, err := monitors.Get(client, "5d4b5228-33b0-4e60-b225-9b727c1a20e7").Extract() + if err != nil { + t.Fatalf("Unexpected Get error: %v", err) + } + + th.CheckDeepEquals(t, HealthmonitorDb, *actual) +} + +func TestDeleteHealthmonitor(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleHealthmonitorDeletionSuccessfully(t) + + res := monitors.Delete(fake.ServiceClient(), "5d4b5228-33b0-4e60-b225-9b727c1a20e7") + th.AssertNoErr(t, res.Err) +} + +func TestUpdateHealthmonitor(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleHealthmonitorUpdateSuccessfully(t) + + client := fake.ServiceClient() + actual, err := monitors.Update(client, "5d4b5228-33b0-4e60-b225-9b727c1a20e7", monitors.UpdateOpts{ + Name: "NewHealthmonitorName", + Delay: 3, + Timeout: 20, + MaxRetries: 10, + URLPath: "/another_check", + ExpectedCodes: "301", + }).Extract() + if err != nil { + t.Fatalf("Unexpected Update error: %v", err) + } + + th.CheckDeepEquals(t, HealthmonitorUpdated, *actual) +} + +func TestDelayMustBeGreaterOrEqualThanTimeout(t *testing.T) { + _, err := monitors.Create(fake.ServiceClient(), monitors.CreateOpts{ + Type: "HTTP", + PoolID: "d459f7d8-c6ee-439d-8713-d3fc08aeed8d", + Delay: 1, + Timeout: 10, + MaxRetries: 5, + URLPath: "/check", + ExpectedCodes: "200-299", + }).Extract() + + if err == nil { + t.Fatalf("Expected error, got none") + } + + _, err = monitors.Update(fake.ServiceClient(), "453105b9-1754-413f-aab1-55f1af620750", monitors.UpdateOpts{ + Delay: 1, + Timeout: 10, + }).Extract() + + if err == nil { + t.Fatalf("Expected error, got none") + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors/urls.go new file mode 100644 index 0000000000..a222e52a93 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors/urls.go @@ -0,0 +1,16 @@ +package monitors + +import "github.com/gophercloud/gophercloud" + +const ( + rootPath = "lbaas" + resourcePath = "healthmonitors" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools/requests.go new file mode 100644 index 0000000000..093df6ad0f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools/requests.go @@ -0,0 +1,334 @@ +package pools + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToPoolListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the Pool attributes you want to see returned. SortKey allows you to +// sort by a particular Pool attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + LBMethod string `q:"lb_algorithm"` + Protocol string `q:"protocol"` + TenantID string `q:"tenant_id"` + AdminStateUp *bool `q:"admin_state_up"` + Name string `q:"name"` + ID string `q:"id"` + LoadbalancerID string `q:"loadbalancer_id"` + ListenerID string `q:"listener_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToPoolListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToPoolListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// pools. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those pools that are owned by the +// tenant who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := rootURL(c) + if opts != nil { + query, err := opts.ToPoolListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return PoolPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +type LBMethod string +type Protocol string + +// Supported attributes for create/update operations. +const ( + LBMethodRoundRobin LBMethod = "ROUND_ROBIN" + LBMethodLeastConnections LBMethod = "LEAST_CONNECTIONS" + LBMethodSourceIp LBMethod = "SOURCE_IP" + + ProtocolTCP Protocol = "TCP" + ProtocolHTTP Protocol = "HTTP" + ProtocolHTTPS Protocol = "HTTPS" +) + +// CreateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Create operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type CreateOptsBuilder interface { + ToPoolCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts struct { + // The algorithm used to distribute load between the members of the pool. The + // current specification supports LBMethodRoundRobin, LBMethodLeastConnections + // and LBMethodSourceIp as valid values for this attribute. + LBMethod LBMethod `json:"lb_algorithm" required:"true"` + // The protocol used by the pool members, you can use either + // ProtocolTCP, ProtocolHTTP, or ProtocolHTTPS. + Protocol Protocol `json:"protocol" required:"true"` + // The Loadbalancer on which the members of the pool will be associated with. + // Note: one of LoadbalancerID or ListenerID must be provided. + LoadbalancerID string `json:"loadbalancer_id,omitempty" xor:"ListenerID"` + // The Listener on which the members of the pool will be associated with. + // Note: one of LoadbalancerID or ListenerID must be provided. + ListenerID string `json:"listener_id,omitempty" xor:"LoadbalancerID"` + // Only required if the caller has an admin role and wants to create a pool + // for another tenant. + TenantID string `json:"tenant_id,omitempty"` + // Name of the pool. + Name string `json:"name,omitempty"` + // Human-readable description for the pool. + Description string `json:"description,omitempty"` + // Omit this field to prevent session persistence. + Persistence *SessionPersistence `json:"session_persistence,omitempty"` + // The administrative state of the Pool. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +// ToPoolCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToPoolCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "pool") +} + +// Create accepts a CreateOpts struct and uses the values to create a new +// load balancer pool. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToPoolCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(rootURL(c), b, &r.Body, nil) + return +} + +// Get retrieves a particular pool based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(resourceURL(c, id), &r.Body, nil) + return +} + +// UpdateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Update operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type UpdateOptsBuilder interface { + ToPoolUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts is the common options struct used in this package's Update +// operation. +type UpdateOpts struct { + // Name of the pool. + Name string `json:"name,omitempty"` + // Human-readable description for the pool. + Description string `json:"description,omitempty"` + // The algorithm used to distribute load between the members of the pool. The + // current specification supports LBMethodRoundRobin, LBMethodLeastConnections + // and LBMethodSourceIp as valid values for this attribute. + LBMethod LBMethod `json:"lb_algorithm,omitempty"` + // The administrative state of the Pool. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +// ToPoolUpdateMap casts a UpdateOpts struct to a map. +func (opts UpdateOpts) ToPoolUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "pool") +} + +// Update allows pools to be updated. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToPoolUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete will permanently delete a particular pool based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(resourceURL(c, id), nil) + return +} + +// ListMemberOptsBuilder allows extensions to add additional parameters to the +// ListMembers request. +type ListMembersOptsBuilder interface { + ToMembersListQuery() (string, error) +} + +// ListMembersOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the Member attributes you want to see returned. SortKey allows you to +// sort by a particular Member attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListMembersOpts struct { + Name string `q:"name"` + Weight int `q:"weight"` + AdminStateUp *bool `q:"admin_state_up"` + TenantID string `q:"tenant_id"` + Address string `q:"address"` + ProtocolPort int `q:"protocol_port"` + ID string `q:"id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToMemberListQuery formats a ListOpts into a query string. +func (opts ListMembersOpts) ToMembersListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// ListMembers returns a Pager which allows you to iterate over a collection of +// members. It accepts a ListMembersOptsBuilder, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those members that are owned by the +// tenant who submits the request, unless an admin user submits the request. +func ListMembers(c *gophercloud.ServiceClient, poolID string, opts ListMembersOptsBuilder) pagination.Pager { + url := memberRootURL(c, poolID) + if opts != nil { + query, err := opts.ToMembersListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return MemberPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateMemberOptsBuilder is the interface options structs have to satisfy in order +// to be used in the CreateMember operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type CreateMemberOptsBuilder interface { + ToMemberCreateMap() (map[string]interface{}, error) +} + +// CreateMemberOpts is the common options struct used in this package's CreateMember +// operation. +type CreateMemberOpts struct { + // Required. The IP address of the member to receive traffic from the load balancer. + Address string `json:"address" required:"true"` + // Required. The port on which to listen for client traffic. + ProtocolPort int `json:"protocol_port" required:"true"` + // Optional. Name of the Member. + Name string `json:"name,omitempty"` + // Only required if the caller has an admin role and wants to create a Member + // for another tenant. + TenantID string `json:"tenant_id,omitempty"` + // Optional. A positive integer value that indicates the relative portion of + // traffic that this member should receive from the pool. For example, a + // member with a weight of 10 receives five times as much traffic as a member + // with a weight of 2. + Weight int `json:"weight,omitempty"` + // Optional. If you omit this parameter, LBaaS uses the vip_subnet_id + // parameter value for the subnet UUID. + SubnetID string `json:"subnet_id,omitempty"` + // Optional. The administrative state of the Pool. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +// ToMemberCreateMap casts a CreateOpts struct to a map. +func (opts CreateMemberOpts) ToMemberCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "member") +} + +// CreateMember will create and associate a Member with a particular Pool. +func CreateMember(c *gophercloud.ServiceClient, poolID string, opts CreateMemberOpts) (r CreateMemberResult) { + b, err := opts.ToMemberCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(memberRootURL(c, poolID), b, &r.Body, nil) + return +} + +// GetMember retrieves a particular Pool Member based on its unique ID. +func GetMember(c *gophercloud.ServiceClient, poolID string, memberID string) (r GetMemberResult) { + _, r.Err = c.Get(memberResourceURL(c, poolID, memberID), &r.Body, nil) + return +} + +// MemberUpdateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Update operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type UpdateMemberOptsBuilder interface { + ToMemberUpdateMap() (map[string]interface{}, error) +} + +// UpdateMemberOpts is the common options struct used in this package's Update +// operation. +type UpdateMemberOpts struct { + // Optional. Name of the Member. + Name string `json:"name,omitempty"` + // Optional. A positive integer value that indicates the relative portion of + // traffic that this member should receive from the pool. For example, a + // member with a weight of 10 receives five times as much traffic as a member + // with a weight of 2. + Weight int `json:"weight,omitempty"` + // Optional. The administrative state of the Pool. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +// ToMemberUpdateMap casts a UpdateOpts struct to a map. +func (opts UpdateMemberOpts) ToMemberUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "member") +} + +// Update allows Member to be updated. +func UpdateMember(c *gophercloud.ServiceClient, poolID string, memberID string, opts UpdateMemberOptsBuilder) (r UpdateMemberResult) { + b, err := opts.ToMemberUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(memberResourceURL(c, poolID, memberID), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201, 202}, + }) + return +} + +// DisassociateMember will remove and disassociate a Member from a particular Pool. +func DeleteMember(c *gophercloud.ServiceClient, poolID string, memberID string) (r DeleteMemberResult) { + _, r.Err = c.Delete(memberResourceURL(c, poolID, memberID), nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools/results.go new file mode 100644 index 0000000000..0e0bf366b7 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools/results.go @@ -0,0 +1,242 @@ +package pools + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors" + "github.com/gophercloud/gophercloud/pagination" +) + +// SessionPersistence represents the session persistence feature of the load +// balancing service. It attempts to force connections or requests in the same +// session to be processed by the same member as long as it is ative. Three +// types of persistence are supported: +// +// SOURCE_IP: With this mode, all connections originating from the same source +// IP address, will be handled by the same Member of the Pool. +// HTTP_COOKIE: With this persistence mode, the load balancing function will +// create a cookie on the first request from a client. Subsequent +// requests containing the same cookie value will be handled by +// the same Member of the Pool. +// APP_COOKIE: With this persistence mode, the load balancing function will +// rely on a cookie established by the backend application. All +// requests carrying the same cookie value will be handled by the +// same Member of the Pool. +type SessionPersistence struct { + // The type of persistence mode + Type string `json:"type"` + + // Name of cookie if persistence mode is set appropriately + CookieName string `json:"cookie_name,omitempty"` +} + +type LoadBalancerID struct { + ID string `json:"id"` +} + +type ListenerID struct { + ID string `json:"id"` +} + +// Pool represents a logical set of devices, such as web servers, that you +// group together to receive and process traffic. The load balancing function +// chooses a Member of the Pool according to the configured load balancing +// method to handle the new requests or connections received on the VIP address. +type Pool struct { + // The load-balancer algorithm, which is round-robin, least-connections, and + // so on. This value, which must be supported, is dependent on the provider. + // Round-robin must be supported. + LBMethod string `json:"lb_algorithm"` + // The protocol of the Pool, which is TCP, HTTP, or HTTPS. + Protocol string `json:"protocol"` + // Description for the Pool. + Description string `json:"description"` + // A list of listeners objects IDs. + Listeners []ListenerID `json:"listeners"` //[]map[string]interface{} + // A list of member objects IDs. + Members []Member `json:"members"` + // The ID of associated health monitor. + MonitorID string `json:"healthmonitor_id"` + // The network on which the members of the Pool will be located. Only members + // that are on this network can be added to the Pool. + SubnetID string `json:"subnet_id"` + // Owner of the Pool. Only an administrative user can specify a tenant ID + // other than its own. + TenantID string `json:"tenant_id"` + // The administrative state of the Pool, which is up (true) or down (false). + AdminStateUp bool `json:"admin_state_up"` + // Pool name. Does not have to be unique. + Name string `json:"name"` + // The unique ID for the Pool. + ID string `json:"id"` + // A list of load balancer objects IDs. + Loadbalancers []LoadBalancerID `json:"loadbalancers"` + // Indicates whether connections in the same session will be processed by the + // same Pool member or not. + Persistence SessionPersistence `json:"session_persistence"` + // The provider + Provider string `json:"provider"` + Monitor monitors.Monitor `json:"healthmonitor"` +} + +// PoolPage is the page returned by a pager when traversing over a +// collection of pools. +type PoolPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of pools has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r PoolPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"pools_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a PoolPage struct is empty. +func (r PoolPage) IsEmpty() (bool, error) { + is, err := ExtractPools(r) + return len(is) == 0, err +} + +// ExtractPools accepts a Page struct, specifically a PoolPage struct, +// and extracts the elements into a slice of Router structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractPools(r pagination.Page) ([]Pool, error) { + var s struct { + Pools []Pool `json:"pools"` + } + err := (r.(PoolPage)).ExtractInto(&s) + return s.Pools, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a router. +func (r commonResult) Extract() (*Pool, error) { + var s struct { + Pool *Pool `json:"pool"` + } + err := r.ExtractInto(&s) + return s.Pool, err +} + +// CreateResult represents the result of a Create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a Get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an Update operation. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a Delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// Member represents the application running on a backend server. +type Member struct { + // Name of the Member. + Name string `json:"name"` + // Weight of Member. + Weight int `json:"weight"` + // The administrative state of the member, which is up (true) or down (false). + AdminStateUp bool `json:"admin_state_up"` + // Owner of the Member. Only an administrative user can specify a tenant ID + // other than its own. + TenantID string `json:"tenant_id"` + // parameter value for the subnet UUID. + SubnetID string `json:"subnet_id"` + // The Pool to which the Member belongs. + PoolID string `json:"pool_id"` + // The IP address of the Member. + Address string `json:"address"` + // The port on which the application is hosted. + ProtocolPort int `json:"protocol_port"` + // The unique ID for the Member. + ID string `json:"id"` +} + +// MemberPage is the page returned by a pager when traversing over a +// collection of Members in a Pool. +type MemberPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of members has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r MemberPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"members_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a MemberPage struct is empty. +func (r MemberPage) IsEmpty() (bool, error) { + is, err := ExtractMembers(r) + return len(is) == 0, err +} + +// ExtractMembers accepts a Page struct, specifically a RouterPage struct, +// and extracts the elements into a slice of Router structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractMembers(r pagination.Page) ([]Member, error) { + var s struct { + Members []Member `json:"members"` + } + err := (r.(MemberPage)).ExtractInto(&s) + return s.Members, err +} + +type commonMemberResult struct { + gophercloud.Result +} + +// ExtractMember is a function that accepts a result and extracts a router. +func (r commonMemberResult) Extract() (*Member, error) { + var s struct { + Member *Member `json:"member"` + } + err := r.ExtractInto(&s) + return s.Member, err +} + +// CreateMemberResult represents the result of a CreateMember operation. +type CreateMemberResult struct { + commonMemberResult +} + +// GetMemberResult represents the result of a GetMember operation. +type GetMemberResult struct { + commonMemberResult +} + +// UpdateMemberResult represents the result of an UpdateMember operation. +type UpdateMemberResult struct { + commonMemberResult +} + +// DeleteMemberResult represents the result of a DeleteMember operation. +type DeleteMemberResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools/testing/doc.go new file mode 100644 index 0000000000..65eb52174c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools/testing/doc.go @@ -0,0 +1,2 @@ +// networking_extensions_lbaas_v2_pools_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools/testing/fixtures.go new file mode 100644 index 0000000000..df9d1fd05c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools/testing/fixtures.go @@ -0,0 +1,388 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +// PoolsListBody contains the canned body of a pool list response. +const PoolsListBody = ` +{ + "pools":[ + { + "lb_algorithm":"ROUND_ROBIN", + "protocol":"HTTP", + "description":"", + "healthmonitor_id": "466c8345-28d8-4f84-a246-e04380b0461d", + "members":[{"id": "53306cda-815d-4354-9fe4-59e09da9c3c5"}], + "listeners":[{"id": "2a280670-c202-4b0b-a562-34077415aabf"}], + "loadbalancers":[{"id": "79e05663-7f03-45d2-a092-8b94062f22ab"}], + "id":"72741b06-df4d-4715-b142-276b6bce75ab", + "name":"web", + "admin_state_up":true, + "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea", + "provider": "haproxy" + }, + { + "lb_algorithm":"LEAST_CONNECTION", + "protocol":"HTTP", + "description":"", + "healthmonitor_id": "5f6c8345-28d8-4f84-a246-e04380b0461d", + "members":[{"id": "67306cda-815d-4354-9fe4-59e09da9c3c5"}], + "listeners":[{"id": "2a280670-c202-4b0b-a562-34077415aabf"}], + "loadbalancers":[{"id": "79e05663-7f03-45d2-a092-8b94062f22ab"}], + "id":"c3741b06-df4d-4715-b142-276b6bce75ab", + "name":"db", + "admin_state_up":true, + "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea", + "provider": "haproxy" + } + ] +} +` + +// SinglePoolBody is the canned body of a Get request on an existing pool. +const SinglePoolBody = ` +{ + "pool": { + "lb_algorithm":"LEAST_CONNECTION", + "protocol":"HTTP", + "description":"", + "healthmonitor_id": "5f6c8345-28d8-4f84-a246-e04380b0461d", + "members":[{"id": "67306cda-815d-4354-9fe4-59e09da9c3c5"}], + "listeners":[{"id": "2a280670-c202-4b0b-a562-34077415aabf"}], + "loadbalancers":[{"id": "79e05663-7f03-45d2-a092-8b94062f22ab"}], + "id":"c3741b06-df4d-4715-b142-276b6bce75ab", + "name":"db", + "admin_state_up":true, + "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea", + "provider": "haproxy" + } +} +` + +// PostUpdatePoolBody is the canned response body of a Update request on an existing pool. +const PostUpdatePoolBody = ` +{ + "pool": { + "lb_algorithm":"LEAST_CONNECTION", + "protocol":"HTTP", + "description":"", + "healthmonitor_id": "5f6c8345-28d8-4f84-a246-e04380b0461d", + "members":[{"id": "67306cda-815d-4354-9fe4-59e09da9c3c5"}], + "listeners":[{"id": "2a280670-c202-4b0b-a562-34077415aabf"}], + "loadbalancers":[{"id": "79e05663-7f03-45d2-a092-8b94062f22ab"}], + "id":"c3741b06-df4d-4715-b142-276b6bce75ab", + "name":"db", + "admin_state_up":true, + "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea", + "provider": "haproxy" + } +} +` + +var ( + PoolWeb = pools.Pool{ + LBMethod: "ROUND_ROBIN", + Protocol: "HTTP", + Description: "", + MonitorID: "466c8345-28d8-4f84-a246-e04380b0461d", + TenantID: "83657cfcdfe44cd5920adaf26c48ceea", + AdminStateUp: true, + Name: "web", + Members: []pools.Member{{ID: "53306cda-815d-4354-9fe4-59e09da9c3c5"}}, + ID: "72741b06-df4d-4715-b142-276b6bce75ab", + Loadbalancers: []pools.LoadBalancerID{{ID: "79e05663-7f03-45d2-a092-8b94062f22ab"}}, + Listeners: []pools.ListenerID{{ID: "2a280670-c202-4b0b-a562-34077415aabf"}}, + Provider: "haproxy", + } + PoolDb = pools.Pool{ + LBMethod: "LEAST_CONNECTION", + Protocol: "HTTP", + Description: "", + MonitorID: "5f6c8345-28d8-4f84-a246-e04380b0461d", + TenantID: "83657cfcdfe44cd5920adaf26c48ceea", + AdminStateUp: true, + Name: "db", + Members: []pools.Member{{ID: "67306cda-815d-4354-9fe4-59e09da9c3c5"}}, + ID: "c3741b06-df4d-4715-b142-276b6bce75ab", + Loadbalancers: []pools.LoadBalancerID{{ID: "79e05663-7f03-45d2-a092-8b94062f22ab"}}, + Listeners: []pools.ListenerID{{ID: "2a280670-c202-4b0b-a562-34077415aabf"}}, + Provider: "haproxy", + } + PoolUpdated = pools.Pool{ + LBMethod: "LEAST_CONNECTION", + Protocol: "HTTP", + Description: "", + MonitorID: "5f6c8345-28d8-4f84-a246-e04380b0461d", + TenantID: "83657cfcdfe44cd5920adaf26c48ceea", + AdminStateUp: true, + Name: "db", + Members: []pools.Member{{ID: "67306cda-815d-4354-9fe4-59e09da9c3c5"}}, + ID: "c3741b06-df4d-4715-b142-276b6bce75ab", + Loadbalancers: []pools.LoadBalancerID{{ID: "79e05663-7f03-45d2-a092-8b94062f22ab"}}, + Listeners: []pools.ListenerID{{ID: "2a280670-c202-4b0b-a562-34077415aabf"}}, + Provider: "haproxy", + } +) + +// HandlePoolListSuccessfully sets up the test server to respond to a pool List request. +func HandlePoolListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/v2.0/lbaas/pools", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, PoolsListBody) + case "45e08a3e-a78f-4b40-a229-1e7e23eee1ab": + fmt.Fprintf(w, `{ "pools": [] }`) + default: + t.Fatalf("/v2.0/lbaas/pools invoked with unexpected marker=[%s]", marker) + } + }) +} + +// HandlePoolCreationSuccessfully sets up the test server to respond to a pool creation request +// with a given response. +func HandlePoolCreationSuccessfully(t *testing.T, response string) { + th.Mux.HandleFunc("/v2.0/lbaas/pools", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ + "pool": { + "lb_algorithm": "ROUND_ROBIN", + "protocol": "HTTP", + "name": "Example pool", + "tenant_id": "2ffc6e22aae24e4795f87155d24c896f", + "loadbalancer_id": "79e05663-7f03-45d2-a092-8b94062f22ab" + } + }`) + + w.WriteHeader(http.StatusAccepted) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, response) + }) +} + +// HandlePoolGetSuccessfully sets up the test server to respond to a pool Get request. +func HandlePoolGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/v2.0/lbaas/pools/c3741b06-df4d-4715-b142-276b6bce75ab", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + fmt.Fprintf(w, SinglePoolBody) + }) +} + +// HandlePoolDeletionSuccessfully sets up the test server to respond to a pool deletion request. +func HandlePoolDeletionSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/v2.0/lbaas/pools/c3741b06-df4d-4715-b142-276b6bce75ab", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandlePoolUpdateSuccessfully sets up the test server to respond to a pool Update request. +func HandlePoolUpdateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/v2.0/lbaas/pools/c3741b06-df4d-4715-b142-276b6bce75ab", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestJSONRequest(t, r, `{ + "pool": { + "name": "NewPoolName", + "lb_algorithm": "LEAST_CONNECTIONS" + } + }`) + + fmt.Fprintf(w, PostUpdatePoolBody) + }) +} + +// MembersListBody contains the canned body of a member list response. +const MembersListBody = ` +{ + "members":[ + { + "id": "2a280670-c202-4b0b-a562-34077415aabf", + "address": "10.0.2.10", + "weight": 5, + "name": "web", + "subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9", + "tenant_id": "2ffc6e22aae24e4795f87155d24c896f", + "admin_state_up":true, + "protocol_port": 80 + }, + { + "id": "fad389a3-9a4a-4762-a365-8c7038508b5d", + "address": "10.0.2.11", + "weight": 10, + "name": "db", + "subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9", + "tenant_id": "2ffc6e22aae24e4795f87155d24c896f", + "admin_state_up":false, + "protocol_port": 80 + } + ] +} +` + +// SingleMemberBody is the canned body of a Get request on an existing member. +const SingleMemberBody = ` +{ + "member": { + "id": "fad389a3-9a4a-4762-a365-8c7038508b5d", + "address": "10.0.2.11", + "weight": 10, + "name": "db", + "subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9", + "tenant_id": "2ffc6e22aae24e4795f87155d24c896f", + "admin_state_up":false, + "protocol_port": 80 + } +} +` + +// PostUpdateMemberBody is the canned response body of a Update request on an existing member. +const PostUpdateMemberBody = ` +{ + "member": { + "id": "fad389a3-9a4a-4762-a365-8c7038508b5d", + "address": "10.0.2.11", + "weight": 10, + "name": "db", + "subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9", + "tenant_id": "2ffc6e22aae24e4795f87155d24c896f", + "admin_state_up":false, + "protocol_port": 80 + } +} +` + +var ( + MemberWeb = pools.Member{ + SubnetID: "1981f108-3c48-48d2-b908-30f7d28532c9", + TenantID: "2ffc6e22aae24e4795f87155d24c896f", + AdminStateUp: true, + Name: "web", + ID: "2a280670-c202-4b0b-a562-34077415aabf", + Address: "10.0.2.10", + Weight: 5, + ProtocolPort: 80, + } + MemberDb = pools.Member{ + SubnetID: "1981f108-3c48-48d2-b908-30f7d28532c9", + TenantID: "2ffc6e22aae24e4795f87155d24c896f", + AdminStateUp: false, + Name: "db", + ID: "fad389a3-9a4a-4762-a365-8c7038508b5d", + Address: "10.0.2.11", + Weight: 10, + ProtocolPort: 80, + } + MemberUpdated = pools.Member{ + SubnetID: "1981f108-3c48-48d2-b908-30f7d28532c9", + TenantID: "2ffc6e22aae24e4795f87155d24c896f", + AdminStateUp: false, + Name: "db", + ID: "fad389a3-9a4a-4762-a365-8c7038508b5d", + Address: "10.0.2.11", + Weight: 10, + ProtocolPort: 80, + } +) + +// HandleMemberListSuccessfully sets up the test server to respond to a member List request. +func HandleMemberListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/v2.0/lbaas/pools/332abe93-f488-41ba-870b-2ac66be7f853/members", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, MembersListBody) + case "45e08a3e-a78f-4b40-a229-1e7e23eee1ab": + fmt.Fprintf(w, `{ "members": [] }`) + default: + t.Fatalf("/v2.0/lbaas/pools/332abe93-f488-41ba-870b-2ac66be7f853/members invoked with unexpected marker=[%s]", marker) + } + }) +} + +// HandleMemberCreationSuccessfully sets up the test server to respond to a member creation request +// with a given response. +func HandleMemberCreationSuccessfully(t *testing.T, response string) { + th.Mux.HandleFunc("/v2.0/lbaas/pools/332abe93-f488-41ba-870b-2ac66be7f853/members", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ + "member": { + "address": "10.0.2.11", + "weight": 10, + "name": "db", + "subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9", + "tenant_id": "2ffc6e22aae24e4795f87155d24c896f", + "protocol_port": 80 + } + }`) + + w.WriteHeader(http.StatusAccepted) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, response) + }) +} + +// HandleMemberGetSuccessfully sets up the test server to respond to a member Get request. +func HandleMemberGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/v2.0/lbaas/pools/332abe93-f488-41ba-870b-2ac66be7f853/members/2a280670-c202-4b0b-a562-34077415aabf", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + fmt.Fprintf(w, SingleMemberBody) + }) +} + +// HandleMemberDeletionSuccessfully sets up the test server to respond to a member deletion request. +func HandleMemberDeletionSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/v2.0/lbaas/pools/332abe93-f488-41ba-870b-2ac66be7f853/members/2a280670-c202-4b0b-a562-34077415aabf", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleMemberUpdateSuccessfully sets up the test server to respond to a member Update request. +func HandleMemberUpdateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/v2.0/lbaas/pools/332abe93-f488-41ba-870b-2ac66be7f853/members/2a280670-c202-4b0b-a562-34077415aabf", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestJSONRequest(t, r, `{ + "member": { + "name": "newMemberName", + "weight": 4 + } + }`) + + fmt.Fprintf(w, PostUpdateMemberBody) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools/testing/requests_test.go new file mode 100644 index 0000000000..4af00ecc25 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools/testing/requests_test.go @@ -0,0 +1,262 @@ +package testing + +import ( + "testing" + + fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestListPools(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandlePoolListSuccessfully(t) + + pages := 0 + err := pools.List(fake.ServiceClient(), pools.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := pools.ExtractPools(page) + if err != nil { + return false, err + } + + if len(actual) != 2 { + t.Fatalf("Expected 2 pools, got %d", len(actual)) + } + th.CheckDeepEquals(t, PoolWeb, actual[0]) + th.CheckDeepEquals(t, PoolDb, actual[1]) + + return true, nil + }) + + th.AssertNoErr(t, err) + + if pages != 1 { + t.Errorf("Expected 1 page, saw %d", pages) + } +} + +func TestListAllPools(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandlePoolListSuccessfully(t) + + allPages, err := pools.List(fake.ServiceClient(), pools.ListOpts{}).AllPages() + th.AssertNoErr(t, err) + actual, err := pools.ExtractPools(allPages) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, PoolWeb, actual[0]) + th.CheckDeepEquals(t, PoolDb, actual[1]) +} + +func TestCreatePool(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandlePoolCreationSuccessfully(t, SinglePoolBody) + + actual, err := pools.Create(fake.ServiceClient(), pools.CreateOpts{ + LBMethod: pools.LBMethodRoundRobin, + Protocol: "HTTP", + Name: "Example pool", + TenantID: "2ffc6e22aae24e4795f87155d24c896f", + LoadbalancerID: "79e05663-7f03-45d2-a092-8b94062f22ab", + }).Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, PoolDb, *actual) +} + +func TestGetPool(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandlePoolGetSuccessfully(t) + + client := fake.ServiceClient() + actual, err := pools.Get(client, "c3741b06-df4d-4715-b142-276b6bce75ab").Extract() + if err != nil { + t.Fatalf("Unexpected Get error: %v", err) + } + + th.CheckDeepEquals(t, PoolDb, *actual) +} + +func TestDeletePool(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandlePoolDeletionSuccessfully(t) + + res := pools.Delete(fake.ServiceClient(), "c3741b06-df4d-4715-b142-276b6bce75ab") + th.AssertNoErr(t, res.Err) +} + +func TestUpdatePool(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandlePoolUpdateSuccessfully(t) + + client := fake.ServiceClient() + actual, err := pools.Update(client, "c3741b06-df4d-4715-b142-276b6bce75ab", pools.UpdateOpts{ + Name: "NewPoolName", + LBMethod: pools.LBMethodLeastConnections, + }).Extract() + if err != nil { + t.Fatalf("Unexpected Update error: %v", err) + } + + th.CheckDeepEquals(t, PoolUpdated, *actual) +} + +func TestRequiredPoolCreateOpts(t *testing.T) { + res := pools.Create(fake.ServiceClient(), pools.CreateOpts{}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = pools.Create(fake.ServiceClient(), pools.CreateOpts{ + LBMethod: pools.LBMethod("invalid"), + Protocol: pools.ProtocolHTTPS, + LoadbalancerID: "69055154-f603-4a28-8951-7cc2d9e54a9a", + }) + if res.Err == nil { + t.Fatalf("Expected error, but got none") + } + + res = pools.Create(fake.ServiceClient(), pools.CreateOpts{ + LBMethod: pools.LBMethodRoundRobin, + Protocol: pools.Protocol("invalid"), + LoadbalancerID: "69055154-f603-4a28-8951-7cc2d9e54a9a", + }) + if res.Err == nil { + t.Fatalf("Expected error, but got none") + } + + res = pools.Create(fake.ServiceClient(), pools.CreateOpts{ + LBMethod: pools.LBMethodRoundRobin, + Protocol: pools.ProtocolHTTPS, + }) + if res.Err == nil { + t.Fatalf("Expected error, but got none") + } +} + +func TestListMembers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleMemberListSuccessfully(t) + + pages := 0 + err := pools.ListMembers(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", pools.ListMembersOpts{}).EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := pools.ExtractMembers(page) + if err != nil { + return false, err + } + + if len(actual) != 2 { + t.Fatalf("Expected 2 members, got %d", len(actual)) + } + th.CheckDeepEquals(t, MemberWeb, actual[0]) + th.CheckDeepEquals(t, MemberDb, actual[1]) + + return true, nil + }) + + th.AssertNoErr(t, err) + + if pages != 1 { + t.Errorf("Expected 1 page, saw %d", pages) + } +} + +func TestListAllMembers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleMemberListSuccessfully(t) + + allPages, err := pools.ListMembers(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", pools.ListMembersOpts{}).AllPages() + th.AssertNoErr(t, err) + actual, err := pools.ExtractMembers(allPages) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, MemberWeb, actual[0]) + th.CheckDeepEquals(t, MemberDb, actual[1]) +} + +func TestCreateMember(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleMemberCreationSuccessfully(t, SingleMemberBody) + + actual, err := pools.CreateMember(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", pools.CreateMemberOpts{ + Name: "db", + SubnetID: "1981f108-3c48-48d2-b908-30f7d28532c9", + TenantID: "2ffc6e22aae24e4795f87155d24c896f", + Address: "10.0.2.11", + ProtocolPort: 80, + Weight: 10, + }).Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, MemberDb, *actual) +} + +func TestRequiredMemberCreateOpts(t *testing.T) { + res := pools.CreateMember(fake.ServiceClient(), "", pools.CreateMemberOpts{}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = pools.CreateMember(fake.ServiceClient(), "", pools.CreateMemberOpts{Address: "1.2.3.4", ProtocolPort: 80}) + if res.Err == nil { + t.Fatalf("Expected error, but got none") + } + res = pools.CreateMember(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", pools.CreateMemberOpts{ProtocolPort: 80}) + if res.Err == nil { + t.Fatalf("Expected error, but got none") + } + res = pools.CreateMember(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", pools.CreateMemberOpts{Address: "1.2.3.4"}) + if res.Err == nil { + t.Fatalf("Expected error, but got none") + } +} + +func TestGetMember(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleMemberGetSuccessfully(t) + + client := fake.ServiceClient() + actual, err := pools.GetMember(client, "332abe93-f488-41ba-870b-2ac66be7f853", "2a280670-c202-4b0b-a562-34077415aabf").Extract() + if err != nil { + t.Fatalf("Unexpected Get error: %v", err) + } + + th.CheckDeepEquals(t, MemberDb, *actual) +} + +func TestDeleteMember(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleMemberDeletionSuccessfully(t) + + res := pools.DeleteMember(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", "2a280670-c202-4b0b-a562-34077415aabf") + th.AssertNoErr(t, res.Err) +} + +func TestUpdateMember(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleMemberUpdateSuccessfully(t) + + client := fake.ServiceClient() + actual, err := pools.UpdateMember(client, "332abe93-f488-41ba-870b-2ac66be7f853", "2a280670-c202-4b0b-a562-34077415aabf", pools.UpdateMemberOpts{ + Name: "newMemberName", + Weight: 4, + }).Extract() + if err != nil { + t.Fatalf("Unexpected Update error: %v", err) + } + + th.CheckDeepEquals(t, MemberUpdated, *actual) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools/urls.go new file mode 100644 index 0000000000..bceca67707 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools/urls.go @@ -0,0 +1,25 @@ +package pools + +import "github.com/gophercloud/gophercloud" + +const ( + rootPath = "lbaas" + resourcePath = "pools" + memberPath = "members" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} + +func memberRootURL(c *gophercloud.ServiceClient, poolId string) string { + return c.ServiceURL(rootPath, resourcePath, poolId, memberPath) +} + +func memberResourceURL(c *gophercloud.ServiceClient, poolID string, memeberID string) string { + return c.ServiceURL(rootPath, resourcePath, poolID, memberPath, memeberID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/portsbinding/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/portsbinding/doc.go new file mode 100644 index 0000000000..0d2ed5897a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/portsbinding/doc.go @@ -0,0 +1,3 @@ +// Package portsbinding provides information and interaction with the port +// binding extension for the OpenStack Networking service. +package portsbinding diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/portsbinding/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/portsbinding/requests.go new file mode 100644 index 0000000000..b46172be50 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/portsbinding/requests.go @@ -0,0 +1,113 @@ +package portsbinding + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/networking/v2/ports" +) + +// Get retrieves a specific port based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(getURL(c, id), &r.Body, nil) + return +} + +// CreateOpts represents the attributes used when creating a new +// port with extended attributes. +type CreateOpts struct { + // CreateOptsBuilder is the interface options structs have to satisfy in order + // to be used in the main Create operation in this package. + ports.CreateOptsBuilder `json:"-"` + // The ID of the host where the port is allocated + HostID string `json:"binding:host_id,omitempty"` + // The virtual network interface card (vNIC) type that is bound to the + // neutron port + VNICType string `json:"binding:vnic_type,omitempty"` + // A dictionary that enables the application running on the specified + // host to pass and receive virtual network interface (VIF) port-specific + // information to the plug-in + Profile map[string]string `json:"binding:profile,omitempty"` +} + +// ToPortCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToPortCreateMap() (map[string]interface{}, error) { + b1, err := opts.CreateOptsBuilder.ToPortCreateMap() + if err != nil { + return nil, err + } + + b2, err := gophercloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + port := b1["port"].(map[string]interface{}) + + for k, v := range b2 { + port[k] = v + } + + return map[string]interface{}{"port": port}, nil +} + +// Create accepts a CreateOpts struct and creates a new port with extended attributes. +// You must remember to provide a NetworkID value. +func Create(c *gophercloud.ServiceClient, opts ports.CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToPortCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(createURL(c), b, &r.Body, nil) + return +} + +// UpdateOpts represents the attributes used when updating an existing port. +type UpdateOpts struct { + // UpdateOptsBuilder is the interface options structs have to satisfy in order + // to be used in the main Update operation in this package. + ports.UpdateOptsBuilder `json:"-"` + // The ID of the host where the port is allocated + HostID string `json:"binding:host_id,omitempty"` + // The virtual network interface card (vNIC) type that is bound to the + // neutron port + VNICType string `json:"binding:vnic_type,omitempty"` + // A dictionary that enables the application running on the specified + // host to pass and receive virtual network interface (VIF) port-specific + // information to the plug-in + Profile map[string]string `json:"binding:profile,omitempty"` +} + +// ToPortUpdateMap casts an UpdateOpts struct to a map. +func (opts UpdateOpts) ToPortUpdateMap() (map[string]interface{}, error) { + b1, err := opts.UpdateOptsBuilder.ToPortUpdateMap() + if err != nil { + return nil, err + } + + b2, err := gophercloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + port := b1["port"].(map[string]interface{}) + + for k, v := range b2 { + port[k] = v + } + + return map[string]interface{}{"port": port}, nil +} + +// Update accepts a UpdateOpts struct and updates an existing port using the +// values provided. +func Update(c *gophercloud.ServiceClient, id string, opts ports.UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToPortUpdateMap() + if err != nil { + r.Err = err + return r + } + _, r.Err = c.Put(updateURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/portsbinding/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/portsbinding/results.go new file mode 100644 index 0000000000..952747358c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/portsbinding/results.go @@ -0,0 +1,73 @@ +package portsbinding + +import ( + "github.com/gophercloud/gophercloud" + + "github.com/gophercloud/gophercloud/openstack/networking/v2/ports" + "github.com/gophercloud/gophercloud/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a port resource. +func (r commonResult) Extract() (*Port, error) { + var s struct { + Port *Port `json:"port"` + } + err := r.ExtractInto(&s) + return s.Port, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + commonResult +} + +// IP is a sub-struct that represents an individual IP. +type IP struct { + SubnetID string `json:"subnet_id"` + IPAddress string `json:"ip_address"` +} + +// Port represents a Neutron port. See package documentation for a top-level +// description of what this is. +type Port struct { + ports.Port + // The ID of the host where the port is allocated + HostID string `json:"binding:host_id"` + // A dictionary that enables the application to pass information about + // functions that the Networking API provides. + VIFDetails map[string]interface{} `json:"binding:vif_details"` + // The VIF type for the port. + VIFType string `json:"binding:vif_type"` + // The virtual network interface card (vNIC) type that is bound to the + // neutron port + VNICType string `json:"binding:vnic_type"` + // A dictionary that enables the application running on the specified + // host to pass and receive virtual network interface (VIF) port-specific + // information to the plug-in + Profile map[string]string `json:"binding:profile"` +} + +// ExtractPorts accepts a Page struct, specifically a PortPage struct, +// and extracts the elements into a slice of Port structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractPorts(r pagination.Page) ([]Port, error) { + var s struct { + Ports []Port `json:"ports"` + } + err := (r.(ports.PortPage)).ExtractInto(&s) + return s.Ports, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/portsbinding/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/portsbinding/testing/doc.go new file mode 100644 index 0000000000..deb52b1380 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/portsbinding/testing/doc.go @@ -0,0 +1,2 @@ +// networking_extensions_portsbinding_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/portsbinding/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/portsbinding/testing/fixtures.go new file mode 100644 index 0000000000..f688c207cd --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/portsbinding/testing/fixtures.go @@ -0,0 +1,207 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func HandleListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/v2.0/ports", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "ports": [ + { + "status": "ACTIVE", + "binding:host_id": "devstack", + "name": "", + "admin_state_up": true, + "network_id": "70c1db1f-b701-45bd-96e0-a313ee3430b3", + "tenant_id": "", + "device_owner": "network:router_gateway", + "mac_address": "fa:16:3e:58:42:ed", + "fixed_ips": [ + { + "subnet_id": "008ba151-0b8c-4a67-98b5-0d2b87666062", + "ip_address": "172.24.4.2" + } + ], + "id": "d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b", + "security_groups": [], + "device_id": "9ae135f4-b6e0-4dad-9e91-3c223e385824", + "binding:vnic_type": "normal" + } + ] +} + `) + }) +} + +func HandleGet(t *testing.T) { + th.Mux.HandleFunc("/v2.0/ports/46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "port": { + "status": "ACTIVE", + "binding:host_id": "devstack", + "name": "", + "allowed_address_pairs": [], + "admin_state_up": true, + "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7", + "tenant_id": "7e02058126cc4950b75f9970368ba177", + "extra_dhcp_opts": [], + "binding:vif_details": { + "port_filter": true, + "ovs_hybrid_plug": true + }, + "binding:vif_type": "ovs", + "device_owner": "network:router_interface", + "port_security_enabled": false, + "mac_address": "fa:16:3e:23:fd:d7", + "binding:profile": {}, + "binding:vnic_type": "normal", + "fixed_ips": [ + { + "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2", + "ip_address": "10.0.0.1" + } + ], + "id": "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2", + "security_groups": [], + "device_id": "5e3898d7-11be-483e-9732-b2f5eccd2b2e" + } +} + `) + }) +} + +func HandleCreate(t *testing.T) { + th.Mux.HandleFunc("/v2.0/ports", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "port": { + "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7", + "name": "private-port", + "admin_state_up": true, + "fixed_ips": [ + { + "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2", + "ip_address": "10.0.0.2" + } + ], + "security_groups": ["foo"], + "binding:host_id": "HOST1", + "binding:vnic_type": "normal" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "port": { + "status": "DOWN", + "name": "private-port", + "allowed_address_pairs": [], + "admin_state_up": true, + "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7", + "tenant_id": "d6700c0c9ffa4f1cb322cd4a1f3906fa", + "device_owner": "", + "mac_address": "fa:16:3e:c9:cb:f0", + "fixed_ips": [ + { + "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2", + "ip_address": "10.0.0.2" + } + ], + "binding:host_id": "HOST1", + "binding:vnic_type": "normal", + "id": "65c0ee9f-d634-4522-8954-51021b570b0d", + "security_groups": [ + "f0ac4394-7e4a-4409-9701-ba8be283dbc3" + ], + "device_id": "" + } +} + `) + }) +} + +func HandleUpdate(t *testing.T) { + th.Mux.HandleFunc("/v2.0/ports/65c0ee9f-d634-4522-8954-51021b570b0d", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "port": { + "name": "new_port_name", + "fixed_ips": [ + { + "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2", + "ip_address": "10.0.0.3" + } + ], + "security_groups": [ + "f0ac4394-7e4a-4409-9701-ba8be283dbc3" + ], + "binding:host_id": "HOST1", + "binding:vnic_type": "normal", + "allowed_address_pairs": null + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "port": { + "status": "DOWN", + "name": "new_port_name", + "admin_state_up": true, + "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7", + "tenant_id": "d6700c0c9ffa4f1cb322cd4a1f3906fa", + "device_owner": "", + "mac_address": "fa:16:3e:c9:cb:f0", + "fixed_ips": [ + { + "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2", + "ip_address": "10.0.0.3" + } + ], + "id": "65c0ee9f-d634-4522-8954-51021b570b0d", + "security_groups": [ + "f0ac4394-7e4a-4409-9701-ba8be283dbc3" + ], + "device_id": "", + "binding:host_id": "HOST1", + "binding:vnic_type": "normal" + } +} + `) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/portsbinding/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/portsbinding/testing/requests_test.go new file mode 100644 index 0000000000..f41f1cc47a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/portsbinding/testing/requests_test.go @@ -0,0 +1,164 @@ +package testing + +import ( + "testing" + + fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/portsbinding" + "github.com/gophercloud/gophercloud/openstack/networking/v2/ports" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleListSuccessfully(t) + + count := 0 + + ports.List(fake.ServiceClient(), ports.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := portsbinding.ExtractPorts(page) + th.AssertNoErr(t, err) + + expected := []portsbinding.Port{ + { + Port: ports.Port{ + Status: "ACTIVE", + Name: "", + AdminStateUp: true, + NetworkID: "70c1db1f-b701-45bd-96e0-a313ee3430b3", + TenantID: "", + DeviceOwner: "network:router_gateway", + MACAddress: "fa:16:3e:58:42:ed", + FixedIPs: []ports.IP{ + { + SubnetID: "008ba151-0b8c-4a67-98b5-0d2b87666062", + IPAddress: "172.24.4.2", + }, + }, + ID: "d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b", + SecurityGroups: []string{}, + DeviceID: "9ae135f4-b6e0-4dad-9e91-3c223e385824", + }, + VNICType: "normal", + HostID: "devstack", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleGet(t) + + n, err := portsbinding.Get(fake.ServiceClient(), "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Status, "ACTIVE") + th.AssertEquals(t, n.Name, "") + th.AssertEquals(t, n.AdminStateUp, true) + th.AssertEquals(t, n.NetworkID, "a87cc70a-3e15-4acf-8205-9b711a3531b7") + th.AssertEquals(t, n.TenantID, "7e02058126cc4950b75f9970368ba177") + th.AssertEquals(t, n.DeviceOwner, "network:router_interface") + th.AssertEquals(t, n.MACAddress, "fa:16:3e:23:fd:d7") + th.AssertDeepEquals(t, n.FixedIPs, []ports.IP{ + {SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.1"}, + }) + th.AssertEquals(t, n.ID, "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2") + th.AssertDeepEquals(t, n.SecurityGroups, []string{}) + th.AssertEquals(t, n.DeviceID, "5e3898d7-11be-483e-9732-b2f5eccd2b2e") + + th.AssertEquals(t, n.HostID, "devstack") + th.AssertEquals(t, n.VNICType, "normal") + th.AssertEquals(t, n.VIFType, "ovs") + th.AssertDeepEquals(t, n.VIFDetails, map[string]interface{}{"port_filter": true, "ovs_hybrid_plug": true}) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleCreate(t) + + asu := true + options := portsbinding.CreateOpts{ + CreateOptsBuilder: ports.CreateOpts{ + Name: "private-port", + AdminStateUp: &asu, + NetworkID: "a87cc70a-3e15-4acf-8205-9b711a3531b7", + FixedIPs: []ports.IP{ + {SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.2"}, + }, + SecurityGroups: []string{"foo"}, + }, + HostID: "HOST1", + VNICType: "normal", + } + n, err := portsbinding.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Status, "DOWN") + th.AssertEquals(t, n.Name, "private-port") + th.AssertEquals(t, n.AdminStateUp, true) + th.AssertEquals(t, n.NetworkID, "a87cc70a-3e15-4acf-8205-9b711a3531b7") + th.AssertEquals(t, n.TenantID, "d6700c0c9ffa4f1cb322cd4a1f3906fa") + th.AssertEquals(t, n.DeviceOwner, "") + th.AssertEquals(t, n.MACAddress, "fa:16:3e:c9:cb:f0") + th.AssertDeepEquals(t, n.FixedIPs, []ports.IP{ + {SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.2"}, + }) + th.AssertEquals(t, n.ID, "65c0ee9f-d634-4522-8954-51021b570b0d") + th.AssertDeepEquals(t, n.SecurityGroups, []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"}) + th.AssertEquals(t, n.HostID, "HOST1") + th.AssertEquals(t, n.VNICType, "normal") +} + +func TestRequiredCreateOpts(t *testing.T) { + res := portsbinding.Create(fake.ServiceClient(), portsbinding.CreateOpts{CreateOptsBuilder: ports.CreateOpts{}}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleUpdate(t) + + options := portsbinding.UpdateOpts{ + UpdateOptsBuilder: ports.UpdateOpts{ + Name: "new_port_name", + FixedIPs: []ports.IP{ + {SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"}, + }, + SecurityGroups: []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"}, + }, + HostID: "HOST1", + VNICType: "normal", + } + + s, err := portsbinding.Update(fake.ServiceClient(), "65c0ee9f-d634-4522-8954-51021b570b0d", options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, s.Name, "new_port_name") + th.AssertDeepEquals(t, s.FixedIPs, []ports.IP{ + {SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"}, + }) + th.AssertDeepEquals(t, s.SecurityGroups, []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"}) + th.AssertEquals(t, s.HostID, "HOST1") + th.AssertEquals(t, s.VNICType, "normal") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/portsbinding/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/portsbinding/urls.go new file mode 100644 index 0000000000..a531a7ee73 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/portsbinding/urls.go @@ -0,0 +1,23 @@ +package portsbinding + +import "github.com/gophercloud/gophercloud" + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("ports", id) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("ports") +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func createURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/provider/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/provider/doc.go new file mode 100755 index 0000000000..373da44f84 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/provider/doc.go @@ -0,0 +1,21 @@ +// Package provider gives access to the provider Neutron plugin, allowing +// network extended attributes. The provider extended attributes for networks +// enable administrative users to specify how network objects map to the +// underlying networking infrastructure. These extended attributes also appear +// when administrative users query networks. +// +// For more information about extended attributes, see the NetworkExtAttrs +// struct. The actual semantics of these attributes depend on the technology +// back end of the particular plug-in. See the plug-in documentation and the +// OpenStack Cloud Administrator Guide to understand which values should be +// specific for each of these attributes when OpenStack Networking is deployed +// with a particular plug-in. The examples shown in this chapter refer to the +// Open vSwitch plug-in. +// +// The default policy settings enable only users with administrative rights to +// specify these parameters in requests and to see their values in responses. By +// default, the provider network extension attributes are completely hidden from +// regular tenants. As a rule of thumb, if these attributes are not visible in a +// GET /networks/ operation, this implies the user submitting the +// request is not authorized to view or manipulate provider network attributes. +package provider diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/provider/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/provider/requests.go new file mode 100644 index 0000000000..32c27970a4 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/provider/requests.go @@ -0,0 +1,28 @@ +package provider + +import ( + "github.com/gophercloud/gophercloud/openstack/networking/v2/networks" +) + +// CreateOptsExt adds a Segments option to the base Network CreateOpts. +type CreateOptsExt struct { + networks.CreateOptsBuilder + Segments []Segment `json:"segments,omitempty"` +} + +// ToNetworkCreateMap adds segments to the base network creation options. +func (opts CreateOptsExt) ToNetworkCreateMap() (map[string]interface{}, error) { + base, err := opts.CreateOptsBuilder.ToNetworkCreateMap() + if err != nil { + return nil, err + } + + if opts.Segments == nil { + return base, nil + } + + providerMap := base["network"].(map[string]interface{}) + providerMap["segments"] = opts.Segments + + return base, nil +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/provider/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/provider/results.go new file mode 100644 index 0000000000..55770d8b8f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/provider/results.go @@ -0,0 +1,126 @@ +package provider + +import ( + "encoding/json" + "strconv" + + "github.com/gophercloud/gophercloud/openstack/networking/v2/networks" + "github.com/gophercloud/gophercloud/pagination" +) + +// NetworkExtAttrs represents an extended form of a Network with additional fields. +type NetworkExtAttrs struct { + // UUID for the network + ID string `json:"id"` + + // Human-readable name for the network. Might not be unique. + Name string `json:"name"` + + // The administrative state of network. If false (down), the network does not forward packets. + AdminStateUp bool `json:"admin_state_up"` + + // Indicates whether network is currently operational. Possible values include + // `ACTIVE', `DOWN', `BUILD', or `ERROR'. Plug-ins might define additional values. + Status string `json:"status"` + + // Subnets associated with this network. + Subnets []string `json:"subnets"` + + // Owner of network. Only admin users can specify a tenant_id other than its own. + TenantID string `json:"tenant_id"` + + // Specifies whether the network resource can be accessed by any tenant or not. + Shared bool `json:"shared"` + + // Specifies the nature of the physical network mapped to this network + // resource. Examples are flat, vlan, or gre. + NetworkType string `json:"provider:network_type"` + + // Identifies the physical network on top of which this network object is + // being implemented. The OpenStack Networking API does not expose any facility + // for retrieving the list of available physical networks. As an example, in + // the Open vSwitch plug-in this is a symbolic name which is then mapped to + // specific bridges on each compute host through the Open vSwitch plug-in + // configuration file. + PhysicalNetwork string `json:"provider:physical_network"` + + // Identifies an isolated segment on the physical network; the nature of the + // segment depends on the segmentation model defined by network_type. For + // instance, if network_type is vlan, then this is a vlan identifier; + // otherwise, if network_type is gre, then this will be a gre key. + SegmentationID string `json:"provider:segmentation_id"` + + // Segments is an array of Segment which defines multiple physical bindings to logical networks. + Segments []Segment `json:"segments"` +} + +// Segment defines a physical binding to a logical network. +type Segment struct { + PhysicalNetwork string `json:"provider:physical_network"` + NetworkType string `json:"provider:network_type"` + SegmentationID int `json:"provider:segmentation_id"` +} + +func (n *NetworkExtAttrs) UnmarshalJSON(b []byte) error { + type tmp NetworkExtAttrs + var networkExtAttrs *struct { + tmp + SegmentationID interface{} `json:"provider:segmentation_id"` + } + + if err := json.Unmarshal(b, &networkExtAttrs); err != nil { + return err + } + + *n = NetworkExtAttrs(networkExtAttrs.tmp) + + switch t := networkExtAttrs.SegmentationID.(type) { + case float64: + n.SegmentationID = strconv.FormatFloat(t, 'f', -1, 64) + case string: + n.SegmentationID = string(t) + } + + return nil +} + +// ExtractGet decorates a GetResult struct returned from a networks.Get() +// function with extended attributes. +func ExtractGet(r networks.GetResult) (*NetworkExtAttrs, error) { + var s struct { + Network *NetworkExtAttrs `json:"network"` + } + err := r.ExtractInto(&s) + return s.Network, err +} + +// ExtractCreate decorates a CreateResult struct returned from a networks.Create() +// function with extended attributes. +func ExtractCreate(r networks.CreateResult) (*NetworkExtAttrs, error) { + var s struct { + Network *NetworkExtAttrs `json:"network"` + } + err := r.ExtractInto(&s) + return s.Network, err +} + +// ExtractUpdate decorates a UpdateResult struct returned from a +// networks.Update() function with extended attributes. +func ExtractUpdate(r networks.UpdateResult) (*NetworkExtAttrs, error) { + var s struct { + Network *NetworkExtAttrs `json:"network"` + } + err := r.ExtractInto(&s) + return s.Network, err +} + +// ExtractList accepts a Page struct, specifically a NetworkPage struct, and +// extracts the elements into a slice of NetworkExtAttrs structs. In other +// words, a generic collection is mapped into a relevant slice. +func ExtractList(r pagination.Page) ([]NetworkExtAttrs, error) { + var s struct { + Networks []NetworkExtAttrs `json:"networks" json:"networks"` + } + err := (r.(networks.NetworkPage)).ExtractInto(&s) + return s.Networks, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/provider/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/provider/testing/doc.go new file mode 100644 index 0000000000..370ce194d0 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/provider/testing/doc.go @@ -0,0 +1,2 @@ +// networking_extensions_provider_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/provider/testing/results_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/provider/testing/results_test.go new file mode 100644 index 0000000000..a6e4d24f09 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/provider/testing/results_test.go @@ -0,0 +1,336 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud" + fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/provider" + "github.com/gophercloud/gophercloud/openstack/networking/v2/networks" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "networks": [ + { + "status": "ACTIVE", + "subnets": [ + "54d6f61d-db07-451c-9ab3-b9609b6b6f0b" + ], + "name": "private-network", + "admin_state_up": true, + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "shared": true, + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "provider:segmentation_id": null, + "provider:physical_network": null, + "provider:network_type": "local" + }, + { + "status": "ACTIVE", + "subnets": [ + "08eae331-0402-425a-923c-34f7cfe39c1b" + ], + "name": "private", + "admin_state_up": true, + "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e", + "shared": true, + "id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324", + "provider:segmentation_id": 1234567890, + "provider:physical_network": null, + "provider:network_type": "local" + } + ] +} + `) + }) + + count := 0 + + networks.List(fake.ServiceClient(), networks.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := provider.ExtractList(page) + if err != nil { + t.Errorf("Failed to extract networks: %v", err) + return false, err + } + + expected := []provider.NetworkExtAttrs{ + { + Status: "ACTIVE", + Subnets: []string{"54d6f61d-db07-451c-9ab3-b9609b6b6f0b"}, + Name: "private-network", + AdminStateUp: true, + TenantID: "4fd44f30292945e481c7b8a0c8908869", + Shared: true, + ID: "d32019d3-bc6e-4319-9c1d-6722fc136a22", + NetworkType: "local", + PhysicalNetwork: "", + SegmentationID: "", + }, + { + Status: "ACTIVE", + Subnets: []string{"08eae331-0402-425a-923c-34f7cfe39c1b"}, + Name: "private", + AdminStateUp: true, + TenantID: "26a7980765d0414dbc1fc1f88cdb7e6e", + Shared: true, + ID: "db193ab3-96e3-4cb3-8fc5-05f4296d0324", + NetworkType: "local", + PhysicalNetwork: "", + SegmentationID: "1234567890", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/networks/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "network": { + "status": "ACTIVE", + "subnets": [ + "54d6f61d-db07-451c-9ab3-b9609b6b6f0b" + ], + "name": "private-network", + "provider:physical_network": null, + "admin_state_up": true, + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "provider:network_type": "local", + "shared": true, + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "provider:segmentation_id": null + } +} + `) + }) + + res := networks.Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22") + n, err := provider.ExtractGet(res) + + th.AssertNoErr(t, err) + + th.AssertEquals(t, "", n.PhysicalNetwork) + th.AssertEquals(t, "local", n.NetworkType) + th.AssertEquals(t, "", n.SegmentationID) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "network": { + "name": "sample_network", + "admin_state_up": true + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "network": { + "status": "ACTIVE", + "subnets": [ + "54d6f61d-db07-451c-9ab3-b9609b6b6f0b" + ], + "name": "private-network", + "provider:physical_network": null, + "admin_state_up": true, + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "provider:network_type": "local", + "shared": true, + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "provider:segmentation_id": null + } +} + `) + }) + + options := networks.CreateOpts{Name: "sample_network", AdminStateUp: gophercloud.Enabled} + res := networks.Create(fake.ServiceClient(), options) + n, err := provider.ExtractCreate(res) + + th.AssertNoErr(t, err) + + th.AssertEquals(t, "", n.PhysicalNetwork) + th.AssertEquals(t, "local", n.NetworkType) + th.AssertEquals(t, "", n.SegmentationID) +} + +func TestCreateWithMultipleProvider(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "network": { + "name": "sample_network", + "admin_state_up": true, + "shared": true, + "tenant_id": "12345", + "segments": [ + { + "provider:segmentation_id": 666, + "provider:physical_network": "br-ex", + "provider:network_type": "vxlan" + }, + { + "provider:segmentation_id": 615, + "provider:physical_network": "br-ex", + "provider:network_type": "vxlan" + } + ] + } +} + `) + + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, ` +{ + "network": { + "status": "ACTIVE", + "name": "sample_network", + "admin_state_up": true, + "shared": true, + "tenant_id": "12345", + "segments": [ + { + "provider:segmentation_id": 666, + "provider:physical_network": "br-ex", + "provider:network_type": "vlan" + }, + { + "provider:segmentation_id": 615, + "provider:physical_network": "br-ex", + "provider:network_type": "vlan" + } + ] + } +} + `) + }) + + iTrue := true + segments := []provider.Segment{ + provider.Segment{NetworkType: "vxlan", PhysicalNetwork: "br-ex", SegmentationID: 666}, + provider.Segment{NetworkType: "vxlan", PhysicalNetwork: "br-ex", SegmentationID: 615}, + } + + networkCreateOpts := networks.CreateOpts{ + Name: "sample_network", + AdminStateUp: &iTrue, + Shared: &iTrue, + TenantID: "12345", + } + + providerCreateOpts := provider.CreateOptsExt{ + CreateOptsBuilder: networkCreateOpts, + Segments: segments, + } + + res := networks.Create(fake.ServiceClient(), providerCreateOpts) + _, err := provider.ExtractCreate(res) + th.AssertNoErr(t, err) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "network": { + "name": "new_network_name", + "admin_state_up": false, + "shared": true + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "network": { + "status": "ACTIVE", + "subnets": [ + "54d6f61d-db07-451c-9ab3-b9609b6b6f0b" + ], + "name": "private-network", + "provider:physical_network": null, + "admin_state_up": true, + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "provider:network_type": "local", + "shared": true, + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "provider:segmentation_id": null + } +} + `) + }) + + iTrue := true + options := networks.UpdateOpts{Name: "new_network_name", AdminStateUp: gophercloud.Disabled, Shared: &iTrue} + res := networks.Update(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", options) + n, err := provider.ExtractUpdate(res) + + th.AssertNoErr(t, err) + + th.AssertEquals(t, "", n.PhysicalNetwork) + th.AssertEquals(t, "local", n.NetworkType) + th.AssertEquals(t, "", n.SegmentationID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/doc.go new file mode 100644 index 0000000000..31f744ccd7 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/doc.go @@ -0,0 +1,32 @@ +// Package security contains functionality to work with security group and +// security group rules Neutron resources. +// +// Security groups and security group rules allows administrators and tenants +// the ability to specify the type of traffic and direction (ingress/egress) +// that is allowed to pass through a port. A security group is a container for +// security group rules. +// +// When a port is created in Networking it is associated with a security group. +// If a security group is not specified the port is associated with a 'default' +// security group. By default, this group drops all ingress traffic and allows +// all egress. Rules can be added to this group in order to change the behaviour. +// +// The basic characteristics of Neutron Security Groups are: +// +// For ingress traffic (to an instance) +// - Only traffic matched with security group rules are allowed. +// - When there is no rule defined, all traffic is dropped. +// +// For egress traffic (from an instance) +// - Only traffic matched with security group rules are allowed. +// - When there is no rule defined, all egress traffic are dropped. +// - When a new security group is created, rules to allow all egress traffic +// is automatically added. +// +// "default security group" is defined for each tenant. +// - For the default security group a rule which allows intercommunication +// among hosts associated with the default security group is defined by default. +// - As a result, all egress traffic and intercommunication in the default +// group are allowed and all ingress from outside of the default group is +// dropped by default (in the default security group). +package security diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/requests.go new file mode 100644 index 0000000000..7e1f608128 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/requests.go @@ -0,0 +1,138 @@ +package groups + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the floating IP attributes you want to see returned. SortKey allows you to +// sort by a particular network attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + ID string `q:"id"` + Name string `q:"name"` + TenantID string `q:"tenant_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// List returns a Pager which allows you to iterate over a collection of +// security groups. It accepts a ListOpts struct, which allows you to filter +// and sort the returned collection for greater efficiency. +func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + q, err := gophercloud.BuildQueryString(&opts) + if err != nil { + return pagination.Pager{Err: err} + } + u := rootURL(c) + q.String() + return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page { + return SecGroupPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +type CreateOptsBuilder interface { + ToSecGroupCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains all the values needed to create a new security group. +type CreateOpts struct { + // Required. Human-readable name for the Security Group. Does not have to be unique. + Name string `json:"name" required:"true"` + // Required for admins. Indicates the owner of the Security Group. + TenantID string `json:"tenant_id,omitempty"` + // Optional. Describes the security group. + Description string `json:"description,omitempty"` +} + +func (opts CreateOpts) ToSecGroupCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "security_group") +} + +// Create is an operation which provisions a new security group with default +// security group rules for the IPv4 and IPv6 ether types. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToSecGroupCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(rootURL(c), b, &r.Body, nil) + return +} + +type UpdateOptsBuilder interface { + ToSecGroupUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contains all the values needed to update an existing security group. +type UpdateOpts struct { + // Human-readable name for the Security Group. Does not have to be unique. + Name string `json:"name,omitempty"` + // Optional. Describes the security group. + Description string `json:"description,omitempty"` +} + +func (opts UpdateOpts) ToSecGroupUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "security_group") +} + +// Update is an operation which updates an existing security group. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToSecGroupUpdateMap() + if err != nil { + r.Err = err + return + } + + _, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Get retrieves a particular security group based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(resourceURL(c, id), &r.Body, nil) + return +} + +// Delete will permanently delete a particular security group based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(resourceURL(c, id), nil) + return +} + +// IDFromName is a convenience function that returns a security group's ID given its name. +func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + pages, err := List(client, ListOpts{}).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractGroups(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", gophercloud.ErrResourceNotFound{Name: name, ResourceType: "security group"} + case 1: + return id, nil + default: + return "", gophercloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "security group"} + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/results.go new file mode 100644 index 0000000000..d737abb061 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/results.go @@ -0,0 +1,99 @@ +package groups + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules" + "github.com/gophercloud/gophercloud/pagination" +) + +// SecGroup represents a container for security group rules. +type SecGroup struct { + // The UUID for the security group. + ID string + + // Human-readable name for the security group. Might not be unique. Cannot be + // named "default" as that is automatically created for a tenant. + Name string + + // The security group description. + Description string + + // A slice of security group rules that dictate the permitted behaviour for + // traffic entering and leaving the group. + Rules []rules.SecGroupRule `json:"security_group_rules"` + + // Owner of the security group. Only admin users can specify a TenantID + // other than their own. + TenantID string `json:"tenant_id"` +} + +// SecGroupPage is the page returned by a pager when traversing over a +// collection of security groups. +type SecGroupPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of security groups has +// reached the end of a page and the pager seeks to traverse over a new one. In +// order to do this, it needs to construct the next page's URL. +func (r SecGroupPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"security_groups_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a SecGroupPage struct is empty. +func (r SecGroupPage) IsEmpty() (bool, error) { + is, err := ExtractGroups(r) + return len(is) == 0, err +} + +// ExtractGroups accepts a Page struct, specifically a SecGroupPage struct, +// and extracts the elements into a slice of SecGroup structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractGroups(r pagination.Page) ([]SecGroup, error) { + var s struct { + SecGroups []SecGroup `json:"security_groups"` + } + err := (r.(SecGroupPage)).ExtractInto(&s) + return s.SecGroups, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a security group. +func (r commonResult) Extract() (*SecGroup, error) { + var s struct { + SecGroup *SecGroup `json:"security_group"` + } + err := r.ExtractInto(&s) + return s.SecGroup, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/testing/doc.go new file mode 100644 index 0000000000..69d5db7495 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/testing/doc.go @@ -0,0 +1,2 @@ +// networking_extensions_security_groups_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/testing/fixtures.go new file mode 100644 index 0000000000..9e4a931fe4 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/testing/fixtures.go @@ -0,0 +1,156 @@ +package testing + +import ( + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules" +) + +const SecurityGroupListResponse = ` +{ + "security_groups": [ + { + "description": "default", + "id": "85cc3048-abc3-43cc-89b3-377341426ac5", + "name": "default", + "security_group_rules": [], + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } + ] +} +` + +var SecurityGroup1 = groups.SecGroup{ + Description: "default", + ID: "85cc3048-abc3-43cc-89b3-377341426ac5", + Name: "default", + Rules: []rules.SecGroupRule{}, + TenantID: "e4f50856753b4dc6afee5fa6b9b6c550", +} + +const SecurityGroupCreateRequest = ` +{ + "security_group": { + "name": "new-webservers", + "description": "security group for webservers" + } +} +` + +const SecurityGroupCreateResponse = ` +{ + "security_group": { + "description": "security group for webservers", + "id": "2076db17-a522-4506-91de-c6dd8e837028", + "name": "new-webservers", + "security_group_rules": [ + { + "direction": "egress", + "ethertype": "IPv4", + "id": "38ce2d8e-e8f1-48bd-83c2-d33cb9f50c3d", + "port_range_max": null, + "port_range_min": null, + "protocol": null, + "remote_group_id": null, + "remote_ip_prefix": null, + "security_group_id": "2076db17-a522-4506-91de-c6dd8e837028", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + }, + { + "direction": "egress", + "ethertype": "IPv6", + "id": "565b9502-12de-4ffd-91e9-68885cff6ae1", + "port_range_max": null, + "port_range_min": null, + "protocol": null, + "remote_group_id": null, + "remote_ip_prefix": null, + "security_group_id": "2076db17-a522-4506-91de-c6dd8e837028", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } + ], + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } +} +` + +const SecurityGroupUpdateRequest = ` +{ + "security_group": { + "name": "newer-webservers" + } +} +` + +const SecurityGroupUpdateResponse = ` +{ + "security_group": { + "description": "security group for webservers", + "id": "2076db17-a522-4506-91de-c6dd8e837028", + "name": "newer-webservers", + "security_group_rules": [ + { + "direction": "egress", + "ethertype": "IPv4", + "id": "38ce2d8e-e8f1-48bd-83c2-d33cb9f50c3d", + "port_range_max": null, + "port_range_min": null, + "protocol": null, + "remote_group_id": null, + "remote_ip_prefix": null, + "security_group_id": "2076db17-a522-4506-91de-c6dd8e837028", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + }, + { + "direction": "egress", + "ethertype": "IPv6", + "id": "565b9502-12de-4ffd-91e9-68885cff6ae1", + "port_range_max": null, + "port_range_min": null, + "protocol": null, + "remote_group_id": null, + "remote_ip_prefix": null, + "security_group_id": "2076db17-a522-4506-91de-c6dd8e837028", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } + ], + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } +} +` + +const SecurityGroupGetResponse = ` +{ + "security_group": { + "description": "default", + "id": "85cc3048-abc3-43cc-89b3-377341426ac5", + "name": "default", + "security_group_rules": [ + { + "direction": "egress", + "ethertype": "IPv6", + "id": "3c0e45ff-adaf-4124-b083-bf390e5482ff", + "port_range_max": null, + "port_range_min": null, + "protocol": null, + "remote_group_id": null, + "remote_ip_prefix": null, + "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + }, + { + "direction": "egress", + "ethertype": "IPv4", + "id": "93aa42e5-80db-4581-9391-3a608bd0e448", + "port_range_max": null, + "port_range_min": null, + "protocol": null, + "remote_group_id": null, + "remote_ip_prefix": null, + "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } + ], + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } +} +` diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/testing/requests_test.go new file mode 100644 index 0000000000..5bb8c70c5b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/testing/requests_test.go @@ -0,0 +1,134 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-groups", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, SecurityGroupListResponse) + }) + + count := 0 + + groups.List(fake.ServiceClient(), groups.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := groups.ExtractGroups(page) + if err != nil { + t.Errorf("Failed to extract secgroups: %v", err) + return false, err + } + + expected := []groups.SecGroup{SecurityGroup1} + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-groups", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, SecurityGroupCreateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, SecurityGroupCreateResponse) + }) + + opts := groups.CreateOpts{Name: "new-webservers", Description: "security group for webservers"} + _, err := groups.Create(fake.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-groups/2076db17-a522-4506-91de-c6dd8e837028", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, SecurityGroupUpdateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, SecurityGroupUpdateResponse) + }) + + opts := groups.UpdateOpts{Name: "newer-webservers"} + sg, err := groups.Update(fake.ServiceClient(), "2076db17-a522-4506-91de-c6dd8e837028", opts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "newer-webservers", sg.Name) + th.AssertEquals(t, "security group for webservers", sg.Description) + th.AssertEquals(t, "2076db17-a522-4506-91de-c6dd8e837028", sg.ID) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-groups/85cc3048-abc3-43cc-89b3-377341426ac5", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, SecurityGroupGetResponse) + }) + + sg, err := groups.Get(fake.ServiceClient(), "85cc3048-abc3-43cc-89b3-377341426ac5").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "default", sg.Description) + th.AssertEquals(t, "85cc3048-abc3-43cc-89b3-377341426ac5", sg.ID) + th.AssertEquals(t, "default", sg.Name) + th.AssertEquals(t, 2, len(sg.Rules)) + th.AssertEquals(t, "e4f50856753b4dc6afee5fa6b9b6c550", sg.TenantID) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-groups/4ec89087-d057-4e2c-911f-60a3b47ee304", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := groups.Delete(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304") + th.AssertNoErr(t, res.Err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/urls.go new file mode 100644 index 0000000000..104cbcc558 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/urls.go @@ -0,0 +1,13 @@ +package groups + +import "github.com/gophercloud/gophercloud" + +const rootPath = "security-groups" + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules/requests.go new file mode 100644 index 0000000000..59ba721d6f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules/requests.go @@ -0,0 +1,145 @@ +package rules + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the security group attributes you want to see returned. SortKey allows you to +// sort by a particular network attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Direction string `q:"direction"` + EtherType string `q:"ethertype"` + ID string `q:"id"` + PortRangeMax int `q:"port_range_max"` + PortRangeMin int `q:"port_range_min"` + Protocol string `q:"protocol"` + RemoteGroupID string `q:"remote_group_id"` + RemoteIPPrefix string `q:"remote_ip_prefix"` + SecGroupID string `q:"security_group_id"` + TenantID string `q:"tenant_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// List returns a Pager which allows you to iterate over a collection of +// security group rules. It accepts a ListOpts struct, which allows you to filter +// and sort the returned collection for greater efficiency. +func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + q, err := gophercloud.BuildQueryString(&opts) + if err != nil { + return pagination.Pager{Err: err} + } + u := rootURL(c) + q.String() + return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page { + return SecGroupRulePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +type RuleDirection string +type RuleProtocol string +type RuleEtherType string + +// Constants useful for CreateOpts +const ( + DirIngress RuleDirection = "ingress" + DirEgress RuleDirection = "egress" + EtherType4 RuleEtherType = "IPv4" + EtherType6 RuleEtherType = "IPv6" + ProtocolAH RuleProtocol = "ah" + ProtocolDCCP RuleProtocol = "dccp" + ProtocolEGP RuleProtocol = "egp" + ProtocolESP RuleProtocol = "esp" + ProtocolGRE RuleProtocol = "gre" + ProtocolICMP RuleProtocol = "icmp" + ProtocolIGMP RuleProtocol = "igmp" + ProtocolIPv6Encap RuleProtocol = "ipv6-encap" + ProtocolIPv6Frag RuleProtocol = "ipv6-frag" + ProtocolIPv6ICMP RuleProtocol = "ipv6-icmp" + ProtocolIPv6NoNxt RuleProtocol = "ipv6-nonxt" + ProtocolIPv6Opts RuleProtocol = "ipv6-opts" + ProtocolIPv6Route RuleProtocol = "ipv6-route" + ProtocolOSPF RuleProtocol = "ospf" + ProtocolPGM RuleProtocol = "pgm" + ProtocolRSVP RuleProtocol = "rsvp" + ProtocolSCTP RuleProtocol = "sctp" + ProtocolTCP RuleProtocol = "tcp" + ProtocolUDP RuleProtocol = "udp" + ProtocolUDPLite RuleProtocol = "udplite" + ProtocolVRRP RuleProtocol = "vrrp" +) + +// CreateOptsBuilder is what types must satisfy to be used as Create +// options. +type CreateOptsBuilder interface { + ToSecGroupRuleCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains all the values needed to create a new security group rule. +type CreateOpts struct { + // Required. Must be either "ingress" or "egress": the direction in which the + // security group rule is applied. + Direction RuleDirection `json:"direction" required:"true"` + // Required. Must be "IPv4" or "IPv6", and addresses represented in CIDR must + // match the ingress or egress rules. + EtherType RuleEtherType `json:"ethertype" required:"true"` + // Required. The security group ID to associate with this security group rule. + SecGroupID string `json:"security_group_id" required:"true"` + // Optional. The maximum port number in the range that is matched by the + // security group rule. The PortRangeMin attribute constrains the PortRangeMax + // attribute. If the protocol is ICMP, this value must be an ICMP type. + PortRangeMax int `json:"port_range_max,omitempty"` + // Optional. The minimum port number in the range that is matched by the + // security group rule. If the protocol is TCP or UDP, this value must be + // less than or equal to the value of the PortRangeMax attribute. If the + // protocol is ICMP, this value must be an ICMP type. + PortRangeMin int `json:"port_range_min,omitempty"` + // Optional. The protocol that is matched by the security group rule. Valid + // values are "tcp", "udp", "icmp" or an empty string. + Protocol RuleProtocol `json:"protocol,omitempty"` + // Optional. The remote group ID to be associated with this security group + // rule. You can specify either RemoteGroupID or RemoteIPPrefix. + RemoteGroupID string `json:"remote_group_id,omitempty"` + // Optional. The remote IP prefix to be associated with this security group + // rule. You can specify either RemoteGroupID or RemoteIPPrefix. This + // attribute matches the specified IP prefix as the source IP address of the + // IP packet. + RemoteIPPrefix string `json:"remote_ip_prefix,omitempty"` + // Required for admins. Indicates the owner of the VIP. + TenantID string `json:"tenant_id,omitempty"` +} + +// ToSecGroupRuleCreateMap allows CreateOpts to satisfy the CreateOptsBuilder +// interface +func (opts CreateOpts) ToSecGroupRuleCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "security_group_rule") +} + +// Create is an operation which adds a new security group rule and associates it +// with an existing security group (whose ID is specified in CreateOpts). +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToSecGroupRuleCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(rootURL(c), b, &r.Body, nil) + return +} + +// Get retrieves a particular security group rule based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(resourceURL(c, id), &r.Body, nil) + return +} + +// Delete will permanently delete a particular security group rule based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(resourceURL(c, id), nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules/results.go new file mode 100644 index 0000000000..18476a602c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules/results.go @@ -0,0 +1,118 @@ +package rules + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// SecGroupRule represents a rule to dictate the behaviour of incoming or +// outgoing traffic for a particular security group. +type SecGroupRule struct { + // The UUID for this security group rule. + ID string + + // The direction in which the security group rule is applied. The only values + // allowed are "ingress" or "egress". For a compute instance, an ingress + // security group rule is applied to incoming (ingress) traffic for that + // instance. An egress rule is applied to traffic leaving the instance. + Direction string + + // Must be IPv4 or IPv6, and addresses represented in CIDR must match the + // ingress or egress rules. + EtherType string `json:"ethertype"` + + // The security group ID to associate with this security group rule. + SecGroupID string `json:"security_group_id"` + + // The minimum port number in the range that is matched by the security group + // rule. If the protocol is TCP or UDP, this value must be less than or equal + // to the value of the PortRangeMax attribute. If the protocol is ICMP, this + // value must be an ICMP type. + PortRangeMin int `json:"port_range_min"` + + // The maximum port number in the range that is matched by the security group + // rule. The PortRangeMin attribute constrains the PortRangeMax attribute. If + // the protocol is ICMP, this value must be an ICMP type. + PortRangeMax int `json:"port_range_max"` + + // The protocol that is matched by the security group rule. Valid values are + // "tcp", "udp", "icmp" or an empty string. + Protocol string + + // The remote group ID to be associated with this security group rule. You + // can specify either RemoteGroupID or RemoteIPPrefix. + RemoteGroupID string `json:"remote_group_id"` + + // The remote IP prefix to be associated with this security group rule. You + // can specify either RemoteGroupID or RemoteIPPrefix . This attribute + // matches the specified IP prefix as the source IP address of the IP packet. + RemoteIPPrefix string `json:"remote_ip_prefix"` + + // The owner of this security group rule. + TenantID string `json:"tenant_id"` +} + +// SecGroupRulePage is the page returned by a pager when traversing over a +// collection of security group rules. +type SecGroupRulePage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of security group rules has +// reached the end of a page and the pager seeks to traverse over a new one. In +// order to do this, it needs to construct the next page's URL. +func (r SecGroupRulePage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"security_group_rules_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a SecGroupRulePage struct is empty. +func (r SecGroupRulePage) IsEmpty() (bool, error) { + is, err := ExtractRules(r) + return len(is) == 0, err +} + +// ExtractRules accepts a Page struct, specifically a SecGroupRulePage struct, +// and extracts the elements into a slice of SecGroupRule structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractRules(r pagination.Page) ([]SecGroupRule, error) { + var s struct { + SecGroupRules []SecGroupRule `json:"security_group_rules"` + } + err := (r.(SecGroupRulePage)).ExtractInto(&s) + return s.SecGroupRules, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a security rule. +func (r commonResult) Extract() (*SecGroupRule, error) { + var s struct { + SecGroupRule *SecGroupRule `json:"security_group_rule"` + } + err := r.ExtractInto(&s) + return s.SecGroupRule, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules/testing/doc.go new file mode 100644 index 0000000000..a4f7b43c74 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules/testing/doc.go @@ -0,0 +1,2 @@ +// networking_extensions_security_rules_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules/testing/requests_test.go new file mode 100644 index 0000000000..968fd04d8f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules/testing/requests_test.go @@ -0,0 +1,236 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-group-rules", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_group_rules": [ + { + "direction": "egress", + "ethertype": "IPv6", + "id": "3c0e45ff-adaf-4124-b083-bf390e5482ff", + "port_range_max": null, + "port_range_min": null, + "protocol": null, + "remote_group_id": null, + "remote_ip_prefix": null, + "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + }, + { + "direction": "egress", + "ethertype": "IPv4", + "id": "93aa42e5-80db-4581-9391-3a608bd0e448", + "port_range_max": null, + "port_range_min": null, + "protocol": null, + "remote_group_id": null, + "remote_ip_prefix": null, + "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } + ] +} + `) + }) + + count := 0 + + rules.List(fake.ServiceClient(), rules.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := rules.ExtractRules(page) + if err != nil { + t.Errorf("Failed to extract secrules: %v", err) + return false, err + } + + expected := []rules.SecGroupRule{ + { + Direction: "egress", + EtherType: "IPv6", + ID: "3c0e45ff-adaf-4124-b083-bf390e5482ff", + PortRangeMax: 0, + PortRangeMin: 0, + Protocol: "", + RemoteGroupID: "", + RemoteIPPrefix: "", + SecGroupID: "85cc3048-abc3-43cc-89b3-377341426ac5", + TenantID: "e4f50856753b4dc6afee5fa6b9b6c550", + }, + { + Direction: "egress", + EtherType: "IPv4", + ID: "93aa42e5-80db-4581-9391-3a608bd0e448", + PortRangeMax: 0, + PortRangeMin: 0, + Protocol: "", + RemoteGroupID: "", + RemoteIPPrefix: "", + SecGroupID: "85cc3048-abc3-43cc-89b3-377341426ac5", + TenantID: "e4f50856753b4dc6afee5fa6b9b6c550", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-group-rules", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "security_group_rule": { + "direction": "ingress", + "port_range_min": 80, + "ethertype": "IPv4", + "port_range_max": 80, + "protocol": "tcp", + "remote_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5", + "security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "security_group_rule": { + "direction": "ingress", + "ethertype": "IPv4", + "id": "2bc0accf-312e-429a-956e-e4407625eb62", + "port_range_max": 80, + "port_range_min": 80, + "protocol": "tcp", + "remote_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5", + "remote_ip_prefix": null, + "security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } +} + `) + }) + + opts := rules.CreateOpts{ + Direction: "ingress", + PortRangeMin: 80, + EtherType: rules.EtherType4, + PortRangeMax: 80, + Protocol: "tcp", + RemoteGroupID: "85cc3048-abc3-43cc-89b3-377341426ac5", + SecGroupID: "a7734e61-b545-452d-a3cd-0189cbd9747a", + } + _, err := rules.Create(fake.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) +} + +func TestRequiredCreateOpts(t *testing.T) { + res := rules.Create(fake.ServiceClient(), rules.CreateOpts{Direction: rules.DirIngress}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = rules.Create(fake.ServiceClient(), rules.CreateOpts{Direction: rules.DirIngress, EtherType: rules.EtherType4}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = rules.Create(fake.ServiceClient(), rules.CreateOpts{Direction: rules.DirIngress, EtherType: rules.EtherType4}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = rules.Create(fake.ServiceClient(), rules.CreateOpts{Direction: rules.DirIngress, EtherType: rules.EtherType4, SecGroupID: "something", Protocol: "foo"}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-group-rules/3c0e45ff-adaf-4124-b083-bf390e5482ff", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_group_rule": { + "direction": "egress", + "ethertype": "IPv6", + "id": "3c0e45ff-adaf-4124-b083-bf390e5482ff", + "port_range_max": null, + "port_range_min": null, + "protocol": null, + "remote_group_id": null, + "remote_ip_prefix": null, + "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } +} + `) + }) + + sr, err := rules.Get(fake.ServiceClient(), "3c0e45ff-adaf-4124-b083-bf390e5482ff").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "egress", sr.Direction) + th.AssertEquals(t, "IPv6", sr.EtherType) + th.AssertEquals(t, "3c0e45ff-adaf-4124-b083-bf390e5482ff", sr.ID) + th.AssertEquals(t, 0, sr.PortRangeMax) + th.AssertEquals(t, 0, sr.PortRangeMin) + th.AssertEquals(t, "", sr.Protocol) + th.AssertEquals(t, "", sr.RemoteGroupID) + th.AssertEquals(t, "", sr.RemoteIPPrefix) + th.AssertEquals(t, "85cc3048-abc3-43cc-89b3-377341426ac5", sr.SecGroupID) + th.AssertEquals(t, "e4f50856753b4dc6afee5fa6b9b6c550", sr.TenantID) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-group-rules/4ec89087-d057-4e2c-911f-60a3b47ee304", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := rules.Delete(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304") + th.AssertNoErr(t, res.Err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules/urls.go new file mode 100644 index 0000000000..a5ede0e89b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules/urls.go @@ -0,0 +1,13 @@ +package rules + +import "github.com/gophercloud/gophercloud" + +const rootPath = "security-group-rules" + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/testing/delegate_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/testing/delegate_test.go new file mode 100644 index 0000000000..20a85f95b2 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/testing/delegate_test.go @@ -0,0 +1,106 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + common "github.com/gophercloud/gophercloud/openstack/common/extensions" + fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/extensions", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + + fmt.Fprintf(w, ` +{ + "extensions": [ + { + "updated": "2013-01-20T00:00:00-00:00", + "name": "Neutron Service Type Management", + "links": [], + "namespace": "http://docs.openstack.org/ext/neutron/service-type/api/v1.0", + "alias": "service-type", + "description": "API for retrieving service providers for Neutron advanced services" + } + ] +} + `) + }) + + count := 0 + + extensions.List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := extensions.ExtractExtensions(page) + if err != nil { + t.Errorf("Failed to extract extensions: %v", err) + } + + expected := []extensions.Extension{ + { + Extension: common.Extension{ + Updated: "2013-01-20T00:00:00-00:00", + Name: "Neutron Service Type Management", + Links: []interface{}{}, + Namespace: "http://docs.openstack.org/ext/neutron/service-type/api/v1.0", + Alias: "service-type", + Description: "API for retrieving service providers for Neutron advanced services", + }, + }, + } + + th.AssertDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/extensions/agent", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "extension": { + "updated": "2013-02-03T10:00:00-00:00", + "name": "agent", + "links": [], + "namespace": "http://docs.openstack.org/ext/agent/api/v2.0", + "alias": "agent", + "description": "The agent management extension." + } +} + `) + }) + + ext, err := extensions.Get(fake.ServiceClient(), "agent").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, ext.Updated, "2013-02-03T10:00:00-00:00") + th.AssertEquals(t, ext.Name, "agent") + th.AssertEquals(t, ext.Namespace, "http://docs.openstack.org/ext/agent/api/v2.0") + th.AssertEquals(t, ext.Alias, "agent") + th.AssertEquals(t, ext.Description, "The agent management extension.") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/testing/doc.go new file mode 100644 index 0000000000..5a104fbc86 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/testing/doc.go @@ -0,0 +1,2 @@ +// networking_extensions_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/doc.go new file mode 100644 index 0000000000..c87a7ce270 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/doc.go @@ -0,0 +1,9 @@ +// Package networks contains functionality for working with Neutron network +// resources. A network is an isolated virtual layer-2 broadcast domain that is +// typically reserved for the tenant who created it (unless you configure the +// network to be shared). Tenants can create multiple networks until the +// thresholds per-tenant quota is reached. +// +// In the v2.0 Networking API, the network is the main entity. Ports and subnets +// are always associated with a network. +package networks diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/requests.go new file mode 100644 index 0000000000..876a00bb0b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/requests.go @@ -0,0 +1,168 @@ +package networks + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToNetworkListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the network attributes you want to see returned. SortKey allows you to sort +// by a particular network attribute. SortDir sets the direction, and is either +// `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Status string `q:"status"` + Name string `q:"name"` + AdminStateUp *bool `q:"admin_state_up"` + TenantID string `q:"tenant_id"` + Shared *bool `q:"shared"` + ID string `q:"id"` + Marker string `q:"marker"` + Limit int `q:"limit"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToNetworkListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToNetworkListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// networks. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToNetworkListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return NetworkPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific network based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(getURL(c, id), &r.Body, nil) + return +} + +// CreateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Create operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type CreateOptsBuilder interface { + ToNetworkCreateMap() (map[string]interface{}, error) +} + +// CreateOpts satisfies the CreateOptsBuilder interface +type CreateOpts struct { + AdminStateUp *bool `json:"admin_state_up,omitempty"` + Name string `json:"name,omitempty"` + Shared *bool `json:"shared,omitempty"` + TenantID string `json:"tenant_id,omitempty"` +} + +// ToNetworkCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToNetworkCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "network") +} + +// Create accepts a CreateOpts struct and creates a new network using the values +// provided. This operation does not actually require a request body, i.e. the +// CreateOpts struct argument can be empty. +// +// The tenant ID that is contained in the URI is the tenant that creates the +// network. An admin user, however, has the option of specifying another tenant +// ID in the CreateOpts struct. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToNetworkCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(createURL(c), b, &r.Body, nil) + return +} + +// UpdateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Update operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type UpdateOptsBuilder interface { + ToNetworkUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts satisfies the UpdateOptsBuilder interface +type UpdateOpts struct { + AdminStateUp *bool `json:"admin_state_up,omitempty"` + Name string `json:"name,omitempty"` + Shared *bool `json:"shared,omitempty"` +} + +// ToNetworkUpdateMap casts a UpdateOpts struct to a map. +func (opts UpdateOpts) ToNetworkUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "network") +} + +// Update accepts a UpdateOpts struct and updates an existing network using the +// values provided. For more information, see the Create function. +func Update(c *gophercloud.ServiceClient, networkID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToNetworkUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(updateURL(c, networkID), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + return +} + +// Delete accepts a unique ID and deletes the network associated with it. +func Delete(c *gophercloud.ServiceClient, networkID string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, networkID), nil) + return +} + +// IDFromName is a convenience function that returns a network's ID given its name. +func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + pages, err := List(client, nil).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractNetworks(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", gophercloud.ErrResourceNotFound{Name: name, ResourceType: "network"} + case 1: + return id, nil + default: + return "", gophercloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "network"} + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/results.go new file mode 100644 index 0000000000..d9289800fd --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/results.go @@ -0,0 +1,101 @@ +package networks + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a network resource. +func (r commonResult) Extract() (*Network, error) { + var s struct { + Network *Network `json:"network"` + } + err := r.ExtractInto(&s) + return s.Network, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// Network represents, well, a network. +type Network struct { + // UUID for the network + ID string `json:"id"` + + // Human-readable name for the network. Might not be unique. + Name string `json:"name"` + + // The administrative state of network. If false (down), the network does not forward packets. + AdminStateUp bool `json:"admin_state_up"` + + // Indicates whether network is currently operational. Possible values include + // `ACTIVE', `DOWN', `BUILD', or `ERROR'. Plug-ins might define additional values. + Status string `json:"status"` + + // Subnets associated with this network. + Subnets []string `json:"subnets"` + + // Owner of network. Only admin users can specify a tenant_id other than its own. + TenantID string `json:"tenant_id"` + + // Specifies whether the network resource can be accessed by any tenant or not. + Shared bool `json:"shared"` +} + +// NetworkPage is the page returned by a pager when traversing over a +// collection of networks. +type NetworkPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of networks has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r NetworkPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"networks_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a NetworkPage struct is empty. +func (r NetworkPage) IsEmpty() (bool, error) { + is, err := ExtractNetworks(r) + return len(is) == 0, err +} + +// ExtractNetworks accepts a Page struct, specifically a NetworkPage struct, +// and extracts the elements into a slice of Network structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractNetworks(r pagination.Page) ([]Network, error) { + var s struct { + Networks []Network `json:"networks"` + } + err := (r.(NetworkPage)).ExtractInto(&s) + return s.Networks, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/testing/doc.go new file mode 100644 index 0000000000..860bd7a972 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/testing/doc.go @@ -0,0 +1,2 @@ +// networking_networks_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/testing/requests_test.go new file mode 100644 index 0000000000..5b9f03d9e4 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/testing/requests_test.go @@ -0,0 +1,277 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common" + "github.com/gophercloud/gophercloud/openstack/networking/v2/networks" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "networks": [ + { + "status": "ACTIVE", + "subnets": [ + "54d6f61d-db07-451c-9ab3-b9609b6b6f0b" + ], + "name": "private-network", + "admin_state_up": true, + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "shared": true, + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22" + }, + { + "status": "ACTIVE", + "subnets": [ + "08eae331-0402-425a-923c-34f7cfe39c1b" + ], + "name": "private", + "admin_state_up": true, + "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e", + "shared": true, + "id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324" + } + ] +} + `) + }) + + client := fake.ServiceClient() + count := 0 + + networks.List(client, networks.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := networks.ExtractNetworks(page) + if err != nil { + t.Errorf("Failed to extract networks: %v", err) + return false, err + } + + expected := []networks.Network{ + { + Status: "ACTIVE", + Subnets: []string{"54d6f61d-db07-451c-9ab3-b9609b6b6f0b"}, + Name: "private-network", + AdminStateUp: true, + TenantID: "4fd44f30292945e481c7b8a0c8908869", + Shared: true, + ID: "d32019d3-bc6e-4319-9c1d-6722fc136a22", + }, + { + Status: "ACTIVE", + Subnets: []string{"08eae331-0402-425a-923c-34f7cfe39c1b"}, + Name: "private", + AdminStateUp: true, + TenantID: "26a7980765d0414dbc1fc1f88cdb7e6e", + Shared: true, + ID: "db193ab3-96e3-4cb3-8fc5-05f4296d0324", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/networks/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "network": { + "status": "ACTIVE", + "subnets": [ + "54d6f61d-db07-451c-9ab3-b9609b6b6f0b" + ], + "name": "private-network", + "admin_state_up": true, + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "shared": true, + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22" + } +} + `) + }) + + n, err := networks.Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Status, "ACTIVE") + th.AssertDeepEquals(t, n.Subnets, []string{"54d6f61d-db07-451c-9ab3-b9609b6b6f0b"}) + th.AssertEquals(t, n.Name, "private-network") + th.AssertEquals(t, n.AdminStateUp, true) + th.AssertEquals(t, n.TenantID, "4fd44f30292945e481c7b8a0c8908869") + th.AssertEquals(t, n.Shared, true) + th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "network": { + "name": "sample_network", + "admin_state_up": true + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "network": { + "status": "ACTIVE", + "subnets": [], + "name": "net1", + "admin_state_up": true, + "tenant_id": "9bacb3c5d39d41a79512987f338cf177", + "shared": false, + "id": "4e8e5957-649f-477b-9e5b-f1f75b21c03c" + } +} + `) + }) + + iTrue := true + options := networks.CreateOpts{Name: "sample_network", AdminStateUp: &iTrue} + n, err := networks.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Status, "ACTIVE") + th.AssertDeepEquals(t, n.Subnets, []string{}) + th.AssertEquals(t, n.Name, "net1") + th.AssertEquals(t, n.AdminStateUp, true) + th.AssertEquals(t, n.TenantID, "9bacb3c5d39d41a79512987f338cf177") + th.AssertEquals(t, n.Shared, false) + th.AssertEquals(t, n.ID, "4e8e5957-649f-477b-9e5b-f1f75b21c03c") +} + +func TestCreateWithOptionalFields(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "network": { + "name": "sample_network", + "admin_state_up": true, + "shared": true, + "tenant_id": "12345" + } +} + `) + + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `{}`) + }) + + iTrue := true + options := networks.CreateOpts{Name: "sample_network", AdminStateUp: &iTrue, Shared: &iTrue, TenantID: "12345"} + _, err := networks.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "network": { + "name": "new_network_name", + "admin_state_up": false, + "shared": true + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "network": { + "status": "ACTIVE", + "subnets": [], + "name": "new_network_name", + "admin_state_up": false, + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "shared": true, + "id": "4e8e5957-649f-477b-9e5b-f1f75b21c03c" + } +} + `) + }) + + iTrue, iFalse := true, false + options := networks.UpdateOpts{Name: "new_network_name", AdminStateUp: &iFalse, Shared: &iTrue} + n, err := networks.Update(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Name, "new_network_name") + th.AssertEquals(t, n.AdminStateUp, false) + th.AssertEquals(t, n.Shared, true) + th.AssertEquals(t, n.ID, "4e8e5957-649f-477b-9e5b-f1f75b21c03c") +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := networks.Delete(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c") + th.AssertNoErr(t, res.Err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/urls.go new file mode 100644 index 0000000000..4a8fb1dc7d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/urls.go @@ -0,0 +1,31 @@ +package networks + +import "github.com/gophercloud/gophercloud" + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("networks", id) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("networks") +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func listURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func createURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/doc.go new file mode 100644 index 0000000000..f16a4bb01b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/doc.go @@ -0,0 +1,8 @@ +// Package ports contains functionality for working with Neutron port resources. +// A port represents a virtual switch port on a logical network switch. Virtual +// instances attach their interfaces into ports. The logical port also defines +// the MAC address and the IP address(es) to be assigned to the interfaces +// plugged into them. When IP addresses are associated to a port, this also +// implies the port is associated with a subnet, as the IP address was taken +// from the allocation pool for a specific subnet. +package ports diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/requests.go new file mode 100644 index 0000000000..d353b7ed38 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/requests.go @@ -0,0 +1,180 @@ +package ports + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToPortListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the port attributes you want to see returned. SortKey allows you to sort +// by a particular port attribute. SortDir sets the direction, and is either +// `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Status string `q:"status"` + Name string `q:"name"` + AdminStateUp *bool `q:"admin_state_up"` + NetworkID string `q:"network_id"` + TenantID string `q:"tenant_id"` + DeviceOwner string `q:"device_owner"` + MACAddress string `q:"mac_address"` + ID string `q:"id"` + DeviceID string `q:"device_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToPortListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToPortListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// ports. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those ports that are owned by the tenant +// who submits the request, unless the request is submitted by a user with +// administrative rights. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToPortListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return PortPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific port based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(getURL(c, id), &r.Body, nil) + return +} + +// CreateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Create operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type CreateOptsBuilder interface { + ToPortCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents the attributes used when creating a new port. +type CreateOpts struct { + NetworkID string `json:"network_id" required:"true"` + Name string `json:"name,omitempty"` + AdminStateUp *bool `json:"admin_state_up,omitempty"` + MACAddress string `json:"mac_address,omitempty"` + FixedIPs interface{} `json:"fixed_ips,omitempty"` + DeviceID string `json:"device_id,omitempty"` + DeviceOwner string `json:"device_owner,omitempty"` + TenantID string `json:"tenant_id,omitempty"` + SecurityGroups []string `json:"security_groups,omitempty"` + AllowedAddressPairs []AddressPair `json:"allowed_address_pairs,omitempty"` +} + +// ToPortCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToPortCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "port") +} + +// Create accepts a CreateOpts struct and creates a new network using the values +// provided. You must remember to provide a NetworkID value. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToPortCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(createURL(c), b, &r.Body, nil) + return +} + +// UpdateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Update operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type UpdateOptsBuilder interface { + ToPortUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents the attributes used when updating an existing port. +type UpdateOpts struct { + Name string `json:"name,omitempty"` + AdminStateUp *bool `json:"admin_state_up,omitempty"` + FixedIPs interface{} `json:"fixed_ips,omitempty"` + DeviceID string `json:"device_id,omitempty"` + DeviceOwner string `json:"device_owner,omitempty"` + SecurityGroups []string `json:"security_groups"` + AllowedAddressPairs []AddressPair `json:"allowed_address_pairs"` +} + +// ToPortUpdateMap casts an UpdateOpts struct to a map. +func (opts UpdateOpts) ToPortUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "port") +} + +// Update accepts a UpdateOpts struct and updates an existing port using the +// values provided. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToPortUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(updateURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + return +} + +// Delete accepts a unique ID and deletes the port associated with it. +func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, id), nil) + return +} + +// IDFromName is a convenience function that returns a port's ID given its name. +func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + pages, err := List(client, nil).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractPorts(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", gophercloud.ErrResourceNotFound{Name: name, ResourceType: "port"} + case 1: + return id, nil + default: + return "", gophercloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "port"} + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/results.go new file mode 100644 index 0000000000..57a1765c6c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/results.go @@ -0,0 +1,119 @@ +package ports + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a port resource. +func (r commonResult) Extract() (*Port, error) { + var s struct { + Port *Port `json:"port"` + } + err := r.ExtractInto(&s) + return s.Port, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// IP is a sub-struct that represents an individual IP. +type IP struct { + SubnetID string `json:"subnet_id"` + IPAddress string `json:"ip_address,omitempty"` +} + +// AddressPair contains the IP Address and the MAC address. +type AddressPair struct { + IPAddress string `json:"ip_address,omitempty"` + MACAddress string `json:"mac_address,omitempty"` +} + +// Port represents a Neutron port. See package documentation for a top-level +// description of what this is. +type Port struct { + // UUID for the port. + ID string `json:"id"` + // Network that this port is associated with. + NetworkID string `json:"network_id"` + // Human-readable name for the port. Might not be unique. + Name string `json:"name"` + // Administrative state of port. If false (down), port does not forward packets. + AdminStateUp bool `json:"admin_state_up"` + // Indicates whether network is currently operational. Possible values include + // `ACTIVE', `DOWN', `BUILD', or `ERROR'. Plug-ins might define additional values. + Status string `json:"status"` + // Mac address to use on this port. + MACAddress string `json:"mac_address"` + // Specifies IP addresses for the port thus associating the port itself with + // the subnets where the IP addresses are picked from + FixedIPs []IP `json:"fixed_ips"` + // Owner of network. Only admin users can specify a tenant_id other than its own. + TenantID string `json:"tenant_id"` + // Identifies the entity (e.g.: dhcp agent) using this port. + DeviceOwner string `json:"device_owner"` + // Specifies the IDs of any security groups associated with a port. + SecurityGroups []string `json:"security_groups"` + // Identifies the device (e.g., virtual server) using this port. + DeviceID string `json:"device_id"` + // Identifies the list of IP addresses the port will recognize/accept + AllowedAddressPairs []AddressPair `json:"allowed_address_pairs"` +} + +// PortPage is the page returned by a pager when traversing over a collection +// of network ports. +type PortPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of ports has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r PortPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"ports_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a PortPage struct is empty. +func (r PortPage) IsEmpty() (bool, error) { + is, err := ExtractPorts(r) + return len(is) == 0, err +} + +// ExtractPorts accepts a Page struct, specifically a PortPage struct, +// and extracts the elements into a slice of Port structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractPorts(r pagination.Page) ([]Port, error) { + var s struct { + Ports []Port `json:"ports"` + } + err := (r.(PortPage)).ExtractInto(&s) + return s.Ports, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/testing/doc.go new file mode 100644 index 0000000000..70a559a1c7 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/testing/doc.go @@ -0,0 +1,2 @@ +// networking_ports_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/testing/requests_test.go new file mode 100644 index 0000000000..1da6ad3a3d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/testing/requests_test.go @@ -0,0 +1,519 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common" + "github.com/gophercloud/gophercloud/openstack/networking/v2/ports" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/ports", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "ports": [ + { + "status": "ACTIVE", + "binding:host_id": "devstack", + "name": "", + "admin_state_up": true, + "network_id": "70c1db1f-b701-45bd-96e0-a313ee3430b3", + "tenant_id": "", + "device_owner": "network:router_gateway", + "mac_address": "fa:16:3e:58:42:ed", + "fixed_ips": [ + { + "subnet_id": "008ba151-0b8c-4a67-98b5-0d2b87666062", + "ip_address": "172.24.4.2" + } + ], + "id": "d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b", + "security_groups": [], + "device_id": "9ae135f4-b6e0-4dad-9e91-3c223e385824" + } + ] +} + `) + }) + + count := 0 + + ports.List(fake.ServiceClient(), ports.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ports.ExtractPorts(page) + if err != nil { + t.Errorf("Failed to extract subnets: %v", err) + return false, nil + } + + expected := []ports.Port{ + { + Status: "ACTIVE", + Name: "", + AdminStateUp: true, + NetworkID: "70c1db1f-b701-45bd-96e0-a313ee3430b3", + TenantID: "", + DeviceOwner: "network:router_gateway", + MACAddress: "fa:16:3e:58:42:ed", + FixedIPs: []ports.IP{ + { + SubnetID: "008ba151-0b8c-4a67-98b5-0d2b87666062", + IPAddress: "172.24.4.2", + }, + }, + ID: "d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b", + SecurityGroups: []string{}, + DeviceID: "9ae135f4-b6e0-4dad-9e91-3c223e385824", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/ports/46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "port": { + "status": "ACTIVE", + "name": "", + "admin_state_up": true, + "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7", + "tenant_id": "7e02058126cc4950b75f9970368ba177", + "device_owner": "network:router_interface", + "mac_address": "fa:16:3e:23:fd:d7", + "fixed_ips": [ + { + "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2", + "ip_address": "10.0.0.1" + } + ], + "id": "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2", + "security_groups": [], + "device_id": "5e3898d7-11be-483e-9732-b2f5eccd2b2e" + } +} + `) + }) + + n, err := ports.Get(fake.ServiceClient(), "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Status, "ACTIVE") + th.AssertEquals(t, n.Name, "") + th.AssertEquals(t, n.AdminStateUp, true) + th.AssertEquals(t, n.NetworkID, "a87cc70a-3e15-4acf-8205-9b711a3531b7") + th.AssertEquals(t, n.TenantID, "7e02058126cc4950b75f9970368ba177") + th.AssertEquals(t, n.DeviceOwner, "network:router_interface") + th.AssertEquals(t, n.MACAddress, "fa:16:3e:23:fd:d7") + th.AssertDeepEquals(t, n.FixedIPs, []ports.IP{ + {SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.1"}, + }) + th.AssertEquals(t, n.ID, "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2") + th.AssertDeepEquals(t, n.SecurityGroups, []string{}) + th.AssertEquals(t, n.Status, "ACTIVE") + th.AssertEquals(t, n.DeviceID, "5e3898d7-11be-483e-9732-b2f5eccd2b2e") +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/ports", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "port": { + "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7", + "name": "private-port", + "admin_state_up": true, + "fixed_ips": [ + { + "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2", + "ip_address": "10.0.0.2" + } + ], + "security_groups": ["foo"], + "allowed_address_pairs": [ + { + "ip_address": "10.0.0.4", + "mac_address": "fa:16:3e:c9:cb:f0" + } + ] + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "port": { + "status": "DOWN", + "name": "private-port", + "admin_state_up": true, + "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7", + "tenant_id": "d6700c0c9ffa4f1cb322cd4a1f3906fa", + "device_owner": "", + "mac_address": "fa:16:3e:c9:cb:f0", + "fixed_ips": [ + { + "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2", + "ip_address": "10.0.0.2" + } + ], + "id": "65c0ee9f-d634-4522-8954-51021b570b0d", + "security_groups": [ + "f0ac4394-7e4a-4409-9701-ba8be283dbc3" + ], + "allowed_address_pairs": [ + { + "ip_address": "10.0.0.4", + "mac_address": "fa:16:3e:c9:cb:f0" + } + ], + "device_id": "" + } +} + `) + }) + + asu := true + options := ports.CreateOpts{ + Name: "private-port", + AdminStateUp: &asu, + NetworkID: "a87cc70a-3e15-4acf-8205-9b711a3531b7", + FixedIPs: []ports.IP{ + {SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.2"}, + }, + SecurityGroups: []string{"foo"}, + AllowedAddressPairs: []ports.AddressPair{ + {IPAddress: "10.0.0.4", MACAddress: "fa:16:3e:c9:cb:f0"}, + }, + } + n, err := ports.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Status, "DOWN") + th.AssertEquals(t, n.Name, "private-port") + th.AssertEquals(t, n.AdminStateUp, true) + th.AssertEquals(t, n.NetworkID, "a87cc70a-3e15-4acf-8205-9b711a3531b7") + th.AssertEquals(t, n.TenantID, "d6700c0c9ffa4f1cb322cd4a1f3906fa") + th.AssertEquals(t, n.DeviceOwner, "") + th.AssertEquals(t, n.MACAddress, "fa:16:3e:c9:cb:f0") + th.AssertDeepEquals(t, n.FixedIPs, []ports.IP{ + {SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.2"}, + }) + th.AssertEquals(t, n.ID, "65c0ee9f-d634-4522-8954-51021b570b0d") + th.AssertDeepEquals(t, n.SecurityGroups, []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"}) + th.AssertDeepEquals(t, n.AllowedAddressPairs, []ports.AddressPair{ + {IPAddress: "10.0.0.4", MACAddress: "fa:16:3e:c9:cb:f0"}, + }) +} + +func TestRequiredCreateOpts(t *testing.T) { + res := ports.Create(fake.ServiceClient(), ports.CreateOpts{}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/ports/65c0ee9f-d634-4522-8954-51021b570b0d", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "port": { + "name": "new_port_name", + "fixed_ips": [ + { + "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2", + "ip_address": "10.0.0.3" + } + ], + "allowed_address_pairs": [ + { + "ip_address": "10.0.0.4", + "mac_address": "fa:16:3e:c9:cb:f0" + } + ], + "security_groups": [ + "f0ac4394-7e4a-4409-9701-ba8be283dbc3" + ] + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "port": { + "status": "DOWN", + "name": "new_port_name", + "admin_state_up": true, + "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7", + "tenant_id": "d6700c0c9ffa4f1cb322cd4a1f3906fa", + "device_owner": "", + "mac_address": "fa:16:3e:c9:cb:f0", + "fixed_ips": [ + { + "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2", + "ip_address": "10.0.0.3" + } + ], + "allowed_address_pairs": [ + { + "ip_address": "10.0.0.4", + "mac_address": "fa:16:3e:c9:cb:f0" + } + ], + "id": "65c0ee9f-d634-4522-8954-51021b570b0d", + "security_groups": [ + "f0ac4394-7e4a-4409-9701-ba8be283dbc3" + ], + "device_id": "" + } +} + `) + }) + + options := ports.UpdateOpts{ + Name: "new_port_name", + FixedIPs: []ports.IP{ + {SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"}, + }, + SecurityGroups: []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"}, + AllowedAddressPairs: []ports.AddressPair{ + {IPAddress: "10.0.0.4", MACAddress: "fa:16:3e:c9:cb:f0"}, + }, + } + + s, err := ports.Update(fake.ServiceClient(), "65c0ee9f-d634-4522-8954-51021b570b0d", options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, s.Name, "new_port_name") + th.AssertDeepEquals(t, s.FixedIPs, []ports.IP{ + {SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"}, + }) + th.AssertDeepEquals(t, s.AllowedAddressPairs, []ports.AddressPair{ + {IPAddress: "10.0.0.4", MACAddress: "fa:16:3e:c9:cb:f0"}, + }) + th.AssertDeepEquals(t, s.SecurityGroups, []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"}) +} + +func TestRemoveSecurityGroups(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/ports/65c0ee9f-d634-4522-8954-51021b570b0d", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "port": { + "name": "new_port_name", + "fixed_ips": [ + { + "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2", + "ip_address": "10.0.0.3" + } + ], + "allowed_address_pairs": [ + { + "ip_address": "10.0.0.4", + "mac_address": "fa:16:3e:c9:cb:f0" + } + ], + "security_groups": [] + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "port": { + "status": "DOWN", + "name": "new_port_name", + "admin_state_up": true, + "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7", + "tenant_id": "d6700c0c9ffa4f1cb322cd4a1f3906fa", + "device_owner": "", + "mac_address": "fa:16:3e:c9:cb:f0", + "fixed_ips": [ + { + "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2", + "ip_address": "10.0.0.3" + } + ], + "allowed_address_pairs": [ + { + "ip_address": "10.0.0.4", + "mac_address": "fa:16:3e:c9:cb:f0" + } + ], + "id": "65c0ee9f-d634-4522-8954-51021b570b0d", + "device_id": "" + } +} + `) + }) + + options := ports.UpdateOpts{ + Name: "new_port_name", + FixedIPs: []ports.IP{ + {SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"}, + }, + SecurityGroups: []string{}, + AllowedAddressPairs: []ports.AddressPair{ + {IPAddress: "10.0.0.4", MACAddress: "fa:16:3e:c9:cb:f0"}, + }, + } + + s, err := ports.Update(fake.ServiceClient(), "65c0ee9f-d634-4522-8954-51021b570b0d", options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, s.Name, "new_port_name") + th.AssertDeepEquals(t, s.FixedIPs, []ports.IP{ + {SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"}, + }) + th.AssertDeepEquals(t, s.AllowedAddressPairs, []ports.AddressPair{ + {IPAddress: "10.0.0.4", MACAddress: "fa:16:3e:c9:cb:f0"}, + }) + th.AssertDeepEquals(t, s.SecurityGroups, []string(nil)) +} + +func TestRemoveAllowedAddressPairs(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/ports/65c0ee9f-d634-4522-8954-51021b570b0d", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "port": { + "name": "new_port_name", + "fixed_ips": [ + { + "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2", + "ip_address": "10.0.0.3" + } + ], + "allowed_address_pairs": [], + "security_groups": [ + "f0ac4394-7e4a-4409-9701-ba8be283dbc3" + ] + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "port": { + "status": "DOWN", + "name": "new_port_name", + "admin_state_up": true, + "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7", + "tenant_id": "d6700c0c9ffa4f1cb322cd4a1f3906fa", + "device_owner": "", + "mac_address": "fa:16:3e:c9:cb:f0", + "fixed_ips": [ + { + "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2", + "ip_address": "10.0.0.3" + } + ], + "id": "65c0ee9f-d634-4522-8954-51021b570b0d", + "security_groups": [ + "f0ac4394-7e4a-4409-9701-ba8be283dbc3" + ], + "device_id": "" + } +} + `) + }) + + options := ports.UpdateOpts{ + Name: "new_port_name", + FixedIPs: []ports.IP{ + {SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"}, + }, + SecurityGroups: []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"}, + AllowedAddressPairs: []ports.AddressPair{}, + } + + s, err := ports.Update(fake.ServiceClient(), "65c0ee9f-d634-4522-8954-51021b570b0d", options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, s.Name, "new_port_name") + th.AssertDeepEquals(t, s.FixedIPs, []ports.IP{ + {SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"}, + }) + th.AssertDeepEquals(t, s.AllowedAddressPairs, []ports.AddressPair(nil)) + th.AssertDeepEquals(t, s.SecurityGroups, []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"}) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/ports/65c0ee9f-d634-4522-8954-51021b570b0d", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := ports.Delete(fake.ServiceClient(), "65c0ee9f-d634-4522-8954-51021b570b0d") + th.AssertNoErr(t, res.Err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/urls.go new file mode 100644 index 0000000000..600d6f2fd9 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/urls.go @@ -0,0 +1,31 @@ +package ports + +import "github.com/gophercloud/gophercloud" + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("ports", id) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("ports") +} + +func listURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func createURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/doc.go new file mode 100644 index 0000000000..43e8296c7f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/doc.go @@ -0,0 +1,10 @@ +// Package subnets contains functionality for working with Neutron subnet +// resources. A subnet represents an IP address block that can be used to +// assign IP addresses to virtual instances. Each subnet must have a CIDR and +// must be associated with a network. IPs can either be selected from the whole +// subnet CIDR or from allocation pools specified by the user. +// +// A subnet can also have a gateway, a list of DNS name servers, and host routes. +// This information is pushed to instances whose interfaces are associated with +// the subnet. +package subnets diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/requests.go new file mode 100644 index 0000000000..ec3769554b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/requests.go @@ -0,0 +1,194 @@ +package subnets + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToSubnetListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the subnet attributes you want to see returned. SortKey allows you to sort +// by a particular subnet attribute. SortDir sets the direction, and is either +// `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Name string `q:"name"` + EnableDHCP *bool `q:"enable_dhcp"` + NetworkID string `q:"network_id"` + TenantID string `q:"tenant_id"` + IPVersion int `q:"ip_version"` + GatewayIP string `q:"gateway_ip"` + CIDR string `q:"cidr"` + ID string `q:"id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToSubnetListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToSubnetListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// subnets. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those subnets that are owned by the tenant +// who submits the request, unless the request is submitted by a user with +// administrative rights. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToSubnetListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return SubnetPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific subnet based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(getURL(c, id), &r.Body, nil) + return +} + +// CreateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Create operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type CreateOptsBuilder interface { + ToSubnetCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents the attributes used when creating a new subnet. +type CreateOpts struct { + NetworkID string `json:"network_id" required:"true"` + CIDR string `json:"cidr" required:"true"` + Name string `json:"name,omitempty"` + TenantID string `json:"tenant_id,omitempty"` + AllocationPools []AllocationPool `json:"allocation_pools,omitempty"` + GatewayIP *string `json:"gateway_ip,omitempty"` + IPVersion gophercloud.IPVersion `json:"ip_version,omitempty"` + EnableDHCP *bool `json:"enable_dhcp,omitempty"` + DNSNameservers []string `json:"dns_nameservers,omitempty"` + HostRoutes []HostRoute `json:"host_routes,omitempty"` +} + +// ToSubnetCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToSubnetCreateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "subnet") + if err != nil { + return nil, err + } + + if m := b["subnet"].(map[string]interface{}); m["gateway_ip"] == "" { + m["gateway_ip"] = nil + } + + return b, nil +} + +// Create accepts a CreateOpts struct and creates a new subnet using the values +// provided. You must remember to provide a valid NetworkID, CIDR and IP version. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToSubnetCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(createURL(c), b, &r.Body, nil) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToSubnetUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents the attributes used when updating an existing subnet. +type UpdateOpts struct { + Name string `json:"name,omitempty"` + AllocationPools []AllocationPool `json:"allocation_pools,omitempty"` + GatewayIP *string `json:"gateway_ip,omitempty"` + DNSNameservers []string `json:"dns_nameservers,omitempty"` + HostRoutes []HostRoute `json:"host_routes,omitempty"` + EnableDHCP *bool `json:"enable_dhcp,omitempty"` +} + +// ToSubnetUpdateMap casts an UpdateOpts struct to a map. +func (opts UpdateOpts) ToSubnetUpdateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "subnet") + if err != nil { + return nil, err + } + + if m := b["subnet"].(map[string]interface{}); m["gateway_ip"] == "" { + m["gateway_ip"] = nil + } + + return b, nil +} + +// Update accepts a UpdateOpts struct and updates an existing subnet using the +// values provided. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToSubnetUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(updateURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + return +} + +// Delete accepts a unique ID and deletes the subnet associated with it. +func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, id), nil) + return +} + +// IDFromName is a convenience function that returns a subnet's ID given its name. +func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + pages, err := List(client, nil).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractSubnets(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", gophercloud.ErrResourceNotFound{Name: name, ResourceType: "subnet"} + case 1: + return id, nil + default: + return "", gophercloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "subnet"} + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/results.go new file mode 100644 index 0000000000..ab5cce124e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/results.go @@ -0,0 +1,117 @@ +package subnets + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a subnet resource. +func (r commonResult) Extract() (*Subnet, error) { + var s struct { + Subnet *Subnet `json:"subnet"` + } + err := r.ExtractInto(&s) + return s.Subnet, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// AllocationPool represents a sub-range of cidr available for dynamic +// allocation to ports, e.g. {Start: "10.0.0.2", End: "10.0.0.254"} +type AllocationPool struct { + Start string `json:"start"` + End string `json:"end"` +} + +// HostRoute represents a route that should be used by devices with IPs from +// a subnet (not including local subnet route). +type HostRoute struct { + DestinationCIDR string `json:"destination"` + NextHop string `json:"nexthop"` +} + +// Subnet represents a subnet. See package documentation for a top-level +// description of what this is. +type Subnet struct { + // UUID representing the subnet + ID string `json:"id"` + // UUID of the parent network + NetworkID string `json:"network_id"` + // Human-readable name for the subnet. Might not be unique. + Name string `json:"name"` + // IP version, either `4' or `6' + IPVersion int `json:"ip_version"` + // CIDR representing IP range for this subnet, based on IP version + CIDR string `json:"cidr"` + // Default gateway used by devices in this subnet + GatewayIP string `json:"gateway_ip"` + // DNS name servers used by hosts in this subnet. + DNSNameservers []string `json:"dns_nameservers"` + // Sub-ranges of CIDR available for dynamic allocation to ports. See AllocationPool. + AllocationPools []AllocationPool `json:"allocation_pools"` + // Routes that should be used by devices with IPs from this subnet (not including local subnet route). + HostRoutes []HostRoute `json:"host_routes"` + // Specifies whether DHCP is enabled for this subnet or not. + EnableDHCP bool `json:"enable_dhcp"` + // Owner of network. Only admin users can specify a tenant_id other than its own. + TenantID string `json:"tenant_id"` +} + +// SubnetPage is the page returned by a pager when traversing over a collection +// of subnets. +type SubnetPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of subnets has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r SubnetPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"subnets_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a SubnetPage struct is empty. +func (r SubnetPage) IsEmpty() (bool, error) { + is, err := ExtractSubnets(r) + return len(is) == 0, err +} + +// ExtractSubnets accepts a Page struct, specifically a SubnetPage struct, +// and extracts the elements into a slice of Subnet structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractSubnets(r pagination.Page) ([]Subnet, error) { + var s struct { + Subnets []Subnet `json:"subnets"` + } + err := (r.(SubnetPage)).ExtractInto(&s) + return s.Subnets, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/testing/doc.go new file mode 100644 index 0000000000..43be31a693 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/testing/doc.go @@ -0,0 +1,2 @@ +// networking_subnets_v2 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/testing/fixtures.go new file mode 100644 index 0000000000..f9104c5aa3 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/testing/fixtures.go @@ -0,0 +1,399 @@ +package testing + +import ( + "github.com/gophercloud/gophercloud/openstack/networking/v2/subnets" +) + +const SubnetListResult = ` +{ + "subnets": [ + { + "name": "private-subnet", + "enable_dhcp": true, + "network_id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324", + "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e", + "dns_nameservers": [], + "allocation_pools": [ + { + "start": "10.0.0.2", + "end": "10.0.0.254" + } + ], + "host_routes": [], + "ip_version": 4, + "gateway_ip": "10.0.0.1", + "cidr": "10.0.0.0/24", + "id": "08eae331-0402-425a-923c-34f7cfe39c1b" + }, + { + "name": "my_subnet", + "enable_dhcp": true, + "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "dns_nameservers": [], + "allocation_pools": [ + { + "start": "192.0.0.2", + "end": "192.255.255.254" + } + ], + "host_routes": [], + "ip_version": 4, + "gateway_ip": "192.0.0.1", + "cidr": "192.0.0.0/8", + "id": "54d6f61d-db07-451c-9ab3-b9609b6b6f0b" + }, + { + "name": "my_gatewayless_subnet", + "enable_dhcp": true, + "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a23", + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "dns_nameservers": [], + "allocation_pools": [ + { + "start": "192.168.1.2", + "end": "192.168.1.254" + } + ], + "host_routes": [], + "ip_version": 4, + "gateway_ip": null, + "cidr": "192.168.1.0/24", + "id": "54d6f61d-db07-451c-9ab3-b9609b6b6f0c" + } + ] +} +` + +var Subnet1 = subnets.Subnet{ + Name: "private-subnet", + EnableDHCP: true, + NetworkID: "db193ab3-96e3-4cb3-8fc5-05f4296d0324", + TenantID: "26a7980765d0414dbc1fc1f88cdb7e6e", + DNSNameservers: []string{}, + AllocationPools: []subnets.AllocationPool{ + { + Start: "10.0.0.2", + End: "10.0.0.254", + }, + }, + HostRoutes: []subnets.HostRoute{}, + IPVersion: 4, + GatewayIP: "10.0.0.1", + CIDR: "10.0.0.0/24", + ID: "08eae331-0402-425a-923c-34f7cfe39c1b", +} + +var Subnet2 = subnets.Subnet{ + Name: "my_subnet", + EnableDHCP: true, + NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a22", + TenantID: "4fd44f30292945e481c7b8a0c8908869", + DNSNameservers: []string{}, + AllocationPools: []subnets.AllocationPool{ + { + Start: "192.0.0.2", + End: "192.255.255.254", + }, + }, + HostRoutes: []subnets.HostRoute{}, + IPVersion: 4, + GatewayIP: "192.0.0.1", + CIDR: "192.0.0.0/8", + ID: "54d6f61d-db07-451c-9ab3-b9609b6b6f0b", +} + +var Subnet3 = subnets.Subnet{ + Name: "my_gatewayless_subnet", + EnableDHCP: true, + NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a23", + TenantID: "4fd44f30292945e481c7b8a0c8908869", + DNSNameservers: []string{}, + AllocationPools: []subnets.AllocationPool{ + { + Start: "192.168.1.2", + End: "192.168.1.254", + }, + }, + HostRoutes: []subnets.HostRoute{}, + IPVersion: 4, + GatewayIP: "", + CIDR: "192.168.1.0/24", + ID: "54d6f61d-db07-451c-9ab3-b9609b6b6f0c", +} + +const SubnetGetResult = ` +{ + "subnet": { + "name": "my_subnet", + "enable_dhcp": true, + "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "dns_nameservers": [], + "allocation_pools": [ + { + "start": "192.0.0.2", + "end": "192.255.255.254" + } + ], + "host_routes": [], + "ip_version": 4, + "gateway_ip": "192.0.0.1", + "cidr": "192.0.0.0/8", + "id": "54d6f61d-db07-451c-9ab3-b9609b6b6f0b" + } +} +` + +const SubnetCreateRequest = ` +{ + "subnet": { + "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "ip_version": 4, + "gateway_ip": "192.168.199.1", + "cidr": "192.168.199.0/24", + "dns_nameservers": ["foo"], + "allocation_pools": [ + { + "start": "192.168.199.2", + "end": "192.168.199.254" + } + ], + "host_routes": [{"destination":"","nexthop": "bar"}] + } +} +` + +const SubnetCreateResult = ` +{ + "subnet": { + "name": "", + "enable_dhcp": true, + "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "dns_nameservers": [], + "allocation_pools": [ + { + "start": "192.168.199.2", + "end": "192.168.199.254" + } + ], + "host_routes": [], + "ip_version": 4, + "gateway_ip": "192.168.199.1", + "cidr": "192.168.199.0/24", + "id": "3b80198d-4f7b-4f77-9ef5-774d54e17126" + } +} +` + +const SubnetCreateWithNoGatewayRequest = ` +{ + "subnet": { + "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a23", + "ip_version": 4, + "cidr": "192.168.1.0/24", + "gateway_ip": null, + "allocation_pools": [ + { + "start": "192.168.1.2", + "end": "192.168.1.254" + } + ] + } +} +` + +const SubnetCreateWithNoGatewayResponse = ` +{ + "subnet": { + "name": "", + "enable_dhcp": true, + "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a23", + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "allocation_pools": [ + { + "start": "192.168.1.2", + "end": "192.168.1.254" + } + ], + "host_routes": [], + "ip_version": 4, + "gateway_ip": null, + "cidr": "192.168.1.0/24", + "id": "54d6f61d-db07-451c-9ab3-b9609b6b6f0c" + } +} +` + +const SubnetCreateWithDefaultGatewayRequest = ` +{ + "subnet": { + "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a23", + "ip_version": 4, + "cidr": "192.168.1.0/24", + "allocation_pools": [ + { + "start": "192.168.1.2", + "end": "192.168.1.254" + } + ] + } +} +` + +const SubnetCreateWithDefaultGatewayResponse = ` +{ + "subnet": { + "name": "", + "enable_dhcp": true, + "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a23", + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "allocation_pools": [ + { + "start": "192.168.1.2", + "end": "192.168.1.254" + } + ], + "host_routes": [], + "ip_version": 4, + "gateway_ip": "192.168.1.1", + "cidr": "192.168.1.0/24", + "id": "54d6f61d-db07-451c-9ab3-b9609b6b6f0c" + } +} +` + +const SubnetUpdateRequest = ` +{ + "subnet": { + "name": "my_new_subnet", + "dns_nameservers": ["foo"], + "host_routes": [{"destination":"","nexthop": "bar"}] + } +} +` + +const SubnetUpdateResponse = ` +{ + "subnet": { + "name": "my_new_subnet", + "enable_dhcp": true, + "network_id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324", + "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e", + "dns_nameservers": [], + "allocation_pools": [ + { + "start": "10.0.0.2", + "end": "10.0.0.254" + } + ], + "host_routes": [], + "ip_version": 4, + "gateway_ip": "10.0.0.1", + "cidr": "10.0.0.0/24", + "id": "08eae331-0402-425a-923c-34f7cfe39c1b" + } +} +` + +const SubnetUpdateGatewayRequest = ` +{ + "subnet": { + "name": "my_new_subnet", + "gateway_ip": "10.0.0.1" + } +} +` + +const SubnetUpdateGatewayResponse = ` +{ + "subnet": { + "name": "my_new_subnet", + "enable_dhcp": true, + "network_id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324", + "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e", + "dns_nameservers": [], + "allocation_pools": [ + { + "start": "10.0.0.2", + "end": "10.0.0.254" + } + ], + "host_routes": [], + "ip_version": 4, + "gateway_ip": "10.0.0.1", + "cidr": "10.0.0.0/24", + "id": "08eae331-0402-425a-923c-34f7cfe39c1b" + } +} +` + +const SubnetUpdateRemoveGatewayRequest = ` +{ + "subnet": { + "name": "my_new_subnet", + "gateway_ip": null + } +} +` + +const SubnetUpdateRemoveGatewayResponse = ` +{ + "subnet": { + "name": "my_new_subnet", + "enable_dhcp": true, + "network_id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324", + "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e", + "dns_nameservers": [], + "allocation_pools": [ + { + "start": "10.0.0.2", + "end": "10.0.0.254" + } + ], + "host_routes": [], + "ip_version": 4, + "gateway_ip": null, + "cidr": "10.0.0.0/24", + "id": "08eae331-0402-425a-923c-34f7cfe39c1b" + } +} +` + +const SubnetUpdateAllocationPoolRequest = ` +{ + "subnet": { + "name": "my_new_subnet", + "allocation_pools": [ + { + "start": "10.1.0.2", + "end": "10.1.0.254" + } + ] + } +} +` + +const SubnetUpdateAllocationPoolResponse = ` +{ + "subnet": { + "name": "my_new_subnet", + "enable_dhcp": true, + "network_id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324", + "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e", + "dns_nameservers": [], + "allocation_pools": [ + { + "start": "10.1.0.2", + "end": "10.1.0.254" + } + ], + "host_routes": [], + "ip_version": 4, + "gateway_ip": "10.0.0.1", + "cidr": "10.0.0.0/24", + "id": "08eae331-0402-425a-923c-34f7cfe39c1b" + } +} +` diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/testing/requests_test.go new file mode 100644 index 0000000000..a563f70cdf --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/testing/requests_test.go @@ -0,0 +1,403 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common" + "github.com/gophercloud/gophercloud/openstack/networking/v2/subnets" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/subnets", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, SubnetListResult) + }) + + count := 0 + + subnets.List(fake.ServiceClient(), subnets.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := subnets.ExtractSubnets(page) + if err != nil { + t.Errorf("Failed to extract subnets: %v", err) + return false, nil + } + + expected := []subnets.Subnet{ + Subnet1, + Subnet2, + Subnet3, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/subnets/54d6f61d-db07-451c-9ab3-b9609b6b6f0b", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, SubnetGetResult) + }) + + s, err := subnets.Get(fake.ServiceClient(), "54d6f61d-db07-451c-9ab3-b9609b6b6f0b").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, s.Name, "my_subnet") + th.AssertEquals(t, s.EnableDHCP, true) + th.AssertEquals(t, s.NetworkID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") + th.AssertEquals(t, s.TenantID, "4fd44f30292945e481c7b8a0c8908869") + th.AssertDeepEquals(t, s.DNSNameservers, []string{}) + th.AssertDeepEquals(t, s.AllocationPools, []subnets.AllocationPool{ + { + Start: "192.0.0.2", + End: "192.255.255.254", + }, + }) + th.AssertDeepEquals(t, s.HostRoutes, []subnets.HostRoute{}) + th.AssertEquals(t, s.IPVersion, 4) + th.AssertEquals(t, s.GatewayIP, "192.0.0.1") + th.AssertEquals(t, s.CIDR, "192.0.0.0/8") + th.AssertEquals(t, s.ID, "54d6f61d-db07-451c-9ab3-b9609b6b6f0b") +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/subnets", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, SubnetCreateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, SubnetCreateResult) + }) + + var gatewayIP = "192.168.199.1" + opts := subnets.CreateOpts{ + NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a22", + IPVersion: 4, + CIDR: "192.168.199.0/24", + GatewayIP: &gatewayIP, + AllocationPools: []subnets.AllocationPool{ + { + Start: "192.168.199.2", + End: "192.168.199.254", + }, + }, + DNSNameservers: []string{"foo"}, + HostRoutes: []subnets.HostRoute{ + {NextHop: "bar"}, + }, + } + s, err := subnets.Create(fake.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, s.Name, "") + th.AssertEquals(t, s.EnableDHCP, true) + th.AssertEquals(t, s.NetworkID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") + th.AssertEquals(t, s.TenantID, "4fd44f30292945e481c7b8a0c8908869") + th.AssertDeepEquals(t, s.DNSNameservers, []string{}) + th.AssertDeepEquals(t, s.AllocationPools, []subnets.AllocationPool{ + { + Start: "192.168.199.2", + End: "192.168.199.254", + }, + }) + th.AssertDeepEquals(t, s.HostRoutes, []subnets.HostRoute{}) + th.AssertEquals(t, s.IPVersion, 4) + th.AssertEquals(t, s.GatewayIP, "192.168.199.1") + th.AssertEquals(t, s.CIDR, "192.168.199.0/24") + th.AssertEquals(t, s.ID, "3b80198d-4f7b-4f77-9ef5-774d54e17126") +} + +func TestCreateNoGateway(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/subnets", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, SubnetCreateWithNoGatewayRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, SubnetCreateWithNoGatewayResponse) + }) + + var noGateway = "" + opts := subnets.CreateOpts{ + NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a23", + IPVersion: 4, + CIDR: "192.168.1.0/24", + GatewayIP: &noGateway, + AllocationPools: []subnets.AllocationPool{ + { + Start: "192.168.1.2", + End: "192.168.1.254", + }, + }, + DNSNameservers: []string{}, + } + s, err := subnets.Create(fake.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, s.Name, "") + th.AssertEquals(t, s.EnableDHCP, true) + th.AssertEquals(t, s.NetworkID, "d32019d3-bc6e-4319-9c1d-6722fc136a23") + th.AssertEquals(t, s.TenantID, "4fd44f30292945e481c7b8a0c8908869") + th.AssertDeepEquals(t, s.AllocationPools, []subnets.AllocationPool{ + { + Start: "192.168.1.2", + End: "192.168.1.254", + }, + }) + th.AssertDeepEquals(t, s.HostRoutes, []subnets.HostRoute{}) + th.AssertEquals(t, s.IPVersion, 4) + th.AssertEquals(t, s.GatewayIP, "") + th.AssertEquals(t, s.CIDR, "192.168.1.0/24") + th.AssertEquals(t, s.ID, "54d6f61d-db07-451c-9ab3-b9609b6b6f0c") +} + +func TestCreateDefaultGateway(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/subnets", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, SubnetCreateWithDefaultGatewayRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, SubnetCreateWithDefaultGatewayResponse) + }) + + opts := subnets.CreateOpts{ + NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a23", + IPVersion: 4, + CIDR: "192.168.1.0/24", + AllocationPools: []subnets.AllocationPool{ + { + Start: "192.168.1.2", + End: "192.168.1.254", + }, + }, + DNSNameservers: []string{}, + } + s, err := subnets.Create(fake.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, s.Name, "") + th.AssertEquals(t, s.EnableDHCP, true) + th.AssertEquals(t, s.NetworkID, "d32019d3-bc6e-4319-9c1d-6722fc136a23") + th.AssertEquals(t, s.TenantID, "4fd44f30292945e481c7b8a0c8908869") + th.AssertDeepEquals(t, s.AllocationPools, []subnets.AllocationPool{ + { + Start: "192.168.1.2", + End: "192.168.1.254", + }, + }) + th.AssertDeepEquals(t, s.HostRoutes, []subnets.HostRoute{}) + th.AssertEquals(t, s.IPVersion, 4) + th.AssertEquals(t, s.GatewayIP, "192.168.1.1") + th.AssertEquals(t, s.CIDR, "192.168.1.0/24") + th.AssertEquals(t, s.ID, "54d6f61d-db07-451c-9ab3-b9609b6b6f0c") +} + +func TestRequiredCreateOpts(t *testing.T) { + res := subnets.Create(fake.ServiceClient(), subnets.CreateOpts{}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + + res = subnets.Create(fake.ServiceClient(), subnets.CreateOpts{NetworkID: "foo"}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + + res = subnets.Create(fake.ServiceClient(), subnets.CreateOpts{NetworkID: "foo", CIDR: "bar", IPVersion: 40}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, SubnetUpdateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, SubnetUpdateResponse) + }) + + opts := subnets.UpdateOpts{ + Name: "my_new_subnet", + DNSNameservers: []string{"foo"}, + HostRoutes: []subnets.HostRoute{ + {NextHop: "bar"}, + }, + } + s, err := subnets.Update(fake.ServiceClient(), "08eae331-0402-425a-923c-34f7cfe39c1b", opts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, s.Name, "my_new_subnet") + th.AssertEquals(t, s.ID, "08eae331-0402-425a-923c-34f7cfe39c1b") +} + +func TestUpdateGateway(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, SubnetUpdateGatewayRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, SubnetUpdateGatewayResponse) + }) + + var gatewayIP = "10.0.0.1" + opts := subnets.UpdateOpts{ + Name: "my_new_subnet", + GatewayIP: &gatewayIP, + } + s, err := subnets.Update(fake.ServiceClient(), "08eae331-0402-425a-923c-34f7cfe39c1b", opts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, s.Name, "my_new_subnet") + th.AssertEquals(t, s.ID, "08eae331-0402-425a-923c-34f7cfe39c1b") + th.AssertEquals(t, s.GatewayIP, "10.0.0.1") +} + +func TestUpdateRemoveGateway(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, SubnetUpdateRemoveGatewayRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, SubnetUpdateRemoveGatewayResponse) + }) + + var noGateway = "" + opts := subnets.UpdateOpts{ + Name: "my_new_subnet", + GatewayIP: &noGateway, + } + s, err := subnets.Update(fake.ServiceClient(), "08eae331-0402-425a-923c-34f7cfe39c1b", opts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, s.Name, "my_new_subnet") + th.AssertEquals(t, s.ID, "08eae331-0402-425a-923c-34f7cfe39c1b") + th.AssertEquals(t, s.GatewayIP, "") +} + +func TestUpdateAllocationPool(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, SubnetUpdateAllocationPoolRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, SubnetUpdateAllocationPoolResponse) + }) + + opts := subnets.UpdateOpts{ + Name: "my_new_subnet", + AllocationPools: []subnets.AllocationPool{ + { + Start: "10.1.0.2", + End: "10.1.0.254", + }, + }, + } + s, err := subnets.Update(fake.ServiceClient(), "08eae331-0402-425a-923c-34f7cfe39c1b", opts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, s.Name, "my_new_subnet") + th.AssertEquals(t, s.ID, "08eae331-0402-425a-923c-34f7cfe39c1b") + th.AssertDeepEquals(t, s.AllocationPools, []subnets.AllocationPool{ + { + Start: "10.1.0.2", + End: "10.1.0.254", + }, + }) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := subnets.Delete(fake.ServiceClient(), "08eae331-0402-425a-923c-34f7cfe39c1b") + th.AssertNoErr(t, res.Err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/testing/results_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/testing/results_test.go new file mode 100644 index 0000000000..a227ccde99 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/testing/results_test.go @@ -0,0 +1,59 @@ +package testing + +import ( + "encoding/json" + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/networking/v2/subnets" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestHostRoute(t *testing.T) { + sejson := []byte(` + {"subnet": { + "name": "test-subnet", + "enable_dhcp": false, + "network_id": "3e66c41e-cbbd-4019-9aab-740b7e4150a0", + "tenant_id": "f86e123198cf42d19c8854c5f80c2f06", + "dns_nameservers": [], + "gateway_ip": "172.16.0.1", + "ipv6_ra_mode": null, + "allocation_pools": [ + { + "start": "172.16.0.2", + "end": "172.16.255.254" + } + ], + "host_routes": [ + { + "destination": "172.20.1.0/24", + "nexthop": "172.16.0.2" + } + ], + "ip_version": 4, + "ipv6_address_mode": null, + "cidr": "172.16.0.0/16", + "id": "6dcaa873-7115-41af-9ef5-915f73636e43", + "subnetpool_id": null + }} +`) + + var dejson interface{} + err := json.Unmarshal(sejson, &dejson) + if err != nil { + t.Fatalf("%s", err) + } + + resp := gophercloud.Result{Body: dejson} + var subnetWrapper struct { + Subnet subnets.Subnet `json:"subnet"` + } + err = resp.ExtractInto(&subnetWrapper) + if err != nil { + t.Fatalf("%s", err) + } + route := subnetWrapper.Subnet.HostRoutes[0] + th.AssertEquals(t, route.NextHop, "172.16.0.2") + th.AssertEquals(t, route.DestinationCIDR, "172.20.1.0/24") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/urls.go new file mode 100644 index 0000000000..7a4f2f7dd4 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/urls.go @@ -0,0 +1,31 @@ +package subnets + +import "github.com/gophercloud/gophercloud" + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("subnets", id) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("subnets") +} + +func listURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func createURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/accounts/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/accounts/doc.go new file mode 100644 index 0000000000..f5f894a9e5 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/accounts/doc.go @@ -0,0 +1,8 @@ +// Package accounts contains functionality for working with Object Storage +// account resources. An account is the top-level resource the object storage +// hierarchy: containers belong to accounts, objects belong to containers. +// +// Another way of thinking of an account is like a namespace for all your +// resources. It is synonymous with a project or tenant in other OpenStack +// services. +package accounts diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/accounts/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/accounts/requests.go new file mode 100644 index 0000000000..b5beef28b8 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/accounts/requests.go @@ -0,0 +1,100 @@ +package accounts + +import "github.com/gophercloud/gophercloud" + +// GetOptsBuilder allows extensions to add additional headers to the Get +// request. +type GetOptsBuilder interface { + ToAccountGetMap() (map[string]string, error) +} + +// GetOpts is a structure that contains parameters for getting an account's +// metadata. +type GetOpts struct { + Newest bool `h:"X-Newest"` +} + +// ToAccountGetMap formats a GetOpts into a map[string]string of headers. +func (opts GetOpts) ToAccountGetMap() (map[string]string, error) { + return gophercloud.BuildHeaders(opts) +} + +// Get is a function that retrieves an account's metadata. To extract just the +// custom metadata, call the ExtractMetadata method on the GetResult. To extract +// all the headers that are returned (including the metadata), call the +// ExtractHeader method on the GetResult. +func Get(c *gophercloud.ServiceClient, opts GetOptsBuilder) (r GetResult) { + h := make(map[string]string) + if opts != nil { + headers, err := opts.ToAccountGetMap() + if err != nil { + r.Err = err + return + } + for k, v := range headers { + h[k] = v + } + } + resp, err := c.Request("HEAD", getURL(c), &gophercloud.RequestOpts{ + MoreHeaders: h, + OkCodes: []int{204}, + }) + if resp != nil { + r.Header = resp.Header + } + r.Err = err + return +} + +// UpdateOptsBuilder allows extensions to add additional headers to the Update +// request. +type UpdateOptsBuilder interface { + ToAccountUpdateMap() (map[string]string, error) +} + +// UpdateOpts is a structure that contains parameters for updating, creating, or +// deleting an account's metadata. +type UpdateOpts struct { + Metadata map[string]string + ContentType string `h:"Content-Type"` + DetectContentType bool `h:"X-Detect-Content-Type"` + TempURLKey string `h:"X-Account-Meta-Temp-URL-Key"` + TempURLKey2 string `h:"X-Account-Meta-Temp-URL-Key-2"` +} + +// ToAccountUpdateMap formats an UpdateOpts into a map[string]string of headers. +func (opts UpdateOpts) ToAccountUpdateMap() (map[string]string, error) { + headers, err := gophercloud.BuildHeaders(opts) + if err != nil { + return nil, err + } + for k, v := range opts.Metadata { + headers["X-Account-Meta-"+k] = v + } + return headers, err +} + +// Update is a function that creates, updates, or deletes an account's metadata. +// To extract the headers returned, call the Extract method on the UpdateResult. +func Update(c *gophercloud.ServiceClient, opts UpdateOptsBuilder) (r UpdateResult) { + h := make(map[string]string) + if opts != nil { + headers, err := opts.ToAccountUpdateMap() + if err != nil { + r.Err = err + return + } + for k, v := range headers { + h[k] = v + } + } + resp, err := c.Request("POST", updateURL(c), &gophercloud.RequestOpts{ + MoreHeaders: h, + OkCodes: []int{201, 202, 204}, + }) + if resp != nil { + r.Header = resp.Header + } + r.Err = err + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/accounts/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/accounts/results.go new file mode 100644 index 0000000000..9bc8340477 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/accounts/results.go @@ -0,0 +1,167 @@ +package accounts + +import ( + "encoding/json" + "strconv" + "strings" + "time" + + "github.com/gophercloud/gophercloud" +) + +// UpdateResult is returned from a call to the Update function. +type UpdateResult struct { + gophercloud.HeaderResult +} + +// UpdateHeader represents the headers returned in the response from an Update request. +type UpdateHeader struct { + ContentLength int64 `json:"-"` + ContentType string `json:"Content-Type"` + TransID string `json:"X-Trans-Id"` + Date time.Time `json:"-"` +} + +func (r *UpdateHeader) UnmarshalJSON(b []byte) error { + type tmp UpdateHeader + var s struct { + tmp + ContentLength string `json:"Content-Length"` + Date gophercloud.JSONRFC1123 `json:"Date"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = UpdateHeader(s.tmp) + + switch s.ContentLength { + case "": + r.ContentLength = 0 + default: + r.ContentLength, err = strconv.ParseInt(s.ContentLength, 10, 64) + if err != nil { + return err + } + } + + r.Date = time.Time(s.Date) + + return err +} + +// Extract will return a struct of headers returned from a call to Get. To obtain +// a map of headers, call the ExtractHeader method on the GetResult. +func (r UpdateResult) Extract() (*UpdateHeader, error) { + var s *UpdateHeader + err := r.ExtractInto(&s) + return s, err +} + +// GetHeader represents the headers returned in the response from a Get request. +type GetHeader struct { + BytesUsed int64 `json:"-"` + ContainerCount int64 `json:"-"` + ContentLength int64 `json:"-"` + ObjectCount int64 `json:"-"` + ContentType string `json:"Content-Type"` + TransID string `json:"X-Trans-Id"` + TempURLKey string `json:"X-Account-Meta-Temp-URL-Key"` + TempURLKey2 string `json:"X-Account-Meta-Temp-URL-Key-2"` + Date time.Time `json:"-"` +} + +func (r *GetHeader) UnmarshalJSON(b []byte) error { + type tmp GetHeader + var s struct { + tmp + BytesUsed string `json:"X-Account-Bytes-Used"` + ContentLength string `json:"Content-Length"` + ContainerCount string `json:"X-Account-Container-Count"` + ObjectCount string `json:"X-Account-Object-Count"` + Date string `json:"Date"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = GetHeader(s.tmp) + + switch s.BytesUsed { + case "": + r.BytesUsed = 0 + default: + r.BytesUsed, err = strconv.ParseInt(s.BytesUsed, 10, 64) + if err != nil { + return err + } + } + + switch s.ContentLength { + case "": + r.ContentLength = 0 + default: + r.ContentLength, err = strconv.ParseInt(s.ContentLength, 10, 64) + if err != nil { + return err + } + } + + switch s.ObjectCount { + case "": + r.ObjectCount = 0 + default: + r.ObjectCount, err = strconv.ParseInt(s.ObjectCount, 10, 64) + if err != nil { + return err + } + } + + switch s.ContainerCount { + case "": + r.ContainerCount = 0 + default: + r.ContainerCount, err = strconv.ParseInt(s.ContainerCount, 10, 64) + if err != nil { + return err + } + } + + if s.Date != "" { + r.Date, err = time.Parse(time.RFC1123, s.Date) + } + + return err +} + +// GetResult is returned from a call to the Get function. +type GetResult struct { + gophercloud.HeaderResult +} + +// Extract will return a struct of headers returned from a call to Get. To obtain +// a map of headers, call the ExtractHeader method on the GetResult. +func (r GetResult) Extract() (*GetHeader, error) { + var s *GetHeader + err := r.ExtractInto(&s) + return s, err +} + +// ExtractMetadata is a function that takes a GetResult (of type *htts.Response) +// and returns the custom metatdata associated with the account. +func (r GetResult) ExtractMetadata() (map[string]string, error) { + if r.Err != nil { + return nil, r.Err + } + + metadata := make(map[string]string) + for k, v := range r.Header { + if strings.HasPrefix(k, "X-Account-Meta-") { + key := strings.TrimPrefix(k, "X-Account-Meta-") + metadata[key] = v[0] + } + } + return metadata, nil +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/accounts/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/accounts/testing/doc.go new file mode 100644 index 0000000000..b8fdf88c37 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/accounts/testing/doc.go @@ -0,0 +1,2 @@ +// objectstorage_accounts_v1 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/accounts/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/accounts/testing/fixtures.go new file mode 100644 index 0000000000..fff3071475 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/accounts/testing/fixtures.go @@ -0,0 +1,39 @@ +package testing + +import ( + "net/http" + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +// HandleGetAccountSuccessfully creates an HTTP handler at `/` on the test handler mux that +// responds with a `Get` response. +func HandleGetAccountSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "HEAD") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Set("X-Account-Container-Count", "2") + w.Header().Set("X-Account-Object-Count", "5") + w.Header().Set("X-Account-Bytes-Used", "14") + w.Header().Set("X-Account-Meta-Subject", "books") + w.Header().Set("Date", "Fri, 17 Jan 2014 16:09:56 GMT") + + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleUpdateAccountSuccessfully creates an HTTP handler at `/` on the test handler mux that +// responds with a `Update` response. +func HandleUpdateAccountSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "X-Account-Meta-Gophercloud-Test", "accounts") + + w.Header().Set("Date", "Fri, 17 Jan 2014 16:09:56 GMT") + w.WriteHeader(http.StatusNoContent) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/accounts/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/accounts/testing/requests_test.go new file mode 100644 index 0000000000..97852f1957 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/accounts/testing/requests_test.go @@ -0,0 +1,55 @@ +package testing + +import ( + "testing" + "time" + + "github.com/gophercloud/gophercloud/openstack/objectstorage/v1/accounts" + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +var ( + loc, _ = time.LoadLocation("GMT") +) + +func TestUpdateAccount(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleUpdateAccountSuccessfully(t) + + options := &accounts.UpdateOpts{Metadata: map[string]string{"gophercloud-test": "accounts"}} + res := accounts.Update(fake.ServiceClient(), options) + th.AssertNoErr(t, res.Err) + + expected := &accounts.UpdateHeader{ + Date: time.Date(2014, time.January, 17, 16, 9, 56, 0, loc), // Fri, 17 Jan 2014 16:09:56 GMT + } + actual, err := res.Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expected, actual) +} + +func TestGetAccount(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetAccountSuccessfully(t) + + expectedMetadata := map[string]string{"Subject": "books"} + res := accounts.Get(fake.ServiceClient(), &accounts.GetOpts{}) + th.AssertNoErr(t, res.Err) + actualMetadata, _ := res.ExtractMetadata() + th.CheckDeepEquals(t, expectedMetadata, actualMetadata) + _, err := res.Extract() + th.AssertNoErr(t, err) + + expected := &accounts.GetHeader{ + ContainerCount: 2, + ObjectCount: 5, + BytesUsed: 14, + Date: time.Date(2014, time.January, 17, 16, 9, 56, 0, loc), // Fri, 17 Jan 2014 16:09:56 GMT + } + actual, err := res.Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expected, actual) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/accounts/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/accounts/urls.go new file mode 100644 index 0000000000..71540b1daf --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/accounts/urls.go @@ -0,0 +1,11 @@ +package accounts + +import "github.com/gophercloud/gophercloud" + +func getURL(c *gophercloud.ServiceClient) string { + return c.Endpoint +} + +func updateURL(c *gophercloud.ServiceClient) string { + return getURL(c) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/containers/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/containers/doc.go new file mode 100644 index 0000000000..5fed5537f1 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/containers/doc.go @@ -0,0 +1,8 @@ +// Package containers contains functionality for working with Object Storage +// container resources. A container serves as a logical namespace for objects +// that are placed inside it - an object with the same name in two different +// containers represents two different objects. +// +// In addition to containing objects, you can also use the container to control +// access to objects by using an access control list (ACL). +package containers diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/containers/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/containers/requests.go new file mode 100644 index 0000000000..a66867394c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/containers/requests.go @@ -0,0 +1,191 @@ +package containers + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the List +// request. +type ListOptsBuilder interface { + ToContainerListParams() (bool, string, error) +} + +// ListOpts is a structure that holds options for listing containers. +type ListOpts struct { + Full bool + Limit int `q:"limit"` + Marker string `q:"marker"` + EndMarker string `q:"end_marker"` + Format string `q:"format"` + Prefix string `q:"prefix"` + Delimiter string `q:"delimiter"` +} + +// ToContainerListParams formats a ListOpts into a query string and boolean +// representing whether to list complete information for each container. +func (opts ListOpts) ToContainerListParams() (bool, string, error) { + q, err := gophercloud.BuildQueryString(opts) + return opts.Full, q.String(), err +} + +// List is a function that retrieves containers associated with the account as +// well as account metadata. It returns a pager which can be iterated with the +// EachPage function. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + headers := map[string]string{"Accept": "text/plain", "Content-Type": "text/plain"} + + url := listURL(c) + if opts != nil { + full, query, err := opts.ToContainerListParams() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + + if full { + headers = map[string]string{"Accept": "application/json", "Content-Type": "application/json"} + } + } + + pager := pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + p := ContainerPage{pagination.MarkerPageBase{PageResult: r}} + p.MarkerPageBase.Owner = p + return p + }) + pager.Headers = headers + return pager +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToContainerCreateMap() (map[string]string, error) +} + +// CreateOpts is a structure that holds parameters for creating a container. +type CreateOpts struct { + Metadata map[string]string + ContainerRead string `h:"X-Container-Read"` + ContainerSyncTo string `h:"X-Container-Sync-To"` + ContainerSyncKey string `h:"X-Container-Sync-Key"` + ContainerWrite string `h:"X-Container-Write"` + ContentType string `h:"Content-Type"` + DetectContentType bool `h:"X-Detect-Content-Type"` + IfNoneMatch string `h:"If-None-Match"` + VersionsLocation string `h:"X-Versions-Location"` +} + +// ToContainerCreateMap formats a CreateOpts into a map of headers. +func (opts CreateOpts) ToContainerCreateMap() (map[string]string, error) { + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + return nil, err + } + for k, v := range opts.Metadata { + h["X-Container-Meta-"+k] = v + } + return h, nil +} + +// Create is a function that creates a new container. +func Create(c *gophercloud.ServiceClient, containerName string, opts CreateOptsBuilder) (r CreateResult) { + h := make(map[string]string) + if opts != nil { + headers, err := opts.ToContainerCreateMap() + if err != nil { + r.Err = err + return + } + for k, v := range headers { + h[k] = v + } + } + resp, err := c.Request("PUT", createURL(c, containerName), &gophercloud.RequestOpts{ + MoreHeaders: h, + OkCodes: []int{201, 202, 204}, + }) + if resp != nil { + r.Header = resp.Header + } + r.Err = err + return +} + +// Delete is a function that deletes a container. +func Delete(c *gophercloud.ServiceClient, containerName string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, containerName), nil) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToContainerUpdateMap() (map[string]string, error) +} + +// UpdateOpts is a structure that holds parameters for updating, creating, or +// deleting a container's metadata. +type UpdateOpts struct { + Metadata map[string]string + ContainerRead string `h:"X-Container-Read"` + ContainerSyncTo string `h:"X-Container-Sync-To"` + ContainerSyncKey string `h:"X-Container-Sync-Key"` + ContainerWrite string `h:"X-Container-Write"` + ContentType string `h:"Content-Type"` + DetectContentType bool `h:"X-Detect-Content-Type"` + RemoveVersionsLocation string `h:"X-Remove-Versions-Location"` + VersionsLocation string `h:"X-Versions-Location"` +} + +// ToContainerUpdateMap formats a CreateOpts into a map of headers. +func (opts UpdateOpts) ToContainerUpdateMap() (map[string]string, error) { + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + return nil, err + } + for k, v := range opts.Metadata { + h["X-Container-Meta-"+k] = v + } + return h, nil +} + +// Update is a function that creates, updates, or deletes a container's +// metadata. +func Update(c *gophercloud.ServiceClient, containerName string, opts UpdateOptsBuilder) (r UpdateResult) { + h := make(map[string]string) + if opts != nil { + headers, err := opts.ToContainerUpdateMap() + if err != nil { + r.Err = err + return + } + + for k, v := range headers { + h[k] = v + } + } + resp, err := c.Request("POST", updateURL(c, containerName), &gophercloud.RequestOpts{ + MoreHeaders: h, + OkCodes: []int{201, 202, 204}, + }) + if resp != nil { + r.Header = resp.Header + } + r.Err = err + return +} + +// Get is a function that retrieves the metadata of a container. To extract just +// the custom metadata, pass the GetResult response to the ExtractMetadata +// function. +func Get(c *gophercloud.ServiceClient, containerName string) (r GetResult) { + resp, err := c.Request("HEAD", getURL(c, containerName), &gophercloud.RequestOpts{ + OkCodes: []int{200, 204}, + }) + if resp != nil { + r.Header = resp.Header + } + r.Err = err + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/containers/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/containers/results.go new file mode 100644 index 0000000000..8c11b8c834 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/containers/results.go @@ -0,0 +1,343 @@ +package containers + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Container represents a container resource. +type Container struct { + // The total number of bytes stored in the container. + Bytes int64 `json:"bytes"` + + // The total number of objects stored in the container. + Count int64 `json:"count"` + + // The name of the container. + Name string `json:"name"` +} + +// ContainerPage is the page returned by a pager when traversing over a +// collection of containers. +type ContainerPage struct { + pagination.MarkerPageBase +} + +//IsEmpty returns true if a ListResult contains no container names. +func (r ContainerPage) IsEmpty() (bool, error) { + names, err := ExtractNames(r) + return len(names) == 0, err +} + +// LastMarker returns the last container name in a ListResult. +func (r ContainerPage) LastMarker() (string, error) { + names, err := ExtractNames(r) + if err != nil { + return "", err + } + if len(names) == 0 { + return "", nil + } + return names[len(names)-1], nil +} + +// ExtractInfo is a function that takes a ListResult and returns the containers' information. +func ExtractInfo(r pagination.Page) ([]Container, error) { + var s []Container + err := (r.(ContainerPage)).ExtractInto(&s) + return s, err +} + +// ExtractNames is a function that takes a ListResult and returns the containers' names. +func ExtractNames(page pagination.Page) ([]string, error) { + casted := page.(ContainerPage) + ct := casted.Header.Get("Content-Type") + + switch { + case strings.HasPrefix(ct, "application/json"): + parsed, err := ExtractInfo(page) + if err != nil { + return nil, err + } + + names := make([]string, 0, len(parsed)) + for _, container := range parsed { + names = append(names, container.Name) + } + return names, nil + case strings.HasPrefix(ct, "text/plain"): + names := make([]string, 0, 50) + + body := string(page.(ContainerPage).Body.([]uint8)) + for _, name := range strings.Split(body, "\n") { + if len(name) > 0 { + names = append(names, name) + } + } + + return names, nil + default: + return nil, fmt.Errorf("Cannot extract names from response with content-type: [%s]", ct) + } +} + +// GetHeader represents the headers returned in the response from a Get request. +type GetHeader struct { + AcceptRanges string `json:"Accept-Ranges"` + BytesUsed int64 `json:"-"` + ContentLength int64 `json:"-"` + ContentType string `json:"Content-Type"` + Date time.Time `json:"-"` + ObjectCount int64 `json:"-"` + Read []string `json:"-"` + TransID string `json:"X-Trans-Id"` + VersionsLocation string `json:"X-Versions-Location"` + Write []string `json:"-"` +} + +func (r *GetHeader) UnmarshalJSON(b []byte) error { + type tmp GetHeader + var s struct { + tmp + BytesUsed string `json:"X-Container-Bytes-Used"` + ContentLength string `json:"Content-Length"` + ObjectCount string `json:"X-Container-Object-Count"` + Write string `json:"X-Container-Write"` + Read string `json:"X-Container-Read"` + Date gophercloud.JSONRFC1123 `json:"Date"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = GetHeader(s.tmp) + + switch s.BytesUsed { + case "": + r.BytesUsed = 0 + default: + r.BytesUsed, err = strconv.ParseInt(s.BytesUsed, 10, 64) + if err != nil { + return err + } + } + + switch s.ContentLength { + case "": + r.ContentLength = 0 + default: + r.ContentLength, err = strconv.ParseInt(s.ContentLength, 10, 64) + if err != nil { + return err + } + } + + switch s.ObjectCount { + case "": + r.ObjectCount = 0 + default: + r.ObjectCount, err = strconv.ParseInt(s.ObjectCount, 10, 64) + if err != nil { + return err + } + } + + r.Read = strings.Split(s.Read, ",") + r.Write = strings.Split(s.Write, ",") + + r.Date = time.Time(s.Date) + + return err +} + +// GetResult represents the result of a get operation. +type GetResult struct { + gophercloud.HeaderResult +} + +// Extract will return a struct of headers returned from a call to Get. To obtain +// a map of headers, call the ExtractHeader method on the GetResult. +func (r GetResult) Extract() (*GetHeader, error) { + var s *GetHeader + err := r.ExtractInto(&s) + return s, err +} + +// ExtractMetadata is a function that takes a GetResult (of type *stts.Response) +// and returns the custom metadata associated with the container. +func (r GetResult) ExtractMetadata() (map[string]string, error) { + if r.Err != nil { + return nil, r.Err + } + metadata := make(map[string]string) + for k, v := range r.Header { + if strings.HasPrefix(k, "X-Container-Meta-") { + key := strings.TrimPrefix(k, "X-Container-Meta-") + metadata[key] = v[0] + } + } + return metadata, nil +} + +// CreateHeader represents the headers returned in the response from a Create request. +type CreateHeader struct { + ContentLength int64 `json:"-"` + ContentType string `json:"Content-Type"` + Date time.Time `json:"-"` + TransID string `json:"X-Trans-Id"` +} + +func (r *CreateHeader) UnmarshalJSON(b []byte) error { + type tmp CreateHeader + var s struct { + tmp + ContentLength string `json:"Content-Length"` + Date gophercloud.JSONRFC1123 `json:"Date"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = CreateHeader(s.tmp) + + switch s.ContentLength { + case "": + r.ContentLength = 0 + default: + r.ContentLength, err = strconv.ParseInt(s.ContentLength, 10, 64) + if err != nil { + return err + } + } + + r.Date = time.Time(s.Date) + + return err +} + +// CreateResult represents the result of a create operation. To extract the +// the headers from the HTTP response, you can invoke the 'ExtractHeader' +// method on the result struct. +type CreateResult struct { + gophercloud.HeaderResult +} + +// Extract will return a struct of headers returned from a call to Create. To obtain +// a map of headers, call the ExtractHeader method on the CreateResult. +func (r CreateResult) Extract() (*CreateHeader, error) { + var s *CreateHeader + err := r.ExtractInto(&s) + return s, err +} + +// UpdateHeader represents the headers returned in the response from a Update request. +type UpdateHeader struct { + ContentLength int64 `json:"-"` + ContentType string `json:"Content-Type"` + Date time.Time `json:"-"` + TransID string `json:"X-Trans-Id"` +} + +func (r *UpdateHeader) UnmarshalJSON(b []byte) error { + type tmp UpdateHeader + var s struct { + tmp + ContentLength string `json:"Content-Length"` + Date gophercloud.JSONRFC1123 `json:"Date"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = UpdateHeader(s.tmp) + + switch s.ContentLength { + case "": + r.ContentLength = 0 + default: + r.ContentLength, err = strconv.ParseInt(s.ContentLength, 10, 64) + if err != nil { + return err + } + } + + r.Date = time.Time(s.Date) + + return err +} + +// UpdateResult represents the result of an update operation. To extract the +// the headers from the HTTP response, you can invoke the 'ExtractHeader' +// method on the result struct. +type UpdateResult struct { + gophercloud.HeaderResult +} + +// Extract will return a struct of headers returned from a call to Update. To obtain +// a map of headers, call the ExtractHeader method on the UpdateResult. +func (r UpdateResult) Extract() (*UpdateHeader, error) { + var s *UpdateHeader + err := r.ExtractInto(&s) + return s, err +} + +// DeleteHeader represents the headers returned in the response from a Delete request. +type DeleteHeader struct { + ContentLength int64 `json:"-"` + ContentType string `json:"Content-Type"` + Date time.Time `json:"-"` + TransID string `json:"X-Trans-Id"` +} + +func (r *DeleteHeader) UnmarshalJSON(b []byte) error { + type tmp DeleteHeader + var s struct { + tmp + ContentLength string `json:"Content-Length"` + Date gophercloud.JSONRFC1123 `json:"Date"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = DeleteHeader(s.tmp) + + switch s.ContentLength { + case "": + r.ContentLength = 0 + default: + r.ContentLength, err = strconv.ParseInt(s.ContentLength, 10, 64) + if err != nil { + return err + } + } + + r.Date = time.Time(s.Date) + + return err +} + +// DeleteResult represents the result of a delete operation. To extract the +// the headers from the HTTP response, you can invoke the 'ExtractHeader' +// method on the result struct. +type DeleteResult struct { + gophercloud.HeaderResult +} + +// Extract will return a struct of headers returned from a call to Delete. To obtain +// a map of headers, call the ExtractHeader method on the DeleteResult. +func (r DeleteResult) Extract() (*DeleteHeader, error) { + var s *DeleteHeader + err := r.ExtractInto(&s) + return s, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/containers/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/containers/testing/doc.go new file mode 100644 index 0000000000..c27fa49329 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/containers/testing/doc.go @@ -0,0 +1,2 @@ +// objectstorage_containers_v1 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/containers/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/containers/testing/fixtures.go new file mode 100644 index 0000000000..b68230a8a4 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/containers/testing/fixtures.go @@ -0,0 +1,154 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud/openstack/objectstorage/v1/containers" + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +// ExpectedListInfo is the result expected from a call to `List` when full +// info is requested. +var ExpectedListInfo = []containers.Container{ + { + Count: 0, + Bytes: 0, + Name: "janeausten", + }, + { + Count: 1, + Bytes: 14, + Name: "marktwain", + }, +} + +// ExpectedListNames is the result expected from a call to `List` when just +// container names are requested. +var ExpectedListNames = []string{"janeausten", "marktwain"} + +// HandleListContainerInfoSuccessfully creates an HTTP handler at `/` on the test handler mux that +// responds with a `List` response when full info is requested. +func HandleListContainerInfoSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, `[ + { + "count": 0, + "bytes": 0, + "name": "janeausten" + }, + { + "count": 1, + "bytes": 14, + "name": "marktwain" + } + ]`) + case "janeausten": + fmt.Fprintf(w, `[ + { + "count": 1, + "bytes": 14, + "name": "marktwain" + } + ]`) + case "marktwain": + fmt.Fprintf(w, `[]`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) +} + +// HandleListContainerNamesSuccessfully creates an HTTP handler at `/` on the test handler mux that +// responds with a `ListNames` response when only container names are requested. +func HandleListContainerNamesSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "text/plain") + + w.Header().Set("Content-Type", "text/plain") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, "janeausten\nmarktwain\n") + case "janeausten": + fmt.Fprintf(w, "marktwain\n") + case "marktwain": + fmt.Fprintf(w, ``) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) +} + +// HandleCreateContainerSuccessfully creates an HTTP handler at `/testContainer` on the test handler mux that +// responds with a `Create` response. +func HandleCreateContainerSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Add("X-Container-Meta-Foo", "bar") + w.Header().Set("Content-Length", "0") + w.Header().Set("Content-Type", "text/html; charset=UTF-8") + w.Header().Set("Date", "Wed, 17 Aug 2016 19:25:43 GMT") + w.Header().Set("X-Trans-Id", "tx554ed59667a64c61866f1-0058b4ba37") + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleDeleteContainerSuccessfully creates an HTTP handler at `/testContainer` on the test handler mux that +// responds with a `Delete` response. +func HandleDeleteContainerSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleUpdateContainerSuccessfully creates an HTTP handler at `/testContainer` on the test handler mux that +// responds with a `Update` response. +func HandleUpdateContainerSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleGetContainerSuccessfully creates an HTTP handler at `/testContainer` on the test handler mux that +// responds with a `Get` response. +func HandleGetContainerSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "HEAD") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + w.Header().Set("Accept-Ranges", "bytes") + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("Date", "Wed, 17 Aug 2016 19:25:43 GMT") + w.Header().Set("X-Container-Bytes-Used", "100") + w.Header().Set("X-Container-Object-Count", "4") + w.Header().Set("X-Container-Read", "test") + w.Header().Set("X-Container-Write", "test2,user4") + w.Header().Set("X-Timestamp", "1471298837.95721") + w.Header().Set("X-Trans-Id", "tx554ed59667a64c61866f1-0057b4ba37") + w.WriteHeader(http.StatusNoContent) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/containers/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/containers/testing/requests_test.go new file mode 100644 index 0000000000..bb0c784008 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/containers/testing/requests_test.go @@ -0,0 +1,144 @@ +package testing + +import ( + "testing" + "time" + + "github.com/gophercloud/gophercloud/openstack/objectstorage/v1/containers" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +var ( + metadata = map[string]string{"gophercloud-test": "containers"} + loc, _ = time.LoadLocation("GMT") +) + +func TestListContainerInfo(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListContainerInfoSuccessfully(t) + + count := 0 + err := containers.List(fake.ServiceClient(), &containers.ListOpts{Full: true}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := containers.ExtractInfo(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ExpectedListInfo, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestListAllContainerInfo(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListContainerInfoSuccessfully(t) + + allPages, err := containers.List(fake.ServiceClient(), &containers.ListOpts{Full: true}).AllPages() + th.AssertNoErr(t, err) + actual, err := containers.ExtractInfo(allPages) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedListInfo, actual) +} + +func TestListContainerNames(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListContainerNamesSuccessfully(t) + + count := 0 + err := containers.List(fake.ServiceClient(), &containers.ListOpts{Full: false}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := containers.ExtractNames(page) + if err != nil { + t.Errorf("Failed to extract container names: %v", err) + return false, err + } + + th.CheckDeepEquals(t, ExpectedListNames, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestListAllContainerNames(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListContainerNamesSuccessfully(t) + + allPages, err := containers.List(fake.ServiceClient(), &containers.ListOpts{Full: false}).AllPages() + th.AssertNoErr(t, err) + actual, err := containers.ExtractNames(allPages) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedListNames, actual) +} + +func TestCreateContainer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateContainerSuccessfully(t) + + options := containers.CreateOpts{ContentType: "application/json", Metadata: map[string]string{"foo": "bar"}} + res := containers.Create(fake.ServiceClient(), "testContainer", options) + th.CheckEquals(t, "bar", res.Header["X-Container-Meta-Foo"][0]) + + expected := &containers.CreateHeader{ + ContentLength: 0, + ContentType: "text/html; charset=UTF-8", + Date: time.Date(2016, time.August, 17, 19, 25, 43, 0, loc), //Wed, 17 Aug 2016 19:25:43 GMT + TransID: "tx554ed59667a64c61866f1-0058b4ba37", + } + actual, err := res.Extract() + th.CheckNoErr(t, err) + th.AssertDeepEquals(t, expected, actual) +} + +func TestDeleteContainer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteContainerSuccessfully(t) + + res := containers.Delete(fake.ServiceClient(), "testContainer") + th.CheckNoErr(t, res.Err) +} + +func TestUpateContainer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleUpdateContainerSuccessfully(t) + + options := &containers.UpdateOpts{Metadata: map[string]string{"foo": "bar"}} + res := containers.Update(fake.ServiceClient(), "testContainer", options) + th.CheckNoErr(t, res.Err) +} + +func TestGetContainer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetContainerSuccessfully(t) + + res := containers.Get(fake.ServiceClient(), "testContainer") + _, err := res.ExtractMetadata() + th.CheckNoErr(t, err) + + expected := &containers.GetHeader{ + AcceptRanges: "bytes", + BytesUsed: 100, + ContentType: "application/json; charset=utf-8", + Date: time.Date(2016, time.August, 17, 19, 25, 43, 0, loc), //Wed, 17 Aug 2016 19:25:43 GMT + ObjectCount: 4, + Read: []string{"test"}, + TransID: "tx554ed59667a64c61866f1-0057b4ba37", + Write: []string{"test2", "user4"}, + } + actual, err := res.Extract() + th.CheckNoErr(t, err) + th.AssertDeepEquals(t, expected, actual) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/containers/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/containers/urls.go new file mode 100644 index 0000000000..9b380470dd --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/containers/urls.go @@ -0,0 +1,23 @@ +package containers + +import "github.com/gophercloud/gophercloud" + +func listURL(c *gophercloud.ServiceClient) string { + return c.Endpoint +} + +func createURL(c *gophercloud.ServiceClient, container string) string { + return c.ServiceURL(container) +} + +func getURL(c *gophercloud.ServiceClient, container string) string { + return createURL(c, container) +} + +func deleteURL(c *gophercloud.ServiceClient, container string) string { + return createURL(c, container) +} + +func updateURL(c *gophercloud.ServiceClient, container string) string { + return createURL(c, container) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/objects/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/objects/doc.go new file mode 100644 index 0000000000..30a9adde1c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/objects/doc.go @@ -0,0 +1,5 @@ +// Package objects contains functionality for working with Object Storage +// object resources. An object is a resource that represents and contains data +// - such as documents, images, and so on. You can also store custom metadata +// with an object. +package objects diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/objects/errors.go b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/objects/errors.go new file mode 100644 index 0000000000..5c4ae44d31 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/objects/errors.go @@ -0,0 +1,13 @@ +package objects + +import "github.com/gophercloud/gophercloud" + +// ErrWrongChecksum is the error when the checksum generated for an object +// doesn't match the ETAG header. +type ErrWrongChecksum struct { + gophercloud.BaseError +} + +func (e ErrWrongChecksum) Error() string { + return "Local checksum does not match API ETag header" +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/objects/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/objects/requests.go new file mode 100644 index 0000000000..0ab5e17111 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/objects/requests.go @@ -0,0 +1,453 @@ +package objects + +import ( + "bytes" + "crypto/hmac" + "crypto/md5" + "crypto/sha1" + "fmt" + "io" + "strings" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/objectstorage/v1/accounts" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the List +// request. +type ListOptsBuilder interface { + ToObjectListParams() (bool, string, error) +} + +// ListOpts is a structure that holds parameters for listing objects. +type ListOpts struct { + // Full is a true/false value that represents the amount of object information + // returned. If Full is set to true, then the content-type, number of bytes, hash + // date last modified, and name are returned. If set to false or not set, then + // only the object names are returned. + Full bool + Limit int `q:"limit"` + Marker string `q:"marker"` + EndMarker string `q:"end_marker"` + Format string `q:"format"` + Prefix string `q:"prefix"` + Delimiter string `q:"delimiter"` + Path string `q:"path"` +} + +// ToObjectListParams formats a ListOpts into a query string and boolean +// representing whether to list complete information for each object. +func (opts ListOpts) ToObjectListParams() (bool, string, error) { + q, err := gophercloud.BuildQueryString(opts) + return opts.Full, q.String(), err +} + +// List is a function that retrieves all objects in a container. It also returns the details +// for the container. To extract only the object information or names, pass the ListResult +// response to the ExtractInfo or ExtractNames function, respectively. +func List(c *gophercloud.ServiceClient, containerName string, opts ListOptsBuilder) pagination.Pager { + headers := map[string]string{"Accept": "text/plain", "Content-Type": "text/plain"} + + url := listURL(c, containerName) + if opts != nil { + full, query, err := opts.ToObjectListParams() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + + if full { + headers = map[string]string{"Accept": "application/json", "Content-Type": "application/json"} + } + } + + pager := pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + p := ObjectPage{pagination.MarkerPageBase{PageResult: r}} + p.MarkerPageBase.Owner = p + return p + }) + pager.Headers = headers + return pager +} + +// DownloadOptsBuilder allows extensions to add additional parameters to the +// Download request. +type DownloadOptsBuilder interface { + ToObjectDownloadParams() (map[string]string, string, error) +} + +// DownloadOpts is a structure that holds parameters for downloading an object. +type DownloadOpts struct { + IfMatch string `h:"If-Match"` + IfModifiedSince time.Time `h:"If-Modified-Since"` + IfNoneMatch string `h:"If-None-Match"` + IfUnmodifiedSince time.Time `h:"If-Unmodified-Since"` + Range string `h:"Range"` + Expires string `q:"expires"` + MultipartManifest string `q:"multipart-manifest"` + Signature string `q:"signature"` +} + +// ToObjectDownloadParams formats a DownloadOpts into a query string and map of +// headers. +func (opts DownloadOpts) ToObjectDownloadParams() (map[string]string, string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return nil, "", err + } + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + return nil, q.String(), err + } + return h, q.String(), nil +} + +// Download is a function that retrieves the content and metadata for an object. +// To extract just the content, pass the DownloadResult response to the +// ExtractContent function. +func Download(c *gophercloud.ServiceClient, containerName, objectName string, opts DownloadOptsBuilder) (r DownloadResult) { + url := downloadURL(c, containerName, objectName) + h := make(map[string]string) + if opts != nil { + headers, query, err := opts.ToObjectDownloadParams() + if err != nil { + r.Err = err + return + } + for k, v := range headers { + h[k] = v + } + url += query + } + + resp, err := c.Get(url, nil, &gophercloud.RequestOpts{ + MoreHeaders: h, + OkCodes: []int{200, 304}, + }) + if resp != nil { + r.Header = resp.Header + r.Body = resp.Body + } + r.Err = err + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToObjectCreateParams() (io.Reader, map[string]string, string, error) +} + +// CreateOpts is a structure that holds parameters for creating an object. +type CreateOpts struct { + Content io.Reader + Metadata map[string]string + CacheControl string `h:"Cache-Control"` + ContentDisposition string `h:"Content-Disposition"` + ContentEncoding string `h:"Content-Encoding"` + ContentLength int64 `h:"Content-Length"` + ContentType string `h:"Content-Type"` + CopyFrom string `h:"X-Copy-From"` + DeleteAfter int `h:"X-Delete-After"` + DeleteAt int `h:"X-Delete-At"` + DetectContentType string `h:"X-Detect-Content-Type"` + ETag string `h:"ETag"` + IfNoneMatch string `h:"If-None-Match"` + ObjectManifest string `h:"X-Object-Manifest"` + TransferEncoding string `h:"Transfer-Encoding"` + Expires string `q:"expires"` + MultipartManifest string `q:"multipart-manifest"` + Signature string `q:"signature"` +} + +// ToObjectCreateParams formats a CreateOpts into a query string and map of +// headers. +func (opts CreateOpts) ToObjectCreateParams() (io.Reader, map[string]string, string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return nil, nil, "", err + } + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + return nil, nil, "", err + } + + for k, v := range opts.Metadata { + h["X-Object-Meta-"+k] = v + } + + hash := md5.New() + buf := bytes.NewBuffer([]byte{}) + _, err = io.Copy(io.MultiWriter(hash, buf), opts.Content) + if err != nil { + return nil, nil, "", err + } + localChecksum := fmt.Sprintf("%x", hash.Sum(nil)) + h["ETag"] = localChecksum + + return buf, h, q.String(), nil +} + +// Create is a function that creates a new object or replaces an existing object. If the returned response's ETag +// header fails to match the local checksum, the failed request will automatically be retried up to a maximum of 3 times. +func Create(c *gophercloud.ServiceClient, containerName, objectName string, opts CreateOptsBuilder) (r CreateResult) { + url := createURL(c, containerName, objectName) + h := make(map[string]string) + var b io.Reader + if opts != nil { + tmpB, headers, query, err := opts.ToObjectCreateParams() + if err != nil { + r.Err = err + return + } + for k, v := range headers { + h[k] = v + } + url += query + b = tmpB + } + + resp, err := c.Put(url, nil, nil, &gophercloud.RequestOpts{ + RawBody: b, + MoreHeaders: h, + }) + r.Err = err + if resp != nil { + r.Header = resp.Header + } + return +} + +// CopyOptsBuilder allows extensions to add additional parameters to the +// Copy request. +type CopyOptsBuilder interface { + ToObjectCopyMap() (map[string]string, error) +} + +// CopyOpts is a structure that holds parameters for copying one object to +// another. +type CopyOpts struct { + Metadata map[string]string + ContentDisposition string `h:"Content-Disposition"` + ContentEncoding string `h:"Content-Encoding"` + ContentType string `h:"Content-Type"` + Destination string `h:"Destination" required:"true"` +} + +// ToObjectCopyMap formats a CopyOpts into a map of headers. +func (opts CopyOpts) ToObjectCopyMap() (map[string]string, error) { + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + return nil, err + } + for k, v := range opts.Metadata { + h["X-Object-Meta-"+k] = v + } + return h, nil +} + +// Copy is a function that copies one object to another. +func Copy(c *gophercloud.ServiceClient, containerName, objectName string, opts CopyOptsBuilder) (r CopyResult) { + h := make(map[string]string) + headers, err := opts.ToObjectCopyMap() + if err != nil { + r.Err = err + return + } + + for k, v := range headers { + h[k] = v + } + + url := copyURL(c, containerName, objectName) + resp, err := c.Request("COPY", url, &gophercloud.RequestOpts{ + MoreHeaders: h, + OkCodes: []int{201}, + }) + if resp != nil { + r.Header = resp.Header + } + r.Err = err + return +} + +// DeleteOptsBuilder allows extensions to add additional parameters to the +// Delete request. +type DeleteOptsBuilder interface { + ToObjectDeleteQuery() (string, error) +} + +// DeleteOpts is a structure that holds parameters for deleting an object. +type DeleteOpts struct { + MultipartManifest string `q:"multipart-manifest"` +} + +// ToObjectDeleteQuery formats a DeleteOpts into a query string. +func (opts DeleteOpts) ToObjectDeleteQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// Delete is a function that deletes an object. +func Delete(c *gophercloud.ServiceClient, containerName, objectName string, opts DeleteOptsBuilder) (r DeleteResult) { + url := deleteURL(c, containerName, objectName) + if opts != nil { + query, err := opts.ToObjectDeleteQuery() + if err != nil { + r.Err = err + return + } + url += query + } + resp, err := c.Delete(url, nil) + if resp != nil { + r.Header = resp.Header + } + r.Err = err + return +} + +// GetOptsBuilder allows extensions to add additional parameters to the +// Get request. +type GetOptsBuilder interface { + ToObjectGetQuery() (string, error) +} + +// GetOpts is a structure that holds parameters for getting an object's metadata. +type GetOpts struct { + Expires string `q:"expires"` + Signature string `q:"signature"` +} + +// ToObjectGetQuery formats a GetOpts into a query string. +func (opts GetOpts) ToObjectGetQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// Get is a function that retrieves the metadata of an object. To extract just the custom +// metadata, pass the GetResult response to the ExtractMetadata function. +func Get(c *gophercloud.ServiceClient, containerName, objectName string, opts GetOptsBuilder) (r GetResult) { + url := getURL(c, containerName, objectName) + if opts != nil { + query, err := opts.ToObjectGetQuery() + if err != nil { + r.Err = err + return + } + url += query + } + resp, err := c.Request("HEAD", url, &gophercloud.RequestOpts{ + OkCodes: []int{200, 204}, + }) + if resp != nil { + r.Header = resp.Header + } + r.Err = err + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToObjectUpdateMap() (map[string]string, error) +} + +// UpdateOpts is a structure that holds parameters for updating, creating, or deleting an +// object's metadata. +type UpdateOpts struct { + Metadata map[string]string + ContentDisposition string `h:"Content-Disposition"` + ContentEncoding string `h:"Content-Encoding"` + ContentType string `h:"Content-Type"` + DeleteAfter int `h:"X-Delete-After"` + DeleteAt int `h:"X-Delete-At"` + DetectContentType bool `h:"X-Detect-Content-Type"` +} + +// ToObjectUpdateMap formats a UpdateOpts into a map of headers. +func (opts UpdateOpts) ToObjectUpdateMap() (map[string]string, error) { + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + return nil, err + } + for k, v := range opts.Metadata { + h["X-Object-Meta-"+k] = v + } + return h, nil +} + +// Update is a function that creates, updates, or deletes an object's metadata. +func Update(c *gophercloud.ServiceClient, containerName, objectName string, opts UpdateOptsBuilder) (r UpdateResult) { + h := make(map[string]string) + if opts != nil { + headers, err := opts.ToObjectUpdateMap() + if err != nil { + r.Err = err + return + } + + for k, v := range headers { + h[k] = v + } + } + url := updateURL(c, containerName, objectName) + resp, err := c.Post(url, nil, nil, &gophercloud.RequestOpts{ + MoreHeaders: h, + }) + if resp != nil { + r.Header = resp.Header + } + r.Err = err + return +} + +// HTTPMethod represents an HTTP method string (e.g. "GET"). +type HTTPMethod string + +var ( + // GET represents an HTTP "GET" method. + GET HTTPMethod = "GET" + // POST represents an HTTP "POST" method. + POST HTTPMethod = "POST" +) + +// CreateTempURLOpts are options for creating a temporary URL for an object. +type CreateTempURLOpts struct { + // (REQUIRED) Method is the HTTP method to allow for users of the temp URL. Valid values + // are "GET" and "POST". + Method HTTPMethod + // (REQUIRED) TTL is the number of seconds the temp URL should be active. + TTL int + // (Optional) Split is the string on which to split the object URL. Since only + // the object path is used in the hash, the object URL needs to be parsed. If + // empty, the default OpenStack URL split point will be used ("/v1/"). + Split string +} + +// CreateTempURL is a function for creating a temporary URL for an object. It +// allows users to have "GET" or "POST" access to a particular tenant's object +// for a limited amount of time. +func CreateTempURL(c *gophercloud.ServiceClient, containerName, objectName string, opts CreateTempURLOpts) (string, error) { + if opts.Split == "" { + opts.Split = "/v1/" + } + duration := time.Duration(opts.TTL) * time.Second + expiry := time.Now().Add(duration).Unix() + getHeader, err := accounts.Get(c, nil).Extract() + if err != nil { + return "", err + } + secretKey := []byte(getHeader.TempURLKey) + url := getURL(c, containerName, objectName) + splitPath := strings.Split(url, opts.Split) + baseURL, objectPath := splitPath[0], splitPath[1] + objectPath = opts.Split + objectPath + body := fmt.Sprintf("%s\n%d\n%s", opts.Method, expiry, objectPath) + hash := hmac.New(sha1.New, secretKey) + hash.Write([]byte(body)) + hexsum := fmt.Sprintf("%x", hash.Sum(nil)) + return fmt.Sprintf("%s%s?temp_url_sig=%s&temp_url_expires=%d", baseURL, objectPath, hexsum, expiry), nil +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/objects/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/objects/results.go new file mode 100644 index 0000000000..0dcdbe2fbe --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/objects/results.go @@ -0,0 +1,493 @@ +package objects + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "strconv" + "strings" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Object is a structure that holds information related to a storage object. +type Object struct { + // Bytes is the total number of bytes that comprise the object. + Bytes int64 `json:"bytes"` + + // ContentType is the content type of the object. + ContentType string `json:"content_type"` + + // Hash represents the MD5 checksum value of the object's content. + Hash string `json:"hash"` + + // LastModified is the time the object was last modified, represented + // as a string. + LastModified time.Time `json:"-"` + + // Name is the unique name for the object. + Name string `json:"name"` +} + +func (r *Object) UnmarshalJSON(b []byte) error { + type tmp Object + var s *struct { + tmp + LastModified gophercloud.JSONRFC3339MilliNoZ `json:"last_modified"` + } + + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = Object(s.tmp) + + r.LastModified = time.Time(s.LastModified) + + return nil + +} + +// ObjectPage is a single page of objects that is returned from a call to the +// List function. +type ObjectPage struct { + pagination.MarkerPageBase +} + +// IsEmpty returns true if a ListResult contains no object names. +func (r ObjectPage) IsEmpty() (bool, error) { + names, err := ExtractNames(r) + return len(names) == 0, err +} + +// LastMarker returns the last object name in a ListResult. +func (r ObjectPage) LastMarker() (string, error) { + names, err := ExtractNames(r) + if err != nil { + return "", err + } + if len(names) == 0 { + return "", nil + } + return names[len(names)-1], nil +} + +// ExtractInfo is a function that takes a page of objects and returns their full information. +func ExtractInfo(r pagination.Page) ([]Object, error) { + var s []Object + err := (r.(ObjectPage)).ExtractInto(&s) + return s, err +} + +// ExtractNames is a function that takes a page of objects and returns only their names. +func ExtractNames(r pagination.Page) ([]string, error) { + casted := r.(ObjectPage) + ct := casted.Header.Get("Content-Type") + switch { + case strings.HasPrefix(ct, "application/json"): + parsed, err := ExtractInfo(r) + if err != nil { + return nil, err + } + + names := make([]string, 0, len(parsed)) + for _, object := range parsed { + names = append(names, object.Name) + } + + return names, nil + case strings.HasPrefix(ct, "text/plain"): + names := make([]string, 0, 50) + + body := string(r.(ObjectPage).Body.([]uint8)) + for _, name := range strings.Split(body, "\n") { + if len(name) > 0 { + names = append(names, name) + } + } + + return names, nil + case strings.HasPrefix(ct, "text/html"): + return []string{}, nil + default: + return nil, fmt.Errorf("Cannot extract names from response with content-type: [%s]", ct) + } +} + +// DownloadHeader represents the headers returned in the response from a Download request. +type DownloadHeader struct { + AcceptRanges string `json:"Accept-Ranges"` + ContentDisposition string `json:"Content-Disposition"` + ContentEncoding string `json:"Content-Encoding"` + ContentLength int64 `json:"-"` + ContentType string `json:"Content-Type"` + Date time.Time `json:"-"` + DeleteAt time.Time `json:"-"` + ETag string `json:"Etag"` + LastModified time.Time `json:"-"` + ObjectManifest string `json:"X-Object-Manifest"` + StaticLargeObject bool `json:"X-Static-Large-Object"` + TransID string `json:"X-Trans-Id"` +} + +func (r *DownloadHeader) UnmarshalJSON(b []byte) error { + type tmp DownloadHeader + var s struct { + tmp + ContentLength string `json:"Content-Length"` + Date gophercloud.JSONRFC1123 `json:"Date"` + DeleteAt gophercloud.JSONUnix `json:"X-Delete-At"` + LastModified gophercloud.JSONRFC1123 `json:"Last-Modified"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = DownloadHeader(s.tmp) + + switch s.ContentLength { + case "": + r.ContentLength = 0 + default: + r.ContentLength, err = strconv.ParseInt(s.ContentLength, 10, 64) + if err != nil { + return err + } + } + + r.Date = time.Time(s.Date) + r.DeleteAt = time.Time(s.DeleteAt) + r.LastModified = time.Time(s.LastModified) + + return nil +} + +// DownloadResult is a *http.Response that is returned from a call to the Download function. +type DownloadResult struct { + gophercloud.HeaderResult + Body io.ReadCloser +} + +// Extract will return a struct of headers returned from a call to Download. To obtain +// a map of headers, call the ExtractHeader method on the DownloadResult. +func (r DownloadResult) Extract() (*DownloadHeader, error) { + var s *DownloadHeader + err := r.ExtractInto(&s) + return s, err +} + +// ExtractContent is a function that takes a DownloadResult's io.Reader body +// and reads all available data into a slice of bytes. Please be aware that due +// the nature of io.Reader is forward-only - meaning that it can only be read +// once and not rewound. You can recreate a reader from the output of this +// function by using bytes.NewReader(downloadBytes) +func (r *DownloadResult) ExtractContent() ([]byte, error) { + if r.Err != nil { + return nil, r.Err + } + defer r.Body.Close() + body, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, err + } + r.Body.Close() + return body, nil +} + +// GetHeader represents the headers returned in the response from a Get request. +type GetHeader struct { + ContentDisposition string `json:"Content-Disposition"` + ContentEncoding string `json:"Content-Encoding"` + ContentLength int64 `json:"-"` + ContentType string `json:"Content-Type"` + Date time.Time `json:"-"` + DeleteAt time.Time `json:"-"` + ETag string `json:"Etag"` + LastModified time.Time `json:"-"` + ObjectManifest string `json:"X-Object-Manifest"` + StaticLargeObject bool `json:"X-Static-Large-Object"` + TransID string `json:"X-Trans-Id"` +} + +func (r *GetHeader) UnmarshalJSON(b []byte) error { + type tmp GetHeader + var s struct { + tmp + ContentLength string `json:"Content-Length"` + Date gophercloud.JSONRFC1123 `json:"Date"` + DeleteAt gophercloud.JSONUnix `json:"X-Delete-At"` + LastModified gophercloud.JSONRFC1123 `json:"Last-Modified"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = GetHeader(s.tmp) + + switch s.ContentLength { + case "": + r.ContentLength = 0 + default: + r.ContentLength, err = strconv.ParseInt(s.ContentLength, 10, 64) + if err != nil { + return err + } + } + + r.Date = time.Time(s.Date) + r.DeleteAt = time.Time(s.DeleteAt) + r.LastModified = time.Time(s.LastModified) + + return nil +} + +// GetResult is a *http.Response that is returned from a call to the Get function. +type GetResult struct { + gophercloud.HeaderResult +} + +// Extract will return a struct of headers returned from a call to Get. To obtain +// a map of headers, call the ExtractHeader method on the GetResult. +func (r GetResult) Extract() (*GetHeader, error) { + var s *GetHeader + err := r.ExtractInto(&s) + return s, err +} + +// ExtractMetadata is a function that takes a GetResult (of type *http.Response) +// and returns the custom metadata associated with the object. +func (r GetResult) ExtractMetadata() (map[string]string, error) { + if r.Err != nil { + return nil, r.Err + } + metadata := make(map[string]string) + for k, v := range r.Header { + if strings.HasPrefix(k, "X-Object-Meta-") { + key := strings.TrimPrefix(k, "X-Object-Meta-") + metadata[key] = v[0] + } + } + return metadata, nil +} + +// CreateHeader represents the headers returned in the response from a Create request. +type CreateHeader struct { + ContentLength int64 `json:"-"` + ContentType string `json:"Content-Type"` + Date time.Time `json:"-"` + ETag string `json:"Etag"` + LastModified time.Time `json:"-"` + TransID string `json:"X-Trans-Id"` +} + +func (r *CreateHeader) UnmarshalJSON(b []byte) error { + type tmp CreateHeader + var s struct { + tmp + ContentLength string `json:"Content-Length"` + Date gophercloud.JSONRFC1123 `json:"Date"` + LastModified gophercloud.JSONRFC1123 `json:"Last-Modified"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = CreateHeader(s.tmp) + + switch s.ContentLength { + case "": + r.ContentLength = 0 + default: + r.ContentLength, err = strconv.ParseInt(s.ContentLength, 10, 64) + if err != nil { + return err + } + } + + r.Date = time.Time(s.Date) + r.LastModified = time.Time(s.LastModified) + + return nil +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + checksum string + gophercloud.HeaderResult +} + +// Extract will return a struct of headers returned from a call to Create. To obtain +// a map of headers, call the ExtractHeader method on the CreateResult. +func (r CreateResult) Extract() (*CreateHeader, error) { + //if r.Header.Get("ETag") != fmt.Sprintf("%x", localChecksum) { + // return nil, ErrWrongChecksum{} + //} + var s *CreateHeader + err := r.ExtractInto(&s) + return s, err +} + +// UpdateHeader represents the headers returned in the response from a Update request. +type UpdateHeader struct { + ContentLength int64 `json:"-"` + ContentType string `json:"Content-Type"` + Date time.Time `json:"-"` + TransID string `json:"X-Trans-Id"` +} + +func (r *UpdateHeader) UnmarshalJSON(b []byte) error { + type tmp UpdateHeader + var s struct { + tmp + ContentLength string `json:"Content-Length"` + Date gophercloud.JSONRFC1123 `json:"Date"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = UpdateHeader(s.tmp) + + switch s.ContentLength { + case "": + r.ContentLength = 0 + default: + r.ContentLength, err = strconv.ParseInt(s.ContentLength, 10, 64) + if err != nil { + return err + } + } + + r.Date = time.Time(s.Date) + + return nil +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + gophercloud.HeaderResult +} + +// Extract will return a struct of headers returned from a call to Update. To obtain +// a map of headers, call the ExtractHeader method on the UpdateResult. +func (r UpdateResult) Extract() (*UpdateHeader, error) { + var s *UpdateHeader + err := r.ExtractInto(&s) + return s, err +} + +// DeleteHeader represents the headers returned in the response from a Delete request. +type DeleteHeader struct { + ContentLength int64 `json:"Content-Length"` + ContentType string `json:"Content-Type"` + Date time.Time `json:"-"` + TransID string `json:"X-Trans-Id"` +} + +func (r *DeleteHeader) UnmarshalJSON(b []byte) error { + type tmp DeleteHeader + var s struct { + tmp + ContentLength string `json:"Content-Length"` + Date gophercloud.JSONRFC1123 `json:"Date"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = DeleteHeader(s.tmp) + + switch s.ContentLength { + case "": + r.ContentLength = 0 + default: + r.ContentLength, err = strconv.ParseInt(s.ContentLength, 10, 64) + if err != nil { + return err + } + } + + r.Date = time.Time(s.Date) + + return nil +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.HeaderResult +} + +// Extract will return a struct of headers returned from a call to Delete. To obtain +// a map of headers, call the ExtractHeader method on the DeleteResult. +func (r DeleteResult) Extract() (*DeleteHeader, error) { + var s *DeleteHeader + err := r.ExtractInto(&s) + return s, err +} + +// CopyHeader represents the headers returned in the response from a Copy request. +type CopyHeader struct { + ContentLength int64 `json:"-"` + ContentType string `json:"Content-Type"` + CopiedFrom string `json:"X-Copied-From"` + CopiedFromLastModified time.Time `json:"-"` + Date time.Time `json:"-"` + ETag string `json:"Etag"` + LastModified time.Time `json:"-"` + TransID string `json:"X-Trans-Id"` +} + +func (r *CopyHeader) UnmarshalJSON(b []byte) error { + type tmp CopyHeader + var s struct { + tmp + ContentLength string `json:"Content-Length"` + CopiedFromLastModified gophercloud.JSONRFC1123 `json:"X-Copied-From-Last-Modified"` + Date gophercloud.JSONRFC1123 `json:"Date"` + LastModified gophercloud.JSONRFC1123 `json:"Last-Modified"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = CopyHeader(s.tmp) + + switch s.ContentLength { + case "": + r.ContentLength = 0 + default: + r.ContentLength, err = strconv.ParseInt(s.ContentLength, 10, 64) + if err != nil { + return err + } + } + + r.Date = time.Time(s.Date) + r.CopiedFromLastModified = time.Time(s.CopiedFromLastModified) + r.LastModified = time.Time(s.LastModified) + + return nil +} + +// CopyResult represents the result of a copy operation. +type CopyResult struct { + gophercloud.HeaderResult +} + +// Extract will return a struct of headers returned from a call to Copy. To obtain +// a map of headers, call the ExtractHeader method on the CopyResult. +func (r CopyResult) Extract() (*CopyHeader, error) { + var s *CopyHeader + err := r.ExtractInto(&s) + return s, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/objects/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/objects/testing/doc.go new file mode 100644 index 0000000000..f008a801de --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/objects/testing/doc.go @@ -0,0 +1,2 @@ +// objectstorage_objects_v1 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/objects/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/objects/testing/fixtures.go new file mode 100644 index 0000000000..08faab89a6 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/objects/testing/fixtures.go @@ -0,0 +1,214 @@ +package testing + +import ( + "crypto/md5" + "fmt" + "io" + "net/http" + "testing" + "time" + + "github.com/gophercloud/gophercloud/openstack/objectstorage/v1/objects" + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +// HandleDownloadObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux that +// responds with a `Download` response. +func HandleDownloadObjectSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + w.Header().Set("Date", "Wed, 10 Nov 2009 23:00:00 GMT") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "Successful download with Gophercloud") + }) +} + +// ExpectedListInfo is the result expected from a call to `List` when full +// info is requested. +var ExpectedListInfo = []objects.Object{ + { + Hash: "451e372e48e0f6b1114fa0724aa79fa1", + LastModified: time.Date(2016, time.August, 17, 22, 11, 58, 602650000, time.UTC), //"2016-08-17T22:11:58.602650" + Bytes: 14, + Name: "goodbye", + ContentType: "application/octet-stream", + }, + { + Hash: "451e372e48e0f6b1114fa0724aa79fa1", + LastModified: time.Date(2016, time.August, 17, 22, 11, 58, 602650000, time.UTC), + Bytes: 14, + Name: "hello", + ContentType: "application/octet-stream", + }, +} + +// ExpectedListNames is the result expected from a call to `List` when just +// object names are requested. +var ExpectedListNames = []string{"hello", "goodbye"} + +// HandleListObjectsInfoSuccessfully creates an HTTP handler at `/testContainer` on the test handler mux that +// responds with a `List` response when full info is requested. +func HandleListObjectsInfoSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, `[ + { + "hash": "451e372e48e0f6b1114fa0724aa79fa1", + "last_modified": "2016-08-17T22:11:58.602650", + "bytes": 14, + "name": "goodbye", + "content_type": "application/octet-stream" + }, + { + "hash": "451e372e48e0f6b1114fa0724aa79fa1", + "last_modified": "2016-08-17T22:11:58.602650", + "bytes": 14, + "name": "hello", + "content_type": "application/octet-stream" + } + ]`) + case "hello": + fmt.Fprintf(w, `[]`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) +} + +// HandleListObjectNamesSuccessfully creates an HTTP handler at `/testContainer` on the test handler mux that +// responds with a `List` response when only object names are requested. +func HandleListObjectNamesSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "text/plain") + + w.Header().Set("Content-Type", "text/plain") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, "hello\ngoodbye\n") + case "goodbye": + fmt.Fprintf(w, "") + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) +} + +// HandleCreateTextObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux +// that responds with a `Create` response. A Content-Type of "text/plain" is expected. +func HandleCreateTextObjectSuccessfully(t *testing.T, content string) { + th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "text/plain") + th.TestHeader(t, r, "Accept", "application/json") + + hash := md5.New() + io.WriteString(hash, content) + localChecksum := hash.Sum(nil) + + w.Header().Set("ETag", fmt.Sprintf("%x", localChecksum)) + w.WriteHeader(http.StatusCreated) + }) +} + +// HandleCreateTextWithCacheControlSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler +// mux that responds with a `Create` response. A Cache-Control of `max-age="3600", public` is expected. +func HandleCreateTextWithCacheControlSuccessfully(t *testing.T, content string) { + th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Cache-Control", `max-age="3600", public`) + th.TestHeader(t, r, "Accept", "application/json") + + hash := md5.New() + io.WriteString(hash, content) + localChecksum := hash.Sum(nil) + + w.Header().Set("ETag", fmt.Sprintf("%x", localChecksum)) + w.WriteHeader(http.StatusCreated) + }) +} + +// HandleCreateTypelessObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler +// mux that responds with a `Create` response. No Content-Type header may be present in the request, so that server- +// side content-type detection will be triggered properly. +func HandleCreateTypelessObjectSuccessfully(t *testing.T, content string) { + th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + if contentType, present := r.Header["Content-Type"]; present { + t.Errorf("Expected Content-Type header to be omitted, but was %#v", contentType) + } + + hash := md5.New() + io.WriteString(hash, content) + localChecksum := hash.Sum(nil) + + w.Header().Set("ETag", fmt.Sprintf("%x", localChecksum)) + w.WriteHeader(http.StatusCreated) + }) +} + +// HandleCopyObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux that +// responds with a `Copy` response. +func HandleCopyObjectSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "COPY") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "Destination", "/newTestContainer/newTestObject") + w.WriteHeader(http.StatusCreated) + }) +} + +// HandleDeleteObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux that +// responds with a `Delete` response. +func HandleDeleteObjectSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleUpdateObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux that +// responds with a `Update` response. +func HandleUpdateObjectSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "X-Object-Meta-Gophercloud-Test", "objects") + w.WriteHeader(http.StatusAccepted) + }) +} + +// HandleGetObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux that +// responds with a `Get` response. +func HandleGetObjectSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "HEAD") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + w.Header().Add("X-Object-Meta-Gophercloud-Test", "objects") + w.WriteHeader(http.StatusNoContent) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/objects/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/objects/testing/requests_test.go new file mode 100644 index 0000000000..4f2663212a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/objects/testing/requests_test.go @@ -0,0 +1,196 @@ +package testing + +import ( + "bytes" + "io" + "strings" + "testing" + "time" + + "github.com/gophercloud/gophercloud/openstack/objectstorage/v1/objects" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +var ( + loc, _ = time.LoadLocation("GMT") +) + +func TestDownloadReader(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDownloadObjectSuccessfully(t) + + response := objects.Download(fake.ServiceClient(), "testContainer", "testObject", nil) + defer response.Body.Close() + + // Check reader + buf := bytes.NewBuffer(make([]byte, 0)) + io.CopyN(buf, response.Body, 10) + th.CheckEquals(t, "Successful", string(buf.Bytes())) +} + +func TestDownloadExtraction(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDownloadObjectSuccessfully(t) + + response := objects.Download(fake.ServiceClient(), "testContainer", "testObject", nil) + + // Check []byte extraction + bytes, err := response.ExtractContent() + th.AssertNoErr(t, err) + th.CheckEquals(t, "Successful download with Gophercloud", string(bytes)) + + expected := &objects.DownloadHeader{ + ContentLength: 36, + ContentType: "text/plain; charset=utf-8", + Date: time.Date(2009, time.November, 10, 23, 0, 0, 0, loc), + } + actual, err := response.Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expected, actual) +} + +func TestListObjectInfo(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListObjectsInfoSuccessfully(t) + + count := 0 + options := &objects.ListOpts{Full: true} + err := objects.List(fake.ServiceClient(), "testContainer", options).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := objects.ExtractInfo(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ExpectedListInfo, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestListObjectNames(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListObjectNamesSuccessfully(t) + + count := 0 + options := &objects.ListOpts{Full: false} + err := objects.List(fake.ServiceClient(), "testContainer", options).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := objects.ExtractNames(page) + if err != nil { + t.Errorf("Failed to extract container names: %v", err) + return false, err + } + + th.CheckDeepEquals(t, ExpectedListNames, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestCreateObject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + content := "Did gyre and gimble in the wabe" + + HandleCreateTextObjectSuccessfully(t, content) + + options := &objects.CreateOpts{ContentType: "text/plain", Content: strings.NewReader(content)} + res := objects.Create(fake.ServiceClient(), "testContainer", "testObject", options) + th.AssertNoErr(t, res.Err) +} + +func TestCreateObjectWithCacheControl(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + content := "All mimsy were the borogoves" + + HandleCreateTextWithCacheControlSuccessfully(t, content) + + options := &objects.CreateOpts{ + CacheControl: `max-age="3600", public`, + Content: strings.NewReader(content), + } + res := objects.Create(fake.ServiceClient(), "testContainer", "testObject", options) + th.AssertNoErr(t, res.Err) +} + +func TestCreateObjectWithoutContentType(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + content := "The sky was the color of television, tuned to a dead channel." + + HandleCreateTypelessObjectSuccessfully(t, content) + + res := objects.Create(fake.ServiceClient(), "testContainer", "testObject", &objects.CreateOpts{Content: strings.NewReader(content)}) + th.AssertNoErr(t, res.Err) +} + +/* +func TestErrorIsRaisedForChecksumMismatch(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("ETag", "acbd18db4cc2f85cedef654fccc4a4d8") + w.WriteHeader(http.StatusCreated) + }) + + content := strings.NewReader("The sky was the color of television, tuned to a dead channel.") + res := Create(fake.ServiceClient(), "testContainer", "testObject", &CreateOpts{Content: content}) + + err := fmt.Errorf("Local checksum does not match API ETag header") + th.AssertDeepEquals(t, err, res.Err) +} +*/ + +func TestCopyObject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCopyObjectSuccessfully(t) + + options := &objects.CopyOpts{Destination: "/newTestContainer/newTestObject"} + res := objects.Copy(fake.ServiceClient(), "testContainer", "testObject", options) + th.AssertNoErr(t, res.Err) +} + +func TestDeleteObject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteObjectSuccessfully(t) + + res := objects.Delete(fake.ServiceClient(), "testContainer", "testObject", nil) + th.AssertNoErr(t, res.Err) +} + +func TestUpateObjectMetadata(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleUpdateObjectSuccessfully(t) + + options := &objects.UpdateOpts{Metadata: map[string]string{"Gophercloud-Test": "objects"}} + res := objects.Update(fake.ServiceClient(), "testContainer", "testObject", options) + th.AssertNoErr(t, res.Err) +} + +func TestGetObject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetObjectSuccessfully(t) + + expected := map[string]string{"Gophercloud-Test": "objects"} + actual, err := objects.Get(fake.ServiceClient(), "testContainer", "testObject", nil).ExtractMetadata() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expected, actual) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/objects/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/objects/urls.go new file mode 100644 index 0000000000..b3ac304b74 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/objects/urls.go @@ -0,0 +1,33 @@ +package objects + +import ( + "github.com/gophercloud/gophercloud" +) + +func listURL(c *gophercloud.ServiceClient, container string) string { + return c.ServiceURL(container) +} + +func copyURL(c *gophercloud.ServiceClient, container, object string) string { + return c.ServiceURL(container, object) +} + +func createURL(c *gophercloud.ServiceClient, container, object string) string { + return copyURL(c, container, object) +} + +func getURL(c *gophercloud.ServiceClient, container, object string) string { + return copyURL(c, container, object) +} + +func deleteURL(c *gophercloud.ServiceClient, container, object string) string { + return copyURL(c, container, object) +} + +func downloadURL(c *gophercloud.ServiceClient, container, object string) string { + return copyURL(c, container, object) +} + +func updateURL(c *gophercloud.ServiceClient, container, object string) string { + return copyURL(c, container, object) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/swauth/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/swauth/requests.go new file mode 100644 index 0000000000..e8589ae0cb --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/swauth/requests.go @@ -0,0 +1,70 @@ +package swauth + +import "github.com/gophercloud/gophercloud" + +// AuthOptsBuilder describes struct types that can be accepted by the Auth call. +// The AuthOpts struct in this package does. +type AuthOptsBuilder interface { + ToAuthOptsMap() (map[string]string, error) +} + +// AuthOpts specifies an authentication request. +type AuthOpts struct { + // User is an Swauth-based username in username:tenant format. + User string `h:"X-Auth-User" required:"true"` + // Key is a secret/password to authenticate the User with. + Key string `h:"X-Auth-Key" required:"true"` +} + +// ToAuthOptsMap formats an AuthOpts structure into a request body. +func (opts AuthOpts) ToAuthOptsMap() (map[string]string, error) { + return gophercloud.BuildHeaders(opts) +} + +// Auth performs an authentication request for a Swauth-based user. +func Auth(c *gophercloud.ProviderClient, opts AuthOptsBuilder) (r GetAuthResult) { + h := make(map[string]string) + + if opts != nil { + headers, err := opts.ToAuthOptsMap() + if err != nil { + r.Err = err + return + } + + for k, v := range headers { + h[k] = v + } + } + + resp, err := c.Request("GET", getURL(c), &gophercloud.RequestOpts{ + MoreHeaders: h, + OkCodes: []int{200}, + }) + + if resp != nil { + r.Header = resp.Header + } + + r.Err = err + + return r +} + +// NewObjectStorageV1 creates a Swauth-authenticated *gophercloud.ServiceClient +// client that can issue ObjectStorage-based API calls. +func NewObjectStorageV1(pc *gophercloud.ProviderClient, authOpts AuthOpts) (*gophercloud.ServiceClient, error) { + auth, err := Auth(pc, authOpts).Extract() + if err != nil { + return nil, err + } + + swiftClient := &gophercloud.ServiceClient{ + ProviderClient: pc, + Endpoint: gophercloud.NormalizeURL(auth.StorageURL), + } + + swiftClient.TokenID = auth.Token + + return swiftClient, nil +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/swauth/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/swauth/results.go new file mode 100644 index 0000000000..294c43c07c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/swauth/results.go @@ -0,0 +1,27 @@ +package swauth + +import ( + "github.com/gophercloud/gophercloud" +) + +// GetAuthResult temporarily contains the response from a Swauth +// authentication call. +type GetAuthResult struct { + gophercloud.HeaderResult +} + +// AuthResult contains the authentication information from a Swauth +// authentication request. +type AuthResult struct { + Token string `json:"X-Auth-Token"` + StorageURL string `json:"X-Storage-Url"` + CDNURL string `json:"X-CDN-Management-Url"` +} + +// Extract is a method that attempts to interpret any Swauth authentication +// response as a AuthResult struct. +func (r GetAuthResult) Extract() (*AuthResult, error) { + var s *AuthResult + err := r.ExtractInto(&s) + return s, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/swauth/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/swauth/testing/doc.go new file mode 100644 index 0000000000..ff3bf3797b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/swauth/testing/doc.go @@ -0,0 +1,2 @@ +// objectstorage_swauth_v1 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/swauth/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/swauth/testing/fixtures.go new file mode 100644 index 0000000000..79858f5c87 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/swauth/testing/fixtures.go @@ -0,0 +1,29 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud/openstack/objectstorage/v1/swauth" + th "github.com/gophercloud/gophercloud/testhelper" +) + +// AuthResult is the expected result of AuthOutput +var AuthResult = swauth.AuthResult{ + Token: "AUTH_tk6223e6071f8f4299aa334b48015484a1", + StorageURL: "http://127.0.0.1:8080/v1/AUTH_test/", +} + +// HandleAuthSuccessfully configures the test server to respond to an Auth request. +func HandleAuthSuccessfully(t *testing.T, authOpts swauth.AuthOpts) { + th.Mux.HandleFunc("/auth/v1.0", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-User", authOpts.User) + th.TestHeader(t, r, "X-Auth-Key", authOpts.Key) + + w.Header().Add("X-Auth-Token", AuthResult.Token) + w.Header().Add("X-Storage-Url", AuthResult.StorageURL) + fmt.Fprintf(w, "") + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/swauth/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/swauth/testing/requests_test.go new file mode 100644 index 0000000000..57b503463f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/swauth/testing/requests_test.go @@ -0,0 +1,27 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack" + "github.com/gophercloud/gophercloud/openstack/objectstorage/v1/swauth" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestAuth(t *testing.T) { + authOpts := swauth.AuthOpts{ + User: "test:tester", + Key: "testing", + } + + th.SetupHTTP() + defer th.TeardownHTTP() + HandleAuthSuccessfully(t, authOpts) + + providerClient, err := openstack.NewClient(th.Endpoint()) + th.AssertNoErr(t, err) + + swiftClient, err := swauth.NewObjectStorageV1(providerClient, authOpts) + th.AssertNoErr(t, err) + th.AssertEquals(t, swiftClient.TokenID, AuthResult.Token) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/swauth/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/swauth/urls.go new file mode 100644 index 0000000000..a30cabd60e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/objectstorage/v1/swauth/urls.go @@ -0,0 +1,7 @@ +package swauth + +import "github.com/gophercloud/gophercloud" + +func getURL(c *gophercloud.ProviderClient) string { + return c.IdentityBase + "auth/v1.0" +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/apiversions/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/apiversions/doc.go new file mode 100644 index 0000000000..f2db622d1f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/apiversions/doc.go @@ -0,0 +1,4 @@ +// Package apiversions provides information and interaction with the different +// API versions for the OpenStack Heat service. This functionality is not +// restricted to this particular version. +package apiversions diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/apiversions/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/apiversions/requests.go new file mode 100644 index 0000000000..ff383cff68 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/apiversions/requests.go @@ -0,0 +1,13 @@ +package apiversions + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListVersions lists all the Neutron API versions available to end-users +func ListVersions(c *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(c, apiVersionsURL(c), func(r pagination.PageResult) pagination.Page { + return APIVersionPage{pagination.SinglePageBase(r)} + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/apiversions/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/apiversions/results.go new file mode 100644 index 0000000000..a7c22a2739 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/apiversions/results.go @@ -0,0 +1,36 @@ +package apiversions + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// APIVersion represents an API version for Neutron. It contains the status of +// the API, and its unique ID. +type APIVersion struct { + Status string `json:"status"` + ID string `json:"id"` + Links []gophercloud.Link `json:"links"` +} + +// APIVersionPage is the page returned by a pager when traversing over a +// collection of API versions. +type APIVersionPage struct { + pagination.SinglePageBase +} + +// IsEmpty checks whether an APIVersionPage struct is empty. +func (r APIVersionPage) IsEmpty() (bool, error) { + is, err := ExtractAPIVersions(r) + return len(is) == 0, err +} + +// ExtractAPIVersions takes a collection page, extracts all of the elements, +// and returns them a slice of APIVersion structs. It is effectively a cast. +func ExtractAPIVersions(r pagination.Page) ([]APIVersion, error) { + var s struct { + APIVersions []APIVersion `json:"versions"` + } + err := (r.(APIVersionPage)).ExtractInto(&s) + return s.APIVersions, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/apiversions/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/apiversions/testing/doc.go new file mode 100644 index 0000000000..3d545fdda0 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/apiversions/testing/doc.go @@ -0,0 +1,2 @@ +// orchestration_apiversions_v1 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/apiversions/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/apiversions/testing/requests_test.go new file mode 100644 index 0000000000..ac59b6c6c2 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/apiversions/testing/requests_test.go @@ -0,0 +1,90 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/orchestration/v1/apiversions" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestListVersions(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "versions": [ + { + "status": "CURRENT", + "id": "v1.0", + "links": [ + { + "href": "http://23.253.228.211:8000/v1", + "rel": "self" + } + ] + } + ] +}`) + }) + + count := 0 + + apiversions.ListVersions(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := apiversions.ExtractAPIVersions(page) + if err != nil { + t.Errorf("Failed to extract API versions: %v", err) + return false, err + } + + expected := []apiversions.APIVersion{ + { + Status: "CURRENT", + ID: "v1.0", + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "http://23.253.228.211:8000/v1", + Rel: "self", + }, + }, + }, + } + + th.AssertDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestNonJSONCannotBeExtractedIntoAPIVersions(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + apiversions.ListVersions(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + if _, err := apiversions.ExtractAPIVersions(page); err == nil { + t.Fatalf("Expected error, got nil") + } + return true, nil + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/apiversions/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/apiversions/urls.go new file mode 100644 index 0000000000..0205405a04 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/apiversions/urls.go @@ -0,0 +1,7 @@ +package apiversions + +import "github.com/gophercloud/gophercloud" + +func apiVersionsURL(c *gophercloud.ServiceClient) string { + return c.Endpoint +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/buildinfo/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/buildinfo/doc.go new file mode 100644 index 0000000000..183e8dfa76 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/buildinfo/doc.go @@ -0,0 +1,2 @@ +// Package buildinfo provides build information about heat deployments. +package buildinfo diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/buildinfo/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/buildinfo/requests.go new file mode 100644 index 0000000000..32f6032d66 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/buildinfo/requests.go @@ -0,0 +1,9 @@ +package buildinfo + +import "github.com/gophercloud/gophercloud" + +// Get retreives data for the given stack template. +func Get(c *gophercloud.ServiceClient) (r GetResult) { + _, r.Err = c.Get(getURL(c), &r.Body, nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/buildinfo/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/buildinfo/results.go new file mode 100644 index 0000000000..c3d2cdbeff --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/buildinfo/results.go @@ -0,0 +1,29 @@ +package buildinfo + +import ( + "github.com/gophercloud/gophercloud" +) + +// Revision represents the API/Engine revision of a Heat deployment. +type Revision struct { + Revision string `json:"revision"` +} + +// BuildInfo represents the build information for a Heat deployment. +type BuildInfo struct { + API Revision `json:"api"` + Engine Revision `json:"engine"` +} + +// GetResult represents the result of a Get operation. +type GetResult struct { + gophercloud.Result +} + +// Extract returns a pointer to a BuildInfo object and is called after a +// Get operation. +func (r GetResult) Extract() (*BuildInfo, error) { + var s *BuildInfo + err := r.ExtractInto(&s) + return s, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/buildinfo/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/buildinfo/testing/doc.go new file mode 100644 index 0000000000..3655974438 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/buildinfo/testing/doc.go @@ -0,0 +1,2 @@ +// orchestration_buildinfo_v1 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/buildinfo/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/buildinfo/testing/fixtures.go new file mode 100644 index 0000000000..c240d5f581 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/buildinfo/testing/fixtures.go @@ -0,0 +1,46 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud/openstack/orchestration/v1/buildinfo" + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +// GetExpected represents the expected object from a Get request. +var GetExpected = &buildinfo.BuildInfo{ + API: buildinfo.Revision{ + Revision: "2.4.5", + }, + Engine: buildinfo.Revision{ + Revision: "1.2.1", + }, +} + +// GetOutput represents the response body from a Get request. +const GetOutput = ` +{ + "api": { + "revision": "2.4.5" + }, + "engine": { + "revision": "1.2.1" + } +}` + +// HandleGetSuccessfully creates an HTTP handler at `/build_info` +// on the test handler mux that responds with a `Get` response. +func HandleGetSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/build_info", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, output) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/buildinfo/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/buildinfo/testing/requests_test.go new file mode 100644 index 0000000000..bd2e164af0 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/buildinfo/testing/requests_test.go @@ -0,0 +1,21 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/orchestration/v1/buildinfo" + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestGetTemplate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetSuccessfully(t, GetOutput) + + actual, err := buildinfo.Get(fake.ServiceClient()).Extract() + th.AssertNoErr(t, err) + + expected := GetExpected + th.AssertDeepEquals(t, expected, actual) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/buildinfo/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/buildinfo/urls.go new file mode 100644 index 0000000000..28a2128df8 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/buildinfo/urls.go @@ -0,0 +1,7 @@ +package buildinfo + +import "github.com/gophercloud/gophercloud" + +func getURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("build_info") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackevents/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackevents/doc.go new file mode 100644 index 0000000000..51cdd97473 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackevents/doc.go @@ -0,0 +1,4 @@ +// Package stackevents provides operations for finding, listing, and retrieving +// stack events. Stack events are events that take place on stacks such as +// updating and abandoning. +package stackevents diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackevents/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackevents/requests.go new file mode 100644 index 0000000000..e6e7f79147 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackevents/requests.go @@ -0,0 +1,182 @@ +package stackevents + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Find retrieves stack events for the given stack name. +func Find(c *gophercloud.ServiceClient, stackName string) (r FindResult) { + _, r.Err = c.Get(findURL(c, stackName), &r.Body, nil) + return +} + +// SortDir is a type for specifying in which direction to sort a list of events. +type SortDir string + +// SortKey is a type for specifying by which key to sort a list of events. +type SortKey string + +// ResourceStatus is a type for specifying by which resource status to filter a +// list of events. +type ResourceStatus string + +// ResourceAction is a type for specifying by which resource action to filter a +// list of events. +type ResourceAction string + +var ( + // ResourceStatusInProgress is used to filter a List request by the 'IN_PROGRESS' status. + ResourceStatusInProgress ResourceStatus = "IN_PROGRESS" + // ResourceStatusComplete is used to filter a List request by the 'COMPLETE' status. + ResourceStatusComplete ResourceStatus = "COMPLETE" + // ResourceStatusFailed is used to filter a List request by the 'FAILED' status. + ResourceStatusFailed ResourceStatus = "FAILED" + + // ResourceActionCreate is used to filter a List request by the 'CREATE' action. + ResourceActionCreate ResourceAction = "CREATE" + // ResourceActionDelete is used to filter a List request by the 'DELETE' action. + ResourceActionDelete ResourceAction = "DELETE" + // ResourceActionUpdate is used to filter a List request by the 'UPDATE' action. + ResourceActionUpdate ResourceAction = "UPDATE" + // ResourceActionRollback is used to filter a List request by the 'ROLLBACK' action. + ResourceActionRollback ResourceAction = "ROLLBACK" + // ResourceActionSuspend is used to filter a List request by the 'SUSPEND' action. + ResourceActionSuspend ResourceAction = "SUSPEND" + // ResourceActionResume is used to filter a List request by the 'RESUME' action. + ResourceActionResume ResourceAction = "RESUME" + // ResourceActionAbandon is used to filter a List request by the 'ABANDON' action. + ResourceActionAbandon ResourceAction = "ABANDON" + + // SortAsc is used to sort a list of stacks in ascending order. + SortAsc SortDir = "asc" + // SortDesc is used to sort a list of stacks in descending order. + SortDesc SortDir = "desc" + + // SortName is used to sort a list of stacks by name. + SortName SortKey = "name" + // SortResourceType is used to sort a list of stacks by resource type. + SortResourceType SortKey = "resource_type" + // SortCreatedAt is used to sort a list of stacks by date created. + SortCreatedAt SortKey = "created_at" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToStackEventListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Marker and Limit are used for pagination. +type ListOpts struct { + // The stack resource ID with which to start the listing. + Marker string `q:"marker"` + // Integer value for the limit of values to return. + Limit int `q:"limit"` + // Filters the event list by the specified ResourceAction. You can use this + // filter multiple times to filter by multiple resource actions: CREATE, DELETE, + // UPDATE, ROLLBACK, SUSPEND, RESUME or ADOPT. + ResourceActions []ResourceAction `q:"resource_action"` + // Filters the event list by the specified resource_status. You can use this + // filter multiple times to filter by multiple resource statuses: IN_PROGRESS, + // COMPLETE or FAILED. + ResourceStatuses []ResourceStatus `q:"resource_status"` + // Filters the event list by the specified resource_name. You can use this + // filter multiple times to filter by multiple resource names. + ResourceNames []string `q:"resource_name"` + // Filters the event list by the specified resource_type. You can use this + // filter multiple times to filter by multiple resource types: OS::Nova::Server, + // OS::Cinder::Volume, and so on. + ResourceTypes []string `q:"resource_type"` + // Sorts the event list by: resource_type or created_at. + SortKey SortKey `q:"sort_keys"` + // The sort direction of the event list. Which is asc (ascending) or desc (descending). + SortDir SortDir `q:"sort_dir"` +} + +// ToStackEventListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToStackEventListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List makes a request against the API to list resources for the given stack. +func List(client *gophercloud.ServiceClient, stackName, stackID string, opts ListOptsBuilder) pagination.Pager { + url := listURL(client, stackName, stackID) + if opts != nil { + query, err := opts.ToStackEventListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + p := EventPage{pagination.MarkerPageBase{PageResult: r}} + p.MarkerPageBase.Owner = p + return p + }) +} + +// ListResourceEventsOptsBuilder allows extensions to add additional parameters to the +// ListResourceEvents request. +type ListResourceEventsOptsBuilder interface { + ToResourceEventListQuery() (string, error) +} + +// ListResourceEventsOpts allows the filtering and sorting of paginated resource events through +// the API. Marker and Limit are used for pagination. +type ListResourceEventsOpts struct { + // The stack resource ID with which to start the listing. + Marker string `q:"marker"` + // Integer value for the limit of values to return. + Limit int `q:"limit"` + // Filters the event list by the specified ResourceAction. You can use this + // filter multiple times to filter by multiple resource actions: CREATE, DELETE, + // UPDATE, ROLLBACK, SUSPEND, RESUME or ADOPT. + ResourceActions []string `q:"resource_action"` + // Filters the event list by the specified resource_status. You can use this + // filter multiple times to filter by multiple resource statuses: IN_PROGRESS, + // COMPLETE or FAILED. + ResourceStatuses []string `q:"resource_status"` + // Filters the event list by the specified resource_name. You can use this + // filter multiple times to filter by multiple resource names. + ResourceNames []string `q:"resource_name"` + // Filters the event list by the specified resource_type. You can use this + // filter multiple times to filter by multiple resource types: OS::Nova::Server, + // OS::Cinder::Volume, and so on. + ResourceTypes []string `q:"resource_type"` + // Sorts the event list by: resource_type or created_at. + SortKey SortKey `q:"sort_keys"` + // The sort direction of the event list. Which is asc (ascending) or desc (descending). + SortDir SortDir `q:"sort_dir"` +} + +// ToResourceEventListQuery formats a ListResourceEventsOpts into a query string. +func (opts ListResourceEventsOpts) ToResourceEventListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// ListResourceEvents makes a request against the API to list resources for the given stack. +func ListResourceEvents(client *gophercloud.ServiceClient, stackName, stackID, resourceName string, opts ListResourceEventsOptsBuilder) pagination.Pager { + url := listResourceEventsURL(client, stackName, stackID, resourceName) + if opts != nil { + query, err := opts.ToResourceEventListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + p := EventPage{pagination.MarkerPageBase{PageResult: r}} + p.MarkerPageBase.Owner = p + return p + }) +} + +// Get retreives data for the given stack resource. +func Get(c *gophercloud.ServiceClient, stackName, stackID, resourceName, eventID string) (r GetResult) { + _, r.Err = c.Get(getURL(c, stackName, stackID, resourceName, eventID), &r.Body, nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackevents/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackevents/results.go new file mode 100644 index 0000000000..46fb0ff088 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackevents/results.go @@ -0,0 +1,119 @@ +package stackevents + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Event represents a stack event. +type Event struct { + // The name of the resource for which the event occurred. + ResourceName string `json:"resource_name"` + // The time the event occurred. + Time time.Time `json:"-"` + // The URLs to the event. + Links []gophercloud.Link `json:"links"` + // The logical ID of the stack resource. + LogicalResourceID string `json:"logical_resource_id"` + // The reason of the status of the event. + ResourceStatusReason string `json:"resource_status_reason"` + // The status of the event. + ResourceStatus string `json:"resource_status"` + // The physical ID of the stack resource. + PhysicalResourceID string `json:"physical_resource_id"` + // The event ID. + ID string `json:"id"` + // Properties of the stack resource. + ResourceProperties map[string]interface{} `json:"resource_properties"` +} + +func (r *Event) UnmarshalJSON(b []byte) error { + type tmp Event + var s struct { + tmp + Time gophercloud.JSONRFC3339NoZ `json:"event_time"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = Event(s.tmp) + + r.Time = time.Time(s.Time) + + return nil +} + +// FindResult represents the result of a Find operation. +type FindResult struct { + gophercloud.Result +} + +// Extract returns a slice of Event objects and is called after a +// Find operation. +func (r FindResult) Extract() ([]Event, error) { + var s struct { + Events []Event `json:"events"` + } + err := r.ExtractInto(&s) + return s.Events, err +} + +// EventPage abstracts the raw results of making a List() request against the API. +// As OpenStack extensions may freely alter the response bodies of structures returned to the client, you may only safely access the +// data provided through the ExtractResources call. +type EventPage struct { + pagination.MarkerPageBase +} + +// IsEmpty returns true if a page contains no Server results. +func (r EventPage) IsEmpty() (bool, error) { + events, err := ExtractEvents(r) + return len(events) == 0, err +} + +// LastMarker returns the last stack ID in a ListResult. +func (r EventPage) LastMarker() (string, error) { + events, err := ExtractEvents(r) + if err != nil { + return "", err + } + if len(events) == 0 { + return "", nil + } + return events[len(events)-1].ID, nil +} + +// ExtractEvents interprets the results of a single page from a List() call, producing a slice of Event entities. +func ExtractEvents(r pagination.Page) ([]Event, error) { + var s struct { + Events []Event `json:"events"` + } + err := (r.(EventPage)).ExtractInto(&s) + return s.Events, err +} + +// ExtractResourceEvents interprets the results of a single page from a +// ListResourceEvents() call, producing a slice of Event entities. +func ExtractResourceEvents(page pagination.Page) ([]Event, error) { + return ExtractEvents(page) +} + +// GetResult represents the result of a Get operation. +type GetResult struct { + gophercloud.Result +} + +// Extract returns a pointer to an Event object and is called after a +// Get operation. +func (r GetResult) Extract() (*Event, error) { + var s struct { + Event *Event `json:"event"` + } + err := r.ExtractInto(&s) + return s.Event, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackevents/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackevents/testing/doc.go new file mode 100644 index 0000000000..2e22a6c167 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackevents/testing/doc.go @@ -0,0 +1,2 @@ +// orchestration_stackevents_v1 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackevents/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackevents/testing/fixtures.go new file mode 100644 index 0000000000..a40e8d4f60 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackevents/testing/fixtures.go @@ -0,0 +1,447 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackevents" + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +// FindExpected represents the expected object from a Find request. +var FindExpected = []stackevents.Event{ + { + ResourceName: "hello_world", + Time: time.Date(2015, 2, 5, 21, 33, 11, 0, time.UTC), + Links: []gophercloud.Link{ + { + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/06feb26f-9298-4a9b-8749-9d770e5d577a", + Rel: "self", + }, + { + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + Rel: "resource", + }, + { + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + Rel: "stack", + }, + }, + LogicalResourceID: "hello_world", + ResourceStatusReason: "state changed", + ResourceStatus: "CREATE_IN_PROGRESS", + PhysicalResourceID: "", + ID: "06feb26f-9298-4a9b-8749-9d770e5d577a", + }, + { + ResourceName: "hello_world", + Time: time.Date(2015, 2, 5, 21, 33, 27, 0, time.UTC), + Links: []gophercloud.Link{ + { + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18", + Rel: "self", + }, + { + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + Rel: "resource", + }, + { + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + Rel: "stack", + }, + }, + LogicalResourceID: "hello_world", + ResourceStatusReason: "state changed", + ResourceStatus: "CREATE_COMPLETE", + PhysicalResourceID: "49181cd6-169a-4130-9455-31185bbfc5bf", + ID: "93940999-7d40-44ae-8de4-19624e7b8d18", + }, +} + +// FindOutput represents the response body from a Find request. +const FindOutput = ` +{ + "events": [ + { + "resource_name": "hello_world", + "event_time": "2015-02-05T21:33:11", + "links": [ + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/06feb26f-9298-4a9b-8749-9d770e5d577a", + "rel": "self" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + "rel": "resource" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + "rel": "stack" + } + ], + "logical_resource_id": "hello_world", + "resource_status_reason": "state changed", + "resource_status": "CREATE_IN_PROGRESS", + "physical_resource_id": null, + "id": "06feb26f-9298-4a9b-8749-9d770e5d577a" + }, + { + "resource_name": "hello_world", + "event_time": "2015-02-05T21:33:27", + "links": [ + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18", + "rel": "self" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + "rel": "resource" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + "rel": "stack" + } + ], + "logical_resource_id": "hello_world", + "resource_status_reason": "state changed", + "resource_status": "CREATE_COMPLETE", + "physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf", + "id": "93940999-7d40-44ae-8de4-19624e7b8d18" + } + ] +}` + +// HandleFindSuccessfully creates an HTTP handler at `/stacks/postman_stack/events` +// on the test handler mux that responds with a `Find` response. +func HandleFindSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/stacks/postman_stack/events", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, output) + }) +} + +// ListExpected represents the expected object from a List request. +var ListExpected = []stackevents.Event{ + { + ResourceName: "hello_world", + Time: time.Date(2015, 2, 5, 21, 33, 11, 0, time.UTC), + Links: []gophercloud.Link{ + { + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/06feb26f-9298-4a9b-8749-9d770e5d577a", + Rel: "self", + }, + { + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + Rel: "resource", + }, + { + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + Rel: "stack", + }, + }, + LogicalResourceID: "hello_world", + ResourceStatusReason: "state changed", + ResourceStatus: "CREATE_IN_PROGRESS", + PhysicalResourceID: "", + ID: "06feb26f-9298-4a9b-8749-9d770e5d577a", + }, + { + ResourceName: "hello_world", + Time: time.Date(2015, 2, 5, 21, 33, 27, 0, time.UTC), + Links: []gophercloud.Link{ + { + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18", + Rel: "self", + }, + { + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + Rel: "resource", + }, + { + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + Rel: "stack", + }, + }, + LogicalResourceID: "hello_world", + ResourceStatusReason: "state changed", + ResourceStatus: "CREATE_COMPLETE", + PhysicalResourceID: "49181cd6-169a-4130-9455-31185bbfc5bf", + ID: "93940999-7d40-44ae-8de4-19624e7b8d18", + }, +} + +// ListOutput represents the response body from a List request. +const ListOutput = ` +{ + "events": [ + { + "resource_name": "hello_world", + "event_time": "2015-02-05T21:33:11", + "links": [ + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/06feb26f-9298-4a9b-8749-9d770e5d577a", + "rel": "self" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + "rel": "resource" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + "rel": "stack" + } + ], + "logical_resource_id": "hello_world", + "resource_status_reason": "state changed", + "resource_status": "CREATE_IN_PROGRESS", + "physical_resource_id": null, + "id": "06feb26f-9298-4a9b-8749-9d770e5d577a" + }, + { + "resource_name": "hello_world", + "event_time": "2015-02-05T21:33:27", + "links": [ + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18", + "rel": "self" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + "rel": "resource" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + "rel": "stack" + } + ], + "logical_resource_id": "hello_world", + "resource_status_reason": "state changed", + "resource_status": "CREATE_COMPLETE", + "physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf", + "id": "93940999-7d40-44ae-8de4-19624e7b8d18" + } + ] +}` + +// HandleListSuccessfully creates an HTTP handler at `/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/events` +// on the test handler mux that responds with a `List` response. +func HandleListSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/events", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, output) + case "93940999-7d40-44ae-8de4-19624e7b8d18": + fmt.Fprintf(w, `{"events":[]}`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) +} + +// ListResourceEventsExpected represents the expected object from a ListResourceEvents request. +var ListResourceEventsExpected = []stackevents.Event{ + { + ResourceName: "hello_world", + Time: time.Date(2015, 2, 5, 21, 33, 11, 0, time.UTC), + Links: []gophercloud.Link{ + { + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/06feb26f-9298-4a9b-8749-9d770e5d577a", + Rel: "self", + }, + { + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + Rel: "resource", + }, + { + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + Rel: "stack", + }, + }, + LogicalResourceID: "hello_world", + ResourceStatusReason: "state changed", + ResourceStatus: "CREATE_IN_PROGRESS", + PhysicalResourceID: "", + ID: "06feb26f-9298-4a9b-8749-9d770e5d577a", + }, + { + ResourceName: "hello_world", + Time: time.Date(2015, 2, 5, 21, 33, 27, 0, time.UTC), + Links: []gophercloud.Link{ + { + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18", + Rel: "self", + }, + { + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + Rel: "resource", + }, + { + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + Rel: "stack", + }, + }, + LogicalResourceID: "hello_world", + ResourceStatusReason: "state changed", + ResourceStatus: "CREATE_COMPLETE", + PhysicalResourceID: "49181cd6-169a-4130-9455-31185bbfc5bf", + ID: "93940999-7d40-44ae-8de4-19624e7b8d18", + }, +} + +// ListResourceEventsOutput represents the response body from a ListResourceEvents request. +const ListResourceEventsOutput = ` +{ + "events": [ + { + "resource_name": "hello_world", + "event_time": "2015-02-05T21:33:11", + "links": [ + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/06feb26f-9298-4a9b-8749-9d770e5d577a", + "rel": "self" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + "rel": "resource" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + "rel": "stack" + } + ], + "logical_resource_id": "hello_world", + "resource_status_reason": "state changed", + "resource_status": "CREATE_IN_PROGRESS", + "physical_resource_id": null, + "id": "06feb26f-9298-4a9b-8749-9d770e5d577a" + }, + { + "resource_name": "hello_world", + "event_time": "2015-02-05T21:33:27", + "links": [ + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18", + "rel": "self" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + "rel": "resource" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + "rel": "stack" + } + ], + "logical_resource_id": "hello_world", + "resource_status_reason": "state changed", + "resource_status": "CREATE_COMPLETE", + "physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf", + "id": "93940999-7d40-44ae-8de4-19624e7b8d18" + } + ] +}` + +// HandleListResourceEventsSuccessfully creates an HTTP handler at `/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/resources/my_resource/events` +// on the test handler mux that responds with a `ListResourceEvents` response. +func HandleListResourceEventsSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/resources/my_resource/events", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, output) + case "93940999-7d40-44ae-8de4-19624e7b8d18": + fmt.Fprintf(w, `{"events":[]}`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) +} + +// GetExpected represents the expected object from a Get request. +var GetExpected = &stackevents.Event{ + ResourceName: "hello_world", + Time: time.Date(2015, 2, 5, 21, 33, 27, 0, time.UTC), + Links: []gophercloud.Link{ + { + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18", + Rel: "self", + }, + { + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + Rel: "resource", + }, + { + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + Rel: "stack", + }, + }, + LogicalResourceID: "hello_world", + ResourceStatusReason: "state changed", + ResourceStatus: "CREATE_COMPLETE", + PhysicalResourceID: "49181cd6-169a-4130-9455-31185bbfc5bf", + ID: "93940999-7d40-44ae-8de4-19624e7b8d18", +} + +// GetOutput represents the response body from a Get request. +const GetOutput = ` +{ + "event":{ + "resource_name": "hello_world", + "event_time": "2015-02-05T21:33:27", + "links": [ + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18", + "rel": "self" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + "rel": "resource" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + "rel": "stack" + } + ], + "logical_resource_id": "hello_world", + "resource_status_reason": "state changed", + "resource_status": "CREATE_COMPLETE", + "physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf", + "id": "93940999-7d40-44ae-8de4-19624e7b8d18" + } +}` + +// HandleGetSuccessfully creates an HTTP handler at `/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/resources/my_resource/events/93940999-7d40-44ae-8de4-19624e7b8d18` +// on the test handler mux that responds with a `Get` response. +func HandleGetSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/resources/my_resource/events/93940999-7d40-44ae-8de4-19624e7b8d18", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, output) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackevents/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackevents/testing/requests_test.go new file mode 100644 index 0000000000..0ad3fc31f6 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackevents/testing/requests_test.go @@ -0,0 +1,72 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackevents" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestFindEvents(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleFindSuccessfully(t, FindOutput) + + actual, err := stackevents.Find(fake.ServiceClient(), "postman_stack").Extract() + th.AssertNoErr(t, err) + + expected := FindExpected + th.AssertDeepEquals(t, expected, actual) +} + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListSuccessfully(t, ListOutput) + + count := 0 + err := stackevents.List(fake.ServiceClient(), "hello_world", "49181cd6-169a-4130-9455-31185bbfc5bf", nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := stackevents.ExtractEvents(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ListExpected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestListResourceEvents(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListResourceEventsSuccessfully(t, ListResourceEventsOutput) + + count := 0 + err := stackevents.ListResourceEvents(fake.ServiceClient(), "hello_world", "49181cd6-169a-4130-9455-31185bbfc5bf", "my_resource", nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := stackevents.ExtractEvents(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ListResourceEventsExpected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestGetEvent(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetSuccessfully(t, GetOutput) + + actual, err := stackevents.Get(fake.ServiceClient(), "hello_world", "49181cd6-169a-4130-9455-31185bbfc5bf", "my_resource", "93940999-7d40-44ae-8de4-19624e7b8d18").Extract() + th.AssertNoErr(t, err) + + expected := GetExpected + th.AssertDeepEquals(t, expected, actual) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackevents/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackevents/urls.go new file mode 100644 index 0000000000..6b6b330894 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackevents/urls.go @@ -0,0 +1,19 @@ +package stackevents + +import "github.com/gophercloud/gophercloud" + +func findURL(c *gophercloud.ServiceClient, stackName string) string { + return c.ServiceURL("stacks", stackName, "events") +} + +func listURL(c *gophercloud.ServiceClient, stackName, stackID string) string { + return c.ServiceURL("stacks", stackName, stackID, "events") +} + +func listResourceEventsURL(c *gophercloud.ServiceClient, stackName, stackID, resourceName string) string { + return c.ServiceURL("stacks", stackName, stackID, "resources", resourceName, "events") +} + +func getURL(c *gophercloud.ServiceClient, stackName, stackID, resourceName, eventID string) string { + return c.ServiceURL("stacks", stackName, stackID, "resources", resourceName, "events", eventID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackresources/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackresources/doc.go new file mode 100644 index 0000000000..e4f8b08dcc --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackresources/doc.go @@ -0,0 +1,5 @@ +// Package stackresources provides operations for working with stack resources. +// A resource is a template artifact that represents some component of your +// desired architecture (a Cloud Server, a group of scaled Cloud Servers, a load +// balancer, some configuration management system, and so forth). +package stackresources diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackresources/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackresources/requests.go new file mode 100644 index 0000000000..f368b76c6d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackresources/requests.go @@ -0,0 +1,77 @@ +package stackresources + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Find retrieves stack resources for the given stack name. +func Find(c *gophercloud.ServiceClient, stackName string) (r FindResult) { + _, r.Err = c.Get(findURL(c, stackName), &r.Body, nil) + return +} + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToStackResourceListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Marker and Limit are used for pagination. +type ListOpts struct { + // Include resources from nest stacks up to Depth levels of recursion. + Depth int `q:"nested_depth"` +} + +// ToStackResourceListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToStackResourceListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List makes a request against the API to list resources for the given stack. +func List(client *gophercloud.ServiceClient, stackName, stackID string, opts ListOptsBuilder) pagination.Pager { + url := listURL(client, stackName, stackID) + if opts != nil { + query, err := opts.ToStackResourceListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ResourcePage{pagination.SinglePageBase(r)} + }) +} + +// Get retreives data for the given stack resource. +func Get(c *gophercloud.ServiceClient, stackName, stackID, resourceName string) (r GetResult) { + _, r.Err = c.Get(getURL(c, stackName, stackID, resourceName), &r.Body, nil) + return +} + +// Metadata retreives the metadata for the given stack resource. +func Metadata(c *gophercloud.ServiceClient, stackName, stackID, resourceName string) (r MetadataResult) { + _, r.Err = c.Get(metadataURL(c, stackName, stackID, resourceName), &r.Body, nil) + return +} + +// ListTypes makes a request against the API to list resource types. +func ListTypes(client *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, listTypesURL(client), func(r pagination.PageResult) pagination.Page { + return ResourceTypePage{pagination.SinglePageBase(r)} + }) +} + +// Schema retreives the schema for the given resource type. +func Schema(c *gophercloud.ServiceClient, resourceType string) (r SchemaResult) { + _, r.Err = c.Get(schemaURL(c, resourceType), &r.Body, nil) + return +} + +// Template retreives the template representation for the given resource type. +func Template(c *gophercloud.ServiceClient, resourceType string) (r TemplateResult) { + _, r.Err = c.Get(templateURL(c, resourceType), &r.Body, nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackresources/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackresources/results.go new file mode 100644 index 0000000000..59c02a38c1 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackresources/results.go @@ -0,0 +1,185 @@ +package stackresources + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Resource represents a stack resource. +type Resource struct { + Attributes map[string]interface{} `json:"attributes"` + CreationTime time.Time `json:"-"` + Description string `json:"description"` + Links []gophercloud.Link `json:"links"` + LogicalID string `json:"logical_resource_id"` + Name string `json:"resource_name"` + PhysicalID string `json:"physical_resource_id"` + RequiredBy []interface{} `json:"required_by"` + Status string `json:"resource_status"` + StatusReason string `json:"resource_status_reason"` + Type string `json:"resource_type"` + UpdatedTime time.Time `json:"-"` +} + +func (r *Resource) UnmarshalJSON(b []byte) error { + type tmp Resource + var s struct { + tmp + CreationTime gophercloud.JSONRFC3339NoZ `json:"creation_time"` + UpdatedTime gophercloud.JSONRFC3339NoZ `json:"updated_time"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Resource(s.tmp) + + r.CreationTime = time.Time(s.CreationTime) + r.UpdatedTime = time.Time(s.UpdatedTime) + + return nil +} + +// FindResult represents the result of a Find operation. +type FindResult struct { + gophercloud.Result +} + +// Extract returns a slice of Resource objects and is called after a +// Find operation. +func (r FindResult) Extract() ([]Resource, error) { + var s struct { + Resources []Resource `json:"resources"` + } + err := r.ExtractInto(&s) + return s.Resources, err +} + +// ResourcePage abstracts the raw results of making a List() request against the API. +// As OpenStack extensions may freely alter the response bodies of structures returned to the client, you may only safely access the +// data provided through the ExtractResources call. +type ResourcePage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a page contains no Server results. +func (r ResourcePage) IsEmpty() (bool, error) { + resources, err := ExtractResources(r) + return len(resources) == 0, err +} + +// ExtractResources interprets the results of a single page from a List() call, producing a slice of Resource entities. +func ExtractResources(r pagination.Page) ([]Resource, error) { + var s struct { + Resources []Resource `json:"resources"` + } + err := (r.(ResourcePage)).ExtractInto(&s) + return s.Resources, err +} + +// GetResult represents the result of a Get operation. +type GetResult struct { + gophercloud.Result +} + +// Extract returns a pointer to a Resource object and is called after a +// Get operation. +func (r GetResult) Extract() (*Resource, error) { + var s struct { + Resource *Resource `json:"resource"` + } + err := r.ExtractInto(&s) + return s.Resource, err +} + +// MetadataResult represents the result of a Metadata operation. +type MetadataResult struct { + gophercloud.Result +} + +// Extract returns a map object and is called after a +// Metadata operation. +func (r MetadataResult) Extract() (map[string]string, error) { + var s struct { + Meta map[string]string `json:"metadata"` + } + err := r.ExtractInto(&s) + return s.Meta, err +} + +// ResourceTypePage abstracts the raw results of making a ListTypes() request against the API. +// As OpenStack extensions may freely alter the response bodies of structures returned to the client, you may only safely access the +// data provided through the ExtractResourceTypes call. +type ResourceTypePage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a ResourceTypePage contains no resource types. +func (r ResourceTypePage) IsEmpty() (bool, error) { + rts, err := ExtractResourceTypes(r) + return len(rts) == 0, err +} + +// ResourceTypes represents the type that holds the result of ExtractResourceTypes. +// We define methods on this type to sort it before output +type ResourceTypes []string + +func (r ResourceTypes) Len() int { + return len(r) +} + +func (r ResourceTypes) Swap(i, j int) { + r[i], r[j] = r[j], r[i] +} + +func (r ResourceTypes) Less(i, j int) bool { + return r[i] < r[j] +} + +// ExtractResourceTypes extracts and returns resource types. +func ExtractResourceTypes(r pagination.Page) (ResourceTypes, error) { + var s struct { + ResourceTypes ResourceTypes `json:"resource_types"` + } + err := (r.(ResourceTypePage)).ExtractInto(&s) + return s.ResourceTypes, err +} + +// TypeSchema represents a stack resource schema. +type TypeSchema struct { + Attributes map[string]interface{} `json:"attributes"` + Properties map[string]interface{} `json:"properties"` + ResourceType string `json:"resource_type"` + SupportStatus map[string]interface{} `json:"support_status"` +} + +// SchemaResult represents the result of a Schema operation. +type SchemaResult struct { + gophercloud.Result +} + +// Extract returns a pointer to a TypeSchema object and is called after a +// Schema operation. +func (r SchemaResult) Extract() (*TypeSchema, error) { + var s *TypeSchema + err := r.ExtractInto(&s) + return s, err +} + +// TemplateResult represents the result of a Template operation. +type TemplateResult struct { + gophercloud.Result +} + +// Extract returns the template and is called after a +// Template operation. +func (r TemplateResult) Extract() ([]byte, error) { + if r.Err != nil { + return nil, r.Err + } + template, err := json.MarshalIndent(r.Body, "", " ") + return template, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackresources/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackresources/testing/doc.go new file mode 100644 index 0000000000..16e1dae291 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackresources/testing/doc.go @@ -0,0 +1,2 @@ +// orchestration_stackresources_v1 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackresources/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackresources/testing/fixtures.go new file mode 100644 index 0000000000..e8903374ec --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackresources/testing/fixtures.go @@ -0,0 +1,440 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackresources" + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +// FindExpected represents the expected object from a Find request. +var FindExpected = []stackresources.Resource{ + { + Name: "hello_world", + Links: []gophercloud.Link{ + { + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + Rel: "self", + }, + { + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + Rel: "stack", + }, + }, + LogicalID: "hello_world", + StatusReason: "state changed", + UpdatedTime: time.Date(2015, 2, 5, 21, 33, 11, 0, time.UTC), + CreationTime: time.Date(2015, 2, 5, 21, 33, 10, 0, time.UTC), + RequiredBy: []interface{}{}, + Status: "CREATE_IN_PROGRESS", + PhysicalID: "49181cd6-169a-4130-9455-31185bbfc5bf", + Type: "OS::Nova::Server", + Attributes: map[string]interface{}{"SXSW": "atx"}, + Description: "Some resource", + }, +} + +// FindOutput represents the response body from a Find request. +const FindOutput = ` +{ + "resources": [ + { + "description": "Some resource", + "attributes": {"SXSW": "atx"}, + "resource_name": "hello_world", + "links": [ + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + "rel": "self" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + "rel": "stack" + } + ], + "logical_resource_id": "hello_world", + "resource_status_reason": "state changed", + "updated_time": "2015-02-05T21:33:11", + "creation_time": "2015-02-05T21:33:10", + "required_by": [], + "resource_status": "CREATE_IN_PROGRESS", + "physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf", + "resource_type": "OS::Nova::Server" + } + ] +}` + +// HandleFindSuccessfully creates an HTTP handler at `/stacks/hello_world/resources` +// on the test handler mux that responds with a `Find` response. +func HandleFindSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/stacks/hello_world/resources", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, output) + }) +} + +// ListExpected represents the expected object from a List request. +var ListExpected = []stackresources.Resource{ + { + Name: "hello_world", + Links: []gophercloud.Link{ + { + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + Rel: "self", + }, + { + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + Rel: "stack", + }, + }, + LogicalID: "hello_world", + StatusReason: "state changed", + UpdatedTime: time.Date(2015, 2, 5, 21, 33, 11, 0, time.UTC), + CreationTime: time.Date(2015, 2, 5, 21, 33, 10, 0, time.UTC), + RequiredBy: []interface{}{}, + Status: "CREATE_IN_PROGRESS", + PhysicalID: "49181cd6-169a-4130-9455-31185bbfc5bf", + Type: "OS::Nova::Server", + Attributes: map[string]interface{}{"SXSW": "atx"}, + Description: "Some resource", + }, +} + +// ListOutput represents the response body from a List request. +const ListOutput = `{ + "resources": [ + { + "resource_name": "hello_world", + "links": [ + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + "rel": "self" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + "rel": "stack" + } + ], + "logical_resource_id": "hello_world", + "resource_status_reason": "state changed", + "updated_time": "2015-02-05T21:33:11", + "required_by": [], + "resource_status": "CREATE_IN_PROGRESS", + "physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf", + "creation_time": "2015-02-05T21:33:10", + "resource_type": "OS::Nova::Server", + "attributes": {"SXSW": "atx"}, + "description": "Some resource" + } +] +}` + +// HandleListSuccessfully creates an HTTP handler at `/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/resources` +// on the test handler mux that responds with a `List` response. +func HandleListSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/resources", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, output) + case "49181cd6-169a-4130-9455-31185bbfc5bf": + fmt.Fprintf(w, `{"resources":[]}`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) +} + +// GetExpected represents the expected object from a Get request. +var GetExpected = &stackresources.Resource{ + Name: "wordpress_instance", + Links: []gophercloud.Link{ + { + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e/resources/wordpress_instance", + Rel: "self", + }, + { + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e", + Rel: "stack", + }, + }, + LogicalID: "wordpress_instance", + Attributes: map[string]interface{}{"SXSW": "atx"}, + StatusReason: "state changed", + UpdatedTime: time.Date(2014, 12, 10, 18, 34, 35, 0, time.UTC), + RequiredBy: []interface{}{}, + Status: "CREATE_COMPLETE", + PhysicalID: "00e3a2fe-c65d-403c-9483-4db9930dd194", + Type: "OS::Nova::Server", +} + +// GetOutput represents the response body from a Get request. +const GetOutput = ` +{ + "resource": { + "description": "Some resource", + "attributes": {"SXSW": "atx"}, + "resource_name": "wordpress_instance", + "description": "", + "links": [ + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e/resources/wordpress_instance", + "rel": "self" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e", + "rel": "stack" + } + ], + "logical_resource_id": "wordpress_instance", + "resource_status": "CREATE_COMPLETE", + "updated_time": "2014-12-10T18:34:35", + "required_by": [], + "resource_status_reason": "state changed", + "physical_resource_id": "00e3a2fe-c65d-403c-9483-4db9930dd194", + "resource_type": "OS::Nova::Server" + } +}` + +// HandleGetSuccessfully creates an HTTP handler at `/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e/resources/wordpress_instance` +// on the test handler mux that responds with a `Get` response. +func HandleGetSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e/resources/wordpress_instance", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, output) + }) +} + +// MetadataExpected represents the expected object from a Metadata request. +var MetadataExpected = map[string]string{ + "number": "7", + "animal": "auk", +} + +// MetadataOutput represents the response body from a Metadata request. +const MetadataOutput = ` +{ + "metadata": { + "number": "7", + "animal": "auk" + } +}` + +// HandleMetadataSuccessfully creates an HTTP handler at `/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e/resources/wordpress_instance/metadata` +// on the test handler mux that responds with a `Metadata` response. +func HandleMetadataSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e/resources/wordpress_instance/metadata", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, output) + }) +} + +// ListTypesExpected represents the expected object from a ListTypes request. +var ListTypesExpected = stackresources.ResourceTypes{ + "OS::Nova::Server", + "OS::Heat::RandomString", + "OS::Swift::Container", + "OS::Trove::Instance", + "OS::Nova::FloatingIPAssociation", + "OS::Cinder::VolumeAttachment", + "OS::Nova::FloatingIP", + "OS::Nova::KeyPair", +} + +// same as above, but sorted +var SortedListTypesExpected = stackresources.ResourceTypes{ + "OS::Cinder::VolumeAttachment", + "OS::Heat::RandomString", + "OS::Nova::FloatingIP", + "OS::Nova::FloatingIPAssociation", + "OS::Nova::KeyPair", + "OS::Nova::Server", + "OS::Swift::Container", + "OS::Trove::Instance", +} + +// ListTypesOutput represents the response body from a ListTypes request. +const ListTypesOutput = ` +{ + "resource_types": [ + "OS::Nova::Server", + "OS::Heat::RandomString", + "OS::Swift::Container", + "OS::Trove::Instance", + "OS::Nova::FloatingIPAssociation", + "OS::Cinder::VolumeAttachment", + "OS::Nova::FloatingIP", + "OS::Nova::KeyPair" + ] +}` + +// HandleListTypesSuccessfully creates an HTTP handler at `/resource_types` +// on the test handler mux that responds with a `ListTypes` response. +func HandleListTypesSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/resource_types", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, output) + }) +} + +// GetSchemaExpected represents the expected object from a Schema request. +var GetSchemaExpected = &stackresources.TypeSchema{ + Attributes: map[string]interface{}{ + "an_attribute": map[string]interface{}{ + "description": "An attribute description .", + }, + }, + Properties: map[string]interface{}{ + "a_property": map[string]interface{}{ + "update_allowed": false, + "required": true, + "type": "string", + "description": "A resource description.", + }, + }, + ResourceType: "OS::Heat::AResourceName", + SupportStatus: map[string]interface{}{ + "message": "A status message", + "status": "SUPPORTED", + "version": "2014.1", + }, +} + +// GetSchemaOutput represents the response body from a Schema request. +const GetSchemaOutput = ` +{ + "attributes": { + "an_attribute": { + "description": "An attribute description ." + } + }, + "properties": { + "a_property": { + "update_allowed": false, + "required": true, + "type": "string", + "description": "A resource description." + } + }, + "resource_type": "OS::Heat::AResourceName", + "support_status": { + "message": "A status message", + "status": "SUPPORTED", + "version": "2014.1" + } +}` + +// HandleGetSchemaSuccessfully creates an HTTP handler at `/resource_types/OS::Heat::AResourceName` +// on the test handler mux that responds with a `Schema` response. +func HandleGetSchemaSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/resource_types/OS::Heat::AResourceName", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, output) + }) +} + +// GetTemplateExpected represents the expected object from a Template request. +var GetTemplateExpected = "{\n \"HeatTemplateFormatVersion\": \"2012-12-12\",\n \"Outputs\": {\n \"private_key\": {\n \"Description\": \"The private key if it has been saved.\",\n \"Value\": \"{\\\"Fn::GetAtt\\\": [\\\"KeyPair\\\", \\\"private_key\\\"]}\"\n },\n \"public_key\": {\n \"Description\": \"The public key.\",\n \"Value\": \"{\\\"Fn::GetAtt\\\": [\\\"KeyPair\\\", \\\"public_key\\\"]}\"\n }\n },\n \"Parameters\": {\n \"name\": {\n \"Description\": \"The name of the key pair.\",\n \"Type\": \"String\"\n },\n \"public_key\": {\n \"Description\": \"The optional public key. This allows users to supply the public key from a pre-existing key pair. If not supplied, a new key pair will be generated.\",\n \"Type\": \"String\"\n },\n \"save_private_key\": {\n \"AllowedValues\": [\n \"True\",\n \"true\",\n \"False\",\n \"false\"\n ],\n \"Default\": false,\n \"Description\": \"True if the system should remember a generated private key; False otherwise.\",\n \"Type\": \"String\"\n }\n },\n \"Resources\": {\n \"KeyPair\": {\n \"Properties\": {\n \"name\": {\n \"Ref\": \"name\"\n },\n \"public_key\": {\n \"Ref\": \"public_key\"\n },\n \"save_private_key\": {\n \"Ref\": \"save_private_key\"\n }\n },\n \"Type\": \"OS::Nova::KeyPair\"\n }\n }\n}" + +// GetTemplateOutput represents the response body from a Template request. +const GetTemplateOutput = ` +{ + "HeatTemplateFormatVersion": "2012-12-12", + "Outputs": { + "private_key": { + "Description": "The private key if it has been saved.", + "Value": "{\"Fn::GetAtt\": [\"KeyPair\", \"private_key\"]}" + }, + "public_key": { + "Description": "The public key.", + "Value": "{\"Fn::GetAtt\": [\"KeyPair\", \"public_key\"]}" + } + }, + "Parameters": { + "name": { + "Description": "The name of the key pair.", + "Type": "String" + }, + "public_key": { + "Description": "The optional public key. This allows users to supply the public key from a pre-existing key pair. If not supplied, a new key pair will be generated.", + "Type": "String" + }, + "save_private_key": { + "AllowedValues": [ + "True", + "true", + "False", + "false" + ], + "Default": false, + "Description": "True if the system should remember a generated private key; False otherwise.", + "Type": "String" + } + }, + "Resources": { + "KeyPair": { + "Properties": { + "name": { + "Ref": "name" + }, + "public_key": { + "Ref": "public_key" + }, + "save_private_key": { + "Ref": "save_private_key" + } + }, + "Type": "OS::Nova::KeyPair" + } + } +}` + +// HandleGetTemplateSuccessfully creates an HTTP handler at `/resource_types/OS::Heat::AResourceName/template` +// on the test handler mux that responds with a `Template` response. +func HandleGetTemplateSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/resource_types/OS::Heat::AResourceName/template", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, output) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackresources/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackresources/testing/requests_test.go new file mode 100644 index 0000000000..c714047fa3 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackresources/testing/requests_test.go @@ -0,0 +1,112 @@ +package testing + +import ( + "sort" + "testing" + + "github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackresources" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestFindResources(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleFindSuccessfully(t, FindOutput) + + actual, err := stackresources.Find(fake.ServiceClient(), "hello_world").Extract() + th.AssertNoErr(t, err) + + expected := FindExpected + th.AssertDeepEquals(t, expected, actual) +} + +func TestListResources(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListSuccessfully(t, ListOutput) + + count := 0 + err := stackresources.List(fake.ServiceClient(), "hello_world", "49181cd6-169a-4130-9455-31185bbfc5bf", nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := stackresources.ExtractResources(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ListExpected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestGetResource(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetSuccessfully(t, GetOutput) + + actual, err := stackresources.Get(fake.ServiceClient(), "teststack", "0b1771bd-9336-4f2b-ae86-a80f971faf1e", "wordpress_instance").Extract() + th.AssertNoErr(t, err) + + expected := GetExpected + th.AssertDeepEquals(t, expected, actual) +} + +func TestResourceMetadata(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleMetadataSuccessfully(t, MetadataOutput) + + actual, err := stackresources.Metadata(fake.ServiceClient(), "teststack", "0b1771bd-9336-4f2b-ae86-a80f971faf1e", "wordpress_instance").Extract() + th.AssertNoErr(t, err) + + expected := MetadataExpected + th.AssertDeepEquals(t, expected, actual) +} + +func TestListResourceTypes(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListTypesSuccessfully(t, ListTypesOutput) + + count := 0 + err := stackresources.ListTypes(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := stackresources.ExtractResourceTypes(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ListTypesExpected, actual) + // test if sorting works + sort.Sort(actual) + th.CheckDeepEquals(t, SortedListTypesExpected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestGetResourceSchema(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetSchemaSuccessfully(t, GetSchemaOutput) + + actual, err := stackresources.Schema(fake.ServiceClient(), "OS::Heat::AResourceName").Extract() + th.AssertNoErr(t, err) + + expected := GetSchemaExpected + th.AssertDeepEquals(t, expected, actual) +} + +func TestGetResourceTemplate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetTemplateSuccessfully(t, GetTemplateOutput) + + actual, err := stackresources.Template(fake.ServiceClient(), "OS::Heat::AResourceName").Extract() + th.AssertNoErr(t, err) + + expected := GetTemplateExpected + th.AssertDeepEquals(t, expected, string(actual)) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackresources/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackresources/urls.go new file mode 100644 index 0000000000..bbddc69cbc --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackresources/urls.go @@ -0,0 +1,31 @@ +package stackresources + +import "github.com/gophercloud/gophercloud" + +func findURL(c *gophercloud.ServiceClient, stackName string) string { + return c.ServiceURL("stacks", stackName, "resources") +} + +func listURL(c *gophercloud.ServiceClient, stackName, stackID string) string { + return c.ServiceURL("stacks", stackName, stackID, "resources") +} + +func getURL(c *gophercloud.ServiceClient, stackName, stackID, resourceName string) string { + return c.ServiceURL("stacks", stackName, stackID, "resources", resourceName) +} + +func metadataURL(c *gophercloud.ServiceClient, stackName, stackID, resourceName string) string { + return c.ServiceURL("stacks", stackName, stackID, "resources", resourceName, "metadata") +} + +func listTypesURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("resource_types") +} + +func schemaURL(c *gophercloud.ServiceClient, typeName string) string { + return c.ServiceURL("resource_types", typeName) +} + +func templateURL(c *gophercloud.ServiceClient, typeName string) string { + return c.ServiceURL("resource_types", typeName, "template") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/doc.go new file mode 100644 index 0000000000..19231b5137 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/doc.go @@ -0,0 +1,8 @@ +// Package stacks provides operation for working with Heat stacks. A stack is a +// group of resources (servers, load balancers, databases, and so forth) +// combined to fulfill a useful purpose. Based on a template, Heat orchestration +// engine creates an instantiated set of resources (a stack) to run the +// application framework or component specified (in the template). A stack is a +// running instance of a template. The result of creating a stack is a deployment +// of the application framework or component. +package stacks diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/environment.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/environment.go new file mode 100644 index 0000000000..86989186fa --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/environment.go @@ -0,0 +1,134 @@ +package stacks + +import "strings" + +// Environment is a structure that represents stack environments +type Environment struct { + TE +} + +// EnvironmentSections is a map containing allowed sections in a stack environment file +var EnvironmentSections = map[string]bool{ + "parameters": true, + "parameter_defaults": true, + "resource_registry": true, +} + +// Validate validates the contents of the Environment +func (e *Environment) Validate() error { + if e.Parsed == nil { + if err := e.Parse(); err != nil { + return err + } + } + for key := range e.Parsed { + if _, ok := EnvironmentSections[key]; !ok { + return ErrInvalidEnvironment{Section: key} + } + } + return nil +} + +// Parse environment file to resolve the URL's of the resources. This is done by +// reading from the `Resource Registry` section, which is why the function is +// named GetRRFileContents. +func (e *Environment) getRRFileContents(ignoreIf igFunc) error { + // initialize environment if empty + if e.Files == nil { + e.Files = make(map[string]string) + } + if e.fileMaps == nil { + e.fileMaps = make(map[string]string) + } + + // get the resource registry + rr := e.Parsed["resource_registry"] + + // search the resource registry for URLs + switch rr.(type) { + // process further only if the resource registry is a map + case map[string]interface{}, map[interface{}]interface{}: + rrMap, err := toStringKeys(rr) + if err != nil { + return err + } + // the resource registry might contain a base URL for the resource. If + // such a field is present, use it. Otherwise, use the default base URL. + var baseURL string + if val, ok := rrMap["base_url"]; ok { + baseURL = val.(string) + } else { + baseURL = e.baseURL + } + + // The contents of the resource may be located in a remote file, which + // will be a template. Instantiate a temporary template to manage the + // contents. + tempTemplate := new(Template) + tempTemplate.baseURL = baseURL + tempTemplate.client = e.client + + // Fetch the contents of remote resource URL's + if err = tempTemplate.getFileContents(rr, ignoreIf, false); err != nil { + return err + } + // check the `resources` section (if it exists) for more URL's. Note that + // the previous call to GetFileContents was (deliberately) not recursive + // as we want more control over where to look for URL's + if val, ok := rrMap["resources"]; ok { + switch val.(type) { + // process further only if the contents are a map + case map[string]interface{}, map[interface{}]interface{}: + resourcesMap, err := toStringKeys(val) + if err != nil { + return err + } + for _, v := range resourcesMap { + switch v.(type) { + case map[string]interface{}, map[interface{}]interface{}: + resourceMap, err := toStringKeys(v) + if err != nil { + return err + } + var resourceBaseURL string + // if base_url for the resource type is defined, use it + if val, ok := resourceMap["base_url"]; ok { + resourceBaseURL = val.(string) + } else { + resourceBaseURL = baseURL + } + tempTemplate.baseURL = resourceBaseURL + if err := tempTemplate.getFileContents(v, ignoreIf, false); err != nil { + return err + } + } + } + } + } + // if the resource registry contained any URL's, store them. This can + // then be passed as parameter to api calls to Heat api. + e.Files = tempTemplate.Files + return nil + default: + return nil + } +} + +// function to choose keys whose values are other environment files +func ignoreIfEnvironment(key string, value interface{}) bool { + // base_url and hooks refer to components which cannot have urls + if key == "base_url" || key == "hooks" { + return true + } + // if value is not string, it cannot be a URL + valueString, ok := value.(string) + if !ok { + return true + } + // if value contains `::`, it must be a reference to another resource type + // e.g. OS::Nova::Server : Rackspace::Cloud::Server + if strings.Contains(valueString, "::") { + return true + } + return false +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/environment_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/environment_test.go new file mode 100644 index 0000000000..a7e3aaee19 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/environment_test.go @@ -0,0 +1,185 @@ +package stacks + +import ( + "fmt" + "net/http" + "net/url" + "strings" + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestEnvironmentValidation(t *testing.T) { + + environmentJSON := new(Environment) + environmentJSON.Bin = []byte(ValidJSONEnvironment) + err := environmentJSON.Validate() + th.AssertNoErr(t, err) + + environmentYAML := new(Environment) + environmentYAML.Bin = []byte(ValidYAMLEnvironment) + err = environmentYAML.Validate() + th.AssertNoErr(t, err) + + environmentInvalid := new(Environment) + environmentInvalid.Bin = []byte(InvalidEnvironment) + if err = environmentInvalid.Validate(); err == nil { + t.Error("environment validation did not catch invalid environment") + } +} + +func TestEnvironmentParsing(t *testing.T) { + environmentJSON := new(Environment) + environmentJSON.Bin = []byte(ValidJSONEnvironment) + err := environmentJSON.Parse() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, ValidJSONEnvironmentParsed, environmentJSON.Parsed) + + environmentYAML := new(Environment) + environmentYAML.Bin = []byte(ValidJSONEnvironment) + err = environmentYAML.Parse() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, ValidJSONEnvironmentParsed, environmentYAML.Parsed) + + environmentInvalid := new(Environment) + environmentInvalid.Bin = []byte("Keep Austin Weird") + err = environmentInvalid.Parse() + if err == nil { + t.Error("environment parsing did not catch invalid environment") + } +} + +func TestIgnoreIfEnvironment(t *testing.T) { + var keyValueTests = []struct { + key string + value interface{} + out bool + }{ + {"base_url", "afksdf", true}, + {"not_type", "hooks", false}, + {"get_file", "::", true}, + {"hooks", "dfsdfsd", true}, + {"type", "sdfubsduf.yaml", false}, + {"type", "sdfsdufs.environment", false}, + {"type", "sdfsdf.file", false}, + {"type", map[string]string{"key": "value"}, true}, + } + var result bool + for _, kv := range keyValueTests { + result = ignoreIfEnvironment(kv.key, kv.value) + if result != kv.out { + t.Errorf("key: %v, value: %v expected: %v, actual: %v", kv.key, kv.value, kv.out, result) + } + } +} + +func TestGetRRFileContents(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + environmentContent := ` +heat_template_version: 2013-05-23 + +description: + Heat WordPress template to support F18, using only Heat OpenStack-native + resource types, and without the requirement for heat-cfntools in the image. + WordPress is web software you can use to create a beautiful website or blog. + This template installs a single-instance WordPress deployment using a local + MySQL database to store the data. + +parameters: + + key_name: + type: string + description : Name of a KeyPair to enable SSH access to the instance + +resources: + wordpress_instance: + type: OS::Nova::Server + properties: + image: { get_param: image_id } + flavor: { get_param: instance_type } + key_name: { get_param: key_name }` + + dbContent := ` +heat_template_version: 2014-10-16 + +description: + Test template for Trove resource capabilities + +parameters: + db_pass: + type: string + hidden: true + description: Database access password + default: secrete + +resources: + +service_db: + type: OS::Trove::Instance + properties: + name: trove_test_db + datastore_type: mariadb + flavor: 1GB Instance + size: 10 + databases: + - name: test_data + users: + - name: kitchen_sink + password: { get_param: db_pass } + databases: [ test_data ]` + baseurl, err := getBasePath() + th.AssertNoErr(t, err) + + fakeEnvURL := strings.Join([]string{baseurl, "my_env.yaml"}, "/") + urlparsed, err := url.Parse(fakeEnvURL) + th.AssertNoErr(t, err) + // handler for my_env.yaml + th.Mux.HandleFunc(urlparsed.Path, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + w.Header().Set("Content-Type", "application/jason") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, environmentContent) + }) + + fakeDBURL := strings.Join([]string{baseurl, "my_db.yaml"}, "/") + urlparsed, err = url.Parse(fakeDBURL) + th.AssertNoErr(t, err) + + // handler for my_db.yaml + th.Mux.HandleFunc(urlparsed.Path, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + w.Header().Set("Content-Type", "application/jason") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, dbContent) + }) + + client := fakeClient{BaseClient: getHTTPClient()} + env := new(Environment) + env.Bin = []byte(`{"resource_registry": {"My::WP::Server": "my_env.yaml", "resources": {"my_db_server": {"OS::DBInstance": "my_db.yaml"}}}}`) + env.client = client + + err = env.Parse() + th.AssertNoErr(t, err) + err = env.getRRFileContents(ignoreIfEnvironment) + th.AssertNoErr(t, err) + expectedEnvFilesContent := "\nheat_template_version: 2013-05-23\n\ndescription:\n Heat WordPress template to support F18, using only Heat OpenStack-native\n resource types, and without the requirement for heat-cfntools in the image.\n WordPress is web software you can use to create a beautiful website or blog.\n This template installs a single-instance WordPress deployment using a local\n MySQL database to store the data.\n\nparameters:\n\n key_name:\n type: string\n description : Name of a KeyPair to enable SSH access to the instance\n\nresources:\n wordpress_instance:\n type: OS::Nova::Server\n properties:\n image: { get_param: image_id }\n flavor: { get_param: instance_type }\n key_name: { get_param: key_name }" + expectedDBFilesContent := "\nheat_template_version: 2014-10-16\n\ndescription:\n Test template for Trove resource capabilities\n\nparameters:\n db_pass:\n type: string\n hidden: true\n description: Database access password\n default: secrete\n\nresources:\n\nservice_db:\n type: OS::Trove::Instance\n properties:\n name: trove_test_db\n datastore_type: mariadb\n flavor: 1GB Instance\n size: 10\n databases:\n - name: test_data\n users:\n - name: kitchen_sink\n password: { get_param: db_pass }\n databases: [ test_data ]" + + th.AssertEquals(t, expectedEnvFilesContent, env.Files[fakeEnvURL]) + th.AssertEquals(t, expectedDBFilesContent, env.Files[fakeDBURL]) + + env.fixFileRefs() + expectedParsed := map[string]interface{}{ + "resource_registry": "2015-04-30", + "My::WP::Server": fakeEnvURL, + "resources": map[string]interface{}{ + "my_db_server": map[string]interface{}{ + "OS::DBInstance": fakeDBURL, + }, + }, + } + env.Parse() + th.AssertDeepEquals(t, expectedParsed, env.Parsed) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/errors.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/errors.go new file mode 100644 index 0000000000..cd6c18f758 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/errors.go @@ -0,0 +1,33 @@ +package stacks + +import ( + "fmt" + + "github.com/gophercloud/gophercloud" +) + +type ErrInvalidEnvironment struct { + gophercloud.BaseError + Section string +} + +func (e ErrInvalidEnvironment) Error() string { + return fmt.Sprintf("Environment has wrong section: %s", e.Section) +} + +type ErrInvalidDataFormat struct { + gophercloud.BaseError +} + +func (e ErrInvalidDataFormat) Error() string { + return fmt.Sprintf("Data in neither json nor yaml format.") +} + +type ErrInvalidTemplateFormatVersion struct { + gophercloud.BaseError + Version string +} + +func (e ErrInvalidTemplateFormatVersion) Error() string { + return fmt.Sprintf("Template format version not found.") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/fixtures.go new file mode 100644 index 0000000000..d6fd0750f3 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/fixtures.go @@ -0,0 +1,199 @@ +package stacks + +// ValidJSONTemplate is a valid OpenStack Heat template in JSON format +const ValidJSONTemplate = ` +{ + "heat_template_version": "2014-10-16", + "parameters": { + "flavor": { + "default": 4353, + "description": "Flavor for the server to be created", + "hidden": true, + "type": "string" + } + }, + "resources": { + "test_server": { + "properties": { + "flavor": "2 GB General Purpose v1", + "image": "Debian 7 (Wheezy) (PVHVM)", + "name": "test-server" + }, + "type": "OS::Nova::Server" + } + } +} +` + +// ValidYAMLTemplate is a valid OpenStack Heat template in YAML format +const ValidYAMLTemplate = ` +heat_template_version: 2014-10-16 +parameters: + flavor: + type: string + description: Flavor for the server to be created + default: 4353 + hidden: true +resources: + test_server: + type: "OS::Nova::Server" + properties: + name: test-server + flavor: 2 GB General Purpose v1 + image: Debian 7 (Wheezy) (PVHVM) +` + +// InvalidTemplateNoVersion is an invalid template as it has no `version` section +const InvalidTemplateNoVersion = ` +parameters: + flavor: + type: string + description: Flavor for the server to be created + default: 4353 + hidden: true +resources: + test_server: + type: "OS::Nova::Server" + properties: + name: test-server + flavor: 2 GB General Purpose v1 + image: Debian 7 (Wheezy) (PVHVM) +` + +// ValidJSONEnvironment is a valid environment for a stack in JSON format +const ValidJSONEnvironment = ` +{ + "parameters": { + "user_key": "userkey" + }, + "resource_registry": { + "My::WP::Server": "file:///home/shardy/git/heat-templates/hot/F18/WordPress_Native.yaml", + "OS::Quantum*": "OS::Neutron*", + "AWS::CloudWatch::Alarm": "file:///etc/heat/templates/AWS_CloudWatch_Alarm.yaml", + "OS::Metering::Alarm": "OS::Ceilometer::Alarm", + "AWS::RDS::DBInstance": "file:///etc/heat/templates/AWS_RDS_DBInstance.yaml", + "resources": { + "my_db_server": { + "OS::DBInstance": "file:///home/mine/all_my_cool_templates/db.yaml" + }, + "my_server": { + "OS::DBInstance": "file:///home/mine/all_my_cool_templates/db.yaml", + "hooks": "pre-create" + }, + "nested_stack": { + "nested_resource": { + "hooks": "pre-update" + }, + "another_resource": { + "hooks": [ + "pre-create", + "pre-update" + ] + } + } + } + } +} +` + +// ValidYAMLEnvironment is a valid environment for a stack in YAML format +const ValidYAMLEnvironment = ` +parameters: + user_key: userkey +resource_registry: + My::WP::Server: file:///home/shardy/git/heat-templates/hot/F18/WordPress_Native.yaml + # allow older templates with Quantum in them. + "OS::Quantum*": "OS::Neutron*" + # Choose your implementation of AWS::CloudWatch::Alarm + "AWS::CloudWatch::Alarm": "file:///etc/heat/templates/AWS_CloudWatch_Alarm.yaml" + #"AWS::CloudWatch::Alarm": "OS::Heat::CWLiteAlarm" + "OS::Metering::Alarm": "OS::Ceilometer::Alarm" + "AWS::RDS::DBInstance": "file:///etc/heat/templates/AWS_RDS_DBInstance.yaml" + resources: + my_db_server: + "OS::DBInstance": file:///home/mine/all_my_cool_templates/db.yaml + my_server: + "OS::DBInstance": file:///home/mine/all_my_cool_templates/db.yaml + hooks: pre-create + nested_stack: + nested_resource: + hooks: pre-update + another_resource: + hooks: [pre-create, pre-update] +` + +// InvalidEnvironment is an invalid environment as it has an extra section called `resources` +const InvalidEnvironment = ` +parameters: + flavor: + type: string + description: Flavor for the server to be created + default: 4353 + hidden: true +resources: + test_server: + type: "OS::Nova::Server" + properties: + name: test-server + flavor: 2 GB General Purpose v1 + image: Debian 7 (Wheezy) (PVHVM) +parameter_defaults: + KeyName: heat_key +` + +// ValidJSONEnvironmentParsed is the expected parsed version of ValidJSONEnvironment +var ValidJSONEnvironmentParsed = map[string]interface{}{ + "parameters": map[string]interface{}{ + "user_key": "userkey", + }, + "resource_registry": map[string]interface{}{ + "My::WP::Server": "file:///home/shardy/git/heat-templates/hot/F18/WordPress_Native.yaml", + "OS::Quantum*": "OS::Neutron*", + "AWS::CloudWatch::Alarm": "file:///etc/heat/templates/AWS_CloudWatch_Alarm.yaml", + "OS::Metering::Alarm": "OS::Ceilometer::Alarm", + "AWS::RDS::DBInstance": "file:///etc/heat/templates/AWS_RDS_DBInstance.yaml", + "resources": map[string]interface{}{ + "my_db_server": map[string]interface{}{ + "OS::DBInstance": "file:///home/mine/all_my_cool_templates/db.yaml", + }, + "my_server": map[string]interface{}{ + "OS::DBInstance": "file:///home/mine/all_my_cool_templates/db.yaml", + "hooks": "pre-create", + }, + "nested_stack": map[string]interface{}{ + "nested_resource": map[string]interface{}{ + "hooks": "pre-update", + }, + "another_resource": map[string]interface{}{ + "hooks": []interface{}{ + "pre-create", + "pre-update", + }, + }, + }, + }, + }, +} + +// ValidJSONTemplateParsed is the expected parsed version of ValidJSONTemplate +var ValidJSONTemplateParsed = map[string]interface{}{ + "heat_template_version": "2014-10-16", + "parameters": map[string]interface{}{ + "flavor": map[string]interface{}{ + "default": 4353, + "description": "Flavor for the server to be created", + "hidden": true, + "type": "string", + }, + }, + "resources": map[string]interface{}{ + "test_server": map[string]interface{}{ + "properties": map[string]interface{}{ + "flavor": "2 GB General Purpose v1", + "image": "Debian 7 (Wheezy) (PVHVM)", + "name": "test-server", + }, + "type": "OS::Nova::Server", + }, + }, +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/requests.go new file mode 100644 index 0000000000..91f38ee7e6 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/requests.go @@ -0,0 +1,440 @@ +package stacks + +import ( + "strings" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// CreateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Create operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type CreateOptsBuilder interface { + ToStackCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts struct { + // The name of the stack. It must start with an alphabetic character. + Name string `json:"stack_name" required:"true"` + // A structure that contains either the template file or url. Call the + // associated methods to extract the information relevant to send in a create request. + TemplateOpts *Template `json:"-" required:"true"` + // Enables or disables deletion of all stack resources when a stack + // creation fails. Default is true, meaning all resources are not deleted when + // stack creation fails. + DisableRollback *bool `json:"disable_rollback,omitempty"` + // A structure that contains details for the environment of the stack. + EnvironmentOpts *Environment `json:"-"` + // User-defined parameters to pass to the template. + Parameters map[string]string `json:"parameters,omitempty"` + // The timeout for stack creation in minutes. + Timeout int `json:"timeout_mins,omitempty"` + // A list of tags to assosciate with the Stack + Tags []string `json:"-"` +} + +// ToStackCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToStackCreateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + if err := opts.TemplateOpts.Parse(); err != nil { + return nil, err + } + + if err := opts.TemplateOpts.getFileContents(opts.TemplateOpts.Parsed, ignoreIfTemplate, true); err != nil { + return nil, err + } + opts.TemplateOpts.fixFileRefs() + b["template"] = string(opts.TemplateOpts.Bin) + + files := make(map[string]string) + for k, v := range opts.TemplateOpts.Files { + files[k] = v + } + + if opts.EnvironmentOpts != nil { + if err := opts.EnvironmentOpts.Parse(); err != nil { + return nil, err + } + if err := opts.EnvironmentOpts.getRRFileContents(ignoreIfEnvironment); err != nil { + return nil, err + } + opts.EnvironmentOpts.fixFileRefs() + for k, v := range opts.EnvironmentOpts.Files { + files[k] = v + } + b["environment"] = string(opts.EnvironmentOpts.Bin) + } + + if len(files) > 0 { + b["files"] = files + } + + if opts.Tags != nil { + b["tags"] = strings.Join(opts.Tags, ",") + } + + return b, nil +} + +// Create accepts a CreateOpts struct and creates a new stack using the values +// provided. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToStackCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(createURL(c), b, &r.Body, nil) + return +} + +// AdoptOptsBuilder is the interface options structs have to satisfy in order +// to be used in the Adopt function in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type AdoptOptsBuilder interface { + ToStackAdoptMap() (map[string]interface{}, error) +} + +// AdoptOpts is the common options struct used in this package's Adopt +// operation. +type AdoptOpts struct { + // Existing resources data represented as a string to add to the + // new stack. Data returned by Abandon could be provided as AdoptsStackData. + AdoptStackData string `json:"adopt_stack_data" required:"true"` + // The name of the stack. It must start with an alphabetic character. + Name string `json:"stack_name" required:"true"` + // A structure that contains either the template file or url. Call the + // associated methods to extract the information relevant to send in a create request. + TemplateOpts *Template `json:"-" required:"true"` + // The timeout for stack creation in minutes. + Timeout int `json:"timeout_mins,omitempty"` + // A structure that contains either the template file or url. Call the + // associated methods to extract the information relevant to send in a create request. + //TemplateOpts *Template `json:"-" required:"true"` + // Enables or disables deletion of all stack resources when a stack + // creation fails. Default is true, meaning all resources are not deleted when + // stack creation fails. + DisableRollback *bool `json:"disable_rollback,omitempty"` + // A structure that contains details for the environment of the stack. + EnvironmentOpts *Environment `json:"-"` + // User-defined parameters to pass to the template. + Parameters map[string]string `json:"parameters,omitempty"` +} + +// ToStackAdoptMap casts a CreateOpts struct to a map. +func (opts AdoptOpts) ToStackAdoptMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + if err := opts.TemplateOpts.Parse(); err != nil { + return nil, err + } + + if err := opts.TemplateOpts.getFileContents(opts.TemplateOpts.Parsed, ignoreIfTemplate, true); err != nil { + return nil, err + } + opts.TemplateOpts.fixFileRefs() + b["template"] = string(opts.TemplateOpts.Bin) + + files := make(map[string]string) + for k, v := range opts.TemplateOpts.Files { + files[k] = v + } + + if opts.EnvironmentOpts != nil { + if err := opts.EnvironmentOpts.Parse(); err != nil { + return nil, err + } + if err := opts.EnvironmentOpts.getRRFileContents(ignoreIfEnvironment); err != nil { + return nil, err + } + opts.EnvironmentOpts.fixFileRefs() + for k, v := range opts.EnvironmentOpts.Files { + files[k] = v + } + b["environment"] = string(opts.EnvironmentOpts.Bin) + } + + if len(files) > 0 { + b["files"] = files + } + + return b, nil +} + +// Adopt accepts an AdoptOpts struct and creates a new stack using the resources +// from another stack. +func Adopt(c *gophercloud.ServiceClient, opts AdoptOptsBuilder) (r AdoptResult) { + b, err := opts.ToStackAdoptMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(adoptURL(c), b, &r.Body, nil) + return +} + +// SortDir is a type for specifying in which direction to sort a list of stacks. +type SortDir string + +// SortKey is a type for specifying by which key to sort a list of stacks. +type SortKey string + +var ( + // SortAsc is used to sort a list of stacks in ascending order. + SortAsc SortDir = "asc" + // SortDesc is used to sort a list of stacks in descending order. + SortDesc SortDir = "desc" + // SortName is used to sort a list of stacks by name. + SortName SortKey = "name" + // SortStatus is used to sort a list of stacks by status. + SortStatus SortKey = "status" + // SortCreatedAt is used to sort a list of stacks by date created. + SortCreatedAt SortKey = "created_at" + // SortUpdatedAt is used to sort a list of stacks by date updated. + SortUpdatedAt SortKey = "updated_at" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToStackListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the network attributes you want to see returned. SortKey allows you to sort +// by a particular network attribute. SortDir sets the direction, and is either +// `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Status string `q:"status"` + Name string `q:"name"` + Marker string `q:"marker"` + Limit int `q:"limit"` + SortKey SortKey `q:"sort_keys"` + SortDir SortDir `q:"sort_dir"` +} + +// ToStackListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToStackListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// List returns a Pager which allows you to iterate over a collection of +// stacks. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToStackListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + createPage := func(r pagination.PageResult) pagination.Page { + return StackPage{pagination.SinglePageBase(r)} + } + return pagination.NewPager(c, url, createPage) +} + +// Get retreives a stack based on the stack name and stack ID. +func Get(c *gophercloud.ServiceClient, stackName, stackID string) (r GetResult) { + _, r.Err = c.Get(getURL(c, stackName, stackID), &r.Body, nil) + return +} + +// UpdateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the Update operation in this package. +type UpdateOptsBuilder interface { + ToStackUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contains the common options struct used in this package's Update +// operation. +type UpdateOpts struct { + // A structure that contains either the template file or url. Call the + // associated methods to extract the information relevant to send in a create request. + TemplateOpts *Template `json:"-" required:"true"` + // A structure that contains details for the environment of the stack. + EnvironmentOpts *Environment `json:"-"` + // User-defined parameters to pass to the template. + Parameters map[string]string `json:"parameters,omitempty"` + // The timeout for stack creation in minutes. + Timeout int `json:"timeout_mins,omitempty"` + // A list of tags to assosciate with the Stack + Tags []string `json:"-"` +} + +// ToStackUpdateMap casts a CreateOpts struct to a map. +func (opts UpdateOpts) ToStackUpdateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + if err := opts.TemplateOpts.Parse(); err != nil { + return nil, err + } + + if err := opts.TemplateOpts.getFileContents(opts.TemplateOpts.Parsed, ignoreIfTemplate, true); err != nil { + return nil, err + } + opts.TemplateOpts.fixFileRefs() + b["template"] = string(opts.TemplateOpts.Bin) + + files := make(map[string]string) + for k, v := range opts.TemplateOpts.Files { + files[k] = v + } + + if opts.EnvironmentOpts != nil { + if err := opts.EnvironmentOpts.Parse(); err != nil { + return nil, err + } + if err := opts.EnvironmentOpts.getRRFileContents(ignoreIfEnvironment); err != nil { + return nil, err + } + opts.EnvironmentOpts.fixFileRefs() + for k, v := range opts.EnvironmentOpts.Files { + files[k] = v + } + b["environment"] = string(opts.EnvironmentOpts.Bin) + } + + if len(files) > 0 { + b["files"] = files + } + + if opts.Tags != nil { + b["tags"] = strings.Join(opts.Tags, ",") + } + + return b, nil +} + +// Update accepts an UpdateOpts struct and updates an existing stack using the values +// provided. +func Update(c *gophercloud.ServiceClient, stackName, stackID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToStackUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(updateURL(c, stackName, stackID), b, nil, nil) + return +} + +// Delete deletes a stack based on the stack name and stack ID. +func Delete(c *gophercloud.ServiceClient, stackName, stackID string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, stackName, stackID), nil) + return +} + +// PreviewOptsBuilder is the interface options structs have to satisfy in order +// to be used in the Preview operation in this package. +type PreviewOptsBuilder interface { + ToStackPreviewMap() (map[string]interface{}, error) +} + +// PreviewOpts contains the common options struct used in this package's Preview +// operation. +type PreviewOpts struct { + // The name of the stack. It must start with an alphabetic character. + Name string `json:"stack_name" required:"true"` + // The timeout for stack creation in minutes. + Timeout int `json:"timeout_mins" required:"true"` + // A structure that contains either the template file or url. Call the + // associated methods to extract the information relevant to send in a create request. + TemplateOpts *Template `json:"-" required:"true"` + // Enables or disables deletion of all stack resources when a stack + // creation fails. Default is true, meaning all resources are not deleted when + // stack creation fails. + DisableRollback *bool `json:"disable_rollback,omitempty"` + // A structure that contains details for the environment of the stack. + EnvironmentOpts *Environment `json:"-"` + // User-defined parameters to pass to the template. + Parameters map[string]string `json:"parameters,omitempty"` +} + +// ToStackPreviewMap casts a PreviewOpts struct to a map. +func (opts PreviewOpts) ToStackPreviewMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + if err := opts.TemplateOpts.Parse(); err != nil { + return nil, err + } + + if err := opts.TemplateOpts.getFileContents(opts.TemplateOpts.Parsed, ignoreIfTemplate, true); err != nil { + return nil, err + } + opts.TemplateOpts.fixFileRefs() + b["template"] = string(opts.TemplateOpts.Bin) + + files := make(map[string]string) + for k, v := range opts.TemplateOpts.Files { + files[k] = v + } + + if opts.EnvironmentOpts != nil { + if err := opts.EnvironmentOpts.Parse(); err != nil { + return nil, err + } + if err := opts.EnvironmentOpts.getRRFileContents(ignoreIfEnvironment); err != nil { + return nil, err + } + opts.EnvironmentOpts.fixFileRefs() + for k, v := range opts.EnvironmentOpts.Files { + files[k] = v + } + b["environment"] = string(opts.EnvironmentOpts.Bin) + } + + if len(files) > 0 { + b["files"] = files + } + + return b, nil +} + +// Preview accepts a PreviewOptsBuilder interface and creates a preview of a stack using the values +// provided. +func Preview(c *gophercloud.ServiceClient, opts PreviewOptsBuilder) (r PreviewResult) { + b, err := opts.ToStackPreviewMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(previewURL(c), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Abandon deletes the stack with the provided stackName and stackID, but leaves its +// resources intact, and returns data describing the stack and its resources. +func Abandon(c *gophercloud.ServiceClient, stackName, stackID string) (r AbandonResult) { + _, r.Err = c.Delete(abandonURL(c, stackName, stackID), &gophercloud.RequestOpts{ + JSONResponse: &r.Body, + OkCodes: []int{200}, + }) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/results.go new file mode 100644 index 0000000000..8df541940e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/results.go @@ -0,0 +1,238 @@ +package stacks + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// CreatedStack represents the object extracted from a Create operation. +type CreatedStack struct { + ID string `json:"id"` + Links []gophercloud.Link `json:"links"` +} + +// CreateResult represents the result of a Create operation. +type CreateResult struct { + gophercloud.Result +} + +// Extract returns a pointer to a CreatedStack object and is called after a +// Create operation. +func (r CreateResult) Extract() (*CreatedStack, error) { + var s struct { + CreatedStack *CreatedStack `json:"stack"` + } + err := r.ExtractInto(&s) + return s.CreatedStack, err +} + +// AdoptResult represents the result of an Adopt operation. AdoptResult has the +// same form as CreateResult. +type AdoptResult struct { + CreateResult +} + +// StackPage is a pagination.Pager that is returned from a call to the List function. +type StackPage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a ListResult contains no Stacks. +func (r StackPage) IsEmpty() (bool, error) { + stacks, err := ExtractStacks(r) + return len(stacks) == 0, err +} + +// ListedStack represents an element in the slice extracted from a List operation. +type ListedStack struct { + CreationTime time.Time `json:"-"` + Description string `json:"description"` + ID string `json:"id"` + Links []gophercloud.Link `json:"links"` + Name string `json:"stack_name"` + Status string `json:"stack_status"` + StatusReason string `json:"stack_status_reason"` + Tags []string `json:"tags"` + UpdatedTime time.Time `json:"-"` +} + +func (r *ListedStack) UnmarshalJSON(b []byte) error { + type tmp ListedStack + var s struct { + tmp + CreationTime gophercloud.JSONRFC3339NoZ `json:"creation_time"` + UpdatedTime gophercloud.JSONRFC3339NoZ `json:"updated_time"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = ListedStack(s.tmp) + + r.CreationTime = time.Time(s.CreationTime) + r.UpdatedTime = time.Time(s.UpdatedTime) + + return nil +} + +// ExtractStacks extracts and returns a slice of ListedStack. It is used while iterating +// over a stacks.List call. +func ExtractStacks(r pagination.Page) ([]ListedStack, error) { + var s struct { + ListedStacks []ListedStack `json:"stacks"` + } + err := (r.(StackPage)).ExtractInto(&s) + return s.ListedStacks, err +} + +// RetrievedStack represents the object extracted from a Get operation. +type RetrievedStack struct { + Capabilities []interface{} `json:"capabilities"` + CreationTime time.Time `json:"-"` + Description string `json:"description"` + DisableRollback bool `json:"disable_rollback"` + ID string `json:"id"` + Links []gophercloud.Link `json:"links"` + NotificationTopics []interface{} `json:"notification_topics"` + Outputs []map[string]interface{} `json:"outputs"` + Parameters map[string]string `json:"parameters"` + Name string `json:"stack_name"` + Status string `json:"stack_status"` + StatusReason string `json:"stack_status_reason"` + Tags []string `json:"tags"` + TemplateDescription string `json:"template_description"` + Timeout int `json:"timeout_mins"` + UpdatedTime time.Time `json:"-"` +} + +func (r *RetrievedStack) UnmarshalJSON(b []byte) error { + type tmp RetrievedStack + var s struct { + tmp + CreationTime gophercloud.JSONRFC3339NoZ `json:"creation_time"` + UpdatedTime gophercloud.JSONRFC3339NoZ `json:"updated_time"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = RetrievedStack(s.tmp) + + r.CreationTime = time.Time(s.CreationTime) + r.UpdatedTime = time.Time(s.UpdatedTime) + + return nil +} + +// GetResult represents the result of a Get operation. +type GetResult struct { + gophercloud.Result +} + +// Extract returns a pointer to a RetrievedStack object and is called after a +// Get operation. +func (r GetResult) Extract() (*RetrievedStack, error) { + var s struct { + Stack *RetrievedStack `json:"stack"` + } + err := r.ExtractInto(&s) + return s.Stack, err +} + +// UpdateResult represents the result of a Update operation. +type UpdateResult struct { + gophercloud.ErrResult +} + +// DeleteResult represents the result of a Delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// PreviewedStack represents the result of a Preview operation. +type PreviewedStack struct { + Capabilities []interface{} `json:"capabilities"` + CreationTime time.Time `json:"-"` + Description string `json:"description"` + DisableRollback bool `json:"disable_rollback"` + ID string `json:"id"` + Links []gophercloud.Link `json:"links"` + Name string `json:"stack_name"` + NotificationTopics []interface{} `json:"notification_topics"` + Parameters map[string]string `json:"parameters"` + Resources []interface{} `json:"resources"` + TemplateDescription string `json:"template_description"` + Timeout int `json:"timeout_mins"` + UpdatedTime time.Time `json:"-"` +} + +func (r *PreviewedStack) UnmarshalJSON(b []byte) error { + type tmp PreviewedStack + var s struct { + tmp + CreationTime gophercloud.JSONRFC3339NoZ `json:"creation_time"` + UpdatedTime gophercloud.JSONRFC3339NoZ `json:"updated_time"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = PreviewedStack(s.tmp) + + r.CreationTime = time.Time(s.CreationTime) + r.UpdatedTime = time.Time(s.UpdatedTime) + + return nil +} + +// PreviewResult represents the result of a Preview operation. +type PreviewResult struct { + gophercloud.Result +} + +// Extract returns a pointer to a PreviewedStack object and is called after a +// Preview operation. +func (r PreviewResult) Extract() (*PreviewedStack, error) { + var s struct { + PreviewedStack *PreviewedStack `json:"stack"` + } + err := r.ExtractInto(&s) + return s.PreviewedStack, err +} + +// AbandonedStack represents the result of an Abandon operation. +type AbandonedStack struct { + Status string `json:"status"` + Name string `json:"name"` + Template map[string]interface{} `json:"template"` + Action string `json:"action"` + ID string `json:"id"` + Resources map[string]interface{} `json:"resources"` + Files map[string]string `json:"files"` + StackUserProjectID string `json:"stack_user_project_id"` + ProjectID string `json:"project_id"` + Environment map[string]interface{} `json:"environment"` +} + +// AbandonResult represents the result of an Abandon operation. +type AbandonResult struct { + gophercloud.Result +} + +// Extract returns a pointer to an AbandonedStack object and is called after an +// Abandon operation. +func (r AbandonResult) Extract() (*AbandonedStack, error) { + var s *AbandonedStack + err := r.ExtractInto(&s) + return s, err +} + +// String converts an AbandonResult to a string. This is useful to when passing +// the result of an Abandon operation to an AdoptOpts AdoptStackData field. +func (r AbandonResult) String() (string, error) { + out, err := json.Marshal(r) + return string(out), err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/template.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/template.go new file mode 100644 index 0000000000..4cf5aae41a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/template.go @@ -0,0 +1,141 @@ +package stacks + +import ( + "fmt" + "reflect" + "strings" + + "github.com/gophercloud/gophercloud" +) + +// Template is a structure that represents OpenStack Heat templates +type Template struct { + TE +} + +// TemplateFormatVersions is a map containing allowed variations of the template format version +// Note that this contains the permitted variations of the _keys_ not the values. +var TemplateFormatVersions = map[string]bool{ + "HeatTemplateFormatVersion": true, + "heat_template_version": true, + "AWSTemplateFormatVersion": true, +} + +// Validate validates the contents of the Template +func (t *Template) Validate() error { + if t.Parsed == nil { + if err := t.Parse(); err != nil { + return err + } + } + var invalid string + for key := range t.Parsed { + if _, ok := TemplateFormatVersions[key]; ok { + return nil + } + invalid = key + } + return ErrInvalidTemplateFormatVersion{Version: invalid} +} + +// GetFileContents recursively parses a template to search for urls. These urls +// are assumed to point to other templates (known in OpenStack Heat as child +// templates). The contents of these urls are fetched and stored in the `Files` +// parameter of the template structure. This is the only way that a user can +// use child templates that are located in their filesystem; urls located on the +// web (e.g. on github or swift) can be fetched directly by Heat engine. +func (t *Template) getFileContents(te interface{}, ignoreIf igFunc, recurse bool) error { + // initialize template if empty + if t.Files == nil { + t.Files = make(map[string]string) + } + if t.fileMaps == nil { + t.fileMaps = make(map[string]string) + } + switch te.(type) { + // if te is a map + case map[string]interface{}, map[interface{}]interface{}: + teMap, err := toStringKeys(te) + if err != nil { + return err + } + for k, v := range teMap { + value, ok := v.(string) + if !ok { + // if the value is not a string, recursively parse that value + if err := t.getFileContents(v, ignoreIf, recurse); err != nil { + return err + } + } else if !ignoreIf(k, value) { + // at this point, the k, v pair has a reference to an external template. + // The assumption of heatclient is that value v is a reference + // to a file in the users environment + + // create a new child template + childTemplate := new(Template) + + // initialize child template + + // get the base location of the child template + baseURL, err := gophercloud.NormalizePathURL(t.baseURL, value) + if err != nil { + return err + } + childTemplate.baseURL = baseURL + childTemplate.client = t.client + + // fetch the contents of the child template + if err := childTemplate.Parse(); err != nil { + return err + } + + // process child template recursively if required. This is + // required if the child template itself contains references to + // other templates + if recurse { + if err := childTemplate.getFileContents(childTemplate.Parsed, ignoreIf, recurse); err != nil { + return err + } + } + // update parent template with current child templates' content. + // At this point, the child template has been parsed recursively. + t.fileMaps[value] = childTemplate.URL + t.Files[childTemplate.URL] = string(childTemplate.Bin) + + } + } + return nil + // if te is a slice, call the function on each element of the slice. + case []interface{}: + teSlice := te.([]interface{}) + for i := range teSlice { + if err := t.getFileContents(teSlice[i], ignoreIf, recurse); err != nil { + return err + } + } + // if te is anything else, return + case string, bool, float64, nil, int: + return nil + default: + return gophercloud.ErrUnexpectedType{Actual: fmt.Sprintf("%v", reflect.TypeOf(te))} + } + return nil +} + +// function to choose keys whose values are other template files +func ignoreIfTemplate(key string, value interface{}) bool { + // key must be either `get_file` or `type` for value to be a URL + if key != "get_file" && key != "type" { + return true + } + // value must be a string + valueString, ok := value.(string) + if !ok { + return true + } + // `.template` and `.yaml` are allowed suffixes for template URLs when referred to by `type` + if key == "type" && !(strings.HasSuffix(valueString, ".template") || strings.HasSuffix(valueString, ".yaml")) { + return true + } + return false +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/template_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/template_test.go new file mode 100644 index 0000000000..cbe99ed9cf --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/template_test.go @@ -0,0 +1,148 @@ +package stacks + +import ( + "fmt" + "net/http" + "net/url" + "strings" + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestTemplateValidation(t *testing.T) { + templateJSON := new(Template) + templateJSON.Bin = []byte(ValidJSONTemplate) + err := templateJSON.Validate() + th.AssertNoErr(t, err) + + templateYAML := new(Template) + templateYAML.Bin = []byte(ValidYAMLTemplate) + err = templateYAML.Validate() + th.AssertNoErr(t, err) + + templateInvalid := new(Template) + templateInvalid.Bin = []byte(InvalidTemplateNoVersion) + if err = templateInvalid.Validate(); err == nil { + t.Error("Template validation did not catch invalid template") + } +} + +func TestTemplateParsing(t *testing.T) { + templateJSON := new(Template) + templateJSON.Bin = []byte(ValidJSONTemplate) + err := templateJSON.Parse() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, ValidJSONTemplateParsed, templateJSON.Parsed) + + templateYAML := new(Template) + templateYAML.Bin = []byte(ValidJSONTemplate) + err = templateYAML.Parse() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, ValidJSONTemplateParsed, templateYAML.Parsed) + + templateInvalid := new(Template) + templateInvalid.Bin = []byte("Keep Austin Weird") + err = templateInvalid.Parse() + if err == nil { + t.Error("Template parsing did not catch invalid template") + } +} + +func TestIgnoreIfTemplate(t *testing.T) { + var keyValueTests = []struct { + key string + value interface{} + out bool + }{ + {"not_get_file", "afksdf", true}, + {"not_type", "sdfd", true}, + {"get_file", "shdfuisd", false}, + {"type", "dfsdfsd", true}, + {"type", "sdfubsduf.yaml", false}, + {"type", "sdfsdufs.template", false}, + {"type", "sdfsdf.file", true}, + {"type", map[string]string{"key": "value"}, true}, + } + var result bool + for _, kv := range keyValueTests { + result = ignoreIfTemplate(kv.key, kv.value) + if result != kv.out { + t.Errorf("key: %v, value: %v expected: %v, actual: %v", kv.key, kv.value, result, kv.out) + } + } +} + +func TestGetFileContents(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + baseurl, err := getBasePath() + th.AssertNoErr(t, err) + fakeURL := strings.Join([]string{baseurl, "my_nova.yaml"}, "/") + urlparsed, err := url.Parse(fakeURL) + th.AssertNoErr(t, err) + myNovaContent := `heat_template_version: 2014-10-16 +parameters: + flavor: + type: string + description: Flavor for the server to be created + default: 4353 + hidden: true +resources: + test_server: + type: "OS::Nova::Server" + properties: + name: test-server + flavor: 2 GB General Purpose v1 + image: Debian 7 (Wheezy) (PVHVM) + networks: + - {uuid: 11111111-1111-1111-1111-111111111111}` + th.Mux.HandleFunc(urlparsed.Path, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + w.Header().Set("Content-Type", "application/jason") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, myNovaContent) + }) + + client := fakeClient{BaseClient: getHTTPClient()} + te := new(Template) + te.Bin = []byte(`heat_template_version: 2015-04-30 +resources: + my_server: + type: my_nova.yaml`) + te.client = client + + err = te.Parse() + th.AssertNoErr(t, err) + err = te.getFileContents(te.Parsed, ignoreIfTemplate, true) + th.AssertNoErr(t, err) + expectedFiles := map[string]string{ + "my_nova.yaml": `heat_template_version: 2014-10-16 +parameters: + flavor: + type: string + description: Flavor for the server to be created + default: 4353 + hidden: true +resources: + test_server: + type: "OS::Nova::Server" + properties: + name: test-server + flavor: 2 GB General Purpose v1 + image: Debian 7 (Wheezy) (PVHVM) + networks: + - {uuid: 11111111-1111-1111-1111-111111111111}`} + th.AssertEquals(t, expectedFiles["my_nova.yaml"], te.Files[fakeURL]) + te.fixFileRefs() + expectedParsed := map[string]interface{}{ + "heat_template_version": "2015-04-30", + "resources": map[string]interface{}{ + "my_server": map[string]interface{}{ + "type": fakeURL, + }, + }, + } + te.Parse() + th.AssertDeepEquals(t, expectedParsed, te.Parsed) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/testing/doc.go new file mode 100644 index 0000000000..5b3703ea2f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/testing/doc.go @@ -0,0 +1,2 @@ +// orchestration_stacks_v1 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/testing/fixtures.go new file mode 100644 index 0000000000..f3e3b57d1a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/testing/fixtures.go @@ -0,0 +1,407 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks" + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +// CreateExpected represents the expected object from a Create request. +var CreateExpected = &stacks.CreatedStack{ + ID: "16ef0584-4458-41eb-87c8-0dc8d5f66c87", + Links: []gophercloud.Link{ + { + Href: "http://168.28.170.117:8004/v1/98606384f58drad0bhdb7d02779549ac/stacks/stackcreated/16ef0584-4458-41eb-87c8-0dc8d5f66c87", + Rel: "self", + }, + }, +} + +// CreateOutput represents the response body from a Create request. +const CreateOutput = ` +{ + "stack": { + "id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87", + "links": [ + { + "href": "http://168.28.170.117:8004/v1/98606384f58drad0bhdb7d02779549ac/stacks/stackcreated/16ef0584-4458-41eb-87c8-0dc8d5f66c87", + "rel": "self" + } + ] + } +}` + +// HandleCreateSuccessfully creates an HTTP handler at `/stacks` on the test handler mux +// that responds with a `Create` response. +func HandleCreateSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/stacks", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, output) + }) +} + +// ListExpected represents the expected object from a List request. +var ListExpected = []stacks.ListedStack{ + { + Description: "Simple template to test heat commands", + Links: []gophercloud.Link{ + { + Href: "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87", + Rel: "self", + }, + }, + StatusReason: "Stack CREATE completed successfully", + Name: "postman_stack", + CreationTime: time.Date(2015, 2, 3, 20, 7, 39, 0, time.UTC), + Status: "CREATE_COMPLETE", + ID: "16ef0584-4458-41eb-87c8-0dc8d5f66c87", + Tags: []string{"rackspace", "atx"}, + }, + { + Description: "Simple template to test heat commands", + Links: []gophercloud.Link{ + { + Href: "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/gophercloud-test-stack-2/db6977b2-27aa-4775-9ae7-6213212d4ada", + Rel: "self", + }, + }, + StatusReason: "Stack successfully updated", + Name: "gophercloud-test-stack-2", + CreationTime: time.Date(2014, 12, 11, 17, 39, 16, 0, time.UTC), + UpdatedTime: time.Date(2014, 12, 11, 17, 40, 37, 0, time.UTC), + Status: "UPDATE_COMPLETE", + ID: "db6977b2-27aa-4775-9ae7-6213212d4ada", + Tags: []string{"sfo", "satx"}, + }, +} + +// FullListOutput represents the response body from a List request without a marker. +const FullListOutput = ` +{ + "stacks": [ + { + "description": "Simple template to test heat commands", + "links": [ + { + "href": "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87", + "rel": "self" + } + ], + "stack_status_reason": "Stack CREATE completed successfully", + "stack_name": "postman_stack", + "creation_time": "2015-02-03T20:07:39", + "updated_time": null, + "stack_status": "CREATE_COMPLETE", + "id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87", + "tags": ["rackspace", "atx"] + }, + { + "description": "Simple template to test heat commands", + "links": [ + { + "href": "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/gophercloud-test-stack-2/db6977b2-27aa-4775-9ae7-6213212d4ada", + "rel": "self" + } + ], + "stack_status_reason": "Stack successfully updated", + "stack_name": "gophercloud-test-stack-2", + "creation_time": "2014-12-11T17:39:16", + "updated_time": "2014-12-11T17:40:37", + "stack_status": "UPDATE_COMPLETE", + "id": "db6977b2-27aa-4775-9ae7-6213212d4ada", + "tags": ["sfo", "satx"] + } + ] +} +` + +// HandleListSuccessfully creates an HTTP handler at `/stacks` on the test handler mux +// that responds with a `List` response. +func HandleListSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/stacks", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, output) + case "db6977b2-27aa-4775-9ae7-6213212d4ada": + fmt.Fprintf(w, `[]`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) +} + +// GetExpected represents the expected object from a Get request. +var GetExpected = &stacks.RetrievedStack{ + DisableRollback: true, + Description: "Simple template to test heat commands", + Parameters: map[string]string{ + "flavor": "m1.tiny", + "OS::stack_name": "postman_stack", + "OS::stack_id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87", + }, + StatusReason: "Stack CREATE completed successfully", + Name: "postman_stack", + Outputs: []map[string]interface{}{}, + CreationTime: time.Date(2015, 2, 3, 20, 7, 39, 0, time.UTC), + Links: []gophercloud.Link{ + { + Href: "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87", + Rel: "self", + }, + }, + Capabilities: []interface{}{}, + NotificationTopics: []interface{}{}, + Status: "CREATE_COMPLETE", + ID: "16ef0584-4458-41eb-87c8-0dc8d5f66c87", + TemplateDescription: "Simple template to test heat commands", + Tags: []string{"rackspace", "atx"}, +} + +// GetOutput represents the response body from a Get request. +const GetOutput = ` +{ + "stack": { + "disable_rollback": true, + "description": "Simple template to test heat commands", + "parameters": { + "flavor": "m1.tiny", + "OS::stack_name": "postman_stack", + "OS::stack_id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87" + }, + "stack_status_reason": "Stack CREATE completed successfully", + "stack_name": "postman_stack", + "outputs": [], + "creation_time": "2015-02-03T20:07:39", + "links": [ + { + "href": "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87", + "rel": "self" + } + ], + "capabilities": [], + "notification_topics": [], + "timeout_mins": null, + "stack_status": "CREATE_COMPLETE", + "updated_time": null, + "id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87", + "template_description": "Simple template to test heat commands", + "tags": ["rackspace", "atx"] + } +} +` + +// HandleGetSuccessfully creates an HTTP handler at `/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87` +// on the test handler mux that responds with a `Get` response. +func HandleGetSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, output) + }) +} + +// HandleUpdateSuccessfully creates an HTTP handler at `/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87` +// on the test handler mux that responds with an `Update` response. +func HandleUpdateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/stacks/gophercloud-test-stack-2/db6977b2-27aa-4775-9ae7-6213212d4ada", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + }) +} + +// HandleDeleteSuccessfully creates an HTTP handler at `/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87` +// on the test handler mux that responds with a `Delete` response. +func HandleDeleteSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/stacks/gophercloud-test-stack-2/db6977b2-27aa-4775-9ae7-6213212d4ada", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNoContent) + }) +} + +// GetExpected represents the expected object from a Get request. +var PreviewExpected = &stacks.PreviewedStack{ + DisableRollback: true, + Description: "Simple template to test heat commands", + Parameters: map[string]string{ + "flavor": "m1.tiny", + "OS::stack_name": "postman_stack", + "OS::stack_id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87", + }, + Name: "postman_stack", + CreationTime: time.Date(2015, 2, 3, 20, 7, 39, 0, time.UTC), + Links: []gophercloud.Link{ + { + Href: "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87", + Rel: "self", + }, + }, + Capabilities: []interface{}{}, + NotificationTopics: []interface{}{}, + ID: "16ef0584-4458-41eb-87c8-0dc8d5f66c87", + TemplateDescription: "Simple template to test heat commands", +} + +// HandlePreviewSuccessfully creates an HTTP handler at `/stacks/preview` +// on the test handler mux that responds with a `Preview` response. +func HandlePreviewSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/stacks/preview", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, output) + }) +} + +// AbandonExpected represents the expected object from an Abandon request. +var AbandonExpected = &stacks.AbandonedStack{ + Status: "COMPLETE", + Name: "postman_stack", + Template: map[string]interface{}{ + "heat_template_version": "2013-05-23", + "description": "Simple template to test heat commands", + "parameters": map[string]interface{}{ + "flavor": map[string]interface{}{ + "default": "m1.tiny", + "type": "string", + }, + }, + "resources": map[string]interface{}{ + "hello_world": map[string]interface{}{ + "type": "OS::Nova::Server", + "properties": map[string]interface{}{ + "key_name": "heat_key", + "flavor": map[string]interface{}{ + "get_param": "flavor", + }, + "image": "ad091b52-742f-469e-8f3c-fd81cadf0743", + "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n", + }, + }, + }, + }, + Action: "CREATE", + ID: "16ef0584-4458-41eb-87c8-0dc8d5f66c87", + Resources: map[string]interface{}{ + "hello_world": map[string]interface{}{ + "status": "COMPLETE", + "name": "hello_world", + "resource_id": "8a310d36-46fc-436f-8be4-37a696b8ac63", + "action": "CREATE", + "type": "OS::Nova::Server", + }, + }, + Files: map[string]string{ + "file:///Users/prat8228/go/src/github.com/rackspace/rack/my_nova.yaml": "heat_template_version: 2014-10-16\nparameters:\n flavor:\n type: string\n description: Flavor for the server to be created\n default: 4353\n hidden: true\nresources:\n test_server:\n type: \"OS::Nova::Server\"\n properties:\n name: test-server\n flavor: 2 GB General Purpose v1\n image: Debian 7 (Wheezy) (PVHVM)\n", + }, + StackUserProjectID: "897686", + ProjectID: "897686", + Environment: map[string]interface{}{ + "encrypted_param_names": make([]map[string]interface{}, 0), + "parameter_defaults": make(map[string]interface{}), + "parameters": make(map[string]interface{}), + "resource_registry": map[string]interface{}{ + "file:///Users/prat8228/go/src/github.com/rackspace/rack/my_nova.yaml": "file:///Users/prat8228/go/src/github.com/rackspace/rack/my_nova.yaml", + "resources": make(map[string]interface{}), + }, + }, +} + +// AbandonOutput represents the response body from an Abandon request. +const AbandonOutput = ` +{ + "status": "COMPLETE", + "name": "postman_stack", + "template": { + "heat_template_version": "2013-05-23", + "description": "Simple template to test heat commands", + "parameters": { + "flavor": { + "default": "m1.tiny", + "type": "string" + } + }, + "resources": { + "hello_world": { + "type": "OS::Nova::Server", + "properties": { + "key_name": "heat_key", + "flavor": { + "get_param": "flavor" + }, + "image": "ad091b52-742f-469e-8f3c-fd81cadf0743", + "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n" + } + } + } + }, + "action": "CREATE", + "id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87", + "resources": { + "hello_world": { + "status": "COMPLETE", + "name": "hello_world", + "resource_id": "8a310d36-46fc-436f-8be4-37a696b8ac63", + "action": "CREATE", + "type": "OS::Nova::Server" + } + }, + "files": { + "file:///Users/prat8228/go/src/github.com/rackspace/rack/my_nova.yaml": "heat_template_version: 2014-10-16\nparameters:\n flavor:\n type: string\n description: Flavor for the server to be created\n default: 4353\n hidden: true\nresources:\n test_server:\n type: \"OS::Nova::Server\"\n properties:\n name: test-server\n flavor: 2 GB General Purpose v1\n image: Debian 7 (Wheezy) (PVHVM)\n" +}, + "environment": { + "encrypted_param_names": [], + "parameter_defaults": {}, + "parameters": {}, + "resource_registry": { + "file:///Users/prat8228/go/src/github.com/rackspace/rack/my_nova.yaml": "file:///Users/prat8228/go/src/github.com/rackspace/rack/my_nova.yaml", + "resources": {} + } + }, + "stack_user_project_id": "897686", + "project_id": "897686" +}` + +// HandleAbandonSuccessfully creates an HTTP handler at `/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87/abandon` +// on the test handler mux that responds with an `Abandon` response. +func HandleAbandonSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c8/abandon", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, output) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/testing/requests_test.go new file mode 100644 index 0000000000..bdc6229831 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/testing/requests_test.go @@ -0,0 +1,192 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestCreateStack(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateSuccessfully(t, CreateOutput) + template := new(stacks.Template) + template.Bin = []byte(` + { + "heat_template_version": "2013-05-23", + "description": "Simple template to test heat commands", + "parameters": { + "flavor": { + "default": "m1.tiny", + "type": "string" + } + } + }`) + createOpts := stacks.CreateOpts{ + Name: "stackcreated", + Timeout: 60, + TemplateOpts: template, + DisableRollback: gophercloud.Disabled, + } + actual, err := stacks.Create(fake.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + + expected := CreateExpected + th.AssertDeepEquals(t, expected, actual) +} + +func TestAdoptStack(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateSuccessfully(t, CreateOutput) + template := new(stacks.Template) + template.Bin = []byte(` +{ + "stack_name": "postman_stack", + "template": { + "heat_template_version": "2013-05-23", + "description": "Simple template to test heat commands", + "parameters": { + "flavor": { + "default": "m1.tiny", + "type": "string" + } + }, + "resources": { + "hello_world": { + "type":"OS::Nova::Server", + "properties": { + "key_name": "heat_key", + "flavor": { + "get_param": "flavor" + }, + "image": "ad091b52-742f-469e-8f3c-fd81cadf0743", + "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n" + } + } + } + } +}`) + adoptOpts := stacks.AdoptOpts{ + AdoptStackData: `{environment{parameters{}}}`, + Name: "stackcreated", + Timeout: 60, + TemplateOpts: template, + DisableRollback: gophercloud.Disabled, + } + actual, err := stacks.Adopt(fake.ServiceClient(), adoptOpts).Extract() + th.AssertNoErr(t, err) + + expected := CreateExpected + th.AssertDeepEquals(t, expected, actual) +} + +func TestListStack(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListSuccessfully(t, FullListOutput) + + count := 0 + err := stacks.List(fake.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := stacks.ExtractStacks(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ListExpected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestGetStack(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetSuccessfully(t, GetOutput) + + actual, err := stacks.Get(fake.ServiceClient(), "postman_stack", "16ef0584-4458-41eb-87c8-0dc8d5f66c87").Extract() + th.AssertNoErr(t, err) + + expected := GetExpected + th.AssertDeepEquals(t, expected, actual) +} + +func TestUpdateStack(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleUpdateSuccessfully(t) + + template := new(stacks.Template) + template.Bin = []byte(` + { + "heat_template_version": "2013-05-23", + "description": "Simple template to test heat commands", + "parameters": { + "flavor": { + "default": "m1.tiny", + "type": "string" + } + } + }`) + updateOpts := stacks.UpdateOpts{ + TemplateOpts: template, + } + err := stacks.Update(fake.ServiceClient(), "gophercloud-test-stack-2", "db6977b2-27aa-4775-9ae7-6213212d4ada", updateOpts).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestDeleteStack(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteSuccessfully(t) + + err := stacks.Delete(fake.ServiceClient(), "gophercloud-test-stack-2", "db6977b2-27aa-4775-9ae7-6213212d4ada").ExtractErr() + th.AssertNoErr(t, err) +} + +func TestPreviewStack(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandlePreviewSuccessfully(t, GetOutput) + + template := new(stacks.Template) + template.Bin = []byte(` + { + "heat_template_version": "2013-05-23", + "description": "Simple template to test heat commands", + "parameters": { + "flavor": { + "default": "m1.tiny", + "type": "string" + } + } + }`) + previewOpts := stacks.PreviewOpts{ + Name: "stackcreated", + Timeout: 60, + TemplateOpts: template, + DisableRollback: gophercloud.Disabled, + } + actual, err := stacks.Preview(fake.ServiceClient(), previewOpts).Extract() + th.AssertNoErr(t, err) + + expected := PreviewExpected + th.AssertDeepEquals(t, expected, actual) +} + +func TestAbandonStack(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleAbandonSuccessfully(t, AbandonOutput) + + actual, err := stacks.Abandon(fake.ServiceClient(), "postman_stack", "16ef0584-4458-41eb-87c8-0dc8d5f66c8").Extract() + th.AssertNoErr(t, err) + + expected := AbandonExpected + th.AssertDeepEquals(t, expected, actual) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/urls.go new file mode 100644 index 0000000000..b00be54e2a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/urls.go @@ -0,0 +1,35 @@ +package stacks + +import "github.com/gophercloud/gophercloud" + +func createURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("stacks") +} + +func adoptURL(c *gophercloud.ServiceClient) string { + return createURL(c) +} + +func listURL(c *gophercloud.ServiceClient) string { + return createURL(c) +} + +func getURL(c *gophercloud.ServiceClient, name, id string) string { + return c.ServiceURL("stacks", name, id) +} + +func updateURL(c *gophercloud.ServiceClient, name, id string) string { + return getURL(c, name, id) +} + +func deleteURL(c *gophercloud.ServiceClient, name, id string) string { + return getURL(c, name, id) +} + +func previewURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("stacks", "preview") +} + +func abandonURL(c *gophercloud.ServiceClient, name, id string) string { + return c.ServiceURL("stacks", name, id, "abandon") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/utils.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/utils.go new file mode 100644 index 0000000000..71d9e35150 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/utils.go @@ -0,0 +1,160 @@ +package stacks + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "path/filepath" + "reflect" + "strings" + + "github.com/gophercloud/gophercloud" + "gopkg.in/yaml.v2" +) + +// Client is an interface that expects a Get method similar to http.Get. This +// is needed for unit testing, since we can mock an http client. Thus, the +// client will usually be an http.Client EXCEPT in unit tests. +type Client interface { + Get(string) (*http.Response, error) +} + +// TE is a base structure for both Template and Environment +type TE struct { + // Bin stores the contents of the template or environment. + Bin []byte + // URL stores the URL of the template. This is allowed to be a 'file://' + // for local files. + URL string + // Parsed contains a parsed version of Bin. Since there are 2 different + // fields referring to the same value, you must be careful when accessing + // this filed. + Parsed map[string]interface{} + // Files contains a mapping between the urls in templates to their contents. + Files map[string]string + // fileMaps is a map used internally when determining Files. + fileMaps map[string]string + // baseURL represents the location of the template or environment file. + baseURL string + // client is an interface which allows TE to fetch contents from URLS + client Client +} + +// Fetch fetches the contents of a TE from its URL. Once a TE structure has a +// URL, call the fetch method to fetch the contents. +func (t *TE) Fetch() error { + // if the baseURL is not provided, use the current directors as the base URL + if t.baseURL == "" { + u, err := getBasePath() + if err != nil { + return err + } + t.baseURL = u + } + + // if the contents are already present, do nothing. + if t.Bin != nil { + return nil + } + + // get a fqdn from the URL using the baseURL of the TE. For local files, + // the URL's will have the `file` scheme. + u, err := gophercloud.NormalizePathURL(t.baseURL, t.URL) + if err != nil { + return err + } + t.URL = u + + // get an HTTP client if none present + if t.client == nil { + t.client = getHTTPClient() + } + + // use the client to fetch the contents of the TE + resp, err := t.client.Get(t.URL) + if err != nil { + return err + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + t.Bin = body + return nil +} + +// get the basepath of the TE +func getBasePath() (string, error) { + basePath, err := filepath.Abs(".") + if err != nil { + return "", err + } + u, err := gophercloud.NormalizePathURL("", basePath) + if err != nil { + return "", err + } + return u, nil +} + +// get a an HTTP client to retrieve URL's. This client allows the use of `file` +// scheme since we may need to fetch files from users filesystem +func getHTTPClient() Client { + transport := &http.Transport{} + transport.RegisterProtocol("file", http.NewFileTransport(http.Dir("/"))) + return &http.Client{Transport: transport} +} + +// Parse will parse the contents and then validate. The contents MUST be either JSON or YAML. +func (t *TE) Parse() error { + if err := t.Fetch(); err != nil { + return err + } + if jerr := json.Unmarshal(t.Bin, &t.Parsed); jerr != nil { + if yerr := yaml.Unmarshal(t.Bin, &t.Parsed); yerr != nil { + return ErrInvalidDataFormat{} + } + } + return t.Validate() +} + +// Validate validates the contents of TE +func (t *TE) Validate() error { + return nil +} + +// igfunc is a parameter used by GetFileContents and GetRRFileContents to check +// for valid URL's. +type igFunc func(string, interface{}) bool + +// convert map[interface{}]interface{} to map[string]interface{} +func toStringKeys(m interface{}) (map[string]interface{}, error) { + switch m.(type) { + case map[string]interface{}, map[interface{}]interface{}: + typedMap := make(map[string]interface{}) + if _, ok := m.(map[interface{}]interface{}); ok { + for k, v := range m.(map[interface{}]interface{}) { + typedMap[k.(string)] = v + } + } else { + typedMap = m.(map[string]interface{}) + } + return typedMap, nil + default: + return nil, gophercloud.ErrUnexpectedType{Expected: "map[string]interface{}/map[interface{}]interface{}", Actual: fmt.Sprintf("%v", reflect.TypeOf(m))} + } +} + +// fix the reference to files by replacing relative URL's by absolute +// URL's +func (t *TE) fixFileRefs() { + tStr := string(t.Bin) + if t.fileMaps == nil { + return + } + for k, v := range t.fileMaps { + tStr = strings.Replace(tStr, k, v, -1) + } + t.Bin = []byte(tStr) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/utils_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/utils_test.go new file mode 100644 index 0000000000..b64e4dcef5 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks/utils_test.go @@ -0,0 +1,94 @@ +package stacks + +import ( + "fmt" + "net/http" + "net/url" + "strings" + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestTEFixFileRefs(t *testing.T) { + te := TE{ + Bin: []byte(`string_to_replace: my fair lady`), + fileMaps: map[string]string{ + "string_to_replace": "london bridge is falling down", + }, + } + te.fixFileRefs() + th.AssertEquals(t, string(te.Bin), `london bridge is falling down: my fair lady`) +} + +func TestToStringKeys(t *testing.T) { + var test1 interface{} = map[interface{}]interface{}{ + "Adam": "Smith", + "Isaac": "Newton", + } + result1, err := toStringKeys(test1) + th.AssertNoErr(t, err) + + expected := map[string]interface{}{ + "Adam": "Smith", + "Isaac": "Newton", + } + th.AssertDeepEquals(t, result1, expected) +} + +func TestGetBasePath(t *testing.T) { + _, err := getBasePath() + th.AssertNoErr(t, err) +} + +// test if HTTP client can read file type URLS. Read the URL of this file +// because if this test is running, it means this file _must_ exist +func TestGetHTTPClient(t *testing.T) { + client := getHTTPClient() + baseurl, err := getBasePath() + th.AssertNoErr(t, err) + resp, err := client.Get(baseurl) + th.AssertNoErr(t, err) + th.AssertEquals(t, resp.StatusCode, 200) +} + +// Implement a fakeclient that can be used to mock out HTTP requests +type fakeClient struct { + BaseClient Client +} + +// this client's Get method first changes the URL given to point to +// testhelper's (th) endpoints. This is done because the http Mux does not seem +// to work for fqdns with the `file` scheme +func (c fakeClient) Get(url string) (*http.Response, error) { + newurl := strings.Replace(url, "file://", th.Endpoint(), 1) + return c.BaseClient.Get(newurl) +} + +// test the fetch function +func TestFetch(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + baseurl, err := getBasePath() + th.AssertNoErr(t, err) + fakeURL := strings.Join([]string{baseurl, "file.yaml"}, "/") + urlparsed, err := url.Parse(fakeURL) + th.AssertNoErr(t, err) + + th.Mux.HandleFunc(urlparsed.Path, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + w.Header().Set("Content-Type", "application/jason") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "Fee-fi-fo-fum") + }) + + client := fakeClient{BaseClient: getHTTPClient()} + te := TE{ + URL: "file.yaml", + client: client, + } + err = te.Fetch() + th.AssertNoErr(t, err) + th.AssertEquals(t, fakeURL, te.URL) + th.AssertEquals(t, "Fee-fi-fo-fum", string(te.Bin)) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacktemplates/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacktemplates/doc.go new file mode 100644 index 0000000000..5af0bd62a1 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacktemplates/doc.go @@ -0,0 +1,8 @@ +// Package stacktemplates provides operations for working with Heat templates. +// A Cloud Orchestration template is a portable file, written in a user-readable +// language, that describes how a set of resources should be assembled and what +// software should be installed in order to produce a working stack. The template +// specifies what resources should be used, what attributes can be set, and other +// parameters that are critical to the successful, repeatable automation of a +// specific application stack. +package stacktemplates diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacktemplates/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacktemplates/requests.go new file mode 100644 index 0000000000..d248c24ff6 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacktemplates/requests.go @@ -0,0 +1,39 @@ +package stacktemplates + +import "github.com/gophercloud/gophercloud" + +// Get retreives data for the given stack template. +func Get(c *gophercloud.ServiceClient, stackName, stackID string) (r GetResult) { + _, r.Err = c.Get(getURL(c, stackName, stackID), &r.Body, nil) + return +} + +// ValidateOptsBuilder describes struct types that can be accepted by the Validate call. +// The ValidateOpts struct in this package does. +type ValidateOptsBuilder interface { + ToStackTemplateValidateMap() (map[string]interface{}, error) +} + +// ValidateOpts specifies the template validation parameters. +type ValidateOpts struct { + Template string `json:"template" or:"TemplateURL"` + TemplateURL string `json:"template_url" or:"Template"` +} + +// ToStackTemplateValidateMap assembles a request body based on the contents of a ValidateOpts. +func (opts ValidateOpts) ToStackTemplateValidateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "") +} + +// Validate validates the given stack template. +func Validate(c *gophercloud.ServiceClient, opts ValidateOptsBuilder) (r ValidateResult) { + b, err := opts.ToStackTemplateValidateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(validateURL(c), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacktemplates/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacktemplates/results.go new file mode 100644 index 0000000000..bca959b9c1 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacktemplates/results.go @@ -0,0 +1,44 @@ +package stacktemplates + +import ( + "encoding/json" + + "github.com/gophercloud/gophercloud" +) + +// GetResult represents the result of a Get operation. +type GetResult struct { + gophercloud.Result +} + +// Extract returns the JSON template and is called after a Get operation. +func (r GetResult) Extract() ([]byte, error) { + if r.Err != nil { + return nil, r.Err + } + template, err := json.MarshalIndent(r.Body, "", " ") + if err != nil { + return nil, err + } + return template, nil +} + +// ValidatedTemplate represents the parsed object returned from a Validate request. +type ValidatedTemplate struct { + Description string `json:"Description"` + Parameters map[string]interface{} `json:"Parameters"` + ParameterGroups map[string]interface{} `json:"ParameterGroups"` +} + +// ValidateResult represents the result of a Validate operation. +type ValidateResult struct { + gophercloud.Result +} + +// Extract returns a pointer to a ValidatedTemplate object and is called after a +// Validate operation. +func (r ValidateResult) Extract() (*ValidatedTemplate, error) { + var s *ValidatedTemplate + err := r.ExtractInto(&s) + return s, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacktemplates/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacktemplates/testing/doc.go new file mode 100644 index 0000000000..43c6b0f72d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacktemplates/testing/doc.go @@ -0,0 +1,2 @@ +// orchestration_stacktemplates_v1 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacktemplates/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacktemplates/testing/fixtures.go new file mode 100644 index 0000000000..23ec579172 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacktemplates/testing/fixtures.go @@ -0,0 +1,96 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacktemplates" + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +// GetExpected represents the expected object from a Get request. +var GetExpected = "{\n \"description\": \"Simple template to test heat commands\",\n \"heat_template_version\": \"2013-05-23\",\n \"parameters\": {\n \"flavor\": {\n \"default\": \"m1.tiny\",\n \"type\": \"string\"\n }\n },\n \"resources\": {\n \"hello_world\": {\n \"properties\": {\n \"flavor\": {\n \"get_param\": \"flavor\"\n },\n \"image\": \"ad091b52-742f-469e-8f3c-fd81cadf0743\",\n \"key_name\": \"heat_key\"\n },\n \"type\": \"OS::Nova::Server\"\n }\n }\n}" + +// GetOutput represents the response body from a Get request. +const GetOutput = ` +{ + "heat_template_version": "2013-05-23", + "description": "Simple template to test heat commands", + "parameters": { + "flavor": { + "default": "m1.tiny", + "type": "string" + } + }, + "resources": { + "hello_world": { + "type": "OS::Nova::Server", + "properties": { + "key_name": "heat_key", + "flavor": { + "get_param": "flavor" + }, + "image": "ad091b52-742f-469e-8f3c-fd81cadf0743" + } + } + } +}` + +// HandleGetSuccessfully creates an HTTP handler at `/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87/template` +// on the test handler mux that responds with a `Get` response. +func HandleGetSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87/template", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, output) + }) +} + +// ValidateExpected represents the expected object from a Validate request. +var ValidateExpected = &stacktemplates.ValidatedTemplate{ + Description: "Simple template to test heat commands", + Parameters: map[string]interface{}{ + "flavor": map[string]interface{}{ + "Default": "m1.tiny", + "Type": "String", + "NoEcho": "false", + "Description": "", + "Label": "flavor", + }, + }, +} + +// ValidateOutput represents the response body from a Validate request. +const ValidateOutput = ` +{ + "Description": "Simple template to test heat commands", + "Parameters": { + "flavor": { + "Default": "m1.tiny", + "Type": "String", + "NoEcho": "false", + "Description": "", + "Label": "flavor" + } + } +}` + +// HandleValidateSuccessfully creates an HTTP handler at `/validate` +// on the test handler mux that responds with a `Validate` response. +func HandleValidateSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/validate", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, output) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacktemplates/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacktemplates/testing/requests_test.go new file mode 100644 index 0000000000..442bcb7a7f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacktemplates/testing/requests_test.go @@ -0,0 +1,58 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacktemplates" + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestGetTemplate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetSuccessfully(t, GetOutput) + + actual, err := stacktemplates.Get(fake.ServiceClient(), "postman_stack", "16ef0584-4458-41eb-87c8-0dc8d5f66c87").Extract() + th.AssertNoErr(t, err) + + expected := GetExpected + th.AssertDeepEquals(t, expected, string(actual)) +} + +func TestValidateTemplate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleValidateSuccessfully(t, ValidateOutput) + + opts := stacktemplates.ValidateOpts{ + Template: `{ + "heat_template_version": "2013-05-23", + "description": "Simple template to test heat commands", + "parameters": { + "flavor": { + "default": "m1.tiny", + "type": "string" + } + }, + "resources": { + "hello_world": { + "type": "OS::Nova::Server", + "properties": { + "key_name": "heat_key", + "flavor": { + "get_param": "flavor" + }, + "image": "ad091b52-742f-469e-8f3c-fd81cadf0743", + "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n" + } + } + } + }`, + } + actual, err := stacktemplates.Validate(fake.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) + + expected := ValidateExpected + th.AssertDeepEquals(t, expected, actual) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacktemplates/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacktemplates/urls.go new file mode 100644 index 0000000000..aed6b4b9de --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacktemplates/urls.go @@ -0,0 +1,11 @@ +package stacktemplates + +import "github.com/gophercloud/gophercloud" + +func getURL(c *gophercloud.ServiceClient, stackName, stackID string) string { + return c.ServiceURL("stacks", stackName, stackID, "template") +} + +func validateURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("validate") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/apiversions/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/apiversions/doc.go new file mode 100644 index 0000000000..841a9c578c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/apiversions/doc.go @@ -0,0 +1,3 @@ +// Package apiversions provides information and interaction with the different +// API versions for the Shared File System service, code-named Manila. +package apiversions diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/apiversions/errors.go b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/apiversions/errors.go new file mode 100644 index 0000000000..8f0f7628de --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/apiversions/errors.go @@ -0,0 +1,23 @@ +package apiversions + +import ( + "fmt" +) + +// ErrVersionNotFound is the error when the requested API version +// could not be found. +type ErrVersionNotFound struct{} + +func (e ErrVersionNotFound) Error() string { + return fmt.Sprintf("Unable to find requested API version") +} + +// ErrMultipleVersionsFound is the error when a request for an API +// version returns multiple results. +type ErrMultipleVersionsFound struct { + Count int +} + +func (e ErrMultipleVersionsFound) Error() string { + return fmt.Sprintf("Found %d API versions", e.Count) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/apiversions/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/apiversions/requests.go new file mode 100644 index 0000000000..2e1f6639b5 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/apiversions/requests.go @@ -0,0 +1,19 @@ +package apiversions + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// List lists all the API versions available to end-users. +func List(c *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(c, listURL(c), func(r pagination.PageResult) pagination.Page { + return APIVersionPage{pagination.SinglePageBase(r)} + }) +} + +// Get will get a specific API version, specified by major ID. +func Get(client *gophercloud.ServiceClient, v string) (r GetResult) { + _, r.Err = client.Get(getURL(client, v), &r.Body, nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/apiversions/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/apiversions/results.go new file mode 100644 index 0000000000..60c1f1b3ab --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/apiversions/results.go @@ -0,0 +1,73 @@ +package apiversions + +import ( + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// APIVersion represents an API version for the Shared File System service. +type APIVersion struct { + // ID is the unique identifier of the API version. + ID string `json:"id"` + + // MinVersion is the minimum microversion supported. + MinVersion string `json:"min_version"` + + // Status is the API versions status. + Status string `json:"status"` + + // Updated is the date when the API was last updated. + Updated time.Time `json:"updated"` + + // Version is the maximum microversion supported. + Version string `json:"version"` +} + +// APIVersionPage is the page returned by a pager when traversing over a +// collection of API versions. +type APIVersionPage struct { + pagination.SinglePageBase +} + +// IsEmpty checks whether an APIVersionPage struct is empty. +func (r APIVersionPage) IsEmpty() (bool, error) { + is, err := ExtractAPIVersions(r) + return len(is) == 0, err +} + +// ExtractAPIVersions takes a collection page, extracts all of the elements, +// and returns them a slice of APIVersion structs. It is effectively a cast. +func ExtractAPIVersions(r pagination.Page) ([]APIVersion, error) { + var s struct { + Versions []APIVersion `json:"versions"` + } + err := (r.(APIVersionPage)).ExtractInto(&s) + return s.Versions, err +} + +// GetResult represents the result of a get operation. +type GetResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts an API version resource. +func (r GetResult) Extract() (*APIVersion, error) { + var s struct { + Versions []APIVersion `json:"versions"` + } + err := r.ExtractInto(&s) + if err != nil { + return nil, err + } + + switch len(s.Versions) { + case 0: + return nil, ErrVersionNotFound{} + case 1: + return &s.Versions[0], nil + default: + return nil, ErrMultipleVersionsFound{Count: len(s.Versions)} + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/apiversions/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/apiversions/testing/doc.go new file mode 100644 index 0000000000..12e4bda0f9 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/apiversions/testing/doc.go @@ -0,0 +1,2 @@ +// apiversions_v1 +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/apiversions/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/apiversions/testing/fixtures.go new file mode 100644 index 0000000000..9707d62af2 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/apiversions/testing/fixtures.go @@ -0,0 +1,227 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/gophercloud/gophercloud/openstack/sharedfilesystems/apiversions" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +const ManilaAPIVersionResponse = ` +{ + "versions": [ + { + "id": "v2.0", + "links": [ + { + "href": "http://docs.openstack.org/", + "rel": "describedby", + "type": "text/html" + }, + { + "href": "http://localhost:8786/v2/", + "rel": "self" + } + ], + "media-types": [ + { + "base": "application/json", + "type": "application/vnd.openstack.share+json;version=1" + } + ], + "min_version": "2.0", + "status": "CURRENT", + "updated": "2015-08-27T11:33:21Z", + "version": "2.32" + } + ] +} +` + +const ManilaAPIInvalidVersionResponse_1 = ` +{ + "versions": [ + ] +} +` + +const ManilaAPIInvalidVersionResponse_2 = ` +{ + "versions": [ + { + "id": "v2.0", + "links": [ + { + "href": "http://docs.openstack.org/", + "rel": "describedby", + "type": "text/html" + }, + { + "href": "http://localhost:8786/v2/", + "rel": "self" + } + ], + "media-types": [ + { + "base": "application/json", + "type": "application/vnd.openstack.share+json;version=1" + } + ], + "min_version": "2.0", + "status": "CURRENT", + "updated": "2015-08-27T11:33:21Z", + "version": "2.32" + }, + { + "id": "v2.9", + "links": [ + { + "href": "http://docs.openstack.org/", + "rel": "describedby", + "type": "text/html" + }, + { + "href": "http://localhost:8786/v2/", + "rel": "self" + } + ], + "media-types": [ + { + "base": "application/json", + "type": "application/vnd.openstack.share+json;version=1" + } + ], + "min_version": "2.9", + "status": "CURRENT", + "updated": "2015-08-27T11:33:21Z", + "version": "2.99" + } + ] +} +` + +const ManilaAllAPIVersionsResponse = ` +{ + "versions": [ + { + "id": "v1.0", + "links": [ + { + "href": "http://docs.openstack.org/", + "rel": "describedby", + "type": "text/html" + }, + { + "href": "http://localhost:8786/v1/", + "rel": "self" + } + ], + "media-types": [ + { + "base": "application/json", + "type": "application/vnd.openstack.share+json;version=1" + } + ], + "min_version": "", + "status": "DEPRECATED", + "updated": "2015-08-27T11:33:21Z", + "version": "" + }, + { + "id": "v2.0", + "links": [ + { + "href": "http://docs.openstack.org/", + "rel": "describedby", + "type": "text/html" + }, + { + "href": "http://localhost:8786/v2/", + "rel": "self" + } + ], + "media-types": [ + { + "base": "application/json", + "type": "application/vnd.openstack.share+json;version=1" + } + ], + "min_version": "2.0", + "status": "CURRENT", + "updated": "2015-08-27T11:33:21Z", + "version": "2.32" + } + ] +} +` + +var ManilaAPIVersion1Result = apiversions.APIVersion{ + ID: "v1.0", + Status: "DEPRECATED", + Updated: time.Date(2015, 8, 27, 11, 33, 21, 0, time.UTC), +} + +var ManilaAPIVersion2Result = apiversions.APIVersion{ + ID: "v2.0", + Status: "CURRENT", + Updated: time.Date(2015, 8, 27, 11, 33, 21, 0, time.UTC), + MinVersion: "2.0", + Version: "2.32", +} + +var ManilaAllAPIVersionResults = []apiversions.APIVersion{ + ManilaAPIVersion1Result, + ManilaAPIVersion2Result, +} + +func MockListResponse(t *testing.T) { + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ManilaAllAPIVersionsResponse) + }) +} + +func MockGetResponse(t *testing.T) { + th.Mux.HandleFunc("/v2/", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ManilaAPIVersionResponse) + }) +} + +func MockGetNoResponse(t *testing.T) { + th.Mux.HandleFunc("/v2/", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ManilaAPIInvalidVersionResponse_1) + }) +} + +func MockGetMultipleResponses(t *testing.T) { + th.Mux.HandleFunc("/v2/", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ManilaAPIInvalidVersionResponse_2) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/apiversions/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/apiversions/testing/requests_test.go new file mode 100644 index 0000000000..8b5501fd8e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/apiversions/testing/requests_test.go @@ -0,0 +1,56 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/sharedfilesystems/apiversions" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestListAPIVersions(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockListResponse(t) + + allVersions, err := apiversions.List(client.ServiceClient()).AllPages() + th.AssertNoErr(t, err) + + actual, err := apiversions.ExtractAPIVersions(allVersions) + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, ManilaAllAPIVersionResults, actual) +} + +func TestGetAPIVersion(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockGetResponse(t) + + actual, err := apiversions.Get(client.ServiceClient(), "v2").Extract() + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, ManilaAPIVersion2Result, *actual) +} + +func TestGetNoAPIVersion(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockGetNoResponse(t) + + _, err := apiversions.Get(client.ServiceClient(), "v2").Extract() + th.AssertEquals(t, err.Error(), "Unable to find requested API version") +} + +func TestGetMultipleAPIVersion(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockGetMultipleResponses(t) + + _, err := apiversions.Get(client.ServiceClient(), "v2").Extract() + th.AssertEquals(t, err.Error(), "Found 2 API versions") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/apiversions/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/apiversions/urls.go new file mode 100644 index 0000000000..6a30ca91e2 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/apiversions/urls.go @@ -0,0 +1,20 @@ +package apiversions + +import ( + "net/url" + "strings" + + "github.com/gophercloud/gophercloud" +) + +func getURL(c *gophercloud.ServiceClient, version string) string { + u, _ := url.Parse(c.ServiceURL("")) + u.Path = "/" + strings.TrimRight(version, "/") + "/" + return u.String() +} + +func listURL(c *gophercloud.ServiceClient) string { + u, _ := url.Parse(c.ServiceURL("")) + u.Path = "/" + return u.String() +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/availabilityzones/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/availabilityzones/requests.go new file mode 100644 index 0000000000..df10b856eb --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/availabilityzones/requests.go @@ -0,0 +1,13 @@ +package availabilityzones + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// List will return the existing availability zones. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page { + return AvailabilityZonePage{pagination.SinglePageBase(r)} + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/availabilityzones/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/availabilityzones/results.go new file mode 100644 index 0000000000..83a76c1a83 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/availabilityzones/results.go @@ -0,0 +1,59 @@ +package availabilityzones + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// AvailabilityZone contains all the information associated with an OpenStack +// AvailabilityZone. +type AvailabilityZone struct { + // The availability zone ID. + ID string `json:"id"` + // The name of the availability zone. + Name string `json:"name"` + // The date and time stamp when the availability zone was created. + CreatedAt time.Time `json:"-"` + // The date and time stamp when the availability zone was updated. + UpdatedAt time.Time `json:"-"` +} + +type commonResult struct { + gophercloud.Result +} + +// ListResult contains the response body and error from a List request. +type AvailabilityZonePage struct { + pagination.SinglePageBase +} + +// ExtractAvailabilityZones will get the AvailabilityZone objects out of the shareTypeAccessResult object. +func ExtractAvailabilityZones(r pagination.Page) ([]AvailabilityZone, error) { + var a struct { + AvailabilityZone []AvailabilityZone `json:"availability_zones"` + } + err := (r.(AvailabilityZonePage)).ExtractInto(&a) + return a.AvailabilityZone, err +} + +func (r *AvailabilityZone) UnmarshalJSON(b []byte) error { + type tmp AvailabilityZone + var s struct { + tmp + CreatedAt gophercloud.JSONRFC3339MilliNoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339MilliNoZ `json:"updated_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = AvailabilityZone(s.tmp) + + r.CreatedAt = time.Time(s.CreatedAt) + r.UpdatedAt = time.Time(s.UpdatedAt) + + return nil +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/availabilityzones/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/availabilityzones/testing/fixtures.go new file mode 100644 index 0000000000..e5db8cda66 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/availabilityzones/testing/fixtures.go @@ -0,0 +1,32 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +func MockListResponse(t *testing.T) { + th.Mux.HandleFunc("/os-availability-zone", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` + { + "availability_zones": [ + { + "name": "nova", + "created_at": "2015-09-18T09:50:55.000000", + "updated_at": null, + "id": "388c983d-258e-4a0e-b1ba-10da37d766db" + } + ] + }`) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/availabilityzones/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/availabilityzones/testing/requests_test.go new file mode 100644 index 0000000000..76c8574fc5 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/availabilityzones/testing/requests_test.go @@ -0,0 +1,34 @@ +package testing + +import ( + "testing" + "time" + + "github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/availabilityzones" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +// Verifies that availability zones can be listed correctly +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockListResponse(t) + + allPages, err := availabilityzones.List(client.ServiceClient()).AllPages() + th.AssertNoErr(t, err) + actual, err := availabilityzones.ExtractAvailabilityZones(allPages) + th.AssertNoErr(t, err) + var nilTime time.Time + expected := []availabilityzones.AvailabilityZone{ + { + Name: "nova", + CreatedAt: time.Date(2015, 9, 18, 9, 50, 55, 0, time.UTC), + UpdatedAt: nilTime, + ID: "388c983d-258e-4a0e-b1ba-10da37d766db", + }, + } + + th.CheckDeepEquals(t, expected, actual) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/availabilityzones/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/availabilityzones/urls.go new file mode 100644 index 0000000000..fb4cdcf4e2 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/availabilityzones/urls.go @@ -0,0 +1,7 @@ +package availabilityzones + +import "github.com/gophercloud/gophercloud" + +func listURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("os-availability-zone") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/securityservices/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/securityservices/requests.go new file mode 100644 index 0000000000..8ef1ba1166 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/securityservices/requests.go @@ -0,0 +1,176 @@ +package securityservices + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +type SecurityServiceType string + +// Valid security service types +const ( + LDAP SecurityServiceType = "ldap" + Kerberos SecurityServiceType = "kerberos" + ActiveDirectory SecurityServiceType = "active_directory" +) + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToSecurityServiceCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains options for creating a SecurityService. This object is +// passed to the securityservices.Create function. For more information about +// these parameters, see the SecurityService object. +type CreateOpts struct { + // The security service type. A valid value is ldap, kerberos, or active_directory + Type SecurityServiceType `json:"type" required:"true"` + // The security service name + Name string `json:"name,omitempty"` + // The security service description + Description string `json:"description,omitempty"` + // The DNS IP address that is used inside the tenant network + DNSIP string `json:"dns_ip,omitempty"` + // The security service user or group name that is used by the tenant + User string `json:"user,omitempty"` + // The user password, if you specify a user + Password string `json:"password,omitempty"` + // The security service domain + Domain string `json:"domain,omitempty"` + // The security service host name or IP address + Server string `json:"server,omitempty"` +} + +// ToSecurityServicesCreateMap assembles a request body based on the contents of a +// CreateOpts. +func (opts CreateOpts) ToSecurityServiceCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "security_service") +} + +// Create will create a new SecurityService based on the values in CreateOpts. To +// extract the SecurityService object from the response, call the Extract method +// on the CreateResult. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToSecurityServiceCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete will delete the existing SecurityService with the provided ID. +func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} + +// ListOptsBuilder allows extensions to add additional parameters to the List +// request. +type ListOptsBuilder interface { + ToSecurityServiceListQuery() (string, error) +} + +// ListOpts holds options for listing SecurityServices. It is passed to the +// securityservices.List function. +type ListOpts struct { + // admin-only option. Set it to true to see all tenant security services. + AllTenants bool `q:"all_tenants"` + // The security service ID + ID string `q:"id"` + // The security service domain + Domain string `q:"domain"` + // The security service type. A valid value is ldap, kerberos, or active_directory + Type SecurityServiceType `q:"type"` + // The security service name + Name string `q:"name"` + // The DNS IP address that is used inside the tenant network + DNSIP string `q:"dns_ip"` + // The security service user or group name that is used by the tenant + User string `q:"user"` + // The security service host name or IP address + Server string `q:"server"` + // The ID of the share network using security services + ShareNetworkID string `q:"share_network_id"` +} + +// ToSecurityServiceListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToSecurityServiceListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns SecurityServices optionally limited by the conditions provided in ListOpts. +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToSecurityServiceListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return SecurityServicePage{pagination.SinglePageBase(r)} + }) +} + +// Get retrieves the SecurityService with the provided ID. To extract the SecurityService +// object from the response, call the Extract method on the GetResult. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToSecurityServiceUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contain options for updating an existing SecurityService. This object is passed +// to the securityservices.Update function. For more information about the parameters, see +// the SecurityService object. +type UpdateOpts struct { + // The security service name + Name string `json:"name"` + // The security service description + Description string `json:"description,omitempty"` + // The security service type. A valid value is ldap, kerberos, or active_directory + Type string `json:"type,omitempty"` + // The DNS IP address that is used inside the tenant network + DNSIP string `json:"dns_ip,omitempty"` + // The security service user or group name that is used by the tenant + User string `json:"user,omitempty"` + // The user password, if you specify a user + Password string `json:"password,omitempty"` + // The security service domain + Domain string `json:"domain,omitempty"` + // The security service host name or IP address + Server string `json:"server,omitempty"` +} + +// ToSecurityServiceUpdateMap assembles a request body based on the contents of an +// UpdateOpts. +func (opts UpdateOpts) ToSecurityServiceUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "security_service") +} + +// Update will update the SecurityService with provided information. To extract the updated +// SecurityService from the response, call the Extract method on the UpdateResult. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToSecurityServiceUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/securityservices/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/securityservices/results.go new file mode 100644 index 0000000000..ce18b8f76f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/securityservices/results.go @@ -0,0 +1,113 @@ +package securityservices + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// SecurityService contains all the information associated with an OpenStack +// SecurityService. +type SecurityService struct { + // The security service ID + ID string `json:"id"` + // The UUID of the project where the security service was created + ProjectID string `json:"project_id"` + // The security service domain + Domain string `json:"domain"` + // The security service status + Status string `json:"status"` + // The security service type. A valid value is ldap, kerberos, or active_directory + Type string `json:"type"` + // The security service name + Name string `json:"name"` + // The security service description + Description string `json:"description"` + // The DNS IP address that is used inside the tenant network + DNSIP string `json:"dns_ip"` + // The security service user or group name that is used by the tenant + User string `json:"user"` + // The user password, if you specify a user + Password string `json:"password"` + // The security service host name or IP address + Server string `json:"server"` + // The date and time stamp when the security service was created + CreatedAt time.Time `json:"-"` + // The date and time stamp when the security service was updated + UpdatedAt time.Time `json:"-"` +} + +func (r *SecurityService) UnmarshalJSON(b []byte) error { + type tmp SecurityService + var s struct { + tmp + CreatedAt gophercloud.JSONRFC3339MilliNoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339MilliNoZ `json:"updated_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = SecurityService(s.tmp) + + r.CreatedAt = time.Time(s.CreatedAt) + r.UpdatedAt = time.Time(s.UpdatedAt) + + return nil +} + +type commonResult struct { + gophercloud.Result +} + +// SecurityServicePage is a pagination.pager that is returned from a call to the List function. +type SecurityServicePage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a ListResult contains no SecurityServices. +func (r SecurityServicePage) IsEmpty() (bool, error) { + securityServices, err := ExtractSecurityServices(r) + return len(securityServices) == 0, err +} + +// ExtractSecurityServices extracts and returns SecurityServices. It is used while +// iterating over a securityservices.List call. +func ExtractSecurityServices(r pagination.Page) ([]SecurityService, error) { + var s struct { + SecurityServices []SecurityService `json:"security_services"` + } + err := (r.(SecurityServicePage)).ExtractInto(&s) + return s.SecurityServices, err +} + +// Extract will get the SecurityService object out of the commonResult object. +func (r commonResult) Extract() (*SecurityService, error) { + var s struct { + SecurityService *SecurityService `json:"security_service"` + } + err := r.ExtractInto(&s) + return s.SecurityService, err +} + +// CreateResult contains the response body and error from a Create request. +type CreateResult struct { + commonResult +} + +// DeleteResult contains the response body and error from a Delete request. +type DeleteResult struct { + gophercloud.ErrResult +} + +// GetResult contains the response body and error from a Get request. +type GetResult struct { + commonResult +} + +// UpdateResult contains the response body and error from an Update request. +type UpdateResult struct { + commonResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/securityservices/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/securityservices/testing/fixtures.go new file mode 100644 index 0000000000..528c854537 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/securityservices/testing/fixtures.go @@ -0,0 +1,192 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +func MockCreateResponse(t *testing.T) { + th.Mux.HandleFunc("/security-services", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` + { + "security_service": { + "description": "Creating my first Security Service", + "dns_ip": "10.0.0.0/24", + "user": "demo", + "password": "***", + "type": "kerberos", + "name": "SecServ1" + } + }`) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` + { + "security_service": { + "status": "new", + "domain": null, + "project_id": "16e1ab15c35a457e9c2b2aa189f544e1", + "name": "SecServ1", + "created_at": "2015-09-07T12:19:10.695211", + "updated_at": null, + "server": null, + "dns_ip": "10.0.0.0/24", + "user": "demo", + "password": "supersecret", + "type": "kerberos", + "id": "3c829734-0679-4c17-9637-801da48c0d5f", + "description": "Creating my first Security Service" + } + }`) + }) +} + +func MockDeleteResponse(t *testing.T) { + th.Mux.HandleFunc("/security-services/securityServiceID", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusAccepted) + }) +} + +func MockListResponse(t *testing.T) { + th.Mux.HandleFunc("/security-services/detail", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` + { + "security_services": [ + { + "status": "new", + "domain": null, + "project_id": "16e1ab15c35a457e9c2b2aa189f544e1", + "name": "SecServ1", + "created_at": "2015-09-07T12:19:10.000000", + "description": "Creating my first Security Service", + "updated_at": null, + "server": null, + "dns_ip": "10.0.0.0/24", + "user": "demo", + "password": "supersecret", + "type": "kerberos", + "id": "3c829734-0679-4c17-9637-801da48c0d5f" + }, + { + "status": "new", + "domain": null, + "project_id": "16e1ab15c35a457e9c2b2aa189f544e1", + "name": "SecServ2", + "created_at": "2015-09-07T12:25:03.000000", + "description": "Creating my second Security Service", + "updated_at": null, + "server": null, + "dns_ip": "10.0.0.0/24", + "user": null, + "password": null, + "type": "ldap", + "id": "5a1d3a12-34a7-4087-8983-50e9ed03509a" + } + ] + }`) + }) +} + +func MockFilteredListResponse(t *testing.T) { + th.Mux.HandleFunc("/security-services/detail", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` + { + "security_services": [ + { + "status": "new", + "domain": null, + "project_id": "16e1ab15c35a457e9c2b2aa189f544e1", + "name": "SecServ1", + "created_at": "2015-09-07T12:19:10.000000", + "description": "Creating my first Security Service", + "updated_at": null, + "server": null, + "dns_ip": "10.0.0.0/24", + "user": "demo", + "password": "supersecret", + "type": "kerberos", + "id": "3c829734-0679-4c17-9637-801da48c0d5f" + } + ] + }`) + }) +} + +func MockGetResponse(t *testing.T) { + th.Mux.HandleFunc("/security-services/3c829734-0679-4c17-9637-801da48c0d5f", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` + { + "security_service": { + "status": "new", + "domain": null, + "project_id": "16e1ab15c35a457e9c2b2aa189f544e1", + "name": "SecServ1", + "created_at": "2015-09-07T12:19:10.000000", + "updated_at": null, + "server": null, + "dns_ip": "10.0.0.0/24", + "user": "demo", + "password": "supersecret", + "type": "kerberos", + "id": "3c829734-0679-4c17-9637-801da48c0d5f", + "description": "Creating my first Security Service" + } + }`) + }) +} + +func MockUpdateResponse(t *testing.T) { + th.Mux.HandleFunc("/security-services/securityServiceID", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` + { + "security_service": { + "status": "new", + "domain": null, + "project_id": "16e1ab15c35a457e9c2b2aa189f544e1", + "name": "SecServ2", + "created_at": "2015-09-07T12:19:10.000000", + "updated_at": "2015-09-07T12:20:10.000000", + "server": null, + "dns_ip": "10.0.0.0/24", + "user": "demo", + "password": "supersecret", + "type": "kerberos", + "id": "securityServiceID", + "description": "Updating my first Security Service" + } + } + `) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/securityservices/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/securityservices/testing/requests_test.go new file mode 100644 index 0000000000..06d9153d3a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/securityservices/testing/requests_test.go @@ -0,0 +1,208 @@ +package testing + +import ( + "testing" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/securityservices" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +// Verifies that a security service can be created correctly +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockCreateResponse(t) + + options := &securityservices.CreateOpts{ + Name: "SecServ1", + Description: "Creating my first Security Service", + DNSIP: "10.0.0.0/24", + User: "demo", + Password: "***", + Type: "kerberos", + } + + s, err := securityservices.Create(client.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, s.Name, "SecServ1") + th.AssertEquals(t, s.Description, "Creating my first Security Service") + th.AssertEquals(t, s.User, "demo") + th.AssertEquals(t, s.DNSIP, "10.0.0.0/24") + th.AssertEquals(t, s.Password, "supersecret") + th.AssertEquals(t, s.Type, "kerberos") +} + +// Verifies that a security service cannot be created without a type +func TestCreateFails(t *testing.T) { + options := &securityservices.CreateOpts{ + Name: "SecServ1", + Description: "Creating my first Security Service", + DNSIP: "10.0.0.0/24", + User: "demo", + Password: "***", + } + + _, err := securityservices.Create(client.ServiceClient(), options).Extract() + if _, ok := err.(gophercloud.ErrMissingInput); !ok { + t.Fatal("ErrMissingInput was expected to occur") + } +} + +// Verifies that security service deletion works +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockDeleteResponse(t) + + res := securityservices.Delete(client.ServiceClient(), "securityServiceID") + th.AssertNoErr(t, res.Err) +} + +// Verifies that security services can be listed correctly +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockListResponse(t) + + allPages, err := securityservices.List(client.ServiceClient(), &securityservices.ListOpts{}).AllPages() + th.AssertNoErr(t, err) + actual, err := securityservices.ExtractSecurityServices(allPages) + th.AssertNoErr(t, err) + var nilTime time.Time + expected := []securityservices.SecurityService{ + { + Status: "new", + Domain: "", + ProjectID: "16e1ab15c35a457e9c2b2aa189f544e1", + Name: "SecServ1", + CreatedAt: time.Date(2015, 9, 7, 12, 19, 10, 0, time.UTC), + Description: "Creating my first Security Service", + UpdatedAt: nilTime, + Server: "", + DNSIP: "10.0.0.0/24", + User: "demo", + Password: "supersecret", + Type: "kerberos", + ID: "3c829734-0679-4c17-9637-801da48c0d5f", + }, + { + Status: "new", + Domain: "", + ProjectID: "16e1ab15c35a457e9c2b2aa189f544e1", + Name: "SecServ2", + CreatedAt: time.Date(2015, 9, 7, 12, 25, 03, 0, time.UTC), + Description: "Creating my second Security Service", + UpdatedAt: nilTime, + Server: "", + DNSIP: "10.0.0.0/24", + User: "", + Password: "", + Type: "ldap", + ID: "5a1d3a12-34a7-4087-8983-50e9ed03509a", + }, + } + + th.CheckDeepEquals(t, expected, actual) +} + +// Verifies that security services list can be called with query parameters +func TestFilteredList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockFilteredListResponse(t) + + options := &securityservices.ListOpts{ + Type: "kerberos", + } + + allPages, err := securityservices.List(client.ServiceClient(), options).AllPages() + th.AssertNoErr(t, err) + actual, err := securityservices.ExtractSecurityServices(allPages) + th.AssertNoErr(t, err) + var nilTime time.Time + expected := []securityservices.SecurityService{ + { + Status: "new", + Domain: "", + ProjectID: "16e1ab15c35a457e9c2b2aa189f544e1", + Name: "SecServ1", + CreatedAt: time.Date(2015, 9, 7, 12, 19, 10, 0, time.UTC), + Description: "Creating my first Security Service", + UpdatedAt: nilTime, + Server: "", + DNSIP: "10.0.0.0/24", + User: "demo", + Password: "supersecret", + Type: "kerberos", + ID: "3c829734-0679-4c17-9637-801da48c0d5f", + }, + } + + th.CheckDeepEquals(t, expected, actual) +} + +// Verifies that it is possible to get a security service +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockGetResponse(t) + + var nilTime time.Time + expected := securityservices.SecurityService{ + ID: "3c829734-0679-4c17-9637-801da48c0d5f", + Name: "SecServ1", + CreatedAt: time.Date(2015, 9, 7, 12, 19, 10, 0, time.UTC), + Description: "Creating my first Security Service", + Type: "kerberos", + UpdatedAt: nilTime, + ProjectID: "16e1ab15c35a457e9c2b2aa189f544e1", + Status: "new", + Domain: "", + Server: "", + DNSIP: "10.0.0.0/24", + User: "demo", + Password: "supersecret", + } + + n, err := securityservices.Get(client.ServiceClient(), "3c829734-0679-4c17-9637-801da48c0d5f").Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, &expected, n) +} + +// Verifies that it is possible to update a security service +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockUpdateResponse(t) + expected := securityservices.SecurityService{ + ID: "securityServiceID", + Name: "SecServ2", + CreatedAt: time.Date(2015, 9, 7, 12, 19, 10, 0, time.UTC), + Description: "Updating my first Security Service", + Type: "kerberos", + UpdatedAt: time.Date(2015, 9, 7, 12, 20, 10, 0, time.UTC), + ProjectID: "16e1ab15c35a457e9c2b2aa189f544e1", + Status: "new", + Domain: "", + Server: "", + DNSIP: "10.0.0.0/24", + User: "demo", + Password: "supersecret", + } + + options := securityservices.UpdateOpts{Name: "SecServ2"} + s, err := securityservices.Update(client.ServiceClient(), "securityServiceID", options).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &expected, s) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/securityservices/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/securityservices/urls.go new file mode 100644 index 0000000000..c19b1f1062 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/securityservices/urls.go @@ -0,0 +1,23 @@ +package securityservices + +import "github.com/gophercloud/gophercloud" + +func createURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("security-services") +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("security-services", id) +} + +func listURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("security-services", "detail") +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return deleteURL(c, id) +} + +func updateURL(c *gophercloud.ServiceClient, id string) string { + return deleteURL(c, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharenetworks/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharenetworks/requests.go new file mode 100644 index 0000000000..cdc026c011 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharenetworks/requests.go @@ -0,0 +1,233 @@ +package sharenetworks + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToShareNetworkCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains options for creating a ShareNetwork. This object is +// passed to the sharenetworks.Create function. For more information about +// these parameters, see the ShareNetwork object. +type CreateOpts struct { + // The UUID of the Neutron network to set up for share servers + NeutronNetID string `json:"neutron_net_id,omitempty"` + // The UUID of the Neutron subnet to set up for share servers + NeutronSubnetID string `json:"neutron_subnet_id,omitempty"` + // The UUID of the nova network to set up for share servers + NovaNetID string `json:"nova_net_id,omitempty"` + // The share network name + Name string `json:"name"` + // The share network description + Description string `json:"description"` +} + +// ToShareNetworkCreateMap assembles a request body based on the contents of a +// CreateOpts. +func (opts CreateOpts) ToShareNetworkCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "share_network") +} + +// Create will create a new ShareNetwork based on the values in CreateOpts. To +// extract the ShareNetwork object from the response, call the Extract method +// on the CreateResult. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToShareNetworkCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 202}, + }) + return +} + +// Delete will delete the existing ShareNetwork with the provided ID. +func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} + +// ListOptsBuilder allows extensions to add additional parameters to the List +// request. +type ListOptsBuilder interface { + ToShareNetworkListQuery() (string, error) +} + +// ListOpts holds options for listing ShareNetworks. It is passed to the +// sharenetworks.List function. +type ListOpts struct { + // admin-only option. Set it to true to see all tenant share networks. + AllTenants bool `q:"all_tenants"` + // The UUID of the project where the share network was created + ProjectID string `q:"project_id"` + // The neutron network ID + NeutronNetID string `q:"neutron_net_id"` + // The neutron subnet ID + NeutronSubnetID string `q:"neutron_subnet_id"` + // The nova network ID + NovaNetID string `q:"nova_net_id"` + // The network type. A valid value is VLAN, VXLAN, GRE or flat + NetworkType string `q:"network_type"` + // The Share Network name + Name string `q:"name"` + // The Share Network description + Description string `q:"description"` + // The Share Network IP version + IPVersion gophercloud.IPVersion `q:"ip_version"` + // The Share Network segmentation ID + SegmentationID int `q:"segmentation_id"` + // List all share networks created after the given date + CreatedSince string `q:"created_since"` + // List all share networks created before the given date + CreatedBefore string `q:"created_before"` + // Limit specifies the page size. + Limit int `q:"limit"` + // Limit specifies the page number. + Offset int `q:"offset"` +} + +// ToShareNetworkListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToShareNetworkListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// ListDetail returns ShareNetworks optionally limited by the conditions provided in ListOpts. +func ListDetail(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listDetailURL(client) + if opts != nil { + query, err := opts.ToShareNetworkListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + p := ShareNetworkPage{pagination.MarkerPageBase{PageResult: r}} + p.MarkerPageBase.Owner = p + return p + }) +} + +// Get retrieves the ShareNetwork with the provided ID. To extract the ShareNetwork +// object from the response, call the Extract method on the GetResult. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToShareNetworkUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contain options for updating an existing ShareNetwork. This object is passed +// to the sharenetworks.Update function. For more information about the parameters, see +// the ShareNetwork object. +type UpdateOpts struct { + // The share network name + Name string `json:"name,omitempty"` + // The share network description + Description string `json:"description,omitempty"` + // The UUID of the Neutron network to set up for share servers + NeutronNetID string `json:"neutron_net_id,omitempty"` + // The UUID of the Neutron subnet to set up for share servers + NeutronSubnetID string `json:"neutron_subnet_id,omitempty"` + // The UUID of the nova network to set up for share servers + NovaNetID string `json:"nova_net_id,omitempty"` +} + +// ToShareNetworkUpdateMap assembles a request body based on the contents of an +// UpdateOpts. +func (opts UpdateOpts) ToShareNetworkUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "share_network") +} + +// Update will update the ShareNetwork with provided information. To extract the updated +// ShareNetwork from the response, call the Extract method on the UpdateResult. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToShareNetworkUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// AddSecurityServiceOptsBuilder allows extensions to add additional parameters to the +// AddSecurityService request. +type AddSecurityServiceOptsBuilder interface { + ToShareNetworkAddSecurityServiceMap() (map[string]interface{}, error) +} + +// AddSecurityServiceOpts contain options for adding a security service to an +// existing ShareNetwork. This object is passed to the sharenetworks.AddSecurityService +// function. For more information about the parameters, see the ShareNetwork object. +type AddSecurityServiceOpts struct { + SecurityServiceID string `json:"security_service_id"` +} + +// ToShareNetworkAddSecurityServiceMap assembles a request body based on the contents of an +// AddSecurityServiceOpts. +func (opts AddSecurityServiceOpts) ToShareNetworkAddSecurityServiceMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "add_security_service") +} + +// AddSecurityService will add the security service to a ShareNetwork. To extract the updated +// ShareNetwork from the response, call the Extract method on the UpdateResult. +func AddSecurityService(client *gophercloud.ServiceClient, id string, opts AddSecurityServiceOptsBuilder) (r UpdateResult) { + b, err := opts.ToShareNetworkAddSecurityServiceMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(addSecurityServiceURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// RemoveSecurityServiceOptsBuilder allows extensions to add additional parameters to the +// RemoveSecurityService request. +type RemoveSecurityServiceOptsBuilder interface { + ToShareNetworkRemoveSecurityServiceMap() (map[string]interface{}, error) +} + +// RemoveSecurityServiceOpts contain options for removing a security service from an +// existing ShareNetwork. This object is passed to the sharenetworks.RemoveSecurityService +// function. For more information about the parameters, see the ShareNetwork object. +type RemoveSecurityServiceOpts struct { + SecurityServiceID string `json:"security_service_id"` +} + +// ToShareNetworkRemoveSecurityServiceMap assembles a request body based on the contents of an +// RemoveSecurityServiceOpts. +func (opts RemoveSecurityServiceOpts) ToShareNetworkRemoveSecurityServiceMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "remove_security_service") +} + +// RemoveSecurityService will remove the security service from a ShareNetwork. To extract the updated +// ShareNetwork from the response, call the Extract method on the UpdateResult. +func RemoveSecurityService(client *gophercloud.ServiceClient, id string, opts RemoveSecurityServiceOptsBuilder) (r UpdateResult) { + b, err := opts.ToShareNetworkRemoveSecurityServiceMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(removeSecurityServiceURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharenetworks/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharenetworks/results.go new file mode 100644 index 0000000000..fdb7256953 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharenetworks/results.go @@ -0,0 +1,182 @@ +package sharenetworks + +import ( + "encoding/json" + "net/url" + "strconv" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ShareNetwork contains all the information associated with an OpenStack +// ShareNetwork. +type ShareNetwork struct { + // The Share Network ID + ID string `json:"id"` + // The UUID of the project where the share network was created + ProjectID string `json:"project_id"` + // The neutron network ID + NeutronNetID string `json:"neutron_net_id"` + // The neutron subnet ID + NeutronSubnetID string `json:"neutron_subnet_id"` + // The nova network ID + NovaNetID string `json:"nova_net_id"` + // The network type. A valid value is VLAN, VXLAN, GRE or flat + NetworkType string `json:"network_type"` + // The segmentation ID + SegmentationID int `json:"segmentation_id"` + // The IP block from which to allocate the network, in CIDR notation + CIDR string `json:"cidr"` + // The IP version of the network. A valid value is 4 or 6 + IPVersion int `json:"ip_version"` + // The Share Network name + Name string `json:"name"` + // The Share Network description + Description string `json:"description"` + // The date and time stamp when the Share Network was created + CreatedAt time.Time `json:"-"` + // The date and time stamp when the Share Network was updated + UpdatedAt time.Time `json:"-"` +} + +func (r *ShareNetwork) UnmarshalJSON(b []byte) error { + type tmp ShareNetwork + var s struct { + tmp + CreatedAt gophercloud.JSONRFC3339MilliNoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339MilliNoZ `json:"updated_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = ShareNetwork(s.tmp) + + r.CreatedAt = time.Time(s.CreatedAt) + r.UpdatedAt = time.Time(s.UpdatedAt) + + return nil +} + +type commonResult struct { + gophercloud.Result +} + +// ShareNetworkPage is a pagination.pager that is returned from a call to the List function. +type ShareNetworkPage struct { + pagination.MarkerPageBase +} + +// NextPageURL generates the URL for the page of results after this one. +func (r ShareNetworkPage) NextPageURL() (string, error) { + currentURL := r.URL + mark, err := r.Owner.LastMarker() + if err != nil { + return "", err + } + + q := currentURL.Query() + q.Set("offset", mark) + currentURL.RawQuery = q.Encode() + return currentURL.String(), nil +} + +// LastMarker returns the last offset in a ListResult. +func (r ShareNetworkPage) LastMarker() (string, error) { + maxInt := strconv.Itoa(int(^uint(0) >> 1)) + shareNetworks, err := ExtractShareNetworks(r) + if err != nil { + return maxInt, err + } + if len(shareNetworks) == 0 { + return maxInt, nil + } + + u, err := url.Parse(r.URL.String()) + if err != nil { + return maxInt, err + } + queryParams := u.Query() + offset := queryParams.Get("offset") + limit := queryParams.Get("limit") + + // Limit is not present, only one page required + if limit == "" { + return maxInt, nil + } + + iOffset := 0 + if offset != "" { + iOffset, err = strconv.Atoi(offset) + if err != nil { + return maxInt, err + } + } + iLimit, err := strconv.Atoi(limit) + if err != nil { + return maxInt, err + } + iOffset = iOffset + iLimit + offset = strconv.Itoa(iOffset) + + return offset, nil +} + +// IsEmpty satisifies the IsEmpty method of the Page interface +func (r ShareNetworkPage) IsEmpty() (bool, error) { + shareNetworks, err := ExtractShareNetworks(r) + return len(shareNetworks) == 0, err +} + +// ExtractShareNetworks extracts and returns ShareNetworks. It is used while +// iterating over a sharenetworks.List call. +func ExtractShareNetworks(r pagination.Page) ([]ShareNetwork, error) { + var s struct { + ShareNetworks []ShareNetwork `json:"share_networks"` + } + err := (r.(ShareNetworkPage)).ExtractInto(&s) + return s.ShareNetworks, err +} + +// Extract will get the ShareNetwork object out of the commonResult object. +func (r commonResult) Extract() (*ShareNetwork, error) { + var s struct { + ShareNetwork *ShareNetwork `json:"share_network"` + } + err := r.ExtractInto(&s) + return s.ShareNetwork, err +} + +// CreateResult contains the response body and error from a Create request. +type CreateResult struct { + commonResult +} + +// DeleteResult contains the response body and error from a Delete request. +type DeleteResult struct { + gophercloud.ErrResult +} + +// GetResult contains the response body and error from a Get request. +type GetResult struct { + commonResult +} + +// UpdateResult contains the response body and error from an Update request. +type UpdateResult struct { + commonResult +} + +// AddSecurityServiceResult contains the response body and error from a security +// service addition request. +type AddSecurityServiceResult struct { + commonResult +} + +// RemoveSecurityServiceResult contains the response body and error from a security +// service removal request. +type RemoveSecurityServiceResult struct { + commonResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharenetworks/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharenetworks/testing/fixtures.go new file mode 100644 index 0000000000..ee5567a636 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharenetworks/testing/fixtures.go @@ -0,0 +1,359 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +func createReq(name, description, network, subnetwork string) string { + return fmt.Sprintf(`{ + "share_network": { + "name": "%s", + "description": "%s", + "neutron_net_id": "%s", + "neutron_subnet_id": "%s" + } + }`, name, description, network, subnetwork) +} + +func createResp(name, description, network, subnetwork string) string { + return fmt.Sprintf(` + { + "share_network": { + "name": "%s", + "description": "%s", + "segmentation_id": null, + "created_at": "2015-09-07T14:37:00.583656", + "updated_at": null, + "id": "77eb3421-4549-4789-ac39-0d5185d68c29", + "neutron_net_id": "%s", + "neutron_subnet_id": "%s", + "ip_version": null, + "nova_net_id": null, + "cidr": null, + "project_id": "e10a683c20da41248cfd5e1ab3d88c62", + "network_type": null + } + }`, name, description, network, subnetwork) +} + +func MockCreateResponse(t *testing.T) { + th.Mux.HandleFunc("/share-networks", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, createReq("my_network", + "This is my share network", + "998b42ee-2cee-4d36-8b95-67b5ca1f2109", + "53482b62-2c84-4a53-b6ab-30d9d9800d06")) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, createResp("my_network", + "This is my share network", + "998b42ee-2cee-4d36-8b95-67b5ca1f2109", + "53482b62-2c84-4a53-b6ab-30d9d9800d06")) + }) +} + +func MockDeleteResponse(t *testing.T) { + th.Mux.HandleFunc("/share-networks/fa158a3d-6d9f-4187-9ca5-abbb82646eb2", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusAccepted) + }) +} + +func MockListResponse(t *testing.T) { + th.Mux.HandleFunc("/share-networks/detail", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + r.ParseForm() + marker := r.Form.Get("offset") + + switch marker { + case "": + fmt.Fprintf(w, `{ + "share_networks": [ + { + "name": "net_my1", + "segmentation_id": null, + "created_at": "2015-09-04T14:57:13.000000", + "neutron_subnet_id": "53482b62-2c84-4a53-b6ab-30d9d9800d06", + "updated_at": null, + "id": "32763294-e3d4-456a-998d-60047677c2fb", + "neutron_net_id": "998b42ee-2cee-4d36-8b95-67b5ca1f2109", + "ip_version": null, + "nova_net_id": null, + "cidr": null, + "project_id": "16e1ab15c35a457e9c2b2aa189f544e1", + "network_type": null, + "description": "descr" + }, + { + "name": "net_my", + "segmentation_id": null, + "created_at": "2015-09-04T14:54:25.000000", + "neutron_subnet_id": "53482b62-2c84-4a53-b6ab-30d9d9800d06", + "updated_at": null, + "id": "713df749-aac0-4a54-af52-10f6c991e80c", + "neutron_net_id": "998b42ee-2cee-4d36-8b95-67b5ca1f2109", + "ip_version": null, + "nova_net_id": null, + "cidr": null, + "project_id": "16e1ab15c35a457e9c2b2aa189f544e1", + "network_type": null, + "description": "desecr" + }, + { + "name": null, + "segmentation_id": null, + "created_at": "2015-09-04T14:51:41.000000", + "neutron_subnet_id": null, + "updated_at": null, + "id": "fa158a3d-6d9f-4187-9ca5-abbb82646eb2", + "neutron_net_id": null, + "ip_version": null, + "nova_net_id": null, + "cidr": null, + "project_id": "16e1ab15c35a457e9c2b2aa189f544e1", + "network_type": null, + "description": null + } + ] + }`) + default: + fmt.Fprintf(w, ` + { + "share_networks": [] + }`) + } + }) +} + +func MockFilteredListResponse(t *testing.T) { + th.Mux.HandleFunc("/share-networks/detail", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + r.ParseForm() + marker := r.Form.Get("offset") + switch marker { + case "": + fmt.Fprintf(w, ` + { + "share_networks": [ + { + "name": "net_my1", + "segmentation_id": null, + "created_at": "2015-09-04T14:57:13.000000", + "neutron_subnet_id": "53482b62-2c84-4a53-b6ab-30d9d9800d06", + "updated_at": null, + "id": "32763294-e3d4-456a-998d-60047677c2fb", + "neutron_net_id": "998b42ee-2cee-4d36-8b95-67b5ca1f2109", + "ip_version": null, + "nova_net_id": null, + "cidr": null, + "project_id": "16e1ab15c35a457e9c2b2aa189f544e1", + "network_type": null, + "description": "descr" + } + ] + }`) + case "1": + fmt.Fprintf(w, ` + { + "share_networks": [ + { + "name": "net_my1", + "segmentation_id": null, + "created_at": "2015-09-04T14:57:13.000000", + "neutron_subnet_id": "53482b62-2c84-4a53-b6ab-30d9d9800d06", + "updated_at": null, + "id": "32763294-e3d4-456a-998d-60047677c2fb", + "neutron_net_id": "998b42ee-2cee-4d36-8b95-67b5ca1f2109", + "ip_version": null, + "nova_net_id": null, + "cidr": null, + "project_id": "16e1ab15c35a457e9c2b2aa189f544e1", + "network_type": null, + "description": "descr" + } + ] + }`) + case "2": + fmt.Fprintf(w, ` + { + "share_networks": [ + { + "name": "net_my1", + "segmentation_id": null, + "created_at": "2015-09-04T14:57:13.000000", + "neutron_subnet_id": "53482b62-2c84-4a53-b6ab-30d9d9800d06", + "updated_at": null, + "id": "32763294-e3d4-456a-998d-60047677c2fb", + "neutron_net_id": "998b42ee-2cee-4d36-8b95-67b5ca1f2109", + "ip_version": null, + "nova_net_id": null, + "cidr": null, + "project_id": "16e1ab15c35a457e9c2b2aa189f544e1", + "network_type": null, + "description": "descr" + } + ] + }`) + default: + fmt.Fprintf(w, ` + { + "share_networks": [] + }`) + } + }) +} + +func MockGetResponse(t *testing.T) { + th.Mux.HandleFunc("/share-networks/7f950b52-6141-4a08-bbb5-bb7ffa3ea5fd", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` + { + "share_network": { + "name": "net_my1", + "segmentation_id": null, + "created_at": "2015-09-04T14:56:45.000000", + "neutron_subnet_id": "53482b62-2c84-4a53-b6ab-30d9d9800d06", + "updated_at": null, + "id": "7f950b52-6141-4a08-bbb5-bb7ffa3ea5fd", + "neutron_net_id": "998b42ee-2cee-4d36-8b95-67b5ca1f2109", + "ip_version": null, + "nova_net_id": null, + "cidr": null, + "project_id": "16e1ab15c35a457e9c2b2aa189f544e1", + "network_type": null, + "description": "descr" + } + }`) + }) +} + +func MockUpdateNeutronResponse(t *testing.T) { + th.Mux.HandleFunc("/share-networks/713df749-aac0-4a54-af52-10f6c991e80c", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` + { + "share_network": { + "name": "net_my2", + "segmentation_id": null, + "created_at": "2015-09-04T14:54:25.000000", + "neutron_subnet_id": "new-neutron-subnet-id", + "updated_at": "2015-09-07T08:02:53.512184", + "id": "713df749-aac0-4a54-af52-10f6c991e80c", + "neutron_net_id": "new-neutron-id", + "ip_version": 4, + "nova_net_id": null, + "cidr": null, + "project_id": "16e1ab15c35a457e9c2b2aa189f544e1", + "network_type": null, + "description": "new description" + } + } + `) + }) +} + +func MockUpdateNovaResponse(t *testing.T) { + th.Mux.HandleFunc("/share-networks/713df749-aac0-4a54-af52-10f6c991e80c", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` + { + "share_network": { + "name": "net_my2", + "segmentation_id": null, + "created_at": "2015-09-04T14:54:25.000000", + "neutron_subnet_id": null, + "updated_at": "2015-09-07T08:02:53.512184", + "id": "713df749-aac0-4a54-af52-10f6c991e80c", + "neutron_net_id": null, + "ip_version": 4, + "nova_net_id": "new-nova-id", + "cidr": null, + "project_id": "16e1ab15c35a457e9c2b2aa189f544e1", + "network_type": null, + "description": "new description" + } + } + `) + }) +} + +func MockAddSecurityServiceResponse(t *testing.T) { + th.Mux.HandleFunc("/share-networks/shareNetworkID/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` + { + "share_network": { + "name": "net2", + "segmentation_id": null, + "created_at": "2015-09-07T12:31:12.000000", + "neutron_subnet_id": null, + "updated_at": null, + "id": "d8ae6799-2567-4a89-aafb-fa4424350d2b", + "neutron_net_id": null, + "ip_version": 4, + "nova_net_id": "998b42ee-2cee-4d36-8b95-67b5ca1f2109", + "cidr": null, + "project_id": "16e1ab15c35a457e9c2b2aa189f544e1", + "network_type": null, + "description": null + } + }`) + }) +} + +func MockRemoveSecurityServiceResponse(t *testing.T) { + th.Mux.HandleFunc("/share-networks/shareNetworkID/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` + { + "share_network": { + "name": "net2", + "segmentation_id": null, + "created_at": "2015-09-07T12:31:12.000000", + "neutron_subnet_id": null, + "updated_at": null, + "id": "d8ae6799-2567-4a89-aafb-fa4424350d2b", + "neutron_net_id": null, + "ip_version": null, + "nova_net_id": "998b42ee-2cee-4d36-8b95-67b5ca1f2109", + "cidr": null, + "project_id": "16e1ab15c35a457e9c2b2aa189f544e1", + "network_type": null, + "description": null + } + }`) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharenetworks/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharenetworks/testing/requests_test.go new file mode 100644 index 0000000000..efa4691861 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharenetworks/testing/requests_test.go @@ -0,0 +1,280 @@ +package testing + +import ( + "testing" + "time" + + "github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharenetworks" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +// Verifies that a share network can be created correctly +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockCreateResponse(t) + + options := &sharenetworks.CreateOpts{ + Name: "my_network", + Description: "This is my share network", + NeutronNetID: "998b42ee-2cee-4d36-8b95-67b5ca1f2109", + NeutronSubnetID: "53482b62-2c84-4a53-b6ab-30d9d9800d06", + } + + n, err := sharenetworks.Create(client.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Name, "my_network") + th.AssertEquals(t, n.Description, "This is my share network") + th.AssertEquals(t, n.NeutronNetID, "998b42ee-2cee-4d36-8b95-67b5ca1f2109") + th.AssertEquals(t, n.NeutronSubnetID, "53482b62-2c84-4a53-b6ab-30d9d9800d06") +} + +// Verifies that share network deletion works +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockDeleteResponse(t) + + res := sharenetworks.Delete(client.ServiceClient(), "fa158a3d-6d9f-4187-9ca5-abbb82646eb2") + th.AssertNoErr(t, res.Err) +} + +// Verifies that share networks can be listed correctly +func TestListDetail(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockListResponse(t) + + allPages, err := sharenetworks.ListDetail(client.ServiceClient(), &sharenetworks.ListOpts{}).AllPages() + + th.AssertNoErr(t, err) + actual, err := sharenetworks.ExtractShareNetworks(allPages) + th.AssertNoErr(t, err) + + var nilTime time.Time + expected := []sharenetworks.ShareNetwork{ + { + ID: "32763294-e3d4-456a-998d-60047677c2fb", + Name: "net_my1", + CreatedAt: time.Date(2015, 9, 4, 14, 57, 13, 0, time.UTC), + Description: "descr", + NetworkType: "", + CIDR: "", + NovaNetID: "", + NeutronNetID: "998b42ee-2cee-4d36-8b95-67b5ca1f2109", + NeutronSubnetID: "53482b62-2c84-4a53-b6ab-30d9d9800d06", + IPVersion: 0, + SegmentationID: 0, + UpdatedAt: nilTime, + ProjectID: "16e1ab15c35a457e9c2b2aa189f544e1", + }, + { + ID: "713df749-aac0-4a54-af52-10f6c991e80c", + Name: "net_my", + CreatedAt: time.Date(2015, 9, 4, 14, 54, 25, 0, time.UTC), + Description: "desecr", + NetworkType: "", + CIDR: "", + NovaNetID: "", + NeutronNetID: "998b42ee-2cee-4d36-8b95-67b5ca1f2109", + NeutronSubnetID: "53482b62-2c84-4a53-b6ab-30d9d9800d06", + IPVersion: 0, + SegmentationID: 0, + UpdatedAt: nilTime, + ProjectID: "16e1ab15c35a457e9c2b2aa189f544e1", + }, + { + ID: "fa158a3d-6d9f-4187-9ca5-abbb82646eb2", + Name: "", + CreatedAt: time.Date(2015, 9, 4, 14, 51, 41, 0, time.UTC), + Description: "", + NetworkType: "", + CIDR: "", + NovaNetID: "", + NeutronNetID: "", + NeutronSubnetID: "", + IPVersion: 0, + SegmentationID: 0, + UpdatedAt: nilTime, + ProjectID: "16e1ab15c35a457e9c2b2aa189f544e1", + }, + } + + th.CheckDeepEquals(t, expected, actual) +} + +// Verifies that share networks list can be called with query parameters +func TestPaginatedListDetail(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockFilteredListResponse(t) + + options := &sharenetworks.ListOpts{ + Offset: 0, + Limit: 1, + } + + count := 0 + + err := sharenetworks.ListDetail(client.ServiceClient(), options).EachPage(func(page pagination.Page) (bool, error) { + count++ + _, err := sharenetworks.ExtractShareNetworks(page) + if err != nil { + t.Errorf("Failed to extract share networks: %v", err) + return false, err + } + + return true, nil + }) + th.AssertNoErr(t, err) + + th.AssertEquals(t, count, 3) +} + +// Verifies that it is possible to get a share network +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockGetResponse(t) + + var nilTime time.Time + expected := sharenetworks.ShareNetwork{ + ID: "7f950b52-6141-4a08-bbb5-bb7ffa3ea5fd", + Name: "net_my1", + CreatedAt: time.Date(2015, 9, 4, 14, 56, 45, 0, time.UTC), + Description: "descr", + NetworkType: "", + CIDR: "", + NovaNetID: "", + NeutronNetID: "998b42ee-2cee-4d36-8b95-67b5ca1f2109", + NeutronSubnetID: "53482b62-2c84-4a53-b6ab-30d9d9800d06", + IPVersion: 0, + SegmentationID: 0, + UpdatedAt: nilTime, + ProjectID: "16e1ab15c35a457e9c2b2aa189f544e1", + } + + n, err := sharenetworks.Get(client.ServiceClient(), "7f950b52-6141-4a08-bbb5-bb7ffa3ea5fd").Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, &expected, n) +} + +// Verifies that it is possible to update a share network using neutron network +func TestUpdateNeutron(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockUpdateNeutronResponse(t) + + expected := sharenetworks.ShareNetwork{ + ID: "713df749-aac0-4a54-af52-10f6c991e80c", + Name: "net_my2", + CreatedAt: time.Date(2015, 9, 4, 14, 54, 25, 0, time.UTC), + Description: "new description", + NetworkType: "", + CIDR: "", + NovaNetID: "", + NeutronNetID: "new-neutron-id", + NeutronSubnetID: "new-neutron-subnet-id", + IPVersion: 4, + SegmentationID: 0, + UpdatedAt: time.Date(2015, 9, 7, 8, 2, 53, 512184000, time.UTC), + ProjectID: "16e1ab15c35a457e9c2b2aa189f544e1", + } + + options := sharenetworks.UpdateOpts{ + Name: "net_my2", + Description: "new description", + NeutronNetID: "new-neutron-id", + NeutronSubnetID: "new-neutron-subnet-id", + } + + v, err := sharenetworks.Update(client.ServiceClient(), "713df749-aac0-4a54-af52-10f6c991e80c", options).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &expected, v) +} + +// Verifies that it is possible to update a share network using nova network +func TestUpdateNova(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockUpdateNovaResponse(t) + + expected := sharenetworks.ShareNetwork{ + ID: "713df749-aac0-4a54-af52-10f6c991e80c", + Name: "net_my2", + CreatedAt: time.Date(2015, 9, 4, 14, 54, 25, 0, time.UTC), + Description: "new description", + NetworkType: "", + CIDR: "", + NovaNetID: "new-nova-id", + NeutronNetID: "", + NeutronSubnetID: "", + IPVersion: 4, + SegmentationID: 0, + UpdatedAt: time.Date(2015, 9, 7, 8, 2, 53, 512184000, time.UTC), + ProjectID: "16e1ab15c35a457e9c2b2aa189f544e1", + } + + options := sharenetworks.UpdateOpts{ + Name: "net_my2", + Description: "new description", + NovaNetID: "new-nova-id", + } + + v, err := sharenetworks.Update(client.ServiceClient(), "713df749-aac0-4a54-af52-10f6c991e80c", options).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &expected, v) +} + +// Verifies that it is possible to add a security service to a share network +func TestAddSecurityService(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockAddSecurityServiceResponse(t) + + var nilTime time.Time + expected := sharenetworks.ShareNetwork{ + ID: "d8ae6799-2567-4a89-aafb-fa4424350d2b", + Name: "net2", + CreatedAt: time.Date(2015, 9, 7, 12, 31, 12, 0, time.UTC), + Description: "", + NetworkType: "", + CIDR: "", + NovaNetID: "998b42ee-2cee-4d36-8b95-67b5ca1f2109", + NeutronNetID: "", + NeutronSubnetID: "", + IPVersion: 4, + SegmentationID: 0, + UpdatedAt: nilTime, + ProjectID: "16e1ab15c35a457e9c2b2aa189f544e1", + } + + options := sharenetworks.AddSecurityServiceOpts{SecurityServiceID: "securityServiceID"} + s, err := sharenetworks.AddSecurityService(client.ServiceClient(), "shareNetworkID", options).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &expected, s) +} + +// Verifies that it is possible to remove a security service from a share network +func TestRemoveSecurityService(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockRemoveSecurityServiceResponse(t) + + options := sharenetworks.RemoveSecurityServiceOpts{SecurityServiceID: "securityServiceID"} + _, err := sharenetworks.RemoveSecurityService(client.ServiceClient(), "shareNetworkID", options).Extract() + th.AssertNoErr(t, err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharenetworks/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharenetworks/urls.go new file mode 100644 index 0000000000..667bd2a526 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharenetworks/urls.go @@ -0,0 +1,31 @@ +package sharenetworks + +import "github.com/gophercloud/gophercloud" + +func createURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("share-networks") +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("share-networks", id) +} + +func listDetailURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("share-networks", "detail") +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return deleteURL(c, id) +} + +func updateURL(c *gophercloud.ServiceClient, id string) string { + return deleteURL(c, id) +} + +func addSecurityServiceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("share-networks", id, "action") +} + +func removeSecurityServiceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("share-networks", id, "action") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/shares/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/shares/requests.go new file mode 100644 index 0000000000..cfa8460aeb --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/shares/requests.go @@ -0,0 +1,81 @@ +package shares + +import ( + "github.com/gophercloud/gophercloud" +) + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToShareCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains the options for create a Share. This object is +// passed to shares.Create(). For more information about these parameters, +// please refer to the Share object, or the shared file systems API v2 +// documentation +type CreateOpts struct { + // Defines the share protocol to use + ShareProto string `json:"share_proto" required:"true"` + // Size in GB + Size int `json:"size" required:"true"` + // Defines the share name + Name string `json:"name,omitempty"` + // Share description + Description string `json:"description,omitempty"` + // DisplayName is equivalent to Name. The API supports using both + // This is an inherited attribute from the block storage API + DisplayName string `json:"display_name,omitempty"` + // DisplayDescription is equivalent to Description. The API supports using bot + // This is an inherited attribute from the block storage API + DisplayDescription string `json:"display_description,omitempty"` + // ShareType defines the sharetype. If omitted, a default share type is used + ShareType string `json:"share_type,omitempty"` + // VolumeType is deprecated but supported. Either ShareType or VolumeType can be used + VolumeType string `json:"volume_type,omitempty"` + // The UUID from which to create a share + SnapshotID string `json:"snapshot_id,omitempty"` + // Determines whether or not the share is public + IsPublic *bool `json:"is_public,omitempty"` + // Key value pairs of user defined metadata + Metadata map[string]string `json:"metadata,omitempty"` + // The UUID of the share network to which the share belongs to + ShareNetworkID string `json:"share_network_id,omitempty"` + // The UUID of the consistency group to which the share belongs to + ConsistencyGroupID string `json:"consistency_group_id,omitempty"` + // The availability zone of the share + AvailabilityZone string `json:"availability_zone,omitempty"` +} + +// ToShareCreateMap assembles a request body based on the contents of a +// CreateOpts. +func (opts CreateOpts) ToShareCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "share") +} + +// Create will create a new Share based on the values in CreateOpts. To extract +// the Share object from the response, call the Extract method on the +// CreateResult. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToShareCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + return +} + +// Delete will delete an existing Share with the given UUID. +func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} + +// Get will get a single share with given UUID +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/shares/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/shares/results.go new file mode 100644 index 0000000000..224d1dfd17 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/shares/results.go @@ -0,0 +1,112 @@ +package shares + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud" +) + +// Share contains all information associated with an OpenStack Share +type Share struct { + // The availability zone of the share + AvailabilityZone string `json:"availability_zone"` + // A description of the share + Description string `json:"description,omitempty"` + // DisplayDescription is inherited from BlockStorage API. + // Both Description and DisplayDescription can be used + DisplayDescription string `json:"display_description,omitempty"` + // DisplayName is inherited from BlockStorage API + // Both DisplayName and Name can be used + DisplayName string `json:"display_name,omitempty"` + // Indicates whether a share has replicas or not. + HasReplicas bool `json:"has_replicas"` + // The host name of the share + Host string `json:"host"` + // The UUID of the share + ID string `json:"id"` + // Indicates the visibility of the share + IsPublic bool `json:"is_public,omitempty"` + // Share links for pagination + Links []map[string]string `json:"links"` + // Key, value -pairs of custom metadata + Metadata map[string]string `json:"metadata,omitempty"` + // The name of the share + Name string `json:"name,omitempty"` + // The UUID of the project to which this share belongs to + ProjectID string `json:"project_id"` + // The share replication type + ReplicationType string `json:"replication_type,omitempty"` + // The UUID of the share network + ShareNetworkID string `json:"share_network_id"` + // The shared file system protocol + ShareProto string `json:"share_proto"` + // The UUID of the share server + ShareServerID string `json:"share_server_id"` + // The UUID of the share type. + ShareType string `json:"share_type"` + // The name of the share type. + ShareTypeName string `json:"share_type_name"` + // Size of the share in GB + Size int `json:"size"` + // UUID of the snapshot from which to create the share + SnapshotID string `json:"snapshot_id"` + // The share status + Status string `json:"status"` + // The task state, used for share migration + TaskState string `json:"task_state"` + // The type of the volume + VolumeType string `json:"volume_type,omitempty"` + // The UUID of the consistency group this share belongs to + ConsistencyGroupID string `json:"consistency_group_id"` + // Used for filtering backends which either support or do not support share snapshots + SnapshotSupport bool `json:"snapshot_support"` + SourceCgsnapshotMemberID string `json:"source_cgsnapshot_member_id"` + // Timestamp when the share was created + CreatedAt time.Time `json:"-"` +} + +func (r *Share) UnmarshalJSON(b []byte) error { + type tmp Share + var s struct { + tmp + CreatedAt gophercloud.JSONRFC3339MilliNoZ `json:"created_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Share(s.tmp) + + r.CreatedAt = time.Time(s.CreatedAt) + + return nil +} + +type commonResult struct { + gophercloud.Result +} + +// Extract will get the Share object from the commonResult +func (r commonResult) Extract() (*Share, error) { + var s struct { + Share *Share `json:"share"` + } + err := r.ExtractInto(&s) + return s.Share, err +} + +// CreateResult contains the result.. +type CreateResult struct { + commonResult +} + +// DeleteResult contains the delete results +type DeleteResult struct { + gophercloud.ErrResult +} + +// GetResult contains the get result +type GetResult struct { + commonResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/shares/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/shares/testing/fixtures.go new file mode 100644 index 0000000000..a747f0805e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/shares/testing/fixtures.go @@ -0,0 +1,143 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +const ( + shareEndpoint = "/shares" + shareID = "011d21e2-fbc3-4e4a-9993-9ea223f73264" +) + +var createRequest = `{ + "share": { + "name": "my_test_share", + "size": 1, + "share_proto": "NFS" + } + }` + +var createResponse = `{ + "share": { + "name": "my_test_share", + "share_proto": "NFS", + "size": 1, + "status": null, + "share_server_id": null, + "project_id": "16e1ab15c35a457e9c2b2aa189f544e1", + "share_type": "25747776-08e5-494f-ab40-a64b9d20d8f7", + "share_type_name": "default", + "availability_zone": null, + "created_at": "2015-09-18T10:25:24.533287", + "export_location": null, + "links": [ + { + "href": "http://172.18.198.54:8786/v1/16e1ab15c35a457e9c2b2aa189f544e1/shares/011d21e2-fbc3-4e4a-9993-9ea223f73264", + "rel": "self" + }, + { + "href": "http://172.18.198.54:8786/16e1ab15c35a457e9c2b2aa189f544e1/shares/011d21e2-fbc3-4e4a-9993-9ea223f73264", + "rel": "bookmark" + } + ], + "share_network_id": null, + "export_locations": [], + "host": null, + "access_rules_status": "active", + "has_replicas": false, + "replication_type": null, + "task_state": null, + "snapshot_support": true, + "consistency_group_id": "9397c191-8427-4661-a2e8-b23820dc01d4", + "source_cgsnapshot_member_id": null, + "volume_type": "default", + "snapshot_id": null, + "is_public": true, + "metadata": { + "project": "my_app", + "aim": "doc" + }, + "id": "011d21e2-fbc3-4e4a-9993-9ea223f73264", + "description": "My custom share London" + } + }` + +// MockCreateResponse creates a mock response +func MockCreateResponse(t *testing.T) { + th.Mux.HandleFunc(shareEndpoint, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, createRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, createResponse) + }) +} + +// MockDeleteResponse creates a mock delete response +func MockDeleteResponse(t *testing.T) { + th.Mux.HandleFunc(shareEndpoint+"/"+shareID, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusAccepted) + }) +} + +var getResponse = `{ + "share": { + "links": [ + { + "href": "http://172.18.198.54:8786/v2/16e1ab15c35a457e9c2b2aa189f544e1/shares/011d21e2-fbc3-4e4a-9993-9ea223f73264", + "rel": "self" + }, + { + "href": "http://172.18.198.54:8786/16e1ab15c35a457e9c2b2aa189f544e1/shares/011d21e2-fbc3-4e4a-9993-9ea223f73264", + "rel": "bookmark" + } + ], + "availability_zone": "nova", + "share_network_id": "713df749-aac0-4a54-af52-10f6c991e80c", + "share_server_id": "e268f4aa-d571-43dd-9ab3-f49ad06ffaef", + "snapshot_id": null, + "id": "011d21e2-fbc3-4e4a-9993-9ea223f73264", + "size": 1, + "share_type": "25747776-08e5-494f-ab40-a64b9d20d8f7", + "share_type_name": "default", + "consistency_group_id": "9397c191-8427-4661-a2e8-b23820dc01d4", + "project_id": "16e1ab15c35a457e9c2b2aa189f544e1", + "metadata": { + "project": "my_app", + "aim": "doc" + }, + "status": "available", + "description": "My custom share London", + "host": "manila2@generic1#GENERIC1", + "has_replicas": false, + "replication_type": null, + "task_state": null, + "is_public": true, + "snapshot_support": true, + "name": "my_test_share", + "created_at": "2015-09-18T10:25:24.000000", + "share_proto": "NFS", + "volume_type": "default", + "source_cgsnapshot_member_id": null + } +}` + +// MockGetResponse creates a mock get response +func MockGetResponse(t *testing.T) { + th.Mux.HandleFunc(shareEndpoint+"/"+shareID, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, getResponse) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/shares/testing/request_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/shares/testing/request_test.go new file mode 100644 index 0000000000..5b700a6823 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/shares/testing/request_test.go @@ -0,0 +1,84 @@ +package testing + +import ( + "testing" + "time" + + "github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/shares" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockCreateResponse(t) + + options := &shares.CreateOpts{Size: 1, Name: "my_test_share", ShareProto: "NFS"} + n, err := shares.Create(client.ServiceClient(), options).Extract() + + th.AssertNoErr(t, err) + th.AssertEquals(t, n.Name, "my_test_share") + th.AssertEquals(t, n.Size, 1) + th.AssertEquals(t, n.ShareProto, "NFS") +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockDeleteResponse(t) + + result := shares.Delete(client.ServiceClient(), shareID) + th.AssertNoErr(t, result.Err) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockGetResponse(t) + + s, err := shares.Get(client.ServiceClient(), shareID).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, s, &shares.Share{ + AvailabilityZone: "nova", + ShareNetworkID: "713df749-aac0-4a54-af52-10f6c991e80c", + ShareServerID: "e268f4aa-d571-43dd-9ab3-f49ad06ffaef", + SnapshotID: "", + ID: shareID, + Size: 1, + ShareType: "25747776-08e5-494f-ab40-a64b9d20d8f7", + ShareTypeName: "default", + ConsistencyGroupID: "9397c191-8427-4661-a2e8-b23820dc01d4", + ProjectID: "16e1ab15c35a457e9c2b2aa189f544e1", + Metadata: map[string]string{ + "project": "my_app", + "aim": "doc", + }, + Status: "available", + Description: "My custom share London", + Host: "manila2@generic1#GENERIC1", + HasReplicas: false, + ReplicationType: "", + TaskState: "", + SnapshotSupport: true, + Name: "my_test_share", + CreatedAt: time.Date(2015, time.September, 18, 10, 25, 24, 0, time.UTC), + ShareProto: "NFS", + VolumeType: "default", + SourceCgsnapshotMemberID: "", + IsPublic: true, + Links: []map[string]string{ + { + "href": "http://172.18.198.54:8786/v2/16e1ab15c35a457e9c2b2aa189f544e1/shares/011d21e2-fbc3-4e4a-9993-9ea223f73264", + "rel": "self", + }, + { + "href": "http://172.18.198.54:8786/16e1ab15c35a457e9c2b2aa189f544e1/shares/011d21e2-fbc3-4e4a-9993-9ea223f73264", + "rel": "bookmark", + }, + }, + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/shares/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/shares/urls.go new file mode 100644 index 0000000000..309f071bae --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/shares/urls.go @@ -0,0 +1,15 @@ +package shares + +import "github.com/gophercloud/gophercloud" + +func createURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("shares") +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("shares", id) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("shares", id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharetypes/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharetypes/requests.go new file mode 100644 index 0000000000..adb1216240 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharetypes/requests.go @@ -0,0 +1,210 @@ +package sharetypes + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToShareTypeCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains options for creating a ShareType. This object is +// passed to the sharetypes.Create function. For more information about +// these parameters, see the ShareType object. +type CreateOpts struct { + // The share type name + Name string `json:"name" required:"true"` + // Indicates whether a share type is publicly accessible + IsPublic bool `json:"os-share-type-access:is_public"` + // The extra specifications for the share type + ExtraSpecs ExtraSpecsOpts `json:"extra_specs" required:"true"` +} + +// ExtraSpecsOpts represent the extra specifications that can be selected for a share type +type ExtraSpecsOpts struct { + // An extra specification that defines the driver mode for share server, or storage, life cycle management + DriverHandlesShareServers bool `json:"driver_handles_share_servers" required:"true"` + // An extra specification that filters back ends by whether they do or do not support share snapshots + SnapshotSupport *bool `json:"snapshot_support,omitempty"` +} + +// ToShareTypeCreateMap assembles a request body based on the contents of a +// CreateOpts. +func (opts CreateOpts) ToShareTypeCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "share_type") +} + +// Create will create a new ShareType based on the values in CreateOpts. To +// extract the ShareType object from the response, call the Extract method +// on the CreateResult. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToShareTypeCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 202}, + }) + return +} + +// Delete will delete the existing ShareType with the provided ID. +func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} + +// ListOptsBuilder allows extensions to add additional parameters to the List +// request. +type ListOptsBuilder interface { + ToShareTypeListQuery() (string, error) +} + +// ListOpts holds options for listing ShareTypes. It is passed to the +// sharetypes.List function. +type ListOpts struct { + // Select if public types, private types, or both should be listed + IsPublic string `q:"is_public"` +} + +// ToShareTypeListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToShareTypeListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns ShareTypes optionally limited by the conditions provided in ListOpts. +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToShareTypeListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ShareTypePage{pagination.SinglePageBase(r)} + }) +} + +// GetDefault will retrieve the default ShareType. +func GetDefault(client *gophercloud.ServiceClient) (r GetDefaultResult) { + _, r.Err = client.Get(getDefaultURL(client), &r.Body, nil) + return +} + +// GetExtraSpecs will retrieve the extra specifications for a given ShareType. +func GetExtraSpecs(client *gophercloud.ServiceClient, id string) (r GetExtraSpecsResult) { + _, r.Err = client.Get(getExtraSpecsURL(client, id), &r.Body, nil) + return +} + +// SetExtraSpecsOptsBuilder allows extensions to add additional parameters to the +// SetExtraSpecs request. +type SetExtraSpecsOptsBuilder interface { + ToShareTypeSetExtraSpecsMap() (map[string]interface{}, error) +} + +type SetExtraSpecsOpts struct { + // A list of all extra specifications to be added to a ShareType + Specs map[string]interface{} `json:"extra_specs"` +} + +// ToShareTypeSetExtraSpecsMap assembles a request body based on the contents of a +// SetExtraSpecsOpts. +func (opts SetExtraSpecsOpts) ToShareTypeSetExtraSpecsMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "") +} + +// SetExtraSpecs will set new specifications for a ShareType based on the values +// in SetExtraSpecsOpts. To extract the extra specifications object from the response, +// call the Extract method on the SetExtraSpecsResult. +func SetExtraSpecs(client *gophercloud.ServiceClient, id string, opts SetExtraSpecsOptsBuilder) (r SetExtraSpecsResult) { + b, err := opts.ToShareTypeSetExtraSpecsMap() + if err != nil { + r.Err = err + return + } + + _, r.Err = client.Post(setExtraSpecsURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 202}, + }) + return +} + +// UnsetExtraSpecs will unset an extra specification for an existing ShareType. +func UnsetExtraSpecs(client *gophercloud.ServiceClient, id string, key string) (r UnsetExtraSpecsResult) { + _, r.Err = client.Delete(unsetExtraSpecsURL(client, id, key), nil) + return +} + +// ShowAccess will show access details for an existing ShareType. +func ShowAccess(client *gophercloud.ServiceClient, id string) (r ShowAccessResult) { + _, r.Err = client.Get(showAccessURL(client, id), &r.Body, nil) + return +} + +// AddAccessOptsBuilder allows extensions to add additional parameters to the +// AddAccess +type AddAccessOptsBuilder interface { + ToAddAccessMap() (map[string]interface{}, error) +} + +type AccessOpts struct { + // The UUID of the project to which access to the share type is granted. + Project string `json:"project"` +} + +// ToAddAccessMap assembles a request body based on the contents of a +// AccessOpts. +func (opts AccessOpts) ToAddAccessMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "addProjectAccess") +} + +// AddAccess will add access to a ShareType based on the values +// in AccessOpts. +func AddAccess(client *gophercloud.ServiceClient, id string, opts AddAccessOptsBuilder) (r AddAccessResult) { + b, err := opts.ToAddAccessMap() + if err != nil { + r.Err = err + return + } + + _, r.Err = client.Post(addAccessURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{200, 202}, + }) + return +} + +// RemoveAccessOptsBuilder allows extensions to add additional parameters to the +// RemoveAccess +type RemoveAccessOptsBuilder interface { + ToRemoveAccessMap() (map[string]interface{}, error) +} + +// ToRemoveAccessMap assembles a request body based on the contents of a +// AccessOpts. +func (opts AccessOpts) ToRemoveAccessMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "removeProjectAccess") +} + +// RemoveAccess will remove access to a ShareType based on the values +// in AccessOpts. +func RemoveAccess(client *gophercloud.ServiceClient, id string, opts RemoveAccessOptsBuilder) (r RemoveAccessResult) { + b, err := opts.ToRemoveAccessMap() + if err != nil { + r.Err = err + return + } + + _, r.Err = client.Post(removeAccessURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{200, 202}, + }) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharetypes/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharetypes/results.go new file mode 100644 index 0000000000..f60d757766 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharetypes/results.go @@ -0,0 +1,139 @@ +package sharetypes + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ShareType contains all the information associated with an OpenStack +// ShareType. +type ShareType struct { + // The Share Type ID + ID string `json:"id"` + // The Share Type name + Name string `json:"name"` + // Indicates whether a share type is publicly accessible + IsPublic bool `json:"os-share-type-access:is_public"` + // The required extra specifications for the share type + RequiredExtraSpecs map[string]interface{} `json:"required_extra_specs"` + // The extra specifications for the share type + ExtraSpecs map[string]interface{} `json:"extra_specs"` +} + +type commonResult struct { + gophercloud.Result +} + +// Extract will get the ShareType object out of the commonResult object. +func (r commonResult) Extract() (*ShareType, error) { + var s struct { + ShareType *ShareType `json:"share_type"` + } + err := r.ExtractInto(&s) + return s.ShareType, err +} + +// CreateResult contains the response body and error from a Create request. +type CreateResult struct { + commonResult +} + +// DeleteResult contains the response body and error from a Delete request. +type DeleteResult struct { + gophercloud.ErrResult +} + +// ShareTypePage is a pagination.pager that is returned from a call to the List function. +type ShareTypePage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a ListResult contains no ShareTypes. +func (r ShareTypePage) IsEmpty() (bool, error) { + shareTypes, err := ExtractShareTypes(r) + return len(shareTypes) == 0, err +} + +// ExtractShareTypes extracts and returns ShareTypes. It is used while +// iterating over a sharetypes.List call. +func ExtractShareTypes(r pagination.Page) ([]ShareType, error) { + var s struct { + ShareTypes []ShareType `json:"share_types"` + } + err := (r.(ShareTypePage)).ExtractInto(&s) + return s.ShareTypes, err +} + +// GetDefaultResult contains the response body and error from a Get Default request. +type GetDefaultResult struct { + commonResult +} + +// ExtraSpecs contains all the information associated with extra specifications +// for an Openstack ShareType. +type ExtraSpecs map[string]interface{} + +type extraSpecsResult struct { + gophercloud.Result +} + +// Extract will get the ExtraSpecs object out of the commonResult object. +func (r extraSpecsResult) Extract() (ExtraSpecs, error) { + var s struct { + Specs ExtraSpecs `json:"extra_specs"` + } + err := r.ExtractInto(&s) + return s.Specs, err +} + +// GetExtraSpecsResult contains the response body and error from a Get Extra Specs request. +type GetExtraSpecsResult struct { + extraSpecsResult +} + +// SetExtraSpecsResult contains the response body and error from a Set Extra Specs request. +type SetExtraSpecsResult struct { + extraSpecsResult +} + +// UnsetExtraSpecsResult contains the response body and error from a Unset Extra Specs request. +type UnsetExtraSpecsResult struct { + gophercloud.ErrResult +} + +// ShareTypeAccess contains all the information associated with an OpenStack +// ShareTypeAccess. +type ShareTypeAccess struct { + // The share type ID of the member. + ShareTypeID string `json:"share_type_id"` + // The UUID of the project for which access to the share type is granted. + ProjectID string `json:"project_id"` +} + +type shareTypeAccessResult struct { + gophercloud.Result +} + +// ShowAccessResult contains the response body and error from a Show access request. +type ShowAccessResult struct { + shareTypeAccessResult +} + +// Extract will get the ShareTypeAccess objects out of the shareTypeAccessResult object. +func (r ShowAccessResult) Extract() ([]ShareTypeAccess, error) { + var s struct { + ShareTypeAccess []ShareTypeAccess `json:"share_type_access"` + } + err := r.ExtractInto(&s) + return s.ShareTypeAccess, err +} + +// AddAccessResult contains the response body and error from a Add Access request. +type AddAccessResult struct { + gophercloud.ErrResult +} + +// RemoveAccessResult contains the response body and error from a Remove Access request. +type RemoveAccessResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharetypes/testing/fixtures.go b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharetypes/testing/fixtures.go new file mode 100644 index 0000000000..7ba85ed189 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharetypes/testing/fixtures.go @@ -0,0 +1,272 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +func MockCreateResponse(t *testing.T) { + th.Mux.HandleFunc("/types", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` + { + "share_type": { + "os-share-type-access:is_public": true, + "extra_specs": { + "driver_handles_share_servers": true, + "snapshot_support": true + }, + "name": "my_new_share_type" + } + }`) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, ` + { + "volume_type": { + "os-share-type-access:is_public": true, + "required_extra_specs": { + "driver_handles_share_servers": true + }, + "extra_specs": { + "snapshot_support": "True", + "driver_handles_share_servers": "True" + }, + "name": "my_new_share_type", + "id": "1d600d02-26a7-4b23-af3d-7d51860fe858" + }, + "share_type": { + "os-share-type-access:is_public": true, + "required_extra_specs": { + "driver_handles_share_servers": true + }, + "extra_specs": { + "snapshot_support": "True", + "driver_handles_share_servers": "True" + }, + "name": "my_new_share_type", + "id": "1d600d02-26a7-4b23-af3d-7d51860fe858" + } + }`) + }) +} + +func MockDeleteResponse(t *testing.T) { + th.Mux.HandleFunc("/types/shareTypeID", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusAccepted) + }) +} + +func MockListResponse(t *testing.T) { + th.Mux.HandleFunc("/types", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` + { + "volume_types": [ + { + "os-share-type-access:is_public": true, + "required_extra_specs": { + "driver_handles_share_servers": "True" + }, + "extra_specs": { + "snapshot_support": "True", + "driver_handles_share_servers": "True" + }, + "name": "default", + "id": "be27425c-f807-4500-a056-d00721db45cf" + }, + { + "os-share-type-access:is_public": true, + "required_extra_specs": { + "driver_handles_share_servers": "false" + }, + "extra_specs": { + "snapshot_support": "True", + "driver_handles_share_servers": "false" + }, + "name": "d", + "id": "f015bebe-c38b-4c49-8832-00143b10253b" + } + ], + "share_types": [ + { + "os-share-type-access:is_public": true, + "required_extra_specs": { + "driver_handles_share_servers": "True" + }, + "extra_specs": { + "snapshot_support": "True", + "driver_handles_share_servers": "True" + }, + "name": "default", + "id": "be27425c-f807-4500-a056-d00721db45cf" + }, + { + "os-share-type-access:is_public": true, + "required_extra_specs": { + "driver_handles_share_servers": "false" + }, + "extra_specs": { + "snapshot_support": "True", + "driver_handles_share_servers": "false" + }, + "name": "d", + "id": "f015bebe-c38b-4c49-8832-00143b10253b" + } + ] + }`) + }) +} + +func MockGetDefaultResponse(t *testing.T) { + th.Mux.HandleFunc("/types/default", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` + { + "volume_type": { + "required_extra_specs": null, + "extra_specs": { + "snapshot_support": "True", + "driver_handles_share_servers": "True" + }, + "name": "default", + "id": "be27425c-f807-4500-a056-d00721db45cf" + }, + "share_type": { + "required_extra_specs": null, + "extra_specs": { + "snapshot_support": "True", + "driver_handles_share_servers": "True" + }, + "name": "default", + "id": "be27425c-f807-4500-a056-d00721db45cf" + } + }`) + }) +} + +func MockGetExtraSpecsResponse(t *testing.T) { + th.Mux.HandleFunc("/types/shareTypeID/extra_specs", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` + { + "extra_specs": { + "snapshot_support": "True", + "driver_handles_share_servers": "True", + "my_custom_extra_spec": "False" + } + }`) + }) +} + +func MockSetExtraSpecsResponse(t *testing.T) { + th.Mux.HandleFunc("/types/shareTypeID/extra_specs", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` + { + "extra_specs": { + "my_key": "my_value" + } + }`) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, ` + { + "extra_specs": { + "my_key": "my_value" + } + }`) + }) +} + +func MockUnsetExtraSpecsResponse(t *testing.T) { + th.Mux.HandleFunc("/types/shareTypeID/extra_specs/my_key", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusAccepted) + }) +} + +func MockShowAccessResponse(t *testing.T) { + th.Mux.HandleFunc("/types/shareTypeID/share_type_access", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` + { + "share_type_access": [ + { + "share_type_id": "1732f284-401d-41d9-a494-425451e8b4b8", + "project_id": "818a3f48dcd644909b3fa2e45a399a27" + }, + { + "share_type_id": "1732f284-401d-41d9-a494-425451e8b4b8", + "project_id": "e1284adea3ee4d2482af5ed214f3ad90" + } + ] + }`) + }) +} + +func MockAddAccessResponse(t *testing.T) { + th.Mux.HandleFunc("/types/shareTypeID/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` + { + "addProjectAccess": { + "project": "e1284adea3ee4d2482af5ed214f3ad90" + } + }`) + w.WriteHeader(http.StatusAccepted) + }) +} + +func MockRemoveAccessResponse(t *testing.T) { + th.Mux.HandleFunc("/types/shareTypeID/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` + { + "removeProjectAccess": { + "project": "e1284adea3ee4d2482af5ed214f3ad90" + } + }`) + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharetypes/testing/requests_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharetypes/testing/requests_test.go new file mode 100644 index 0000000000..1307391b2e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharetypes/testing/requests_test.go @@ -0,0 +1,217 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharetypes" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +// Verifies that a share type can be created correctly +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockCreateResponse(t) + + snapshotSupport := true + extraSpecs := sharetypes.ExtraSpecsOpts{ + DriverHandlesShareServers: true, + SnapshotSupport: &snapshotSupport, + } + + options := &sharetypes.CreateOpts{ + Name: "my_new_share_type", + IsPublic: true, + ExtraSpecs: extraSpecs, + } + + st, err := sharetypes.Create(client.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, st.Name, "my_new_share_type") + th.AssertEquals(t, st.IsPublic, true) +} + +// Verifies that a share type can't be created if the required parameters are missing +func TestCreateFails(t *testing.T) { + options := &sharetypes.CreateOpts{ + Name: "my_new_share_type", + } + + _, err := sharetypes.Create(client.ServiceClient(), options).Extract() + if _, ok := err.(gophercloud.ErrMissingInput); !ok { + t.Fatal("ErrMissingInput was expected to occur") + } + + extraSpecs := sharetypes.ExtraSpecsOpts{ + DriverHandlesShareServers: true, + } + + options = &sharetypes.CreateOpts{ + ExtraSpecs: extraSpecs, + } + + _, err = sharetypes.Create(client.ServiceClient(), options).Extract() + if _, ok := err.(gophercloud.ErrMissingInput); !ok { + t.Fatal("ErrMissingInput was expected to occur") + } +} + +// Verifies that share type deletion works +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockDeleteResponse(t) + res := sharetypes.Delete(client.ServiceClient(), "shareTypeID") + th.AssertNoErr(t, res.Err) +} + +// Verifies that share types can be listed correctly +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockListResponse(t) + + allPages, err := sharetypes.List(client.ServiceClient(), &sharetypes.ListOpts{}).AllPages() + th.AssertNoErr(t, err) + actual, err := sharetypes.ExtractShareTypes(allPages) + th.AssertNoErr(t, err) + expected := []sharetypes.ShareType{ + { + ID: "be27425c-f807-4500-a056-d00721db45cf", + Name: "default", + IsPublic: true, + ExtraSpecs: map[string]interface{}{"snapshot_support": "True", "driver_handles_share_servers": "True"}, + RequiredExtraSpecs: map[string]interface{}{"driver_handles_share_servers": "True"}, + }, + { + ID: "f015bebe-c38b-4c49-8832-00143b10253b", + Name: "d", + IsPublic: true, + ExtraSpecs: map[string]interface{}{"driver_handles_share_servers": "false", "snapshot_support": "True"}, + RequiredExtraSpecs: map[string]interface{}{"driver_handles_share_servers": "false"}, + }, + } + + th.CheckDeepEquals(t, expected, actual) +} + +// Verifies that it is possible to get the default share type +func TestGetDefault(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockGetDefaultResponse(t) + + expected := sharetypes.ShareType{ + ID: "be27425c-f807-4500-a056-d00721db45cf", + Name: "default", + ExtraSpecs: map[string]interface{}{"snapshot_support": "True", "driver_handles_share_servers": "True"}, + RequiredExtraSpecs: map[string]interface{}(nil), + } + + actual, err := sharetypes.GetDefault(client.ServiceClient()).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &expected, actual) +} + +// Verifies that it is possible to get the extra specifications for a share type +func TestGetExtraSpecs(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockGetExtraSpecsResponse(t) + + st, err := sharetypes.GetExtraSpecs(client.ServiceClient(), "shareTypeID").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, st["snapshot_support"], "True") + th.AssertEquals(t, st["driver_handles_share_servers"], "True") + th.AssertEquals(t, st["my_custom_extra_spec"], "False") +} + +// Verifies that an extra specs can be added to a share type +func TestSetExtraSpecs(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockSetExtraSpecsResponse(t) + + options := &sharetypes.SetExtraSpecsOpts{ + Specs: map[string]interface{}{"my_key": "my_value"}, + } + + es, err := sharetypes.SetExtraSpecs(client.ServiceClient(), "shareTypeID", options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, es["my_key"], "my_value") +} + +// Verifies that an extra specification can be unset for a share type +func TestUnsetExtraSpecs(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockUnsetExtraSpecsResponse(t) + res := sharetypes.UnsetExtraSpecs(client.ServiceClient(), "shareTypeID", "my_key") + th.AssertNoErr(t, res.Err) +} + +// Verifies that it is possible to see the access for a share type +func TestShowAccess(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockShowAccessResponse(t) + + expected := []sharetypes.ShareTypeAccess{ + { + ShareTypeID: "1732f284-401d-41d9-a494-425451e8b4b8", + ProjectID: "818a3f48dcd644909b3fa2e45a399a27", + }, + { + ShareTypeID: "1732f284-401d-41d9-a494-425451e8b4b8", + ProjectID: "e1284adea3ee4d2482af5ed214f3ad90", + }, + } + + shareType, err := sharetypes.ShowAccess(client.ServiceClient(), "shareTypeID").Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, expected, shareType) +} + +// Verifies that an access can be added to a share type +func TestAddAccess(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockAddAccessResponse(t) + + options := &sharetypes.AccessOpts{ + Project: "e1284adea3ee4d2482af5ed214f3ad90", + } + + err := sharetypes.AddAccess(client.ServiceClient(), "shareTypeID", options).ExtractErr() + th.AssertNoErr(t, err) +} + +// Verifies that an access can be removed from a share type +func TestRemoveAccess(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockRemoveAccessResponse(t) + + options := &sharetypes.AccessOpts{ + Project: "e1284adea3ee4d2482af5ed214f3ad90", + } + + err := sharetypes.RemoveAccess(client.ServiceClient(), "shareTypeID", options).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharetypes/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharetypes/urls.go new file mode 100644 index 0000000000..42779b1f97 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharetypes/urls.go @@ -0,0 +1,43 @@ +package sharetypes + +import "github.com/gophercloud/gophercloud" + +func createURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("types") +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("types", id) +} + +func listURL(c *gophercloud.ServiceClient) string { + return createURL(c) +} + +func getDefaultURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("types", "default") +} + +func getExtraSpecsURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("types", id, "extra_specs") +} + +func setExtraSpecsURL(c *gophercloud.ServiceClient, id string) string { + return getExtraSpecsURL(c, id) +} + +func unsetExtraSpecsURL(c *gophercloud.ServiceClient, id string, key string) string { + return c.ServiceURL("types", id, "extra_specs", key) +} + +func showAccessURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("types", id, "share_type_access") +} + +func addAccessURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("types", id, "action") +} + +func removeAccessURL(c *gophercloud.ServiceClient, id string) string { + return addAccessURL(c, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/testing/client_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/testing/client_test.go new file mode 100644 index 0000000000..3fe768fa42 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/testing/client_test.go @@ -0,0 +1,315 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack" + th "github.com/gophercloud/gophercloud/testhelper" +) + +const ID = "0123456789" + +func TestAuthenticatedClientV3(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, ` + { + "versions": { + "values": [ + { + "status": "stable", + "id": "v3.0", + "links": [ + { "href": "%s", "rel": "self" } + ] + }, + { + "status": "stable", + "id": "v2.0", + "links": [ + { "href": "%s", "rel": "self" } + ] + } + ] + } + } + `, th.Endpoint()+"v3/", th.Endpoint()+"v2.0/") + }) + + th.Mux.HandleFunc("/v3/auth/tokens", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("X-Subject-Token", ID) + + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `{ "token": { "expires_at": "2013-02-02T18:30:59.000000Z" } }`) + }) + + options := gophercloud.AuthOptions{ + Username: "me", + Password: "secret", + DomainName: "default", + TenantName: "project", + IdentityEndpoint: th.Endpoint(), + } + client, err := openstack.AuthenticatedClient(options) + th.AssertNoErr(t, err) + th.CheckEquals(t, ID, client.TokenID) +} + +func TestAuthenticatedClientV2(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, ` + { + "versions": { + "values": [ + { + "status": "experimental", + "id": "v3.0", + "links": [ + { "href": "%s", "rel": "self" } + ] + }, + { + "status": "stable", + "id": "v2.0", + "links": [ + { "href": "%s", "rel": "self" } + ] + } + ] + } + } + `, th.Endpoint()+"v3/", th.Endpoint()+"v2.0/") + }) + + th.Mux.HandleFunc("/v2.0/tokens", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, ` + { + "access": { + "token": { + "id": "01234567890", + "expires": "2014-10-01T10:00:00.000000Z" + }, + "serviceCatalog": [ + { + "name": "Cloud Servers", + "type": "compute", + "endpoints": [ + { + "tenantId": "t1000", + "publicURL": "https://compute.north.host.com/v1/t1000", + "internalURL": "https://compute.north.internal/v1/t1000", + "region": "North", + "versionId": "1", + "versionInfo": "https://compute.north.host.com/v1/", + "versionList": "https://compute.north.host.com/" + }, + { + "tenantId": "t1000", + "publicURL": "https://compute.north.host.com/v1.1/t1000", + "internalURL": "https://compute.north.internal/v1.1/t1000", + "region": "North", + "versionId": "1.1", + "versionInfo": "https://compute.north.host.com/v1.1/", + "versionList": "https://compute.north.host.com/" + } + ], + "endpoints_links": [] + }, + { + "name": "Cloud Files", + "type": "object-store", + "endpoints": [ + { + "tenantId": "t1000", + "publicURL": "https://storage.north.host.com/v1/t1000", + "internalURL": "https://storage.north.internal/v1/t1000", + "region": "North", + "versionId": "1", + "versionInfo": "https://storage.north.host.com/v1/", + "versionList": "https://storage.north.host.com/" + }, + { + "tenantId": "t1000", + "publicURL": "https://storage.south.host.com/v1/t1000", + "internalURL": "https://storage.south.internal/v1/t1000", + "region": "South", + "versionId": "1", + "versionInfo": "https://storage.south.host.com/v1/", + "versionList": "https://storage.south.host.com/" + } + ] + } + ] + } + } + `) + }) + + options := gophercloud.AuthOptions{ + Username: "me", + Password: "secret", + IdentityEndpoint: th.Endpoint(), + } + client, err := openstack.AuthenticatedClient(options) + th.AssertNoErr(t, err) + th.CheckEquals(t, "01234567890", client.TokenID) +} + +func TestIdentityAdminV3Client(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, ` + { + "versions": { + "values": [ + { + "status": "stable", + "id": "v3.0", + "links": [ + { "href": "%s", "rel": "self" } + ] + }, + { + "status": "stable", + "id": "v2.0", + "links": [ + { "href": "%s", "rel": "self" } + ] + } + ] + } + } + `, th.Endpoint()+"v3/", th.Endpoint()+"v2.0/") + }) + + th.Mux.HandleFunc("/v3/auth/tokens", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("X-Subject-Token", ID) + + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, ` + { + "token": { + "audit_ids": ["VcxU2JYqT8OzfUVvrjEITQ", "qNUTIJntTzO1-XUk5STybw"], + "catalog": [ + { + "endpoints": [ + { + "id": "39dc322ce86c4111b4f06c2eeae0841b", + "interface": "public", + "region": "RegionOne", + "url": "http://localhost:5000" + }, + { + "id": "ec642f27474842e78bf059f6c48f4e99", + "interface": "internal", + "region": "RegionOne", + "url": "http://localhost:5000" + }, + { + "id": "c609fc430175452290b62a4242e8a7e8", + "interface": "admin", + "region": "RegionOne", + "url": "http://localhost:35357" + } + ], + "id": "4363ae44bdf34a3981fde3b823cb9aa2", + "type": "identity", + "name": "keystone" + } + ], + "expires_at": "2013-02-27T18:30:59.999999Z", + "is_domain": false, + "issued_at": "2013-02-27T16:30:59.999999Z", + "methods": [ + "password" + ], + "project": { + "domain": { + "id": "1789d1", + "name": "example.com" + }, + "id": "263fd9", + "name": "project-x" + }, + "roles": [ + { + "id": "76e72a", + "name": "admin" + }, + { + "id": "f4f392", + "name": "member" + } + ], + "service_providers": [ + { + "auth_url":"https://example.com:5000/v3/OS-FEDERATION/identity_providers/acme/protocols/saml2/auth", + "id": "sp1", + "sp_url": "https://example.com:5000/Shibboleth.sso/SAML2/ECP" + }, + { + "auth_url":"https://other.example.com:5000/v3/OS-FEDERATION/identity_providers/acme/protocols/saml2/auth", + "id": "sp2", + "sp_url": "https://other.example.com:5000/Shibboleth.sso/SAML2/ECP" + } + ], + "user": { + "domain": { + "id": "1789d1", + "name": "example.com" + }, + "id": "0ca8f6", + "name": "Joe", + "password_expires_at": "2016-11-06T15:32:17.000000" + } + } +} + `) + }) + + options := gophercloud.AuthOptions{ + Username: "me", + Password: "secret", + DomainID: "12345", + IdentityEndpoint: th.Endpoint(), + } + pc, err := openstack.AuthenticatedClient(options) + th.AssertNoErr(t, err) + sc, err := openstack.NewIdentityV3(pc, gophercloud.EndpointOpts{ + Availability: gophercloud.AvailabilityAdmin, + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, "http://localhost:35357/", sc.Endpoint) +} + +func testAuthenticatedClientFails(t *testing.T, endpoint string) { + options := gophercloud.AuthOptions{ + Username: "me", + Password: "secret", + DomainName: "default", + TenantName: "project", + IdentityEndpoint: endpoint, + } + _, err := openstack.AuthenticatedClient(options) + if err == nil { + t.Fatal("expected error but call succeeded") + } +} + +func TestAuthenticatedClientV3Fails(t *testing.T) { + testAuthenticatedClientFails(t, "http://bad-address.example.com/v3") +} + +func TestAuthenticatedClientV2Fails(t *testing.T) { + testAuthenticatedClientFails(t, "http://bad-address.example.com/v2.0") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/testing/doc.go new file mode 100644 index 0000000000..34cfe7ac6d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/testing/doc.go @@ -0,0 +1,2 @@ +// openstack +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/testing/endpoint_location_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/testing/endpoint_location_test.go new file mode 100644 index 0000000000..ea7bdd2bf0 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/testing/endpoint_location_test.go @@ -0,0 +1,231 @@ +package testing + +import ( + "strings" + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack" + tokens2 "github.com/gophercloud/gophercloud/openstack/identity/v2/tokens" + tokens3 "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens" + th "github.com/gophercloud/gophercloud/testhelper" +) + +// Service catalog fixtures take too much vertical space! +var catalog2 = tokens2.ServiceCatalog{ + Entries: []tokens2.CatalogEntry{ + tokens2.CatalogEntry{ + Type: "same", + Name: "same", + Endpoints: []tokens2.Endpoint{ + tokens2.Endpoint{ + Region: "same", + PublicURL: "https://public.correct.com/", + InternalURL: "https://internal.correct.com/", + AdminURL: "https://admin.correct.com/", + }, + tokens2.Endpoint{ + Region: "different", + PublicURL: "https://badregion.com/", + }, + }, + }, + tokens2.CatalogEntry{ + Type: "same", + Name: "different", + Endpoints: []tokens2.Endpoint{ + tokens2.Endpoint{ + Region: "same", + PublicURL: "https://badname.com/", + }, + tokens2.Endpoint{ + Region: "different", + PublicURL: "https://badname.com/+badregion", + }, + }, + }, + tokens2.CatalogEntry{ + Type: "different", + Name: "different", + Endpoints: []tokens2.Endpoint{ + tokens2.Endpoint{ + Region: "same", + PublicURL: "https://badtype.com/+badname", + }, + tokens2.Endpoint{ + Region: "different", + PublicURL: "https://badtype.com/+badregion+badname", + }, + }, + }, + }, +} + +func TestV2EndpointExact(t *testing.T) { + expectedURLs := map[gophercloud.Availability]string{ + gophercloud.AvailabilityPublic: "https://public.correct.com/", + gophercloud.AvailabilityAdmin: "https://admin.correct.com/", + gophercloud.AvailabilityInternal: "https://internal.correct.com/", + } + + for availability, expected := range expectedURLs { + actual, err := openstack.V2EndpointURL(&catalog2, gophercloud.EndpointOpts{ + Type: "same", + Name: "same", + Region: "same", + Availability: availability, + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, expected, actual) + } +} + +func TestV2EndpointNone(t *testing.T) { + _, actual := openstack.V2EndpointURL(&catalog2, gophercloud.EndpointOpts{ + Type: "nope", + Availability: gophercloud.AvailabilityPublic, + }) + expected := &gophercloud.ErrEndpointNotFound{} + th.CheckEquals(t, expected.Error(), actual.Error()) +} + +func TestV2EndpointMultiple(t *testing.T) { + _, err := openstack.V2EndpointURL(&catalog2, gophercloud.EndpointOpts{ + Type: "same", + Region: "same", + Availability: gophercloud.AvailabilityPublic, + }) + if !strings.HasPrefix(err.Error(), "Discovered 2 matching endpoints:") { + t.Errorf("Received unexpected error: %v", err) + } +} + +func TestV2EndpointBadAvailability(t *testing.T) { + _, err := openstack.V2EndpointURL(&catalog2, gophercloud.EndpointOpts{ + Type: "same", + Name: "same", + Region: "same", + Availability: "wat", + }) + th.CheckEquals(t, "Unexpected availability in endpoint query: wat", err.Error()) +} + +var catalog3 = tokens3.ServiceCatalog{ + Entries: []tokens3.CatalogEntry{ + tokens3.CatalogEntry{ + Type: "same", + Name: "same", + Endpoints: []tokens3.Endpoint{ + tokens3.Endpoint{ + ID: "1", + Region: "same", + Interface: "public", + URL: "https://public.correct.com/", + }, + tokens3.Endpoint{ + ID: "2", + Region: "same", + Interface: "admin", + URL: "https://admin.correct.com/", + }, + tokens3.Endpoint{ + ID: "3", + Region: "same", + Interface: "internal", + URL: "https://internal.correct.com/", + }, + tokens3.Endpoint{ + ID: "4", + Region: "different", + Interface: "public", + URL: "https://badregion.com/", + }, + }, + }, + tokens3.CatalogEntry{ + Type: "same", + Name: "different", + Endpoints: []tokens3.Endpoint{ + tokens3.Endpoint{ + ID: "5", + Region: "same", + Interface: "public", + URL: "https://badname.com/", + }, + tokens3.Endpoint{ + ID: "6", + Region: "different", + Interface: "public", + URL: "https://badname.com/+badregion", + }, + }, + }, + tokens3.CatalogEntry{ + Type: "different", + Name: "different", + Endpoints: []tokens3.Endpoint{ + tokens3.Endpoint{ + ID: "7", + Region: "same", + Interface: "public", + URL: "https://badtype.com/+badname", + }, + tokens3.Endpoint{ + ID: "8", + Region: "different", + Interface: "public", + URL: "https://badtype.com/+badregion+badname", + }, + }, + }, + }, +} + +func TestV3EndpointExact(t *testing.T) { + expectedURLs := map[gophercloud.Availability]string{ + gophercloud.AvailabilityPublic: "https://public.correct.com/", + gophercloud.AvailabilityAdmin: "https://admin.correct.com/", + gophercloud.AvailabilityInternal: "https://internal.correct.com/", + } + + for availability, expected := range expectedURLs { + actual, err := openstack.V3EndpointURL(&catalog3, gophercloud.EndpointOpts{ + Type: "same", + Name: "same", + Region: "same", + Availability: availability, + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, expected, actual) + } +} + +func TestV3EndpointNone(t *testing.T) { + _, actual := openstack.V3EndpointURL(&catalog3, gophercloud.EndpointOpts{ + Type: "nope", + Availability: gophercloud.AvailabilityPublic, + }) + expected := &gophercloud.ErrEndpointNotFound{} + th.CheckEquals(t, expected.Error(), actual.Error()) +} + +func TestV3EndpointMultiple(t *testing.T) { + _, err := openstack.V3EndpointURL(&catalog3, gophercloud.EndpointOpts{ + Type: "same", + Region: "same", + Availability: gophercloud.AvailabilityPublic, + }) + if !strings.HasPrefix(err.Error(), "Discovered 2 matching endpoints:") { + t.Errorf("Received unexpected error: %v", err) + } +} + +func TestV3EndpointBadAvailability(t *testing.T) { + _, err := openstack.V3EndpointURL(&catalog3, gophercloud.EndpointOpts{ + Type: "same", + Name: "same", + Region: "same", + Availability: "wat", + }) + th.CheckEquals(t, "Unexpected availability in endpoint query: wat", err.Error()) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/utils/choose_version.go b/vendor/github.com/gophercloud/gophercloud/openstack/utils/choose_version.go new file mode 100644 index 0000000000..c605d08444 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/utils/choose_version.go @@ -0,0 +1,114 @@ +package utils + +import ( + "fmt" + "strings" + + "github.com/gophercloud/gophercloud" +) + +// Version is a supported API version, corresponding to a vN package within the appropriate service. +type Version struct { + ID string + Suffix string + Priority int +} + +var goodStatus = map[string]bool{ + "current": true, + "supported": true, + "stable": true, +} + +// ChooseVersion queries the base endpoint of an API to choose the most recent non-experimental alternative from a service's +// published versions. +// It returns the highest-Priority Version among the alternatives that are provided, as well as its corresponding endpoint. +func ChooseVersion(client *gophercloud.ProviderClient, recognized []*Version) (*Version, string, error) { + type linkResp struct { + Href string `json:"href"` + Rel string `json:"rel"` + } + + type valueResp struct { + ID string `json:"id"` + Status string `json:"status"` + Links []linkResp `json:"links"` + } + + type versionsResp struct { + Values []valueResp `json:"values"` + } + + type response struct { + Versions versionsResp `json:"versions"` + } + + normalize := func(endpoint string) string { + if !strings.HasSuffix(endpoint, "/") { + return endpoint + "/" + } + return endpoint + } + identityEndpoint := normalize(client.IdentityEndpoint) + + // If a full endpoint is specified, check version suffixes for a match first. + for _, v := range recognized { + if strings.HasSuffix(identityEndpoint, v.Suffix) { + return v, identityEndpoint, nil + } + } + + var resp response + _, err := client.Request("GET", client.IdentityBase, &gophercloud.RequestOpts{ + JSONResponse: &resp, + OkCodes: []int{200, 300}, + }) + + if err != nil { + return nil, "", err + } + + byID := make(map[string]*Version) + for _, version := range recognized { + byID[version.ID] = version + } + + var highest *Version + var endpoint string + + for _, value := range resp.Versions.Values { + href := "" + for _, link := range value.Links { + if link.Rel == "self" { + href = normalize(link.Href) + } + } + + if matching, ok := byID[value.ID]; ok { + // Prefer a version that exactly matches the provided endpoint. + if href == identityEndpoint { + if href == "" { + return nil, "", fmt.Errorf("Endpoint missing in version %s response from %s", value.ID, client.IdentityBase) + } + return matching, href, nil + } + + // Otherwise, find the highest-priority version with a whitelisted status. + if goodStatus[strings.ToLower(value.Status)] { + if highest == nil || matching.Priority > highest.Priority { + highest = matching + endpoint = href + } + } + } + } + + if highest == nil { + return nil, "", fmt.Errorf("No supported version available from endpoint %s", client.IdentityBase) + } + if endpoint == "" { + return nil, "", fmt.Errorf("Endpoint missing in version %s response from %s", highest.ID, client.IdentityBase) + } + + return highest, endpoint, nil +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/utils/testing/choose_version_test.go b/vendor/github.com/gophercloud/gophercloud/openstack/utils/testing/choose_version_test.go new file mode 100644 index 0000000000..9c0119cb28 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/utils/testing/choose_version_test.go @@ -0,0 +1,119 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/utils" + "github.com/gophercloud/gophercloud/testhelper" +) + +func setupVersionHandler() { + testhelper.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, ` + { + "versions": { + "values": [ + { + "status": "stable", + "id": "v3.0", + "links": [ + { "href": "%s/v3.0", "rel": "self" } + ] + }, + { + "status": "stable", + "id": "v2.0", + "links": [ + { "href": "%s/v2.0", "rel": "self" } + ] + } + ] + } + } + `, testhelper.Server.URL, testhelper.Server.URL) + }) +} + +func TestChooseVersion(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + setupVersionHandler() + + v2 := &utils.Version{ID: "v2.0", Priority: 2, Suffix: "blarg"} + v3 := &utils.Version{ID: "v3.0", Priority: 3, Suffix: "hargl"} + + c := &gophercloud.ProviderClient{ + IdentityBase: testhelper.Endpoint(), + IdentityEndpoint: "", + } + v, endpoint, err := utils.ChooseVersion(c, []*utils.Version{v2, v3}) + + if err != nil { + t.Fatalf("Unexpected error from ChooseVersion: %v", err) + } + + if v != v3 { + t.Errorf("Expected %#v to win, but %#v did instead", v3, v) + } + + expected := testhelper.Endpoint() + "v3.0/" + if endpoint != expected { + t.Errorf("Expected endpoint [%s], but was [%s] instead", expected, endpoint) + } +} + +func TestChooseVersionOpinionatedLink(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + setupVersionHandler() + + v2 := &utils.Version{ID: "v2.0", Priority: 2, Suffix: "nope"} + v3 := &utils.Version{ID: "v3.0", Priority: 3, Suffix: "northis"} + + c := &gophercloud.ProviderClient{ + IdentityBase: testhelper.Endpoint(), + IdentityEndpoint: testhelper.Endpoint() + "v2.0/", + } + v, endpoint, err := utils.ChooseVersion(c, []*utils.Version{v2, v3}) + if err != nil { + t.Fatalf("Unexpected error from ChooseVersion: %v", err) + } + + if v != v2 { + t.Errorf("Expected %#v to win, but %#v did instead", v2, v) + } + + expected := testhelper.Endpoint() + "v2.0/" + if endpoint != expected { + t.Errorf("Expected endpoint [%s], but was [%s] instead", expected, endpoint) + } +} + +func TestChooseVersionFromSuffix(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + v2 := &utils.Version{ID: "v2.0", Priority: 2, Suffix: "/v2.0/"} + v3 := &utils.Version{ID: "v3.0", Priority: 3, Suffix: "/v3.0/"} + + c := &gophercloud.ProviderClient{ + IdentityBase: testhelper.Endpoint(), + IdentityEndpoint: testhelper.Endpoint() + "v2.0/", + } + v, endpoint, err := utils.ChooseVersion(c, []*utils.Version{v2, v3}) + if err != nil { + t.Fatalf("Unexpected error from ChooseVersion: %v", err) + } + + if v != v2 { + t.Errorf("Expected %#v to win, but %#v did instead", v2, v) + } + + expected := testhelper.Endpoint() + "v2.0/" + if endpoint != expected { + t.Errorf("Expected endpoint [%s], but was [%s] instead", expected, endpoint) + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/utils/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/utils/testing/doc.go new file mode 100644 index 0000000000..66ecc07982 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/utils/testing/doc.go @@ -0,0 +1,2 @@ +//utils +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/pagination/http.go b/vendor/github.com/gophercloud/gophercloud/pagination/http.go new file mode 100644 index 0000000000..757295c423 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/pagination/http.go @@ -0,0 +1,60 @@ +package pagination + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "net/url" + "strings" + + "github.com/gophercloud/gophercloud" +) + +// PageResult stores the HTTP response that returned the current page of results. +type PageResult struct { + gophercloud.Result + url.URL +} + +// PageResultFrom parses an HTTP response as JSON and returns a PageResult containing the +// results, interpreting it as JSON if the content type indicates. +func PageResultFrom(resp *http.Response) (PageResult, error) { + var parsedBody interface{} + + defer resp.Body.Close() + rawBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return PageResult{}, err + } + + if strings.HasPrefix(resp.Header.Get("Content-Type"), "application/json") { + err = json.Unmarshal(rawBody, &parsedBody) + if err != nil { + return PageResult{}, err + } + } else { + parsedBody = rawBody + } + + return PageResultFromParsed(resp, parsedBody), err +} + +// PageResultFromParsed constructs a PageResult from an HTTP response that has already had its +// body parsed as JSON (and closed). +func PageResultFromParsed(resp *http.Response, body interface{}) PageResult { + return PageResult{ + Result: gophercloud.Result{ + Body: body, + Header: resp.Header, + }, + URL: *resp.Request.URL, + } +} + +// Request performs an HTTP request and extracts the http.Response from the result. +func Request(client *gophercloud.ServiceClient, headers map[string]string, url string) (*http.Response, error) { + return client.Get(url, nil, &gophercloud.RequestOpts{ + MoreHeaders: headers, + OkCodes: []int{200, 204, 300}, + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/pagination/linked.go b/vendor/github.com/gophercloud/gophercloud/pagination/linked.go new file mode 100644 index 0000000000..3656fb7f8f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/pagination/linked.go @@ -0,0 +1,92 @@ +package pagination + +import ( + "fmt" + "reflect" + + "github.com/gophercloud/gophercloud" +) + +// LinkedPageBase may be embedded to implement a page that provides navigational "Next" and "Previous" links within its result. +type LinkedPageBase struct { + PageResult + + // LinkPath lists the keys that should be traversed within a response to arrive at the "next" pointer. + // If any link along the path is missing, an empty URL will be returned. + // If any link results in an unexpected value type, an error will be returned. + // When left as "nil", []string{"links", "next"} will be used as a default. + LinkPath []string +} + +// NextPageURL extracts the pagination structure from a JSON response and returns the "next" link, if one is present. +// It assumes that the links are available in a "links" element of the top-level response object. +// If this is not the case, override NextPageURL on your result type. +func (current LinkedPageBase) NextPageURL() (string, error) { + var path []string + var key string + + if current.LinkPath == nil { + path = []string{"links", "next"} + } else { + path = current.LinkPath + } + + submap, ok := current.Body.(map[string]interface{}) + if !ok { + err := gophercloud.ErrUnexpectedType{} + err.Expected = "map[string]interface{}" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(current.Body)) + return "", err + } + + for { + key, path = path[0], path[1:len(path)] + + value, ok := submap[key] + if !ok { + return "", nil + } + + if len(path) > 0 { + submap, ok = value.(map[string]interface{}) + if !ok { + err := gophercloud.ErrUnexpectedType{} + err.Expected = "map[string]interface{}" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(value)) + return "", err + } + } else { + if value == nil { + // Actual null element. + return "", nil + } + + url, ok := value.(string) + if !ok { + err := gophercloud.ErrUnexpectedType{} + err.Expected = "string" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(value)) + return "", err + } + + return url, nil + } + } +} + +// IsEmpty satisifies the IsEmpty method of the Page interface +func (current LinkedPageBase) IsEmpty() (bool, error) { + if b, ok := current.Body.([]interface{}); ok { + return len(b) == 0, nil + } + err := gophercloud.ErrUnexpectedType{} + err.Expected = "[]interface{}" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(current.Body)) + return true, err +} + +// GetBody returns the linked page's body. This method is needed to satisfy the +// Page interface. +func (current LinkedPageBase) GetBody() interface{} { + return current.Body +} diff --git a/vendor/github.com/gophercloud/gophercloud/pagination/marker.go b/vendor/github.com/gophercloud/gophercloud/pagination/marker.go new file mode 100644 index 0000000000..52e53bae85 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/pagination/marker.go @@ -0,0 +1,58 @@ +package pagination + +import ( + "fmt" + "reflect" + + "github.com/gophercloud/gophercloud" +) + +// MarkerPage is a stricter Page interface that describes additional functionality required for use with NewMarkerPager. +// For convenience, embed the MarkedPageBase struct. +type MarkerPage interface { + Page + + // LastMarker returns the last "marker" value on this page. + LastMarker() (string, error) +} + +// MarkerPageBase is a page in a collection that's paginated by "limit" and "marker" query parameters. +type MarkerPageBase struct { + PageResult + + // Owner is a reference to the embedding struct. + Owner MarkerPage +} + +// NextPageURL generates the URL for the page of results after this one. +func (current MarkerPageBase) NextPageURL() (string, error) { + currentURL := current.URL + + mark, err := current.Owner.LastMarker() + if err != nil { + return "", err + } + + q := currentURL.Query() + q.Set("marker", mark) + currentURL.RawQuery = q.Encode() + + return currentURL.String(), nil +} + +// IsEmpty satisifies the IsEmpty method of the Page interface +func (current MarkerPageBase) IsEmpty() (bool, error) { + if b, ok := current.Body.([]interface{}); ok { + return len(b) == 0, nil + } + err := gophercloud.ErrUnexpectedType{} + err.Expected = "[]interface{}" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(current.Body)) + return true, err +} + +// GetBody returns the linked page's body. This method is needed to satisfy the +// Page interface. +func (current MarkerPageBase) GetBody() interface{} { + return current.Body +} diff --git a/vendor/github.com/gophercloud/gophercloud/pagination/pager.go b/vendor/github.com/gophercloud/gophercloud/pagination/pager.go new file mode 100644 index 0000000000..6f1609ef2e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/pagination/pager.go @@ -0,0 +1,238 @@ +package pagination + +import ( + "errors" + "fmt" + "net/http" + "reflect" + "strings" + + "github.com/gophercloud/gophercloud" +) + +var ( + // ErrPageNotAvailable is returned from a Pager when a next or previous page is requested, but does not exist. + ErrPageNotAvailable = errors.New("The requested page does not exist.") +) + +// Page must be satisfied by the result type of any resource collection. +// It allows clients to interact with the resource uniformly, regardless of whether or not or how it's paginated. +// Generally, rather than implementing this interface directly, implementors should embed one of the concrete PageBase structs, +// instead. +// Depending on the pagination strategy of a particular resource, there may be an additional subinterface that the result type +// will need to implement. +type Page interface { + + // NextPageURL generates the URL for the page of data that follows this collection. + // Return "" if no such page exists. + NextPageURL() (string, error) + + // IsEmpty returns true if this Page has no items in it. + IsEmpty() (bool, error) + + // GetBody returns the Page Body. This is used in the `AllPages` method. + GetBody() interface{} +} + +// Pager knows how to advance through a specific resource collection, one page at a time. +type Pager struct { + client *gophercloud.ServiceClient + + initialURL string + + createPage func(r PageResult) Page + + Err error + + // Headers supplies additional HTTP headers to populate on each paged request. + Headers map[string]string +} + +// NewPager constructs a manually-configured pager. +// Supply the URL for the first page, a function that requests a specific page given a URL, and a function that counts a page. +func NewPager(client *gophercloud.ServiceClient, initialURL string, createPage func(r PageResult) Page) Pager { + return Pager{ + client: client, + initialURL: initialURL, + createPage: createPage, + } +} + +// WithPageCreator returns a new Pager that substitutes a different page creation function. This is +// useful for overriding List functions in delegation. +func (p Pager) WithPageCreator(createPage func(r PageResult) Page) Pager { + return Pager{ + client: p.client, + initialURL: p.initialURL, + createPage: createPage, + } +} + +func (p Pager) fetchNextPage(url string) (Page, error) { + resp, err := Request(p.client, p.Headers, url) + if err != nil { + return nil, err + } + + remembered, err := PageResultFrom(resp) + if err != nil { + return nil, err + } + + return p.createPage(remembered), nil +} + +// EachPage iterates over each page returned by a Pager, yielding one at a time to a handler function. +// Return "false" from the handler to prematurely stop iterating. +func (p Pager) EachPage(handler func(Page) (bool, error)) error { + if p.Err != nil { + return p.Err + } + currentURL := p.initialURL + for { + currentPage, err := p.fetchNextPage(currentURL) + if err != nil { + return err + } + + empty, err := currentPage.IsEmpty() + if err != nil { + return err + } + if empty { + return nil + } + + ok, err := handler(currentPage) + if err != nil { + return err + } + if !ok { + return nil + } + + currentURL, err = currentPage.NextPageURL() + if err != nil { + return err + } + if currentURL == "" { + return nil + } + } +} + +// AllPages returns all the pages from a `List` operation in a single page, +// allowing the user to retrieve all the pages at once. +func (p Pager) AllPages() (Page, error) { + // pagesSlice holds all the pages until they get converted into as Page Body. + var pagesSlice []interface{} + // body will contain the final concatenated Page body. + var body reflect.Value + + // Grab a test page to ascertain the page body type. + testPage, err := p.fetchNextPage(p.initialURL) + if err != nil { + return nil, err + } + // Store the page type so we can use reflection to create a new mega-page of + // that type. + pageType := reflect.TypeOf(testPage) + + // if it's a single page, just return the testPage (first page) + if _, found := pageType.FieldByName("SinglePageBase"); found { + return testPage, nil + } + + // Switch on the page body type. Recognized types are `map[string]interface{}`, + // `[]byte`, and `[]interface{}`. + switch pb := testPage.GetBody().(type) { + case map[string]interface{}: + // key is the map key for the page body if the body type is `map[string]interface{}`. + var key string + // Iterate over the pages to concatenate the bodies. + err = p.EachPage(func(page Page) (bool, error) { + b := page.GetBody().(map[string]interface{}) + for k, v := range b { + // If it's a linked page, we don't want the `links`, we want the other one. + if !strings.HasSuffix(k, "links") { + // check the field's type. we only want []interface{} (which is really []map[string]interface{}) + switch vt := v.(type) { + case []interface{}: + key = k + pagesSlice = append(pagesSlice, vt...) + } + } + } + return true, nil + }) + if err != nil { + return nil, err + } + // Set body to value of type `map[string]interface{}` + body = reflect.MakeMap(reflect.MapOf(reflect.TypeOf(key), reflect.TypeOf(pagesSlice))) + body.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(pagesSlice)) + case []byte: + // Iterate over the pages to concatenate the bodies. + err = p.EachPage(func(page Page) (bool, error) { + b := page.GetBody().([]byte) + pagesSlice = append(pagesSlice, b) + // seperate pages with a comma + pagesSlice = append(pagesSlice, []byte{10}) + return true, nil + }) + if err != nil { + return nil, err + } + if len(pagesSlice) > 0 { + // Remove the trailing comma. + pagesSlice = pagesSlice[:len(pagesSlice)-1] + } + var b []byte + // Combine the slice of slices in to a single slice. + for _, slice := range pagesSlice { + b = append(b, slice.([]byte)...) + } + // Set body to value of type `bytes`. + body = reflect.New(reflect.TypeOf(b)).Elem() + body.SetBytes(b) + case []interface{}: + // Iterate over the pages to concatenate the bodies. + err = p.EachPage(func(page Page) (bool, error) { + b := page.GetBody().([]interface{}) + pagesSlice = append(pagesSlice, b...) + return true, nil + }) + if err != nil { + return nil, err + } + // Set body to value of type `[]interface{}` + body = reflect.MakeSlice(reflect.TypeOf(pagesSlice), len(pagesSlice), len(pagesSlice)) + for i, s := range pagesSlice { + body.Index(i).Set(reflect.ValueOf(s)) + } + default: + err := gophercloud.ErrUnexpectedType{} + err.Expected = "map[string]interface{}/[]byte/[]interface{}" + err.Actual = fmt.Sprintf("%T", pb) + return nil, err + } + + // Each `Extract*` function is expecting a specific type of page coming back, + // otherwise the type assertion in those functions will fail. pageType is needed + // to create a type in this method that has the same type that the `Extract*` + // function is expecting and set the Body of that object to the concatenated + // pages. + page := reflect.New(pageType) + // Set the page body to be the concatenated pages. + page.Elem().FieldByName("Body").Set(body) + // Set any additional headers that were pass along. The `objectstorage` pacakge, + // for example, passes a Content-Type header. + h := make(http.Header) + for k, v := range p.Headers { + h.Add(k, v) + } + page.Elem().FieldByName("Header").Set(reflect.ValueOf(h)) + // Type assert the page to a Page interface so that the type assertion in the + // `Extract*` methods will work. + return page.Elem().Interface().(Page), err +} diff --git a/vendor/github.com/gophercloud/gophercloud/pagination/pkg.go b/vendor/github.com/gophercloud/gophercloud/pagination/pkg.go new file mode 100644 index 0000000000..912daea364 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/pagination/pkg.go @@ -0,0 +1,4 @@ +/* +Package pagination contains utilities and convenience structs that implement common pagination idioms within OpenStack APIs. +*/ +package pagination diff --git a/vendor/github.com/gophercloud/gophercloud/pagination/single.go b/vendor/github.com/gophercloud/gophercloud/pagination/single.go new file mode 100644 index 0000000000..4251d6491e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/pagination/single.go @@ -0,0 +1,33 @@ +package pagination + +import ( + "fmt" + "reflect" + + "github.com/gophercloud/gophercloud" +) + +// SinglePageBase may be embedded in a Page that contains all of the results from an operation at once. +type SinglePageBase PageResult + +// NextPageURL always returns "" to indicate that there are no more pages to return. +func (current SinglePageBase) NextPageURL() (string, error) { + return "", nil +} + +// IsEmpty satisifies the IsEmpty method of the Page interface +func (current SinglePageBase) IsEmpty() (bool, error) { + if b, ok := current.Body.([]interface{}); ok { + return len(b) == 0, nil + } + err := gophercloud.ErrUnexpectedType{} + err.Expected = "[]interface{}" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(current.Body)) + return true, err +} + +// GetBody returns the single page's body. This method is needed to satisfy the +// Page interface. +func (current SinglePageBase) GetBody() interface{} { + return current.Body +} diff --git a/vendor/github.com/gophercloud/gophercloud/pagination/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/pagination/testing/doc.go new file mode 100644 index 0000000000..0bc1eb3807 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/pagination/testing/doc.go @@ -0,0 +1,2 @@ +// pagination +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/pagination/testing/linked_test.go b/vendor/github.com/gophercloud/gophercloud/pagination/testing/linked_test.go new file mode 100644 index 0000000000..3533e445a3 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/pagination/testing/linked_test.go @@ -0,0 +1,112 @@ +package testing + +import ( + "fmt" + "net/http" + "reflect" + "testing" + + "github.com/gophercloud/gophercloud/pagination" + "github.com/gophercloud/gophercloud/testhelper" +) + +// LinkedPager sample and test cases. + +type LinkedPageResult struct { + pagination.LinkedPageBase +} + +func (r LinkedPageResult) IsEmpty() (bool, error) { + is, err := ExtractLinkedInts(r) + return len(is) == 0, err +} + +func ExtractLinkedInts(r pagination.Page) ([]int, error) { + var s struct { + Ints []int `json:"ints"` + } + err := (r.(LinkedPageResult)).ExtractInto(&s) + return s.Ints, err +} + +func createLinked(t *testing.T) pagination.Pager { + testhelper.SetupHTTP() + + testhelper.Mux.HandleFunc("/page1", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ "ints": [1, 2, 3], "links": { "next": "%s/page2" } }`, testhelper.Server.URL) + }) + + testhelper.Mux.HandleFunc("/page2", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ "ints": [4, 5, 6], "links": { "next": "%s/page3" } }`, testhelper.Server.URL) + }) + + testhelper.Mux.HandleFunc("/page3", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ "ints": [7, 8, 9], "links": { "next": null } }`) + }) + + client := createClient() + + createPage := func(r pagination.PageResult) pagination.Page { + return LinkedPageResult{pagination.LinkedPageBase{PageResult: r}} + } + + return pagination.NewPager(client, testhelper.Server.URL+"/page1", createPage) +} + +func TestEnumerateLinked(t *testing.T) { + pager := createLinked(t) + defer testhelper.TeardownHTTP() + + callCount := 0 + err := pager.EachPage(func(page pagination.Page) (bool, error) { + actual, err := ExtractLinkedInts(page) + if err != nil { + return false, err + } + + t.Logf("Handler invoked with %v", actual) + + var expected []int + switch callCount { + case 0: + expected = []int{1, 2, 3} + case 1: + expected = []int{4, 5, 6} + case 2: + expected = []int{7, 8, 9} + default: + t.Fatalf("Unexpected call count: %d", callCount) + return false, nil + } + + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Call %d: Expected %#v, but was %#v", callCount, expected, actual) + } + + callCount++ + return true, nil + }) + if err != nil { + t.Errorf("Unexpected error for page iteration: %v", err) + } + + if callCount != 3 { + t.Errorf("Expected 3 calls, but was %d", callCount) + } +} + +func TestAllPagesLinked(t *testing.T) { + pager := createLinked(t) + defer testhelper.TeardownHTTP() + + page, err := pager.AllPages() + testhelper.AssertNoErr(t, err) + + expected := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} + actual, err := ExtractLinkedInts(page) + testhelper.AssertNoErr(t, err) + testhelper.CheckDeepEquals(t, expected, actual) +} diff --git a/vendor/github.com/gophercloud/gophercloud/pagination/testing/marker_test.go b/vendor/github.com/gophercloud/gophercloud/pagination/testing/marker_test.go new file mode 100644 index 0000000000..7b1a6daf4a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/pagination/testing/marker_test.go @@ -0,0 +1,127 @@ +package testing + +import ( + "fmt" + "net/http" + "strings" + "testing" + + "github.com/gophercloud/gophercloud/pagination" + "github.com/gophercloud/gophercloud/testhelper" +) + +// MarkerPager sample and test cases. + +type MarkerPageResult struct { + pagination.MarkerPageBase +} + +func (r MarkerPageResult) IsEmpty() (bool, error) { + results, err := ExtractMarkerStrings(r) + if err != nil { + return true, err + } + return len(results) == 0, err +} + +func (r MarkerPageResult) LastMarker() (string, error) { + results, err := ExtractMarkerStrings(r) + if err != nil { + return "", err + } + if len(results) == 0 { + return "", nil + } + return results[len(results)-1], nil +} + +func createMarkerPaged(t *testing.T) pagination.Pager { + testhelper.SetupHTTP() + + testhelper.Mux.HandleFunc("/page", func(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + ms := r.Form["marker"] + switch { + case len(ms) == 0: + fmt.Fprintf(w, "aaa\nbbb\nccc") + case len(ms) == 1 && ms[0] == "ccc": + fmt.Fprintf(w, "ddd\neee\nfff") + case len(ms) == 1 && ms[0] == "fff": + fmt.Fprintf(w, "ggg\nhhh\niii") + case len(ms) == 1 && ms[0] == "iii": + w.WriteHeader(http.StatusNoContent) + default: + t.Errorf("Request with unexpected marker: [%v]", ms) + } + }) + + client := createClient() + + createPage := func(r pagination.PageResult) pagination.Page { + p := MarkerPageResult{pagination.MarkerPageBase{PageResult: r}} + p.MarkerPageBase.Owner = p + return p + } + + return pagination.NewPager(client, testhelper.Server.URL+"/page", createPage) +} + +func ExtractMarkerStrings(page pagination.Page) ([]string, error) { + content := page.(MarkerPageResult).Body.([]uint8) + parts := strings.Split(string(content), "\n") + results := make([]string, 0, len(parts)) + for _, part := range parts { + if len(part) > 0 { + results = append(results, part) + } + } + return results, nil +} + +func TestEnumerateMarker(t *testing.T) { + pager := createMarkerPaged(t) + defer testhelper.TeardownHTTP() + + callCount := 0 + err := pager.EachPage(func(page pagination.Page) (bool, error) { + actual, err := ExtractMarkerStrings(page) + if err != nil { + return false, err + } + + t.Logf("Handler invoked with %v", actual) + + var expected []string + switch callCount { + case 0: + expected = []string{"aaa", "bbb", "ccc"} + case 1: + expected = []string{"ddd", "eee", "fff"} + case 2: + expected = []string{"ggg", "hhh", "iii"} + default: + t.Fatalf("Unexpected call count: %d", callCount) + return false, nil + } + + testhelper.CheckDeepEquals(t, expected, actual) + + callCount++ + return true, nil + }) + testhelper.AssertNoErr(t, err) + testhelper.AssertEquals(t, callCount, 3) +} + +func TestAllPagesMarker(t *testing.T) { + pager := createMarkerPaged(t) + defer testhelper.TeardownHTTP() + + page, err := pager.AllPages() + testhelper.AssertNoErr(t, err) + + expected := []string{"aaa", "bbb", "ccc", "ddd", "eee", "fff", "ggg", "hhh", "iii"} + actual, err := ExtractMarkerStrings(page) + testhelper.AssertNoErr(t, err) + testhelper.CheckDeepEquals(t, expected, actual) +} diff --git a/vendor/github.com/gophercloud/gophercloud/pagination/testing/pagination_test.go b/vendor/github.com/gophercloud/gophercloud/pagination/testing/pagination_test.go new file mode 100644 index 0000000000..170dca45ca --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/pagination/testing/pagination_test.go @@ -0,0 +1,13 @@ +package testing + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/testhelper" +) + +func createClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{ + ProviderClient: &gophercloud.ProviderClient{TokenID: "abc123"}, + Endpoint: testhelper.Endpoint(), + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/pagination/testing/single_test.go b/vendor/github.com/gophercloud/gophercloud/pagination/testing/single_test.go new file mode 100644 index 0000000000..8d95e948bf --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/pagination/testing/single_test.go @@ -0,0 +1,79 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud/pagination" + "github.com/gophercloud/gophercloud/testhelper" +) + +// SinglePage sample and test cases. + +type SinglePageResult struct { + pagination.SinglePageBase +} + +func (r SinglePageResult) IsEmpty() (bool, error) { + is, err := ExtractSingleInts(r) + if err != nil { + return true, err + } + return len(is) == 0, nil +} + +func ExtractSingleInts(r pagination.Page) ([]int, error) { + var s struct { + Ints []int `json:"ints"` + } + err := (r.(SinglePageResult)).ExtractInto(&s) + return s.Ints, err +} + +func setupSinglePaged() pagination.Pager { + testhelper.SetupHTTP() + client := createClient() + + testhelper.Mux.HandleFunc("/only", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ "ints": [1, 2, 3] }`) + }) + + createPage := func(r pagination.PageResult) pagination.Page { + return SinglePageResult{pagination.SinglePageBase(r)} + } + + return pagination.NewPager(client, testhelper.Server.URL+"/only", createPage) +} + +func TestEnumerateSinglePaged(t *testing.T) { + callCount := 0 + pager := setupSinglePaged() + defer testhelper.TeardownHTTP() + + err := pager.EachPage(func(page pagination.Page) (bool, error) { + callCount++ + + expected := []int{1, 2, 3} + actual, err := ExtractSingleInts(page) + testhelper.AssertNoErr(t, err) + testhelper.CheckDeepEquals(t, expected, actual) + return true, nil + }) + testhelper.CheckNoErr(t, err) + testhelper.CheckEquals(t, 1, callCount) +} + +func TestAllPagesSingle(t *testing.T) { + pager := setupSinglePaged() + defer testhelper.TeardownHTTP() + + page, err := pager.AllPages() + testhelper.AssertNoErr(t, err) + + expected := []int{1, 2, 3} + actual, err := ExtractSingleInts(page) + testhelper.AssertNoErr(t, err) + testhelper.CheckDeepEquals(t, expected, actual) +} diff --git a/vendor/github.com/gophercloud/gophercloud/params.go b/vendor/github.com/gophercloud/gophercloud/params.go new file mode 100644 index 0000000000..e484fe1c1e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/params.go @@ -0,0 +1,445 @@ +package gophercloud + +import ( + "encoding/json" + "fmt" + "net/url" + "reflect" + "strconv" + "strings" + "time" +) + +// BuildRequestBody builds a map[string]interface from the given `struct`. If +// parent is not the empty string, the final map[string]interface returned will +// encapsulate the built one +// +func BuildRequestBody(opts interface{}, parent string) (map[string]interface{}, error) { + optsValue := reflect.ValueOf(opts) + if optsValue.Kind() == reflect.Ptr { + optsValue = optsValue.Elem() + } + + optsType := reflect.TypeOf(opts) + if optsType.Kind() == reflect.Ptr { + optsType = optsType.Elem() + } + + optsMap := make(map[string]interface{}) + if optsValue.Kind() == reflect.Struct { + //fmt.Printf("optsValue.Kind() is a reflect.Struct: %+v\n", optsValue.Kind()) + for i := 0; i < optsValue.NumField(); i++ { + v := optsValue.Field(i) + f := optsType.Field(i) + + if f.Name != strings.Title(f.Name) { + //fmt.Printf("Skipping field: %s...\n", f.Name) + continue + } + + //fmt.Printf("Starting on field: %s...\n", f.Name) + + zero := isZero(v) + //fmt.Printf("v is zero?: %v\n", zero) + + // if the field has a required tag that's set to "true" + if requiredTag := f.Tag.Get("required"); requiredTag == "true" { + //fmt.Printf("Checking required field [%s]:\n\tv: %+v\n\tisZero:%v\n", f.Name, v.Interface(), zero) + // if the field's value is zero, return a missing-argument error + if zero { + // if the field has a 'required' tag, it can't have a zero-value + err := ErrMissingInput{} + err.Argument = f.Name + return nil, err + } + } + + if xorTag := f.Tag.Get("xor"); xorTag != "" { + //fmt.Printf("Checking `xor` tag for field [%s] with value %+v:\n\txorTag: %s\n", f.Name, v, xorTag) + xorField := optsValue.FieldByName(xorTag) + var xorFieldIsZero bool + if reflect.ValueOf(xorField.Interface()) == reflect.Zero(xorField.Type()) { + xorFieldIsZero = true + } else { + if xorField.Kind() == reflect.Ptr { + xorField = xorField.Elem() + } + xorFieldIsZero = isZero(xorField) + } + if !(zero != xorFieldIsZero) { + err := ErrMissingInput{} + err.Argument = fmt.Sprintf("%s/%s", f.Name, xorTag) + err.Info = fmt.Sprintf("Exactly one of %s and %s must be provided", f.Name, xorTag) + return nil, err + } + } + + if orTag := f.Tag.Get("or"); orTag != "" { + //fmt.Printf("Checking `or` tag for field with:\n\tname: %+v\n\torTag:%s\n", f.Name, orTag) + //fmt.Printf("field is zero?: %v\n", zero) + if zero { + orField := optsValue.FieldByName(orTag) + var orFieldIsZero bool + if reflect.ValueOf(orField.Interface()) == reflect.Zero(orField.Type()) { + orFieldIsZero = true + } else { + if orField.Kind() == reflect.Ptr { + orField = orField.Elem() + } + orFieldIsZero = isZero(orField) + } + if orFieldIsZero { + err := ErrMissingInput{} + err.Argument = fmt.Sprintf("%s/%s", f.Name, orTag) + err.Info = fmt.Sprintf("At least one of %s and %s must be provided", f.Name, orTag) + return nil, err + } + } + } + + if v.Kind() == reflect.Struct || (v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct) { + if zero { + //fmt.Printf("value before change: %+v\n", optsValue.Field(i)) + if jsonTag := f.Tag.Get("json"); jsonTag != "" { + jsonTagPieces := strings.Split(jsonTag, ",") + if len(jsonTagPieces) > 1 && jsonTagPieces[1] == "omitempty" { + if v.CanSet() { + if !v.IsNil() { + if v.Kind() == reflect.Ptr { + v.Set(reflect.Zero(v.Type())) + } + } + //fmt.Printf("value after change: %+v\n", optsValue.Field(i)) + } + } + } + continue + } + + //fmt.Printf("Calling BuildRequestBody with:\n\tv: %+v\n\tf.Name:%s\n", v.Interface(), f.Name) + _, err := BuildRequestBody(v.Interface(), f.Name) + if err != nil { + return nil, err + } + } + } + + //fmt.Printf("opts: %+v \n", opts) + + b, err := json.Marshal(opts) + if err != nil { + return nil, err + } + + //fmt.Printf("string(b): %s\n", string(b)) + + err = json.Unmarshal(b, &optsMap) + if err != nil { + return nil, err + } + + //fmt.Printf("optsMap: %+v\n", optsMap) + + if parent != "" { + optsMap = map[string]interface{}{parent: optsMap} + } + //fmt.Printf("optsMap after parent added: %+v\n", optsMap) + return optsMap, nil + } + // Return an error if the underlying type of 'opts' isn't a struct. + return nil, fmt.Errorf("Options type is not a struct.") +} + +// EnabledState is a convenience type, mostly used in Create and Update +// operations. Because the zero value of a bool is FALSE, we need to use a +// pointer instead to indicate zero-ness. +type EnabledState *bool + +// Convenience vars for EnabledState values. +var ( + iTrue = true + iFalse = false + + Enabled EnabledState = &iTrue + Disabled EnabledState = &iFalse +) + +// IPVersion is a type for the possible IP address versions. Valid instances +// are IPv4 and IPv6 +type IPVersion int + +const ( + // IPv4 is used for IP version 4 addresses + IPv4 IPVersion = 4 + // IPv6 is used for IP version 6 addresses + IPv6 IPVersion = 6 +) + +// IntToPointer is a function for converting integers into integer pointers. +// This is useful when passing in options to operations. +func IntToPointer(i int) *int { + return &i +} + +/* +MaybeString is an internal function to be used by request methods in individual +resource packages. + +It takes a string that might be a zero value and returns either a pointer to its +address or nil. This is useful for allowing users to conveniently omit values +from an options struct by leaving them zeroed, but still pass nil to the JSON +serializer so they'll be omitted from the request body. +*/ +func MaybeString(original string) *string { + if original != "" { + return &original + } + return nil +} + +/* +MaybeInt is an internal function to be used by request methods in individual +resource packages. + +Like MaybeString, it accepts an int that may or may not be a zero value, and +returns either a pointer to its address or nil. It's intended to hint that the +JSON serializer should omit its field. +*/ +func MaybeInt(original int) *int { + if original != 0 { + return &original + } + return nil +} + +/* +func isUnderlyingStructZero(v reflect.Value) bool { + switch v.Kind() { + case reflect.Ptr: + return isUnderlyingStructZero(v.Elem()) + default: + return isZero(v) + } +} +*/ + +var t time.Time + +func isZero(v reflect.Value) bool { + //fmt.Printf("\n\nchecking isZero for value: %+v\n", v) + switch v.Kind() { + case reflect.Ptr: + if v.IsNil() { + return true + } + return false + case reflect.Func, reflect.Map, reflect.Slice: + return v.IsNil() + case reflect.Array: + z := true + for i := 0; i < v.Len(); i++ { + z = z && isZero(v.Index(i)) + } + return z + case reflect.Struct: + if v.Type() == reflect.TypeOf(t) { + if v.Interface().(time.Time).IsZero() { + return true + } + return false + } + z := true + for i := 0; i < v.NumField(); i++ { + z = z && isZero(v.Field(i)) + } + return z + } + // Compare other types directly: + z := reflect.Zero(v.Type()) + //fmt.Printf("zero type for value: %+v\n\n\n", z) + return v.Interface() == z.Interface() +} + +/* +BuildQueryString is an internal function to be used by request methods in +individual resource packages. + +It accepts a tagged structure and expands it into a URL struct. Field names are +converted into query parameters based on a "q" tag. For example: + + type struct Something { + Bar string `q:"x_bar"` + Baz int `q:"lorem_ipsum"` + } + + instance := Something{ + Bar: "AAA", + Baz: "BBB", + } + +will be converted into "?x_bar=AAA&lorem_ipsum=BBB". + +The struct's fields may be strings, integers, or boolean values. Fields left at +their type's zero value will be omitted from the query. +*/ +func BuildQueryString(opts interface{}) (*url.URL, error) { + optsValue := reflect.ValueOf(opts) + if optsValue.Kind() == reflect.Ptr { + optsValue = optsValue.Elem() + } + + optsType := reflect.TypeOf(opts) + if optsType.Kind() == reflect.Ptr { + optsType = optsType.Elem() + } + + params := url.Values{} + + if optsValue.Kind() == reflect.Struct { + for i := 0; i < optsValue.NumField(); i++ { + v := optsValue.Field(i) + f := optsType.Field(i) + qTag := f.Tag.Get("q") + + // if the field has a 'q' tag, it goes in the query string + if qTag != "" { + tags := strings.Split(qTag, ",") + + // if the field is set, add it to the slice of query pieces + if !isZero(v) { + loop: + switch v.Kind() { + case reflect.Ptr: + v = v.Elem() + goto loop + case reflect.String: + params.Add(tags[0], v.String()) + case reflect.Int: + params.Add(tags[0], strconv.FormatInt(v.Int(), 10)) + case reflect.Bool: + params.Add(tags[0], strconv.FormatBool(v.Bool())) + case reflect.Slice: + switch v.Type().Elem() { + case reflect.TypeOf(0): + for i := 0; i < v.Len(); i++ { + params.Add(tags[0], strconv.FormatInt(v.Index(i).Int(), 10)) + } + default: + for i := 0; i < v.Len(); i++ { + params.Add(tags[0], v.Index(i).String()) + } + } + } + } else { + // Otherwise, the field is not set. + if len(tags) == 2 && tags[1] == "required" { + // And the field is required. Return an error. + return nil, fmt.Errorf("Required query parameter [%s] not set.", f.Name) + } + } + } + } + + return &url.URL{RawQuery: params.Encode()}, nil + } + // Return an error if the underlying type of 'opts' isn't a struct. + return nil, fmt.Errorf("Options type is not a struct.") +} + +/* +BuildHeaders is an internal function to be used by request methods in +individual resource packages. + +It accepts an arbitrary tagged structure and produces a string map that's +suitable for use as the HTTP headers of an outgoing request. Field names are +mapped to header names based in "h" tags. + + type struct Something { + Bar string `h:"x_bar"` + Baz int `h:"lorem_ipsum"` + } + + instance := Something{ + Bar: "AAA", + Baz: "BBB", + } + +will be converted into: + + map[string]string{ + "x_bar": "AAA", + "lorem_ipsum": "BBB", + } + +Untagged fields and fields left at their zero values are skipped. Integers, +booleans and string values are supported. +*/ +func BuildHeaders(opts interface{}) (map[string]string, error) { + optsValue := reflect.ValueOf(opts) + if optsValue.Kind() == reflect.Ptr { + optsValue = optsValue.Elem() + } + + optsType := reflect.TypeOf(opts) + if optsType.Kind() == reflect.Ptr { + optsType = optsType.Elem() + } + + optsMap := make(map[string]string) + if optsValue.Kind() == reflect.Struct { + for i := 0; i < optsValue.NumField(); i++ { + v := optsValue.Field(i) + f := optsType.Field(i) + hTag := f.Tag.Get("h") + + // if the field has a 'h' tag, it goes in the header + if hTag != "" { + tags := strings.Split(hTag, ",") + + // if the field is set, add it to the slice of query pieces + if !isZero(v) { + switch v.Kind() { + case reflect.String: + optsMap[tags[0]] = v.String() + case reflect.Int: + optsMap[tags[0]] = strconv.FormatInt(v.Int(), 10) + case reflect.Bool: + optsMap[tags[0]] = strconv.FormatBool(v.Bool()) + } + } else { + // Otherwise, the field is not set. + if len(tags) == 2 && tags[1] == "required" { + // And the field is required. Return an error. + return optsMap, fmt.Errorf("Required header not set.") + } + } + } + + } + return optsMap, nil + } + // Return an error if the underlying type of 'opts' isn't a struct. + return optsMap, fmt.Errorf("Options type is not a struct.") +} + +// IDSliceToQueryString takes a slice of elements and converts them into a query +// string. For example, if name=foo and slice=[]int{20, 40, 60}, then the +// result would be `?name=20&name=40&name=60' +func IDSliceToQueryString(name string, ids []int) string { + str := "" + for k, v := range ids { + if k == 0 { + str += "?" + } else { + str += "&" + } + str += fmt.Sprintf("%s=%s", name, strconv.Itoa(v)) + } + return str +} + +// IntWithinRange returns TRUE if an integer falls within a defined range, and +// FALSE if not. +func IntWithinRange(val, min, max int) bool { + return val > min && val < max +} diff --git a/vendor/github.com/gophercloud/gophercloud/provider_client.go b/vendor/github.com/gophercloud/gophercloud/provider_client.go new file mode 100644 index 0000000000..f88682381d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/provider_client.go @@ -0,0 +1,307 @@ +package gophercloud + +import ( + "bytes" + "encoding/json" + "io" + "io/ioutil" + "net/http" + "strings" +) + +// DefaultUserAgent is the default User-Agent string set in the request header. +const DefaultUserAgent = "gophercloud/2.0.0" + +// UserAgent represents a User-Agent header. +type UserAgent struct { + // prepend is the slice of User-Agent strings to prepend to DefaultUserAgent. + // All the strings to prepend are accumulated and prepended in the Join method. + prepend []string +} + +// Prepend prepends a user-defined string to the default User-Agent string. Users +// may pass in one or more strings to prepend. +func (ua *UserAgent) Prepend(s ...string) { + ua.prepend = append(s, ua.prepend...) +} + +// Join concatenates all the user-defined User-Agend strings with the default +// Gophercloud User-Agent string. +func (ua *UserAgent) Join() string { + uaSlice := append(ua.prepend, DefaultUserAgent) + return strings.Join(uaSlice, " ") +} + +// ProviderClient stores details that are required to interact with any +// services within a specific provider's API. +// +// Generally, you acquire a ProviderClient by calling the NewClient method in +// the appropriate provider's child package, providing whatever authentication +// credentials are required. +type ProviderClient struct { + // IdentityBase is the base URL used for a particular provider's identity + // service - it will be used when issuing authenticatation requests. It + // should point to the root resource of the identity service, not a specific + // identity version. + IdentityBase string + + // IdentityEndpoint is the identity endpoint. This may be a specific version + // of the identity service. If this is the case, this endpoint is used rather + // than querying versions first. + IdentityEndpoint string + + // TokenID is the ID of the most recently issued valid token. + TokenID string + + // EndpointLocator describes how this provider discovers the endpoints for + // its constituent services. + EndpointLocator EndpointLocator + + // HTTPClient allows users to interject arbitrary http, https, or other transit behaviors. + HTTPClient http.Client + + // UserAgent represents the User-Agent header in the HTTP request. + UserAgent UserAgent + + // ReauthFunc is the function used to re-authenticate the user if the request + // fails with a 401 HTTP response code. This a needed because there may be multiple + // authentication functions for different Identity service versions. + ReauthFunc func() error + + Debug bool +} + +// AuthenticatedHeaders returns a map of HTTP headers that are common for all +// authenticated service requests. +func (client *ProviderClient) AuthenticatedHeaders() map[string]string { + if client.TokenID == "" { + return map[string]string{} + } + return map[string]string{"X-Auth-Token": client.TokenID} +} + +// RequestOpts customizes the behavior of the provider.Request() method. +type RequestOpts struct { + // JSONBody, if provided, will be encoded as JSON and used as the body of the HTTP request. The + // content type of the request will default to "application/json" unless overridden by MoreHeaders. + // It's an error to specify both a JSONBody and a RawBody. + JSONBody interface{} + // RawBody contains an io.Reader that will be consumed by the request directly. No content-type + // will be set unless one is provided explicitly by MoreHeaders. + RawBody io.Reader + // JSONResponse, if provided, will be populated with the contents of the response body parsed as + // JSON. + JSONResponse interface{} + // OkCodes contains a list of numeric HTTP status codes that should be interpreted as success. If + // the response has a different code, an error will be returned. + OkCodes []int + // MoreHeaders specifies additional HTTP headers to be provide on the request. If a header is + // provided with a blank value (""), that header will be *omitted* instead: use this to suppress + // the default Accept header or an inferred Content-Type, for example. + MoreHeaders map[string]string + // ErrorContext specifies the resource error type to return if an error is encountered. + // This lets resources override default error messages based on the response status code. + ErrorContext error +} + +var applicationJSON = "application/json" + +// Request performs an HTTP request using the ProviderClient's current HTTPClient. An authentication +// header will automatically be provided. +func (client *ProviderClient) Request(method, url string, options *RequestOpts) (*http.Response, error) { + var body io.Reader + var contentType *string + + // Derive the content body by either encoding an arbitrary object as JSON, or by taking a provided + // io.ReadSeeker as-is. Default the content-type to application/json. + if options.JSONBody != nil { + if options.RawBody != nil { + panic("Please provide only one of JSONBody or RawBody to gophercloud.Request().") + } + + rendered, err := json.Marshal(options.JSONBody) + if err != nil { + return nil, err + } + + body = bytes.NewReader(rendered) + contentType = &applicationJSON + } + + if options.RawBody != nil { + body = options.RawBody + } + + // Construct the http.Request. + req, err := http.NewRequest(method, url, body) + if err != nil { + return nil, err + } + + // Populate the request headers. Apply options.MoreHeaders last, to give the caller the chance to + // modify or omit any header. + if contentType != nil { + req.Header.Set("Content-Type", *contentType) + } + req.Header.Set("Accept", applicationJSON) + + for k, v := range client.AuthenticatedHeaders() { + req.Header.Add(k, v) + } + + // Set the User-Agent header + req.Header.Set("User-Agent", client.UserAgent.Join()) + + if options.MoreHeaders != nil { + for k, v := range options.MoreHeaders { + if v != "" { + req.Header.Set(k, v) + } else { + req.Header.Del(k) + } + } + } + + // Set connection parameter to close the connection immediately when we've got the response + req.Close = true + + // Issue the request. + resp, err := client.HTTPClient.Do(req) + if err != nil { + return nil, err + } + + // Allow default OkCodes if none explicitly set + if options.OkCodes == nil { + options.OkCodes = defaultOkCodes(method) + } + + // Validate the HTTP response status. + var ok bool + for _, code := range options.OkCodes { + if resp.StatusCode == code { + ok = true + break + } + } + + if !ok { + body, _ := ioutil.ReadAll(resp.Body) + resp.Body.Close() + //pc := make([]uintptr, 1) + //runtime.Callers(2, pc) + //f := runtime.FuncForPC(pc[0]) + respErr := ErrUnexpectedResponseCode{ + URL: url, + Method: method, + Expected: options.OkCodes, + Actual: resp.StatusCode, + Body: body, + } + //respErr.Function = "gophercloud.ProviderClient.Request" + + errType := options.ErrorContext + switch resp.StatusCode { + case http.StatusBadRequest: + err = ErrDefault400{respErr} + if error400er, ok := errType.(Err400er); ok { + err = error400er.Error400(respErr) + } + case http.StatusUnauthorized: + if client.ReauthFunc != nil { + err = client.ReauthFunc() + if err != nil { + e := &ErrUnableToReauthenticate{} + e.ErrOriginal = respErr + return nil, e + } + if options.RawBody != nil { + if seeker, ok := options.RawBody.(io.Seeker); ok { + seeker.Seek(0, 0) + } + } + resp, err = client.Request(method, url, options) + if err != nil { + switch err.(type) { + case *ErrUnexpectedResponseCode: + e := &ErrErrorAfterReauthentication{} + e.ErrOriginal = err.(*ErrUnexpectedResponseCode) + return nil, e + default: + e := &ErrErrorAfterReauthentication{} + e.ErrOriginal = err + return nil, e + } + } + return resp, nil + } + err = ErrDefault401{respErr} + if error401er, ok := errType.(Err401er); ok { + err = error401er.Error401(respErr) + } + case http.StatusNotFound: + err = ErrDefault404{respErr} + if error404er, ok := errType.(Err404er); ok { + err = error404er.Error404(respErr) + } + case http.StatusMethodNotAllowed: + err = ErrDefault405{respErr} + if error405er, ok := errType.(Err405er); ok { + err = error405er.Error405(respErr) + } + case http.StatusRequestTimeout: + err = ErrDefault408{respErr} + if error408er, ok := errType.(Err408er); ok { + err = error408er.Error408(respErr) + } + case 429: + err = ErrDefault429{respErr} + if error429er, ok := errType.(Err429er); ok { + err = error429er.Error429(respErr) + } + case http.StatusInternalServerError: + err = ErrDefault500{respErr} + if error500er, ok := errType.(Err500er); ok { + err = error500er.Error500(respErr) + } + case http.StatusServiceUnavailable: + err = ErrDefault503{respErr} + if error503er, ok := errType.(Err503er); ok { + err = error503er.Error503(respErr) + } + } + + if err == nil { + err = respErr + } + + return resp, err + } + + // Parse the response body as JSON, if requested to do so. + if options.JSONResponse != nil { + defer resp.Body.Close() + if err := json.NewDecoder(resp.Body).Decode(options.JSONResponse); err != nil { + return nil, err + } + } + + return resp, nil +} + +func defaultOkCodes(method string) []int { + switch { + case method == "GET": + return []int{200} + case method == "POST": + return []int{201, 202} + case method == "PUT": + return []int{201, 202} + case method == "PATCH": + return []int{200, 204} + case method == "DELETE": + return []int{202, 204} + } + + return []int{} +} diff --git a/vendor/github.com/gophercloud/gophercloud/results.go b/vendor/github.com/gophercloud/gophercloud/results.go new file mode 100644 index 0000000000..76c16ef8ff --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/results.go @@ -0,0 +1,336 @@ +package gophercloud + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "reflect" + "strconv" + "time" +) + +/* +Result is an internal type to be used by individual resource packages, but its +methods will be available on a wide variety of user-facing embedding types. + +It acts as a base struct that other Result types, returned from request +functions, can embed for convenience. All Results capture basic information +from the HTTP transaction that was performed, including the response body, +HTTP headers, and any errors that happened. + +Generally, each Result type will have an Extract method that can be used to +further interpret the result's payload in a specific context. Extensions or +providers can then provide additional extraction functions to pull out +provider- or extension-specific information as well. +*/ +type Result struct { + // Body is the payload of the HTTP response from the server. In most cases, + // this will be the deserialized JSON structure. + Body interface{} + + // Header contains the HTTP header structure from the original response. + Header http.Header + + // Err is an error that occurred during the operation. It's deferred until + // extraction to make it easier to chain the Extract call. + Err error +} + +// ExtractInto allows users to provide an object into which `Extract` will extract +// the `Result.Body`. This would be useful for OpenStack providers that have +// different fields in the response object than OpenStack proper. +func (r Result) ExtractInto(to interface{}) error { + if r.Err != nil { + return r.Err + } + + if reader, ok := r.Body.(io.Reader); ok { + if readCloser, ok := reader.(io.Closer); ok { + defer readCloser.Close() + } + return json.NewDecoder(reader).Decode(to) + } + + b, err := json.Marshal(r.Body) + if err != nil { + return err + } + err = json.Unmarshal(b, to) + + return err +} + +func (r Result) extractIntoPtr(to interface{}, label string) error { + if label == "" { + return r.ExtractInto(&to) + } + + var m map[string]interface{} + err := r.ExtractInto(&m) + if err != nil { + return err + } + + b, err := json.Marshal(m[label]) + if err != nil { + return err + } + + err = json.Unmarshal(b, &to) + return err +} + +// ExtractIntoStructPtr will unmarshal the Result (r) into the provided +// interface{} (to). +// +// NOTE: For internal use only +// +// `to` must be a pointer to an underlying struct type +// +// If provided, `label` will be filtered out of the response +// body prior to `r` being unmarshalled into `to`. +func (r Result) ExtractIntoStructPtr(to interface{}, label string) error { + if r.Err != nil { + return r.Err + } + + t := reflect.TypeOf(to) + if k := t.Kind(); k != reflect.Ptr { + return fmt.Errorf("Expected pointer, got %v", k) + } + switch t.Elem().Kind() { + case reflect.Struct: + return r.extractIntoPtr(to, label) + default: + return fmt.Errorf("Expected pointer to struct, got: %v", t) + } +} + +// ExtractIntoSlicePtr will unmarshal the Result (r) into the provided +// interface{} (to). +// +// NOTE: For internal use only +// +// `to` must be a pointer to an underlying slice type +// +// If provided, `label` will be filtered out of the response +// body prior to `r` being unmarshalled into `to`. +func (r Result) ExtractIntoSlicePtr(to interface{}, label string) error { + if r.Err != nil { + return r.Err + } + + t := reflect.TypeOf(to) + if k := t.Kind(); k != reflect.Ptr { + return fmt.Errorf("Expected pointer, got %v", k) + } + switch t.Elem().Kind() { + case reflect.Slice: + return r.extractIntoPtr(to, label) + default: + return fmt.Errorf("Expected pointer to slice, got: %v", t) + } +} + +// PrettyPrintJSON creates a string containing the full response body as +// pretty-printed JSON. It's useful for capturing test fixtures and for +// debugging extraction bugs. If you include its output in an issue related to +// a buggy extraction function, we will all love you forever. +func (r Result) PrettyPrintJSON() string { + pretty, err := json.MarshalIndent(r.Body, "", " ") + if err != nil { + panic(err.Error()) + } + return string(pretty) +} + +// ErrResult is an internal type to be used by individual resource packages, but +// its methods will be available on a wide variety of user-facing embedding +// types. +// +// It represents results that only contain a potential error and +// nothing else. Usually, if the operation executed successfully, the Err field +// will be nil; otherwise it will be stocked with a relevant error. Use the +// ExtractErr method +// to cleanly pull it out. +type ErrResult struct { + Result +} + +// ExtractErr is a function that extracts error information, or nil, from a result. +func (r ErrResult) ExtractErr() error { + return r.Err +} + +/* +HeaderResult is an internal type to be used by individual resource packages, but +its methods will be available on a wide variety of user-facing embedding types. + +It represents a result that only contains an error (possibly nil) and an +http.Header. This is used, for example, by the objectstorage packages in +openstack, because most of the operations don't return response bodies, but do +have relevant information in headers. +*/ +type HeaderResult struct { + Result +} + +// ExtractHeader will return the http.Header and error from the HeaderResult. +// +// header, err := objects.Create(client, "my_container", objects.CreateOpts{}).ExtractHeader() +func (r HeaderResult) ExtractInto(to interface{}) error { + if r.Err != nil { + return r.Err + } + + tmpHeaderMap := map[string]string{} + for k, v := range r.Header { + if len(v) > 0 { + tmpHeaderMap[k] = v[0] + } + } + + b, err := json.Marshal(tmpHeaderMap) + if err != nil { + return err + } + err = json.Unmarshal(b, to) + + return err +} + +// RFC3339Milli describes a common time format used by some API responses. +const RFC3339Milli = "2006-01-02T15:04:05.999999Z" + +type JSONRFC3339Milli time.Time + +func (jt *JSONRFC3339Milli) UnmarshalJSON(data []byte) error { + b := bytes.NewBuffer(data) + dec := json.NewDecoder(b) + var s string + if err := dec.Decode(&s); err != nil { + return err + } + t, err := time.Parse(RFC3339Milli, s) + if err != nil { + return err + } + *jt = JSONRFC3339Milli(t) + return nil +} + +const RFC3339MilliNoZ = "2006-01-02T15:04:05.999999" + +type JSONRFC3339MilliNoZ time.Time + +func (jt *JSONRFC3339MilliNoZ) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if s == "" { + return nil + } + t, err := time.Parse(RFC3339MilliNoZ, s) + if err != nil { + return err + } + *jt = JSONRFC3339MilliNoZ(t) + return nil +} + +type JSONRFC1123 time.Time + +func (jt *JSONRFC1123) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if s == "" { + return nil + } + t, err := time.Parse(time.RFC1123, s) + if err != nil { + return err + } + *jt = JSONRFC1123(t) + return nil +} + +type JSONUnix time.Time + +func (jt *JSONUnix) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if s == "" { + return nil + } + unix, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return err + } + t = time.Unix(unix, 0) + *jt = JSONUnix(t) + return nil +} + +// RFC3339NoZ is the time format used in Heat (Orchestration). +const RFC3339NoZ = "2006-01-02T15:04:05" + +type JSONRFC3339NoZ time.Time + +func (jt *JSONRFC3339NoZ) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if s == "" { + return nil + } + t, err := time.Parse(RFC3339NoZ, s) + if err != nil { + return err + } + *jt = JSONRFC3339NoZ(t) + return nil +} + +/* +Link is an internal type to be used in packages of collection resources that are +paginated in a certain way. + +It's a response substructure common to many paginated collection results that is +used to point to related pages. Usually, the one we care about is the one with +Rel field set to "next". +*/ +type Link struct { + Href string `json:"href"` + Rel string `json:"rel"` +} + +/* +ExtractNextURL is an internal function useful for packages of collection +resources that are paginated in a certain way. + +It attempts to extract the "next" URL from slice of Link structs, or +"" if no such URL is present. +*/ +func ExtractNextURL(links []Link) (string, error) { + var url string + + for _, l := range links { + if l.Rel == "next" { + url = l.Href + } + } + + if url == "" { + return "", nil + } + + return url, nil +} diff --git a/vendor/github.com/gophercloud/gophercloud/script/acceptancetest b/vendor/github.com/gophercloud/gophercloud/script/acceptancetest new file mode 100755 index 0000000000..9debd48b62 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/script/acceptancetest @@ -0,0 +1,5 @@ +#!/bin/bash +# +# Run the acceptance tests. + +exec go test -p=1 github.com/gophercloud/gophercloud/acceptance/... $@ diff --git a/vendor/github.com/gophercloud/gophercloud/script/acceptancetest_environments/keystonev2-lbaasv1.sh b/vendor/github.com/gophercloud/gophercloud/script/acceptancetest_environments/keystonev2-lbaasv1.sh new file mode 100644 index 0000000000..c74db62477 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/script/acceptancetest_environments/keystonev2-lbaasv1.sh @@ -0,0 +1,194 @@ +#!/bin/bash +# +# This script is useful for creating a devstack environment to run gophercloud +# acceptance tests on. +# +# This can be considered a "legacy" devstack environment since it uses +# Keystone v2 and LBaaS v1. +# +# To run, simply execute this script within a virtual machine. +# +# The following OpenStack versions are installed: +# * OpenStack Mitaka +# * Keystone v2 +# * Glance v1 and v2 +# * Nova v2 and v2.1 +# * Cinder v1 and v2 +# * Trove v1 +# * Swift v1 +# * Neutron v2 +# * Neutron LBaaS v1.0 +# * Neutron FWaaS v2.0 +# * Manila v2 +# +# Go 1.6 is also installed. + +set -e + +cd +sudo apt-get update +sudo apt-get install -y git make mercurial + +sudo wget -O /usr/local/bin/gimme https://raw.githubusercontent.com/travis-ci/gimme/master/gimme +sudo chmod +x /usr/local/bin/gimme +gimme 1.6 >> .bashrc + +mkdir ~/go +eval "$(/usr/local/bin/gimme 1.6)" +echo 'export GOPATH=$HOME/go' >> .bashrc +export GOPATH=$HOME/go +source .bashrc + +go get golang.org/x/crypto/ssh +go get github.com/gophercloud/gophercloud + +git clone https://git.openstack.org/openstack-dev/devstack -b stable/mitaka +cd devstack +cat >local.conf <> openrc +echo export OS_IMAGE_ID="$_IMAGE_ID" >> openrc +echo export OS_NETWORK_ID=$_NETWORK_ID >> openrc +echo export OS_EXTGW_ID=$_EXTGW_ID >> openrc +echo export OS_POOL_NAME="public" >> openrc +echo export OS_FLAVOR_ID=99 >> openrc +echo export OS_FLAVOR_ID_RESIZE=98 >> openrc + +# Manila share-network needs to be created +_IDTOVALUE="-F id -f value" +_NEUTRON_NET_ID=$(neutron net-list --name private $_IDTOVALUE) +_NEUTRON_IPV4_SUB=$(neutron subnet-list \ + --ip_version 4 \ + --network_id "$_NEUTRON_NET_ID" \ + $_IDTOVALUE) + +manila share-network-create \ + --neutron-net-id "$_NEUTRON_NET_ID" \ + --neutron-subnet-id "$_NEUTRON_IPV4_SUB" \ + --name "acc_share_nw" + +_SHARE_NETWORK=$(manila share-network-list \ + --neutron-net-id "$_NEUTRON_NET_ID" \ + --neutron-subnet-id "$_NEUTRON_IPV4_SUB" \ + --name "acc_share_nw" \ + | awk 'FNR == 4 {print $2}') + +echo export OS_SHARE_NETWORK_ID="$_SHARE_NETWORK" >> openrc +source openrc demo diff --git a/vendor/github.com/gophercloud/gophercloud/script/acceptancetest_environments/keystonev3-lbaasv2.sh b/vendor/github.com/gophercloud/gophercloud/script/acceptancetest_environments/keystonev3-lbaasv2.sh new file mode 100644 index 0000000000..5cc9212ddc --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/script/acceptancetest_environments/keystonev3-lbaasv2.sh @@ -0,0 +1,208 @@ +#!/bin/bash +# +# This script is useful for creating a devstack environment to run gophercloud +# acceptance tests on. +# +# To run, simply execute this script within a virtual machine. +# +# The following OpenStack versions are installed: +# * OpenStack Mitaka +# * Keystone v3 +# * Glance v1 and v2 +# * Nova v2 and v2.1 +# * Cinder v1 and v2 +# * Trove v1 +# * Swift v1 +# * Neutron v2 +# * Neutron LBaaS v2.0 +# * Neutron FWaaS v2.0 +# +# Go 1.6 is also installed. + +set -e + +cd +sudo apt-get update +sudo apt-get install -y git make mercurial + +sudo wget -O /usr/local/bin/gimme https://raw.githubusercontent.com/travis-ci/gimme/master/gimme +sudo chmod +x /usr/local/bin/gimme +gimme 1.6 >> .bashrc + +mkdir ~/go +eval "$(/usr/local/bin/gimme 1.6)" +echo 'export GOPATH=$HOME/go' >> .bashrc +export GOPATH=$HOME/go + +export PATH=$PATH:$HOME/terraform:$HOME/go/bin +echo 'export PATH=$PATH:$HOME/terraform:$HOME/go/bin' >> .bashrc +source .bashrc + +go get golang.org/x/crypto/ssh +go get github.com/gophercloud/gophercloud + +git clone https://git.openstack.org/openstack-dev/devstack -b stable/mitaka +cd devstack +cat >local.conf <> openrc <> openrc +echo export OS_IMAGE_ID="$_IMAGE_ID" >> openrc +echo export OS_NETWORK_ID=$_NETWORK_ID >> openrc +echo export OS_EXTGW_ID=$_EXTGW_ID >> openrc +echo export OS_POOL_NAME="public" >> openrc +echo export OS_FLAVOR_ID=99 >> openrc +echo export OS_FLAVOR_ID_RESIZE=98 >> openrc +source openrc demo diff --git a/vendor/github.com/gophercloud/gophercloud/script/bootstrap b/vendor/github.com/gophercloud/gophercloud/script/bootstrap new file mode 100755 index 0000000000..78a195dcf7 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/script/bootstrap @@ -0,0 +1,25 @@ +#!/bin/bash +# +# This script helps new contributors set up their local workstation for +# gophercloud development and contributions. + +# Create the environment +export GOPATH=$HOME/go/gophercloud +mkdir -p $GOPATH + +# Download gophercloud into that environment +go get github.com/gophercloud/gophercloud +cd $GOPATH/src/github.com/gophercloud/gophercloud +git checkout master + +# Write out the env.sh convenience file. +cd $GOPATH +cat <env.sh +#!/bin/bash +export GOPATH=$(pwd) +export GOPHERCLOUD=$GOPATH/src/github.com/gophercloud/gophercloud +EOF +chmod a+x env.sh + +# Make changes immediately available as a convenience. +. ./env.sh diff --git a/vendor/github.com/gophercloud/gophercloud/script/cibuild b/vendor/github.com/gophercloud/gophercloud/script/cibuild new file mode 100755 index 0000000000..1cb389e7dc --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/script/cibuild @@ -0,0 +1,5 @@ +#!/bin/bash +# +# Test script to be invoked by Travis. + +exec script/unittest -v diff --git a/vendor/github.com/gophercloud/gophercloud/script/coverage b/vendor/github.com/gophercloud/gophercloud/script/coverage new file mode 100755 index 0000000000..3efa81ba5a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/script/coverage @@ -0,0 +1,12 @@ +#!/bin/bash + +set -e + +n=1 +for testpkg in $(go list ./testing ./.../testing); do + covpkg="${testpkg/"/testing"/}" + go test -covermode count -coverprofile "testing_"$n.coverprofile -coverpkg $covpkg $testpkg 2>/dev/null + n=$((n+1)) +done +gocovmerge `ls *.coverprofile` > cover.out +rm *.coverprofile diff --git a/vendor/github.com/gophercloud/gophercloud/script/format b/vendor/github.com/gophercloud/gophercloud/script/format new file mode 100755 index 0000000000..8ed602fde0 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/script/format @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +goimports="goimports" + +find_files() { + find . -not \( \ + \( \ + -wholename './output' \ + -o -wholename './_output' \ + -o -wholename './_gopath' \ + -o -wholename './release' \ + -o -wholename './target' \ + -o -wholename '*/third_party/*' \ + -o -wholename '*/vendor/*' \ + \) -prune \ + \) -name '*.go' +} + +diff=$(find_files | xargs ${goimports} -d -e 2>&1) +if [[ -n "${diff}" ]]; then + echo "${diff}" + exit 1 +fi diff --git a/vendor/github.com/gophercloud/gophercloud/script/test b/vendor/github.com/gophercloud/gophercloud/script/test new file mode 100755 index 0000000000..1e03dff8ab --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/script/test @@ -0,0 +1,5 @@ +#!/bin/bash +# +# Run all the tests. + +exec go test -tags 'acceptance fixtures' ./... $@ diff --git a/vendor/github.com/gophercloud/gophercloud/script/unittest b/vendor/github.com/gophercloud/gophercloud/script/unittest new file mode 100755 index 0000000000..2c65d06034 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/script/unittest @@ -0,0 +1,5 @@ +#!/bin/bash +# +# Run the unit tests. + +exec go test ./testing ./.../testing $@ diff --git a/vendor/github.com/gophercloud/gophercloud/service_client.go b/vendor/github.com/gophercloud/gophercloud/service_client.go new file mode 100644 index 0000000000..1160fefa7c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/service_client.go @@ -0,0 +1,122 @@ +package gophercloud + +import ( + "io" + "net/http" + "strings" +) + +// ServiceClient stores details required to interact with a specific service API implemented by a provider. +// Generally, you'll acquire these by calling the appropriate `New` method on a ProviderClient. +type ServiceClient struct { + // ProviderClient is a reference to the provider that implements this service. + *ProviderClient + + // Endpoint is the base URL of the service's API, acquired from a service catalog. + // It MUST end with a /. + Endpoint string + + // ResourceBase is the base URL shared by the resources within a service's API. It should include + // the API version and, like Endpoint, MUST end with a / if set. If not set, the Endpoint is used + // as-is, instead. + ResourceBase string + + // This is the service client type (e.g. compute, sharev2). + // NOTE: FOR INTERNAL USE ONLY. DO NOT SET. GOPHERCLOUD WILL SET THIS. + // It is only exported because it gets set in a different package. + Type string + + // The microversion of the service to use. Set this to use a particular microversion. + Microversion string +} + +// ResourceBaseURL returns the base URL of any resources used by this service. It MUST end with a /. +func (client *ServiceClient) ResourceBaseURL() string { + if client.ResourceBase != "" { + return client.ResourceBase + } + return client.Endpoint +} + +// ServiceURL constructs a URL for a resource belonging to this provider. +func (client *ServiceClient) ServiceURL(parts ...string) string { + return client.ResourceBaseURL() + strings.Join(parts, "/") +} + +func (client *ServiceClient) initReqOpts(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) { + if v, ok := (JSONBody).(io.Reader); ok { + opts.RawBody = v + } else if JSONBody != nil { + opts.JSONBody = JSONBody + } + + if JSONResponse != nil { + opts.JSONResponse = JSONResponse + } + + if opts.MoreHeaders == nil { + opts.MoreHeaders = make(map[string]string) + } + + if client.Microversion != "" { + client.setMicroversionHeader(opts) + } +} + +// Get calls `Request` with the "GET" HTTP verb. +func (client *ServiceClient) Get(url string, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = new(RequestOpts) + } + client.initReqOpts(url, nil, JSONResponse, opts) + return client.Request("GET", url, opts) +} + +// Post calls `Request` with the "POST" HTTP verb. +func (client *ServiceClient) Post(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = new(RequestOpts) + } + client.initReqOpts(url, JSONBody, JSONResponse, opts) + return client.Request("POST", url, opts) +} + +// Put calls `Request` with the "PUT" HTTP verb. +func (client *ServiceClient) Put(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = new(RequestOpts) + } + client.initReqOpts(url, JSONBody, JSONResponse, opts) + return client.Request("PUT", url, opts) +} + +// Patch calls `Request` with the "PATCH" HTTP verb. +func (client *ServiceClient) Patch(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = new(RequestOpts) + } + client.initReqOpts(url, JSONBody, JSONResponse, opts) + return client.Request("PATCH", url, opts) +} + +// Delete calls `Request` with the "DELETE" HTTP verb. +func (client *ServiceClient) Delete(url string, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = new(RequestOpts) + } + client.initReqOpts(url, nil, nil, opts) + return client.Request("DELETE", url, opts) +} + +func (client *ServiceClient) setMicroversionHeader(opts *RequestOpts) { + switch client.Type { + case "compute": + opts.MoreHeaders["X-OpenStack-Nova-API-Version"] = client.Microversion + case "sharev2": + opts.MoreHeaders["X-OpenStack-Manila-API-Version"] = client.Microversion + } + + if client.Type != "" { + opts.MoreHeaders["OpenStack-API-Version"] = client.Type + " " + client.Microversion + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/testhelper/client/fake.go b/vendor/github.com/gophercloud/gophercloud/testhelper/client/fake.go new file mode 100644 index 0000000000..3d81cc97b9 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/testhelper/client/fake.go @@ -0,0 +1,17 @@ +package client + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/testhelper" +) + +// Fake token to use. +const TokenID = "cbc36478b0bd8e67e89469c7749d4127" + +// ServiceClient returns a generic service client for use in tests. +func ServiceClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{ + ProviderClient: &gophercloud.ProviderClient{TokenID: TokenID}, + Endpoint: testhelper.Endpoint(), + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/testhelper/convenience.go b/vendor/github.com/gophercloud/gophercloud/testhelper/convenience.go new file mode 100644 index 0000000000..25f6720e82 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/testhelper/convenience.go @@ -0,0 +1,348 @@ +package testhelper + +import ( + "bytes" + "encoding/json" + "fmt" + "path/filepath" + "reflect" + "runtime" + "strings" + "testing" +) + +const ( + logBodyFmt = "\033[1;31m%s %s\033[0m" + greenCode = "\033[0m\033[1;32m" + yellowCode = "\033[0m\033[1;33m" + resetCode = "\033[0m\033[1;31m" +) + +func prefix(depth int) string { + _, file, line, _ := runtime.Caller(depth) + return fmt.Sprintf("Failure in %s, line %d:", filepath.Base(file), line) +} + +func green(str interface{}) string { + return fmt.Sprintf("%s%#v%s", greenCode, str, resetCode) +} + +func yellow(str interface{}) string { + return fmt.Sprintf("%s%#v%s", yellowCode, str, resetCode) +} + +func logFatal(t *testing.T, str string) { + t.Fatalf(logBodyFmt, prefix(3), str) +} + +func logError(t *testing.T, str string) { + t.Errorf(logBodyFmt, prefix(3), str) +} + +type diffLogger func([]string, interface{}, interface{}) + +type visit struct { + a1 uintptr + a2 uintptr + typ reflect.Type +} + +// Recursively visits the structures of "expected" and "actual". The diffLogger function will be +// invoked with each different value encountered, including the reference path that was followed +// to get there. +func deepDiffEqual(expected, actual reflect.Value, visited map[visit]bool, path []string, logDifference diffLogger) { + defer func() { + // Fall back to the regular reflect.DeepEquals function. + if r := recover(); r != nil { + var e, a interface{} + if expected.IsValid() { + e = expected.Interface() + } + if actual.IsValid() { + a = actual.Interface() + } + + if !reflect.DeepEqual(e, a) { + logDifference(path, e, a) + } + } + }() + + if !expected.IsValid() && actual.IsValid() { + logDifference(path, nil, actual.Interface()) + return + } + if expected.IsValid() && !actual.IsValid() { + logDifference(path, expected.Interface(), nil) + return + } + if !expected.IsValid() && !actual.IsValid() { + return + } + + hard := func(k reflect.Kind) bool { + switch k { + case reflect.Array, reflect.Map, reflect.Slice, reflect.Struct: + return true + } + return false + } + + if expected.CanAddr() && actual.CanAddr() && hard(expected.Kind()) { + addr1 := expected.UnsafeAddr() + addr2 := actual.UnsafeAddr() + + if addr1 > addr2 { + addr1, addr2 = addr2, addr1 + } + + if addr1 == addr2 { + // References are identical. We can short-circuit + return + } + + typ := expected.Type() + v := visit{addr1, addr2, typ} + if visited[v] { + // Already visited. + return + } + + // Remember this visit for later. + visited[v] = true + } + + switch expected.Kind() { + case reflect.Array: + for i := 0; i < expected.Len(); i++ { + hop := append(path, fmt.Sprintf("[%d]", i)) + deepDiffEqual(expected.Index(i), actual.Index(i), visited, hop, logDifference) + } + return + case reflect.Slice: + if expected.IsNil() != actual.IsNil() { + logDifference(path, expected.Interface(), actual.Interface()) + return + } + if expected.Len() == actual.Len() && expected.Pointer() == actual.Pointer() { + return + } + for i := 0; i < expected.Len(); i++ { + hop := append(path, fmt.Sprintf("[%d]", i)) + deepDiffEqual(expected.Index(i), actual.Index(i), visited, hop, logDifference) + } + return + case reflect.Interface: + if expected.IsNil() != actual.IsNil() { + logDifference(path, expected.Interface(), actual.Interface()) + return + } + deepDiffEqual(expected.Elem(), actual.Elem(), visited, path, logDifference) + return + case reflect.Ptr: + deepDiffEqual(expected.Elem(), actual.Elem(), visited, path, logDifference) + return + case reflect.Struct: + for i, n := 0, expected.NumField(); i < n; i++ { + field := expected.Type().Field(i) + hop := append(path, "."+field.Name) + deepDiffEqual(expected.Field(i), actual.Field(i), visited, hop, logDifference) + } + return + case reflect.Map: + if expected.IsNil() != actual.IsNil() { + logDifference(path, expected.Interface(), actual.Interface()) + return + } + if expected.Len() == actual.Len() && expected.Pointer() == actual.Pointer() { + return + } + + var keys []reflect.Value + if expected.Len() >= actual.Len() { + keys = expected.MapKeys() + } else { + keys = actual.MapKeys() + } + + for _, k := range keys { + expectedValue := expected.MapIndex(k) + actualValue := actual.MapIndex(k) + + if !expectedValue.IsValid() { + logDifference(path, nil, actual.Interface()) + return + } + if !actualValue.IsValid() { + logDifference(path, expected.Interface(), nil) + return + } + + hop := append(path, fmt.Sprintf("[%v]", k)) + deepDiffEqual(expectedValue, actualValue, visited, hop, logDifference) + } + return + case reflect.Func: + if expected.IsNil() != actual.IsNil() { + logDifference(path, expected.Interface(), actual.Interface()) + } + return + default: + if expected.Interface() != actual.Interface() { + logDifference(path, expected.Interface(), actual.Interface()) + } + } +} + +func deepDiff(expected, actual interface{}, logDifference diffLogger) { + if expected == nil || actual == nil { + logDifference([]string{}, expected, actual) + return + } + + expectedValue := reflect.ValueOf(expected) + actualValue := reflect.ValueOf(actual) + + if expectedValue.Type() != actualValue.Type() { + logDifference([]string{}, expected, actual) + return + } + deepDiffEqual(expectedValue, actualValue, map[visit]bool{}, []string{}, logDifference) +} + +// AssertEquals compares two arbitrary values and performs a comparison. If the +// comparison fails, a fatal error is raised that will fail the test +func AssertEquals(t *testing.T, expected, actual interface{}) { + if expected != actual { + logFatal(t, fmt.Sprintf("expected %s but got %s", green(expected), yellow(actual))) + } +} + +// CheckEquals is similar to AssertEquals, except with a non-fatal error +func CheckEquals(t *testing.T, expected, actual interface{}) { + if expected != actual { + logError(t, fmt.Sprintf("expected %s but got %s", green(expected), yellow(actual))) + } +} + +// AssertDeepEquals - like Equals - performs a comparison - but on more complex +// structures that requires deeper inspection +func AssertDeepEquals(t *testing.T, expected, actual interface{}) { + pre := prefix(2) + + differed := false + deepDiff(expected, actual, func(path []string, expected, actual interface{}) { + differed = true + t.Errorf("\033[1;31m%sat %s expected %s, but got %s\033[0m", + pre, + strings.Join(path, ""), + green(expected), + yellow(actual)) + }) + if differed { + logFatal(t, "The structures were different.") + } +} + +// CheckDeepEquals is similar to AssertDeepEquals, except with a non-fatal error +func CheckDeepEquals(t *testing.T, expected, actual interface{}) { + pre := prefix(2) + + deepDiff(expected, actual, func(path []string, expected, actual interface{}) { + t.Errorf("\033[1;31m%s at %s expected %s, but got %s\033[0m", + pre, + strings.Join(path, ""), + green(expected), + yellow(actual)) + }) +} + +func isByteArrayEquals(t *testing.T, expectedBytes []byte, actualBytes []byte) bool { + return bytes.Equal(expectedBytes, actualBytes) +} + +// AssertByteArrayEquals a convenience function for checking whether two byte arrays are equal +func AssertByteArrayEquals(t *testing.T, expectedBytes []byte, actualBytes []byte) { + if !isByteArrayEquals(t, expectedBytes, actualBytes) { + logFatal(t, "The bytes differed.") + } +} + +// CheckByteArrayEquals a convenience function for silent checking whether two byte arrays are equal +func CheckByteArrayEquals(t *testing.T, expectedBytes []byte, actualBytes []byte) { + if !isByteArrayEquals(t, expectedBytes, actualBytes) { + logError(t, "The bytes differed.") + } +} + +// isJSONEquals is a utility function that implements JSON comparison for AssertJSONEquals and +// CheckJSONEquals. +func isJSONEquals(t *testing.T, expectedJSON string, actual interface{}) bool { + var parsedExpected, parsedActual interface{} + err := json.Unmarshal([]byte(expectedJSON), &parsedExpected) + if err != nil { + t.Errorf("Unable to parse expected value as JSON: %v", err) + return false + } + + jsonActual, err := json.Marshal(actual) + AssertNoErr(t, err) + err = json.Unmarshal(jsonActual, &parsedActual) + AssertNoErr(t, err) + + if !reflect.DeepEqual(parsedExpected, parsedActual) { + prettyExpected, err := json.MarshalIndent(parsedExpected, "", " ") + if err != nil { + t.Logf("Unable to pretty-print expected JSON: %v\n%s", err, expectedJSON) + } else { + // We can't use green() here because %#v prints prettyExpected as a byte array literal, which + // is... unhelpful. Converting it to a string first leaves "\n" uninterpreted for some reason. + t.Logf("Expected JSON:\n%s%s%s", greenCode, prettyExpected, resetCode) + } + + prettyActual, err := json.MarshalIndent(actual, "", " ") + if err != nil { + t.Logf("Unable to pretty-print actual JSON: %v\n%#v", err, actual) + } else { + // We can't use yellow() for the same reason. + t.Logf("Actual JSON:\n%s%s%s", yellowCode, prettyActual, resetCode) + } + + return false + } + return true +} + +// AssertJSONEquals serializes a value as JSON, parses an expected string as JSON, and ensures that +// both are consistent. If they aren't, the expected and actual structures are pretty-printed and +// shown for comparison. +// +// This is useful for comparing structures that are built as nested map[string]interface{} values, +// which are a pain to construct as literals. +func AssertJSONEquals(t *testing.T, expectedJSON string, actual interface{}) { + if !isJSONEquals(t, expectedJSON, actual) { + logFatal(t, "The generated JSON structure differed.") + } +} + +// CheckJSONEquals is similar to AssertJSONEquals, but nonfatal. +func CheckJSONEquals(t *testing.T, expectedJSON string, actual interface{}) { + if !isJSONEquals(t, expectedJSON, actual) { + logError(t, "The generated JSON structure differed.") + } +} + +// AssertNoErr is a convenience function for checking whether an error value is +// an actual error +func AssertNoErr(t *testing.T, e error) { + if e != nil { + logFatal(t, fmt.Sprintf("unexpected error %s", yellow(e.Error()))) + } +} + +// CheckNoErr is similar to AssertNoErr, except with a non-fatal error +func CheckNoErr(t *testing.T, e error) { + if e != nil { + logError(t, fmt.Sprintf("unexpected error %s", yellow(e.Error()))) + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/testhelper/doc.go b/vendor/github.com/gophercloud/gophercloud/testhelper/doc.go new file mode 100644 index 0000000000..25b4dfebbb --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/testhelper/doc.go @@ -0,0 +1,4 @@ +/* +Package testhelper container methods that are useful for writing unit tests. +*/ +package testhelper diff --git a/vendor/github.com/gophercloud/gophercloud/testhelper/fixture/helper.go b/vendor/github.com/gophercloud/gophercloud/testhelper/fixture/helper.go new file mode 100644 index 0000000000..fe98c86f99 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/testhelper/fixture/helper.go @@ -0,0 +1,31 @@ +package fixture + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func SetupHandler(t *testing.T, url, method, requestBody, responseBody string, status int) { + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, method) + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + if requestBody != "" { + th.TestJSONRequest(t, r, requestBody) + } + + if responseBody != "" { + w.Header().Add("Content-Type", "application/json") + } + + w.WriteHeader(status) + + if responseBody != "" { + fmt.Fprintf(w, responseBody) + } + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/testhelper/http_responses.go b/vendor/github.com/gophercloud/gophercloud/testhelper/http_responses.go new file mode 100644 index 0000000000..e1f1f9ac0e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/testhelper/http_responses.go @@ -0,0 +1,91 @@ +package testhelper + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "reflect" + "testing" +) + +var ( + // Mux is a multiplexer that can be used to register handlers. + Mux *http.ServeMux + + // Server is an in-memory HTTP server for testing. + Server *httptest.Server +) + +// SetupHTTP prepares the Mux and Server. +func SetupHTTP() { + Mux = http.NewServeMux() + Server = httptest.NewServer(Mux) +} + +// TeardownHTTP releases HTTP-related resources. +func TeardownHTTP() { + Server.Close() +} + +// Endpoint returns a fake endpoint that will actually target the Mux. +func Endpoint() string { + return Server.URL + "/" +} + +// TestFormValues ensures that all the URL parameters given to the http.Request are the same as values. +func TestFormValues(t *testing.T, r *http.Request, values map[string]string) { + want := url.Values{} + for k, v := range values { + want.Add(k, v) + } + + r.ParseForm() + if !reflect.DeepEqual(want, r.Form) { + t.Errorf("Request parameters = %v, want %v", r.Form, want) + } +} + +// TestMethod checks that the Request has the expected method (e.g. GET, POST). +func TestMethod(t *testing.T, r *http.Request, expected string) { + if expected != r.Method { + t.Errorf("Request method = %v, expected %v", r.Method, expected) + } +} + +// TestHeader checks that the header on the http.Request matches the expected value. +func TestHeader(t *testing.T, r *http.Request, header string, expected string) { + if actual := r.Header.Get(header); expected != actual { + t.Errorf("Header %s = %s, expected %s", header, actual, expected) + } +} + +// TestBody verifies that the request body matches an expected body. +func TestBody(t *testing.T, r *http.Request, expected string) { + b, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Errorf("Unable to read body: %v", err) + } + str := string(b) + if expected != str { + t.Errorf("Body = %s, expected %s", str, expected) + } +} + +// TestJSONRequest verifies that the JSON payload of a request matches an expected structure, without asserting things about +// whitespace or ordering. +func TestJSONRequest(t *testing.T, r *http.Request, expected string) { + b, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Errorf("Unable to read request body: %v", err) + } + + var actualJSON interface{} + err = json.Unmarshal(b, &actualJSON) + if err != nil { + t.Errorf("Unable to parse request body as JSON: %v", err) + } + + CheckJSONEquals(t, expected, actualJSON) +} diff --git a/vendor/github.com/gophercloud/gophercloud/testing/doc.go b/vendor/github.com/gophercloud/gophercloud/testing/doc.go new file mode 100644 index 0000000000..244a62e323 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/testing/doc.go @@ -0,0 +1,2 @@ +// gophercloud +package testing diff --git a/vendor/github.com/gophercloud/gophercloud/testing/endpoint_search_test.go b/vendor/github.com/gophercloud/gophercloud/testing/endpoint_search_test.go new file mode 100644 index 0000000000..22476cbb14 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/testing/endpoint_search_test.go @@ -0,0 +1,20 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestApplyDefaultsToEndpointOpts(t *testing.T) { + eo := gophercloud.EndpointOpts{Availability: gophercloud.AvailabilityPublic} + eo.ApplyDefaults("compute") + expected := gophercloud.EndpointOpts{Availability: gophercloud.AvailabilityPublic, Type: "compute"} + th.CheckDeepEquals(t, expected, eo) + + eo = gophercloud.EndpointOpts{Type: "compute"} + eo.ApplyDefaults("object-store") + expected = gophercloud.EndpointOpts{Availability: gophercloud.AvailabilityPublic, Type: "compute"} + th.CheckDeepEquals(t, expected, eo) +} diff --git a/vendor/github.com/gophercloud/gophercloud/testing/params_test.go b/vendor/github.com/gophercloud/gophercloud/testing/params_test.go new file mode 100644 index 0000000000..8757079c2a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/testing/params_test.go @@ -0,0 +1,254 @@ +package testing + +import ( + "net/url" + "reflect" + "testing" + + "github.com/gophercloud/gophercloud" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestMaybeString(t *testing.T) { + testString := "" + var expected *string + actual := gophercloud.MaybeString(testString) + th.CheckDeepEquals(t, expected, actual) + + testString = "carol" + expected = &testString + actual = gophercloud.MaybeString(testString) + th.CheckDeepEquals(t, expected, actual) +} + +func TestMaybeInt(t *testing.T) { + testInt := 0 + var expected *int + actual := gophercloud.MaybeInt(testInt) + th.CheckDeepEquals(t, expected, actual) + + testInt = 4 + expected = &testInt + actual = gophercloud.MaybeInt(testInt) + th.CheckDeepEquals(t, expected, actual) +} + +func TestBuildQueryString(t *testing.T) { + type testVar string + iFalse := false + opts := struct { + J int `q:"j"` + R string `q:"r,required"` + C bool `q:"c"` + S []string `q:"s"` + TS []testVar `q:"ts"` + TI []int `q:"ti"` + F *bool `q:"f"` + }{ + J: 2, + R: "red", + C: true, + S: []string{"one", "two", "three"}, + TS: []testVar{"a", "b"}, + TI: []int{1, 2}, + F: &iFalse, + } + expected := &url.URL{RawQuery: "c=true&f=false&j=2&r=red&s=one&s=two&s=three&ti=1&ti=2&ts=a&ts=b"} + actual, err := gophercloud.BuildQueryString(&opts) + if err != nil { + t.Errorf("Error building query string: %v", err) + } + th.CheckDeepEquals(t, expected, actual) + + opts = struct { + J int `q:"j"` + R string `q:"r,required"` + C bool `q:"c"` + S []string `q:"s"` + TS []testVar `q:"ts"` + TI []int `q:"ti"` + F *bool `q:"f"` + }{ + J: 2, + C: true, + } + _, err = gophercloud.BuildQueryString(&opts) + if err == nil { + t.Errorf("Expected error: 'Required field not set'") + } + th.CheckDeepEquals(t, expected, actual) + + _, err = gophercloud.BuildQueryString(map[string]interface{}{"Number": 4}) + if err == nil { + t.Errorf("Expected error: 'Options type is not a struct'") + } +} + +func TestBuildHeaders(t *testing.T) { + testStruct := struct { + Accept string `h:"Accept"` + Num int `h:"Number,required"` + Style bool `h:"Style"` + }{ + Accept: "application/json", + Num: 4, + Style: true, + } + expected := map[string]string{"Accept": "application/json", "Number": "4", "Style": "true"} + actual, err := gophercloud.BuildHeaders(&testStruct) + th.CheckNoErr(t, err) + th.CheckDeepEquals(t, expected, actual) + + testStruct.Num = 0 + _, err = gophercloud.BuildHeaders(&testStruct) + if err == nil { + t.Errorf("Expected error: 'Required header not set'") + } + + _, err = gophercloud.BuildHeaders(map[string]interface{}{"Number": 4}) + if err == nil { + t.Errorf("Expected error: 'Options type is not a struct'") + } +} + +func TestQueriesAreEscaped(t *testing.T) { + type foo struct { + Name string `q:"something"` + Shape string `q:"else"` + } + + expected := &url.URL{RawQuery: "else=Triangl+e&something=blah%2B%3F%21%21foo"} + + actual, err := gophercloud.BuildQueryString(foo{Name: "blah+?!!foo", Shape: "Triangl e"}) + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, expected, actual) +} + +func TestBuildRequestBody(t *testing.T) { + type PasswordCredentials struct { + Username string `json:"username" required:"true"` + Password string `json:"password" required:"true"` + } + + type TokenCredentials struct { + ID string `json:"id,omitempty" required:"true"` + } + + type orFields struct { + Filler int `json:"filler,omitempty"` + F1 int `json:"f1,omitempty" or:"F2"` + F2 int `json:"f2,omitempty" or:"F1"` + } + + // AuthOptions wraps a gophercloud AuthOptions in order to adhere to the AuthOptionsBuilder + // interface. + type AuthOptions struct { + PasswordCredentials *PasswordCredentials `json:"passwordCredentials,omitempty" xor:"TokenCredentials"` + + // The TenantID and TenantName fields are optional for the Identity V2 API. + // Some providers allow you to specify a TenantName instead of the TenantId. + // Some require both. Your provider's authentication policies will determine + // how these fields influence authentication. + TenantID string `json:"tenantId,omitempty"` + TenantName string `json:"tenantName,omitempty"` + + // TokenCredentials allows users to authenticate (possibly as another user) with an + // authentication token ID. + TokenCredentials *TokenCredentials `json:"token,omitempty" xor:"PasswordCredentials"` + + OrFields *orFields `json:"or_fields,omitempty"` + } + + var successCases = []struct { + opts AuthOptions + expected map[string]interface{} + }{ + { + AuthOptions{ + PasswordCredentials: &PasswordCredentials{ + Username: "me", + Password: "swordfish", + }, + }, + map[string]interface{}{ + "auth": map[string]interface{}{ + "passwordCredentials": map[string]interface{}{ + "password": "swordfish", + "username": "me", + }, + }, + }, + }, + { + AuthOptions{ + TokenCredentials: &TokenCredentials{ + ID: "1234567", + }, + }, + map[string]interface{}{ + "auth": map[string]interface{}{ + "token": map[string]interface{}{ + "id": "1234567", + }, + }, + }, + }, + } + + for _, successCase := range successCases { + actual, err := gophercloud.BuildRequestBody(successCase.opts, "auth") + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, successCase.expected, actual) + } + + var failCases = []struct { + opts AuthOptions + expected error + }{ + { + AuthOptions{ + TenantID: "987654321", + TenantName: "me", + }, + gophercloud.ErrMissingInput{}, + }, + { + AuthOptions{ + TokenCredentials: &TokenCredentials{ + ID: "1234567", + }, + PasswordCredentials: &PasswordCredentials{ + Username: "me", + Password: "swordfish", + }, + }, + gophercloud.ErrMissingInput{}, + }, + { + AuthOptions{ + PasswordCredentials: &PasswordCredentials{ + Password: "swordfish", + }, + }, + gophercloud.ErrMissingInput{}, + }, + { + AuthOptions{ + PasswordCredentials: &PasswordCredentials{ + Username: "me", + Password: "swordfish", + }, + OrFields: &orFields{ + Filler: 2, + }, + }, + gophercloud.ErrMissingInput{}, + }, + } + + for _, failCase := range failCases { + _, err := gophercloud.BuildRequestBody(failCase.opts, "auth") + th.AssertDeepEquals(t, reflect.TypeOf(failCase.expected), reflect.TypeOf(err)) + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/testing/provider_client_test.go b/vendor/github.com/gophercloud/gophercloud/testing/provider_client_test.go new file mode 100644 index 0000000000..7c0e84eae6 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/testing/provider_client_test.go @@ -0,0 +1,36 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestAuthenticatedHeaders(t *testing.T) { + p := &gophercloud.ProviderClient{ + TokenID: "1234", + } + expected := map[string]string{"X-Auth-Token": "1234"} + actual := p.AuthenticatedHeaders() + th.CheckDeepEquals(t, expected, actual) +} + +func TestUserAgent(t *testing.T) { + p := &gophercloud.ProviderClient{} + + p.UserAgent.Prepend("custom-user-agent/2.4.0") + expected := "custom-user-agent/2.4.0 gophercloud/2.0.0" + actual := p.UserAgent.Join() + th.CheckEquals(t, expected, actual) + + p.UserAgent.Prepend("another-custom-user-agent/0.3.0", "a-third-ua/5.9.0") + expected = "another-custom-user-agent/0.3.0 a-third-ua/5.9.0 custom-user-agent/2.4.0 gophercloud/2.0.0" + actual = p.UserAgent.Join() + th.CheckEquals(t, expected, actual) + + p.UserAgent = gophercloud.UserAgent{} + expected = "gophercloud/2.0.0" + actual = p.UserAgent.Join() + th.CheckEquals(t, expected, actual) +} diff --git a/vendor/github.com/gophercloud/gophercloud/testing/service_client_test.go b/vendor/github.com/gophercloud/gophercloud/testing/service_client_test.go new file mode 100644 index 0000000000..904b303ee9 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/testing/service_client_test.go @@ -0,0 +1,15 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestServiceURL(t *testing.T) { + c := &gophercloud.ServiceClient{Endpoint: "http://123.45.67.8/"} + expected := "http://123.45.67.8/more/parts/here" + actual := c.ServiceURL("more", "parts", "here") + th.CheckEquals(t, expected, actual) +} diff --git a/vendor/github.com/gophercloud/gophercloud/testing/util_test.go b/vendor/github.com/gophercloud/gophercloud/testing/util_test.go new file mode 100644 index 0000000000..ae3e448fa9 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/testing/util_test.go @@ -0,0 +1,122 @@ +package testing + +import ( + "errors" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/gophercloud/gophercloud" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestWaitFor(t *testing.T) { + err := gophercloud.WaitFor(2, func() (bool, error) { + return true, nil + }) + th.CheckNoErr(t, err) +} + +func TestWaitForTimeout(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + + err := gophercloud.WaitFor(1, func() (bool, error) { + return false, nil + }) + th.AssertEquals(t, "A timeout occurred", err.Error()) +} + +func TestWaitForError(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + + err := gophercloud.WaitFor(2, func() (bool, error) { + return false, errors.New("Error has occurred") + }) + th.AssertEquals(t, "Error has occurred", err.Error()) +} + +func TestWaitForPredicateExceed(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + + err := gophercloud.WaitFor(1, func() (bool, error) { + time.Sleep(4 * time.Second) + return false, errors.New("Just wasting time") + }) + th.AssertEquals(t, "A timeout occurred", err.Error()) +} + +func TestNormalizeURL(t *testing.T) { + urls := []string{ + "NoSlashAtEnd", + "SlashAtEnd/", + } + expected := []string{ + "NoSlashAtEnd/", + "SlashAtEnd/", + } + for i := 0; i < len(expected); i++ { + th.CheckEquals(t, expected[i], gophercloud.NormalizeURL(urls[i])) + } + +} + +func TestNormalizePathURL(t *testing.T) { + baseDir, _ := os.Getwd() + + rawPath := "template.yaml" + basePath, _ := filepath.Abs(".") + result, _ := gophercloud.NormalizePathURL(basePath, rawPath) + expected := strings.Join([]string{"file:/", filepath.ToSlash(baseDir), "template.yaml"}, "/") + th.CheckEquals(t, expected, result) + + rawPath = "http://www.google.com" + basePath, _ = filepath.Abs(".") + result, _ = gophercloud.NormalizePathURL(basePath, rawPath) + expected = "http://www.google.com" + th.CheckEquals(t, expected, result) + + rawPath = "very/nested/file.yaml" + basePath, _ = filepath.Abs(".") + result, _ = gophercloud.NormalizePathURL(basePath, rawPath) + expected = strings.Join([]string{"file:/", filepath.ToSlash(baseDir), "very/nested/file.yaml"}, "/") + th.CheckEquals(t, expected, result) + + rawPath = "very/nested/file.yaml" + basePath = "http://www.google.com" + result, _ = gophercloud.NormalizePathURL(basePath, rawPath) + expected = "http://www.google.com/very/nested/file.yaml" + th.CheckEquals(t, expected, result) + + rawPath = "very/nested/file.yaml/" + basePath = "http://www.google.com/" + result, _ = gophercloud.NormalizePathURL(basePath, rawPath) + expected = "http://www.google.com/very/nested/file.yaml" + th.CheckEquals(t, expected, result) + + rawPath = "very/nested/file.yaml" + basePath = "http://www.google.com/even/more" + result, _ = gophercloud.NormalizePathURL(basePath, rawPath) + expected = "http://www.google.com/even/more/very/nested/file.yaml" + th.CheckEquals(t, expected, result) + + rawPath = "very/nested/file.yaml" + basePath = strings.Join([]string{"file:/", filepath.ToSlash(baseDir), "only/file/even/more"}, "/") + result, _ = gophercloud.NormalizePathURL(basePath, rawPath) + expected = strings.Join([]string{"file:/", filepath.ToSlash(baseDir), "only/file/even/more/very/nested/file.yaml"}, "/") + th.CheckEquals(t, expected, result) + + rawPath = "very/nested/file.yaml/" + basePath = strings.Join([]string{"file:/", filepath.ToSlash(baseDir), "only/file/even/more"}, "/") + result, _ = gophercloud.NormalizePathURL(basePath, rawPath) + expected = strings.Join([]string{"file:/", filepath.ToSlash(baseDir), "only/file/even/more/very/nested/file.yaml"}, "/") + th.CheckEquals(t, expected, result) + +} diff --git a/vendor/github.com/gophercloud/gophercloud/util.go b/vendor/github.com/gophercloud/gophercloud/util.go new file mode 100644 index 0000000000..68f9a5d3ec --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/util.go @@ -0,0 +1,102 @@ +package gophercloud + +import ( + "fmt" + "net/url" + "path/filepath" + "strings" + "time" +) + +// WaitFor polls a predicate function, once per second, up to a timeout limit. +// This is useful to wait for a resource to transition to a certain state. +// To handle situations when the predicate might hang indefinitely, the +// predicate will be prematurely cancelled after the timeout. +// Resource packages will wrap this in a more convenient function that's +// specific to a certain resource, but it can also be useful on its own. +func WaitFor(timeout int, predicate func() (bool, error)) error { + type WaitForResult struct { + Success bool + Error error + } + + start := time.Now().Unix() + + for { + // If a timeout is set, and that's been exceeded, shut it down. + if timeout >= 0 && time.Now().Unix()-start >= int64(timeout) { + return fmt.Errorf("A timeout occurred") + } + + time.Sleep(1 * time.Second) + + var result WaitForResult + ch := make(chan bool, 1) + go func() { + defer close(ch) + satisfied, err := predicate() + result.Success = satisfied + result.Error = err + }() + + select { + case <-ch: + if result.Error != nil { + return result.Error + } + if result.Success { + return nil + } + // If the predicate has not finished by the timeout, cancel it. + case <-time.After(time.Duration(timeout) * time.Second): + return fmt.Errorf("A timeout occurred") + } + } +} + +// NormalizeURL is an internal function to be used by provider clients. +// +// It ensures that each endpoint URL has a closing `/`, as expected by +// ServiceClient's methods. +func NormalizeURL(url string) string { + if !strings.HasSuffix(url, "/") { + return url + "/" + } + return url +} + +// NormalizePathURL is used to convert rawPath to a fqdn, using basePath as +// a reference in the filesystem, if necessary. basePath is assumed to contain +// either '.' when first used, or the file:// type fqdn of the parent resource. +// e.g. myFavScript.yaml => file://opt/lib/myFavScript.yaml +func NormalizePathURL(basePath, rawPath string) (string, error) { + u, err := url.Parse(rawPath) + if err != nil { + return "", err + } + // if a scheme is defined, it must be a fqdn already + if u.Scheme != "" { + return u.String(), nil + } + // if basePath is a url, then child resources are assumed to be relative to it + bu, err := url.Parse(basePath) + if err != nil { + return "", err + } + var basePathSys, absPathSys string + if bu.Scheme != "" { + basePathSys = filepath.FromSlash(bu.Path) + absPathSys = filepath.Join(basePathSys, rawPath) + bu.Path = filepath.ToSlash(absPathSys) + return bu.String(), nil + } + + absPathSys = filepath.Join(basePath, rawPath) + u.Path = filepath.ToSlash(absPathSys) + if err != nil { + return "", err + } + u.Scheme = "file" + return u.String(), nil + +}