From ffbd81ee7cba078ba92d15a79b73a248cd7c2c0a Mon Sep 17 00:00:00 2001 From: zengchen1024 Date: Wed, 28 Mar 2018 17:59:55 +0800 Subject: [PATCH] implement network task for OpenStack platform --- upup/pkg/fi/cloudup/openstack/BUILD.bazel | 1 + upup/pkg/fi/cloudup/openstack/cloud.go | 52 ++++++ .../pkg/fi/cloudup/openstacktasks/BUILD.bazel | 3 + upup/pkg/fi/cloudup/openstacktasks/network.go | 110 ++++++++++++ .../cloudup/openstacktasks/network_fitask.go | 75 ++++++++ .../networking/v2/networks/BUILD.bazel | 17 ++ .../openstack/networking/v2/networks/doc.go | 65 +++++++ .../networking/v2/networks/requests.go | 165 ++++++++++++++++++ .../networking/v2/networks/results.go | 111 ++++++++++++ .../openstack/networking/v2/networks/urls.go | 31 ++++ 10 files changed, 630 insertions(+) create mode 100644 upup/pkg/fi/cloudup/openstacktasks/network.go create mode 100644 upup/pkg/fi/cloudup/openstacktasks/network_fitask.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/BUILD.bazel create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/doc.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/requests.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/results.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/urls.go diff --git a/upup/pkg/fi/cloudup/openstack/BUILD.bazel b/upup/pkg/fi/cloudup/openstack/BUILD.bazel index 4815be62a4..73fdf7118d 100644 --- a/upup/pkg/fi/cloudup/openstack/BUILD.bazel +++ b/upup/pkg/fi/cloudup/openstack/BUILD.bazel @@ -20,6 +20,7 @@ go_library( "//vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes:go_default_library", "//vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups:go_default_library", "//vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules:go_default_library", + "//vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks:go_default_library", "//vendor/k8s.io/api/core/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library", ], diff --git a/upup/pkg/fi/cloudup/openstack/cloud.go b/upup/pkg/fi/cloudup/openstack/cloud.go index 91822b71a3..ecd7328be8 100644 --- a/upup/pkg/fi/cloudup/openstack/cloud.go +++ b/upup/pkg/fi/cloudup/openstack/cloud.go @@ -26,6 +26,7 @@ import ( cinder "github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes" sg "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups" sgr "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules" + "github.com/gophercloud/gophercloud/openstack/networking/v2/networks" "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/kops/dnsprovider/pkg/dnsprovider" @@ -81,6 +82,12 @@ type OpenstackCloud interface { //CreateSecurityGroupRule will create a new Neutron security group rule CreateSecurityGroupRule(opt sgr.CreateOpts) (*sgr.SecGroupRule, error) + + //ListNetworks will return the Neutron networks which match the options + ListNetworks(opt networks.ListOptsBuilder) ([]networks.Network, error) + + //CreateNetwork will create a new Neutron network + CreateNetwork(opt networks.CreateOptsBuilder) (*networks.Network, error) } type openstackCloud struct { @@ -317,3 +324,48 @@ func (c *openstackCloud) CreateSecurityGroupRule(opt sgr.CreateOpts) (*sgr.SecGr return rule, wait.ErrWaitTimeout } } + +func (c *openstackCloud) ListNetworks(opt networks.ListOptsBuilder) ([]networks.Network, error) { + var ns []networks.Network + + done, err := vfs.RetryWithBackoff(readBackoff, func() (bool, error) { + allPages, err := networks.List(c.neutronClient, opt).AllPages() + if err != nil { + return false, fmt.Errorf("error listing networks: %v", err) + } + + r, err := networks.ExtractNetworks(allPages) + if err != nil { + return false, fmt.Errorf("error extracting networks from pages: %v", err) + } + ns = r + return true, nil + }) + if err != nil { + return ns, err + } else if done { + return ns, nil + } else { + return ns, wait.ErrWaitTimeout + } +} + +func (c *openstackCloud) CreateNetwork(opt networks.CreateOptsBuilder) (*networks.Network, error) { + var n *networks.Network + + done, err := vfs.RetryWithBackoff(writeBackoff, func() (bool, error) { + r, err := networks.Create(c.neutronClient, opt).Extract() + if err != nil { + return false, fmt.Errorf("error creating network: %v", err) + } + n = r + return true, nil + }) + if err != nil { + return n, err + } else if done { + return n, nil + } else { + return n, wait.ErrWaitTimeout + } +} diff --git a/upup/pkg/fi/cloudup/openstacktasks/BUILD.bazel b/upup/pkg/fi/cloudup/openstacktasks/BUILD.bazel index f65e8b4e69..d9d566e9ed 100644 --- a/upup/pkg/fi/cloudup/openstacktasks/BUILD.bazel +++ b/upup/pkg/fi/cloudup/openstacktasks/BUILD.bazel @@ -3,6 +3,8 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") go_library( name = "go_default_library", srcs = [ + "network.go", + "network_fitask.go", "securitygroup.go", "securitygroup_fitask.go", "securitygrouprule.go", @@ -18,5 +20,6 @@ go_library( "//vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes:go_default_library", "//vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups:go_default_library", "//vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules:go_default_library", + "//vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks:go_default_library", ], ) diff --git a/upup/pkg/fi/cloudup/openstacktasks/network.go b/upup/pkg/fi/cloudup/openstacktasks/network.go new file mode 100644 index 0000000000..3507c4ff97 --- /dev/null +++ b/upup/pkg/fi/cloudup/openstacktasks/network.go @@ -0,0 +1,110 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package openstacktasks + +import ( + "fmt" + + "github.com/golang/glog" + "github.com/gophercloud/gophercloud/openstack/networking/v2/networks" + "k8s.io/kops/upup/pkg/fi" + "k8s.io/kops/upup/pkg/fi/cloudup/openstack" +) + +//go:generate fitask -type=Network +type Network struct { + ID *string + Name *string + Lifecycle *fi.Lifecycle +} + +var _ fi.CompareWithID = &Network{} + +func (n *Network) CompareWithID() *string { + return n.ID +} + +func (n *Network) Find(context *fi.Context) (*Network, error) { + cloud := context.Cloud.(openstack.OpenstackCloud) + opt := networks.ListOpts{ + Name: fi.StringValue(n.Name), + ID: fi.StringValue(n.ID), + } + ns, err := cloud.ListNetworks(opt) + if err != nil { + return nil, err + } + if ns == nil { + return nil, nil + } else if len(ns) != 1 { + return nil, fmt.Errorf("found multiple networks with name: %s", fi.StringValue(n.Name)) + } + v := ns[0] + actual := &Network{ + ID: fi.String(v.ID), + Name: fi.String(v.Name), + Lifecycle: n.Lifecycle, + } + return actual, nil +} + +func (c *Network) Run(context *fi.Context) error { + return fi.DefaultDeltaRunMethod(c, context) +} + +func (_ *Network) CheckChanges(a, e, changes *Network) error { + if a == nil { + if e.Name == nil { + return fi.RequiredField("Name") + } + } else { + if changes.ID != nil { + return fi.CannotChangeField("ID") + } + if changes.Name != nil { + return fi.CannotChangeField("Name") + } + } + return nil +} + +func (_ *Network) ShouldCreate(a, e, changes *Network) (bool, error) { + return a == nil, nil +} + +func (_ *Network) RenderOpenstack(t *openstack.OpenstackAPITarget, a, e, changes *Network) error { + if a == nil { + glog.V(2).Infof("Creating Network with name:%q", fi.StringValue(e.Name)) + + opt := networks.CreateOpts{ + Name: fi.StringValue(e.Name), + AdminStateUp: fi.Bool(true), + } + + v, err := t.Cloud.CreateNetwork(opt) + if err != nil { + return fmt.Errorf("Error creating network: %v", err) + } + + e.ID = fi.String(v.ID) + glog.V(2).Infof("Creating a new Openstack network, id=%s", v.ID) + return nil + } + + glog.V(2).Infof("Openstack task Network::RenderOpenstack did nothing") + return nil +} diff --git a/upup/pkg/fi/cloudup/openstacktasks/network_fitask.go b/upup/pkg/fi/cloudup/openstacktasks/network_fitask.go new file mode 100644 index 0000000000..7deeb197ad --- /dev/null +++ b/upup/pkg/fi/cloudup/openstacktasks/network_fitask.go @@ -0,0 +1,75 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by ""fitask" -type=Network"; DO NOT EDIT + +package openstacktasks + +import ( + "encoding/json" + + "k8s.io/kops/upup/pkg/fi" +) + +// Network + +// JSON marshalling boilerplate +type realNetwork Network + +// UnmarshalJSON implements conversion to JSON, supporitng an alternate specification of the object as a string +func (o *Network) UnmarshalJSON(data []byte) error { + var jsonName string + if err := json.Unmarshal(data, &jsonName); err == nil { + o.Name = &jsonName + return nil + } + + var r realNetwork + if err := json.Unmarshal(data, &r); err != nil { + return err + } + *o = Network(r) + return nil +} + +var _ fi.HasLifecycle = &Network{} + +// GetLifecycle returns the Lifecycle of the object, implementing fi.HasLifecycle +func (o *Network) GetLifecycle() *fi.Lifecycle { + return o.Lifecycle +} + +// SetLifecycle sets the Lifecycle of the object, implementing fi.SetLifecycle +func (o *Network) SetLifecycle(lifecycle fi.Lifecycle) { + o.Lifecycle = &lifecycle +} + +var _ fi.HasName = &Network{} + +// GetName returns the Name of the object, implementing fi.HasName +func (o *Network) GetName() *string { + return o.Name +} + +// SetName sets the Name of the object, implementing fi.SetName +func (o *Network) SetName(name string) { + o.Name = &name +} + +// String is the stringer function for the task, producing readable output using fi.TaskAsString +func (o *Network) String() string { + return fi.TaskAsString(o) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/BUILD.bazel b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/BUILD.bazel new file mode 100644 index 0000000000..088a8105ec --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/BUILD.bazel @@ -0,0 +1,17 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = [ + "doc.go", + "requests.go", + "results.go", + "urls.go", + ], + importpath = "github.com/gophercloud/gophercloud/openstack/networking/v2/networks", + visibility = ["//visibility:public"], + deps = [ + "//vendor/github.com/gophercloud/gophercloud:go_default_library", + "//vendor/github.com/gophercloud/gophercloud/pagination:go_default_library", + ], +) diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/doc.go new file mode 100644 index 0000000000..e768b71f82 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/doc.go @@ -0,0 +1,65 @@ +/* +Package networks contains functionality for working with Neutron network +resources. A network is an isolated virtual layer-2 broadcast domain that is +typically reserved for the tenant who created it (unless you configure the +network to be shared). Tenants can create multiple networks until the +thresholds per-tenant quota is reached. + +In the v2.0 Networking API, the network is the main entity. Ports and subnets +are always associated with a network. + +Example to List Networks + + listOpts := networks.ListOpts{ + TenantID: "a99e9b4e620e4db09a2dfb6e42a01e66", + } + + allPages, err := networks.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allNetworks, err := networks.ExtractNetworks(allPages) + if err != nil { + panic(err) + } + + for _, network := range allNetworks { + fmt.Printf("%+v", network) + } + +Example to Create a Network + + iTrue := true + createOpts := networks.CreateOpts{ + Name: "network_1", + AdminStateUp: &iTrue, + } + + network, err := networks.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Network + + networkID := "484cda0e-106f-4f4b-bb3f-d413710bbe78" + + updateOpts := networks.UpdateOpts{ + Name: "new_name", + } + + network, err := networks.Update(networkClient, networkID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Network + + networkID := "484cda0e-106f-4f4b-bb3f-d413710bbe78" + err := networks.Delete(networkClient, networkID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package networks diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/requests.go new file mode 100644 index 0000000000..5b61b24719 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/requests.go @@ -0,0 +1,165 @@ +package networks + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToNetworkListQuery() (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 network attributes you want to see returned. SortKey allows you to sort +// by a particular network attribute. SortDir sets the direction, and is either +// `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Status string `q:"status"` + Name string `q:"name"` + AdminStateUp *bool `q:"admin_state_up"` + TenantID string `q:"tenant_id"` + Shared *bool `q:"shared"` + ID string `q:"id"` + Marker string `q:"marker"` + Limit int `q:"limit"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToNetworkListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToNetworkListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// networks. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToNetworkListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return NetworkPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific network 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 +// Create request. +type CreateOptsBuilder interface { + ToNetworkCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents options used to create a network. +type CreateOpts struct { + AdminStateUp *bool `json:"admin_state_up,omitempty"` + Name string `json:"name,omitempty"` + Shared *bool `json:"shared,omitempty"` + TenantID string `json:"tenant_id,omitempty"` +} + +// ToNetworkCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToNetworkCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "network") +} + +// Create accepts a CreateOpts struct and creates a new network using the values +// provided. This operation does not actually require a request body, i.e. the +// CreateOpts struct argument can be empty. +// +// The tenant ID that is contained in the URI is the tenant that creates the +// network. An admin user, however, has the option of specifying another tenant +// ID in the CreateOpts struct. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToNetworkCreateMap() + 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 { + ToNetworkUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents options used to update a network. +type UpdateOpts struct { + AdminStateUp *bool `json:"admin_state_up,omitempty"` + Name string `json:"name,omitempty"` + Shared *bool `json:"shared,omitempty"` +} + +// ToNetworkUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToNetworkUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "network") +} + +// Update accepts a UpdateOpts struct and updates an existing network using the +// values provided. For more information, see the Create function. +func Update(c *gophercloud.ServiceClient, networkID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToNetworkUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(updateURL(c, networkID), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + return +} + +// Delete accepts a unique ID and deletes the network associated with it. +func Delete(c *gophercloud.ServiceClient, networkID string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, networkID), nil) + return +} + +// IDFromName is a convenience function that returns a network's ID, given +// its name. +func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + pages, err := List(client, nil).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractNetworks(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: "network"} + case 1: + return id, nil + default: + return "", gophercloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "network"} + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/results.go new file mode 100644 index 0000000000..ffd0259f1d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/results.go @@ -0,0 +1,111 @@ +package networks + +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 network resource. +func (r commonResult) Extract() (*Network, error) { + var s Network + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "network") +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Network. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Network. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Network. +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 +} + +// Network represents, well, a network. +type Network struct { + // UUID for the network + ID string `json:"id"` + + // Human-readable name for the network. Might not be unique. + Name string `json:"name"` + + // The administrative state of network. If false (down), the network does not + // forward packets. + AdminStateUp bool `json:"admin_state_up"` + + // Indicates whether network is currently operational. Possible values include + // `ACTIVE', `DOWN', `BUILD', or `ERROR'. Plug-ins might define additional + // values. + Status string `json:"status"` + + // Subnets associated with this network. + Subnets []string `json:"subnets"` + + // Owner of network. + TenantID string `json:"tenant_id"` + + // Specifies whether the network resource can be accessed by any tenant. + Shared bool `json:"shared"` +} + +// NetworkPage is the page returned by a pager when traversing over a +// collection of networks. +type NetworkPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of networks 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 NetworkPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"networks_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a NetworkPage struct is empty. +func (r NetworkPage) IsEmpty() (bool, error) { + is, err := ExtractNetworks(r) + return len(is) == 0, err +} + +// ExtractNetworks accepts a Page struct, specifically a NetworkPage struct, +// and extracts the elements into a slice of Network structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractNetworks(r pagination.Page) ([]Network, error) { + var s []Network + err := ExtractNetworksInto(r, &s) + return s, err +} + +func ExtractNetworksInto(r pagination.Page, v interface{}) error { + return r.(NetworkPage).Result.ExtractIntoSlicePtr(v, "networks") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/urls.go new file mode 100644 index 0000000000..4a8fb1dc7d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/urls.go @@ -0,0 +1,31 @@ +package networks + +import "github.com/gophercloud/gophercloud" + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("networks", id) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("networks") +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func listURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +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) +}