From e94c6dd6f745fda770e55abe9bc38be3f2bb70a2 Mon Sep 17 00:00:00 2001 From: Pierre Prinetti Date: Fri, 22 May 2020 15:13:33 +0200 Subject: [PATCH] Add ServerGroupName to OpenstackProviderSpec A server group with the corresponding name and "soft-anti-affinity" policy will be created if it does not exist already. --- .../openstackproviderconfig/v1alpha1/types.go | 8 +- pkg/cloud/openstack/clients/machineservice.go | 84 ++++++++++++++++- .../compute/v2/extensions/servergroups/doc.go | 86 ++++++++++++++++++ .../extensions/servergroups/microversions.go | 32 +++++++ .../v2/extensions/servergroups/requests.go | 67 ++++++++++++++ .../v2/extensions/servergroups/results.go | 89 +++++++++++++++++++ .../v2/extensions/servergroups/urls.go | 25 ++++++ vendor/modules.txt | 1 + 8 files changed, 390 insertions(+), 2 deletions(-) create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/doc.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/microversions.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/requests.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/results.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/urls.go diff --git a/pkg/apis/openstackproviderconfig/v1alpha1/types.go b/pkg/apis/openstackproviderconfig/v1alpha1/types.go index 3c6385ad60..aef7b92ffe 100644 --- a/pkg/apis/openstackproviderconfig/v1alpha1/types.go +++ b/pkg/apis/openstackproviderconfig/v1alpha1/types.go @@ -83,8 +83,14 @@ type OpenstackProviderSpec struct { // The volume metadata to boot from RootVolume *RootVolume `json:"rootVolume,omitempty"` - // The server group to assign the machine to + // The server group to assign the machine to. ServerGroupID string `json:"serverGroupID,omitempty"` + + // The server group to assign the machine to. A server group with that + // name will be created if it does not exist. If both ServerGroupID and + // ServerGroupName are non-empty, they must refer to the same OpenStack + // resource. + ServerGroupName string `json:"serverGroupName,omitempty"` } type SecurityGroupParam struct { diff --git a/pkg/cloud/openstack/clients/machineservice.go b/pkg/cloud/openstack/clients/machineservice.go index 5e9ab32c79..1dba12b623 100644 --- a/pkg/cloud/openstack/clients/machineservice.go +++ b/pkg/cloud/openstack/clients/machineservice.go @@ -36,6 +36,7 @@ import ( "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips" "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs" "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/schedulerhints" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups" "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors" "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens" @@ -395,7 +396,9 @@ func getImageID(is *InstanceService, imageName string) (string, error) { } } -// InstanceCreate creates a compute instance +// InstanceCreate creates a compute instance. +// If ServerGroupName is nonempty and no server group exists with that name, +// then InstanceCreate creates a server group with that name. func (is *InstanceService) InstanceCreate(clusterName string, name string, clusterSpec *openstackconfigv1.OpenstackClusterProviderSpec, config *openstackconfigv1.OpenstackProviderSpec, cmd string, keyName string, configClient configclient.ConfigV1Interface) (instance *Instance, err error) { if config == nil { return nil, fmt.Errorf("create Options need be specified to create instace") @@ -671,6 +674,49 @@ func (is *InstanceService) InstanceCreate(clusterName string, name string, clust } + // The Machine spec accepts both a server group ID and a server group + // name. If both are present, assert that they are consistent, else + // fail. If only the name is present, create the server group. + // + // This block validates or populates config.ServerGroupID. + if config.ServerGroupName != "" { + existingServerGroups, err := getServerGroupsByName(is.computeClient, config.ServerGroupName) + if err != nil { + return nil, fmt.Errorf("retrieving existing server groups: %v", err) + } + + if config.ServerGroupID == "" { + switch len(existingServerGroups) { + case 0: + sg, err := createServerGroup(is.computeClient, config.ServerGroupName) + if err != nil { + return nil, fmt.Errorf("creating the server group: %v", err) + } + config.ServerGroupID = sg.ID + case 1: + config.ServerGroupID = existingServerGroups[0].ID + default: + return nil, fmt.Errorf("multiple server groups found with the same ServerGroupName") + } + } else { + switch len(existingServerGroups) { + case 0: + return nil, fmt.Errorf("incompatible ServerGroupID and ServerGroupName") + default: + var found bool + for _, existingServerGroup := range existingServerGroups { + if existingServerGroup.ID == config.ServerGroupID { + found = true + break + } + } + if !found { + return nil, fmt.Errorf("incompatible ServerGroupID and ServerGroupName") + } + } + } + } + // If the spec sets a server group, then add scheduler hint if config.ServerGroupID != "" { serverCreateOpts = schedulerhints.CreateOptsExt{ @@ -693,6 +739,42 @@ func (is *InstanceService) InstanceCreate(clusterName string, name string, clust return serverToInstance(server), nil } +func createServerGroup(computeClient *gophercloud.ServiceClient, name string) (*servergroups.ServerGroup, error) { + // Microversion "2.15" is the first that supports "soft"-anti-affinity. + // Microversions starting from "2.64" accept policies as a string + // instead of an array. + defer func(microversion string) { + computeClient.Microversion = microversion + }(computeClient.Microversion) + computeClient.Microversion = "2.15" + + return servergroups.Create(computeClient, &servergroups.CreateOpts{ + Name: name, + Policies: []string{"soft-anti-affinity"}, + }).Extract() +} + +func getServerGroupsByName(computeClient *gophercloud.ServiceClient, name string) ([]servergroups.ServerGroup, error) { + pages, err := servergroups.List(computeClient).AllPages() + if err != nil { + return nil, err + } + + allServerGroups, err := servergroups.ExtractServerGroups(pages) + if err != nil { + return nil, err + } + + serverGroups := make([]servergroups.ServerGroup, 0, len(allServerGroups)) + for _, serverGroup := range allServerGroups { + if serverGroup.Name == name { + serverGroups = append(serverGroups, serverGroup) + } + } + + return serverGroups, nil +} + func (is *InstanceService) deleteInstancePorts(id string) error { // get instance port id allInterfaces, err := attachinterfaces.List(is.computeClient, id).AllPages() diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/doc.go new file mode 100644 index 0000000000..7811d32ecf --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/doc.go @@ -0,0 +1,86 @@ +/* +Package servergroups provides the ability to manage server groups. + +Example to List Server Groups + + allpages, err := servergroups.List(computeClient).AllPages() + if err != nil { + panic(err) + } + + allServerGroups, err := servergroups.ExtractServerGroups(allPages) + if err != nil { + panic(err) + } + + for _, sg := range allServerGroups { + fmt.Printf("%#v\n", sg) + } + +Example to Create a Server Group + + createOpts := servergroups.CreateOpts{ + Name: "my_sg", + Policies: []string{"anti-affinity"}, + } + + sg, err := servergroups.Create(computeClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Create a Server Group with additional microversion 2.64 fields + + createOpts := servergroups.CreateOpts{ + Name: "my_sg", + Policy: "anti-affinity", + Rules: &servergroups.Rules{ + MaxServerPerHost: 3, + }, + } + + computeClient.Microversion = "2.64" + result := servergroups.Create(computeClient, createOpts) + + serverGroup, err := result.Extract() + if err != nil { + panic(err) + } + + policy, err := servergroups.ExtractPolicy(result.Result) + if err != nil { + panic(err) + } + + rules, err := servergroups.ExtractRules(result.Result) + if err != nil { + panic(err) + } + +Example to Delete a Server Group + + sgID := "7a6f29ad-e34d-4368-951a-58a08f11cfb7" + err := servergroups.Delete(computeClient, sgID).ExtractErr() + if err != nil { + panic(err) + } + +Example to get additional fields with microversion 2.64 or later + + computeClient.Microversion = "2.64" + result := servergroups.Get(computeClient, "616fb98f-46ca-475e-917e-2563e5a8cd19") + + policy, err := servergroups.ExtractPolicy(result.Result) + if err != nil { + panic(err) + } + fmt.Printf("Policy: %s\n", policy) + + rules, err := servergroups.ExtractRules(result.Result) + if err != nil { + panic(err) + } + fmt.Printf("Max server per host: %s\n", rules.MaxServerPerHost) + +*/ +package servergroups diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/microversions.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/microversions.go new file mode 100644 index 0000000000..4899d9c572 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/microversions.go @@ -0,0 +1,32 @@ +package servergroups + +import "github.com/gophercloud/gophercloud" + +// ExtractPolicy will extract the policy attribute. +// This requires the client to be set to microversion 2.64 or later. +func ExtractPolicy(r gophercloud.Result) (string, error) { + var s struct { + Policy string `json:"policy"` + } + err := r.ExtractIntoStructPtr(&s, "server_group") + + return s.Policy, err +} + +// ExtractRules will extract the rules attribute. +// This requires the client to be set to microversion 2.64 or later. +func ExtractRules(r gophercloud.Result) (Rules, error) { + var s struct { + Rules Rules `json:"rules"` + } + err := r.ExtractIntoStructPtr(&s, "server_group") + + return s.Rules, err +} + +// Rules represents set of rules for a policy. +type Rules struct { + // MaxServerPerHost specifies how many servers can reside on a single compute host. + // It can be used only with the "anti-affinity" policy. + MaxServerPerHost int `json:"max_server_per_host,omitempty"` +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/requests.go new file mode 100644 index 0000000000..5740afaf05 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/requests.go @@ -0,0 +1,67 @@ +package servergroups + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// List returns a Pager that allows you to iterate over a collection of +// ServerGroups. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page { + return ServerGroupPage{pagination.SinglePageBase(r)} + }) +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToServerGroupCreateMap() (map[string]interface{}, error) +} + +// CreateOpts specifies Server Group creation parameters. +type CreateOpts struct { + // Name is the name of the server group. + Name string `json:"name" required:"true"` + + // Policies are the server group policies. + Policies []string `json:"policies,omitempty"` + + // Policy specifies the name of a policy. + // Requires microversion 2.64 or later. + Policy string `json:"policy,omitempty"` + + // Rules specifies the set of rules. + // Requires microversion 2.64 or later. + Rules *Rules `json:"rules,omitempty"` +} + +// ToServerGroupCreateMap constructs a request body from CreateOpts. +func (opts CreateOpts) ToServerGroupCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "server_group") +} + +// Create requests the creation of a new Server Group. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToServerGroupCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Get returns data about a previously created ServerGroup. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// Delete requests the deletion of a previously allocated ServerGroup. +func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/results.go new file mode 100644 index 0000000000..fd6f2c6c76 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/results.go @@ -0,0 +1,89 @@ +package servergroups + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// A ServerGroup creates a policy for instance placement in the cloud. +// You should use extract methods from microversions.go to retrieve additional +// fields. +type ServerGroup struct { + // ID is the unique ID of the Server Group. + ID string `json:"id"` + + // Name is the common name of the server group. + Name string `json:"name"` + + // Polices are the group policies. + // + // Normally a single policy is applied: + // + // "affinity" will place all servers within the server group on the + // same compute node. + // + // "anti-affinity" will place servers within the server group on different + // compute nodes. + Policies []string `json:"policies"` + + // Members are the members of the server group. + Members []string `json:"members"` + + // Metadata includes a list of all user-specified key-value pairs attached + // to the Server Group. + Metadata map[string]interface{} +} + +// ServerGroupPage stores a single page of all ServerGroups results from a +// List call. +type ServerGroupPage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a ServerGroupsPage is empty. +func (page ServerGroupPage) IsEmpty() (bool, error) { + va, err := ExtractServerGroups(page) + return len(va) == 0, err +} + +// ExtractServerGroups interprets a page of results as a slice of +// ServerGroups. +func ExtractServerGroups(r pagination.Page) ([]ServerGroup, error) { + var s struct { + ServerGroups []ServerGroup `json:"server_groups"` + } + err := (r.(ServerGroupPage)).ExtractInto(&s) + return s.ServerGroups, err +} + +type ServerGroupResult struct { + gophercloud.Result +} + +// Extract is a method that attempts to interpret any Server Group resource +// response as a ServerGroup struct. +func (r ServerGroupResult) Extract() (*ServerGroup, error) { + var s struct { + ServerGroup *ServerGroup `json:"server_group"` + } + err := r.ExtractInto(&s) + return s.ServerGroup, err +} + +// CreateResult is the response from a Create operation. Call its Extract method +// to interpret it as a ServerGroup. +type CreateResult struct { + ServerGroupResult +} + +// GetResult is the response from a Get operation. Call its Extract method to +// interpret it as a ServerGroup. +type GetResult struct { + ServerGroupResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr +// method to determine if the call succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/urls.go new file mode 100644 index 0000000000..9a1f99b199 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups/urls.go @@ -0,0 +1,25 @@ +package servergroups + +import "github.com/gophercloud/gophercloud" + +const resourcePath = "os-server-groups" + +func resourceURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(resourcePath) +} + +func listURL(c *gophercloud.ServiceClient) string { + return resourceURL(c) +} + +func createURL(c *gophercloud.ServiceClient) string { + return resourceURL(c) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id) +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return getURL(c, id) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 9be98d3278..48e099a28b 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -100,6 +100,7 @@ github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolum github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/schedulerhints +github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups github.com/gophercloud/gophercloud/openstack/compute/v2/flavors github.com/gophercloud/gophercloud/openstack/compute/v2/images github.com/gophercloud/gophercloud/openstack/compute/v2/servers