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