From 0eef9b42920a8d0ef4ff1d14161320b464540971 Mon Sep 17 00:00:00 2001 From: Andrei Ozerov Date: Tue, 12 Jun 2018 11:38:54 +0300 Subject: [PATCH] OpenStack builder: floating IP refactoring Remove usage of the deprecated OpenStack Compute service floating IP management and add methods to work with the OpenStack Networking service floating IPs API. Remove usage of the deprecated OpenStack Compute service floating IP pools and add methods to work with the OpenStack Networking service external networks API. Move reusable logic of working with the OpenStack Networking service API to a separate methods in the networking.go file. Pass error messages from the API services to the ui messages in the allocate IP step. --- builder/openstack/access_config.go | 7 + builder/openstack/builder.go | 6 +- builder/openstack/networks.go | 114 ++++++++++++++++ builder/openstack/run_config.go | 10 +- builder/openstack/ssh.go | 8 +- builder/openstack/step_allocate_ip.go | 179 +++++++++++++++----------- 6 files changed, 233 insertions(+), 91 deletions(-) create mode 100644 builder/openstack/networks.go diff --git a/builder/openstack/access_config.go b/builder/openstack/access_config.go index 1fcefa659..377a0c3a7 100644 --- a/builder/openstack/access_config.go +++ b/builder/openstack/access_config.go @@ -201,6 +201,13 @@ func (c *AccessConfig) blockStorageV3Client() (*gophercloud.ServiceClient, error }) } +func (c *AccessConfig) networkV2Client() (*gophercloud.ServiceClient, error) { + return openstack.NewNetworkV2(c.osClient, gophercloud.EndpointOpts{ + Region: c.Region, + Availability: c.getEndpointType(), + }) +} + func (c *AccessConfig) getEndpointType() gophercloud.Availability { if c.EndpointType == "internal" || c.EndpointType == "internalURL" { return gophercloud.AvailabilityInternal diff --git a/builder/openstack/builder.go b/builder/openstack/builder.go index 56631cf0b..c505233bb 100644 --- a/builder/openstack/builder.go +++ b/builder/openstack/builder.go @@ -115,9 +115,9 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe Wait: b.config.RackconnectWait, }, &StepAllocateIp{ - FloatingIpPool: b.config.FloatingIpPool, - FloatingIp: b.config.FloatingIp, - ReuseIps: b.config.ReuseIps, + FloatingNetwork: b.config.FloatingNetwork, + FloatingIP: b.config.FloatingIP, + ReuseIPs: b.config.ReuseIPs, }, &communicator.StepConnect{ Config: &b.config.RunConfig.Comm, diff --git a/builder/openstack/networks.go b/builder/openstack/networks.go new file mode 100644 index 000000000..ac56fa531 --- /dev/null +++ b/builder/openstack/networks.go @@ -0,0 +1,114 @@ +package openstack + +import ( + "fmt" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/attachinterfaces" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/external" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips" + "github.com/gophercloud/gophercloud/openstack/networking/v2/networks" + "github.com/gophercloud/gophercloud/pagination" +) + +// ExternalNetwork is a network with external router. +type ExternalNetwork struct { + networks.Network + external.NetworkExternalExt +} + +// FindExternalNetwork returns existing network with external router. +// It will return first network if there are many. +func FindExternalNetwork(client *gophercloud.ServiceClient) (*ExternalNetwork, error) { + var externalNetworks []ExternalNetwork + + allPages, err := networks.List(client, networks.ListOpts{ + Status: "ACTIVE", + }).AllPages() + if err != nil { + return nil, err + } + + // Extract external networks from found networks. + err = networks.ExtractNetworksInto(allPages, &externalNetworks) + if err != nil { + return nil, err + } + + if len(externalNetworks) == 0 { + return nil, fmt.Errorf("no external networks found") + } + + // Return the first external network. + return &externalNetworks[0], nil +} + +// CheckFloatingIP gets a floating IP by its ID and checks if it is already +// associated with any internal interface. +// It returns floating IP if it can be used. +func CheckFloatingIP(client *gophercloud.ServiceClient, id string) (*floatingips.FloatingIP, error) { + floatingIP, err := floatingips.Get(client, id).Extract() + if err != nil { + return nil, err + } + if floatingIP.PortID != "" { + return nil, fmt.Errorf("provided floating IP '%s' is already associated with port '%s'", + id, floatingIP.PortID) + } + + return floatingIP, nil +} + +// FindFreeFloatingIP returns free unassociated floating IP. +// It will return first floating IP if there are many. +func FindFreeFloatingIP(client *gophercloud.ServiceClient) (*floatingips.FloatingIP, error) { + var freeFloatingIP *floatingips.FloatingIP + + pager := floatingips.List(client, floatingips.ListOpts{ + Status: "DOWN", + }) + err := pager.EachPage(func(page pagination.Page) (bool, error) { + candidates, err := floatingips.ExtractFloatingIPs(page) + if err != nil { + return false, err // stop and throw error out + } + + for _, candidate := range candidates { + if candidate.PortID != "" { + continue // this floating IP is associated with port, move to next in list + } + + // Floating IP is able to be allocated. + freeFloatingIP = &candidate + return false, nil // stop iterating over pages + } + return true, nil // try the next page + }) + if err != nil { + return nil, err + } + if freeFloatingIP == nil { + return nil, fmt.Errorf("no free floating IPs found") + } + + return freeFloatingIP, nil +} + +// GetInstancePortID returns internal port of the instance that can be used for +// the association of a floating IP. +// It will return an ID of a first port if there are many. +func GetInstancePortID(client *gophercloud.ServiceClient, id string) (string, error) { + interfacesPage, err := attachinterfaces.List(client, id).AllPages() + if err != nil { + return "", err + } + interfaces, err := attachinterfaces.ExtractInterfaces(interfacesPage) + if err != nil { + return "", err + } + if len(interfaces) == 0 { + return "", fmt.Errorf("instance '%s' has no interfaces", id) + } + + return interfaces[0].PortID, nil +} diff --git a/builder/openstack/run_config.go b/builder/openstack/run_config.go index 9e89e2477..1b2f209c8 100644 --- a/builder/openstack/run_config.go +++ b/builder/openstack/run_config.go @@ -23,9 +23,9 @@ type RunConfig struct { Flavor string `mapstructure:"flavor"` AvailabilityZone string `mapstructure:"availability_zone"` RackconnectWait bool `mapstructure:"rackconnect_wait"` - FloatingIpPool string `mapstructure:"floating_ip_pool"` - FloatingIp string `mapstructure:"floating_ip"` - ReuseIps bool `mapstructure:"reuse_ips"` + FloatingNetwork string `mapstructure:"floating_network"` + FloatingIP string `mapstructure:"floating_ip"` + ReuseIPs bool `mapstructure:"reuse_ips"` SecurityGroups []string `mapstructure:"security_groups"` Networks []string `mapstructure:"networks"` Ports []string `mapstructure:"ports"` @@ -57,10 +57,6 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error { c.TemporaryKeyPairName = fmt.Sprintf("packer_%s", uuid.TimeOrderedUUID()) } - if c.UseFloatingIp && c.FloatingIpPool == "" { - c.FloatingIpPool = "public" - } - // Validation errs := c.Comm.Prepare(ctx) diff --git a/builder/openstack/ssh.go b/builder/openstack/ssh.go index 1dd6672c4..15c9bc36f 100644 --- a/builder/openstack/ssh.go +++ b/builder/openstack/ssh.go @@ -9,8 +9,8 @@ import ( "time" "github.com/gophercloud/gophercloud" - "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips" "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips" packerssh "github.com/hashicorp/packer/communicator/ssh" "github.com/hashicorp/packer/helper/multistep" "golang.org/x/crypto/ssh" @@ -35,9 +35,9 @@ func CommHost( // If we have a floating IP, use that ip := state.Get("access_ip").(*floatingips.FloatingIP) - if ip != nil && ip.IP != "" { - log.Printf("[DEBUG] Using floating IP %s to connect", ip.IP) - return ip.IP, nil + if ip != nil && ip.FloatingIP != "" { + log.Printf("[DEBUG] Using floating IP %s to connect", ip.FloatingIP) + return ip.FloatingIP, nil } if s.AccessIPv4 != "" { diff --git a/builder/openstack/step_allocate_ip.go b/builder/openstack/step_allocate_ip.go index fb53ce434..b5a517680 100644 --- a/builder/openstack/step_allocate_ip.go +++ b/builder/openstack/step_allocate_ip.go @@ -4,17 +4,16 @@ import ( "context" "fmt" - "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips" "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" - "github.com/gophercloud/gophercloud/pagination" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips" "github.com/hashicorp/packer/helper/multistep" "github.com/hashicorp/packer/packer" ) type StepAllocateIp struct { - FloatingIpPool string - FloatingIp string - ReuseIps bool + FloatingNetwork string + FloatingIP string + ReuseIPs bool } func (s *StepAllocateIp) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { @@ -23,123 +22,149 @@ func (s *StepAllocateIp) Run(_ context.Context, state multistep.StateBag) multis server := state.Get("server").(*servers.Server) // We need the v2 compute client - client, err := config.computeV2Client() + computeClient, err := config.computeV2Client() if err != nil { err = fmt.Errorf("Error initializing compute client: %s", err) state.Put("error", err) return multistep.ActionHalt } - var instanceIp floatingips.FloatingIP + // We need the v2 network client + networkClient, err := config.networkV2Client() + if err != nil { + err = fmt.Errorf("Error initializing network client: %s", err) + state.Put("error", err) + return multistep.ActionHalt + } + + var instanceIP floatingips.FloatingIP // This is here in case we error out before putting instanceIp into the // statebag below, because it is requested by Cleanup() - state.Put("access_ip", &instanceIp) + state.Put("access_ip", &instanceIP) - if s.FloatingIp != "" { - instanceIp.IP = s.FloatingIp - } else if s.FloatingIpPool != "" { - // If ReuseIps is set to true and we have a free floating IP in - // the pool, use it first rather than creating one - if s.ReuseIps { - ui.Say(fmt.Sprintf("Searching for unassociated floating IP in pool %s", s.FloatingIpPool)) - pager := floatingips.List(client) - err := pager.EachPage(func(page pagination.Page) (bool, error) { - candidates, err := floatingips.ExtractFloatingIPs(page) - - if err != nil { - return false, err // stop and throw error out - } - - for _, candidate := range candidates { - if candidate.Pool != s.FloatingIpPool || candidate.InstanceID != "" { - continue // move to next in list - } - - // In correct pool and able to be allocated - instanceIp.IP = candidate.IP - ui.Message(fmt.Sprintf("Selected floating IP: %s", instanceIp.IP)) - state.Put("floatingip_istemp", false) - return false, nil // stop iterating over pages - } - return true, nil // try the next page - }) - - if err != nil { - err := fmt.Errorf("Error searching for floating ip from pool '%s'", s.FloatingIpPool) - state.Put("error", err) - ui.Error(err.Error()) - return multistep.ActionHalt - } + // Try to use floating IP provided by the user or find a free floating IP. + if s.FloatingIP != "" { + freeFloatingIP, err := CheckFloatingIP(networkClient, s.FloatingIP) + if err != nil { + err := fmt.Errorf("Error using provided floating IP '%s': %s", s.FloatingIP, err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt } - if instanceIp.IP == "" { - ui.Say(fmt.Sprintf("Creating floating IP...")) - ui.Message(fmt.Sprintf("Pool: %s", s.FloatingIpPool)) - newIp, err := floatingips.Create(client, floatingips.CreateOpts{ - Pool: s.FloatingIpPool, - }).Extract() - if err != nil { - err := fmt.Errorf("Error creating floating ip from pool '%s'", s.FloatingIpPool) - state.Put("error", err) - ui.Error(err.Error()) - return multistep.ActionHalt - } - - instanceIp = *newIp - ui.Message(fmt.Sprintf("Created floating IP: %s", instanceIp.IP)) - state.Put("floatingip_istemp", true) + instanceIP = *freeFloatingIP + ui.Message(fmt.Sprintf("Selected floating IP: '%s' (%s)", instanceIP.ID, instanceIP.FloatingIP)) + state.Put("floatingip_istemp", false) + } else if s.ReuseIPs { + // If ReuseIPs is set to true and we have a free floating IP, use it rather + // than creating one. + ui.Say(fmt.Sprint("Searching for unassociated floating IP")) + freeFloatingIP, err := FindFreeFloatingIP(networkClient) + if err != nil { + err := fmt.Errorf("Error searching for floating IP: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt } + + instanceIP = *freeFloatingIP + ui.Message(fmt.Sprintf("Selected floating IP: '%s' (%s)", instanceIP.ID, instanceIP.FloatingIP)) + state.Put("floatingip_istemp", false) } - if instanceIp.IP != "" { - ui.Say(fmt.Sprintf("Associating floating IP with server...")) - ui.Message(fmt.Sprintf("IP: %s", instanceIp.IP)) - err := floatingips.AssociateInstance(client, server.ID, floatingips.AssociateOpts{ - FloatingIP: instanceIp.IP, - }).ExtractErr() + // Create a new floating IP if it wasn't obtained in the previous step. + if instanceIP.ID == "" { + // Search for the external network that can be used for the floating IPs if + // user hasn't provided any. + floatingNetwork := s.FloatingNetwork + if floatingNetwork == "" { + ui.Say(fmt.Sprintf("Searching for the external network...")) + externalNetwork, err := FindExternalNetwork(networkClient) + if err != nil { + err := fmt.Errorf("Error searching the external network: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + floatingNetwork = externalNetwork.ID + } + + ui.Say(fmt.Sprintf("Creating floating IP...")) + newIP, err := floatingips.Create(networkClient, floatingips.CreateOpts{ + FloatingNetworkID: floatingNetwork, + }).Extract() + if err != nil { + err := fmt.Errorf("Error creating floating IP from floating network '%s': %s", floatingNetwork, err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + instanceIP = *newIP + ui.Message(fmt.Sprintf("Created floating IP: '%s' (%s)", instanceIP.ID, instanceIP.FloatingIP)) + state.Put("floatingip_istemp", true) + } + + // Assoctate a floating IP that was obtained in the previous steps. + if instanceIP.ID != "" { + ui.Say(fmt.Sprintf("Associating floating IP '%s' (%s) with instance port...", + instanceIP.ID, instanceIP.FloatingIP)) + + portID, err := GetInstancePortID(computeClient, server.ID) + if err != nil { + err := fmt.Errorf("Error getting interfaces of the instance '%s': %s", server.ID, err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + _, err = floatingips.Update(networkClient, instanceIP.ID, floatingips.UpdateOpts{ + PortID: &portID, + }).Extract() if err != nil { err := fmt.Errorf( - "Error associating floating IP %s with instance: %s", - instanceIp.IP, err) + "Error associating floating IP '%s' (%s) with instance port '%s': %s", + instanceIP.ID, instanceIP.FloatingIP, portID, err) state.Put("error", err) ui.Error(err.Error()) return multistep.ActionHalt } ui.Message(fmt.Sprintf( - "Added floating IP %s to instance!", instanceIp.IP)) + "Added floating IP '%s' (%s) to instance!", instanceIP.ID, instanceIP.FloatingIP)) } - state.Put("access_ip", &instanceIp) + state.Put("access_ip", &instanceIP) return multistep.ActionContinue } func (s *StepAllocateIp) Cleanup(state multistep.StateBag) { config := state.Get("config").(Config) ui := state.Get("ui").(packer.Ui) - instanceIp := state.Get("access_ip").(*floatingips.FloatingIP) + instanceIP := state.Get("access_ip").(*floatingips.FloatingIP) // Don't delete pool addresses we didn't allocate if state.Get("floatingip_istemp") == false { return } - // We need the v2 compute client - client, err := config.computeV2Client() + // We need the v2 network client + client, err := config.networkV2Client() if err != nil { ui.Error(fmt.Sprintf( - "Error deleting temporary floating IP %s", instanceIp.IP)) + "Error deleting temporary floating IP '%s' (%s)", instanceIP.ID, instanceIP.FloatingIP)) return } - if s.FloatingIpPool != "" && instanceIp.ID != "" { - if err := floatingips.Delete(client, instanceIp.ID).ExtractErr(); err != nil { + if instanceIP.ID != "" { + if err := floatingips.Delete(client, instanceIP.ID).ExtractErr(); err != nil { ui.Error(fmt.Sprintf( - "Error deleting temporary floating IP %s", instanceIp.IP)) + "Error deleting temporary floating IP '%s' (%s)", instanceIP.ID, instanceIP.FloatingIP)) return } - ui.Say(fmt.Sprintf("Deleted temporary floating IP %s", instanceIp.IP)) + ui.Say(fmt.Sprintf("Deleted temporary floating IP '%s' (%s)", instanceIP.ID, instanceIP.FloatingIP)) } }