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.
This commit is contained in:
Andrei Ozerov 2018-06-12 11:38:54 +03:00
parent 68afd3d8da
commit 0eef9b4292
6 changed files with 233 additions and 91 deletions

View File

@ -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

View File

@ -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,

View File

@ -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
}

View File

@ -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)

View File

@ -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 != "" {

View File

@ -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))
}
}