Repository: gophercloud/gophercloud

Issue:
compute/v2/service/Update doesnt allow to set `forced_down` to false due to "omitempty"
### What happened?

The `UpdateOpts` have the field:
```
   ForcedDown bool `json:"forced_down,omitempty"`
```
which mean, if it is `false` it will be dropped from the body, which in turn means one cannot unset a `ForceDown` one has set before.



### Version

v2 (current)

Discussion:

Solution patch:
diff --git a/openstack/compute/v2/services/requests.go b/openstack/compute/v2/services/requests.go
index 32be17cb7..6fce69eb1 100644
--- a/openstack/compute/v2/services/requests.go
+++ b/openstack/compute/v2/services/requests.go
@@ -67,7 +67,7 @@ type UpdateOpts struct {
 
 	// ForcedDown is a manual override to tell nova that the service in question
 	// has been fenced manually by the operations team.
-	ForcedDown bool `json:"forced_down,omitempty"`
+	ForcedDown *bool `json:"forced_down,omitempty"`
 }
 
 // ToServiceUpdateMap formats an UpdateOpts structure into a request body.



Test patch:
diff --git a/openstack/compute/v2/services/testing/fixtures_test.go b/openstack/compute/v2/services/testing/fixtures_test.go
index 47e549e1b..2a17d7d12 100644
--- a/openstack/compute/v2/services/testing/fixtures_test.go
+++ b/openstack/compute/v2/services/testing/fixtures_test.go
@@ -238,6 +238,23 @@ const ServiceUpdate = `
 }
 `
 
+const ServiceUpdateForceDown = `
+{
+	"service":
+	{
+		"id": 1,
+		"binary": "nova-scheduler",
+		"disabled_reason": "test1",
+		"host": "host1",
+		"state": "up",
+		"status": "disabled",
+		"updated_at": "2012-10-29T13:42:02.000000",
+		"forced_down": true,
+		"zone": "internal"
+	}
+}
+`
+
 // FakeServiceUpdateBody represents the updated service
 var FakeServiceUpdateBody = services.Service{
 	Binary:         "nova-scheduler",
@@ -251,6 +268,18 @@ var FakeServiceUpdateBody = services.Service{
 	Zone:           "internal",
 }
 
+var FakeServiceUpdateForceDownBody = services.Service{
+	Binary:         "nova-scheduler",
+	DisabledReason: "test1",
+	ForcedDown:     true,
+	Host:           "host1",
+	ID:             "1",
+	State:          "up",
+	Status:         "disabled",
+	UpdatedAt:      time.Date(2012, 10, 29, 13, 42, 2, 0, time.UTC),
+	Zone:           "internal",
+}
+
 // HandleListPre253Successfully configures the test server to respond to a List
 // request to a Compute server API pre 2.53 microversion release.
 func HandleListPre253Successfully(t *testing.T, fakeServer th.FakeServer) {
@@ -289,6 +318,34 @@ func HandleUpdateSuccessfully(t *testing.T, fakeServer th.FakeServer) {
 	})
 }
 
+// HandleForceDownSuccessfully configures the test server to respond to a Update
+// request to a Compute server with Pike+ release.
+func HandleForceDownSuccessfully(t *testing.T, fakeServer th.FakeServer) {
+	fakeServer.Mux.HandleFunc("/os-services/fake-service-id", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		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, `{"forced_down": true}`)
+
+		fmt.Fprint(w, ServiceUpdateForceDown)
+	})
+}
+
+// HandleDisableForceDownSuccessfully configures the test server to respond to a Update
+// request to a Compute server with Pike+ release.
+func HandleDisableForceDownSuccessfully(t *testing.T, fakeServer th.FakeServer) {
+	fakeServer.Mux.HandleFunc("/os-services/fake-service-id", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		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, `{"forced_down": false}`)
+
+		fmt.Fprint(w, ServiceUpdate)
+	})
+}
+
 // HandleDeleteSuccessfully configures the test server to respond to a Delete
 // request to a Compute server with Pike+ release.
 func HandleDeleteSuccessfully(t *testing.T, fakeServer th.FakeServer) {
diff --git a/openstack/compute/v2/services/testing/requests_test.go b/openstack/compute/v2/services/testing/requests_test.go
index 3fac76abf..d92d7d209 100644
--- a/openstack/compute/v2/services/testing/requests_test.go
+++ b/openstack/compute/v2/services/testing/requests_test.go
@@ -92,6 +92,36 @@ func TestUpdateService(t *testing.T) {
 	th.CheckDeepEquals(t, FakeServiceUpdateBody, *actual)
 }
 
+func TestUpdateServiceForceDown(t *testing.T) {
+	fakeServer := th.SetupHTTP()
+	defer fakeServer.Teardown()
+	HandleForceDownSuccessfully(t, fakeServer)
+
+	client := client.ServiceClient(fakeServer)
+	trueVal := true
+	actual, err := services.Update(context.TODO(), client, "fake-service-id", services.UpdateOpts{ForcedDown: &trueVal}).Extract()
+	if err != nil {
+		t.Fatalf("Unexpected Update error: %v", err)
+	}
+
+	th.CheckDeepEquals(t, FakeServiceUpdateForceDownBody, *actual)
+}
+
+func TestUpdateServiceDisableForceDown(t *testing.T) {
+	fakeServer := th.SetupHTTP()
+	defer fakeServer.Teardown()
+	HandleDisableForceDownSuccessfully(t, fakeServer)
+
+	client := client.ServiceClient(fakeServer)
+	falseVal := false
+	actual, err := services.Update(context.TODO(), client, "fake-service-id", services.UpdateOpts{ForcedDown: &falseVal}).Extract()
+	if err != nil {
+		t.Fatalf("Unexpected Update error: %v", err)
+	}
+
+	th.CheckDeepEquals(t, FakeServiceUpdateBody, *actual)
+}
+
 func TestDeleteService(t *testing.T) {
 	fakeServer := th.SetupHTTP()
 	defer fakeServer.Teardown()