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 { func (c *AccessConfig) getEndpointType() gophercloud.Availability {
if c.EndpointType == "internal" || c.EndpointType == "internalURL" { if c.EndpointType == "internal" || c.EndpointType == "internalURL" {
return gophercloud.AvailabilityInternal 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, Wait: b.config.RackconnectWait,
}, },
&StepAllocateIp{ &StepAllocateIp{
FloatingIpPool: b.config.FloatingIpPool, FloatingNetwork: b.config.FloatingNetwork,
FloatingIp: b.config.FloatingIp, FloatingIP: b.config.FloatingIP,
ReuseIps: b.config.ReuseIps, ReuseIPs: b.config.ReuseIPs,
}, },
&communicator.StepConnect{ &communicator.StepConnect{
Config: &b.config.RunConfig.Comm, 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"` Flavor string `mapstructure:"flavor"`
AvailabilityZone string `mapstructure:"availability_zone"` AvailabilityZone string `mapstructure:"availability_zone"`
RackconnectWait bool `mapstructure:"rackconnect_wait"` RackconnectWait bool `mapstructure:"rackconnect_wait"`
FloatingIpPool string `mapstructure:"floating_ip_pool"` FloatingNetwork string `mapstructure:"floating_network"`
FloatingIp string `mapstructure:"floating_ip"` FloatingIP string `mapstructure:"floating_ip"`
ReuseIps bool `mapstructure:"reuse_ips"` ReuseIPs bool `mapstructure:"reuse_ips"`
SecurityGroups []string `mapstructure:"security_groups"` SecurityGroups []string `mapstructure:"security_groups"`
Networks []string `mapstructure:"networks"` Networks []string `mapstructure:"networks"`
Ports []string `mapstructure:"ports"` Ports []string `mapstructure:"ports"`
@ -57,10 +57,6 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
c.TemporaryKeyPairName = fmt.Sprintf("packer_%s", uuid.TimeOrderedUUID()) c.TemporaryKeyPairName = fmt.Sprintf("packer_%s", uuid.TimeOrderedUUID())
} }
if c.UseFloatingIp && c.FloatingIpPool == "" {
c.FloatingIpPool = "public"
}
// Validation // Validation
errs := c.Comm.Prepare(ctx) errs := c.Comm.Prepare(ctx)

View File

@ -9,8 +9,8 @@ import (
"time" "time"
"github.com/gophercloud/gophercloud" "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/compute/v2/servers"
"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips"
packerssh "github.com/hashicorp/packer/communicator/ssh" packerssh "github.com/hashicorp/packer/communicator/ssh"
"github.com/hashicorp/packer/helper/multistep" "github.com/hashicorp/packer/helper/multistep"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
@ -35,9 +35,9 @@ func CommHost(
// If we have a floating IP, use that // If we have a floating IP, use that
ip := state.Get("access_ip").(*floatingips.FloatingIP) ip := state.Get("access_ip").(*floatingips.FloatingIP)
if ip != nil && ip.IP != "" { if ip != nil && ip.FloatingIP != "" {
log.Printf("[DEBUG] Using floating IP %s to connect", ip.IP) log.Printf("[DEBUG] Using floating IP %s to connect", ip.FloatingIP)
return ip.IP, nil return ip.FloatingIP, nil
} }
if s.AccessIPv4 != "" { if s.AccessIPv4 != "" {

View File

@ -4,17 +4,16 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers" "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/helper/multistep"
"github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/packer"
) )
type StepAllocateIp struct { type StepAllocateIp struct {
FloatingIpPool string FloatingNetwork string
FloatingIp string FloatingIP string
ReuseIps bool ReuseIPs bool
} }
func (s *StepAllocateIp) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { 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) server := state.Get("server").(*servers.Server)
// We need the v2 compute client // We need the v2 compute client
client, err := config.computeV2Client() computeClient, err := config.computeV2Client()
if err != nil { if err != nil {
err = fmt.Errorf("Error initializing compute client: %s", err) err = fmt.Errorf("Error initializing compute client: %s", err)
state.Put("error", err) state.Put("error", err)
return multistep.ActionHalt 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 // This is here in case we error out before putting instanceIp into the
// statebag below, because it is requested by Cleanup() // statebag below, because it is requested by Cleanup()
state.Put("access_ip", &instanceIp) state.Put("access_ip", &instanceIP)
if s.FloatingIp != "" { // Try to use floating IP provided by the user or find a free floating IP.
instanceIp.IP = s.FloatingIp if s.FloatingIP != "" {
} else if s.FloatingIpPool != "" { freeFloatingIP, err := CheckFloatingIP(networkClient, s.FloatingIP)
// If ReuseIps is set to true and we have a free floating IP in if err != nil {
// the pool, use it first rather than creating one err := fmt.Errorf("Error using provided floating IP '%s': %s", s.FloatingIP, err)
if s.ReuseIps { state.Put("error", err)
ui.Say(fmt.Sprintf("Searching for unassociated floating IP in pool %s", s.FloatingIpPool)) ui.Error(err.Error())
pager := floatingips.List(client) return multistep.ActionHalt
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
}
} }
if instanceIp.IP == "" { instanceIP = *freeFloatingIP
ui.Say(fmt.Sprintf("Creating floating IP...")) ui.Message(fmt.Sprintf("Selected floating IP: '%s' (%s)", instanceIP.ID, instanceIP.FloatingIP))
ui.Message(fmt.Sprintf("Pool: %s", s.FloatingIpPool)) state.Put("floatingip_istemp", false)
newIp, err := floatingips.Create(client, floatingips.CreateOpts{ } else if s.ReuseIPs {
Pool: s.FloatingIpPool, // If ReuseIPs is set to true and we have a free floating IP, use it rather
}).Extract() // than creating one.
if err != nil { ui.Say(fmt.Sprint("Searching for unassociated floating IP"))
err := fmt.Errorf("Error creating floating ip from pool '%s'", s.FloatingIpPool) freeFloatingIP, err := FindFreeFloatingIP(networkClient)
state.Put("error", err) if err != nil {
ui.Error(err.Error()) err := fmt.Errorf("Error searching for floating IP: %s", err)
return multistep.ActionHalt 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)
} }
if instanceIp.IP != "" { // Create a new floating IP if it wasn't obtained in the previous step.
ui.Say(fmt.Sprintf("Associating floating IP with server...")) if instanceIP.ID == "" {
ui.Message(fmt.Sprintf("IP: %s", instanceIp.IP)) // Search for the external network that can be used for the floating IPs if
err := floatingips.AssociateInstance(client, server.ID, floatingips.AssociateOpts{ // user hasn't provided any.
FloatingIP: instanceIp.IP, floatingNetwork := s.FloatingNetwork
}).ExtractErr() 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 { if err != nil {
err := fmt.Errorf( err := fmt.Errorf(
"Error associating floating IP %s with instance: %s", "Error associating floating IP '%s' (%s) with instance port '%s': %s",
instanceIp.IP, err) instanceIP.ID, instanceIP.FloatingIP, portID, err)
state.Put("error", err) state.Put("error", err)
ui.Error(err.Error()) ui.Error(err.Error())
return multistep.ActionHalt return multistep.ActionHalt
} }
ui.Message(fmt.Sprintf( 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 return multistep.ActionContinue
} }
func (s *StepAllocateIp) Cleanup(state multistep.StateBag) { func (s *StepAllocateIp) Cleanup(state multistep.StateBag) {
config := state.Get("config").(Config) config := state.Get("config").(Config)
ui := state.Get("ui").(packer.Ui) 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 // Don't delete pool addresses we didn't allocate
if state.Get("floatingip_istemp") == false { if state.Get("floatingip_istemp") == false {
return return
} }
// We need the v2 compute client // We need the v2 network client
client, err := config.computeV2Client() client, err := config.networkV2Client()
if err != nil { if err != nil {
ui.Error(fmt.Sprintf( ui.Error(fmt.Sprintf(
"Error deleting temporary floating IP %s", instanceIp.IP)) "Error deleting temporary floating IP '%s' (%s)", instanceIP.ID, instanceIP.FloatingIP))
return return
} }
if s.FloatingIpPool != "" && instanceIp.ID != "" { if instanceIP.ID != "" {
if err := floatingips.Delete(client, instanceIp.ID).ExtractErr(); err != nil { if err := floatingips.Delete(client, instanceIP.ID).ExtractErr(); err != nil {
ui.Error(fmt.Sprintf( ui.Error(fmt.Sprintf(
"Error deleting temporary floating IP %s", instanceIp.IP)) "Error deleting temporary floating IP '%s' (%s)", instanceIP.ID, instanceIP.FloatingIP))
return 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))
} }
} }