From 5da5b00e1c98a1aafd53ef14533acb80d33aeaaa Mon Sep 17 00:00:00 2001 From: Andrey Kaipov Date: Sat, 26 Oct 2019 17:40:11 -0400 Subject: [PATCH] [builder/openstack] adds option to discover provisioning network --- builder/openstack/builder.go | 7 +- builder/openstack/builder.hcl2spec.go | 2 + builder/openstack/networks.go | 42 +++ builder/openstack/networks_test.go | 54 ++++ builder/openstack/run_config.go | 4 + builder/openstack/step_discover_network.go | 55 ++++ builder/openstack/step_run_source_server.go | 12 +- .../openstack/networking/v2/subnets/doc.go | 135 +++++++++ .../networking/v2/subnets/requests.go | 269 ++++++++++++++++++ .../networking/v2/subnets/results.go | 152 ++++++++++ .../openstack/networking/v2/subnets/urls.go | 31 ++ vendor/modules.txt | 1 + .../openstack/_RunConfig-not-required.html.md | 4 + 13 files changed, 755 insertions(+), 13 deletions(-) create mode 100644 builder/openstack/networks_test.go create mode 100644 builder/openstack/step_discover_network.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/doc.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/requests.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/results.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/urls.go diff --git a/builder/openstack/builder.go b/builder/openstack/builder.go index de8e67d83..8411f803d 100644 --- a/builder/openstack/builder.go +++ b/builder/openstack/builder.go @@ -101,6 +101,11 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack SourceMostRecent: b.config.SourceImageFilters.MostRecent, SourceProperties: b.config.SourceImageFilters.Filters.Properties, }, + &StepDiscoverNetwork{ + Networks: b.config.Networks, + NetworkDiscoveryCIDRs: b.config.NetworkDiscoveryCIDRs, + Ports: b.config.Ports, + }, &StepCreateVolume{ UseBlockStorageVolume: b.config.UseBlockStorageVolume, VolumeName: b.config.VolumeName, @@ -110,8 +115,6 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack &StepRunSourceServer{ Name: b.config.InstanceName, SecurityGroups: b.config.SecurityGroups, - Networks: b.config.Networks, - Ports: b.config.Ports, AvailabilityZone: b.config.AvailabilityZone, UserData: b.config.UserData, UserDataFile: b.config.UserDataFile, diff --git a/builder/openstack/builder.hcl2spec.go b/builder/openstack/builder.hcl2spec.go index e44baa3e1..7977cee1f 100644 --- a/builder/openstack/builder.hcl2spec.go +++ b/builder/openstack/builder.hcl2spec.go @@ -98,6 +98,7 @@ type FlatConfig struct { SecurityGroups []string `mapstructure:"security_groups" required:"false" cty:"security_groups"` Networks []string `mapstructure:"networks" required:"false" cty:"networks"` Ports []string `mapstructure:"ports" required:"false" cty:"ports"` + NetworkDiscoveryCIDRs []string `mapstructure:"network_discovery_cidrs" required:"false" cty:"network_discovery_cidrs"` UserData *string `mapstructure:"user_data" required:"false" cty:"user_data"` UserDataFile *string `mapstructure:"user_data_file" required:"false" cty:"user_data_file"` InstanceName *string `mapstructure:"instance_name" required:"false" cty:"instance_name"` @@ -211,6 +212,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "security_groups": &hcldec.AttrSpec{Name: "security_groups", Type: cty.List(cty.String), Required: false}, "networks": &hcldec.AttrSpec{Name: "networks", Type: cty.List(cty.String), Required: false}, "ports": &hcldec.AttrSpec{Name: "ports", Type: cty.List(cty.String), Required: false}, + "network_discovery_cidrs": &hcldec.AttrSpec{Name: "network_discovery_cidrs", Type: cty.List(cty.String), Required: false}, "user_data": &hcldec.AttrSpec{Name: "user_data", Type: cty.String, Required: false}, "user_data_file": &hcldec.AttrSpec{Name: "user_data_file", Type: cty.String, Required: false}, "instance_name": &hcldec.AttrSpec{Name: "instance_name", Type: cty.String, Required: false}, diff --git a/builder/openstack/networks.go b/builder/openstack/networks.go index 537d9ce5a..3f510d6de 100644 --- a/builder/openstack/networks.go +++ b/builder/openstack/networks.go @@ -3,6 +3,7 @@ package openstack import ( "fmt" "log" + "net" "github.com/google/uuid" "github.com/gophercloud/gophercloud" @@ -10,6 +11,7 @@ import ( "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/openstack/networking/v2/subnets" "github.com/gophercloud/gophercloud/pagination" ) @@ -136,3 +138,43 @@ func GetFloatingIPNetworkIDByName(client *gophercloud.ServiceClient, networkName return externalNetworks[0].ID, nil } + +// DiscoverProvisioningNetwork finds the first network whose subnet matches the given network ranges. +func DiscoverProvisioningNetwork(client *gophercloud.ServiceClient, cidrs []string) (string, error) { + allPages, err := subnets.List(client, subnets.ListOpts{}).AllPages() + if err != nil { + return "", err + } + + allSubnets, err := subnets.ExtractSubnets(allPages) + if err != nil { + return "", err + } + + for _, subnet := range allSubnets { + _, tenantIPNet, err := net.ParseCIDR(subnet.CIDR) + if err != nil { + return "", err + } + + for _, cidr := range cidrs { + _, candidateIPNet, err := net.ParseCIDR(cidr) + if err != nil { + return "", err + } + + if containsNet(candidateIPNet, tenantIPNet) { + return subnet.NetworkID, nil + } + } + } + + return "", fmt.Errorf("failed to discover a provisioning network") +} + +// containsNet returns true whenever IPNet `a` contains IPNet `b` +func containsNet(a *net.IPNet, b *net.IPNet) bool { + aMask, _ := a.Mask.Size() + bMask, _ := b.Mask.Size() + return a.Contains(b.IP) && aMask <= bMask +} diff --git a/builder/openstack/networks_test.go b/builder/openstack/networks_test.go new file mode 100644 index 000000000..fe0a591ea --- /dev/null +++ b/builder/openstack/networks_test.go @@ -0,0 +1,54 @@ +package openstack + +import ( + "net" + "testing" +) + +func testYes(t *testing.T, a, b string) { + var m, n *net.IPNet + _, m, _ = net.ParseCIDR(a) + _, n, _ = net.ParseCIDR(b) + if !containsNet(m, n) { + t.Errorf("%s expected to contain %s", m, n) + } +} + +func testNot(t *testing.T, a, b string) { + var m, n *net.IPNet + _, m, _ = net.ParseCIDR(a) + _, n, _ = net.ParseCIDR(b) + if containsNet(m, n) { + t.Errorf("%s expected to not contain %s", m, n) + } +} + +func TestNetworkDiscovery_SubnetContainsGood_IPv4(t *testing.T) { + testYes(t, "192.168.0.0/23", "192.168.0.0/24") + testYes(t, "192.168.0.0/24", "192.168.0.0/24") + testNot(t, "192.168.0.0/25", "192.168.0.0/24") + + testYes(t, "192.168.101.202/16", "192.168.202.101/16") + testNot(t, "192.168.101.202/24", "192.168.202.101/24") + testNot(t, "192.168.202.101/24", "192.168.101.202/24") + + testYes(t, "0.0.0.0/0", "192.168.0.0/24") + testYes(t, "0.0.0.0/0", "0.0.0.0/1") + testNot(t, "192.168.0.0/24", "0.0.0.0/0") + testNot(t, "0.0.0.0/1", "0.0.0.0/0") +} + +func TestNetworkDiscovery_SubnetContainsGood_IPv6(t *testing.T) { + testYes(t, "2001:db8::/63", "2001:db8::/64") + testYes(t, "2001:db8::/64", "2001:db8::/64") + testNot(t, "2001:db8::/65", "2001:db8::/64") + + testYes(t, "2001:db8:fefe:b00b::/32", "2001:db8:b00b:fefe::/32") + testNot(t, "2001:db8:fefe:b00b::/64", "2001:db8:b00b:fefe::/64") + testNot(t, "2001:db8:b00b:fefe::/64", "2001:db8:fefe:b00b::/64") + + testYes(t, "::/0", "2001:db8::/64") + testYes(t, "::/0", "::/1") + testNot(t, "2001:db8::/64", "::/0") + testNot(t, "::/1", "::/0") +} diff --git a/builder/openstack/run_config.go b/builder/openstack/run_config.go index c04f53884..6d6b979a9 100644 --- a/builder/openstack/run_config.go +++ b/builder/openstack/run_config.go @@ -118,6 +118,10 @@ type RunConfig struct { Networks []string `mapstructure:"networks" required:"false"` // A list of ports by UUID to attach to this instance. Ports []string `mapstructure:"ports" required:"false"` + // A list of network CIDRs to discover the network to attach to this instance. + // The first network whose subnet is contained within any of the given CIDRs + // is used. Ignored if either of the above two options are provided. + NetworkDiscoveryCIDRs []string `mapstructure:"network_discovery_cidrs" required:"false"` // User data to apply when launching the instance. Note that you need to be // careful about escaping characters due to the templates being JSON. It is // often more convenient to use user_data_file, instead. Packer will not diff --git a/builder/openstack/step_discover_network.go b/builder/openstack/step_discover_network.go new file mode 100644 index 000000000..4f653685a --- /dev/null +++ b/builder/openstack/step_discover_network.go @@ -0,0 +1,55 @@ +package openstack + +import ( + "context" + "fmt" + + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" +) + +type StepDiscoverNetwork struct { + Networks []string + NetworkDiscoveryCIDRs []string + Ports []string +} + +func (s *StepDiscoverNetwork) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { + config := state.Get("config").(*Config) + ui := state.Get("ui").(packer.Ui) + + networkClient, err := config.networkV2Client() + if err != nil { + err = fmt.Errorf("Error initializing network client: %s", err) + state.Put("error", err) + return multistep.ActionHalt + } + + networks := []servers.Network{} + for _, port := range s.Ports { + networks = append(networks, servers.Network{Port: port}) + } + for _, uuid := range s.Networks { + networks = append(networks, servers.Network{UUID: uuid}) + } + + cidrs := s.NetworkDiscoveryCIDRs + if len(networks) == 0 && len(cidrs) > 0 { + ui.Say(fmt.Sprintf("Discovering provisioning network...")) + + networkID, err := DiscoverProvisioningNetwork(networkClient, cidrs) + if err != nil { + state.Put("error", err) + return multistep.ActionHalt + } + + ui.Message(fmt.Sprintf("Found network ID: %s", networkID)) + networks = append(networks, servers.Network{UUID: networkID}) + } + + state.Put("networks", networks) + return multistep.ActionContinue +} + +func (s *StepDiscoverNetwork) Cleanup(state multistep.StateBag) {} diff --git a/builder/openstack/step_run_source_server.go b/builder/openstack/step_run_source_server.go index 43382e945..3c3bdff1b 100644 --- a/builder/openstack/step_run_source_server.go +++ b/builder/openstack/step_run_source_server.go @@ -16,8 +16,6 @@ import ( type StepRunSourceServer struct { Name string SecurityGroups []string - Networks []string - Ports []string AvailabilityZone string UserData string UserDataFile string @@ -32,6 +30,7 @@ func (s *StepRunSourceServer) Run(ctx context.Context, state multistep.StateBag) config := state.Get("config").(*Config) flavor := state.Get("flavor_id").(string) sourceImage := state.Get("source_image").(string) + networks := state.Get("networks").([]servers.Network) ui := state.Get("ui").(packer.Ui) // We need the v2 compute client @@ -42,15 +41,6 @@ func (s *StepRunSourceServer) Run(ctx context.Context, state multistep.StateBag) return multistep.ActionHalt } - networks := make([]servers.Network, len(s.Networks)+len(s.Ports)) - i := 0 - for ; i < len(s.Ports); i++ { - networks[i].Port = s.Ports[i] - } - for ; i < len(networks); i++ { - networks[i].UUID = s.Networks[i-len(s.Ports)] - } - userData := []byte(s.UserData) if s.UserDataFile != "" { userData, err = ioutil.ReadFile(s.UserDataFile) diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/doc.go new file mode 100644 index 000000000..7d3a1b9b6 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/doc.go @@ -0,0 +1,135 @@ +/* +Package subnets contains functionality for working with Neutron subnet +resources. A subnet represents an IP address block that can be used to +assign IP addresses to virtual instances. Each subnet must have a CIDR and +must be associated with a network. IPs can either be selected from the whole +subnet CIDR or from allocation pools specified by the user. + +A subnet can also have a gateway, a list of DNS name servers, and host routes. +This information is pushed to instances whose interfaces are associated with +the subnet. + +Example to List Subnets + + listOpts := subnets.ListOpts{ + IPVersion: 4, + } + + allPages, err := subnets.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allSubnets, err := subnets.ExtractSubnets(allPages) + if err != nil { + panic(err) + } + + for _, subnet := range allSubnets { + fmt.Printf("%+v\n", subnet) + } + +Example to Create a Subnet With Specified Gateway + + var gatewayIP = "192.168.199.1" + createOpts := subnets.CreateOpts{ + NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a22", + IPVersion: 4, + CIDR: "192.168.199.0/24", + GatewayIP: &gatewayIP, + AllocationPools: []subnets.AllocationPool{ + { + Start: "192.168.199.2", + End: "192.168.199.254", + }, + }, + DNSNameservers: []string{"foo"}, + } + + subnet, err := subnets.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Create a Subnet With No Gateway + + var noGateway = "" + + createOpts := subnets.CreateOpts{ + NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a23", + IPVersion: 4, + CIDR: "192.168.1.0/24", + GatewayIP: &noGateway, + AllocationPools: []subnets.AllocationPool{ + { + Start: "192.168.1.2", + End: "192.168.1.254", + }, + }, + DNSNameservers: []string{}, + } + + subnet, err := subnets.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Create a Subnet With a Default Gateway + + createOpts := subnets.CreateOpts{ + NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a23", + IPVersion: 4, + CIDR: "192.168.1.0/24", + AllocationPools: []subnets.AllocationPool{ + { + Start: "192.168.1.2", + End: "192.168.1.254", + }, + }, + DNSNameservers: []string{}, + } + + subnet, err := subnets.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Subnet + + subnetID := "db77d064-e34f-4d06-b060-f21e28a61c23" + dnsNameservers := []string{"8.8.8.8"} + name := "new_name" + + updateOpts := subnets.UpdateOpts{ + Name: &name, + DNSNameservers: &dnsNameservers, + } + + subnet, err := subnets.Update(networkClient, subnetID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Remove a Gateway From a Subnet + + var noGateway = "" + subnetID := "db77d064-e34f-4d06-b060-f21e28a61c23" + + updateOpts := subnets.UpdateOpts{ + GatewayIP: &noGateway, + } + + subnet, err := subnets.Update(networkClient, subnetID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Subnet + + subnetID := "db77d064-e34f-4d06-b060-f21e28a61c23" + err := subnets.Delete(networkClient, subnetID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package subnets diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/requests.go new file mode 100644 index 000000000..3e56bf389 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/requests.go @@ -0,0 +1,269 @@ +package subnets + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToSubnetListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the subnet attributes you want to see returned. SortKey allows you to sort +// by a particular subnet attribute. SortDir sets the direction, and is either +// `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Name string `q:"name"` + Description string `q:"description"` + EnableDHCP *bool `q:"enable_dhcp"` + NetworkID string `q:"network_id"` + TenantID string `q:"tenant_id"` + ProjectID string `q:"project_id"` + IPVersion int `q:"ip_version"` + GatewayIP string `q:"gateway_ip"` + CIDR string `q:"cidr"` + IPv6AddressMode string `q:"ipv6_address_mode"` + IPv6RAMode string `q:"ipv6_ra_mode"` + ID string `q:"id"` + SubnetPoolID string `q:"subnetpool_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` + Tags string `q:"tags"` + TagsAny string `q:"tags-any"` + NotTags string `q:"not-tags"` + NotTagsAny string `q:"not-tags-any"` +} + +// ToSubnetListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToSubnetListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// subnets. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those subnets that are owned by the tenant +// who submits the request, unless the request is submitted by a user with +// administrative rights. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToSubnetListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return SubnetPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific subnet based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(getURL(c, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// List request. +type CreateOptsBuilder interface { + ToSubnetCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents the attributes used when creating a new subnet. +type CreateOpts struct { + // NetworkID is the UUID of the network the subnet will be associated with. + NetworkID string `json:"network_id" required:"true"` + + // CIDR is the address CIDR of the subnet. + CIDR string `json:"cidr,omitempty"` + + // Name is a human-readable name of the subnet. + Name string `json:"name,omitempty"` + + // Description of the subnet. + Description string `json:"description,omitempty"` + + // The UUID of the project who owns the Subnet. Only administrative users + // can specify a project UUID other than their own. + TenantID string `json:"tenant_id,omitempty"` + + // The UUID of the project who owns the Subnet. Only administrative users + // can specify a project UUID other than their own. + ProjectID string `json:"project_id,omitempty"` + + // AllocationPools are IP Address pools that will be available for DHCP. + AllocationPools []AllocationPool `json:"allocation_pools,omitempty"` + + // GatewayIP sets gateway information for the subnet. Setting to nil will + // cause a default gateway to automatically be created. Setting to an empty + // string will cause the subnet to be created with no gateway. Setting to + // an explicit address will set that address as the gateway. + GatewayIP *string `json:"gateway_ip,omitempty"` + + // IPVersion is the IP version for the subnet. + IPVersion gophercloud.IPVersion `json:"ip_version,omitempty"` + + // EnableDHCP will either enable to disable the DHCP service. + EnableDHCP *bool `json:"enable_dhcp,omitempty"` + + // DNSNameservers are the nameservers to be set via DHCP. + DNSNameservers []string `json:"dns_nameservers,omitempty"` + + // HostRoutes are any static host routes to be set via DHCP. + HostRoutes []HostRoute `json:"host_routes,omitempty"` + + // The IPv6 address modes specifies mechanisms for assigning IPv6 IP addresses. + IPv6AddressMode string `json:"ipv6_address_mode,omitempty"` + + // The IPv6 router advertisement specifies whether the networking service + // should transmit ICMPv6 packets. + IPv6RAMode string `json:"ipv6_ra_mode,omitempty"` + + // SubnetPoolID is the id of the subnet pool that subnet should be associated to. + SubnetPoolID string `json:"subnetpool_id,omitempty"` + + // Prefixlen is used when user creates a subnet from the subnetpool. It will + // overwrite the "default_prefixlen" value of the referenced subnetpool. + Prefixlen int `json:"prefixlen,omitempty"` +} + +// ToSubnetCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToSubnetCreateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "subnet") + if err != nil { + return nil, err + } + + if m := b["subnet"].(map[string]interface{}); m["gateway_ip"] == "" { + m["gateway_ip"] = nil + } + + return b, nil +} + +// Create accepts a CreateOpts struct and creates a new subnet using the values +// provided. You must remember to provide a valid NetworkID, CIDR and IP +// version. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToSubnetCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(createURL(c), b, &r.Body, nil) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToSubnetUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents the attributes used when updating an existing subnet. +type UpdateOpts struct { + // Name is a human-readable name of the subnet. + Name *string `json:"name,omitempty"` + + // Description of the subnet. + Description *string `json:"description,omitempty"` + + // AllocationPools are IP Address pools that will be available for DHCP. + AllocationPools []AllocationPool `json:"allocation_pools,omitempty"` + + // GatewayIP sets gateway information for the subnet. Setting to nil will + // cause a default gateway to automatically be created. Setting to an empty + // string will cause the subnet to be created with no gateway. Setting to + // an explicit address will set that address as the gateway. + GatewayIP *string `json:"gateway_ip,omitempty"` + + // DNSNameservers are the nameservers to be set via DHCP. + DNSNameservers *[]string `json:"dns_nameservers,omitempty"` + + // HostRoutes are any static host routes to be set via DHCP. + HostRoutes *[]HostRoute `json:"host_routes,omitempty"` + + // EnableDHCP will either enable to disable the DHCP service. + EnableDHCP *bool `json:"enable_dhcp,omitempty"` +} + +// ToSubnetUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToSubnetUpdateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "subnet") + if err != nil { + return nil, err + } + + if m := b["subnet"].(map[string]interface{}); m["gateway_ip"] == "" { + m["gateway_ip"] = nil + } + + return b, nil +} + +// Update accepts a UpdateOpts struct and updates an existing subnet using the +// values provided. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToSubnetUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(updateURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + return +} + +// Delete accepts a unique ID and deletes the subnet associated with it. +func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, id), nil) + return +} + +// IDFromName is a convenience function that returns a subnet's ID, +// given its name. +func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractSubnets(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", gophercloud.ErrResourceNotFound{Name: name, ResourceType: "subnet"} + case 1: + return id, nil + default: + return "", gophercloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "subnet"} + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/results.go new file mode 100644 index 000000000..cf0397019 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/results.go @@ -0,0 +1,152 @@ +package subnets + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a subnet resource. +func (r commonResult) Extract() (*Subnet, error) { + var s struct { + Subnet *Subnet `json:"subnet"` + } + err := r.ExtractInto(&s) + return s.Subnet, err +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Subnet. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Subnet. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Subnet. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// AllocationPool represents a sub-range of cidr available for dynamic +// allocation to ports, e.g. {Start: "10.0.0.2", End: "10.0.0.254"} +type AllocationPool struct { + Start string `json:"start"` + End string `json:"end"` +} + +// HostRoute represents a route that should be used by devices with IPs from +// a subnet (not including local subnet route). +type HostRoute struct { + DestinationCIDR string `json:"destination"` + NextHop string `json:"nexthop"` +} + +// Subnet represents a subnet. See package documentation for a top-level +// description of what this is. +type Subnet struct { + // UUID representing the subnet. + ID string `json:"id"` + + // UUID of the parent network. + NetworkID string `json:"network_id"` + + // Human-readable name for the subnet. Might not be unique. + Name string `json:"name"` + + // Description for the subnet. + Description string `json:"description"` + + // IP version, either `4' or `6'. + IPVersion int `json:"ip_version"` + + // CIDR representing IP range for this subnet, based on IP version. + CIDR string `json:"cidr"` + + // Default gateway used by devices in this subnet. + GatewayIP string `json:"gateway_ip"` + + // DNS name servers used by hosts in this subnet. + DNSNameservers []string `json:"dns_nameservers"` + + // Sub-ranges of CIDR available for dynamic allocation to ports. + // See AllocationPool. + AllocationPools []AllocationPool `json:"allocation_pools"` + + // Routes that should be used by devices with IPs from this subnet + // (not including local subnet route). + HostRoutes []HostRoute `json:"host_routes"` + + // Specifies whether DHCP is enabled for this subnet or not. + EnableDHCP bool `json:"enable_dhcp"` + + // TenantID is the project owner of the subnet. + TenantID string `json:"tenant_id"` + + // ProjectID is the project owner of the subnet. + ProjectID string `json:"project_id"` + + // The IPv6 address modes specifies mechanisms for assigning IPv6 IP addresses. + IPv6AddressMode string `json:"ipv6_address_mode"` + + // The IPv6 router advertisement specifies whether the networking service + // should transmit ICMPv6 packets. + IPv6RAMode string `json:"ipv6_ra_mode"` + + // SubnetPoolID is the id of the subnet pool associated with the subnet. + SubnetPoolID string `json:"subnetpool_id"` + + // Tags optionally set via extensions/attributestags + Tags []string `json:"tags"` +} + +// SubnetPage is the page returned by a pager when traversing over a collection +// of subnets. +type SubnetPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of subnets has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r SubnetPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"subnets_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a SubnetPage struct is empty. +func (r SubnetPage) IsEmpty() (bool, error) { + is, err := ExtractSubnets(r) + return len(is) == 0, err +} + +// ExtractSubnets accepts a Page struct, specifically a SubnetPage struct, +// and extracts the elements into a slice of Subnet structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractSubnets(r pagination.Page) ([]Subnet, error) { + var s struct { + Subnets []Subnet `json:"subnets"` + } + err := (r.(SubnetPage)).ExtractInto(&s) + return s.Subnets, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/urls.go new file mode 100644 index 000000000..7a4f2f7dd --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/urls.go @@ -0,0 +1,31 @@ +package subnets + +import "github.com/gophercloud/gophercloud" + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("subnets", id) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("subnets") +} + +func listURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func createURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index ab0765451..fd6a5ea29 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -275,6 +275,7 @@ github.com/gophercloud/gophercloud/openstack/imageservice/v2/members 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/openstack/networking/v2/subnets github.com/gophercloud/gophercloud/openstack/utils github.com/gophercloud/gophercloud/pagination # github.com/gophercloud/utils v0.0.0-20190124192022-a5c25e7a53a6 diff --git a/website/source/partials/builder/openstack/_RunConfig-not-required.html.md b/website/source/partials/builder/openstack/_RunConfig-not-required.html.md index d037ed872..b8090676f 100644 --- a/website/source/partials/builder/openstack/_RunConfig-not-required.html.md +++ b/website/source/partials/builder/openstack/_RunConfig-not-required.html.md @@ -41,6 +41,10 @@ - `ports` ([]string) - A list of ports by UUID to attach to this instance. +- `network_discovery_cidrs` ([]string) - A list of network CIDRs to discover the network to attach to this instance. + The first network whose subnet is contained within any of the given CIDRs + is used. Ignored if either of the above two options are provided. + - `user_data` (string) - User data to apply when launching the instance. Note that you need to be careful about escaping characters due to the templates being JSON. It is often more convenient to use user_data_file, instead. Packer will not