From 73b7cac4b18576589d6e9cdfcb3b6cc58ce12ca7 Mon Sep 17 00:00:00 2001 From: Mike Fedosin Date: Tue, 1 Oct 2019 15:28:42 +0200 Subject: [PATCH 1/4] Bump github.com/gophercloud/gophercloud --- Gopkg.lock | 4 +- .../openstack/identity/v3/tokens/results.go | 9 +++ .../loadbalancer/v2/listeners/requests.go | 6 ++ .../loadbalancer/v2/listeners/results.go | 3 + .../loadbalancer/v2/loadbalancers/results.go | 46 +++++++++++ .../loadbalancer/v2/monitors/requests.go | 24 +----- .../extensions/layer3/floatingips/results.go | 46 +++++++++++ .../v2/extensions/security/groups/results.go | 46 +++++++++++ .../v2/extensions/subnetpools/results.go | 79 +++++++++++++++++-- .../networking/v2/networks/results.go | 46 +++++++++++ 10 files changed, 278 insertions(+), 31 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index fda2380b1dc..a6c22c902c5 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -336,7 +336,7 @@ [[projects]] branch = "master" - digest = "1:721fd76934b8bef9c9cb85ac5e4a1b8505347ce384f292eadaecfa1b33cb32e6" + digest = "1:1d318d726b1fe59a7719a571f28b899fd608bd8e7a7b09b88c7be6d96eea0852" name = "github.com/gophercloud/gophercloud" packages = [ ".", @@ -372,7 +372,7 @@ "pagination", ] pruneopts = "NUT" - revision = "157432124aab520975efedb5e54b95287785578d" + revision = "863d5406e68f374ee607452672b18a177134bbbc" [[projects]] branch = "master" 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 index 6f26c96bcdc..8af4d634cfa 100644 --- a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/results.go +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/results.go @@ -144,6 +144,15 @@ func (r commonResult) ExtractProject() (*Project, error) { return s.Project, err } +// ExtractDomain returns Domain to which User is authorized. +func (r commonResult) ExtractDomain() (*Domain, error) { + var s struct { + Domain *Domain `json:"domain"` + } + err := r.ExtractInto(&s) + return s.Domain, err +} + // CreateResult is the response from a Create request. Use ExtractToken() // to interpret it as a Token, or ExtractServiceCatalog() to interpret it // as a service catalog. diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners/requests.go index c512e4d72f6..36d00baf854 100644 --- a/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners/requests.go +++ b/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners/requests.go @@ -131,6 +131,9 @@ type CreateOpts struct { // A dictionary of optional headers to insert into the request before it is sent to the backend member. InsertHeaders map[string]string `json:"insert_headers,omitempty"` + + // A list of IPv4, IPv6 or mix of both CIDRs + AllowedCIDRs []string `json:"allowed_cidrs,omitempty"` } // ToListenerCreateMap builds a request body from CreateOpts. @@ -202,6 +205,9 @@ type UpdateOpts struct { // Time, in milliseconds, to wait for additional TCP packets for content inspection TimeoutTCPInspect *int `json:"timeout_tcp_inspect,omitempty"` + + // A list of IPv4, IPv6 or mix of both CIDRs + AllowedCIDRs []string `json:"allowed_cidrs,omitempty"` } // ToListenerUpdateMap builds a request body from UpdateOpts. diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners/results.go index a8f76821018..7bf6734ef7f 100644 --- a/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners/results.go +++ b/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners/results.go @@ -77,6 +77,9 @@ type Listener struct { // A dictionary of optional headers to insert into the request before it is sent to the backend member. InsertHeaders map[string]string `json:"insert_headers"` + + // A list of IPv4, IPv6 or mix of both CIDRs + AllowedCIDRs []string `json:"allowed_cidrs"` } type Stats struct { diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers/results.go index 73372938cf4..2cd54a61dfd 100644 --- a/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers/results.go +++ b/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers/results.go @@ -1,6 +1,9 @@ package loadbalancers import ( + "encoding/json" + "time" + "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners" "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools" @@ -21,6 +24,11 @@ type LoadBalancer struct { // Owner of the LoadBalancer. ProjectID string `json:"project_id"` + // UpdatedAt and CreatedAt contain ISO-8601 timestamps of when the state of the + // loadbalancer last changed, and when it was created. + UpdatedAt time.Time `json:"-"` + CreatedAt time.Time `json:"-"` + // The provisioning status of the LoadBalancer. // This value is ACTIVE, PENDING_CREATE or ERROR. ProvisioningStatus string `json:"provisioning_status"` @@ -65,6 +73,44 @@ type LoadBalancer struct { Tags []string `json:"tags"` } +func (r *LoadBalancer) UnmarshalJSON(b []byte) error { + type tmp LoadBalancer + + // Support for older neutron time format + var s1 struct { + tmp + CreatedAt gophercloud.JSONRFC3339NoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339NoZ `json:"updated_at"` + } + + err := json.Unmarshal(b, &s1) + if err == nil { + *r = LoadBalancer(s1.tmp) + r.CreatedAt = time.Time(s1.CreatedAt) + r.UpdatedAt = time.Time(s1.UpdatedAt) + + return nil + } + + // Support for newer neutron time format + var s2 struct { + tmp + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + } + + err = json.Unmarshal(b, &s2) + if err != nil { + return err + } + + *r = LoadBalancer(s2.tmp) + r.CreatedAt = time.Time(s2.CreatedAt) + r.UpdatedAt = time.Time(s2.UpdatedAt) + + return nil +} + // StatusTree represents the status of a loadbalancer. type StatusTree struct { Loadbalancer *LoadBalancer `json:"loadbalancer"` diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/monitors/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/monitors/requests.go index f728f5a8237..c05289504c7 100644 --- a/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/monitors/requests.go +++ b/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/monitors/requests.go @@ -108,7 +108,6 @@ type CreateOpts struct { MaxRetries int `json:"max_retries" required:"true"` // URI path that will be accessed if Monitor type is HTTP or HTTPS. - // Required for HTTP(S) types. URLPath string `json:"url_path,omitempty"` // The HTTP method used for requests by the Monitor. If this attribute @@ -116,8 +115,8 @@ type CreateOpts struct { 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". Required for HTTP(S) - // types. + // a single status like "200", a range like "200-202", or a combination like + // "200-202, 401". ExpectedCodes string `json:"expected_codes,omitempty"` // TenantID is the UUID of the project who owns the Monitor. @@ -138,24 +137,7 @@ type CreateOpts struct { // ToMonitorCreateMap builds a request body from CreateOpts. 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 + return gophercloud.BuildRequestBody(opts, "healthmonitor") } /* 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 index a9709ccec3f..0d287dffaae 100644 --- 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 @@ -1,6 +1,9 @@ package floatingips import ( + "encoding/json" + "time" + "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/pagination" ) @@ -37,6 +40,11 @@ type FloatingIP struct { // specify a project identifier other than its own. TenantID string `json:"tenant_id"` + // UpdatedAt and CreatedAt contain ISO-8601 timestamps of when the state of + // the floating ip last changed, and when it was created. + UpdatedAt time.Time `json:"-"` + CreatedAt time.Time `json:"-"` + // ProjectID is the project owner of the floating IP. ProjectID string `json:"project_id"` @@ -50,6 +58,44 @@ type FloatingIP struct { Tags []string `json:"tags"` } +func (r *FloatingIP) UnmarshalJSON(b []byte) error { + type tmp FloatingIP + + // Support for older neutron time format + var s1 struct { + tmp + CreatedAt gophercloud.JSONRFC3339NoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339NoZ `json:"updated_at"` + } + + err := json.Unmarshal(b, &s1) + if err == nil { + *r = FloatingIP(s1.tmp) + r.CreatedAt = time.Time(s1.CreatedAt) + r.UpdatedAt = time.Time(s1.UpdatedAt) + + return nil + } + + // Support for newer neutron time format + var s2 struct { + tmp + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + } + + err = json.Unmarshal(b, &s2) + if err != nil { + return err + } + + *r = FloatingIP(s2.tmp) + r.CreatedAt = time.Time(s2.CreatedAt) + r.UpdatedAt = time.Time(s2.UpdatedAt) + + return nil +} + type commonResult struct { gophercloud.Result } 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 index 468952b3e4e..960862bb38a 100644 --- 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 @@ -1,6 +1,9 @@ package groups import ( + "encoding/json" + "time" + "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules" "github.com/gophercloud/gophercloud/pagination" @@ -25,6 +28,11 @@ type SecGroup struct { // TenantID is the project owner of the security group. TenantID string `json:"tenant_id"` + // UpdatedAt and CreatedAt contain ISO-8601 timestamps of when the state of the + // security group last changed, and when it was created. + UpdatedAt time.Time `json:"-"` + CreatedAt time.Time `json:"-"` + // ProjectID is the project owner of the security group. ProjectID string `json:"project_id"` @@ -32,6 +40,44 @@ type SecGroup struct { Tags []string `json:"tags"` } +func (r *SecGroup) UnmarshalJSON(b []byte) error { + type tmp SecGroup + + // Support for older neutron time format + var s1 struct { + tmp + CreatedAt gophercloud.JSONRFC3339NoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339NoZ `json:"updated_at"` + } + + err := json.Unmarshal(b, &s1) + if err == nil { + *r = SecGroup(s1.tmp) + r.CreatedAt = time.Time(s1.CreatedAt) + r.UpdatedAt = time.Time(s1.UpdatedAt) + + return nil + } + + // Support for newer neutron time format + var s2 struct { + tmp + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + } + + err = json.Unmarshal(b, &s2) + if err != nil { + return err + } + + *r = SecGroup(s2.tmp) + r.CreatedAt = time.Time(s2.CreatedAt) + r.UpdatedAt = time.Time(s2.UpdatedAt) + + return nil +} + // SecGroupPage is the page returned by a pager when traversing over a // collection of security groups. type SecGroupPage struct { diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/subnetpools/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/subnetpools/results.go index e97e1e60aea..eadf005781a 100644 --- a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/subnetpools/results.go +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/subnetpools/results.go @@ -67,10 +67,10 @@ type SubnetPool struct { ProjectID string `json:"project_id"` // CreatedAt is the time at which subnetpool has been created. - CreatedAt time.Time `json:"created_at"` + CreatedAt time.Time `json:"-"` // UpdatedAt is the time at which subnetpool has been created. - UpdatedAt time.Time `json:"updated_at"` + UpdatedAt time.Time `json:"-"` // Prefixes is the list of subnet prefixes to assign to the subnetpool. // Neutron API merges adjacent prefixes and treats them as a single prefix. @@ -118,20 +118,83 @@ type SubnetPool struct { func (r *SubnetPool) UnmarshalJSON(b []byte) error { type tmp SubnetPool - var s struct { + + // Support for older neutron time format + var s1 struct { + tmp + DefaultPrefixLen interface{} `json:"default_prefixlen"` + MinPrefixLen interface{} `json:"min_prefixlen"` + MaxPrefixLen interface{} `json:"max_prefixlen"` + + CreatedAt gophercloud.JSONRFC3339NoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339NoZ `json:"updated_at"` + } + + err := json.Unmarshal(b, &s1) + if err == nil { + *r = SubnetPool(s1.tmp) + + r.CreatedAt = time.Time(s1.CreatedAt) + r.UpdatedAt = time.Time(s1.UpdatedAt) + + switch t := s1.DefaultPrefixLen.(type) { + case string: + if r.DefaultPrefixLen, err = strconv.Atoi(t); err != nil { + return err + } + case float64: + r.DefaultPrefixLen = int(t) + default: + return fmt.Errorf("DefaultPrefixLen has unexpected type: %T", t) + } + + switch t := s1.MinPrefixLen.(type) { + case string: + if r.MinPrefixLen, err = strconv.Atoi(t); err != nil { + return err + } + case float64: + r.MinPrefixLen = int(t) + default: + return fmt.Errorf("MinPrefixLen has unexpected type: %T", t) + } + + switch t := s1.MaxPrefixLen.(type) { + case string: + if r.MaxPrefixLen, err = strconv.Atoi(t); err != nil { + return err + } + case float64: + r.MaxPrefixLen = int(t) + default: + return fmt.Errorf("MaxPrefixLen has unexpected type: %T", t) + } + + return nil + } + + // Support for newer neutron time format + var s2 struct { tmp DefaultPrefixLen interface{} `json:"default_prefixlen"` MinPrefixLen interface{} `json:"min_prefixlen"` MaxPrefixLen interface{} `json:"max_prefixlen"` + + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } - err := json.Unmarshal(b, &s) + + err = json.Unmarshal(b, &s2) if err != nil { return err } - *r = SubnetPool(s.tmp) + *r = SubnetPool(s2.tmp) + + r.CreatedAt = time.Time(s2.CreatedAt) + r.UpdatedAt = time.Time(s2.UpdatedAt) - switch t := s.DefaultPrefixLen.(type) { + switch t := s2.DefaultPrefixLen.(type) { case string: if r.DefaultPrefixLen, err = strconv.Atoi(t); err != nil { return err @@ -142,7 +205,7 @@ func (r *SubnetPool) UnmarshalJSON(b []byte) error { return fmt.Errorf("DefaultPrefixLen has unexpected type: %T", t) } - switch t := s.MinPrefixLen.(type) { + switch t := s2.MinPrefixLen.(type) { case string: if r.MinPrefixLen, err = strconv.Atoi(t); err != nil { return err @@ -153,7 +216,7 @@ func (r *SubnetPool) UnmarshalJSON(b []byte) error { return fmt.Errorf("MinPrefixLen has unexpected type: %T", t) } - switch t := s.MaxPrefixLen.(type) { + switch t := s2.MaxPrefixLen.(type) { case string: if r.MaxPrefixLen, err = strconv.Atoi(t); err != nil { return err 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 index f03067415fb..80ca45c06e3 100644 --- a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/results.go +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/results.go @@ -1,6 +1,9 @@ package networks import ( + "encoding/json" + "time" + "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/pagination" ) @@ -70,6 +73,11 @@ type Network struct { // TenantID is the project owner of the network. TenantID string `json:"tenant_id"` + // UpdatedAt and CreatedAt contain ISO-8601 timestamps of when the state of the + // network last changed, and when it was created. + UpdatedAt time.Time `json:"-"` + CreatedAt time.Time `json:"-"` + // ProjectID is the project owner of the network. ProjectID string `json:"project_id"` @@ -84,6 +92,44 @@ type Network struct { Tags []string `json:"tags"` } +func (r *Network) UnmarshalJSON(b []byte) error { + type tmp Network + + // Support for older neutron time format + var s1 struct { + tmp + CreatedAt gophercloud.JSONRFC3339NoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339NoZ `json:"updated_at"` + } + + err := json.Unmarshal(b, &s1) + if err == nil { + *r = Network(s1.tmp) + r.CreatedAt = time.Time(s1.CreatedAt) + r.UpdatedAt = time.Time(s1.UpdatedAt) + + return nil + } + + // Support for newer neutron time format + var s2 struct { + tmp + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + } + + err = json.Unmarshal(b, &s2) + if err != nil { + return err + } + + *r = Network(s2.tmp) + r.CreatedAt = time.Time(s2.CreatedAt) + r.UpdatedAt = time.Time(s2.UpdatedAt) + + return nil +} + // NetworkPage is the page returned by a pager when traversing over a // collection of networks. type NetworkPage struct { From 8c7818254cf48ea01bb3429d6ad12d814bf79cd6 Mon Sep 17 00:00:00 2001 From: Mike Fedosin Date: Tue, 1 Oct 2019 15:35:24 +0200 Subject: [PATCH 2/4] Add gophercloud images to the vendor list --- Gopkg.lock | 5 +- .../gophercloud/gophercloud/internal/pkg.go | 1 + .../gophercloud/gophercloud/internal/util.go | 34 ++ .../openstack/imageservice/v2/images/doc.go | 60 +++ .../imageservice/v2/images/requests.go | 366 ++++++++++++++++++ .../imageservice/v2/images/results.go | 202 ++++++++++ .../openstack/imageservice/v2/images/types.go | 104 +++++ .../openstack/imageservice/v2/images/urls.go | 65 ++++ 8 files changed, 836 insertions(+), 1 deletion(-) create mode 100644 vendor/github.com/gophercloud/gophercloud/internal/pkg.go create mode 100644 vendor/github.com/gophercloud/gophercloud/internal/util.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/doc.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/requests.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/results.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/types.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/urls.go diff --git a/Gopkg.lock b/Gopkg.lock index a6c22c902c5..ee20c1c3b9c 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -336,10 +336,11 @@ [[projects]] branch = "master" - digest = "1:1d318d726b1fe59a7719a571f28b899fd608bd8e7a7b09b88c7be6d96eea0852" + digest = "1:f57b7ddc21b0593c06ce50d1b32ec2fcb91617cb7ee4f94b7a04d86377b3d708" name = "github.com/gophercloud/gophercloud" packages = [ ".", + "internal", "openstack", "openstack/blockstorage/v3/volumes", "openstack/common/extensions", @@ -349,6 +350,7 @@ "openstack/identity/v2/tenants", "openstack/identity/v2/tokens", "openstack/identity/v3/tokens", + "openstack/imageservice/v2/images", "openstack/loadbalancer/v2/apiversions", "openstack/loadbalancer/v2/l7policies", "openstack/loadbalancer/v2/listeners", @@ -1323,6 +1325,7 @@ "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors", "github.com/gophercloud/gophercloud/openstack/compute/v2/servers", "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens", + "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images", "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/apiversions", "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers", "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions", 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 00000000000..5bf0569ce8c --- /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/util.go b/vendor/github.com/gophercloud/gophercloud/internal/util.go new file mode 100644 index 00000000000..8efb283e729 --- /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/imageservice/v2/images/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/doc.go new file mode 100644 index 00000000000..14da9ac90da --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/doc.go @@ -0,0 +1,60 @@ +/* +Package images enables management and retrieval of images from the OpenStack +Image Service. + +Example to List Images + + images.ListOpts{ + Owner: "a7509e1ae65945fda83f3e52c6296017", + } + + allPages, err := images.List(imagesClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allImages, err := images.ExtractImages(allPages) + if err != nil { + panic(err) + } + + for _, image := range allImages { + fmt.Printf("%+v\n", image) + } + +Example to Create an Image + + createOpts := images.CreateOpts{ + Name: "image_name", + Visibility: images.ImageVisibilityPrivate, + } + + image, err := images.Create(imageClient, createOpts) + if err != nil { + panic(err) + } + +Example to Update an Image + + imageID := "1bea47ed-f6a9-463b-b423-14b9cca9ad27" + + updateOpts := images.UpdateOpts{ + images.ReplaceImageName{ + NewName: "new_name", + }, + } + + image, err := images.Update(imageClient, imageID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete an Image + + imageID := "1bea47ed-f6a9-463b-b423-14b9cca9ad27" + err := images.Delete(imageClient, imageID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package images 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 00000000000..4e487ea9e6d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/requests.go @@ -0,0 +1,366 @@ +package images + +import ( + "fmt" + "net/url" + "time" + + "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 { + // ID is the ID of the image. + // Multiple IDs can be specified by constructing a string + // such as "in:uuid1,uuid2,uuid3". + ID string `q:"id"` + + // 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 filters on the name of the image. + // Multiple names can be specified by constructing a string + // such as "in:name1,name2,name3". + Name string `q:"name"` + + // Visibility filters on the visibility of the image. + Visibility ImageVisibility `q:"visibility"` + + // MemberStatus filters on the member status of the image. + MemberStatus ImageMemberStatus `q:"member_status"` + + // Owner filters on the project ID of the image. + Owner string `q:"owner"` + + // Status filters on the status of the image. + // Multiple statuses can be specified by constructing a string + // such as "in:saving,queued". + Status ImageStatus `q:"status"` + + // SizeMin filters on the size_min image property. + SizeMin int64 `q:"size_min"` + + // SizeMax filters on the size_max image property. + SizeMax int64 `q:"size_max"` + + // Sort sorts the results using the new style of sorting. See the OpenStack + // Image API reference for the exact syntax. + // + // Sort cannot be used with the classic sort options (sort_key and sort_dir). + Sort string `q:"sort"` + + // SortKey will sort the results based on a specified image property. + SortKey string `q:"sort_key"` + + // SortDir will sort the list results either ascending or decending. + SortDir string `q:"sort_dir"` + + // Tags filters on specific image tags. + Tags []string `q:"tag"` + + // CreatedAtQuery filters images based on their creation date. + CreatedAtQuery *ImageDateQuery + + // UpdatedAtQuery filters images based on their updated date. + UpdatedAtQuery *ImageDateQuery + + // ContainerFormat filters images based on the container_format. + // Multiple container formats can be specified by constructing a + // string such as "in:bare,ami". + ContainerFormat string `q:"container_format"` + + // DiskFormat filters images based on the disk_format. + // Multiple disk formats can be specified by constructing a string + // such as "in:qcow2,iso". + DiskFormat string `q:"disk_format"` +} + +// ToImageListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToImageListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + params := q.Query() + + if opts.CreatedAtQuery != nil { + createdAt := opts.CreatedAtQuery.Date.Format(time.RFC3339) + if v := opts.CreatedAtQuery.Filter; v != "" { + createdAt = fmt.Sprintf("%s:%s", v, createdAt) + } + + params.Add("created_at", createdAt) + } + + if opts.UpdatedAtQuery != nil { + updatedAt := opts.UpdatedAtQuery.Date.Format(time.RFC3339) + if v := opts.UpdatedAtQuery.Filter; v != "" { + updatedAt = fmt.Sprintf("%s:%s", v, updatedAt) + } + + params.Add("updated_at", updatedAt) + } + + q = &url.URL{RawQuery: params.Encode()} + + 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 { + imagePage := ImagePage{ + serviceURL: c.ServiceURL(), + LinkedPageBase: pagination.LinkedPageBase{PageResult: r}, + } + + return imagePage + }) +} + +// CreateOptsBuilder allows extensions to add parameters to the Create request. +type CreateOptsBuilder interface { + // Returns value that can be passed to json.Marshal + ToImageCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents options used to create an image. +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 allows extensions to add additional parameters to the +// Update request. +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 assembles a request body based on the contents of +// UpdateOpts. +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 represents an updated visibility property request. +type UpdateVisibility struct { + Visibility ImageVisibility +} + +// ToImagePatchMap assembles a request body based on UpdateVisibility. +func (r UpdateVisibility) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/visibility", + "value": r.Visibility, + } +} + +// ReplaceImageName represents an updated image_name property request. +type ReplaceImageName struct { + NewName string +} + +// ToImagePatchMap assembles a request body based on ReplaceImageName. +func (r ReplaceImageName) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/name", + "value": r.NewName, + } +} + +// ReplaceImageChecksum represents an updated checksum property request. +type ReplaceImageChecksum struct { + Checksum string +} + +// ReplaceImageChecksum assembles a request body based on ReplaceImageChecksum. +func (r ReplaceImageChecksum) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/checksum", + "value": r.Checksum, + } +} + +// ReplaceImageTags represents an updated tags property request. +type ReplaceImageTags struct { + NewTags []string +} + +// ToImagePatchMap assembles a request body based on ReplaceImageTags. +func (r ReplaceImageTags) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/tags", + "value": r.NewTags, + } +} + +// ReplaceImageMinDisk represents an updated min_disk property request. +type ReplaceImageMinDisk struct { + NewMinDisk int +} + +// ToImagePatchMap assembles a request body based on ReplaceImageTags. +func (r ReplaceImageMinDisk) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/min_disk", + "value": r.NewMinDisk, + } +} + +// UpdateOp represents a valid update operation. +type UpdateOp string + +const ( + AddOp UpdateOp = "add" + ReplaceOp UpdateOp = "replace" + RemoveOp UpdateOp = "remove" +) + +// UpdateImageProperty represents an update property request. +type UpdateImageProperty struct { + Op UpdateOp + Name string + Value string +} + +// ToImagePatchMap assembles a request body based on UpdateImageProperty. +func (r UpdateImageProperty) ToImagePatchMap() map[string]interface{} { + updateMap := map[string]interface{}{ + "op": r.Op, + "path": fmt.Sprintf("/%s", r.Name), + } + + if r.Value != "" { + updateMap["value"] = r.Value + } + + return updateMap +} 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 00000000000..676181e1f4e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/results.go @@ -0,0 +1,202 @@ +package images + +import ( + "encoding/json" + "fmt" + "reflect" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/internal" + "github.com/gophercloud/gophercloud/pagination" +) + +// Image represents an image found in the OpenStack Image service. +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 ID 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:"-"` + + // 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{} + + // 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: + r.SizeBytes = 0 + 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") + delete(resultMap, "size") + 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. Call its Extract +// method to interpret it as an Image. +type CreateResult struct { + commonResult +} + +// UpdateResult represents the result of an Update operation. Call its Extract +// method to interpret it as an Image. +type UpdateResult struct { + commonResult +} + +// GetResult represents the result of a Get operation. Call its Extract +// method to interpret it as an Image. +type GetResult struct { + commonResult +} + +// DeleteResult represents the result of a Delete operation. Call its +// ExtractErr method to interpret it as an Image. +type DeleteResult struct { + gophercloud.ErrResult +} + +// ImagePage represents the results of a List request. +type ImagePage struct { + serviceURL string + pagination.LinkedPageBase +} + +// IsEmpty returns true if an ImagePage 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.serviceURL, 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/types.go b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/types.go new file mode 100644 index 00000000000..d2f9cbd3bfb --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/types.go @@ -0,0 +1,104 @@ +package images + +import ( + "time" +) + +// 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" +) + +// ImageDateFilter represents a valid filter to use for filtering +// images by their date during a List. +type ImageDateFilter string + +const ( + FilterGT ImageDateFilter = "gt" + FilterGTE ImageDateFilter = "gte" + FilterLT ImageDateFilter = "lt" + FilterLTE ImageDateFilter = "lte" + FilterNEQ ImageDateFilter = "neq" + FilterEQ ImageDateFilter = "eq" +) + +// ImageDateQuery represents a date field to be used for listing images. +// If no filter is specified, the query will act as though FilterEQ was +// set. +type ImageDateQuery struct { + Date time.Time + Filter ImageDateFilter +} 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 00000000000..1780c3c6ca7 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/urls.go @@ -0,0 +1,65 @@ +package images + +import ( + "net/url" + "strings" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/utils" +) + +// `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(serviceURL, requestedNext string) (string, error) { + base, err := utils.BaseEndpoint(serviceURL) + if err != nil { + return "", err + } + + requestedNextURL, err := url.Parse(requestedNext) + if err != nil { + return "", err + } + + base = gophercloud.NormalizeURL(base) + nextPath := base + strings.TrimPrefix(requestedNextURL.Path, "/") + + nextURL, err := url.Parse(nextPath) + if err != nil { + return "", err + } + + nextURL.RawQuery = requestedNextURL.RawQuery + + return nextURL.String(), nil +} From ff961c16a18b1865a87d4cfd2894ee2ff1e68e14 Mon Sep 17 00:00:00 2001 From: Mike Fedosin Date: Mon, 14 Oct 2019 15:36:52 +0200 Subject: [PATCH 3/4] Delete the glance image with RHCOS when the cluster is destroyed --- pkg/destroy/openstack/openstack.go | 40 ++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/pkg/destroy/openstack/openstack.go b/pkg/destroy/openstack/openstack.go index 57c27793dd1..1e6f0a93aca 100644 --- a/pkg/destroy/openstack/openstack.go +++ b/pkg/destroy/openstack/openstack.go @@ -12,6 +12,7 @@ import ( "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes" "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images" "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/apiversions" "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers" "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips" @@ -133,6 +134,7 @@ func populateDeleteFuncs(funcs map[string]deleteFunc) { funcs["deleteContainers"] = deleteContainers funcs["deleteVolumes"] = deleteVolumes funcs["deleteFloatingIPs"] = deleteFloatingIPs + funcs["deleteImages"] = deleteImages } // filterObjects will do client-side filtering given an appropriately filled out @@ -876,3 +878,41 @@ func deleteFloatingIPs(opts *clientconfig.ClientOpts, filter Filter, logger logr } return len(allFloatingIPs) == 0, nil } + +func deleteImages(opts *clientconfig.ClientOpts, filter Filter, logger logrus.FieldLogger) (bool, error) { + logger.Debug("Deleting openstack base image") + defer logger.Debugf("Exiting deleting openstack base image") + + conn, err := clientconfig.NewServiceClient("image", opts) + if err != nil { + logger.Fatalf("%v", err) + os.Exit(1) + } + + listOpts := images.ListOpts{ + Tags: filterTags(filter), + } + + allPages, err := images.List(conn, listOpts).AllPages() + if err != nil { + logger.Fatalf("%v", err) + os.Exit(1) + } + + allImages, err := images.ExtractImages(allPages) + if err != nil { + logger.Fatalf("%v", err) + os.Exit(1) + } + + for _, image := range allImages { + logger.Debugf("Deleting image: %+v", image.ID) + err := images.Delete(conn, image.ID).ExtractErr() + if err != nil { + // This can fail if the image is still in use by other VMs + logger.Debugf("Deleting Image failed: %v", err) + return false, nil + } + } + return true, nil +} From 545daeab21b76dc12e151741ad7ceb8977e2510f Mon Sep 17 00:00:00 2001 From: Mike Fedosin Date: Mon, 14 Oct 2019 16:20:11 +0200 Subject: [PATCH 4/4] OpenStack: automatically populate RHCOS image --- data/data/openstack/bootstrap/main.tf | 7 +----- data/data/openstack/bootstrap/variables.tf | 4 ++-- data/data/openstack/main.tf | 22 +++++++++++++++++-- data/data/openstack/masters/main.tf | 9 ++------ data/data/openstack/masters/variables.tf | 5 +++-- data/data/openstack/variables-openstack.tf | 9 ++++++-- pkg/asset/cluster/tfvars.go | 2 ++ pkg/asset/machines/master.go | 6 +++++- pkg/asset/machines/worker.go | 5 ++++- pkg/asset/rhcos/image.go | 2 +- pkg/rhcos/openstack.go | 14 ++++++++++++ pkg/tfvars/openstack/openstack.go | 25 +++++++++++++++++++--- 12 files changed, 83 insertions(+), 27 deletions(-) diff --git a/data/data/openstack/bootstrap/main.tf b/data/data/openstack/bootstrap/main.tf index da06ecbe521..7b6d8a0684d 100644 --- a/data/data/openstack/bootstrap/main.tf +++ b/data/data/openstack/bootstrap/main.tf @@ -109,11 +109,6 @@ EOF } } -data "openstack_images_image_v2" "bootstrap_image" { - name = var.image_name - most_recent = true -} - data "openstack_compute_flavor_v2" "bootstrap_flavor" { name = var.flavor_name } @@ -121,7 +116,7 @@ data "openstack_compute_flavor_v2" "bootstrap_flavor" { resource "openstack_compute_instance_v2" "bootstrap" { name = "${var.cluster_id}-bootstrap" flavor_id = data.openstack_compute_flavor_v2.bootstrap_flavor.id - image_id = data.openstack_images_image_v2.bootstrap_image.id + image_id = var.base_image_id user_data = data.ignition_config.redirect.rendered diff --git a/data/data/openstack/bootstrap/variables.tf b/data/data/openstack/bootstrap/variables.tf index abd0113bbf7..76aecd4b398 100644 --- a/data/data/openstack/bootstrap/variables.tf +++ b/data/data/openstack/bootstrap/variables.tf @@ -1,6 +1,6 @@ -variable "image_name" { +variable "base_image_id" { type = string - description = "The name of the Glance image for the bootstrap node." + description = "The identifier of the Glance image for the bootstrap node." } variable "extra_tags" { diff --git a/data/data/openstack/main.tf b/data/data/openstack/main.tf index 8149215e35d..6eb9cbcf061 100644 --- a/data/data/openstack/main.tf +++ b/data/data/openstack/main.tf @@ -27,7 +27,7 @@ module "bootstrap" { cluster_id = var.cluster_id extra_tags = var.openstack_extra_tags - image_name = var.openstack_base_image + base_image_id = data.openstack_images_image_v2.base_image.id flavor_name = var.openstack_master_flavor_name ignition = var.ignition_bootstrap api_int_ip = var.openstack_api_int_ip @@ -42,7 +42,7 @@ module "bootstrap" { module "masters" { source = "./masters" - base_image = var.openstack_base_image + base_image_id = data.openstack_images_image_v2.base_image.id cluster_id = var.cluster_id flavor_name = var.openstack_master_flavor_name instance_count = var.master_count @@ -72,3 +72,21 @@ module "topology" { trunk_support = var.openstack_trunk_support octavia_support = var.openstack_octavia_support } + +resource "openstack_images_image_v2" "base_image" { + // we need to create a new image only if the base image url has been provided, plus base image name is -rhcos + count = var.openstack_base_image_url != "" && var.openstack_base_image_name == "${var.cluster_id}-rhcos" ? 1 : 0 + + name = var.openstack_base_image_name + image_source_url = var.openstack_base_image_url + container_format = "bare" + disk_format = "qcow2" + + tags = ["openshiftClusterID=${var.cluster_id}"] +} + +data "openstack_images_image_v2" "base_image" { + name = var.openstack_base_image_name + + depends_on = ["openstack_images_image_v2.base_image"] +} diff --git a/data/data/openstack/masters/main.tf b/data/data/openstack/masters/main.tf index ea60534791f..c39f450541d 100644 --- a/data/data/openstack/masters/main.tf +++ b/data/data/openstack/masters/main.tf @@ -1,8 +1,3 @@ -data "openstack_images_image_v2" "masters_img" { - name = var.base_image - most_recent = true -} - data "openstack_compute_flavor_v2" "masters_flavor" { name = var.flavor_name } @@ -38,7 +33,7 @@ resource "openstack_blockstorage_volume_v3" "master_volume" { size = var.root_volume_size volume_type = var.root_volume_type - image_id = data.openstack_images_image_v2.masters_img.id + image_id = var.base_image_id } resource "openstack_compute_instance_v2" "master_conf" { @@ -46,7 +41,7 @@ resource "openstack_compute_instance_v2" "master_conf" { count = var.instance_count flavor_id = data.openstack_compute_flavor_v2.masters_flavor.id - image_id = var.root_volume_size == null ? data.openstack_images_image_v2.masters_img.id : null + image_id = var.root_volume_size == null ? var.base_image_id : null security_groups = var.master_sg_ids user_data = element( data.ignition_config.master_ignition_config.*.rendered, diff --git a/data/data/openstack/masters/variables.tf b/data/data/openstack/masters/variables.tf index b8bfada44f4..7fdd06ff2bb 100644 --- a/data/data/openstack/masters/variables.tf +++ b/data/data/openstack/masters/variables.tf @@ -1,5 +1,6 @@ -variable "base_image" { - type = string +variable "base_image_id" { + type = string + description = "The identifier of the Glance image for master nodes." } variable "cluster_id" { diff --git a/data/data/openstack/variables-openstack.tf b/data/data/openstack/variables-openstack.tf index 70fc6246ba0..8d378ddf07c 100644 --- a/data/data/openstack/variables-openstack.tf +++ b/data/data/openstack/variables-openstack.tf @@ -10,12 +10,17 @@ variable "openstack_master_root_volume_size" { description = "The size of the volume in gigabytes for the root block device of master nodes." } -variable "openstack_base_image" { +variable "openstack_base_image_name" { type = string - default = "rhcos" description = "Name of the base image to use for the nodes." } +variable "openstack_base_image_url" { + type = string + default = "" + description = "URL of the base image to use for the nodes." +} + variable "openstack_credentials_auth_url" { type = string default = "" diff --git a/pkg/asset/cluster/tfvars.go b/pkg/asset/cluster/tfvars.go index 27c376df782..ecaacd5b64c 100644 --- a/pkg/asset/cluster/tfvars.go +++ b/pkg/asset/cluster/tfvars.go @@ -280,6 +280,8 @@ func (t *TerraformVariables) Generate(parents asset.Parents) error { ingressVIP.String(), installConfig.Config.Platform.OpenStack.TrunkSupport, installConfig.Config.Platform.OpenStack.OctaviaSupport, + string(*rhcosImage), + clusterID.InfraID, ) if err != nil { return errors.Wrapf(err, "failed to get %s Terraform variables", platform) diff --git a/pkg/asset/machines/master.go b/pkg/asset/machines/master.go index 53d3b731551..c03a38f9b37 100644 --- a/pkg/asset/machines/master.go +++ b/pkg/asset/machines/master.go @@ -35,6 +35,7 @@ import ( "github.com/openshift/installer/pkg/asset/machines/machineconfig" "github.com/openshift/installer/pkg/asset/machines/openstack" "github.com/openshift/installer/pkg/asset/rhcos" + rhcosutils "github.com/openshift/installer/pkg/rhcos" "github.com/openshift/installer/pkg/types" awstypes "github.com/openshift/installer/pkg/types/aws" awsdefaults "github.com/openshift/installer/pkg/types/aws/defaults" @@ -181,7 +182,10 @@ func (m *Master) Generate(dependencies asset.Parents) error { mpool.Set(ic.Platform.OpenStack.DefaultMachinePlatform) mpool.Set(pool.Platform.OpenStack) pool.Platform.OpenStack = &mpool - machines, err = openstack.Machines(clusterID.InfraID, ic, pool, string(*rhcosImage), "master", "master-user-data") + + imageName, _ := rhcosutils.GenerateOpenStackImageName(string(*rhcosImage), clusterID.InfraID) + + machines, err = openstack.Machines(clusterID.InfraID, ic, pool, imageName, "master", "master-user-data") if err != nil { return errors.Wrap(err, "failed to create master machine objects") } diff --git a/pkg/asset/machines/worker.go b/pkg/asset/machines/worker.go index 6344f791397..2ce42ac8ab6 100644 --- a/pkg/asset/machines/worker.go +++ b/pkg/asset/machines/worker.go @@ -35,6 +35,7 @@ import ( "github.com/openshift/installer/pkg/asset/machines/machineconfig" "github.com/openshift/installer/pkg/asset/machines/openstack" "github.com/openshift/installer/pkg/asset/rhcos" + rhcosutils "github.com/openshift/installer/pkg/rhcos" "github.com/openshift/installer/pkg/types" awstypes "github.com/openshift/installer/pkg/types/aws" awsdefaults "github.com/openshift/installer/pkg/types/aws/defaults" @@ -247,7 +248,9 @@ func (w *Worker) Generate(dependencies asset.Parents) error { mpool.Set(pool.Platform.OpenStack) pool.Platform.OpenStack = &mpool - sets, err := openstack.MachineSets(clusterID.InfraID, ic, &pool, string(*rhcosImage), "worker", "worker-user-data") + imageName, _ := rhcosutils.GenerateOpenStackImageName(string(*rhcosImage), clusterID.InfraID) + + sets, err := openstack.MachineSets(clusterID.InfraID, ic, &pool, imageName, "worker", "worker-user-data") if err != nil { return errors.Wrap(err, "failed to create master machine objects") } diff --git a/pkg/asset/rhcos/image.go b/pkg/asset/rhcos/image.go index 06418de4174..34e6035ca36 100644 --- a/pkg/asset/rhcos/image.go +++ b/pkg/asset/rhcos/image.go @@ -78,7 +78,7 @@ func osImage(config *types.InstallConfig) (string, error) { case libvirt.Name: osimage, err = rhcos.QEMU(ctx) case openstack.Name: - osimage = "rhcos" + osimage, err = rhcos.OpenStack(ctx) case azure.Name: osimage, err = rhcos.VHD(ctx) case baremetal.Name: diff --git a/pkg/rhcos/openstack.go b/pkg/rhcos/openstack.go index bf24a302c65..b201ecc6ef5 100644 --- a/pkg/rhcos/openstack.go +++ b/pkg/rhcos/openstack.go @@ -27,3 +27,17 @@ func OpenStack(ctx context.Context) (string, error) { return base.ResolveReference(relOpenStack).String(), nil } + +// GenerateOpenStackImageName returns Glance image name for instances. +func GenerateOpenStackImageName(rhcosImage, infraID string) (imageName string, isURL bool) { + // Here we check whether rhcosImage is a URL or not. If this is the first case, it means that Glance image + // should be created by the installer with the universal name "-rhcos". Otherwise, it means + // that we are given the name of the pre-created Glance image, which the installer should use for node + // provisioning. + _, err := url.ParseRequestURI(rhcosImage) + if err != nil { + return rhcosImage, false + } + + return infraID + "-rhcos", true +} diff --git a/pkg/tfvars/openstack/openstack.go b/pkg/tfvars/openstack/openstack.go index 29123db7fbb..2345fa685d4 100644 --- a/pkg/tfvars/openstack/openstack.go +++ b/pkg/tfvars/openstack/openstack.go @@ -4,11 +4,14 @@ package openstack import ( "encoding/json" + "github.com/openshift/installer/pkg/rhcos" + "sigs.k8s.io/cluster-api-provider-openstack/pkg/apis/openstackproviderconfig/v1alpha1" ) type config struct { - BaseImage string `json:"openstack_base_image,omitempty"` + BaseImageName string `json:"openstack_base_image_name,omitempty"` + BaseImageURL string `json:"openstack_base_image_url,omitempty"` ExternalNetwork string `json:"openstack_external_network,omitempty"` Cloud string `json:"openstack_credentials_cloud,omitempty"` FlavorName string `json:"openstack_master_flavor_name,omitempty"` @@ -23,9 +26,8 @@ type config struct { } // TFVars generates OpenStack-specific Terraform variables. -func TFVars(masterConfig *v1alpha1.OpenstackProviderSpec, cloud string, externalNetwork string, lbFloatingIP string, apiVIP string, dnsVIP string, ingressVIP string, trunkSupport string, octaviaSupport string) ([]byte, error) { +func TFVars(masterConfig *v1alpha1.OpenstackProviderSpec, cloud string, externalNetwork string, lbFloatingIP string, apiVIP string, dnsVIP string, ingressVIP string, trunkSupport string, octaviaSupport string, baseImage string, infraID string) ([]byte, error) { cfg := &config{ - BaseImage: masterConfig.Image, ExternalNetwork: externalNetwork, Cloud: cloud, FlavorName: masterConfig.Flavor, @@ -36,6 +38,23 @@ func TFVars(masterConfig *v1alpha1.OpenstackProviderSpec, cloud string, external TrunkSupport: trunkSupport, OctaviaSupport: octaviaSupport, } + + // Normally baseImage contains a URL that we will use to create a new Glance image, but for testing + // purposes we also allow to set a custom Glance image name to skip the uploading. Here we check + // whether baseImage is a URL or not. If this is the first case, it means that the image should be + // created by the installer from the URL. Otherwise, it means that we are given the name of the pre-created + // Glance image, which we should use for instances. + imageName, isURL := rhcos.GenerateOpenStackImageName(baseImage, infraID) + cfg.BaseImageName = imageName + if isURL { + // Valid URL -> use baseImage as a URL that will be used to create new Glance image with name "-rhcos". + cfg.BaseImageURL = baseImage + } else { + // Not a URL -> use baseImage value as a Glance image name. + + // TODO(mfedosin): add validations that this image exists and there are no other images with this name. + } + if masterConfig.RootVolume != nil { cfg.RootVolumeSize = masterConfig.RootVolume.Size cfg.RootVolumeType = masterConfig.RootVolume.VolumeType