Repository: vmware/govmomi
Pull request #2721 (2022-01-27 14:35:33)

Add Appliance shutdown API
Closes: #2672

## Description

This PR adds Appliance shutdown API's mentioned https://developer.vmware.com/apis/vsphere-automation/latest/appliance/appliance/shutdown/

Closes: #2672 

## Type of change

Please mark options that are relevant:

- [ ] Bug fix (non-breaking change which fixes an issue)
- [X] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to
  not work as expected)
- [ ] This change requires a documentation update
- [ ] Build related change

## How Has This Been Tested?

- Tested Manually on live vCenter Server
- Added the govc-test

## Checklist:

- [X] My code follows the CONTRIBUTION [guidelines](./../CONTRIBUTING.md) of
  this project
- [X] I have commented my code, particularly in hard-to-understand areas
- [X] I have made corresponding changes to the documentation
- [X] I have added tests that prove my fix is effective or that my feature works
- [X] New and existing unit tests pass locally with my changes
- [X] Any dependent changes have been merged


Patch:
diff --git a/govc/USAGE.md b/govc/USAGE.md
index 984fd481..711495a1 100644
--- a/govc/USAGE.md
+++ b/govc/USAGE.md
@@ -300,6 +300,10 @@ but appear via `govc $cmd -h`:
  - [vapp.power](#vapppower)
  - [vcsa.log.forwarding.info](#vcsalogforwardinginfo)
  - [vcsa.net.proxy.info](#vcsanetproxyinfo)
+ - [vcsa.shutdown.cancel](#vcsashutdowncancel)
+ - [vcsa.shutdown.get](#vcsashutdownget)
+ - [vcsa.shutdown.poweroff](#vcsashutdownpoweroff)
+ - [vcsa.shutdown.reboot](#vcsashutdownreboot)
  - [version](#version)
  - [vm.change](#vmchange)
  - [vm.clone](#vmclone)
@@ -5066,6 +5070,56 @@ Examples:
 Options:
 ```
 
+## vcsa.shutdown.cancel
+
+```
+Usage: govc vcsa.shutdown.cancel [OPTIONS]
+
+Cancel pending shutdown action.
+Examples:
+  govc vcsa.shutdown.cancel
+
+Options:
+```
+
+## vcsa.shutdown.get
+
+```
+Usage: govc vcsa.shutdown.get [OPTIONS]
+
+Get details about the pending shutdown action.
+Examples:
+  govc vcsa.shutdown.get
+
+Options:
+```
+
+## vcsa.shutdown.poweroff
+
+```
+Usage: govc vcsa.shutdown.poweroff [OPTIONS] REASON
+
+Power off the appliance.
+Examples:
+  govc vcsa.shutdown.poweroff -delay 10 "powering off for maintenance"
+
+Options:
+  -delay=0               Minutes after which poweroff should start.
+```
+
+## vcsa.shutdown.reboot
+
+```
+Usage: govc vcsa.shutdown.reboot [OPTIONS] REASON
+
+Reboot the appliance.
+Examples:
+  govc vcsa.shutdown.reboot -delay 10 "rebooting for maintenance"
+
+Options:
+  -delay=0               Minutes after which reboot should start.
+```
+
 ## version
 
 ```
diff --git a/govc/main.go b/govc/main.go
index 3a9e0436..5c6768a8 100644
--- a/govc/main.go
+++ b/govc/main.go
@@ -90,6 +90,7 @@ import (
 	_ "github.com/vmware/govmomi/govc/vapp"
 	_ "github.com/vmware/govmomi/govc/vcsa/log"
 	_ "github.com/vmware/govmomi/govc/vcsa/proxy"
+	_ "github.com/vmware/govmomi/govc/vcsa/shutdown"
 	_ "github.com/vmware/govmomi/govc/version"
 	_ "github.com/vmware/govmomi/govc/vm"
 	_ "github.com/vmware/govmomi/govc/vm/disk"
diff --git a/govc/vcsa/shutdown/cancel.go b/govc/vcsa/shutdown/cancel.go
new file mode 100644
index 00000000..cb334153
--- /dev/null
+++ b/govc/vcsa/shutdown/cancel.go
@@ -0,0 +1,60 @@
+/*
+Copyright (c) 2022 VMware, Inc. All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package shutdown
+
+import (
+	"context"
+	"flag"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/vapi/appliance/shutdown"
+)
+
+type cancel struct {
+	*flags.ClientFlag
+}
+
+func init() {
+	cli.Register("vcsa.shutdown.cancel", &cancel{})
+}
+
+func (cmd *cancel) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.ClientFlag, ctx = flags.NewClientFlag(ctx)
+	cmd.ClientFlag.Register(ctx, f)
+}
+
+func (cmd *cancel) Description() string {
+	return `Cancel pending shutdown action.
+Examples:
+  govc vcsa.shutdown.cancel`
+}
+
+func (cmd *cancel) Run(ctx context.Context, f *flag.FlagSet) error {
+	c, err := cmd.RestClient()
+	if err != nil {
+		return err
+	}
+
+	m := shutdown.NewManager(c)
+
+	if err = m.Cancel(ctx); err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/govc/vcsa/shutdown/get.go b/govc/vcsa/shutdown/get.go
new file mode 100644
index 00000000..ba71ea6f
--- /dev/null
+++ b/govc/vcsa/shutdown/get.go
@@ -0,0 +1,93 @@
+/*
+Copyright (c) 2022 VMware, Inc. All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package shutdown
+
+import (
+	"context"
+	"flag"
+	"fmt"
+	"io"
+	"text/tabwriter"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/vapi/appliance/shutdown"
+)
+
+type get struct {
+	*flags.ClientFlag
+	*flags.OutputFlag
+}
+
+func init() {
+	cli.Register("vcsa.shutdown.get", &get{})
+}
+
+func (cmd *get) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.ClientFlag, ctx = flags.NewClientFlag(ctx)
+	cmd.ClientFlag.Register(ctx, f)
+
+	cmd.OutputFlag, ctx = flags.NewOutputFlag(ctx)
+	cmd.OutputFlag.Register(ctx, f)
+}
+
+func (cmd *get) Process(ctx context.Context) error {
+	if err := cmd.ClientFlag.Process(ctx); err != nil {
+		return err
+	}
+	if err := cmd.OutputFlag.Process(ctx); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (cmd *get) Description() string {
+	return `Get details about the pending shutdown action.
+Examples:
+  govc vcsa.shutdown.get`
+}
+
+type shutdownStatus struct {
+	Values shutdown.Config `json:"values"`
+}
+
+func (cmd *get) Run(ctx context.Context, f *flag.FlagSet) error {
+	c, err := cmd.RestClient()
+	if err != nil {
+		return err
+	}
+
+	s := shutdown.NewManager(c)
+
+	r, err := s.Get(ctx)
+	if err != nil {
+		return err
+	}
+
+	return cmd.WriteResult(shutdownStatus{
+		Values: r,
+	})
+}
+
+func (res shutdownStatus) Write(w io.Writer) error {
+	tw := tabwriter.NewWriter(w, 2, 0, 2, ' ', 0)
+	fmt.Fprintf(tw, "Action:%s\n", res.Values.Action)
+	fmt.Fprintf(tw, "Reason:%s\n", res.Values.Reason)
+	fmt.Fprintf(tw, "ShutDown Time:%s\n", res.Values.ShutdownTime)
+
+	return tw.Flush()
+}
diff --git a/govc/vcsa/shutdown/poweroff.go b/govc/vcsa/shutdown/poweroff.go
new file mode 100644
index 00000000..7d942f31
--- /dev/null
+++ b/govc/vcsa/shutdown/poweroff.go
@@ -0,0 +1,76 @@
+/*
+Copyright (c) 2022 VMware, Inc. All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package shutdown
+
+import (
+	"context"
+	"flag"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/vapi/appliance/shutdown"
+)
+
+type powerOff struct {
+	*flags.ClientFlag
+
+	reason string
+	delay  int // in minutes
+}
+
+func init() {
+	cli.Register("vcsa.shutdown.poweroff", &powerOff{})
+}
+
+func (cmd *powerOff) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.ClientFlag, ctx = flags.NewClientFlag(ctx)
+	cmd.ClientFlag.Register(ctx, f)
+
+	f.IntVar(&cmd.delay,
+		"delay",
+		0,
+		"Minutes after which poweroff should start.")
+}
+
+func (cmd *powerOff) Usage() string {
+	return "REASON"
+}
+
+func (cmd *powerOff) Description() string {
+	return `Power off the appliance.
+Examples:
+  govc vcsa.shutdown.poweroff -delay 10 "powering off for maintenance"`
+}
+
+func (cmd *powerOff) Run(ctx context.Context, f *flag.FlagSet) error {
+	if f.NArg() != 1 {
+		return flag.ErrHelp
+	}
+
+	c, err := cmd.RestClient()
+	if err != nil {
+		return err
+	}
+
+	m := shutdown.NewManager(c)
+
+	if err = m.PowerOff(ctx, f.Arg(0), cmd.delay); err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/govc/vcsa/shutdown/reboot.go b/govc/vcsa/shutdown/reboot.go
new file mode 100644
index 00000000..2f3bda13
--- /dev/null
+++ b/govc/vcsa/shutdown/reboot.go
@@ -0,0 +1,84 @@
+/*
+Copyright (c) 2022 VMware, Inc. All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package shutdown
+
+import (
+	"context"
+	"flag"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/vapi/appliance/shutdown"
+)
+
+type reboot struct {
+	*flags.ClientFlag
+
+	reason string
+	delay  int // in minutes
+}
+
+func init() {
+	cli.Register("vcsa.shutdown.reboot", &reboot{})
+}
+
+func (cmd *reboot) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.ClientFlag, ctx = flags.NewClientFlag(ctx)
+	cmd.ClientFlag.Register(ctx, f)
+
+	var now int
+	f.IntVar(&cmd.delay,
+		"delay",
+		now,
+		"Minutes after which reboot should start.")
+}
+
+func (cmd *reboot) Process(ctx context.Context) error {
+	if err := cmd.ClientFlag.Process(ctx); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (cmd *reboot) Usage() string {
+	return "REASON"
+}
+
+func (cmd *reboot) Description() string {
+	return `Reboot the appliance.
+Examples:
+  govc vcsa.shutdown.reboot -delay 10 "rebooting for maintenance"`
+}
+
+func (cmd *reboot) Run(ctx context.Context, f *flag.FlagSet) error {
+	if f.NArg() != 1 {
+		return flag.ErrHelp
+	}
+
+	c, err := cmd.RestClient()
+	if err != nil {
+		return err
+	}
+
+	m := shutdown.NewManager(c)
+
+	if err = m.Reboot(ctx, f.Arg(0), cmd.delay); err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/vapi/appliance/shutdown/shutdown.go b/vapi/appliance/shutdown/shutdown.go
new file mode 100644
index 00000000..f924680a
--- /dev/null
+++ b/vapi/appliance/shutdown/shutdown.go
@@ -0,0 +1,95 @@
+/*
+Copyright (c) 2022 VMware, Inc. All Rights Reserved.
+
+SPDX-License-Identifier: Apache-2.0.
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package shutdown
+
+import (
+	"context"
+	"net/http"
+
+	"github.com/vmware/govmomi/vapi/rest"
+)
+
+const (
+	Path     = "/appliance/shutdown"
+	Action   = "action"
+	Cancel   = "cancel"
+	PowerOff = "poweroff"
+	Reboot   = "reboot"
+)
+
+// Manager provides convenience methods for shutdown Action
+type Manager struct {
+	*rest.Client
+}
+
+// NewManager creates a new Manager
+func NewManager(client *rest.Client) *Manager {
+	return &Manager{
+		Client: client,
+	}
+}
+
+// Cancel cancels pending shutdown action.
+func (m *Manager) Cancel(ctx context.Context) error {
+	r := m.Resource(Path).WithParam(Action, Cancel)
+
+	return m.Do(ctx, r.Request(http.MethodPost), nil)
+}
+
+// Spec represents request body class for an operation.
+type Spec struct {
+	Delay  int    `json:"delay"`
+	Reason string `json:"reason"`
+}
+
+// Config  defines shutdown configuration returned by the Shutdown.get operation
+type Config struct {
+	Action       string `json:"action"`
+	Reason       string `json:"reason"`
+	ShutdownTime string `json:"shutdown_time"`
+}
+
+// Get returns details about the pending shutdown action.
+func (m *Manager) Get(ctx context.Context) (Config, error) {
+	r := m.Resource(Path)
+
+	var c Config
+
+	return c, m.Do(ctx, r.Request(http.MethodGet), &c)
+}
+
+// PowerOff powers off the appliance.
+func (m *Manager) PowerOff(ctx context.Context, powerOffReason string, delay int) error {
+	r := m.Resource(Path).WithParam(Action, PowerOff)
+	s := Spec{
+		Delay:  delay,
+		Reason: powerOffReason,
+	}
+
+	return m.Do(ctx, r.Request(http.MethodPost, s), nil)
+}
+
+// Reboot reboots the appliance
+func (m *Manager) Reboot(ctx context.Context, rebootReason string, delay int) error {
+	r := m.Resource(Path).WithParam(Action, Reboot)
+	s := Spec{
+		Delay:  delay,
+		Reason: rebootReason,
+	}
+
+	return m.Do(ctx, r.Request(http.MethodPost, s), nil)
+}
diff --git a/vapi/simulator/simulator.go b/vapi/simulator/simulator.go
index 07e72dee..fcc7461b 100644
--- a/vapi/simulator/simulator.go
+++ b/vapi/simulator/simulator.go
@@ -1,5 +1,5 @@
 /*
-Copyright (c) 2018 VMware, Inc. All Rights Reserved.
+Copyright (c) 2018-2022 VMware, Inc. All Rights Reserved.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -43,6 +43,7 @@ import (
 	"github.com/vmware/govmomi/object"
 	"github.com/vmware/govmomi/ovf"
 	"github.com/vmware/govmomi/simulator"
+	"github.com/vmware/govmomi/vapi/appliance/shutdown"
 	"github.com/vmware/govmomi/vapi/internal"
 	"github.com/vmware/govmomi/vapi/library"
 	"github.com/vmware/govmomi/vapi/rest"
@@ -91,6 +92,7 @@ type handler struct {
 	Library     map[string]*content
 	Update      map[string]update
 	Download    map[string]download
+	Config      shutdown.Config
 }
 
 func init() {
@@ -114,6 +116,7 @@ func New(u *url.URL, settings []vim.BaseOptionValue) (string, http.Handler) {
 		Library:     make(map[string]*content),
 		Update:      make(map[string]update),
 		Download:    make(map[string]download),
+		Config:      shutdown.Config{},
 	}
 
 	handlers := []struct {
@@ -155,6 +158,7 @@ func New(u *url.URL, settings []vim.BaseOptionValue) (string, http.Handler) {
 		{internal.VCenterVMTXLibraryItem + "/", s.libraryItemTemplateID},
 		{internal.VCenterVM + "/", s.vmID},
 		{internal.DebugEcho, s.debugEcho},
+		{shutdown.Path, s.shutdown},
 	}
 
 	for i := range handlers {
@@ -2105,3 +2109,48 @@ func (s *handler) vmID(w http.ResponseWriter, r *http.Request) {
 func (s *handler) debugEcho(w http.ResponseWriter, r *http.Request) {
 	r.Write(w)
 }
+
+func (s *handler) shutdown(w http.ResponseWriter, r *http.Request) {
+	delayShutdown := func(delay int) {
+		go time.AfterFunc(time.Duration(delay)*time.Minute, func() {
+			s.Lock()
+			s.Config.ShutdownTime = ""
+			s.Config.Action = ""
+			s.Config.Reason = ""
+			s.Unlock()
+		})
+	}
+
+	switch r.Method {
+	case http.MethodGet:
+		OK(w, s.Config)
+	case http.MethodPost:
+		switch r.URL.Query().Get(shutdown.Action) {
+		case shutdown.Cancel:
+			delayShutdown(0)
+			w.WriteHeader(http.StatusNoContent)
+		case shutdown.Reboot:
+			var spec shutdown.Spec
+			if s.decode(r, w, &spec) {
+				s.Config.ShutdownTime = time.Now().UTC().Add(time.Duration(spec.Delay) * time.Minute).String()
+				s.Config.Reason = spec.Reason
+				s.Config.Action = shutdown.Reboot
+				delayShutdown(spec.Delay)
+				w.WriteHeader(http.StatusNoContent)
+			}
+		case shutdown.PowerOff:
+			var spec shutdown.Spec
+			if s.decode(r, w, &spec) {
+				s.Config.ShutdownTime = time.Now().Local().Add(time.Duration(spec.Delay) * time.Minute).String()
+				s.Config.Reason = spec.Reason
+				s.Config.Action = shutdown.PowerOff
+				delayShutdown(spec.Delay)
+				w.WriteHeader(http.StatusNoContent)
+			}
+		default:
+			http.NotFound(w, r)
+		}
+	default:
+		http.NotFound(w, r)
+	}
+}


Test patch:
diff --git a/govc/test/vcsa_shutdown.bats b/govc/test/vcsa_shutdown.bats
new file mode 100755
index 00000000..13c06894
--- /dev/null
+++ b/govc/test/vcsa_shutdown.bats
@@ -0,0 +1,82 @@
+#!/usr/bin/env bats
+
+load test_helper
+
+@test "vcsa.shutdown.poweroff" {
+  vcsim_env
+  local output
+
+  run govc vcsa.shutdown.poweroff -delay 50 "powering off for maintenance"
+  assert_success
+
+  status=$(govc vcsa.shutdown.get| grep -ow "poweroff"| awk '{print $1}')
+  assert_equal $status 'poweroff'
+   
+  run govc vcsa.shutdown.poweroff -delay 50 "powering off for maintenance"
+  assert_success
+
+  status=$(govc vcsa.shutdown.get| grep -ow "poweroff"| awk '{print $1}')
+  assert_equal $status 'poweroff'
+
+  run govc vcsa.shutdown.poweroff
+  assert_failure
+
+}
+
+@test "vcsa.shutdown.reboot" {
+  vcsim_env
+  local output
+
+  run govc vcsa.shutdown.reboot -delay 50 "rebooting for maintenance"
+  assert_success
+
+  status=$(govc vcsa.shutdown.get| grep -ow "reboot"| awk '{print $1}')
+  assert_equal $status 'reboot'
+
+  run govc vcsa.shutdown.reboot -delay 50 "rebooting for maintenance"
+  assert_success
+
+  status=$(govc vcsa.shutdown.get| grep -ow "reboot"| awk '{print $1}')
+  assert_equal $status 'reboot'
+
+  run govc vcsa.shutdown.reboot
+  assert_failure
+
+}
+
+
+@test "vcsa.shutdown.get" {
+  vcsim_env
+  local output
+
+  run govc vcsa.shutdown.get
+  assert_success
+
+  run govc vcsa.shutdown.get -json
+  assert_success
+
+  run govc vcsa.shutdown.status -yaml
+  assert_failure
+}
+
+
+@test "vcsa.shutdown.cancel" {
+  vcsim_env
+  local output
+
+  run govc vcsa.shutdown.cancel
+  assert_success
+
+  run govc vcsa.shutdown.reboot -delay 50 "rebooting for maintenance"
+  assert_success
+  run govc vcsa.shutdown.cancel
+  status=$(govc vcsa.shutdown.get| grep -i "Action"| awk '{print $2}')
+  assert_equal $status ''
+  status=$(govc vcsa.shutdown.get| grep -i "ShutDownTime"| awk '{print $2}')
+  assert_equal $status ''
+  status=$(govc vcsa.shutdown.get| grep -i "Reason"| awk '{print $2}')
+  
+
+  run govc vcsa.shutdown.cancel -reason "cancelling shutdown"
+  assert_failure
+}