Repository: gophercloud/gophercloud

Issue:
neutron: add an ability to update port mac_address
### What is missing?

As [api doc](https://docs.openstack.org/api-ref/network/v2/index.html#ports) described, openstack support update port mac_address, so maybe we can support this operation

Hints from issue comments:
@winiciusallan: This will be useful in this [issue](https://github.com/terraform-provider-openstack/terraform-provider-openstack/issues/1942) from Terraform Openstack provider.

Just linking =)

Solution patch:
diff --git a/.github/workflows/backport.yaml b/.github/workflows/backport.yaml
index 86504701..04f06538 100644
--- a/.github/workflows/backport.yaml
+++ b/.github/workflows/backport.yaml
@@ -34,7 +34,7 @@ jobs:
         if: >
           contains(github.event.pull_request.labels.*.name, 'semver:patch')
           || contains(github.event.label.name, 'semver:patch')
-        uses: kiegroup/git-backporting@7d895d030f5cf02f4a76c7f0bc79b41d8747b17c
+        uses: kiegroup/git-backporting@baae3fe1e3c71bc6b1a2699b3bc1e153a19d5ac7
         with:
           target-branch: v1
           pull-request: ${{ github.event.pull_request.url }}
@@ -97,7 +97,7 @@ jobs:
           || contains(github.event.pull_request.labels.*.name, 'semver:minor')
           || contains(github.event.label.name, 'semver:patch')
           || contains(github.event.label.name, 'semver:minor')
-        uses: kiegroup/git-backporting@7d895d030f5cf02f4a76c7f0bc79b41d8747b17c
+        uses: kiegroup/git-backporting@baae3fe1e3c71bc6b1a2699b3bc1e153a19d5ac7
         with:
           target-branch: v2
           pull-request: ${{ github.event.pull_request.url }}
diff --git a/openstack/identity/v3/domains/requests.go b/openstack/identity/v3/domains/requests.go
index 8ce72a9b..94bb567f 100644
--- a/openstack/identity/v3/domains/requests.go
+++ b/openstack/identity/v3/domains/requests.go
@@ -20,6 +20,9 @@ type ListOpts struct {
 
 	// Name filters the response by domain name.
 	Name string `q:"name"`
+
+	// Limit limits the number of projects returned per page.
+	Limit int `q:"limit"`
 }
 
 // ToDomainListQuery formats a ListOpts into a query string.
diff --git a/openstack/identity/v3/projects/requests.go b/openstack/identity/v3/projects/requests.go
index 6fb8448a..5aecc94d 100644
--- a/openstack/identity/v3/projects/requests.go
+++ b/openstack/identity/v3/projects/requests.go
@@ -45,6 +45,9 @@ type ListOpts struct {
 	// NotTagsAny filters on specific project tags. At least one of the tags must be absent for the project.
 	NotTagsAny string `q:"not-tags-any"`
 
+	// Limit limits the number of projects returned per page.
+	Limit int `q:"limit"`
+
 	// Filters filters the response by custom filters such as
 	// 'name__contains=foo'
 	Filters map[string]string `q:"-"`
diff --git a/openstack/networking/v2/ports/requests.go b/openstack/networking/v2/ports/requests.go
index bfff2dff..a9c5c375 100644
--- a/openstack/networking/v2/ports/requests.go
+++ b/openstack/networking/v2/ports/requests.go
@@ -197,6 +197,7 @@ type UpdateOpts struct {
 	AllowedAddressPairs   *[]AddressPair     `json:"allowed_address_pairs,omitempty"`
 	PropagateUplinkStatus *bool              `json:"propagate_uplink_status,omitempty"`
 	ValueSpecs            *map[string]string `json:"value_specs,omitempty"`
+	MACAddress            *string            `json:"mac_address,omitempty"`
 
 	// RevisionNumber implements extension:standard-attr-revisions. If != "" it
 	// will set revision_number=%s. If the revision number does not match, the



Test patch:
diff --git a/internal/acceptance/openstack/networking/v2/ports_test.go b/internal/acceptance/openstack/networking/v2/ports_test.go
index 569beeeb..f6ff48e8 100644
--- a/internal/acceptance/openstack/networking/v2/ports_test.go
+++ b/internal/acceptance/openstack/networking/v2/ports_test.go
@@ -45,9 +45,11 @@ func TestPortsCRUD(t *testing.T) {
 	// Update port
 	newPortName := ""
 	newPortDescription := ""
+	newMACAddress := "aa:bb:cc:dd:ee:ff"
 	updateOpts := ports.UpdateOpts{
 		Name:        &newPortName,
 		Description: &newPortDescription,
+		MACAddress:  &newMACAddress,
 	}
 	newPort, err := ports.Update(context.TODO(), client, port.ID, updateOpts).Extract()
 	th.AssertNoErr(t, err)
@@ -56,6 +58,7 @@ func TestPortsCRUD(t *testing.T) {
 
 	th.AssertEquals(t, newPort.Name, newPortName)
 	th.AssertEquals(t, newPort.Description, newPortDescription)
+	th.AssertEquals(t, newPort.MACAddress, newMACAddress)
 
 	allPages, err := ports.List(client, nil).AllPages(context.TODO())
 	th.AssertNoErr(t, err)
diff --git a/openstack/identity/v3/projects/testing/fixtures_test.go b/openstack/identity/v3/projects/testing/fixtures_test.go
index 5c97fc7d..5782fde8 100644
--- a/openstack/identity/v3/projects/testing/fixtures_test.go
+++ b/openstack/identity/v3/projects/testing/fixtures_test.go
@@ -59,7 +59,19 @@ const ListOutput = `
       "parent_id": null,
       "tags": ["Red", "Team"],
       "test": "old"
-    },
+    }
+  ],
+  "links": {
+    "next": "%sprojects?limit=1&marker=1234",
+    "self": "%sprojects?limit=1",
+    "previous": null
+  }
+}
+`
+
+const ListOutputSecondPage = `
+{
+  "projects": [
     {
       "is_domain": false,
       "description": "The team that is blue",
@@ -73,8 +85,21 @@ const ListOutput = `
       }
     }
   ],
+  "links": {
+    "next": "%sprojects?limit=1&marker=9876",
+    "self": "%sprojects?limit=1&marker=1234",
+    "previous": null
+  }
+}
+`
+
+const ListOutputThirdPage = `
+{
+  "projects": [
+  ],
   "links": {
     "next": null,
+    "self": "%sprojects?limit=1&marker=9876",
     "previous": null
   }
 }
@@ -299,7 +324,15 @@ func HandleListProjectsSuccessfully(t *testing.T, fakeServer th.FakeServer) {
 
 		w.Header().Set("Content-Type", "application/json")
 		w.WriteHeader(http.StatusOK)
-		fmt.Fprint(w, ListOutput)
+
+		switch r.URL.Query().Get("marker") {
+		case "":
+			fmt.Fprintf(w, ListOutput, fakeServer.Endpoint(), fakeServer.Endpoint())
+		case "1234":
+			fmt.Fprintf(w, ListOutputSecondPage, fakeServer.Endpoint(), fakeServer.Endpoint())
+		case "9876":
+			fmt.Fprintf(w, ListOutputThirdPage, fakeServer.Endpoint())
+		}
 	})
 }
 
