Sync baremetal openstack with upstream

Change-Id: I125fc08e2cc4433aeaa470de48823dd4434c2030
Related-PROD: PROD-33018
diff --git a/openstack/baremetal/apiversions/doc.go b/openstack/baremetal/apiversions/doc.go
new file mode 100644
index 0000000..93b5adc
--- /dev/null
+++ b/openstack/baremetal/apiversions/doc.go
@@ -0,0 +1,13 @@
+/*
+Package apiversions provides information about the versions supported by a specific Ironic API.
+
+	Example to list versions
+
+		allVersions, err := apiversions.List(client.ServiceClient()).AllPages()
+
+	Example to get a specific version
+
+		actual, err := apiversions.Get(client.ServiceClient(), "v1").Extract()
+
+*/
+package apiversions
diff --git a/openstack/baremetal/apiversions/requests.go b/openstack/baremetal/apiversions/requests.go
new file mode 100644
index 0000000..73cc0d5
--- /dev/null
+++ b/openstack/baremetal/apiversions/requests.go
@@ -0,0 +1,17 @@
+package apiversions
+
+import (
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git"
+)
+
+// List lists all the API versions available to end users.
+func List(client *gophercloud.ServiceClient) (r ListResult) {
+	_, r.Err = client.Get(listURL(client), &r.Body, nil)
+	return
+}
+
+// Get will get a specific API version, specified by major ID.
+func Get(client *gophercloud.ServiceClient, v string) (r GetResult) {
+	_, r.Err = client.Get(getURL(client, v), &r.Body, nil)
+	return
+}
diff --git a/openstack/baremetal/apiversions/results.go b/openstack/baremetal/apiversions/results.go
new file mode 100644
index 0000000..e9ff957
--- /dev/null
+++ b/openstack/baremetal/apiversions/results.go
@@ -0,0 +1,62 @@
+package apiversions
+
+import (
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git"
+)
+
+// APIVersions represents the result from getting a list of all versions available
+type APIVersions struct {
+	DefaultVersion APIVersion   `json:"default_version"`
+	Versions       []APIVersion `json:"versions"`
+}
+
+// APIVersion represents an API version for Ironic
+type APIVersion struct {
+	// ID is the unique identifier of the API version.
+	ID string `json:"id"`
+
+	// MinVersion is the minimum microversion supported.
+	MinVersion string `json:"min_version"`
+
+	// Status is the API versions status.
+	Status string `json:"status"`
+
+	// Version is the maximum microversion supported.
+	Version string `json:"version"`
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+	gophercloud.Result
+}
+
+// ListResult represents the result of a list operation.
+type ListResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that accepts a get result and extracts an API version resource.
+func (r GetResult) Extract() (*APIVersion, error) {
+	var s struct {
+		Version APIVersion `json:"version"`
+	}
+
+	err := r.ExtractInto(&s)
+	if err != nil {
+		return nil, err
+	}
+
+	return &s.Version, nil
+}
+
+// Extract is a function that accepts a list result and extracts an APIVersions resource
+func (r ListResult) Extract() (*APIVersions, error) {
+	var version APIVersions
+
+	err := r.ExtractInto(&version)
+	if err != nil {
+		return nil, err
+	}
+
+	return &version, nil
+}
diff --git a/openstack/baremetal/apiversions/testing/fixtures.go b/openstack/baremetal/apiversions/testing/fixtures.go
new file mode 100644
index 0000000..5ba7dde
--- /dev/null
+++ b/openstack/baremetal/apiversions/testing/fixtures.go
@@ -0,0 +1,106 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git/openstack/baremetal/apiversions"
+	th "gerrit.mcp.mirantis.net/debian/gophercloud.git/testhelper"
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git/testhelper/client"
+)
+
+const IronicAPIAllVersionResponse = `
+{
+  "default_version": {
+    "status": "CURRENT",
+    "min_version": "1.1",
+    "version": "1.56",
+    "id": "v1",
+    "links": [
+      {
+        "href": "http://localhost:6385/v1/",
+        "rel": "self"
+      }
+    ]
+  },
+  "versions": [
+    {
+      "status": "CURRENT",
+      "min_version": "1.1",
+      "version": "1.56",
+      "id": "v1",
+      "links": [
+        {
+          "href": "http://localhost:6385/v1/",
+          "rel": "self"
+        }
+      ]
+    }
+  ],
+  "name": "OpenStack Ironic API",
+  "description": "Ironic is an OpenStack project which aims to provision baremetal machines."
+}
+`
+
+const IronicAPIVersionResponse = `
+{
+  "media_types": [
+    {
+      "base": "application/json",
+      "type": "application/vnd.openstack.ironic.v1+json"
+    }
+  ],
+  "version": {
+    "status": "CURRENT",
+    "min_version": "1.1",
+    "version": "1.56",
+    "id": "v1",
+    "links": [
+      {
+        "href": "http://localhost:6385/v1/",
+        "rel": "self"
+      }
+    ]
+  },
+  "id": "v1"
+}
+`
+
+var IronicAPIVersion1Result = apiversions.APIVersion{
+	ID:         "v1",
+	Status:     "CURRENT",
+	MinVersion: "1.1",
+	Version:    "1.56",
+}
+
+var IronicAllAPIVersionResults = apiversions.APIVersions{
+	DefaultVersion: IronicAPIVersion1Result,
+	Versions: []apiversions.APIVersion{
+		IronicAPIVersion1Result,
+	},
+}
+
+func MockListResponse(t *testing.T) {
+	th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, IronicAPIAllVersionResponse)
+	})
+}
+
+func MockGetResponse(t *testing.T) {
+	th.Mux.HandleFunc("/v1/", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, IronicAPIVersionResponse)
+	})
+}
diff --git a/openstack/baremetal/apiversions/testing/requests_test.go b/openstack/baremetal/apiversions/testing/requests_test.go
new file mode 100644
index 0000000..d60c347
--- /dev/null
+++ b/openstack/baremetal/apiversions/testing/requests_test.go
@@ -0,0 +1,33 @@
+package testing
+
+import (
+	"testing"
+
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git/openstack/baremetal/apiversions"
+	th "gerrit.mcp.mirantis.net/debian/gophercloud.git/testhelper"
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git/testhelper/client"
+)
+
+func TestListAPIVersions(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockListResponse(t)
+
+	actual, err := apiversions.List(client.ServiceClient()).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertDeepEquals(t, IronicAllAPIVersionResults, *actual)
+}
+
+func TestGetAPIVersion(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockGetResponse(t)
+
+	actual, err := apiversions.Get(client.ServiceClient(), "v1").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertDeepEquals(t, IronicAPIVersion1Result, *actual)
+}
diff --git a/openstack/baremetal/apiversions/urls.go b/openstack/baremetal/apiversions/urls.go
new file mode 100644
index 0000000..29d707b
--- /dev/null
+++ b/openstack/baremetal/apiversions/urls.go
@@ -0,0 +1,13 @@
+package apiversions
+
+import (
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git"
+)
+
+func getURL(c *gophercloud.ServiceClient, version string) string {
+	return c.ServiceURL(version)
+}
+
+func listURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL()
+}
diff --git a/openstack/baremetal/noauth/doc.go b/openstack/baremetal/noauth/doc.go
new file mode 100644
index 0000000..9f83357
--- /dev/null
+++ b/openstack/baremetal/noauth/doc.go
@@ -0,0 +1,17 @@
+/*
+Package noauth provides support for noauth bare metal endpoints.
+
+Example of obtaining and using a client:
+
+	client, err := noauth.NewBareMetalNoAuth(noauth.EndpointOpts{
+		IronicEndpoint: "http://localhost:6385/v1/",
+	})
+	if err != nil {
+		panic(err)
+	}
+
+	client.Microversion = "1.50"
+
+	nodes.ListDetail(client, nodes.ListOpts{})
+*/
+package noauth
diff --git a/openstack/baremetal/noauth/requests.go b/openstack/baremetal/noauth/requests.go
new file mode 100644
index 0000000..0b956e4
--- /dev/null
+++ b/openstack/baremetal/noauth/requests.go
@@ -0,0 +1,36 @@
+package noauth
+
+import (
+	"fmt"
+
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git"
+)
+
+// EndpointOpts specifies a "noauth" Ironic Endpoint.
+type EndpointOpts struct {
+	// IronicEndpoint [required] is currently only used with "noauth" Ironic.
+	// An Ironic endpoint with "auth_strategy=noauth" is necessary, for example:
+	// http://ironic.example.com:6385/v1.
+	IronicEndpoint string
+}
+
+func initClientOpts(client *gophercloud.ProviderClient, eo EndpointOpts) (*gophercloud.ServiceClient, error) {
+	sc := new(gophercloud.ServiceClient)
+	if eo.IronicEndpoint == "" {
+		return nil, fmt.Errorf("IronicEndpoint is required")
+	}
+
+	sc.Endpoint = gophercloud.NormalizeURL(eo.IronicEndpoint)
+	sc.ProviderClient = client
+	return sc, nil
+}
+
+// NewBareMetalNoAuth creates a ServiceClient that may be used to access a
+// "noauth" bare metal service.
+func NewBareMetalNoAuth(eo EndpointOpts) (*gophercloud.ServiceClient, error) {
+	sc, err := initClientOpts(&gophercloud.ProviderClient{}, eo)
+
+	// sc.Type = "baremetal"
+
+	return sc, err
+}
diff --git a/openstack/baremetal/noauth/testing/requests_test.go b/openstack/baremetal/noauth/testing/requests_test.go
new file mode 100644
index 0000000..a2181eb
--- /dev/null
+++ b/openstack/baremetal/noauth/testing/requests_test.go
@@ -0,0 +1,16 @@
+package testing
+
+import (
+	"testing"
+
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git/openstack/baremetal/noauth"
+	th "gerrit.mcp.mirantis.net/debian/gophercloud.git/testhelper"
+)
+
+func TestNoAuth(t *testing.T) {
+	noauthClient, err := noauth.NewBareMetalNoAuth(noauth.EndpointOpts{
+		IronicEndpoint: "http://ironic:6385/v1",
+	})
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, "", noauthClient.TokenID)
+}
diff --git a/openstack/baremetal/v1/allocations/requests.go b/openstack/baremetal/v1/allocations/requests.go
new file mode 100644
index 0000000..0c3055b
--- /dev/null
+++ b/openstack/baremetal/v1/allocations/requests.go
@@ -0,0 +1,131 @@
+package allocations
+
+import (
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git"
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git/pagination"
+)
+
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+	ToAllocationCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts specifies allocation creation parameters
+type CreateOpts struct {
+	// The requested resource class for the allocation.
+	ResourceClass string `json:"resource_class" required:"true"`
+
+	// The list of nodes (names or UUIDs) that should be considered for this allocation. If not provided, all available nodes will be considered.
+	CandidateNodes []string `json:"candidate_nodes,omitempty"`
+
+	// The unique name of the Allocation.
+	Name string `json:"name,omitempty"`
+
+	// The list of requested traits for the allocation.
+	Traits []string `json:"traits,omitempty"`
+
+	// The UUID for the resource.
+	UUID string `json:"uuid,omitempty"`
+
+	// A set of one or more arbitrary metadata key and value pairs.
+	Extra map[string]string `json:"extra,omitempty"`
+}
+
+// ToAllocationCreateMap assembles a request body based on the contents of a CreateOpts.
+func (opts CreateOpts) ToAllocationCreateMap() (map[string]interface{}, error) {
+	body, err := gophercloud.BuildRequestBody(opts, "")
+	if err != nil {
+		return nil, err
+	}
+
+	return body, nil
+}
+
+// Create requests a node to be created
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+	reqBody, err := opts.ToAllocationCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+
+	_, r.Err = client.Post(createURL(client), reqBody, &r.Body, nil)
+	return
+}
+
+type AllocationState string
+
+var (
+	Allocating AllocationState = "allocating"
+	Active                     = "active"
+	Error                      = "error"
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the List request.
+type ListOptsBuilder interface {
+	ToAllocationListQuery() (string, error)
+}
+
+// ListOpts allows the filtering and sorting of paginated collections through the API.
+type ListOpts struct {
+	// Filter the list of allocations by the node UUID or name.
+	Node string `q:"node"`
+
+	// Filter the list of returned nodes, and only return the ones with the specified resource class.
+	ResourceClass string `q:"resource_class"`
+
+	// Filter the list of allocations by the allocation state, one of active, allocating or error.
+	State AllocationState `q:"state"`
+
+	// One or more fields to be returned in the response.
+	Fields []string `q:"fields"`
+
+	// Requests a page size of items.
+	Limit int `q:"limit"`
+
+	// The ID of the last-seen item
+	Marker string `q:"marker"`
+
+	// Sorts the response by the requested sort direction.
+	// Valid value is asc (ascending) or desc (descending). Default is asc.
+	SortDir string `q:"sort_dir"`
+
+	// Sorts the response by the this attribute value. Default is id.
+	SortKey string `q:"sort_key"`
+}
+
+// ToAllocationListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToAllocationListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	return q.String(), err
+}
+
+// List makes a request against the API to list allocations accessible to you.
+func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	url := listURL(client)
+	if opts != nil {
+		query, err := opts.ToAllocationListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+		return AllocationPage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+// Get requests the details of an allocation by ID.
+func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
+	_, r.Err = client.Get(getURL(client, id), &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+// Delete requests the deletion of an allocation
+func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
+	_, r.Err = client.Delete(deleteURL(client, id), nil)
+	return
+}
diff --git a/openstack/baremetal/v1/allocations/results.go b/openstack/baremetal/v1/allocations/results.go
new file mode 100644
index 0000000..63fb2c4
--- /dev/null
+++ b/openstack/baremetal/v1/allocations/results.go
@@ -0,0 +1,114 @@
+package allocations
+
+import (
+	"time"
+
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git"
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git/pagination"
+)
+
+type Allocation struct {
+	// The UUID for the resource.
+	UUID string `json:"uuid"`
+
+	// A list of UUIDs of the nodes that are candidates for this allocation.
+	CandidateNodes []string `json:"candidate_nodes"`
+
+	// The error message for the allocation if it is in the error state, null otherwise.
+	LastError string `json:"last_error"`
+
+	// The unique name of the allocation.
+	Name string `json:"name"`
+
+	// The UUID of the node assigned to the allocation. Will be null if a node is not yet assigned.
+	NodeUUID string `json:"node_uuid"`
+
+	// The current state of the allocation. One of: allocation, active, error
+	State string `json:"state"`
+
+	// The resource class requested for the allocation.
+	ResourceClass string `json:"resource_class"`
+
+	// The list of the traits requested for the allocation.
+	Traits []string `json:"traits"`
+
+	// A set of one or more arbitrary metadata key and value pairs.
+	Extra map[string]string `json:"extra"`
+
+	// The UTC date and time when the resource was created, ISO 8601 format.
+	CreatedAt time.Time `json:"created_at"`
+
+	// The UTC date and time when the resource was updated, ISO 8601 format. May be “null”.
+	UpdatedAt time.Time `json:"updated_at"`
+
+	// A list of relative links. Includes the self and bookmark links.
+	Links []interface{} `json:"links"`
+}
+
+type allocationResult struct {
+	gophercloud.Result
+}
+
+func (r allocationResult) Extract() (*Allocation, error) {
+	var s Allocation
+	err := r.ExtractInto(&s)
+	return &s, err
+}
+
+func (r allocationResult) ExtractInto(v interface{}) error {
+	return r.Result.ExtractIntoStructPtr(v, "")
+}
+
+func ExtractAllocationsInto(r pagination.Page, v interface{}) error {
+	return r.(AllocationPage).Result.ExtractIntoSlicePtr(v, "allocations")
+}
+
+// AllocationPage abstracts the raw results of making a List() request against
+// the API.
+type AllocationPage struct {
+	pagination.LinkedPageBase
+}
+
+// IsEmpty returns true if a page contains no Allocation results.
+func (r AllocationPage) IsEmpty() (bool, error) {
+	s, err := ExtractAllocations(r)
+	return len(s) == 0, err
+}
+
+// NextPageURL uses the response's embedded link reference to navigate to the
+// next page of results.
+func (r AllocationPage) NextPageURL() (string, error) {
+	var s struct {
+		Links []gophercloud.Link `json:"allocations_links"`
+	}
+	err := r.ExtractInto(&s)
+	if err != nil {
+		return "", err
+	}
+	return gophercloud.ExtractNextURL(s.Links)
+}
+
+// ExtractAllocations interprets the results of a single page from a List() call,
+// producing a slice of Allocation entities.
+func ExtractAllocations(r pagination.Page) ([]Allocation, error) {
+	var s []Allocation
+	err := ExtractAllocationsInto(r, &s)
+	return s, err
+}
+
+// GetResult is the response from a Get operation. Call its Extract
+// method to interpret it as a Allocation.
+type GetResult struct {
+	allocationResult
+}
+
+// CreateResult is the response from a Create operation.
+type CreateResult struct {
+	allocationResult
+}
+
+// DeleteResult is the response from a Delete operation. Call its ExtractErr
+// method to determine if the call succeeded or failed.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
diff --git a/openstack/baremetal/v1/allocations/testing/fixtures.go b/openstack/baremetal/v1/allocations/testing/fixtures.go
new file mode 100644
index 0000000..cc7573a
--- /dev/null
+++ b/openstack/baremetal/v1/allocations/testing/fixtures.go
@@ -0,0 +1,168 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git/openstack/baremetal/v1/allocations"
+	th "gerrit.mcp.mirantis.net/debian/gophercloud.git/testhelper"
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git/testhelper/client"
+)
+
+const AllocationListBody = `
+{
+  "allocations": [
+    {
+      "candidate_nodes": [],
+      "created_at": "2019-02-20T09:43:58+00:00",
+      "extra": {},
+      "last_error": null,
+      "links": [
+        {
+          "href": "http://127.0.0.1:6385/v1/allocations/5344a3e2-978a-444e-990a-cbf47c62ef88",
+          "rel": "self"
+        },
+        {
+          "href": "http://127.0.0.1:6385/allocations/5344a3e2-978a-444e-990a-cbf47c62ef88",
+          "rel": "bookmark"
+        }
+      ],
+      "name": "allocation-1",
+      "node_uuid": "6d85703a-565d-469a-96ce-30b6de53079d",
+      "resource_class": "bm-large",
+      "state": "active",
+      "traits": [],
+      "updated_at": "2019-02-20T09:43:58+00:00",
+      "uuid": "5344a3e2-978a-444e-990a-cbf47c62ef88"
+    },
+    {
+      "candidate_nodes": [],
+      "created_at": "2019-02-20T09:43:58+00:00",
+      "extra": {},
+      "last_error": "Failed to process allocation eff80f47-75f0-4d41-b1aa-cf07c201adac: no available nodes match the resource class bm-large.",
+      "links": [
+        {
+          "href": "http://127.0.0.1:6385/v1/allocations/eff80f47-75f0-4d41-b1aa-cf07c201adac",
+          "rel": "self"
+        },
+        {
+          "href": "http://127.0.0.1:6385/allocations/eff80f47-75f0-4d41-b1aa-cf07c201adac",
+          "rel": "bookmark"
+        }
+      ],
+      "name": "allocation-2",
+      "node_uuid": null,
+      "resource_class": "bm-large",
+      "state": "error",
+      "traits": [
+        "CUSTOM_GOLD"
+      ],
+      "updated_at": "2019-02-20T09:43:58+00:00",
+      "uuid": "eff80f47-75f0-4d41-b1aa-cf07c201adac"
+    }
+  ]
+}
+`
+
+const SingleAllocationBody = `
+{
+  "candidate_nodes": ["344a3e2-978a-444e-990a-cbf47c62ef88"],
+  "created_at": "2019-02-20T09:43:58+00:00",
+  "extra": {},
+  "last_error": null,
+  "links": [
+    {
+      "href": "http://127.0.0.1:6385/v1/allocations/5344a3e2-978a-444e-990a-cbf47c62ef88",
+      "rel": "self"
+    },
+    {
+      "href": "http://127.0.0.1:6385/allocations/5344a3e2-978a-444e-990a-cbf47c62ef88",
+      "rel": "bookmark"
+    }
+  ],
+  "name": "allocation-1",
+  "node_uuid": null,
+  "resource_class": "baremetal",
+  "state": "allocating",
+  "traits": ["foo"],
+  "updated_at": null,
+  "uuid": "5344a3e2-978a-444e-990a-cbf47c62ef88"
+}`
+
+var (
+	createdAt, _ = time.Parse(time.RFC3339, "2019-02-20T09:43:58+00:00")
+
+	Allocation1 = allocations.Allocation{
+		UUID:           "5344a3e2-978a-444e-990a-cbf47c62ef88",
+		CandidateNodes: []string{"344a3e2-978a-444e-990a-cbf47c62ef88"},
+		Name:           "allocation-1",
+		State:          "allocating",
+		ResourceClass:  "baremetal",
+		Traits:         []string{"foo"},
+		Extra:          map[string]string{},
+		CreatedAt:      createdAt,
+		Links:          []interface{}{map[string]interface{}{"href": "http://127.0.0.1:6385/v1/allocations/5344a3e2-978a-444e-990a-cbf47c62ef88", "rel": "self"}, map[string]interface{}{"href": "http://127.0.0.1:6385/allocations/5344a3e2-978a-444e-990a-cbf47c62ef88", "rel": "bookmark"}},
+	}
+)
+
+// HandleAllocationListSuccessfully sets up the test server to respond to a allocation List request.
+func HandleAllocationListSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/allocations", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		w.Header().Add("Content-Type", "application/json")
+		r.ParseForm()
+
+		marker := r.Form.Get("marker")
+		switch marker {
+		case "":
+			fmt.Fprintf(w, AllocationListBody)
+
+		case "eff80f47-75f0-4d41-b1aa-cf07c201adac":
+			fmt.Fprintf(w, `{ "allocations": [] }`)
+		default:
+			t.Fatalf("/allocations invoked with unexpected marker=[%s]", marker)
+		}
+	})
+}
+
+// HandleAllocationCreationSuccessfully sets up the test server to respond to a allocation creation request
+// with a given response.
+func HandleAllocationCreationSuccessfully(t *testing.T, response string) {
+	th.Mux.HandleFunc("/allocations", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t, r, `{
+    		"name": "allocation-1",
+    		"resource_class": "baremetal",
+			"candidate_nodes": ["344a3e2-978a-444e-990a-cbf47c62ef88"],
+		 	"traits": ["foo"]
+        }`)
+
+		w.WriteHeader(http.StatusAccepted)
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, response)
+	})
+}
+
+// HandleAllocationDeletionSuccessfully sets up the test server to respond to a allocation deletion request.
+func HandleAllocationDeletionSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/allocations/344a3e2-978a-444e-990a-cbf47c62ef88", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
+
+func HandleAllocationGetSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/allocations/344a3e2-978a-444e-990a-cbf47c62ef88", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+
+		fmt.Fprintf(w, SingleAllocationBody)
+	})
+}
diff --git a/openstack/baremetal/v1/allocations/testing/requests_test.go b/openstack/baremetal/v1/allocations/testing/requests_test.go
new file mode 100644
index 0000000..3579f16
--- /dev/null
+++ b/openstack/baremetal/v1/allocations/testing/requests_test.go
@@ -0,0 +1,79 @@
+package testing
+
+import (
+	"testing"
+
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git/openstack/baremetal/v1/allocations"
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git/pagination"
+	th "gerrit.mcp.mirantis.net/debian/gophercloud.git/testhelper"
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git/testhelper/client"
+)
+
+func TestListAllocations(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleAllocationListSuccessfully(t)
+
+	pages := 0
+	err := allocations.List(client.ServiceClient(), allocations.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := allocations.ExtractAllocations(page)
+		if err != nil {
+			return false, err
+		}
+
+		if len(actual) != 2 {
+			t.Fatalf("Expected 2 allocations, got %d", len(actual))
+		}
+		th.AssertEquals(t, "5344a3e2-978a-444e-990a-cbf47c62ef88", actual[0].UUID)
+		th.AssertEquals(t, "eff80f47-75f0-4d41-b1aa-cf07c201adac", actual[1].UUID)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+
+	if pages != 1 {
+		t.Errorf("Expected 1 page, saw %d", pages)
+	}
+}
+
+func TestCreateAllocation(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleAllocationCreationSuccessfully(t, SingleAllocationBody)
+
+	actual, err := allocations.Create(client.ServiceClient(), allocations.CreateOpts{
+		Name:           "allocation-1",
+		ResourceClass:  "baremetal",
+		CandidateNodes: []string{"344a3e2-978a-444e-990a-cbf47c62ef88"},
+		Traits:         []string{"foo"},
+	}).Extract()
+	th.AssertNoErr(t, err)
+
+	th.CheckDeepEquals(t, Allocation1, *actual)
+}
+
+func TestDeleteAllocation(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleAllocationDeletionSuccessfully(t)
+
+	res := allocations.Delete(client.ServiceClient(), "344a3e2-978a-444e-990a-cbf47c62ef88")
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestGetAllocation(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleAllocationGetSuccessfully(t)
+
+	c := client.ServiceClient()
+	actual, err := allocations.Get(c, "344a3e2-978a-444e-990a-cbf47c62ef88").Extract()
+	if err != nil {
+		t.Fatalf("Unexpected Get error: %v", err)
+	}
+
+	th.CheckDeepEquals(t, Allocation1, *actual)
+}
diff --git a/openstack/baremetal/v1/allocations/urls.go b/openstack/baremetal/v1/allocations/urls.go
new file mode 100644
index 0000000..11529ad
--- /dev/null
+++ b/openstack/baremetal/v1/allocations/urls.go
@@ -0,0 +1,23 @@
+package allocations
+
+import "gerrit.mcp.mirantis.net/debian/gophercloud.git"
+
+func createURL(client *gophercloud.ServiceClient) string {
+	return client.ServiceURL("allocations")
+}
+
+func listURL(client *gophercloud.ServiceClient) string {
+	return createURL(client)
+}
+
+func resourceURL(client *gophercloud.ServiceClient, id string) string {
+	return client.ServiceURL("allocations", id)
+}
+
+func deleteURL(client *gophercloud.ServiceClient, id string) string {
+	return resourceURL(client, id)
+}
+
+func getURL(client *gophercloud.ServiceClient, id string) string {
+	return resourceURL(client, id)
+}
diff --git a/openstack/baremetal/v1/drivers/doc.go b/openstack/baremetal/v1/drivers/doc.go
new file mode 100644
index 0000000..252c01b
--- /dev/null
+++ b/openstack/baremetal/v1/drivers/doc.go
@@ -0,0 +1,43 @@
+/*
+Package drivers contains the functionality for Listing drivers, driver details,
+driver properties and driver logical disk properties
+
+API reference: https://developer.openstack.org/api-ref/baremetal/#drivers-drivers
+
+Example to List Drivers
+
+	drivers.ListDrivers(client.ServiceClient(), drivers.ListDriversOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		driversList, err := drivers.ExtractDrivers(page)
+		if err != nil {
+			return false, err
+		}
+
+		for _, n := range driversList {
+			// Do something
+		}
+
+		return true, nil
+	})
+
+Example to Get single Driver Details
+
+	showDriverDetails, err := drivers.GetDriverDetails(client, "ipmi").Extract()
+	if err != nil {
+		panic(err)
+	}
+
+Example to Get single Driver Properties
+
+	showDriverProperties, err := drivers.GetDriverProperties(client, "ipmi").Extract()
+	if err != nil {
+		panic(err)
+	}
+
+Example to Get single Driver Logical Disk Properties
+
+	showDriverDiskProperties, err := drivers.GetDriverDiskProperties(client, "ipmi").Extract()
+	if err != nil {
+		panic(err)
+	}
+*/
+package drivers
diff --git a/openstack/baremetal/v1/drivers/requests.go b/openstack/baremetal/v1/drivers/requests.go
index 8e338e4..f50baa6 100644
--- a/openstack/baremetal/v1/drivers/requests.go
+++ b/openstack/baremetal/v1/drivers/requests.go
@@ -5,8 +5,66 @@
 	"gerrit.mcp.mirantis.net/debian/gophercloud.git/pagination"
 )
 
-func List(c *gophercloud.ServiceClient) pagination.Pager {
-	return pagination.NewPager(c, listURL(c), func(r pagination.PageResult) pagination.Page {
-		return DriverPage{pagination.SinglePageBase(r)}
+// ListDriversOptsBuilder allows extensions to add additional parameters to the
+// ListDrivers request.
+type ListDriversOptsBuilder interface {
+	ToListDriversOptsQuery() (string, error)
+}
+
+// ListDriversOpts defines query options that can be passed to ListDrivers
+type ListDriversOpts struct {
+	// Provide detailed information about the drivers
+	Detail bool `q:"detail"`
+
+	// Filter the list by the type of the driver
+	Type string `q:"type"`
+}
+
+// ToListDriversOptsQuery formats a ListOpts into a query string
+func (opts ListDriversOpts) ToListDriversOptsQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	return q.String(), err
+}
+
+// ListDrivers makes a request against the API to list all drivers
+func ListDrivers(client *gophercloud.ServiceClient, opts ListDriversOptsBuilder) pagination.Pager {
+	url := driversURL(client)
+	if opts != nil {
+		query, err := opts.ToListDriversOptsQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+		return DriverPage{pagination.LinkedPageBase{PageResult: r}}
 	})
 }
+
+// GetDriverDetails Shows details for a driver
+func GetDriverDetails(client *gophercloud.ServiceClient, driverName string) (r GetDriverResult) {
+	_, r.Err = client.Get(driverDetailsURL(client, driverName), &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+// GetDriverProperties Shows the required and optional parameters that
+// driverName expects to be supplied in the driver_info field for every
+// Node it manages
+func GetDriverProperties(client *gophercloud.ServiceClient, driverName string) (r GetPropertiesResult) {
+	_, r.Err = client.Get(driverPropertiesURL(client, driverName), &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+// GetDriverDiskProperties Show the required and optional parameters that
+// driverName expects to be supplied in the node’s raid_config field, if a
+// RAID configuration change is requested.
+func GetDriverDiskProperties(client *gophercloud.ServiceClient, driverName string) (r GetDiskPropertiesResult) {
+	_, r.Err = client.Get(driverDiskPropertiesURL(client, driverName), &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
diff --git a/openstack/baremetal/v1/drivers/results.go b/openstack/baremetal/v1/drivers/results.go
index b3efacd..c453a09 100644
--- a/openstack/baremetal/v1/drivers/results.go
+++ b/openstack/baremetal/v1/drivers/results.go
@@ -5,59 +5,194 @@
 	"gerrit.mcp.mirantis.net/debian/gophercloud.git/pagination"
 )
 
-type commonResult struct {
+type driverResult struct {
 	gophercloud.Result
 }
 
-func (r commonResult) Extract() (*Driver, error) {
-	var d struct {
-		Driver *Driver `json:"driver"`
-	}
-	err := r.ExtractInto(&d)
-	return d.Driver, err
+// Extract interprets any driverResult as a Driver, if possible.
+func (r driverResult) Extract() (*Driver, error) {
+	var s Driver
+	err := r.ExtractInto(&s)
+	return &s, err
 }
 
-type GetResult struct {
-	commonResult
+func (r driverResult) ExtractInto(v interface{}) error {
+	return r.Result.ExtractIntoStructPtr(v, "")
 }
 
-type Link struct {
-	Href string `json:"href"`
-	Rel  string `json:"rel"`
+func ExtractDriversInto(r pagination.Page, v interface{}) error {
+	return r.(DriverPage).Result.ExtractIntoSlicePtr(v, "drivers")
 }
 
+// Driver represents a driver in the OpenStack Bare Metal API.
 type Driver struct {
-	Name       string   `json:"name"`
-	Type       string   `json:"type"`
-	Hosts      []string `json:"hosts"`
-	Links      []Link   `json:"links"`
-	Properties []Link   `json:"properties"`
+	// Name and Identifier of the driver
+	Name string `json:"name"`
+
+	// A list of active hosts that support this driver
+	Hosts []string `json:"hosts"`
+
+	// Type of this driver (“classic” or “dynamic”)
+	Type string `json:"type"`
+
+	// The default bios interface used for a node with a dynamic driver,
+	// if no bios interface is specified for the node.
+	DefaultBiosInterface string `json:"default_bios_interface"`
+
+	// The default boot interface used for a node with a dynamic driver,
+	// if no boot interface is specified for the node.
+	DefaultBootInterface string `json:"default_boot_interface"`
+
+	// The default console interface used for a node with a dynamic driver,
+	// if no console interface is specified for the node.
+	DefaultConsoleInterface string `json:"default_console_interface"`
+
+	// The default deploy interface used for a node with a dynamic driver,
+	// if no deploy interface is specified for the node.
+	DefaultDeployInterface string `json:"default_deploy_interface"`
+
+	// The default inspection interface used for a node with a dynamic driver,
+	// if no inspection interface is specified for the node.
+	DefaultInspectInterface string `json:"default_inspect_interface"`
+
+	// The default management interface used for a node with a dynamic driver,
+	// if no management interface is specified for the node.
+	DefaultManagementInterface string `json:"default_management_interface"`
+
+	// The default network interface used for a node with a dynamic driver,
+	// if no network interface is specified for the node.
+	DefaultNetworkInterface string `json:"default_network_interface"`
+
+	// The default power interface used for a node with a dynamic driver,
+	// if no power interface is specified for the node.
+	DefaultPowerInterface string `json:"default_power_interface"`
+
+	// The default RAID interface used for a node with a dynamic driver,
+	// if no RAID interface is specified for the node.
+	DefaultRaidInterface string `json:"default_raid_interface"`
+
+	// The default rescue interface used for a node with a dynamic driver,
+	// if no rescue interface is specified for the node.
+	DefaultRescueInterface string `json:"default_rescue_interface"`
+
+	// The default storage interface used for a node with a dynamic driver,
+	// if no storage interface is specified for the node.
+	DefaultStorageInterface string `json:"default_storage_interface"`
+
+	// The default vendor interface used for a node with a dynamic driver,
+	// if no vendor interface is specified for the node.
+	DefaultVendorInterface string `json:"default_vendor_interface"`
+
+	// The enabled bios interfaces for this driver.
+	EnabledBiosInterfaces []string `json:"enabled_bios_interfaces"`
+
+	// The enabled boot interfaces for this driver.
+	EnabledBootInterfaces []string `json:"enabled_boot_interfaces"`
+
+	// The enabled console interfaces for this driver.
+	EnabledConsoleInterface []string `json:"enabled_console_interfaces"`
+
+	// The enabled deploy interfaces for this driver.
+	EnabledDeployInterfaces []string `json:"enabled_deploy_interfaces"`
+
+	// The enabled inspection interfaces for this driver.
+	EnabledInspectInterfaces []string `json:"enabled_inspect_interfaces"`
+
+	// The enabled management interfaces for this driver.
+	EnabledManagementInterfaces []string `json:"enabled_management_interfaces"`
+
+	// The enabled network interfaces for this driver.
+	EnabledNetworkInterfaces []string `json:"enabled_network_interfaces"`
+
+	// The enabled power interfaces for this driver.
+	EnabledPowerInterfaces []string `json:"enabled_power_interfaces"`
+
+	// The enabled rescue interfaces for this driver.
+	EnabledRescueInterfaces []string `json:"enabled_rescue_interfaces"`
+
+	// The enabled RAID interfaces for this driver.
+	EnabledRaidInterfaces []string `json:"enabled_raid_interfaces"`
+
+	// The enabled storage interfaces for this driver.
+	EnabledStorageInterfaces []string `json:"enabled_storage_interfaces"`
+
+	// The enabled vendor interfaces for this driver.
+	EnabledVendorInterfaces []string `json:"enabled_vendor_interfaces"`
+
+	//A list of relative links. Includes the self and bookmark links.
+	Links []interface{} `json:"links"`
+
+	// A list of links to driver properties.
+	Properties []interface{} `json:"properties"`
 }
 
+// DriverPage abstracts the raw results of making a ListDrivers() request
+// against the API.
 type DriverPage struct {
-	pagination.SinglePageBase
+	pagination.LinkedPageBase
 }
 
+// IsEmpty returns true if a page contains no Driver results.
+func (r DriverPage) IsEmpty() (bool, error) {
+	s, err := ExtractDrivers(r)
+	return len(s) == 0, err
+}
+
+// NextPageURL uses the response's embedded link reference to navigate to the
+// next page of results.
 func (r DriverPage) NextPageURL() (string, error) {
-	var d struct {
+	var s struct {
 		Links []gophercloud.Link `json:"drivers_links"`
 	}
-	err := r.ExtractInto(&d)
+	err := r.ExtractInto(&s)
 	if err != nil {
 		return "", err
 	}
-	return gophercloud.ExtractNextURL(d.Links)
+	return gophercloud.ExtractNextURL(s.Links)
 }
 
-func (r DriverPage) IsEmpty() (bool, error) {
-	is, err := ExtractDrivers(r)
-	return len(is) == 0, err
-}
-
+// ExtractDrivers interprets the results of a single page from ListDrivers()
+// call, producing a slice of Driver entities.
 func ExtractDrivers(r pagination.Page) ([]Driver, error) {
-	var d struct {
-		Drivers []Driver `json:"drivers"`
-	}
-	err := (r.(DriverPage)).ExtractInto(&d)
-	return d.Drivers, err
+	var s []Driver
+	err := ExtractDriversInto(r, &s)
+	return s, err
+}
+
+// GetDriverResult is the response from a Get operation.
+// Call its Extract method to interpret it as a Driver.
+type GetDriverResult struct {
+	driverResult
+}
+
+// DriverProperties represents driver properties in the OpenStack Bare Metal API.
+type DriverProperties map[string]interface{}
+
+// Extract interprets any GetPropertiesResult as DriverProperties, if possible.
+func (r GetPropertiesResult) Extract() (*DriverProperties, error) {
+	var s DriverProperties
+	err := r.ExtractInto(&s)
+	return &s, err
+}
+
+// GetPropertiesResult is the response from a GetDriverProperties operation.
+// Call its Extract method to interpret it as DriverProperties.
+type GetPropertiesResult struct {
+	gophercloud.Result
+}
+
+// DiskProperties represents driver disk properties in the OpenStack Bare Metal API.
+type DiskProperties map[string]interface{}
+
+// Extract interprets any GetDiskPropertiesResult as DiskProperties, if possible.
+func (r GetDiskPropertiesResult) Extract() (*DiskProperties, error) {
+	var s DiskProperties
+	err := r.ExtractInto(&s)
+	return &s, err
+}
+
+// GetDiskPropertiesResult is the response from a GetDriverDiskProperties operation.
+// Call its Extract method to interpret it as DiskProperties.
+type GetDiskPropertiesResult struct {
+	gophercloud.Result
 }
diff --git a/openstack/baremetal/v1/drivers/testing/doc.go b/openstack/baremetal/v1/drivers/testing/doc.go
new file mode 100644
index 0000000..266af92
--- /dev/null
+++ b/openstack/baremetal/v1/drivers/testing/doc.go
@@ -0,0 +1,2 @@
+// Package testing contains drivers unit tests
+package testing
diff --git a/openstack/baremetal/v1/drivers/testing/fixtures.go b/openstack/baremetal/v1/drivers/testing/fixtures.go
new file mode 100644
index 0000000..7504030
--- /dev/null
+++ b/openstack/baremetal/v1/drivers/testing/fixtures.go
@@ -0,0 +1,407 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git/openstack/baremetal/v1/drivers"
+	th "gerrit.mcp.mirantis.net/debian/gophercloud.git/testhelper"
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git/testhelper/client"
+)
+
+// ListDriversBody contains the canned body of a drivers.ListDrivers response, without details.
+const ListDriversBody = `
+{
+  "drivers": [
+    {
+      "hosts": [
+        "897ab1dad809"
+      ],
+      "links": [
+        {
+          "href": "http://127.0.0.1:6385/v1/drivers/agent_ipmitool",
+          "rel": "self"
+        },
+        {
+          "href": "http://127.0.0.1:6385/drivers/agent_ipmitool",
+          "rel": "bookmark"
+        }
+      ],
+      "name": "agent_ipmitool",
+      "properties": [
+        {
+          "href": "http://127.0.0.1:6385/v1/drivers/agent_ipmitool/properties",
+          "rel": "self"
+        },
+        {
+          "href": "http://127.0.0.1:6385/drivers/agent_ipmitool/properties",
+          "rel": "bookmark"
+        }
+      ],
+      "type": "classic"
+    },
+    {
+      "hosts": [
+        "897ab1dad809"
+      ],
+      "links": [
+        {
+          "href": "http://127.0.0.1:6385/v1/drivers/fake",
+          "rel": "self"
+        },
+        {
+          "href": "http://127.0.0.1:6385/drivers/fake",
+          "rel": "bookmark"
+        }
+      ],
+      "name": "fake",
+      "properties": [
+        {
+          "href": "http://127.0.0.1:6385/v1/drivers/fake/properties",
+          "rel": "self"
+        },
+        {
+          "href": "http://127.0.0.1:6385/drivers/fake/properties",
+          "rel": "bookmark"
+        }
+      ],
+      "type": "classic"
+    },
+    {
+      "hosts": [
+        "897ab1dad809"
+      ],
+      "links": [
+        {
+          "href": "http://127.0.0.1:6385/v1/drivers/ipmi",
+          "rel": "self"
+        },
+        {
+          "href": "http://127.0.0.1:6385/drivers/ipmi",
+          "rel": "bookmark"
+        }
+      ],
+      "name": "ipmi",
+      "properties": [
+        {
+          "href": "http://127.0.0.1:6385/v1/drivers/ipmi/properties",
+          "rel": "self"
+        },
+        {
+          "href": "http://127.0.0.1:6385/drivers/ipmi/properties",
+          "rel": "bookmark"
+        }
+      ],
+      "type": "dynamic"
+    }
+  ]
+}
+`
+const SingleDriverDetails = `
+{
+  "default_bios_interface": "no-bios",
+  "default_boot_interface": "pxe",
+  "default_console_interface": "no-console",
+  "default_deploy_interface": "iscsi",
+  "default_inspect_interface": "no-inspect",
+  "default_management_interface": "ipmitool",
+  "default_network_interface": "flat",
+  "default_power_interface": "ipmitool",
+  "default_raid_interface": "no-raid",
+  "default_rescue_interface": "no-rescue",
+  "default_storage_interface": "noop",
+  "default_vendor_interface": "no-vendor",
+  "enabled_bios_interfaces": [
+    "no-bios"
+  ],
+  "enabled_boot_interfaces": [
+    "pxe"
+  ],
+  "enabled_console_interfaces": [
+    "no-console"
+  ],
+  "enabled_deploy_interfaces": [
+    "iscsi",
+    "direct"
+  ],
+  "enabled_inspect_interfaces": [
+    "no-inspect"
+  ],
+  "enabled_management_interfaces": [
+    "ipmitool"
+  ],
+  "enabled_network_interfaces": [
+    "flat",
+    "noop"
+  ],
+  "enabled_power_interfaces": [
+    "ipmitool"
+  ],
+  "enabled_raid_interfaces": [
+    "no-raid",
+    "agent"
+  ],
+  "enabled_rescue_interfaces": [
+    "no-rescue"
+  ],
+  "enabled_storage_interfaces": [
+    "noop"
+  ],
+  "enabled_vendor_interfaces": [
+    "no-vendor"
+  ],
+  "hosts": [
+    "897ab1dad809"
+  ],
+  "links": [
+    {
+      "href": "http://127.0.0.1:6385/v1/drivers/ipmi",
+      "rel": "self"
+    },
+    {
+      "href": "http://127.0.0.1:6385/drivers/ipmi",
+      "rel": "bookmark"
+    }
+  ],
+  "name": "ipmi",
+  "properties": [
+    {
+      "href": "http://127.0.0.1:6385/v1/drivers/ipmi/properties",
+      "rel": "self"
+    },
+    {
+      "href": "http://127.0.0.1:6385/drivers/ipmi/properties",
+      "rel": "bookmark"
+    }
+  ],
+  "type": "dynamic"
+}
+`
+
+const SingleDriverProperties = `
+{
+  "deploy_forces_oob_reboot": "Whether Ironic should force a reboot of the Node via the out-of-band channel after deployment is complete. Provides compatibility with older deploy ramdisks. Defaults to False. Optional.",
+  "deploy_kernel": "UUID (from Glance) of the deployment kernel. Required.",
+  "deploy_ramdisk": "UUID (from Glance) of the ramdisk that is mounted at boot time. Required.",
+  "image_http_proxy": "URL of a proxy server for HTTP connections. Optional.",
+  "image_https_proxy": "URL of a proxy server for HTTPS connections. Optional.",
+  "image_no_proxy": "A comma-separated list of host names, IP addresses and domain names (with optional :port) that will be excluded from proxying. To denote a domain name, use a dot to prefix the domain name. This value will be ignored if ` + "``image_http_proxy`` and ``image_https_proxy``" + ` are not specified. Optional.",
+  "ipmi_address": "IP address or hostname of the node. Required.",
+  "ipmi_bridging": "bridging_type; default is \"no\". One of \"single\", \"dual\", \"no\". Optional.",
+  "ipmi_disable_boot_timeout": "By default ironic will send a raw IPMI command to disable the 60 second timeout for booting.  Setting this option to False will NOT send that command; default value is True. Optional.",
+  "ipmi_force_boot_device": "Whether Ironic should specify the boot device to the BMC each time the server is turned on, eg. because the BMC is not capable of remembering the selected boot device across power cycles; default value is False. Optional.",
+  "ipmi_local_address": "local IPMB address for bridged requests. Used only if ipmi_bridging is set to \"single\" or \"dual\". Optional.",
+  "ipmi_password": "password. Optional.",
+  "ipmi_port": "remote IPMI RMCP port. Optional.",
+  "ipmi_priv_level": "privilege level; default is ADMINISTRATOR. One of ADMINISTRATOR, CALLBACK, OPERATOR, USER. Optional.",
+  "ipmi_protocol_version": "the version of the IPMI protocol; default is \"2.0\". One of \"1.5\", \"2.0\". Optional.",
+  "ipmi_target_address": "destination address for bridged request. Required only if ipmi_bridging is set to \"single\" or \"dual\".",
+  "ipmi_target_channel": "destination channel for bridged request. Required only if ipmi_bridging is set to \"single\" or \"dual\".",
+  "ipmi_terminal_port": "node's UDP port to connect to. Only required for console access.",
+  "ipmi_transit_address": "transit address for bridged request. Required only if ipmi_bridging is set to \"dual\".",
+  "ipmi_transit_channel": "transit channel for bridged request. Required only if ipmi_bridging is set to \"dual\".",
+  "ipmi_username": "username; default is NULL user. Optional."
+}
+`
+
+const SingleDriverDiskProperties = `
+{
+  "controller": "Controller to use for this logical disk. If not specified, the driver will choose a suitable RAID controller on the bare metal node. Optional.",
+  "disk_type": "The type of disk preferred. Valid values are 'hdd' and 'ssd'. If this is not specified, disk type will not be a selection criterion for choosing backing physical disks. Optional.",
+  "interface_type": "The interface type of disk. Valid values are 'sata', 'scsi' and 'sas'. If this is not specified, interface type will not be a selection criterion for choosing backing physical disks. Optional.",
+  "is_root_volume": "Specifies whether this disk is a root volume. By default, this is False. Optional.",
+  "number_of_physical_disks": "Number of physical disks to use for this logical disk. By default, the driver uses the minimum number of disks required for that RAID level. Optional.",
+  "physical_disks": "The physical disks to use for this logical disk. If not specified, the driver will choose suitable physical disks to use. Optional.",
+  "raid_level": "RAID level for the logical disk. Valid values are 'JBOD', '0', '1', '2', '5', '6', '1+0', '5+0' and '6+0'. Required.",
+  "share_physical_disks": "Specifies whether other logical disks can share physical disks with this logical disk. By default, this is False. Optional.",
+  "size_gb": "Size in GiB (Integer) for the logical disk. Use 'MAX' as size_gb if this logical disk is supposed to use the rest of the space available. Required.",
+  "volume_name": "Name of the volume to be created. If this is not specified, it will be auto-generated. Optional."
+}
+`
+
+var (
+	DriverAgentIpmitool = drivers.Driver{
+		Name:  "agent_ipmitool",
+		Type:  "classic",
+		Hosts: []string{"897ab1dad809"},
+		Links: []interface{}{
+			map[string]interface{}{
+				"href": "http://127.0.0.1:6385/v1/drivers/agent_ipmitool",
+				"rel":  "self",
+			},
+			map[string]interface{}{
+				"href": "http://127.0.0.1:6385/drivers/agent_ipmitool",
+				"rel":  "bookmark",
+			},
+		},
+		Properties: []interface{}{
+			map[string]interface{}{
+				"href": "http://127.0.0.1:6385/v1/drivers/agent_ipmitool/properties",
+				"rel":  "self",
+			},
+			map[string]interface{}{
+				"href": "http://127.0.0.1:6385/drivers/agent_ipmitool/properties",
+				"rel":  "bookmark",
+			},
+		},
+	}
+
+	DriverFake = drivers.Driver{
+		Name:  "fake",
+		Type:  "classic",
+		Hosts: []string{"897ab1dad809"},
+		Links: []interface{}{
+			map[string]interface{}{
+				"href": "http://127.0.0.1:6385/v1/drivers/fake",
+				"rel":  "self",
+			},
+			map[string]interface{}{
+				"href": "http://127.0.0.1:6385/drivers/fake",
+				"rel":  "bookmark",
+			},
+		},
+		Properties: []interface{}{
+			map[string]interface{}{
+				"href": "http://127.0.0.1:6385/v1/drivers/fake/properties",
+				"rel":  "self",
+			},
+			map[string]interface{}{
+				"href": "http://127.0.0.1:6385/drivers/fake/properties",
+				"rel":  "bookmark",
+			},
+		},
+	}
+
+	DriverIpmi = drivers.Driver{
+		Name:                        "ipmi",
+		Type:                        "dynamic",
+		Hosts:                       []string{"897ab1dad809"},
+		DefaultBiosInterface:        "no-bios",
+		DefaultBootInterface:        "pxe",
+		DefaultConsoleInterface:     "no-console",
+		DefaultDeployInterface:      "iscsi",
+		DefaultInspectInterface:     "no-inspect",
+		DefaultManagementInterface:  "ipmitool",
+		DefaultNetworkInterface:     "flat",
+		DefaultPowerInterface:       "ipmitool",
+		DefaultRaidInterface:        "no-raid",
+		DefaultRescueInterface:      "no-rescue",
+		DefaultStorageInterface:     "noop",
+		DefaultVendorInterface:      "no-vendor",
+		EnabledBiosInterfaces:       []string{"no-bios"},
+		EnabledBootInterfaces:       []string{"pxe"},
+		EnabledConsoleInterface:     []string{"no-console"},
+		EnabledDeployInterfaces:     []string{"iscsi", "direct"},
+		EnabledInspectInterfaces:    []string{"no-inspect"},
+		EnabledManagementInterfaces: []string{"ipmitool"},
+		EnabledNetworkInterfaces:    []string{"flat", "noop"},
+		EnabledPowerInterfaces:      []string{"ipmitool"},
+		EnabledRescueInterfaces:     []string{"no-rescue"},
+		EnabledRaidInterfaces:       []string{"no-raid", "agent"},
+		EnabledStorageInterfaces:    []string{"noop"},
+		EnabledVendorInterfaces:     []string{"no-vendor"},
+		Links: []interface{}{
+			map[string]interface{}{
+				"href": "http://127.0.0.1:6385/v1/drivers/ipmi",
+				"rel":  "self",
+			},
+			map[string]interface{}{
+				"href": "http://127.0.0.1:6385/drivers/ipmi",
+				"rel":  "bookmark",
+			},
+		},
+		Properties: []interface{}{
+			map[string]interface{}{
+				"href": "http://127.0.0.1:6385/v1/drivers/ipmi/properties",
+				"rel":  "self",
+			},
+			map[string]interface{}{
+				"href": "http://127.0.0.1:6385/drivers/ipmi/properties",
+				"rel":  "bookmark",
+			},
+		},
+	}
+
+	DriverIpmiToolProperties = drivers.DriverProperties{
+		"deploy_forces_oob_reboot":  "Whether Ironic should force a reboot of the Node via the out-of-band channel after deployment is complete. Provides compatibility with older deploy ramdisks. Defaults to False. Optional.",
+		"deploy_kernel":             "UUID (from Glance) of the deployment kernel. Required.",
+		"deploy_ramdisk":            "UUID (from Glance) of the ramdisk that is mounted at boot time. Required.",
+		"image_http_proxy":          "URL of a proxy server for HTTP connections. Optional.",
+		"image_https_proxy":         "URL of a proxy server for HTTPS connections. Optional.",
+		"image_no_proxy":            "A comma-separated list of host names, IP addresses and domain names (with optional :port) that will be excluded from proxying. To denote a domain name, use a dot to prefix the domain name. This value will be ignored if ``image_http_proxy`` and ``image_https_proxy`` are not specified. Optional.",
+		"ipmi_address":              "IP address or hostname of the node. Required.",
+		"ipmi_bridging":             "bridging_type; default is \"no\". One of \"single\", \"dual\", \"no\". Optional.",
+		"ipmi_disable_boot_timeout": "By default ironic will send a raw IPMI command to disable the 60 second timeout for booting.  Setting this option to False will NOT send that command; default value is True. Optional.",
+		"ipmi_force_boot_device":    "Whether Ironic should specify the boot device to the BMC each time the server is turned on, eg. because the BMC is not capable of remembering the selected boot device across power cycles; default value is False. Optional.",
+		"ipmi_local_address":        "local IPMB address for bridged requests. Used only if ipmi_bridging is set to \"single\" or \"dual\". Optional.",
+		"ipmi_password":             "password. Optional.",
+		"ipmi_port":                 "remote IPMI RMCP port. Optional.",
+		"ipmi_priv_level":           "privilege level; default is ADMINISTRATOR. One of ADMINISTRATOR, CALLBACK, OPERATOR, USER. Optional.",
+		"ipmi_protocol_version":     "the version of the IPMI protocol; default is \"2.0\". One of \"1.5\", \"2.0\". Optional.",
+		"ipmi_target_address":       "destination address for bridged request. Required only if ipmi_bridging is set to \"single\" or \"dual\".",
+		"ipmi_target_channel":       "destination channel for bridged request. Required only if ipmi_bridging is set to \"single\" or \"dual\".",
+		"ipmi_terminal_port":        "node's UDP port to connect to. Only required for console access.",
+		"ipmi_transit_address":      "transit address for bridged request. Required only if ipmi_bridging is set to \"dual\".",
+		"ipmi_transit_channel":      "transit channel for bridged request. Required only if ipmi_bridging is set to \"dual\".",
+		"ipmi_username":             "username; default is NULL user. Optional.",
+	}
+
+	DriverIpmiToolDisk = drivers.DiskProperties{
+		"controller":               "Controller to use for this logical disk. If not specified, the driver will choose a suitable RAID controller on the bare metal node. Optional.",
+		"disk_type":                "The type of disk preferred. Valid values are 'hdd' and 'ssd'. If this is not specified, disk type will not be a selection criterion for choosing backing physical disks. Optional.",
+		"interface_type":           "The interface type of disk. Valid values are 'sata', 'scsi' and 'sas'. If this is not specified, interface type will not be a selection criterion for choosing backing physical disks. Optional.",
+		"is_root_volume":           "Specifies whether this disk is a root volume. By default, this is False. Optional.",
+		"number_of_physical_disks": "Number of physical disks to use for this logical disk. By default, the driver uses the minimum number of disks required for that RAID level. Optional.",
+		"physical_disks":           "The physical disks to use for this logical disk. If not specified, the driver will choose suitable physical disks to use. Optional.",
+		"raid_level":               "RAID level for the logical disk. Valid values are 'JBOD', '0', '1', '2', '5', '6', '1+0', '5+0' and '6+0'. Required.",
+		"share_physical_disks":     "Specifies whether other logical disks can share physical disks with this logical disk. By default, this is False. Optional.",
+		"size_gb":                  "Size in GiB (Integer) for the logical disk. Use 'MAX' as size_gb if this logical disk is supposed to use the rest of the space available. Required.",
+		"volume_name":              "Name of the volume to be created. If this is not specified, it will be auto-generated. Optional.",
+	}
+)
+
+// HandleListDriversSuccessfully sets up the test server to respond to a drivers ListDrivers request.
+func HandleListDriversSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/drivers", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		w.Header().Add("Content-Type", "application/json")
+		r.ParseForm()
+
+		fmt.Fprintf(w, ListDriversBody)
+	})
+}
+
+// HandleGetDriverDetailsSuccessfully sets up the test server to respond to a drivers GetDriverDetails request.
+func HandleGetDriverDetailsSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/drivers/ipmi", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+
+		fmt.Fprintf(w, SingleDriverDetails)
+	})
+}
+
+// HandleGetDriverPropertiesSuccessfully sets up the test server to respond to a drivers GetDriverProperties request.
+func HandleGetDriverPropertiesSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/drivers/agent_ipmitool/properties", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+
+		fmt.Fprintf(w, SingleDriverProperties)
+	})
+}
+
+// HandleGetDriverDiskPropertiesSuccessfully sets up the test server to respond to a drivers GetDriverDiskProperties request.
+func HandleGetDriverDiskPropertiesSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/drivers/agent_ipmitool/raid/logical_disk_properties", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+
+		fmt.Fprintf(w, SingleDriverDiskProperties)
+	})
+}
diff --git a/openstack/baremetal/v1/drivers/testing/requests_test.go b/openstack/baremetal/v1/drivers/testing/requests_test.go
new file mode 100644
index 0000000..3a9ddf9
--- /dev/null
+++ b/openstack/baremetal/v1/drivers/testing/requests_test.go
@@ -0,0 +1,84 @@
+package testing
+
+import (
+	"testing"
+
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git/openstack/baremetal/v1/drivers"
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git/pagination"
+	th "gerrit.mcp.mirantis.net/debian/gophercloud.git/testhelper"
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git/testhelper/client"
+)
+
+func TestListDrivers(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListDriversSuccessfully(t)
+
+	pages := 0
+	err := drivers.ListDrivers(client.ServiceClient(), drivers.ListDriversOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := drivers.ExtractDrivers(page)
+		if err != nil {
+			return false, err
+		}
+
+		if len(actual) != 3 {
+			t.Fatalf("Expected 3 drivers, got %d", len(actual))
+		}
+
+		th.CheckDeepEquals(t, DriverAgentIpmitool, actual[0])
+		th.CheckDeepEquals(t, DriverFake, actual[1])
+		th.AssertEquals(t, "ipmi", actual[2].Name)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+
+	if pages != 1 {
+		t.Errorf("Expected 1 page, saw %d", pages)
+	}
+}
+
+func TestGetDriverDetails(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGetDriverDetailsSuccessfully(t)
+
+	c := client.ServiceClient()
+	actual, err := drivers.GetDriverDetails(c, "ipmi").Extract()
+	if err != nil {
+		t.Fatalf("Unexpected Get error: %v", err)
+	}
+
+	th.CheckDeepEquals(t, DriverIpmi, *actual)
+}
+
+func TestGetDriverProperties(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGetDriverPropertiesSuccessfully(t)
+
+	c := client.ServiceClient()
+	actual, err := drivers.GetDriverProperties(c, "agent_ipmitool").Extract()
+	if err != nil {
+		t.Fatalf("Unexpected Get error: %v", err)
+	}
+
+	th.CheckDeepEquals(t, DriverIpmiToolProperties, *actual)
+}
+
+func TestGetDriverDiskProperties(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGetDriverDiskPropertiesSuccessfully(t)
+
+	c := client.ServiceClient()
+	actual, err := drivers.GetDriverDiskProperties(c, "agent_ipmitool").Extract()
+	if err != nil {
+		t.Fatalf("Unexpected Get error: %v", err)
+	}
+
+	th.CheckDeepEquals(t, DriverIpmiToolDisk, *actual)
+}
diff --git a/openstack/baremetal/v1/drivers/urls.go b/openstack/baremetal/v1/drivers/urls.go
index 0996543..c8ebbee 100644
--- a/openstack/baremetal/v1/drivers/urls.go
+++ b/openstack/baremetal/v1/drivers/urls.go
@@ -2,6 +2,18 @@
 
 import "gerrit.mcp.mirantis.net/debian/gophercloud.git"
 
-func listURL(c *gophercloud.ServiceClient) string {
-	return c.ServiceURL("drivers")
+func driversURL(client *gophercloud.ServiceClient) string {
+	return client.ServiceURL("drivers")
+}
+
+func driverDetailsURL(client *gophercloud.ServiceClient, driverName string) string {
+	return client.ServiceURL("drivers", driverName)
+}
+
+func driverPropertiesURL(client *gophercloud.ServiceClient, driverName string) string {
+	return client.ServiceURL("drivers", driverName, "properties")
+}
+
+func driverDiskPropertiesURL(client *gophercloud.ServiceClient, driverName string) string {
+	return client.ServiceURL("drivers", driverName, "raid", "logical_disk_properties")
 }
diff --git a/openstack/baremetal/v1/nodes/doc.go b/openstack/baremetal/v1/nodes/doc.go
new file mode 100644
index 0000000..37f60b5
--- /dev/null
+++ b/openstack/baremetal/v1/nodes/doc.go
@@ -0,0 +1,130 @@
+/*
+Package nodes provides information and interaction with the nodes API
+resource in the OpenStack Bare Metal service.
+
+Example to List Nodes with Detail
+
+	nodes.ListDetail(client, nodes.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		nodeList, err := nodes.ExtractNodes(page)
+		if err != nil {
+			return false, err
+		}
+
+		for _, n := range nodeList {
+			// Do something
+		}
+
+		return true, nil
+	})
+
+Example to List Nodes
+
+	listOpts := nodes.ListOpts{
+		ProvisionState: nodes.Deploying,
+		Fields:         []string{"name"},
+	}
+
+	nodes.List(client, listOpts).EachPage(func(page pagination.Page) (bool, error) {
+		nodeList, err := nodes.ExtractNodes(page)
+		if err != nil {
+			return false, err
+		}
+
+		for _, n := range nodeList {
+			// Do something
+		}
+
+		return true, nil
+	})
+
+Example to Create Node
+
+	createOpts := nodes.CreateOpts
+		Driver:        "ipmi",
+		BootInterface: "pxe",
+		Name:          "coconuts",
+		DriverInfo: map[string]interface{}{
+			"ipmi_port":      "6230",
+			"ipmi_username":  "admin",
+			"deploy_kernel":  "http://172.22.0.1/images/tinyipa-stable-rocky.vmlinuz",
+			"ipmi_address":   "192.168.122.1",
+			"deploy_ramdisk": "http://172.22.0.1/images/tinyipa-stable-rocky.gz",
+			"ipmi_password":  "admin",
+		},
+	}
+
+	createNode, err := nodes.Create(client, createOpts).Extract()
+	if err != nil {
+		panic(err)
+	}
+
+Example to Get Node
+
+	showNode, err := nodes.Get(client, "c9afd385-5d89-4ecb-9e1c-68194da6b474").Extract()
+	if err != nil {
+		panic(err)
+	}
+
+Example to Update Node
+
+	updateOpts := nodes.UpdateOpts{
+		nodes.UpdateOperation{
+			Op:    ReplaceOp,
+			Path:  "/maintenance",
+			Value: "true",
+		},
+	}
+
+	updateNode, err := nodes.Update(client, "c9afd385-5d89-4ecb-9e1c-68194da6b474", updateOpts).Extract()
+	if err != nil {
+		panic(err)
+	}
+
+Example to Delete Node
+
+	err = nodes.Delete(client, "c9afd385-5d89-4ecb-9e1c-68194da6b474").ExtractErr()
+	if err != nil {
+		panic(err)
+	}
+
+Example to Validate Node
+
+	validation, err := nodes.Validate(client, "a62b8495-52e2-407b-b3cb-62775d04c2b8").Extract()
+	if err != nil {
+		panic(err)
+	}
+
+Example to inject non-masking interrupts
+
+	err := nodes.InjectNMI(client, "a62b8495-52e2-407b-b3cb-62775d04c2b8").ExtractErr()
+	if err != nil {
+		panic(err)
+	}
+
+Example to get array of supported boot devices for a node
+
+	bootDevices, err := nodes.GetSupportedBootDevices(client, "a62b8495-52e2-407b-b3cb-62775d04c2b8").Extract()
+	if err != nil {
+		panic(err)
+	}
+
+Example to set boot device for a node
+
+	bootOpts := nodes.BootDeviceOpts{
+		BootDevice: "pxe",
+		Persistent: false,
+	}
+
+	err := nodes.SetBootDevice(client, "a62b8495-52e2-407b-b3cb-62775d04c2b8", bootOpts).ExtractErr()
+	if err != nil {
+		panic(err)
+	}
+
+Example to get boot device for a node
+
+	bootDevice, err := nodes.GetBootDevice(client, "a62b8495-52e2-407b-b3cb-62775d04c2b8").Extract()
+	if err != nil {
+		panic(err)
+	}
+*/
+package nodes
diff --git a/openstack/baremetal/v1/nodes/requests.go b/openstack/baremetal/v1/nodes/requests.go
index 32264a0..d247bf9 100644
--- a/openstack/baremetal/v1/nodes/requests.go
+++ b/openstack/baremetal/v1/nodes/requests.go
@@ -1,12 +1,593 @@
 package nodes
 
 import (
+	"fmt"
+
 	"gerrit.mcp.mirantis.net/debian/gophercloud.git"
 	"gerrit.mcp.mirantis.net/debian/gophercloud.git/pagination"
 )
 
-func List(c *gophercloud.ServiceClient) pagination.Pager {
-	return pagination.NewPager(c, listURL(c), func(r pagination.PageResult) pagination.Page {
-		return NodePage{pagination.SinglePageBase(r)}
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+	ToNodeListQuery() (string, error)
+	ToNodeListDetailQuery() (string, error)
+}
+
+// Provision state reports the current provision state of the node, these are only used in filtering
+type ProvisionState string
+
+const (
+	Enroll       ProvisionState = "enroll"
+	Verifying    ProvisionState = "verifying"
+	Manageable   ProvisionState = "manageable"
+	Available    ProvisionState = "available"
+	Active       ProvisionState = "active"
+	DeployWait   ProvisionState = "wait call-back"
+	Deploying    ProvisionState = "deploying"
+	DeployFail   ProvisionState = "deploy failed"
+	DeployDone   ProvisionState = "deploy complete"
+	Deleting     ProvisionState = "deleting"
+	Deleted      ProvisionState = "deleted"
+	Cleaning     ProvisionState = "cleaning"
+	CleanWait    ProvisionState = "clean wait"
+	CleanFail    ProvisionState = "clean failed"
+	Error        ProvisionState = "error"
+	Rebuild      ProvisionState = "rebuild"
+	Inspecting   ProvisionState = "inspecting"
+	InspectFail  ProvisionState = "inspect failed"
+	InspectWait  ProvisionState = "inspect wait"
+	Adopting     ProvisionState = "adopting"
+	AdoptFail    ProvisionState = "adopt failed"
+	Rescue       ProvisionState = "rescue"
+	RescueFail   ProvisionState = "rescue failed"
+	Rescuing     ProvisionState = "rescuing"
+	UnrescueFail ProvisionState = "unrescue failed"
+)
+
+// TargetProvisionState is used when setting the provision state for a node.
+type TargetProvisionState string
+
+const (
+	TargetActive   TargetProvisionState = "active"
+	TargetDeleted  TargetProvisionState = "deleted"
+	TargetManage   TargetProvisionState = "manage"
+	TargetProvide  TargetProvisionState = "provide"
+	TargetInspect  TargetProvisionState = "inspect"
+	TargetAbort    TargetProvisionState = "abort"
+	TargetClean    TargetProvisionState = "clean"
+	TargetAdopt    TargetProvisionState = "adopt"
+	TargetRescue   TargetProvisionState = "rescue"
+	TargetUnrescue TargetProvisionState = "unrescue"
+)
+
+// 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 node attributes you want to see returned. Marker and Limit are used
+// for pagination.
+type ListOpts struct {
+	// Filter the list by specific instance UUID
+	InstanceUUID string `q:"instance_uuid"`
+
+	// Filter the list by chassis UUID
+	ChassisUUID string `q:"chassis_uuid"`
+
+	// Filter the list by maintenance set to True or False
+	Maintenance bool `q:"maintenance"`
+
+	// Nodes which are, or are not, associated with an instance_uuid.
+	Associated bool `q:"associated"`
+
+	// Only return those with the specified provision_state.
+	ProvisionState ProvisionState `q:"provision_state"`
+
+	// Filter the list with the specified driver.
+	Driver string `q:"driver"`
+
+	// Filter the list with the specified resource class.
+	ResourceClass string `q:"resource_class"`
+
+	// Filter the list with the specified conductor_group.
+	ConductorGroup string `q:"conductor_group"`
+
+	// Filter the list with the specified fault.
+	Fault string `q:"fault"`
+
+	// One or more fields to be returned in the response.
+	Fields []string `q:"fields"`
+
+	// Requests a page size of items.
+	Limit int `q:"limit"`
+
+	// The ID of the last-seen item.
+	Marker string `q:"marker"`
+
+	// Sorts the response by the requested sort direction.
+	SortDir string `q:"sort_dir"`
+
+	// Sorts the response by the this attribute value.
+	SortKey string `q:"sort_key"`
+
+	// A string or UUID of the tenant who owns the baremetal node.
+	Owner string `q:"owner"`
+}
+
+// ToNodeListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToNodeListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	return q.String(), err
+}
+
+// List makes a request against the API to list nodes accessible to you.
+func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	url := listURL(client)
+	if opts != nil {
+		query, err := opts.ToNodeListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+		return NodePage{pagination.LinkedPageBase{PageResult: r}}
 	})
 }
+
+// ToNodeListDetailQuery formats a ListOpts into a query string for the list details API.
+func (opts ListOpts) ToNodeListDetailQuery() (string, error) {
+	// Detail endpoint can't filter by Fields
+	if len(opts.Fields) > 0 {
+		return "", fmt.Errorf("fields is not a valid option when getting a detailed listing of nodes")
+	}
+
+	q, err := gophercloud.BuildQueryString(opts)
+	return q.String(), err
+}
+
+// Return a list of bare metal Nodes with complete details. Some filtering is possible by passing in flags in ListOpts,
+// but you cannot limit by the fields returned.
+func ListDetail(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	// This URL is deprecated. In the future, we should compare the microversion and if >= 1.43, hit the listURL
+	// with ListOpts{Detail: true,}
+	url := listDetailURL(client)
+	if opts != nil {
+		query, err := opts.ToNodeListDetailQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+		return NodePage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+// Get requests details on a single node, by ID.
+func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
+	_, r.Err = client.Get(getURL(client, id), &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+	ToNodeCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts specifies node creation parameters.
+type CreateOpts struct {
+	// The boot interface for a Node, e.g. “pxe”.
+	BootInterface string `json:"boot_interface,omitempty"`
+
+	// The conductor group for a node. Case-insensitive string up to 255 characters, containing a-z, 0-9, _, -, and ..
+	ConductorGroup string `json:"conductor_group,omitempty"`
+
+	// The console interface for a node, e.g. “no-console”.
+	ConsoleInterface string `json:"console_interface,omitempty"`
+
+	// The deploy interface for a node, e.g. “iscsi”.
+	DeployInterface string `json:"deploy_interface,omitempty"`
+
+	// All the metadata required by the driver to manage this Node. List of fields varies between drivers, and can
+	// be retrieved from the /v1/drivers/<DRIVER_NAME>/properties resource.
+	DriverInfo map[string]interface{} `json:"driver_info,omitempty"`
+
+	// name of the driver used to manage this Node.
+	Driver string `json:"driver,omitempty"`
+
+	// A set of one or more arbitrary metadata key and value pairs.
+	Extra map[string]interface{} `json:"extra,omitempty"`
+
+	// The interface used for node inspection, e.g. “no-inspect”.
+	InspectInterface string `json:"inspect_interface,omitempty"`
+
+	// Interface for out-of-band node management, e.g. “ipmitool”.
+	ManagementInterface string `json:"management_interface,omitempty"`
+
+	// Human-readable identifier for the Node resource. May be undefined. Certain words are reserved.
+	Name string `json:"name,omitempty"`
+
+	// Which Network Interface provider to use when plumbing the network connections for this Node.
+	NetworkInterface string `json:"network_interface,omitempty"`
+
+	// Interface used for performing power actions on the node, e.g. “ipmitool”.
+	PowerInterface string `json:"power_interface,omitempty"`
+
+	// Physical characteristics of this Node. Populated during inspection, if performed. Can be edited via the REST
+	// API at any time.
+	Properties map[string]interface{} `json:"properties,omitempty"`
+
+	// Interface used for configuring RAID on this node, e.g. “no-raid”.
+	RAIDInterface string `json:"raid_interface,omitempty"`
+
+	// The interface used for node rescue, e.g. “no-rescue”.
+	RescueInterface string `json:"rescue_interface,omitempty"`
+
+	// A string which can be used by external schedulers to identify this Node as a unit of a specific type
+	// of resource.
+	ResourceClass string `json:"resource_class,omitempty"`
+
+	// Interface used for attaching and detaching volumes on this node, e.g. “cinder”.
+	StorageInterface string `json:"storage_interface,omitempty"`
+
+	// The UUID for the resource.
+	UUID string `json:"uuid,omitempty"`
+
+	// Interface for vendor-specific functionality on this node, e.g. “no-vendor”.
+	VendorInterface string `json:"vendor_interface,omitempty"`
+
+	// A string or UUID of the tenant who owns the baremetal node.
+	Owner string `json:"owner,omitempty"`
+}
+
+// ToNodeCreateMap assembles a request body based on the contents of a CreateOpts.
+func (opts CreateOpts) ToNodeCreateMap() (map[string]interface{}, error) {
+	body, err := gophercloud.BuildRequestBody(opts, "")
+	if err != nil {
+		return nil, err
+	}
+
+	return body, nil
+}
+
+// Create requests a node to be created
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+	reqBody, err := opts.ToNodeCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+
+	_, r.Err = client.Post(createURL(client), reqBody, &r.Body, nil)
+	return
+}
+
+type Patch interface {
+	ToNodeUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts is a slice of Patches used to update a node
+type UpdateOpts []Patch
+
+type UpdateOp string
+
+const (
+	ReplaceOp UpdateOp = "replace"
+	AddOp     UpdateOp = "add"
+	RemoveOp  UpdateOp = "remove"
+)
+
+type UpdateOperation struct {
+	Op    UpdateOp    `json:"op" required:"true"`
+	Path  string      `json:"path" required:"true"`
+	Value interface{} `json:"value,omitempty"`
+}
+
+func (opts UpdateOperation) ToNodeUpdateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "")
+}
+
+// Update requests that a node be updated
+func Update(client *gophercloud.ServiceClient, id string, opts UpdateOpts) (r UpdateResult) {
+	body := make([]map[string]interface{}, len(opts))
+	for i, patch := range opts {
+		result, err := patch.ToNodeUpdateMap()
+		if err != nil {
+			r.Err = err
+			return
+		}
+
+		body[i] = result
+	}
+	_, r.Err = client.Patch(updateURL(client, id), body, &r.Body, &gophercloud.RequestOpts{
+		JSONBody: &body,
+		OkCodes:  []int{200},
+	})
+	return
+}
+
+// Delete requests that a node be removed
+func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
+	_, r.Err = client.Delete(deleteURL(client, id), nil)
+	return
+}
+
+// Request that Ironic validate whether the Node’s driver has enough information to manage the Node. This polls each
+// interface on the driver, and returns the status of that interface.
+func Validate(client *gophercloud.ServiceClient, id string) (r ValidateResult) {
+	_, r.Err = client.Get(validateURL(client, id), &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+// Inject NMI (Non-Masking Interrupts) for the given Node. This feature can be used for hardware diagnostics, and
+// actual support depends on a driver.
+func InjectNMI(client *gophercloud.ServiceClient, id string) (r InjectNMIResult) {
+	_, r.Err = client.Put(injectNMIURL(client, id), map[string]string{}, nil, &gophercloud.RequestOpts{
+		OkCodes: []int{204},
+	})
+	return
+}
+
+type BootDeviceOpts struct {
+	BootDevice string `json:"boot_device"` // e.g., 'pxe', 'disk', etc.
+	Persistent bool   `json:"persistent"`  // Whether this is one-time or not
+}
+
+// BootDeviceOptsBuilder allows extensions to add additional parameters to the
+// SetBootDevice request.
+type BootDeviceOptsBuilder interface {
+	ToBootDeviceMap() (map[string]interface{}, error)
+}
+
+// ToBootDeviceSetMap assembles a request body based on the contents of a BootDeviceOpts.
+func (opts BootDeviceOpts) ToBootDeviceMap() (map[string]interface{}, error) {
+	body, err := gophercloud.BuildRequestBody(opts, "")
+	if err != nil {
+		return nil, err
+	}
+
+	return body, nil
+}
+
+// Set the boot device for the given Node, and set it persistently or for one-time boot. The exact behaviour
+// of this depends on the hardware driver.
+func SetBootDevice(client *gophercloud.ServiceClient, id string, bootDevice BootDeviceOptsBuilder) (r SetBootDeviceResult) {
+	reqBody, err := bootDevice.ToBootDeviceMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+
+	_, r.Err = client.Put(bootDeviceURL(client, id), reqBody, nil, &gophercloud.RequestOpts{
+		OkCodes: []int{204},
+	})
+	return
+}
+
+// Get the current boot device for the given Node.
+func GetBootDevice(client *gophercloud.ServiceClient, id string) (r BootDeviceResult) {
+	_, r.Err = client.Get(bootDeviceURL(client, id), &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+// Retrieve the acceptable set of supported boot devices for a specific Node.
+func GetSupportedBootDevices(client *gophercloud.ServiceClient, id string) (r SupportedBootDeviceResult) {
+	_, r.Err = client.Get(supportedBootDeviceURL(client, id), &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+// A cleaning step has required keys ‘interface’ and ‘step’, and optional key ‘args’. If specified,
+// the value for ‘args’ is a keyword variable argument dictionary that is passed to the cleaning step
+// method.
+type CleanStep struct {
+	Interface string                 `json:"interface" required:"true"`
+	Step      string                 `json:"step" required:"true"`
+	Args      map[string]interface{} `json:"args,omitempty"`
+}
+
+// ProvisionStateOptsBuilder allows extensions to add additional parameters to the
+// ChangeProvisionState request.
+type ProvisionStateOptsBuilder interface {
+	ToProvisionStateMap() (map[string]interface{}, error)
+}
+
+// Starting with Ironic API version 1.56, a configdrive may be a JSON object with structured data.
+// Prior to this version, it must be a base64-encoded, gzipped ISO9660 image.
+type ConfigDrive struct {
+	MetaData    map[string]interface{} `json:"meta_data,omitempty"`
+	NetworkData map[string]interface{} `json:"network_data,omitempty"`
+	UserData    interface{}            `json:"user_data,omitempty"`
+}
+
+// ProvisionStateOpts for a request to change a node's provision state. A config drive should be base64-encoded
+// gzipped ISO9660 image.
+type ProvisionStateOpts struct {
+	Target         TargetProvisionState `json:"target" required:"true"`
+	ConfigDrive    interface{}          `json:"configdrive,omitempty"`
+	CleanSteps     []CleanStep          `json:"clean_steps,omitempty"`
+	RescuePassword string               `json:"rescue_password,omitempty"`
+}
+
+// ToProvisionStateMap assembles a request body based on the contents of a CreateOpts.
+func (opts ProvisionStateOpts) ToProvisionStateMap() (map[string]interface{}, error) {
+	body, err := gophercloud.BuildRequestBody(opts, "")
+	if err != nil {
+		return nil, err
+	}
+
+	return body, nil
+}
+
+// Request a change to the Node’s provision state. Acceptable target states depend on the Node’s current provision
+// state. More detailed documentation of the Ironic State Machine is available in the developer docs.
+func ChangeProvisionState(client *gophercloud.ServiceClient, id string, opts ProvisionStateOptsBuilder) (r ChangeStateResult) {
+	reqBody, err := opts.ToProvisionStateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+
+	_, r.Err = client.Put(provisionStateURL(client, id), reqBody, nil, &gophercloud.RequestOpts{
+		OkCodes: []int{202},
+	})
+	return
+}
+
+type TargetPowerState string
+
+// TargetPowerState is used when changing the power state of a node.
+const (
+	PowerOn       TargetPowerState = "power on"
+	PowerOff      TargetPowerState = "power off"
+	Rebooting     TargetPowerState = "rebooting"
+	SoftPowerOff  TargetPowerState = "soft power off"
+	SoftRebooting TargetPowerState = "soft rebooting"
+)
+
+// PowerStateOptsBuilder allows extensions to add additional parameters to the ChangePowerState request.
+type PowerStateOptsBuilder interface {
+	ToPowerStateMap() (map[string]interface{}, error)
+}
+
+// PowerStateOpts for a request to change a node's power state.
+type PowerStateOpts struct {
+	Target  TargetPowerState `json:"target" required:"true"`
+	Timeout int              `json:"timeout,omitempty"`
+}
+
+// ToPowerStateMap assembles a request body based on the contents of a PowerStateOpts.
+func (opts PowerStateOpts) ToPowerStateMap() (map[string]interface{}, error) {
+	body, err := gophercloud.BuildRequestBody(opts, "")
+	if err != nil {
+		return nil, err
+	}
+
+	return body, nil
+}
+
+// Request to change a Node's power state.
+func ChangePowerState(client *gophercloud.ServiceClient, id string, opts PowerStateOptsBuilder) (r ChangePowerStateResult) {
+	reqBody, err := opts.ToPowerStateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+
+	_, r.Err = client.Put(powerStateURL(client, id), reqBody, nil, &gophercloud.RequestOpts{
+		OkCodes: []int{202},
+	})
+	return
+}
+
+// This is the desired RAID configuration on the bare metal node.
+type RAIDConfigOpts struct {
+	LogicalDisks []LogicalDisk `json:"logical_disks"`
+}
+
+// RAIDConfigOptsBuilder allows extensions to modify a set RAID config request.
+type RAIDConfigOptsBuilder interface {
+	ToRAIDConfigMap() (map[string]interface{}, error)
+}
+
+// RAIDLevel type is used to specify the RAID level for a logical disk.
+type RAIDLevel string
+
+const (
+	RAID0  RAIDLevel = "0"
+	RAID1  RAIDLevel = "1"
+	RAID2  RAIDLevel = "2"
+	RAID5  RAIDLevel = "5"
+	RAID6  RAIDLevel = "6"
+	RAID10 RAIDLevel = "1+0"
+	RAID50 RAIDLevel = "5+0"
+	RAID60 RAIDLevel = "6+0"
+)
+
+// DiskType is used to specify the disk type for a logical disk, e.g. hdd or ssd.
+type DiskType string
+
+const (
+	HDD DiskType = "hdd"
+	SSD DiskType = "ssd"
+)
+
+// InterfaceType is used to specify the interface for a logical disk.
+type InterfaceType string
+
+const (
+	SATA InterfaceType = "sata"
+	SCSI InterfaceType = "scsi"
+	SAS  InterfaceType = "sas"
+)
+
+type LogicalDisk struct {
+	// Size (Integer) of the logical disk to be created in GiB.  If unspecified, "MAX" will be used.
+	SizeGB *int `json:"size_gb"`
+
+	// RAID level for the logical disk.
+	RAIDLevel RAIDLevel `json:"raid_level" required:"true"`
+
+	// Name of the volume. Should be unique within the Node. If not specified, volume name will be auto-generated.
+	VolumeName string `json:"volume_name,omitempty"`
+
+	// Set to true if this is the root volume. At most one logical disk can have this set to true.
+	IsRootVolume *bool `json:"is_root_volume,omitempty"`
+
+	// Set to true if this logical disk can share physical disks with other logical disks.
+	SharePhysicalDisks *bool `json:"share_physical_disks,omitempty"`
+
+	// If this is not specified, disk type will not be a criterion to find backing physical disks
+	DiskType DiskType `json:"disk_type,omitempty"`
+
+	// If this is not specified, interface type will not be a criterion to find backing physical disks.
+	InterfaceType InterfaceType `json:"interface_type,omitempty"`
+
+	// Integer, number of disks to use for the logical disk. Defaults to minimum number of disks required
+	// for the particular RAID level.
+	NumberOfPhysicalDisks int `json:"number_of_physical_disks,omitempty"`
+
+	// The name of the controller as read by the RAID interface.
+	Controller string `json:"controller,omitempty"`
+
+	// A list of physical disks to use as read by the RAID interface.
+	PhysicalDisks []string `json:"physical_disks,omitempty"`
+}
+
+func (opts RAIDConfigOpts) ToRAIDConfigMap() (map[string]interface{}, error) {
+	body, err := gophercloud.BuildRequestBody(opts, "")
+	if err != nil {
+		return nil, err
+	}
+
+	for _, v := range body["logical_disks"].([]interface{}) {
+		if logicalDisk, ok := v.(map[string]interface{}); ok {
+			if logicalDisk["size_gb"] == nil {
+				logicalDisk["size_gb"] = "MAX"
+			}
+		}
+	}
+
+	return body, nil
+}
+
+// Request to change a Node's RAID config.
+func SetRAIDConfig(client *gophercloud.ServiceClient, id string, raidConfigOptsBuilder RAIDConfigOptsBuilder) (r ChangeStateResult) {
+	reqBody, err := raidConfigOptsBuilder.ToRAIDConfigMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+
+	_, r.Err = client.Put(raidConfigURL(client, id), reqBody, nil, &gophercloud.RequestOpts{
+		OkCodes: []int{204},
+	})
+	return
+}
diff --git a/openstack/baremetal/v1/nodes/results.go b/openstack/baremetal/v1/nodes/results.go
index 459ed1b..be335b9 100644
--- a/openstack/baremetal/v1/nodes/results.go
+++ b/openstack/baremetal/v1/nodes/results.go
@@ -5,60 +5,309 @@
 	"gerrit.mcp.mirantis.net/debian/gophercloud.git/pagination"
 )
 
-type commonResult struct {
+type nodeResult struct {
 	gophercloud.Result
 }
 
-func (r commonResult) Extract() (*Node, error) {
-	var n struct {
-		Node *Node `json:"node"`
+// Extract interprets any nodeResult as a Node, if possible.
+func (r nodeResult) Extract() (*Node, error) {
+	var s Node
+	err := r.ExtractInto(&s)
+	return &s, err
+}
+
+// Extract interprets a BootDeviceResult as BootDeviceOpts, if possible.
+func (r BootDeviceResult) Extract() (*BootDeviceOpts, error) {
+	var s BootDeviceOpts
+	err := r.ExtractInto(&s)
+	return &s, err
+}
+
+// Extract interprets a SupportedBootDeviceResult as an array of supported boot devices, if possible.
+func (r SupportedBootDeviceResult) Extract() ([]string, error) {
+	var s struct {
+		Devices []string `json:"supported_boot_devices"`
 	}
-	err := r.ExtractInto(&n)
-	return n.Node, err
+
+	err := r.ExtractInto(&s)
+	return s.Devices, err
 }
 
-type GetResult struct {
-	commonResult
+// Extract interprets a ValidateResult as NodeValidation, if possible.
+func (r ValidateResult) Extract() (*NodeValidation, error) {
+	var s NodeValidation
+	err := r.ExtractInto(&s)
+	return &s, err
 }
 
-type Link struct {
-	Href string `json:"href"`
-	Rel  string `json:"rel"`
+func (r nodeResult) ExtractInto(v interface{}) error {
+	return r.Result.ExtractIntoStructPtr(v, "")
 }
 
+func ExtractNodesInto(r pagination.Page, v interface{}) error {
+	return r.(NodePage).Result.ExtractIntoSlicePtr(v, "nodes")
+}
+
+// Node represents a node in the OpenStack Bare Metal API.
 type Node struct {
-	UUID           string `json:"uuid"`
-	InstanceUUID   string `json:"InstanceUUID"`
-	PowerState     string `json:"power_state"`
+	// UUID for the resource.
+	UUID string `json:"uuid"`
+
+	// Identifier for the Node resource. May be undefined. Certain words are reserved.
+	Name string `json:"name"`
+
+	// Current power state of this Node. Usually, “power on” or “power off”, but may be “None”
+	// if Ironic is unable to determine the power state (eg, due to hardware failure).
+	PowerState string `json:"power_state"`
+
+	// A power state transition has been requested, this field represents the requested (ie, “target”)
+	// state either “power on”, “power off”, “rebooting”, “soft power off” or “soft rebooting”.
+	TargetPowerState string `json:"target_power_state"`
+
+	// Current provisioning state of this Node.
 	ProvisionState string `json:"provision_state"`
-	Maintenance    bool   `json:"maintenance"`
-	Links          []Link `json:"links"`
+
+	// A provisioning action has been requested, this field represents the requested (ie, “target”) state. Note
+	// that a Node may go through several states during its transition to this target state. For instance, when
+	// requesting an instance be deployed to an AVAILABLE Node, the Node may go through the following state
+	// change progression: AVAILABLE -> DEPLOYING -> DEPLOYWAIT -> DEPLOYING -> ACTIVE
+	TargetProvisionState string `json:"target_provision_state"`
+
+	// Whether or not this Node is currently in “maintenance mode”. Setting a Node into maintenance mode removes it
+	// from the available resource pool and halts some internal automation. This can happen manually (eg, via an API
+	// request) or automatically when Ironic detects a hardware fault that prevents communication with the machine.
+	Maintenance bool `json:"maintenance"`
+
+	// Description of the reason why this Node was placed into maintenance mode
+	MaintenanceReason string `json:"maintenance_reason"`
+
+	// Fault indicates the active fault detected by ironic, typically the Node is in “maintenance mode”. None means no
+	// fault has been detected by ironic. “power failure” indicates ironic failed to retrieve power state from this
+	// node. There are other possible types, e.g., “clean failure” and “rescue abort failure”.
+	Fault string `json:"fault"`
+
+	// Error from the most recent (last) transaction that started but failed to finish.
+	LastError string `json:"last_error"`
+
+	// Name of an Ironic Conductor host which is holding a lock on this node, if a lock is held. Usually “null”,
+	// but this field can be useful for debugging.
+	Reservation string `json:"reservation"`
+
+	// Name of the driver.
+	Driver string `json:"driver"`
+
+	// The metadata required by the driver to manage this Node. List of fields varies between drivers, and can be
+	// retrieved from the /v1/drivers/<DRIVER_NAME>/properties resource.
+	DriverInfo map[string]interface{} `json:"driver_info"`
+
+	// Metadata set and stored by the Node’s driver. This field is read-only.
+	DriverInternalInfo map[string]interface{} `json:"driver_internal_info"`
+
+	// Characteristics of this Node. Populated by ironic-inspector during inspection. May be edited via the REST
+	// API at any time.
+	Properties map[string]interface{} `json:"properties"`
+
+	// Used to customize the deployed image. May include root partition size, a base 64 encoded config drive, and other
+	// metadata. Note that this field is erased automatically when the instance is deleted (this is done by requesting
+	// the Node provision state be changed to DELETED).
+	InstanceInfo map[string]interface{} `json:"instance_info"`
+
+	// ID of the Nova instance associated with this Node.
+	InstanceUUID string `json:"instance_uuid"`
+
+	// ID of the chassis associated with this Node. May be empty or None.
+	ChassisUUID string `json:"chassis_uuid"`
+
+	// Set of one or more arbitrary metadata key and value pairs.
+	Extra map[string]interface{} `json:"extra"`
+
+	// Whether console access is enabled or disabled on this node.
+	ConsoleEnabled bool `json:"console_enabled"`
+
+	// The current RAID configuration of the node. Introduced with the cleaning feature.
+	RAIDConfig map[string]interface{} `json:"raid_config"`
+
+	// The requested RAID configuration of the node, which will be applied when the Node next transitions
+	// through the CLEANING state. Introduced with the cleaning feature.
+	TargetRAIDConfig map[string]interface{} `json:"target_raid_config"`
+
+	// Current clean step. Introduced with the cleaning feature.
+	CleanStep map[string]interface{} `json:"clean_step"`
+
+	// Current deploy step.
+	DeployStep map[string]interface{} `json:"deploy_step"`
+
+	// String which can be used by external schedulers to identify this Node as a unit of a specific type of resource.
+	// For more details, see: https://docs.openstack.org/ironic/latest/install/configure-nova-flavors.html
+	ResourceClass string `json:"resource_class"`
+
+	// Boot interface for a Node, e.g. “pxe”.
+	BootInterface string `json:"boot_interface"`
+
+	// Console interface for a node, e.g. “no-console”.
+	ConsoleInterface string `json:"console_interface"`
+
+	// Deploy interface for a node, e.g. “iscsi”.
+	DeployInterface string `json:"deploy_interface"`
+
+	// Interface used for node inspection, e.g. “no-inspect”.
+	InspectInterface string `json:"inspect_interface"`
+
+	// For out-of-band node management, e.g. “ipmitool”.
+	ManagementInterface string `json:"management_interface"`
+
+	// Network Interface provider to use when plumbing the network connections for this Node.
+	NetworkInterface string `json:"network_interface"`
+
+	// used for performing power actions on the node, e.g. “ipmitool”.
+	PowerInterface string `json:"power_interface"`
+
+	// Used for configuring RAID on this node, e.g. “no-raid”.
+	RAIDInterface string `json:"raid_interface"`
+
+	// Interface used for node rescue, e.g. “no-rescue”.
+	RescueInterface string `json:"rescue_interface"`
+
+	// Used for attaching and detaching volumes on this node, e.g. “cinder”.
+	StorageInterface string `json:"storage_interface"`
+
+	// Array of traits for this node.
+	Traits []string `json:"traits"`
+
+	// For vendor-specific functionality on this node, e.g. “no-vendor”.
+	VendorInterface string `json:"vendor_interface"`
+
+	// Conductor group for a node. Case-insensitive string up to 255 characters, containing a-z, 0-9, _, -, and ..
+	ConductorGroup string `json:"conductor_group"`
+
+	// The node is protected from undeploying, rebuilding and deletion.
+	Protected bool `json:"protected"`
+
+	// Reason the node is marked as protected.
+	ProtectedReason string `json:"protected_reason"`
+
+	// A string or UUID of the tenant who owns the baremetal node.
+	Owner string `json:"owner"`
 }
 
+// NodePage abstracts the raw results of making a List() request against
+// the API. As OpenStack extensions may freely alter the response bodies of
+// structures returned to the client, you may only safely access the data
+// provided through the ExtractNodes call.
 type NodePage struct {
-	pagination.SinglePageBase
+	pagination.LinkedPageBase
 }
 
+// IsEmpty returns true if a page contains no Node results.
+func (r NodePage) IsEmpty() (bool, error) {
+	s, err := ExtractNodes(r)
+	return len(s) == 0, err
+}
+
+// NextPageURL uses the response's embedded link reference to navigate to the
+// next page of results.
 func (r NodePage) NextPageURL() (string, error) {
-	var n struct {
+	var s struct {
 		Links []gophercloud.Link `json:"nodes_links"`
 	}
-	err := r.ExtractInto(&n)
+	err := r.ExtractInto(&s)
 	if err != nil {
 		return "", err
 	}
-	return gophercloud.ExtractNextURL(n.Links)
+	return gophercloud.ExtractNextURL(s.Links)
 }
 
-func (r NodePage) IsEmpty() (bool, error) {
-	is, err := ExtractNodes(r)
-	return len(is) == 0, err
-}
-
+// ExtractNodes interprets the results of a single page from a List() call,
+// producing a slice of Node entities.
 func ExtractNodes(r pagination.Page) ([]Node, error) {
-	var n struct {
-		Nodes []Node `json:"nodes"`
-	}
-	err := (r.(NodePage)).ExtractInto(&n)
-	return n.Nodes, err
+	var s []Node
+	err := ExtractNodesInto(r, &s)
+	return s, err
+}
+
+// GetResult is the response from a Get operation. Call its Extract
+// method to interpret it as a Node.
+type GetResult struct {
+	nodeResult
+}
+
+// CreateResult is the response from a Create operation.
+type CreateResult struct {
+	nodeResult
+}
+
+// UpdateResult is the response from an Update operation. Call its Extract
+// method to interpret it as a Node.
+type UpdateResult struct {
+	nodeResult
+}
+
+// DeleteResult is the response from a Delete operation. Call its ExtractErr
+// method to determine if the call succeeded or failed.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
+
+// ValidateResult is the response from a Validate operation. Call its Extract
+// method to interpret it as a NodeValidation struct.
+type ValidateResult struct {
+	gophercloud.Result
+}
+
+// InjectNMIResult is the response from an InjectNMI operation. Call its ExtractErr
+// method to determine if the call succeeded or failed.
+type InjectNMIResult struct {
+	gophercloud.ErrResult
+}
+
+// BootDeviceResult is the response from a GetBootDevice operation. Call its Extract
+// method to interpret it as a BootDeviceOpts struct.
+type BootDeviceResult struct {
+	gophercloud.Result
+}
+
+// BootDeviceResult is the response from a GetBootDevice operation. Call its Extract
+// method to interpret it as a BootDeviceOpts struct.
+type SetBootDeviceResult struct {
+	gophercloud.ErrResult
+}
+
+// SupportedBootDeviceResult is the response from a GetSupportedBootDevices operation. Call its Extract
+// method to interpret it as an array of supported boot device values.
+type SupportedBootDeviceResult struct {
+	gophercloud.Result
+}
+
+// ChangePowerStateResult is the response from a ChangePowerState operation. Call its ExtractErr
+// method to determine if the call succeeded or failed.
+type ChangePowerStateResult struct {
+	gophercloud.ErrResult
+}
+
+// Each element in the response will contain a “result” variable, which will have a value of “true” or “false”, and
+// also potentially a reason. A value of nil indicates that the Node’s driver does not support that interface.
+type DriverValidation struct {
+	Result bool   `json:"result"`
+	Reason string `json:"reason"`
+}
+
+//  Ironic validates whether the Node’s driver has enough information to manage the Node. This polls each interface on
+//  the driver, and returns the status of that interface as an DriverValidation struct.
+type NodeValidation struct {
+	Boot       DriverValidation `json:"boot"`
+	Console    DriverValidation `json:"console"`
+	Deploy     DriverValidation `json:"deploy"`
+	Inspect    DriverValidation `json:"inspect"`
+	Management DriverValidation `json:"management"`
+	Network    DriverValidation `json:"network"`
+	Power      DriverValidation `json:"power"`
+	RAID       DriverValidation `json:"raid"`
+	Rescue     DriverValidation `json:"rescue"`
+	Storage    DriverValidation `json:"storage"`
+}
+
+// ChangeStateResult is the response from any state change operation. Call its ExtractErr
+// method to determine if the call succeeded or failed.
+type ChangeStateResult struct {
+	gophercloud.ErrResult
 }
diff --git a/openstack/baremetal/v1/nodes/testing/doc.go b/openstack/baremetal/v1/nodes/testing/doc.go
new file mode 100644
index 0000000..df83778
--- /dev/null
+++ b/openstack/baremetal/v1/nodes/testing/doc.go
@@ -0,0 +1,2 @@
+// nodes unit tests
+package testing
diff --git a/openstack/baremetal/v1/nodes/testing/fixtures.go b/openstack/baremetal/v1/nodes/testing/fixtures.go
new file mode 100644
index 0000000..c073329
--- /dev/null
+++ b/openstack/baremetal/v1/nodes/testing/fixtures.go
@@ -0,0 +1,1033 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git/openstack/baremetal/v1/nodes"
+	th "gerrit.mcp.mirantis.net/debian/gophercloud.git/testhelper"
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git/testhelper/client"
+)
+
+// NodeListBody contains the canned body of a nodes.List response, without detail.
+const NodeListBody = `
+ {
+  "nodes": [
+    {
+      "instance_uuid": null,
+      "links": [
+        {
+          "href": "http://ironic.example.com:6385/v1/nodes/d2630783-6ec8-4836-b556-ab427c4b581e",
+          "rel": "self"
+        },
+        {
+          "href": "http://ironic.example.com:6385/nodes/d2630783-6ec8-4836-b556-ab427c4b581e",
+          "rel": "bookmark"
+        }
+      ],
+      "maintenance": false,
+      "name": "foo",
+      "power_state": null,
+      "provision_state": "enroll"
+    },
+    {
+      "instance_uuid": null,
+      "links": [
+        {
+          "href": "http://ironic.example.com:6385/v1/nodes/08c84581-58f5-4ea2-a0c6-dd2e5d2b3662",
+          "rel": "self"
+        },
+        {
+          "href": "http://ironic.example.com:6385/nodes/08c84581-58f5-4ea2-a0c6-dd2e5d2b3662",
+          "rel": "bookmark"
+        }
+      ],
+      "maintenance": false,
+      "name": "bar",
+      "power_state": null,
+      "provision_state": "enroll"
+    },
+    {
+      "instance_uuid": null,
+      "links": [
+        {
+          "href": "http://ironic.example.com:6385/v1/nodes/c9afd385-5d89-4ecb-9e1c-68194da6b474",
+          "rel": "self"
+        },
+        {
+          "href": "http://ironic.example.com:6385/nodes/c9afd385-5d89-4ecb-9e1c-68194da6b474",
+          "rel": "bookmark"
+        }
+      ],
+      "maintenance": false,
+      "name": "baz",
+      "power_state": null,
+      "provision_state": "enroll"
+    }
+  ]
+}
+`
+
+// NodeListDetailBody contains the canned body of a nodes.ListDetail response.
+const NodeListDetailBody = `
+ {
+  "nodes": [
+    {
+      "bios_interface": "no-bios",
+      "boot_interface": "pxe",
+      "chassis_uuid": null,
+      "clean_step": {},
+      "conductor_group": "",
+      "console_enabled": false,
+      "console_interface": "no-console",
+      "created_at": "2019-01-31T19:59:28+00:00",
+      "deploy_interface": "iscsi",
+      "deploy_step": {},
+      "driver": "ipmi",
+      "driver_info": {
+        "ipmi_port": "6230",
+        "ipmi_username": "admin",
+        "deploy_kernel": "http://172.22.0.1/images/tinyipa-stable-rocky.vmlinuz",
+        "ipmi_address": "192.168.122.1",
+        "deploy_ramdisk": "http://172.22.0.1/images/tinyipa-stable-rocky.gz",
+        "ipmi_password": "admin"
+
+      },
+      "driver_internal_info": {},
+      "extra": {},
+      "fault": null,
+      "inspect_interface": "no-inspect",
+      "inspection_finished_at": null,
+      "inspection_started_at": null,
+      "instance_info": {},
+      "instance_uuid": null,
+      "last_error": null,
+      "links": [
+        {
+          "href": "http://ironic.example.com:6385/v1/nodes/d2630783-6ec8-4836-b556-ab427c4b581e",
+          "rel": "self"
+        },
+        {
+          "href": "http://ironic.example.com:6385/nodes/d2630783-6ec8-4836-b556-ab427c4b581e",
+          "rel": "bookmark"
+        }
+      ],
+      "maintenance": false,
+      "maintenance_reason": null,
+      "management_interface": "ipmitool",
+      "name": "foo",
+      "network_interface": "flat",
+      "portgroups": [
+        {
+          "href": "http://ironic.example.com:6385/v1/nodes/d2630783-6ec8-4836-b556-ab427c4b581e/portgroups",
+          "rel": "self"
+        },
+        {
+          "href": "http://ironic.example.com:6385/nodes/d2630783-6ec8-4836-b556-ab427c4b581e/portgroups",
+          "rel": "bookmark"
+        }
+      ],
+      "ports": [
+        {
+          "href": "http://ironic.example.com:6385/v1/nodes/d2630783-6ec8-4836-b556-ab427c4b581e/ports",
+          "rel": "self"
+        },
+        {
+          "href": "http://ironic.example.com:6385/nodes/d2630783-6ec8-4836-b556-ab427c4b581e/ports",
+          "rel": "bookmark"
+        }
+      ],
+      "power_interface": "ipmitool",
+      "power_state": null,
+      "properties": {},
+      "provision_state": "enroll",
+      "provision_updated_at": null,
+      "raid_config": {},
+      "raid_interface": "no-raid",
+      "rescue_interface": "no-rescue",
+      "reservation": null,
+      "resource_class": null,
+      "states": [
+        {
+          "href": "http://ironic.example.com:6385/v1/nodes/d2630783-6ec8-4836-b556-ab427c4b581e/states",
+          "rel": "self"
+        },
+        {
+          "href": "http://ironic.example.com:6385/nodes/d2630783-6ec8-4836-b556-ab427c4b581e/states",
+          "rel": "bookmark"
+        }
+      ],
+      "storage_interface": "noop",
+      "target_power_state": null,
+      "target_provision_state": null,
+      "target_raid_config": {},
+      "traits": [],
+      "updated_at": null,
+      "uuid": "d2630783-6ec8-4836-b556-ab427c4b581e",
+      "vendor_interface": "ipmitool",
+      "volume": [
+        {
+          "href": "http://ironic.example.com:6385/v1/nodes/d2630783-6ec8-4836-b556-ab427c4b581e/volume",
+          "rel": "self"
+        },
+        {
+          "href": "http://ironic.example.com:6385/nodes/d2630783-6ec8-4836-b556-ab427c4b581e/volume",
+          "rel": "bookmark"
+        }
+      ]
+    },
+    {
+      "bios_interface": "no-bios",
+      "boot_interface": "pxe",
+      "chassis_uuid": null,
+      "clean_step": {},
+      "conductor_group": "",
+      "console_enabled": false,
+      "console_interface": "no-console",
+      "created_at": "2019-01-31T19:59:29+00:00",
+      "deploy_interface": "iscsi",
+      "deploy_step": {},
+      "driver": "ipmi",
+      "driver_info": {},
+      "driver_internal_info": {},
+      "extra": {},
+      "fault": null,
+      "inspect_interface": "no-inspect",
+      "inspection_finished_at": null,
+      "inspection_started_at": null,
+      "instance_info": {},
+      "instance_uuid": null,
+      "last_error": null,
+      "links": [
+        {
+          "href": "http://ironic.example.com:6385/v1/nodes/08c84581-58f5-4ea2-a0c6-dd2e5d2b3662",
+          "rel": "self"
+        },
+        {
+          "href": "http://ironic.example.com:6385/nodes/08c84581-58f5-4ea2-a0c6-dd2e5d2b3662",
+          "rel": "bookmark"
+        }
+      ],
+      "maintenance": false,
+      "maintenance_reason": null,
+      "management_interface": "ipmitool",
+      "name": "bar",
+      "network_interface": "flat",
+      "portgroups": [
+        {
+          "href": "http://ironic.example.com:6385/v1/nodes/08c84581-58f5-4ea2-a0c6-dd2e5d2b3662/portgroups",
+          "rel": "self"
+        },
+        {
+          "href": "http://ironic.example.com:6385/nodes/08c84581-58f5-4ea2-a0c6-dd2e5d2b3662/portgroups",
+          "rel": "bookmark"
+        }
+      ],
+      "ports": [
+        {
+          "href": "http://ironic.example.com:6385/v1/nodes/08c84581-58f5-4ea2-a0c6-dd2e5d2b3662/ports",
+          "rel": "self"
+        },
+        {
+          "href": "http://ironic.example.com:6385/nodes/08c84581-58f5-4ea2-a0c6-dd2e5d2b3662/ports",
+          "rel": "bookmark"
+        }
+      ],
+      "power_interface": "ipmitool",
+      "power_state": null,
+      "properties": {},
+      "provision_state": "enroll",
+      "provision_updated_at": null,
+      "raid_config": {},
+      "raid_interface": "no-raid",
+      "rescue_interface": "no-rescue",
+      "reservation": null,
+      "resource_class": null,
+      "states": [
+        {
+          "href": "http://ironic.example.com:6385/v1/nodes/08c84581-58f5-4ea2-a0c6-dd2e5d2b3662/states",
+          "rel": "self"
+        },
+        {
+          "href": "http://ironic.example.com:6385/nodes/08c84581-58f5-4ea2-a0c6-dd2e5d2b3662/states",
+          "rel": "bookmark"
+        }
+      ],
+      "storage_interface": "noop",
+      "target_power_state": null,
+      "target_provision_state": null,
+      "target_raid_config": {},
+      "traits": [],
+      "updated_at": null,
+      "uuid": "08c84581-58f5-4ea2-a0c6-dd2e5d2b3662",
+      "vendor_interface": "ipmitool",
+      "volume": [
+        {
+          "href": "http://ironic.example.com:6385/v1/nodes/08c84581-58f5-4ea2-a0c6-dd2e5d2b3662/volume",
+          "rel": "self"
+        },
+        {
+          "href": "http://ironic.example.com:6385/nodes/08c84581-58f5-4ea2-a0c6-dd2e5d2b3662/volume",
+          "rel": "bookmark"
+        }
+      ]
+    },
+    {
+      "bios_interface": "no-bios",
+      "boot_interface": "pxe",
+      "chassis_uuid": null,
+      "clean_step": {},
+      "conductor_group": "",
+      "console_enabled": false,
+      "console_interface": "no-console",
+      "created_at": "2019-01-31T19:59:30+00:00",
+      "deploy_interface": "iscsi",
+      "deploy_step": {},
+      "driver": "ipmi",
+      "driver_info": {},
+      "driver_internal_info": {},
+      "extra": {},
+      "fault": null,
+      "inspect_interface": "no-inspect",
+      "inspection_finished_at": null,
+      "inspection_started_at": null,
+      "instance_info": {},
+      "instance_uuid": null,
+      "last_error": null,
+      "links": [
+        {
+          "href": "http://ironic.example.com:6385/v1/nodes/c9afd385-5d89-4ecb-9e1c-68194da6b474",
+          "rel": "self"
+        },
+        {
+          "href": "http://ironic.example.com:6385/nodes/c9afd385-5d89-4ecb-9e1c-68194da6b474",
+          "rel": "bookmark"
+        }
+      ],
+      "maintenance": false,
+      "maintenance_reason": null,
+      "management_interface": "ipmitool",
+      "name": "baz",
+      "network_interface": "flat",
+      "portgroups": [
+        {
+          "href": "http://ironic.example.com:6385/v1/nodes/c9afd385-5d89-4ecb-9e1c-68194da6b474/portgroups",
+          "rel": "self"
+        },
+        {
+          "href": "http://ironic.example.com:6385/nodes/c9afd385-5d89-4ecb-9e1c-68194da6b474/portgroups",
+          "rel": "bookmark"
+        }
+      ],
+      "ports": [
+        {
+          "href": "http://ironic.example.com:6385/v1/nodes/c9afd385-5d89-4ecb-9e1c-68194da6b474/ports",
+          "rel": "self"
+        },
+        {
+          "href": "http://ironic.example.com:6385/nodes/c9afd385-5d89-4ecb-9e1c-68194da6b474/ports",
+          "rel": "bookmark"
+        }
+      ],
+      "power_interface": "ipmitool",
+      "power_state": null,
+      "properties": {},
+      "provision_state": "enroll",
+      "provision_updated_at": null,
+      "raid_config": {},
+      "raid_interface": "no-raid",
+      "rescue_interface": "no-rescue",
+      "reservation": null,
+      "resource_class": null,
+      "states": [
+        {
+          "href": "http://ironic.example.com:6385/v1/nodes/c9afd385-5d89-4ecb-9e1c-68194da6b474/states",
+          "rel": "self"
+        },
+        {
+          "href": "http://ironic.example.com:6385/nodes/c9afd385-5d89-4ecb-9e1c-68194da6b474/states",
+          "rel": "bookmark"
+        }
+      ],
+      "storage_interface": "noop",
+      "target_power_state": null,
+      "target_provision_state": null,
+      "target_raid_config": {},
+      "traits": [],
+      "updated_at": null,
+      "uuid": "c9afd385-5d89-4ecb-9e1c-68194da6b474",
+      "vendor_interface": "ipmitool",
+      "volume": [
+        {
+          "href": "http://ironic.example.com:6385/v1/nodes/c9afd385-5d89-4ecb-9e1c-68194da6b474/volume",
+          "rel": "self"
+        },
+        {
+          "href": "http://ironic.example.com:6385/nodes/c9afd385-5d89-4ecb-9e1c-68194da6b474/volume",
+          "rel": "bookmark"
+        }
+      ]
+    }
+  ]
+}
+`
+
+// SingleNodeBody is the canned body of a Get request on an existing node.
+const SingleNodeBody = `
+{
+  "bios_interface": "no-bios",
+  "boot_interface": "pxe",
+  "chassis_uuid": null,
+  "clean_step": {},
+  "conductor_group": "",
+  "console_enabled": false,
+  "console_interface": "no-console",
+  "created_at": "2019-01-31T19:59:28+00:00",
+  "deploy_interface": "iscsi",
+  "deploy_step": {},
+  "driver": "ipmi",
+  "driver_info": {
+    "ipmi_port": "6230",
+    "ipmi_username": "admin",
+    "deploy_kernel": "http://172.22.0.1/images/tinyipa-stable-rocky.vmlinuz",
+    "ipmi_address": "192.168.122.1",
+    "deploy_ramdisk": "http://172.22.0.1/images/tinyipa-stable-rocky.gz",
+    "ipmi_password": "admin"
+  },
+  "driver_internal_info": {},
+  "extra": {},
+  "fault": null,
+  "inspect_interface": "no-inspect",
+  "inspection_finished_at": null,
+  "inspection_started_at": null,
+  "instance_info": {},
+  "instance_uuid": null,
+  "last_error": null,
+  "links": [
+    {
+      "href": "http://ironic.example.com:6385/v1/nodes/d2630783-6ec8-4836-b556-ab427c4b581e",
+      "rel": "self"
+    },
+    {
+      "href": "http://ironic.example.com:6385/nodes/d2630783-6ec8-4836-b556-ab427c4b581e",
+      "rel": "bookmark"
+    }
+  ],
+  "maintenance": false,
+  "maintenance_reason": null,
+  "management_interface": "ipmitool",
+  "name": "foo",
+  "network_interface": "flat",
+  "portgroups": [
+    {
+      "href": "http://ironic.example.com:6385/v1/nodes/d2630783-6ec8-4836-b556-ab427c4b581e/portgroups",
+      "rel": "self"
+    },
+    {
+      "href": "http://ironic.example.com:6385/nodes/d2630783-6ec8-4836-b556-ab427c4b581e/portgroups",
+      "rel": "bookmark"
+    }
+  ],
+  "ports": [
+    {
+      "href": "http://ironic.example.com:6385/v1/nodes/d2630783-6ec8-4836-b556-ab427c4b581e/ports",
+      "rel": "self"
+    },
+    {
+      "href": "http://ironic.example.com:6385/nodes/d2630783-6ec8-4836-b556-ab427c4b581e/ports",
+      "rel": "bookmark"
+    }
+  ],
+  "power_interface": "ipmitool",
+  "power_state": null,
+  "properties": {},
+  "provision_state": "enroll",
+  "provision_updated_at": null,
+  "raid_config": {},
+  "raid_interface": "no-raid",
+  "rescue_interface": "no-rescue",
+  "reservation": null,
+  "resource_class": null,
+  "states": [
+    {
+      "href": "http://ironic.example.com:6385/v1/nodes/d2630783-6ec8-4836-b556-ab427c4b581e/states",
+      "rel": "self"
+    },
+    {
+      "href": "http://ironic.example.com:6385/nodes/d2630783-6ec8-4836-b556-ab427c4b581e/states",
+      "rel": "bookmark"
+    }
+  ],
+  "storage_interface": "noop",
+  "target_power_state": null,
+  "target_provision_state": null,
+  "target_raid_config": {},
+  "traits": [],
+  "updated_at": null,
+  "uuid": "d2630783-6ec8-4836-b556-ab427c4b581e",
+  "vendor_interface": "ipmitool",
+  "volume": [
+    {
+      "href": "http://ironic.example.com:6385/v1/nodes/d2630783-6ec8-4836-b556-ab427c4b581e/volume",
+      "rel": "self"
+    },
+    {
+      "href": "http://ironic.example.com:6385/nodes/d2630783-6ec8-4836-b556-ab427c4b581e/volume",
+      "rel": "bookmark"
+    }
+  ]
+}
+`
+
+const NodeValidationBody = `
+{
+  "bios": {
+    "reason": "Driver ipmi does not support bios (disabled or not implemented).",
+    "result": false
+  },
+  "boot": {
+    "reason": "Cannot validate image information for node a62b8495-52e2-407b-b3cb-62775d04c2b8 because one or more parameters are missing from its instance_info and insufficent information is present to boot from a remote volume. Missing are: ['ramdisk', 'kernel', 'image_source']",
+    "result": false
+  },
+  "console": {
+    "reason": "Driver ipmi does not support console (disabled or not implemented).",
+    "result": false
+  },
+  "deploy": {
+    "reason": "Cannot validate image information for node a62b8495-52e2-407b-b3cb-62775d04c2b8 because one or more parameters are missing from its instance_info and insufficent information is present to boot from a remote volume. Missing are: ['ramdisk', 'kernel', 'image_source']",
+    "result": false
+  },
+  "inspect": {
+    "reason": "Driver ipmi does not support inspect (disabled or not implemented).",
+    "result": false
+  },
+  "management": {
+    "result": true
+  },
+  "network": {
+    "result": true
+  },
+  "power": {
+    "result": true
+  },
+  "raid": {
+    "reason": "Driver ipmi does not support raid (disabled or not implemented).",
+    "result": false
+  },
+  "rescue": {
+    "reason": "Driver ipmi does not support rescue (disabled or not implemented).",
+    "result": false
+  },
+  "storage": {
+    "result": true
+  }
+}
+`
+
+const NodeBootDeviceBody = `
+{
+  "boot_device":"pxe",
+  "persistent":false
+}
+`
+
+const NodeSupportedBootDeviceBody = `
+{
+  "supported_boot_devices": [
+    "pxe",
+    "disk"
+  ]
+}
+`
+
+const NodeProvisionStateActiveBody = `
+{
+    "target": "active",
+    "configdrive": "http://127.0.0.1/images/test-node-config-drive.iso.gz"
+}
+`
+const NodeProvisionStateCleanBody = `
+{
+    "target": "clean",
+    "clean_steps": [
+        {
+            "interface": "deploy",
+            "step": "upgrade_firmware",
+            "args": {
+                "force": "True"
+            }
+        }
+    ]
+}
+`
+
+const NodeProvisionStateConfigDriveBody = `
+{
+	"target": "active",
+	"configdrive": {
+		"user_data": {
+			"ignition": {
+			"version": "2.2.0"
+			},
+			"systemd": {
+				"units": [
+					{
+						"enabled": true,
+						"name": "example.service"
+					}
+				]
+			}
+		}
+	}
+}
+`
+
+var (
+	NodeFoo = nodes.Node{
+		UUID:                 "d2630783-6ec8-4836-b556-ab427c4b581e",
+		Name:                 "foo",
+		PowerState:           "",
+		TargetPowerState:     "",
+		ProvisionState:       "enroll",
+		TargetProvisionState: "",
+		Maintenance:          false,
+		MaintenanceReason:    "",
+		Fault:                "",
+		LastError:            "",
+		Reservation:          "",
+		Driver:               "ipmi",
+		DriverInfo: map[string]interface{}{
+			"ipmi_port":      "6230",
+			"ipmi_username":  "admin",
+			"deploy_kernel":  "http://172.22.0.1/images/tinyipa-stable-rocky.vmlinuz",
+			"ipmi_address":   "192.168.122.1",
+			"deploy_ramdisk": "http://172.22.0.1/images/tinyipa-stable-rocky.gz",
+			"ipmi_password":  "admin",
+		},
+		DriverInternalInfo:  map[string]interface{}{},
+		Properties:          map[string]interface{}{},
+		InstanceInfo:        map[string]interface{}{},
+		InstanceUUID:        "",
+		ChassisUUID:         "",
+		Extra:               map[string]interface{}{},
+		ConsoleEnabled:      false,
+		RAIDConfig:          map[string]interface{}{},
+		TargetRAIDConfig:    map[string]interface{}{},
+		CleanStep:           map[string]interface{}{},
+		DeployStep:          map[string]interface{}{},
+		ResourceClass:       "",
+		BootInterface:       "pxe",
+		ConsoleInterface:    "no-console",
+		DeployInterface:     "iscsi",
+		InspectInterface:    "no-inspect",
+		ManagementInterface: "ipmitool",
+		NetworkInterface:    "flat",
+		PowerInterface:      "ipmitool",
+		RAIDInterface:       "no-raid",
+		RescueInterface:     "no-rescue",
+		StorageInterface:    "noop",
+		Traits:              []string{},
+		VendorInterface:     "ipmitool",
+		ConductorGroup:      "",
+		Protected:           false,
+		ProtectedReason:     "",
+	}
+
+	NodeFooValidation = nodes.NodeValidation{
+		Boot: nodes.DriverValidation{
+			Result: false,
+			Reason: "Cannot validate image information for node a62b8495-52e2-407b-b3cb-62775d04c2b8 because one or more parameters are missing from its instance_info and insufficent information is present to boot from a remote volume. Missing are: ['ramdisk', 'kernel', 'image_source']",
+		},
+		Console: nodes.DriverValidation{
+			Result: false,
+			Reason: "Driver ipmi does not support console (disabled or not implemented).",
+		},
+		Deploy: nodes.DriverValidation{
+			Result: false,
+			Reason: "Cannot validate image information for node a62b8495-52e2-407b-b3cb-62775d04c2b8 because one or more parameters are missing from its instance_info and insufficent information is present to boot from a remote volume. Missing are: ['ramdisk', 'kernel', 'image_source']",
+		},
+		Inspect: nodes.DriverValidation{
+			Result: false,
+			Reason: "Driver ipmi does not support inspect (disabled or not implemented).",
+		},
+		Management: nodes.DriverValidation{
+			Result: true,
+		},
+		Network: nodes.DriverValidation{
+			Result: true,
+		},
+		Power: nodes.DriverValidation{
+			Result: true,
+		},
+		RAID: nodes.DriverValidation{
+			Result: false,
+			Reason: "Driver ipmi does not support raid (disabled or not implemented).",
+		},
+		Rescue: nodes.DriverValidation{
+			Result: false,
+			Reason: "Driver ipmi does not support rescue (disabled or not implemented).",
+		},
+		Storage: nodes.DriverValidation{
+			Result: true,
+		},
+	}
+
+	NodeBootDevice = nodes.BootDeviceOpts{
+		BootDevice: "pxe",
+		Persistent: false,
+	}
+
+	NodeSupportedBootDevice = []string{
+		"pxe",
+		"disk",
+	}
+
+	NodeBar = nodes.Node{
+		UUID:                 "08c84581-58f5-4ea2-a0c6-dd2e5d2b3662",
+		Name:                 "bar",
+		PowerState:           "",
+		TargetPowerState:     "",
+		ProvisionState:       "enroll",
+		TargetProvisionState: "",
+		Maintenance:          false,
+		MaintenanceReason:    "",
+		Fault:                "",
+		LastError:            "",
+		Reservation:          "",
+		Driver:               "ipmi",
+		DriverInfo:           map[string]interface{}{},
+		DriverInternalInfo:   map[string]interface{}{},
+		Properties:           map[string]interface{}{},
+		InstanceInfo:         map[string]interface{}{},
+		InstanceUUID:         "",
+		ChassisUUID:          "",
+		Extra:                map[string]interface{}{},
+		ConsoleEnabled:       false,
+		RAIDConfig:           map[string]interface{}{},
+		TargetRAIDConfig:     map[string]interface{}{},
+		CleanStep:            map[string]interface{}{},
+		DeployStep:           map[string]interface{}{},
+		ResourceClass:        "",
+		BootInterface:        "pxe",
+		ConsoleInterface:     "no-console",
+		DeployInterface:      "iscsi",
+		InspectInterface:     "no-inspect",
+		ManagementInterface:  "ipmitool",
+		NetworkInterface:     "flat",
+		PowerInterface:       "ipmitool",
+		RAIDInterface:        "no-raid",
+		RescueInterface:      "no-rescue",
+		StorageInterface:     "noop",
+		Traits:               []string{},
+		VendorInterface:      "ipmitool",
+		ConductorGroup:       "",
+		Protected:            false,
+		ProtectedReason:      "",
+	}
+
+	NodeBaz = nodes.Node{
+		UUID:                 "c9afd385-5d89-4ecb-9e1c-68194da6b474",
+		Name:                 "baz",
+		PowerState:           "",
+		TargetPowerState:     "",
+		ProvisionState:       "enroll",
+		TargetProvisionState: "",
+		Maintenance:          false,
+		MaintenanceReason:    "",
+		Fault:                "",
+		LastError:            "",
+		Reservation:          "",
+		Driver:               "ipmi",
+		DriverInfo:           map[string]interface{}{},
+		DriverInternalInfo:   map[string]interface{}{},
+		Properties:           map[string]interface{}{},
+		InstanceInfo:         map[string]interface{}{},
+		InstanceUUID:         "",
+		ChassisUUID:          "",
+		Extra:                map[string]interface{}{},
+		ConsoleEnabled:       false,
+		RAIDConfig:           map[string]interface{}{},
+		TargetRAIDConfig:     map[string]interface{}{},
+		CleanStep:            map[string]interface{}{},
+		DeployStep:           map[string]interface{}{},
+		ResourceClass:        "",
+		BootInterface:        "pxe",
+		ConsoleInterface:     "no-console",
+		DeployInterface:      "iscsi",
+		InspectInterface:     "no-inspect",
+		ManagementInterface:  "ipmitool",
+		NetworkInterface:     "flat",
+		PowerInterface:       "ipmitool",
+		RAIDInterface:        "no-raid",
+		RescueInterface:      "no-rescue",
+		StorageInterface:     "noop",
+		Traits:               []string{},
+		VendorInterface:      "ipmitool",
+		ConductorGroup:       "",
+		Protected:            false,
+		ProtectedReason:      "",
+	}
+
+	ConfigDriveMap = nodes.ConfigDrive{
+		UserData: map[string]interface{}{
+			"ignition": map[string]string{
+				"version": "2.2.0",
+			},
+			"systemd": map[string]interface{}{
+				"units": []map[string]interface{}{{
+					"name":    "example.service",
+					"enabled": true,
+				},
+				},
+			},
+		},
+	}
+)
+
+// HandleNodeListSuccessfully sets up the test server to respond to a server List request.
+func HandleNodeListSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/nodes", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		w.Header().Add("Content-Type", "application/json")
+		r.ParseForm()
+
+		marker := r.Form.Get("marker")
+		switch marker {
+		case "":
+			fmt.Fprintf(w, NodeListBody)
+
+		case "9e5476bd-a4ec-4653-93d6-72c93aa682ba":
+			fmt.Fprintf(w, `{ "servers": [] }`)
+		default:
+			t.Fatalf("/nodes invoked with unexpected marker=[%s]", marker)
+		}
+	})
+}
+
+// HandleNodeListSuccessfully sets up the test server to respond to a server List request.
+func HandleNodeListDetailSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/nodes/detail", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		w.Header().Add("Content-Type", "application/json")
+		r.ParseForm()
+
+		fmt.Fprintf(w, NodeListDetailBody)
+	})
+}
+
+// HandleServerCreationSuccessfully sets up the test server to respond to a server creation request
+// with a given response.
+func HandleNodeCreationSuccessfully(t *testing.T, response string) {
+	th.Mux.HandleFunc("/nodes", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t, r, `{
+          "boot_interface": "pxe",
+          "driver": "ipmi",
+          "driver_info": {
+            "deploy_kernel": "http://172.22.0.1/images/tinyipa-stable-rocky.vmlinuz",
+            "deploy_ramdisk": "http://172.22.0.1/images/tinyipa-stable-rocky.gz",
+            "ipmi_address": "192.168.122.1",
+            "ipmi_password": "admin",
+            "ipmi_port": "6230",
+            "ipmi_username": "admin"
+          },
+          "name": "foo"
+        }`)
+
+		w.WriteHeader(http.StatusAccepted)
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, response)
+	})
+}
+
+// HandleNodeDeletionSuccessfully sets up the test server to respond to a server deletion request.
+func HandleNodeDeletionSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/nodes/asdfasdfasdf", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
+
+func HandleNodeGetSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/nodes/1234asdf", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+
+		fmt.Fprintf(w, SingleNodeBody)
+	})
+}
+
+func HandleNodeUpdateSuccessfully(t *testing.T, response string) {
+	th.Mux.HandleFunc("/nodes/1234asdf", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PATCH")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestJSONRequest(t, r, `[{"op": "replace", "path": "/properties", "value": {"root_gb": 25}}]`)
+
+		fmt.Fprintf(w, response)
+	})
+}
+
+func HandleNodeValidateSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/nodes/1234asdf/validate", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+
+		fmt.Fprintf(w, NodeValidationBody)
+	})
+}
+
+// HandleInjectNMISuccessfully sets up the test server to respond to a node InjectNMI request
+func HandleInjectNMISuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/nodes/1234asdf/management/inject_nmi", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t, r, "{}")
+
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
+
+// HandleSetBootDeviceSuccessfully sets up the test server to respond to a set boot device request for a node
+func HandleSetBootDeviceSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/nodes/1234asdf/management/boot_device", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t, r, NodeBootDeviceBody)
+
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
+
+// HandleGetBootDeviceSuccessfully sets up the test server to respond to a get boot device request for a node
+func HandleGetBootDeviceSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/nodes/1234asdf/management/boot_device", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, NodeBootDeviceBody)
+	})
+}
+
+// HandleGetBootDeviceSuccessfully sets up the test server to respond to a get boot device request for a node
+func HandleGetSupportedBootDeviceSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/nodes/1234asdf/management/boot_device/supported", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, NodeSupportedBootDeviceBody)
+	})
+}
+
+func HandleNodeChangeProvisionStateActive(t *testing.T) {
+	th.Mux.HandleFunc("/nodes/1234asdf/states/provision", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t, r, NodeProvisionStateActiveBody)
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+func HandleNodeChangeProvisionStateClean(t *testing.T) {
+	th.Mux.HandleFunc("/nodes/1234asdf/states/provision", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t, r, NodeProvisionStateCleanBody)
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+func HandleNodeChangeProvisionStateCleanWithConflict(t *testing.T) {
+	th.Mux.HandleFunc("/nodes/1234asdf/states/provision", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t, r, NodeProvisionStateCleanBody)
+		w.WriteHeader(http.StatusConflict)
+	})
+}
+
+func HandleNodeChangeProvisionStateConfigDrive(t *testing.T) {
+	th.Mux.HandleFunc("/nodes/1234asdf/states/provision", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t, r, NodeProvisionStateConfigDriveBody)
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+// HandleChangePowerStateSuccessfully sets up the test server to respond to a change power state request for a node
+func HandleChangePowerStateSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/nodes/1234asdf/states/power", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t, r, `{
+			"target": "power on",
+			"timeout": 100
+		}`)
+
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+// HandleChangePowerStateWithConflict sets up the test server to respond to a change power state request for a node with a 409 error
+func HandleChangePowerStateWithConflict(t *testing.T) {
+	th.Mux.HandleFunc("/nodes/1234asdf/states/power", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t, r, `{
+			"target": "power on",
+			"timeout": 100
+		}`)
+
+		w.WriteHeader(http.StatusConflict)
+	})
+}
+
+func HandleSetRAIDConfig(t *testing.T) {
+	th.Mux.HandleFunc("/nodes/1234asdf/states/raid", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t, r, `
+			{
+			   "logical_disks" : [
+				  {
+					 "size_gb" : 100,
+					 "is_root_volume" : true,
+					 "raid_level" : "1"
+				  }
+			   ]
+			}
+		`)
+
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
+
+func HandleSetRAIDConfigMaxSize(t *testing.T) {
+	th.Mux.HandleFunc("/nodes/1234asdf/states/raid", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t, r, `
+			{
+			   "logical_disks" : [
+				  {
+					 "size_gb" : "MAX",
+					 "is_root_volume" : true,
+					 "raid_level" : "1"
+				  }
+			   ]
+			}
+		`)
+
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
diff --git a/openstack/baremetal/v1/nodes/testing/requests_test.go b/openstack/baremetal/v1/nodes/testing/requests_test.go
new file mode 100644
index 0000000..aa1ac58
--- /dev/null
+++ b/openstack/baremetal/v1/nodes/testing/requests_test.go
@@ -0,0 +1,432 @@
+package testing
+
+import (
+	"testing"
+
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git"
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git/openstack/baremetal/v1/nodes"
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git/pagination"
+	th "gerrit.mcp.mirantis.net/debian/gophercloud.git/testhelper"
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git/testhelper/client"
+)
+
+func TestListDetailNodes(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleNodeListDetailSuccessfully(t)
+
+	pages := 0
+	err := nodes.ListDetail(client.ServiceClient(), nodes.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := nodes.ExtractNodes(page)
+		if err != nil {
+			return false, err
+		}
+
+		if len(actual) != 3 {
+			t.Fatalf("Expected 3 nodes, got %d", len(actual))
+		}
+		th.CheckDeepEquals(t, NodeFoo, actual[0])
+		th.CheckDeepEquals(t, NodeBar, actual[1])
+		th.CheckDeepEquals(t, NodeBaz, actual[2])
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+
+	if pages != 1 {
+		t.Errorf("Expected 1 page, saw %d", pages)
+	}
+}
+
+func TestListNodes(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleNodeListSuccessfully(t)
+
+	pages := 0
+	err := nodes.List(client.ServiceClient(), nodes.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := nodes.ExtractNodes(page)
+		if err != nil {
+			return false, err
+		}
+
+		if len(actual) != 3 {
+			t.Fatalf("Expected 3 nodes, got %d", len(actual))
+		}
+		th.AssertEquals(t, "foo", actual[0].Name)
+		th.AssertEquals(t, "bar", actual[1].Name)
+		th.AssertEquals(t, "baz", actual[2].Name)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+
+	if pages != 1 {
+		t.Errorf("Expected 1 page, saw %d", pages)
+	}
+}
+
+func TestListOpts(t *testing.T) {
+	// Detail cannot take Fields
+	opts := nodes.ListOpts{
+		Fields: []string{"name", "uuid"},
+	}
+
+	_, err := opts.ToNodeListDetailQuery()
+	th.AssertEquals(t, err.Error(), "fields is not a valid option when getting a detailed listing of nodes")
+
+	// Regular ListOpts can
+	query, err := opts.ToNodeListQuery()
+	th.AssertEquals(t, query, "?fields=name&fields=uuid")
+	th.AssertNoErr(t, err)
+}
+
+func TestCreateNode(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleNodeCreationSuccessfully(t, SingleNodeBody)
+
+	actual, err := nodes.Create(client.ServiceClient(), nodes.CreateOpts{
+		Name:          "foo",
+		Driver:        "ipmi",
+		BootInterface: "pxe",
+		DriverInfo: map[string]interface{}{
+			"ipmi_port":      "6230",
+			"ipmi_username":  "admin",
+			"deploy_kernel":  "http://172.22.0.1/images/tinyipa-stable-rocky.vmlinuz",
+			"ipmi_address":   "192.168.122.1",
+			"deploy_ramdisk": "http://172.22.0.1/images/tinyipa-stable-rocky.gz",
+			"ipmi_password":  "admin",
+		},
+	}).Extract()
+	th.AssertNoErr(t, err)
+
+	th.CheckDeepEquals(t, NodeFoo, *actual)
+}
+
+func TestDeleteNode(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleNodeDeletionSuccessfully(t)
+
+	res := nodes.Delete(client.ServiceClient(), "asdfasdfasdf")
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestGetNode(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleNodeGetSuccessfully(t)
+
+	c := client.ServiceClient()
+	actual, err := nodes.Get(c, "1234asdf").Extract()
+	if err != nil {
+		t.Fatalf("Unexpected Get error: %v", err)
+	}
+
+	th.CheckDeepEquals(t, NodeFoo, *actual)
+}
+
+func TestUpdateNode(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleNodeUpdateSuccessfully(t, SingleNodeBody)
+
+	c := client.ServiceClient()
+	actual, err := nodes.Update(c, "1234asdf", nodes.UpdateOpts{
+		nodes.UpdateOperation{
+			Op:   nodes.ReplaceOp,
+			Path: "/properties",
+			Value: map[string]interface{}{
+				"root_gb": 25,
+			},
+		},
+	}).Extract()
+	if err != nil {
+		t.Fatalf("Unexpected Update error: %v", err)
+	}
+
+	th.CheckDeepEquals(t, NodeFoo, *actual)
+}
+
+func TestUpdateRequiredOp(t *testing.T) {
+	c := client.ServiceClient()
+	_, err := nodes.Update(c, "1234asdf", nodes.UpdateOpts{
+		nodes.UpdateOperation{
+			Path:  "/driver",
+			Value: "new-driver",
+		},
+	}).Extract()
+
+	if _, ok := err.(gophercloud.ErrMissingInput); !ok {
+		t.Fatal("ErrMissingInput was expected to occur")
+	}
+
+}
+
+func TestUpdateRequiredPath(t *testing.T) {
+	c := client.ServiceClient()
+	_, err := nodes.Update(c, "1234asdf", nodes.UpdateOpts{
+		nodes.UpdateOperation{
+			Op:    nodes.ReplaceOp,
+			Value: "new-driver",
+		},
+	}).Extract()
+
+	if _, ok := err.(gophercloud.ErrMissingInput); !ok {
+		t.Fatal("ErrMissingInput was expected to occur")
+	}
+}
+
+func TestValidateNode(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleNodeValidateSuccessfully(t)
+
+	c := client.ServiceClient()
+	actual, err := nodes.Validate(c, "1234asdf").Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, NodeFooValidation, *actual)
+}
+
+func TestInjectNMI(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleInjectNMISuccessfully(t)
+
+	c := client.ServiceClient()
+	err := nodes.InjectNMI(c, "1234asdf").ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestSetBootDevice(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleSetBootDeviceSuccessfully(t)
+
+	c := client.ServiceClient()
+	err := nodes.SetBootDevice(c, "1234asdf", nodes.BootDeviceOpts{
+		BootDevice: "pxe",
+		Persistent: false,
+	}).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestGetBootDevice(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGetBootDeviceSuccessfully(t)
+
+	c := client.ServiceClient()
+	bootDevice, err := nodes.GetBootDevice(c, "1234asdf").Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, NodeBootDevice, *bootDevice)
+}
+
+func TestGetSupportedBootDevices(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGetSupportedBootDeviceSuccessfully(t)
+
+	c := client.ServiceClient()
+	bootDevices, err := nodes.GetSupportedBootDevices(c, "1234asdf").Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, NodeSupportedBootDevice, bootDevices)
+}
+
+func TestNodeChangeProvisionStateActive(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleNodeChangeProvisionStateActive(t)
+
+	c := client.ServiceClient()
+	err := nodes.ChangeProvisionState(c, "1234asdf", nodes.ProvisionStateOpts{
+		Target:      nodes.TargetActive,
+		ConfigDrive: "http://127.0.0.1/images/test-node-config-drive.iso.gz",
+	}).ExtractErr()
+
+	th.AssertNoErr(t, err)
+}
+
+func TestHandleNodeChangeProvisionStateConfigDrive(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleNodeChangeProvisionStateConfigDrive(t)
+
+	c := client.ServiceClient()
+
+	err := nodes.ChangeProvisionState(c, "1234asdf", nodes.ProvisionStateOpts{
+		Target:      nodes.TargetActive,
+		ConfigDrive: ConfigDriveMap,
+	}).ExtractErr()
+
+	th.AssertNoErr(t, err)
+}
+
+func TestNodeChangeProvisionStateClean(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleNodeChangeProvisionStateClean(t)
+
+	c := client.ServiceClient()
+	err := nodes.ChangeProvisionState(c, "1234asdf", nodes.ProvisionStateOpts{
+		Target: nodes.TargetClean,
+		CleanSteps: []nodes.CleanStep{
+			{
+				Interface: "deploy",
+				Step:      "upgrade_firmware",
+				Args: map[string]interface{}{
+					"force": "True",
+				},
+			},
+		},
+	}).ExtractErr()
+
+	th.AssertNoErr(t, err)
+}
+
+func TestNodeChangeProvisionStateCleanWithConflict(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleNodeChangeProvisionStateCleanWithConflict(t)
+
+	c := client.ServiceClient()
+	err := nodes.ChangeProvisionState(c, "1234asdf", nodes.ProvisionStateOpts{
+		Target: nodes.TargetClean,
+		CleanSteps: []nodes.CleanStep{
+			{
+				Interface: "deploy",
+				Step:      "upgrade_firmware",
+				Args: map[string]interface{}{
+					"force": "True",
+				},
+			},
+		},
+	}).ExtractErr()
+
+	if _, ok := err.(gophercloud.ErrDefault409); !ok {
+		t.Fatal("ErrDefault409 was expected to occur")
+	}
+}
+
+func TestCleanStepRequiresInterface(t *testing.T) {
+	c := client.ServiceClient()
+	err := nodes.ChangeProvisionState(c, "1234asdf", nodes.ProvisionStateOpts{
+		Target: nodes.TargetClean,
+		CleanSteps: []nodes.CleanStep{
+			{
+				Step: "upgrade_firmware",
+				Args: map[string]interface{}{
+					"force": "True",
+				},
+			},
+		},
+	}).ExtractErr()
+
+	if _, ok := err.(gophercloud.ErrMissingInput); !ok {
+		t.Fatal("ErrMissingInput was expected to occur")
+	}
+}
+
+func TestCleanStepRequiresStep(t *testing.T) {
+	c := client.ServiceClient()
+	err := nodes.ChangeProvisionState(c, "1234asdf", nodes.ProvisionStateOpts{
+		Target: nodes.TargetClean,
+		CleanSteps: []nodes.CleanStep{
+			{
+				Interface: "deploy",
+				Args: map[string]interface{}{
+					"force": "True",
+				},
+			},
+		},
+	}).ExtractErr()
+
+	if _, ok := err.(gophercloud.ErrMissingInput); !ok {
+		t.Fatal("ErrMissingInput was expected to occur")
+	}
+}
+
+func TestChangePowerState(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleChangePowerStateSuccessfully(t)
+
+	opts := nodes.PowerStateOpts{
+		Target:  nodes.PowerOn,
+		Timeout: 100,
+	}
+
+	c := client.ServiceClient()
+	err := nodes.ChangePowerState(c, "1234asdf", opts).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestChangePowerStateWithConflict(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleChangePowerStateWithConflict(t)
+
+	opts := nodes.PowerStateOpts{
+		Target:  nodes.PowerOn,
+		Timeout: 100,
+	}
+
+	c := client.ServiceClient()
+	err := nodes.ChangePowerState(c, "1234asdf", opts).ExtractErr()
+	if _, ok := err.(gophercloud.ErrDefault409); !ok {
+		t.Fatal("ErrDefault409 was expected to occur")
+	}
+}
+
+func TestSetRAIDConfig(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleSetRAIDConfig(t)
+
+	sizeGB := 100
+	isRootVolume := true
+
+	config := nodes.RAIDConfigOpts{
+		LogicalDisks: []nodes.LogicalDisk{
+			{
+				SizeGB:       &sizeGB,
+				IsRootVolume: &isRootVolume,
+				RAIDLevel:    nodes.RAID1,
+			},
+		},
+	}
+
+	c := client.ServiceClient()
+	err := nodes.SetRAIDConfig(c, "1234asdf", config).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+// Without specifying a size, we need to send a string: "MAX"
+func TestSetRAIDConfigMaxSize(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleSetRAIDConfigMaxSize(t)
+
+	isRootVolume := true
+
+	config := nodes.RAIDConfigOpts{
+		LogicalDisks: []nodes.LogicalDisk{
+			{
+				IsRootVolume: &isRootVolume,
+				RAIDLevel:    nodes.RAID1,
+			},
+		},
+	}
+
+	c := client.ServiceClient()
+	err := nodes.SetRAIDConfig(c, "1234asdf", config).ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/baremetal/v1/nodes/urls.go b/openstack/baremetal/v1/nodes/urls.go
index 02aedbf..75b4726 100644
--- a/openstack/baremetal/v1/nodes/urls.go
+++ b/openstack/baremetal/v1/nodes/urls.go
@@ -2,6 +2,58 @@
 
 import "gerrit.mcp.mirantis.net/debian/gophercloud.git"
 
-func listURL(c *gophercloud.ServiceClient) string {
-	return c.ServiceURL("nodes")
+func createURL(client *gophercloud.ServiceClient) string {
+	return client.ServiceURL("nodes")
+}
+
+func listURL(client *gophercloud.ServiceClient) string {
+	return createURL(client)
+}
+
+func listDetailURL(client *gophercloud.ServiceClient) string {
+	return client.ServiceURL("nodes", "detail")
+}
+
+func deleteURL(client *gophercloud.ServiceClient, id string) string {
+	return client.ServiceURL("nodes", id)
+}
+
+func getURL(client *gophercloud.ServiceClient, id string) string {
+	return deleteURL(client, id)
+}
+
+func updateURL(client *gophercloud.ServiceClient, id string) string {
+	return deleteURL(client, id)
+}
+
+func validateURL(client *gophercloud.ServiceClient, id string) string {
+	return client.ServiceURL("nodes", id, "validate")
+}
+
+func injectNMIURL(client *gophercloud.ServiceClient, id string) string {
+	return client.ServiceURL("nodes", id, "management", "inject_nmi")
+}
+
+func bootDeviceURL(client *gophercloud.ServiceClient, id string) string {
+	return client.ServiceURL("nodes", id, "management", "boot_device")
+}
+
+func supportedBootDeviceURL(client *gophercloud.ServiceClient, id string) string {
+	return client.ServiceURL("nodes", id, "management", "boot_device", "supported")
+}
+
+func statesResourceURL(client *gophercloud.ServiceClient, id string, state string) string {
+	return client.ServiceURL("nodes", id, "states", state)
+}
+
+func powerStateURL(client *gophercloud.ServiceClient, id string) string {
+	return statesResourceURL(client, id, "power")
+}
+
+func provisionStateURL(client *gophercloud.ServiceClient, id string) string {
+	return statesResourceURL(client, id, "provision")
+}
+
+func raidConfigURL(client *gophercloud.ServiceClient, id string) string {
+	return statesResourceURL(client, id, "raid")
 }
diff --git a/openstack/baremetal/v1/ports/doc.go b/openstack/baremetal/v1/ports/doc.go
new file mode 100644
index 0000000..eb0579b
--- /dev/null
+++ b/openstack/baremetal/v1/ports/doc.go
@@ -0,0 +1,85 @@
+/*
+ Package ports contains the functionality to Listing, Searching, Creating, Updating,
+ and Deleting of bare metal Port resources
+
+ API reference: https://developer.openstack.org/api-ref/baremetal/#ports-ports
+
+
+Example to List Ports with Detail
+
+ 	ports.ListDetail(client, nil).EachPage(func(page pagination.Page) (bool, error) {
+ 		portList, err := ports.ExtractPorts(page)
+ 		if err != nil {
+ 			return false, err
+ 		}
+
+ 		for _, n := range portList {
+ 			// Do something
+ 		}
+
+ 		return true, nil
+ 	})
+
+Example to List Ports
+
+	listOpts := ports.ListOpts{
+ 		Limit: 10,
+	}
+
+ 	ports.List(client, listOpts).EachPage(func(page pagination.Page) (bool, error) {
+ 		portList, err := ports.ExtractPorts(page)
+ 		if err != nil {
+ 			return false, err
+ 		}
+
+ 		for _, n := range portList {
+ 			// Do something
+ 		}
+
+ 		return true, nil
+ 	})
+
+Example to Create a Port
+
+	createOpts := ports.CreateOpts{
+ 		NodeUUID: "e8920409-e07e-41bb-8cc1-72acb103e2dd",
+		Address: "00:1B:63:84:45:E6",
+    PhysicalNetwork: "my-network",
+	}
+
+ 	createPort, err := ports.Create(client, createOpts).Extract()
+ 	if err != nil {
+ 		panic(err)
+ 	}
+
+Example to Get a Port
+
+ 	showPort, err := ports.Get(client, "c9afd385-5d89-4ecb-9e1c-68194da6b474").Extract()
+ 	if err != nil {
+ 		panic(err)
+ 	}
+
+Example to Update a Port
+
+	updateOpts := ports.UpdateOpts{
+ 		ports.UpdateOperation{
+ 			Op:    ReplaceOp,
+ 			Path:  "/address",
+ 			Value: "22:22:22:22:22:22",
+ 		},
+	}
+
+ 	updatePort, err := ports.Update(client, "c9afd385-5d89-4ecb-9e1c-68194da6b474", updateOpts).Extract()
+ 	if err != nil {
+ 		panic(err)
+ 	}
+
+Example to Delete a Port
+
+ 	err = ports.Delete(client, "c9afd385-5d89-4ecb-9e1c-68194da6b474").ExtractErr()
+ 	if err != nil {
+ 		panic(err)
+	}
+
+*/
+package ports
diff --git a/openstack/baremetal/v1/ports/requests.go b/openstack/baremetal/v1/ports/requests.go
new file mode 100644
index 0000000..00419c6
--- /dev/null
+++ b/openstack/baremetal/v1/ports/requests.go
@@ -0,0 +1,216 @@
+package ports
+
+import (
+	"fmt"
+
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git"
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git/pagination"
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+	ToPortListQuery() (string, error)
+	ToPortListDetailQuery() (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 node attributes you want to see returned. Marker and Limit are used
+// for pagination.
+type ListOpts struct {
+	// Filter the list by the name or uuid of the Node
+	Node string `q:"node"`
+
+	// Filter the list by the Node uuid
+	NodeUUID string `q:"node_uuid"`
+
+	// Filter the list with the specified Portgroup (name or UUID)
+	PortGroup string `q:"portgroup"`
+
+	// Filter the list with the specified physical hardware address, typically MAC
+	Address string `q:"address"`
+
+	// One or more fields to be returned in the response.
+	Fields []string `q:"fields"`
+
+	// Requests a page size of items.
+	Limit int `q:"limit"`
+
+	// The ID of the last-seen item
+	Marker string `q:"marker"`
+
+	// Sorts the response by the requested sort direction.
+	// Valid value is asc (ascending) or desc (descending). Default is asc.
+	SortDir string `q:"sort_dir"`
+
+	// Sorts the response by the this attribute value. Default is id.
+	SortKey string `q:"sort_key"`
+}
+
+// ToPortListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToPortListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	return q.String(), err
+}
+
+// List makes a request against the API to list ports accessible to you.
+func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	url := listURL(client)
+	if opts != nil {
+		query, err := opts.ToPortListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+		return PortPage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+// ToPortListDetailQuery formats a ListOpts into a query string for the list details API.
+func (opts ListOpts) ToPortListDetailQuery() (string, error) {
+	// Detail endpoint can't filter by Fields
+	if len(opts.Fields) > 0 {
+		return "", fmt.Errorf("fields is not a valid option when getting a detailed listing of ports")
+	}
+
+	q, err := gophercloud.BuildQueryString(opts)
+	return q.String(), err
+}
+
+// ListDetail - Return a list ports with complete details.
+// Some filtering is possible by passing in flags in "ListOpts",
+// but you cannot limit by the fields returned.
+func ListDetail(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	url := listDetailURL(client)
+	if opts != nil {
+		query, err := opts.ToPortListDetailQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+		return PortPage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+// Get - requests the details off a port, by ID.
+func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
+	_, r.Err = client.Get(getURL(client, id), &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+	ToPortCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts specifies port creation parameters.
+type CreateOpts struct {
+	// UUID of the Node this resource belongs to.
+	NodeUUID string `json:"node_uuid,omitempty"`
+
+	// Physical hardware address of this network Port,
+	// typically the hardware MAC address.
+	Address string `json:"address,omitempty"`
+
+	// UUID of the Portgroup this resource belongs to.
+	PortGroupUUID string `json:"portgroup_uuid,omitempty"`
+
+	// The Port binding profile. If specified, must contain switch_id (only a MAC
+	// address or an OpenFlow based datapath_id of the switch are accepted in this
+	// field) and port_id (identifier of the physical port on the switch to which
+	// node’s port is connected to) fields. switch_info is an optional string
+	// field to be used to store any vendor-specific information.
+	LocalLinkConnection map[string]interface{} `json:"local_link_connection,omitempty"`
+
+	// Indicates whether PXE is enabled or disabled on the Port.
+	PXEEnabled *bool `json:"pxe_enabled,omitempty"`
+
+	// The name of the physical network to which a port is connected. May be empty.
+	PhysicalNetwork string `json:"physical_network,omitempty"`
+
+	// A set of one or more arbitrary metadata key and value pairs.
+	Extra map[string]interface{} `json:"extra,omitempty"`
+
+	// Indicates whether the Port is a Smart NIC port.
+	IsSmartNIC *bool `json:"is_smartnic,omitempty"`
+}
+
+// ToPortCreateMap assembles a request body based on the contents of a CreateOpts.
+func (opts CreateOpts) ToPortCreateMap() (map[string]interface{}, error) {
+	body, err := gophercloud.BuildRequestBody(opts, "")
+	if err != nil {
+		return nil, err
+	}
+
+	return body, nil
+}
+
+// Create - requests the creation of a port
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+	reqBody, err := opts.ToPortCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+
+	_, r.Err = client.Post(createURL(client), reqBody, &r.Body, nil)
+	return
+}
+
+// TODO Update
+type Patch interface {
+	ToPortUpdateMap() map[string]interface{}
+}
+
+// UpdateOpts is a slice of Patches used to update a port
+type UpdateOpts []Patch
+
+type UpdateOp string
+
+const (
+	ReplaceOp UpdateOp = "replace"
+	AddOp     UpdateOp = "add"
+	RemoveOp  UpdateOp = "remove"
+)
+
+type UpdateOperation struct {
+	Op    UpdateOp    `json:"op,required"`
+	Path  string      `json:"path,required"`
+	Value interface{} `json:"value,omitempty"`
+}
+
+func (opts UpdateOperation) ToPortUpdateMap() map[string]interface{} {
+	return map[string]interface{}{
+		"op":    opts.Op,
+		"path":  opts.Path,
+		"value": opts.Value,
+	}
+}
+
+// Update - requests the update of a port
+func Update(client *gophercloud.ServiceClient, id string, opts UpdateOpts) (r UpdateResult) {
+	body := make([]map[string]interface{}, len(opts))
+	for i, patch := range opts {
+		body[i] = patch.ToPortUpdateMap()
+	}
+
+	_, r.Err = client.Patch(updateURL(client, id), body, &r.Body, &gophercloud.RequestOpts{
+		JSONBody: &body,
+		OkCodes:  []int{200},
+	})
+	return
+}
+
+// Delete - requests the deletion of a port
+func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
+	_, r.Err = client.Delete(deleteURL(client, id), nil)
+	return
+}
diff --git a/openstack/baremetal/v1/ports/results.go b/openstack/baremetal/v1/ports/results.go
new file mode 100644
index 0000000..3b26811
--- /dev/null
+++ b/openstack/baremetal/v1/ports/results.go
@@ -0,0 +1,131 @@
+package ports
+
+import (
+	"time"
+
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git"
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git/pagination"
+)
+
+type portResult struct {
+	gophercloud.Result
+}
+
+func (r portResult) Extract() (*Port, error) {
+	var s Port
+	err := r.ExtractInto(&s)
+	return &s, err
+}
+
+func (r portResult) ExtractInto(v interface{}) error {
+	return r.Result.ExtractIntoStructPtr(v, "")
+}
+
+func ExtractPortsInto(r pagination.Page, v interface{}) error {
+	return r.(PortPage).Result.ExtractIntoSlicePtr(v, "ports")
+}
+
+// Port represents a port in the OpenStack Bare Metal API.
+type Port struct {
+	// UUID for the resource.
+	UUID string `json:"uuid"`
+
+	// Physical hardware address of this network Port,
+	// typically the hardware MAC address.
+	Address string `json:"address"`
+
+	// UUID of the Node this resource belongs to.
+	NodeUUID string `json:"node_uuid"`
+
+	// UUID of the Portgroup this resource belongs to.
+	PortGroupUUID string `json:"portgroup_uuid"`
+
+	// The Port binding profile. If specified, must contain switch_id (only a MAC
+	// address or an OpenFlow based datapath_id of the switch are accepted in this
+	// field) and port_id (identifier of the physical port on the switch to which
+	// node’s port is connected to) fields. switch_info is an optional string
+	// field to be used to store any vendor-specific information.
+	LocalLinkConnection map[string]interface{} `json:"local_link_connection"`
+
+	// Indicates whether PXE is enabled or disabled on the Port.
+	PXEEnabled bool `json:"pxe_enabled"`
+
+	// The name of the physical network to which a port is connected.
+	// May be empty.
+	PhysicalNetwork string `json:"physical_network"`
+
+	// Internal metadata set and stored by the Port. This field is read-only.
+	InternalInfo map[string]interface{} `json:"internal_info"`
+
+	// A set of one or more arbitrary metadata key and value pairs.
+	Extra map[string]interface{} `json:"extra"`
+
+	// The UTC date and time when the resource was created, ISO 8601 format.
+	CreatedAt time.Time `json:"created_at"`
+
+	// The UTC date and time when the resource was updated, ISO 8601 format.
+	// May be “null”.
+	UpdatedAt time.Time `json:"updated_at"`
+
+	// A list of relative links. Includes the self and bookmark links.
+	Links []interface{} `json:"links"`
+
+	// Indicates whether the Port is a Smart NIC port.
+	IsSmartNIC bool `json:"is_smartnic"`
+}
+
+// PortPage abstracts the raw results of making a List() request against
+// the API.
+type PortPage struct {
+	pagination.LinkedPageBase
+}
+
+// IsEmpty returns true if a page contains no Port results.
+func (r PortPage) IsEmpty() (bool, error) {
+	s, err := ExtractPorts(r)
+	return len(s) == 0, err
+}
+
+// NextPageURL uses the response's embedded link reference to navigate to the
+// next page of results.
+func (r PortPage) NextPageURL() (string, error) {
+	var s struct {
+		Links []gophercloud.Link `json:"ports_links"`
+	}
+	err := r.ExtractInto(&s)
+	if err != nil {
+		return "", err
+	}
+	return gophercloud.ExtractNextURL(s.Links)
+}
+
+// ExtractPorts interprets the results of a single page from a List() call,
+// producing a slice of Port entities.
+func ExtractPorts(r pagination.Page) ([]Port, error) {
+	var s []Port
+	err := ExtractPortsInto(r, &s)
+	return s, err
+}
+
+// GetResult is the response from a Get operation. Call its Extract
+// method to interpret it as a Port.
+type GetResult struct {
+	portResult
+}
+
+// CreateResult is the response from a Create operation.
+type CreateResult struct {
+	portResult
+}
+
+// UpdateResult is the response from an Update operation. Call its Extract
+// method to interpret it as a Port.
+type UpdateResult struct {
+	portResult
+}
+
+// DeleteResult is the response from a Delete operation. Call its ExtractErr
+// method to determine if the call succeeded or failed.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
diff --git a/openstack/baremetal/v1/ports/testing/doc.go b/openstack/baremetal/v1/ports/testing/doc.go
new file mode 100644
index 0000000..bf82f4e
--- /dev/null
+++ b/openstack/baremetal/v1/ports/testing/doc.go
@@ -0,0 +1,2 @@
+// ports unit tests
+package testing
diff --git a/openstack/baremetal/v1/ports/testing/fixtures.go b/openstack/baremetal/v1/ports/testing/fixtures.go
new file mode 100644
index 0000000..2fc7278
--- /dev/null
+++ b/openstack/baremetal/v1/ports/testing/fixtures.go
@@ -0,0 +1,252 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git/openstack/baremetal/v1/ports"
+	th "gerrit.mcp.mirantis.net/debian/gophercloud.git/testhelper"
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git/testhelper/client"
+)
+
+// PortListBody contains the canned body of a ports.List response, without detail.
+const PortListBody = `
+{
+   "ports": [
+       {
+           "uuid": "3abe3f36-9708-4e9f-b07e-0f898061d3a7",
+           "links": [
+               {
+                   "href": "http://192.168.0.8/baremetal/v1/ports/3abe3f36-9708-4e9f-b07e-0f898061d3a7",
+                   "rel": "self"
+               },
+               {
+                   "href": "http://192.168.0.8/baremetal/ports/3abe3f36-9708-4e9f-b07e-0f898061d3a7",
+                   "rel": "bookmark"
+               }
+           ],
+           "address": "52:54:00:0a:af:d1"
+       },
+       {
+           "uuid": "f2845e11-dbd4-4728-a8c0-30d19f48924a",
+           "links": [
+               {
+                   "href": "http://192.168.0.8/baremetal/v1/ports/f2845e11-dbd4-4728-a8c0-30d19f48924a",
+                   "rel": "self"
+               },
+               {
+                   "href": "http://192.168.0.8/baremetal/ports/f2845e11-dbd4-4728-a8c0-30d19f48924a",
+                   "rel": "bookmark"
+               }
+           ],
+           "address": "52:54:00:4d:87:e6"
+       }
+   ]
+}
+`
+
+// PortListDetailBody contains the canned body of a port.ListDetail response.
+const PortListDetailBody = `
+{
+   "ports": [
+       {
+           "local_link_connection": {},
+           "node_uuid": "ddd06a60-b91e-4ab4-a6e7-56c0b25b6086",
+           "uuid": "3abe3f36-9708-4e9f-b07e-0f898061d3a7",
+           "links": [
+               {
+                   "href": "http://192.168.0.8/baremetal/v1/ports/3abe3f36-9708-4e9f-b07e-0f898061d3a7",
+                   "rel": "self"
+               },
+               {
+                   "href": "http://192.168.0.8/baremetal/ports/3abe3f36-9708-4e9f-b07e-0f898061d3a7",
+                   "rel": "bookmark"
+               }
+           ],
+           "extra": {},
+           "pxe_enabled": true,
+           "portgroup_uuid": null,
+           "updated_at": "2019-02-15T09:55:19+00:00",
+           "physical_network": null,
+           "address": "52:54:00:0a:af:d1",
+           "internal_info": {
+
+           },
+           "created_at": "2019-02-15T09:52:23+00:00"
+       },
+       {
+           "local_link_connection": {},
+           "node_uuid": "ddd06a60-b91e-4ab4-a6e7-56c0b25b6086",
+           "uuid": "f2845e11-dbd4-4728-a8c0-30d19f48924a",
+           "links": [
+               {
+                   "href": "http://192.168.0.8/baremetal/v1/ports/f2845e11-dbd4-4728-a8c0-30d19f48924a",
+                   "rel": "self"
+               },
+               {
+                   "href": "http://192.168.0.8/baremetal/ports/f2845e11-dbd4-4728-a8c0-30d19f48924a",
+                   "rel": "bookmark"
+               }
+           ],
+           "extra": {},
+           "pxe_enabled": true,
+           "portgroup_uuid": null,
+           "updated_at": "2019-02-15T09:55:19+00:00",
+           "physical_network": null,
+           "address": "52:54:00:4d:87:e6",
+           "internal_info": {},
+           "created_at": "2019-02-15T09:52:24+00:00"
+       }
+   ]
+}
+`
+
+// SinglePortBody is the canned body of a Get request on an existing port.
+const SinglePortBody = `
+{
+    "local_link_connection": {
+
+    },
+    "node_uuid": "ddd06a60-b91e-4ab4-a6e7-56c0b25b6086",
+    "uuid": "f2845e11-dbd4-4728-a8c0-30d19f48924a",
+    "links": [
+        {
+            "href": "http://192.168.0.8/baremetal/v1/ports/f2845e11-dbd4-4728-a8c0-30d19f48924a",
+            "rel": "self"
+        },
+        {
+            "href": "http://192.168.0.8/baremetal/ports/f2845e11-dbd4-4728-a8c0-30d19f48924a",
+            "rel": "bookmark"
+        }
+    ],
+    "extra": {
+
+    },
+    "pxe_enabled": true,
+    "portgroup_uuid": null,
+    "updated_at": "2019-02-15T09:55:19+00:00",
+    "physical_network": null,
+    "address": "52:54:00:4d:87:e6",
+    "internal_info": {
+
+    },
+    "created_at": "2019-02-15T09:52:24+00:00"
+}
+`
+
+var (
+	fooCreated, _ = time.Parse(time.RFC3339, "2019-02-15T09:52:24+00:00")
+	fooUpdated, _ = time.Parse(time.RFC3339, "2019-02-15T09:55:19+00:00")
+	BarCreated, _ = time.Parse(time.RFC3339, "2019-02-15T09:52:23+00:00")
+	BarUpdated, _ = time.Parse(time.RFC3339, "2019-02-15T09:55:19+00:00")
+	PortFoo       = ports.Port{
+		UUID:                "f2845e11-dbd4-4728-a8c0-30d19f48924a",
+		NodeUUID:            "ddd06a60-b91e-4ab4-a6e7-56c0b25b6086",
+		Address:             "52:54:00:4d:87:e6",
+		PXEEnabled:          true,
+		LocalLinkConnection: map[string]interface{}{},
+		InternalInfo:        map[string]interface{}{},
+		Extra:               map[string]interface{}{},
+		CreatedAt:           fooCreated,
+		UpdatedAt:           fooUpdated,
+		Links:               []interface{}{map[string]interface{}{"href": "http://192.168.0.8/baremetal/v1/ports/f2845e11-dbd4-4728-a8c0-30d19f48924a", "rel": "self"}, map[string]interface{}{"href": "http://192.168.0.8/baremetal/ports/f2845e11-dbd4-4728-a8c0-30d19f48924a", "rel": "bookmark"}},
+	}
+
+	PortBar = ports.Port{
+		UUID:                "3abe3f36-9708-4e9f-b07e-0f898061d3a7",
+		NodeUUID:            "ddd06a60-b91e-4ab4-a6e7-56c0b25b6086",
+		Address:             "52:54:00:0a:af:d1",
+		PXEEnabled:          true,
+		LocalLinkConnection: map[string]interface{}{},
+		InternalInfo:        map[string]interface{}{},
+		Extra:               map[string]interface{}{},
+		CreatedAt:           BarCreated,
+		UpdatedAt:           BarUpdated,
+		Links:               []interface{}{map[string]interface{}{"href": "http://192.168.0.8/baremetal/v1/ports/3abe3f36-9708-4e9f-b07e-0f898061d3a7", "rel": "self"}, map[string]interface{}{"rel": "bookmark", "href": "http://192.168.0.8/baremetal/ports/3abe3f36-9708-4e9f-b07e-0f898061d3a7"}},
+	}
+)
+
+// HandlePortListSuccessfully sets up the test server to respond to a port List request.
+func HandlePortListSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/ports", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		w.Header().Add("Content-Type", "application/json")
+		r.ParseForm()
+
+		marker := r.Form.Get("marker")
+		switch marker {
+		case "":
+			fmt.Fprintf(w, PortListBody)
+
+		case "f2845e11-dbd4-4728-a8c0-30d19f48924a":
+			fmt.Fprintf(w, `{ "ports": [] }`)
+		default:
+			t.Fatalf("/ports invoked with unexpected marker=[%s]", marker)
+		}
+	})
+}
+
+// HandlePortListSuccessfully sets up the test server to respond to a port List request.
+func HandlePortListDetailSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/ports/detail", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		w.Header().Add("Content-Type", "application/json")
+		r.ParseForm()
+
+		fmt.Fprintf(w, PortListDetailBody)
+	})
+}
+
+// HandleSPortCreationSuccessfully sets up the test server to respond to a port creation request
+// with a given response.
+func HandlePortCreationSuccessfully(t *testing.T, response string) {
+	th.Mux.HandleFunc("/ports", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t, r, `{
+          "node_uuid": "ddd06a60-b91e-4ab4-a6e7-56c0b25b6086",
+          "address": "52:54:00:4d:87:e6",
+          "pxe_enabled": true
+        }`)
+
+		w.WriteHeader(http.StatusAccepted)
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, response)
+	})
+}
+
+// HandlePortDeletionSuccessfully sets up the test server to respond to a port deletion request.
+func HandlePortDeletionSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/ports/3abe3f36-9708-4e9f-b07e-0f898061d3a7", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
+
+func HandlePortGetSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/ports/f2845e11-dbd4-4728-a8c0-30d19f48924a", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+
+		fmt.Fprintf(w, SinglePortBody)
+	})
+}
+
+func HandlePortUpdateSuccessfully(t *testing.T, response string) {
+	th.Mux.HandleFunc("/ports/f2845e11-dbd4-4728-a8c0-30d19f48924a", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PATCH")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestJSONRequest(t, r, `[{"op": "replace", "path": "/address", "value": "22:22:22:22:22:22"}]`)
+
+		fmt.Fprintf(w, response)
+	})
+}
diff --git a/openstack/baremetal/v1/ports/testing/requests_test.go b/openstack/baremetal/v1/ports/testing/requests_test.go
new file mode 100644
index 0000000..db1f133
--- /dev/null
+++ b/openstack/baremetal/v1/ports/testing/requests_test.go
@@ -0,0 +1,144 @@
+package testing
+
+import (
+	"testing"
+
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git/openstack/baremetal/v1/ports"
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git/pagination"
+	th "gerrit.mcp.mirantis.net/debian/gophercloud.git/testhelper"
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git/testhelper/client"
+)
+
+func TestListDetailPorts(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandlePortListDetailSuccessfully(t)
+
+	pages := 0
+	err := ports.ListDetail(client.ServiceClient(), ports.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := ports.ExtractPorts(page)
+		if err != nil {
+			return false, err
+		}
+
+		if len(actual) != 2 {
+			t.Fatalf("Expected 2 ports, got %d", len(actual))
+		}
+		th.CheckDeepEquals(t, PortBar, actual[0])
+		th.CheckDeepEquals(t, PortFoo, actual[1])
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+
+	if pages != 1 {
+		t.Errorf("Expected 1 page, saw %d", pages)
+	}
+}
+
+func TestListPorts(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandlePortListSuccessfully(t)
+
+	pages := 0
+	err := ports.List(client.ServiceClient(), ports.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := ports.ExtractPorts(page)
+		if err != nil {
+			return false, err
+		}
+
+		if len(actual) != 2 {
+			t.Fatalf("Expected 2 ports, got %d", len(actual))
+		}
+		th.AssertEquals(t, "3abe3f36-9708-4e9f-b07e-0f898061d3a7", actual[0].UUID)
+		th.AssertEquals(t, "f2845e11-dbd4-4728-a8c0-30d19f48924a", actual[1].UUID)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+
+	if pages != 1 {
+		t.Errorf("Expected 1 page, saw %d", pages)
+	}
+}
+
+func TestListOpts(t *testing.T) {
+	// Detail cannot take Fields
+	opts := ports.ListOpts{
+		Fields: []string{"uuid", "address"},
+	}
+
+	_, err := opts.ToPortListDetailQuery()
+	th.AssertEquals(t, err.Error(), "fields is not a valid option when getting a detailed listing of ports")
+
+	// Regular ListOpts can
+	query, err := opts.ToPortListQuery()
+	th.AssertEquals(t, query, "?fields=uuid&fields=address")
+	th.AssertNoErr(t, err)
+}
+
+func TestCreatePort(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandlePortCreationSuccessfully(t, SinglePortBody)
+
+	iTrue := true
+	actual, err := ports.Create(client.ServiceClient(), ports.CreateOpts{
+		NodeUUID:   "ddd06a60-b91e-4ab4-a6e7-56c0b25b6086",
+		Address:    "52:54:00:4d:87:e6",
+		PXEEnabled: &iTrue,
+	}).Extract()
+	th.AssertNoErr(t, err)
+
+	th.CheckDeepEquals(t, PortFoo, *actual)
+}
+
+func TestDeletePort(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandlePortDeletionSuccessfully(t)
+
+	res := ports.Delete(client.ServiceClient(), "3abe3f36-9708-4e9f-b07e-0f898061d3a7")
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestGetPort(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandlePortGetSuccessfully(t)
+
+	c := client.ServiceClient()
+	actual, err := ports.Get(c, "f2845e11-dbd4-4728-a8c0-30d19f48924a").Extract()
+	if err != nil {
+		t.Fatalf("Unexpected Get error: %v", err)
+	}
+
+	th.CheckDeepEquals(t, PortFoo, *actual)
+}
+
+func TestUpdatePort(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandlePortUpdateSuccessfully(t, SinglePortBody)
+
+	c := client.ServiceClient()
+	actual, err := ports.Update(c, "f2845e11-dbd4-4728-a8c0-30d19f48924a", ports.UpdateOpts{
+		ports.UpdateOperation{
+			Op:    ports.ReplaceOp,
+			Path:  "/address",
+			Value: "22:22:22:22:22:22",
+		},
+	}).Extract()
+	if err != nil {
+		t.Fatalf("Unexpected Update error: %v", err)
+	}
+
+	th.CheckDeepEquals(t, PortFoo, *actual)
+}
diff --git a/openstack/baremetal/v1/ports/urls.go b/openstack/baremetal/v1/ports/urls.go
new file mode 100644
index 0000000..80b86b3
--- /dev/null
+++ b/openstack/baremetal/v1/ports/urls.go
@@ -0,0 +1,31 @@
+package ports
+
+import "gerrit.mcp.mirantis.net/debian/gophercloud.git"
+
+func createURL(client *gophercloud.ServiceClient) string {
+	return client.ServiceURL("ports")
+}
+
+func listURL(client *gophercloud.ServiceClient) string {
+	return createURL(client)
+}
+
+func listDetailURL(client *gophercloud.ServiceClient) string {
+	return client.ServiceURL("ports", "detail")
+}
+
+func resourceURL(client *gophercloud.ServiceClient, id string) string {
+	return client.ServiceURL("ports", id)
+}
+
+func deleteURL(client *gophercloud.ServiceClient, id string) string {
+	return resourceURL(client, id)
+}
+
+func getURL(client *gophercloud.ServiceClient, id string) string {
+	return resourceURL(client, id)
+}
+
+func updateURL(client *gophercloud.ServiceClient, id string) string {
+	return resourceURL(client, id)
+}