diff --git a/Gopkg.lock b/Gopkg.lock index 92d7eafe15f..a8bd398a865 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -201,17 +201,20 @@ [[projects]] branch = "master" - digest = "1:56c12cdd01d62ed0836da8d8756f7331b3544dd0dd023f1ab1db69c204776a1c" + digest = "1:5ecf13c53905c3385d0db93a56f904801728e8a9b7dc229242fe5462931347c0" name = "github.com/gophercloud/gophercloud" packages = [ ".", + "internal", "openstack", "openstack/compute/v2/flavors", "openstack/compute/v2/images", "openstack/compute/v2/servers", "openstack/identity/v2/tenants", "openstack/identity/v2/tokens", + "openstack/identity/v3/regions", "openstack/identity/v3/tokens", + "openstack/imageservice/v2/images", "openstack/networking/v2/extensions/layer3/routers", "openstack/networking/v2/extensions/security/groups", "openstack/networking/v2/extensions/security/rules", @@ -810,6 +813,8 @@ "github.com/coreos/ignition/config/v2_2/types", "github.com/ghodss/yaml", "github.com/gophercloud/gophercloud/openstack/compute/v2/servers", + "github.com/gophercloud/gophercloud/openstack/identity/v3/regions", + "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images", "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers", "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups", "github.com/gophercloud/gophercloud/openstack/networking/v2/networks", @@ -839,12 +844,10 @@ "k8s.io/apimachinery/pkg/api/errors", "k8s.io/apimachinery/pkg/apis/meta/v1", "k8s.io/apimachinery/pkg/runtime", - "k8s.io/apimachinery/pkg/util/net", "k8s.io/apimachinery/pkg/util/sets", "k8s.io/apimachinery/pkg/util/wait", "k8s.io/apimachinery/pkg/watch", "k8s.io/client-go/kubernetes", - "k8s.io/client-go/rest", "k8s.io/client-go/tools/clientcmd", "k8s.io/client-go/tools/clientcmd/api/v1", "k8s.io/client-go/tools/watch", diff --git a/pkg/asset/installconfig/openstack/openstack.go b/pkg/asset/installconfig/openstack/openstack.go index 6d3ea877518..0f1b39d51c8 100644 --- a/pkg/asset/installconfig/openstack/openstack.go +++ b/pkg/asset/installconfig/openstack/openstack.go @@ -2,6 +2,13 @@ package openstack import ( + "sort" + "strings" + + "github.com/gophercloud/gophercloud/openstack/identity/v3/regions" + "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images" + "github.com/gophercloud/gophercloud/openstack/networking/v2/networks" + "github.com/gophercloud/utils/openstack/clientconfig" "github.com/pkg/errors" survey "gopkg.in/AlecAivazis/survey.v1" @@ -13,19 +20,161 @@ const ( defaultVPCCIDR = "10.0.0.0/16" ) +// Read the valid cloud names from the clouds.yaml +func getCloudNames() ([]string, error) { + clouds, err := clientconfig.LoadCloudsYAML() + if err != nil { + return nil, err + } + i := 0 + cloudNames := make([]string, len(clouds)) + for k := range clouds { + cloudNames[i] = k + i++ + } + // Sort cloudNames so we can use sort.SearchStrings + sort.Strings(cloudNames) + return cloudNames, nil +} + +func getRegionNames(cloud string) ([]string, error) { + opts := &clientconfig.ClientOpts{ + Cloud: cloud, + } + + conn, err := clientconfig.NewServiceClient("identity", opts) + if err != nil { + return nil, err + } + + listOpts := regions.ListOpts{} + allPages, err := regions.List(conn, listOpts).AllPages() + if err != nil { + return nil, err + } + + allRegions, err := regions.ExtractRegions(allPages) + if err != nil { + return nil, err + } + + regionNames := make([]string, len(allRegions)) + for x, region := range allRegions { + regionNames[x] = region.ID + } + + sort.Strings(regionNames) + return regionNames, nil +} + +func getImageNames(cloud string) ([]string, error) { + opts := &clientconfig.ClientOpts{ + Cloud: cloud, + } + + conn, err := clientconfig.NewServiceClient("image", opts) + if err != nil { + return nil, err + } + + listOpts := images.ListOpts{} + allPages, err := images.List(conn, listOpts).AllPages() + if err != nil { + return nil, err + } + + allImages, err := images.ExtractImages(allPages) + if err != nil { + return nil, err + } + + imageNames := make([]string, len(allImages)) + for x, image := range allImages { + imageNames[x] = image.Name + } + + sort.Strings(imageNames) + return imageNames, nil +} + +func getNetworkNames(cloud string) ([]string, error) { + opts := &clientconfig.ClientOpts{ + Cloud: cloud, + } + + conn, err := clientconfig.NewServiceClient("network", opts) + if err != nil { + return nil, err + } + + listOpts := networks.ListOpts{} + allPages, err := networks.List(conn, listOpts).AllPages() + if err != nil { + return nil, err + } + + allNetworks, err := networks.ExtractNetworks(allPages) + if err != nil { + return nil, err + } + + networkNames := make([]string, len(allNetworks)) + for x, network := range allNetworks { + networkNames[x] = network.Name + } + + sort.Strings(networkNames) + return networkNames, nil +} + // Platform collects OpenStack-specific configuration. func Platform() (*openstack.Platform, error) { + cloudNames, err := getCloudNames() + if err != nil { + return nil, err + } + cloud, err := asset.GenerateUserProvidedAsset( + "OpenStack Cloud", + &survey.Question{ + Prompt: &survey.Select{ + Message: "Cloud", + Help: "The OpenStack cloud name from clouds.yaml.", + Options: cloudNames, + }, + Validate: survey.ComposeValidators(survey.Required, func(ans interface{}) error { + value := ans.(string) + i := sort.SearchStrings(cloudNames, value) + if i == len(cloudNames) || cloudNames[i] != value { + return errors.Errorf("invalid cloud name %q, should be one of %+v", value, strings.Join(cloudNames, ", ")) + } + return nil + }), + }, + "OPENSHIFT_INSTALL_OPENSTACK_CLOUD", + ) + if err != nil { + return nil, err + } + + regionNames, err := getRegionNames(cloud) + if err != nil { + return nil, err + } region, err := asset.GenerateUserProvidedAsset( "OpenStack Region", &survey.Question{ - Prompt: &survey.Input{ + Prompt: &survey.Select{ Message: "Region", Help: "The OpenStack region to be used for installation.", Default: "regionOne", + Options: regionNames, }, Validate: survey.ComposeValidators(survey.Required, func(ans interface{}) error { - //value := ans.(string) - //FIXME(shardy) add some validation here + value := ans.(string) + i := sort.SearchStrings(regionNames, value) + if i == len(regionNames) || regionNames[i] != value { + return errors.Errorf("invalid region name %q, should be one of %+v", value, strings.Join(regionNames, ", ")) + } return nil }), }, @@ -35,17 +184,25 @@ func Platform() (*openstack.Platform, error) { return nil, err } + imageNames, err := getImageNames(cloud) + if err != nil { + return nil, err + } image, err := asset.GenerateUserProvidedAsset( "OpenStack Image", &survey.Question{ - Prompt: &survey.Input{ + Prompt: &survey.Select{ Message: "Image", - Help: "The OpenStack image to be used for installation.", + Help: "The OpenStack image name to be used for installation.", Default: "rhcos", + Options: imageNames, }, Validate: survey.ComposeValidators(survey.Required, func(ans interface{}) error { - //value := ans.(string) - //FIXME(shardy) add some validation here + value := ans.(string) + i := sort.SearchStrings(imageNames, value) + if i == len(imageNames) || imageNames[i] != value { + return errors.Errorf("invalid image name %q, should be one of %+v", value, strings.Join(imageNames, ", ")) + } return nil }), }, @@ -55,37 +212,24 @@ func Platform() (*openstack.Platform, error) { return nil, err } - cloud, err := asset.GenerateUserProvidedAsset( - "OpenStack Cloud", - &survey.Question{ - //TODO(russellb) - We could open clouds.yaml here and read the list of defined clouds - //and then use survey.Select to let the user choose one. - Prompt: &survey.Input{ - Message: "Cloud", - Help: "The OpenStack cloud name from clouds.yaml.", - }, - Validate: survey.ComposeValidators(survey.Required, func(ans interface{}) error { - //value := ans.(string) - //FIXME(russellb) add some validation here - return nil - }), - }, - "OPENSHIFT_INSTALL_OPENSTACK_CLOUD", - ) + networkNames, err := getNetworkNames(cloud) if err != nil { return nil, err } - extNet, err := asset.GenerateUserProvidedAsset( "OpenStack External Network", &survey.Question{ - Prompt: &survey.Input{ + Prompt: &survey.Select{ Message: "ExternalNetwork", - Help: "The OpenStack external network to be used for installation.", + Help: "The OpenStack external network name to be used for installation.", + Options: networkNames, }, Validate: survey.ComposeValidators(survey.Required, func(ans interface{}) error { - //value := ans.(string) - //FIXME(shadower) add some validation here + value := ans.(string) + i := sort.SearchStrings(networkNames, value) + if i == len(networkNames) || networkNames[i] != value { + return errors.Errorf("invalid network name %q, should be one of %+v", value, strings.Join(networkNames, ", ")) + } return nil }), }, 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/identity/v3/regions/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/regions/doc.go new file mode 100644 index 00000000000..a37b05a544a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/regions/doc.go @@ -0,0 +1,63 @@ +/* +Package regions manages and retrieves Regions in the OpenStack Identity Service. + +Example to List Regions + + listOpts := regions.ListOpts{ + ParentRegionID: "RegionOne", + } + + allPages, err := regions.List(identityClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allRegions, err := regions.ExtractRegions(allPages) + if err != nil { + panic(err) + } + + for _, region := range allRegions { + fmt.Printf("%+v\n", region) + } + +Example to Create a Region + + createOpts := regions.CreateOpts{ + ID: "TestRegion", + Description: "Region for testing" + Extra: map[string]interface{}{ + "email": "testregionsupport@example.com", + } + } + + region, err := regions.Create(identityClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Region + + regionID := "TestRegion" + + // There is currently a bug in Keystone where updating the optional Extras + // attributes set in regions.Create is not supported, see: + // https://bugs.launchpad.net/keystone/+bug/1729933 + updateOpts := regions.UpdateOpts{ + Description: "Updated Description for region", + } + + region, err := regions.Update(identityClient, regionID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Region + + regionID := "TestRegion" + err := regions.Delete(identityClient, regionID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package regions diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/regions/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/regions/requests.go new file mode 100644 index 00000000000..aa588fdff4a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/regions/requests.go @@ -0,0 +1,164 @@ +package regions + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to +// the List request +type ListOptsBuilder interface { + ToRegionListQuery() (string, error) +} + +// ListOpts provides options to filter the List results. +type ListOpts struct { + // ParentRegionID filters the response by a parent region ID. + ParentRegionID string `q:"parent_region_id"` +} + +// ToRegionListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToRegionListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List enumerates the Regions 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.ToRegionListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return RegionPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves details on a single region, 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 { + ToRegionCreateMap() (map[string]interface{}, error) +} + +// CreateOpts provides options used to create a region. +type CreateOpts struct { + // ID is the ID of the new region. + ID string `json:"id,omitempty"` + + // Description is a description of the region. + Description string `json:"description,omitempty"` + + // ParentRegionID is the ID of the parent the region to add this region under. + ParentRegionID string `json:"parent_region_id,omitempty"` + + // Extra is free-form extra key/value pairs to describe the region. + Extra map[string]interface{} `json:"-"` +} + +// ToRegionCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToRegionCreateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "region") + if err != nil { + return nil, err + } + + if opts.Extra != nil { + if v, ok := b["region"].(map[string]interface{}); ok { + for key, value := range opts.Extra { + v[key] = value + } + } + } + + return b, nil +} + +// Create creates a new Region. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToRegionCreateMap() + 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 { + ToRegionUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts provides options for updating a region. +type UpdateOpts struct { + // Description is a description of the region. + Description string `json:"description,omitempty"` + + // ParentRegionID is the ID of the parent region. + ParentRegionID string `json:"parent_region_id,omitempty"` + + /* + // Due to a bug in Keystone, the Extra column of the Region table + // is not updatable, see: https://bugs.launchpad.net/keystone/+bug/1729933 + // The following lines should be uncommented once the fix is merged. + + // Extra is free-form extra key/value pairs to describe the region. + Extra map[string]interface{} `json:"-"` + */ +} + +// ToRegionUpdateMap formats a UpdateOpts into an update request. +func (opts UpdateOpts) ToRegionUpdateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "region") + if err != nil { + return nil, err + } + + /* + // Due to a bug in Keystone, the Extra column of the Region table + // is not updatable, see: https://bugs.launchpad.net/keystone/+bug/1729933 + // The following lines should be uncommented once the fix is merged. + + if opts.Extra != nil { + if v, ok := b["region"].(map[string]interface{}); ok { + for key, value := range opts.Extra { + v[key] = value + } + } + } + */ + + return b, nil +} + +// Update updates an existing Region. +func Update(client *gophercloud.ServiceClient, regionID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToRegionUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Patch(updateURL(client, regionID), &b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete deletes a region. +func Delete(client *gophercloud.ServiceClient, regionID string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, regionID), nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/regions/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/regions/results.go new file mode 100644 index 00000000000..a60cb348833 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/regions/results.go @@ -0,0 +1,129 @@ +package regions + +import ( + "encoding/json" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/internal" + "github.com/gophercloud/gophercloud/pagination" +) + +// Region helps manage related users. +type Region struct { + // Description describes the region purpose. + Description string `json:"description"` + + // ID is the unique ID of the region. + ID string `json:"id"` + + // Extra is a collection of miscellaneous key/values. + Extra map[string]interface{} `json:"-"` + + // Links contains referencing links to the region. + Links map[string]interface{} `json:"links"` + + // ParentRegionID is the ID of the parent region. + ParentRegionID string `json:"parent_region_id"` +} + +func (r *Region) UnmarshalJSON(b []byte) error { + type tmp Region + var s struct { + tmp + Extra map[string]interface{} `json:"extra"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Region(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(Region{}, resultMap) + } + } + + return err +} + +type regionResult struct { + gophercloud.Result +} + +// GetResult is the response from a Get operation. Call its Extract method +// to interpret it as a Region. +type GetResult struct { + regionResult +} + +// CreateResult is the response from a Create operation. Call its Extract method +// to interpret it as a Region. +type CreateResult struct { + regionResult +} + +// UpdateResult is the response from an Update operation. Call its Extract +// method to interpret it as a Region. +type UpdateResult struct { + regionResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr to +// determine if the request succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// RegionPage is a single page of Region results. +type RegionPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of Regions contains any results. +func (r RegionPage) IsEmpty() (bool, error) { + regions, err := ExtractRegions(r) + return len(regions) == 0, err +} + +// NextPageURL extracts the "next" link from the links section of the result. +func (r RegionPage) 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 +} + +// ExtractRegions returns a slice of Regions contained in a single page of results. +func ExtractRegions(r pagination.Page) ([]Region, error) { + var s struct { + Regions []Region `json:"regions"` + } + err := (r.(RegionPage)).ExtractInto(&s) + return s.Regions, err +} + +// Extract interprets any region results as a Region. +func (r regionResult) Extract() (*Region, error) { + var s struct { + Region *Region `json:"region"` + } + err := r.ExtractInto(&s) + return s.Region, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/regions/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/regions/urls.go new file mode 100644 index 00000000000..150ecc83582 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/regions/urls.go @@ -0,0 +1,23 @@ +package regions + +import "github.com/gophercloud/gophercloud" + +func listURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("regions") +} + +func getURL(client *gophercloud.ServiceClient, regionID string) string { + return client.ServiceURL("regions", regionID) +} + +func createURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("regions") +} + +func updateURL(client *gophercloud.ServiceClient, regionID string) string { + return client.ServiceURL("regions", regionID) +} + +func deleteURL(client *gophercloud.ServiceClient, regionID string) string { + return client.ServiceURL("regions", regionID) +} 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..16290d395a1 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/requests.go @@ -0,0 +1,352 @@ +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, + } +} + +// 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 +}