diff --git a/openstack/identity/v3/projects/testing/requests_test.go b/openstack/identity/v3/projects/testing/requests_test.go
index 5ecc88e4..ca5bebe4 100644
--- a/openstack/identity/v3/projects/testing/requests_test.go
+++ b/openstack/identity/v3/projects/testing/requests_test.go
@@ -35,19 +35,26 @@ func TestListProjects(t *testing.T) {
 	defer fakeServer.Teardown()
 	HandleListProjectsSuccessfully(t, fakeServer)
 
+	actualProjects := make([]projects.Project, 0, 2)
 	count := 0
-	err := projects.List(client.ServiceClient(fakeServer), nil).EachPage(context.TODO(), func(_ context.Context, page pagination.Page) (bool, error) {
+	opts := projects.ListOpts{
+		Limit: 1,
+	}
+	err := projects.List(client.ServiceClient(fakeServer), opts).EachPage(context.TODO(), func(_ context.Context, page pagination.Page) (bool, error) {
 		count++
 
 		actual, err := projects.ExtractProjects(page)
 		th.AssertNoErr(t, err)
 
-		th.CheckDeepEquals(t, ExpectedProjectSlice, actual)
+		actualProjects = append(actualProjects, actual...)
 
 		return true, nil
 	})
 	th.AssertNoErr(t, err)
-	th.CheckEquals(t, count, 1)
+	th.CheckEquals(t, count, 2)
+
+	th.CheckEquals(t, len(actualProjects), 2)
+	th.CheckDeepEquals(t, ExpectedProjectSlice, actualProjects)
 }
 
 func TestListGroupsFiltersCheck(t *testing.T) {
diff --git a/openstack/networking/v2/ports/testing/fixtures.go b/openstack/networking/v2/ports/testing/fixtures.go
index b4c03fd6..b1165086 100644
--- a/openstack/networking/v2/ports/testing/fixtures.go
+++ b/openstack/networking/v2/ports/testing/fixtures.go
@@ -409,7 +409,8 @@ const UpdateRequest = `
         ],
         "security_groups": [
             "f0ac4394-7e4a-4409-9701-ba8be283dbc3"
-        ]
+        ],
+        "mac_address": "fa:16:3e:c9:cb:f4"
     }
 }
 `
@@ -423,7 +424,7 @@ const UpdateResponse = `
         "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7",
         "tenant_id": "d6700c0c9ffa4f1cb322cd4a1f3906fa",
         "device_owner": "",
-        "mac_address": "fa:16:3e:c9:cb:f0",
+        "mac_address": "fa:16:3e:c9:cb:f4",
         "fixed_ips": [
             {
                 "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2",
diff --git a/openstack/networking/v2/ports/testing/requests_test.go b/openstack/networking/v2/ports/testing/requests_test.go
index ac1a2cd7..f69962f7 100644
--- a/openstack/networking/v2/ports/testing/requests_test.go
+++ b/openstack/networking/v2/ports/testing/requests_test.go
@@ -538,6 +538,7 @@ func TestUpdate(t *testing.T) {
 	})
 
 	name := "new_port_name"
+	newMACAddress := "fa:16:3e:c9:cb:f4"
 	options := ports.UpdateOpts{
 		Name: &name,
 		FixedIPs: []ports.IP{
@@ -547,6 +548,7 @@ func TestUpdate(t *testing.T) {
 		AllowedAddressPairs: &[]ports.AddressPair{
 			{IPAddress: "10.0.0.4", MACAddress: "fa:16:3e:c9:cb:f0"},
 		},
+		MACAddress: &newMACAddress,
 	}
 
 	s, err := ports.Update(context.TODO(), fake.ServiceClient(fakeServer), "65c0ee9f-d634-4522-8954-51021b570b0d", options).Extract()
@@ -727,6 +729,7 @@ func TestUpdateRevision(t *testing.T) {
 	})
 
 	name := "new_port_name"
+	newMACAddress := "fa:16:3e:c9:cb:f4"
 	options := ports.UpdateOpts{
 		Name: &name,
 		FixedIPs: []ports.IP{
@@ -736,6 +739,7 @@ func TestUpdateRevision(t *testing.T) {
 		AllowedAddressPairs: &[]ports.AddressPair{
 			{IPAddress: "10.0.0.4", MACAddress: "fa:16:3e:c9:cb:f0"},
 		},
+		MACAddress: &newMACAddress,
 	}
 	_, err := ports.Update(context.TODO(), fake.ServiceClient(fakeServer), "65c0ee9f-d634-4522-8954-51021b570b0d", options).Extract()
 	th.AssertNoErr(t, err)