[cli] Add windows service commands (#26442)

Adds a new `windows` command which is available when running on
a Windows hosts. The command includes two new subcommands:

* `service install`
* `service uninstall`

The `service install` command will install the called binary into
the Windows program files directory, create a new Windows service,
setup configuration and data directories, and register the service
with the Window eventlog. If the service and/or binary already
exist, the service will be stopped, service and eventlog updated
if needed, binary replaced, and the service started again.

The `service uninstall` command will stop the service, remove the
Windows service, and deregister the service with the eventlog. It
will not remove the configuration/data directory nor will it remove
the installed binary.
This commit is contained in:
Chris Roberts
2025-08-25 10:07:24 -07:00
parent 61c36bdef7
commit c3dcdb5413
17 changed files with 1587 additions and 4 deletions

3
.changelog/26441.txt Normal file
View File

@@ -0,0 +1,3 @@
```release-note:improvement
agent: Allow agent logging to the Windows Event Log
```

3
.changelog/26442.txt Normal file
View File

@@ -0,0 +1,3 @@
```release-note:improvement
cli: Add commands for installing and uninstalling Windows system service
```

View File

@@ -5,7 +5,9 @@ package command
import (
"fmt"
"maps"
"os"
"runtime"
"github.com/hashicorp/cli"
"github.com/hashicorp/nomad/command/agent"
@@ -1339,6 +1341,31 @@ func Commands(metaPtr *Meta, agentUi cli.Ui) map[string]cli.CommandFactory {
},
}
if runtime.GOOS == "windows" {
maps.Copy(all, map[string]cli.CommandFactory{
"windows": func() (cli.Command, error) {
return &WindowsCommand{
Meta: meta,
}, nil
},
"windows service": func() (cli.Command, error) {
return &WindowsServiceCommand{
Meta: meta,
}, nil
},
"windows service install": func() (cli.Command, error) {
return &WindowsServiceInstallCommand{
Meta: meta,
}, nil
},
"windows service uninstall": func() (cli.Command, error) {
return &WindowsServiceUninstallCommand{
Meta: meta,
}, nil
},
})
}
deprecated := map[string]cli.CommandFactory{
"client-config": func() (cli.Command, error) {
return &DeprecatedCommand{

35
command/windows.go Normal file
View File

@@ -0,0 +1,35 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"strings"
"github.com/hashicorp/cli"
)
type WindowsCommand struct {
Meta
}
func (c *WindowsCommand) Help() string {
helpText := `
Usage: nomad windows <subcommand> [options]
This command groups subcommands for managing Nomad as a system service on Windows.
Service::
$ nomad windows service
Refer to the individual subcommand help for detailed usage information.
`
return strings.TrimSpace(helpText)
}
func (c *WindowsCommand) Name() string { return "windows" }
func (c *WindowsCommand) Synopsis() string { return "Manage Nomad as a system service on Windows" }
func (c *WindowsCommand) Run(_ []string) int { return cli.RunResultHelp }

View File

@@ -0,0 +1,41 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"strings"
"github.com/hashicorp/cli"
)
type WindowsServiceCommand struct {
Meta
}
func (c *WindowsServiceCommand) Help() string {
helpText := `
Usage: nomad windows service <subcommand> [options]
This command groups subcommands for managing Nomad as a system service on Windows.
Install:
$ nomad windows service install
Uninstall:
$ nomad windows service uninstall
Refer to the individual subcommand help for detailed usage information.
`
return strings.TrimSpace(helpText)
}
func (c *WindowsServiceCommand) Name() string { return "windows service" }
func (c *WindowsServiceCommand) Synopsis() string {
return "Manage nomad as a system service on Windows"
}
func (c *WindowsServiceCommand) Run(_ []string) int { return cli.RunResultHelp }

View File

@@ -0,0 +1,365 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/hashicorp/nomad/helper/winsvc"
"github.com/posener/complete"
)
type windowsInstallOpts struct {
configDir, dataDir, installDir, binaryPath string
reinstall bool
}
type WindowsServiceInstallCommand struct {
Meta
serviceManagerFn func() (winsvc.WindowsServiceManager, error)
privilegedCheckFn func() bool
winPaths winsvc.WindowsPaths
}
func (c *WindowsServiceInstallCommand) Synopsis() string {
return "Install the Nomad Windows system service"
}
func (c *WindowsServiceInstallCommand) AutocompleteFlags() complete.Flags {
return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetDefault),
complete.Flags{
"-config-dir": complete.PredictDirs("*"),
"-data-dir": complete.PredictDirs("*"),
"-install-dir": complete.PredictDirs("*"),
"-reinstall": complete.PredictNothing,
})
}
func (c *WindowsServiceInstallCommand) Name() string { return "windows service install" }
func (c *WindowsServiceInstallCommand) Help() string {
helpText := `
Usage: nomad windows service install [options]
This command installs Nomad as a Windows system service.
General Options:
` + generalOptionsUsage(usageOptsDefault) + `
Service Install Options:
-config-dir <dir>
Directory to hold the Nomad agent configuration. Defaults
to "{{.ProgramFiles}}\HashiCorp\nomad\bin"
-data-dir <dir>
Directory to hold the Nomad agent state. Defaults
to "{{.ProgramData}}\HashiCorp\nomad\data"
-install-dir <dir>
Directory to install the Nomad binary. Defaults
to "{{.ProgramData}}\HashiCorp\nomad\config"
-reinstall
Allow the nomad Windows service to be stopped during install.
`
return strings.TrimSpace(helpText)
}
func (c *WindowsServiceInstallCommand) Run(args []string) int {
opts := &windowsInstallOpts{}
flags := c.Meta.FlagSet(c.Name(), FlagSetDefault)
flags.Usage = func() { c.Ui.Output(c.Help()) }
flags.StringVar(&opts.configDir, "config-dir", "", "")
flags.StringVar(&opts.dataDir, "data-dir", "", "")
flags.StringVar(&opts.installDir, "install-dir", "", "")
flags.BoolVar(&opts.reinstall, "reinstall", false, "")
if err := flags.Parse(args); err != nil {
return 1
}
if args = flags.Args(); len(args) > 0 {
c.Ui.Error("This command takes no arguments")
c.Ui.Error(commandErrorText(c))
return 1
}
// Set helper functions to defaults if unset
if c.winPaths == nil {
c.winPaths = winsvc.NewWindowsPaths()
}
if c.serviceManagerFn == nil {
c.serviceManagerFn = winsvc.NewWindowsServiceManager
}
if c.privilegedCheckFn == nil {
c.privilegedCheckFn = winsvc.IsPrivilegedProcess
}
// Check that command is being run with elevated permissions
if !c.privilegedCheckFn() {
c.Ui.Error("Service install must be run with Administator privileges")
return 1
}
c.Ui.Output("Installing nomad as a Windows service...")
m, err := c.serviceManagerFn()
if err != nil {
c.Ui.Error(fmt.Sprintf("Could not connect to Windows service manager - %s", err))
return 1
}
defer m.Close()
if err := c.performInstall(m, opts); err != nil {
c.Ui.Error(fmt.Sprintf("Service install failed: %s", err))
return 1
}
c.Ui.Info("Successfully installed nomad Windows service")
return 0
}
func (c *WindowsServiceInstallCommand) performInstall(m winsvc.WindowsServiceManager, opts *windowsInstallOpts) error {
// Check if the nomad service has already been
// registered. If so the service needs to be
// stopped before proceeding with the install.
exists, err := m.IsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME)
if err != nil {
return fmt.Errorf("unable to check for existing service - %w", err)
}
if exists {
nmdSvc, err := m.GetService(winsvc.WINDOWS_SERVICE_NAME)
if err != nil {
return fmt.Errorf("could not get existing service to stop - %w", err)
}
// If the service is running and the reinstall
// option was not set, return an error
running, err := nmdSvc.IsRunning()
if err != nil {
return fmt.Errorf("unable to determine service state - %w", err)
}
if running && !opts.reinstall {
return fmt.Errorf("service is already running. Please run the\ncommand again with -reinstall to stop the service and install.")
}
if running {
c.Ui.Output(" Stopping existing nomad service")
if err := nmdSvc.Stop(); err != nil {
return fmt.Errorf("unable to stop existing service - %w", err)
}
}
}
// Install the nomad binary into the system
if err = c.binaryInstall(opts); err != nil {
return fmt.Errorf("binary install failed - %w", err)
}
c.Ui.Output(fmt.Sprintf(" Nomad binary installed to: %s", opts.binaryPath))
// Create a configuration directory and add
// a basic configuration file if no configuration
// currently exists
if err = c.configInstall(opts); err != nil {
return fmt.Errorf("configuration install failed - %w", err)
}
c.Ui.Output(fmt.Sprintf(" Nomad configuration directory: %s", opts.configDir))
c.Ui.Output(fmt.Sprintf(" Nomad agent data directory: %s", opts.configDir))
// Now let's install that service
if err := c.serviceInstall(m, opts); err != nil {
return fmt.Errorf("service install failed - %w", err)
}
return nil
}
func (c *WindowsServiceInstallCommand) serviceInstall(m winsvc.WindowsServiceManager, opts *windowsInstallOpts) error {
var err error
var srvc winsvc.WindowsService
cmd := fmt.Sprintf("%s agent -config %s", opts.binaryPath, opts.configDir)
exists, err := m.IsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME)
if err != nil {
return fmt.Errorf("service registration check failed - %w", err)
}
// If the service already exists, open it and update. Otherwise
// create a new service.
if exists {
srvc, err = m.GetService(winsvc.WINDOWS_SERVICE_NAME)
if err != nil {
return fmt.Errorf("unable to get existing service - %w", err)
}
defer srvc.Close()
if err := srvc.Configure(winsvc.WindowsServiceConfiguration{
StartType: winsvc.StartAutomatic,
DisplayName: winsvc.WINDOWS_SERVICE_DISPLAY_NAME,
Description: winsvc.WINDOWS_SERVICE_DESCRIPTION,
BinaryPathName: cmd,
}); err != nil {
return fmt.Errorf("unable to configure service - %w", err)
}
} else {
srvc, err = m.CreateService(winsvc.WINDOWS_SERVICE_NAME, opts.binaryPath,
winsvc.WindowsServiceConfiguration{
StartType: winsvc.StartAutomatic,
DisplayName: winsvc.WINDOWS_SERVICE_DISPLAY_NAME,
Description: winsvc.WINDOWS_SERVICE_DESCRIPTION,
BinaryPathName: cmd,
},
)
if err != nil {
return fmt.Errorf("unable to create service - %w", err)
}
defer srvc.Close()
}
// Enable the service in the Windows eventlog
if err := srvc.EnableEventlog(); err != nil {
return fmt.Errorf("could not configure eventlog - %w", err)
}
// Ensure the service is stopped
if err := srvc.Stop(); err != nil {
return fmt.Errorf("could not stop service - %w", err)
}
// Start the service so the new binary is in use
if err := srvc.Start(); err != nil {
return fmt.Errorf("could not start service - %w", err)
}
return nil
}
func (c *WindowsServiceInstallCommand) configInstall(opts *windowsInstallOpts) error {
// If the config or data directory are unset, default them
if opts.configDir == "" {
opts.configDir = filepath.Join(winsvc.WINDOWS_INSTALL_APPDATA_DIRECTORY, "config")
}
if opts.dataDir == "" {
opts.dataDir = filepath.Join(winsvc.WINDOWS_INSTALL_APPDATA_DIRECTORY, "data")
}
var err error
if opts.configDir, err = c.winPaths.Expand(opts.configDir); err != nil {
return fmt.Errorf("cannot generate configuration path - %s", err)
}
if opts.dataDir, err = c.winPaths.Expand(opts.dataDir); err != nil {
return fmt.Errorf("cannot generate data path - %s", err)
}
// Ensure directories exist
if err = c.winPaths.CreateDirectory(opts.configDir, true); err != nil {
return fmt.Errorf("cannot create configuration directory - %s", err)
}
if err = c.winPaths.CreateDirectory(opts.dataDir, true); err != nil {
return fmt.Errorf("cannot create data directory - %s", err)
}
// Check if any configuration files exist
matches, _ := filepath.Glob(filepath.Join(opts.configDir, "*"))
if len(matches) < 1 {
f, err := os.Create(filepath.Join(opts.configDir, "config.hcl"))
if err != nil {
return fmt.Errorf("could not create default configuration file - %s", err)
}
fmt.Fprintf(f, strings.TrimSpace(`
# Full configuration options can be found at https://developer.hashicorp.com/nomad/docs/configuration
data_dir = "%s"
bind_addr = "0.0.0.0"
server {
# license_path is required for Nomad Enterprise as of Nomad v1.1.1+
#license_path = "%s\license.hclic"
enabled = true
bootstrap_expect = 1
}
client {
enabled = true
servers = ["127.0.0.1"]
}
log_level = "WARN"
eventlog {
enabled = true
level = "ERROR"
}
`), strings.ReplaceAll(opts.dataDir, `\`, `\\`), strings.ReplaceAll(opts.configDir, `\`, `\\`))
f.Close()
c.Ui.Output(fmt.Sprintf(" Added initial configuration file: %s", f.Name()))
}
return nil
}
func (c *WindowsServiceInstallCommand) binaryInstall(opts *windowsInstallOpts) error {
// Get the path to the currently executing nomad. This
// will be installed for the service to call.
exePath, err := os.Executable()
if err != nil {
return fmt.Errorf("cannot detect current nomad path - %s", err)
}
// Build the needed paths
if opts.installDir == "" {
opts.installDir = winsvc.WINDOWS_INSTALL_BIN_DIRECTORY
}
opts.installDir, err = c.winPaths.Expand(opts.installDir)
if err != nil {
return fmt.Errorf("cannot generate binary install path - %s", err)
}
opts.binaryPath = filepath.Join(opts.installDir, "nomad.exe")
// Ensure the install directory exists
if err = c.winPaths.CreateDirectory(opts.installDir, false); err != nil {
return fmt.Errorf("could not create binary install directory - %s", err)
}
// Create a new copy of the current binary to install
exeFile, err := os.Open(exePath)
if err != nil {
return fmt.Errorf("cannot open current nomad path for install - %s", err)
}
defer exeFile.Close()
// Copy into a temporary file which can then be moved
// into the correct location.
dstFile, err := os.CreateTemp(os.TempDir(), "nomad*")
if err != nil {
return fmt.Errorf("cannot create copy - %s", err)
}
defer dstFile.Close()
if _, err = io.Copy(dstFile, exeFile); err != nil {
return fmt.Errorf("cannot write nomad binary for install - %s", err)
}
dstFile.Close()
// With a copy ready to be moved into place, ensure that
// the path is clear then move the file.
if err = os.RemoveAll(opts.binaryPath); err != nil {
return fmt.Errorf("cannot remove existing nomad binary install - %s", err)
}
if err = os.Rename(dstFile.Name(), opts.binaryPath); err != nil {
return fmt.Errorf("cannot install new nomad binary - %s", err)
}
return nil
}

View File

@@ -0,0 +1,391 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"bytes"
"errors"
"os"
"path/filepath"
"strings"
"testing"
"text/template"
"github.com/hashicorp/cli"
"github.com/hashicorp/nomad/helper/winsvc"
"github.com/shoenig/test/must"
)
func TestWindowsServiceInstallCommand_Run(t *testing.T) {
t.Parallel()
freshInstallFn := func(m *winsvc.MockWindowsServiceManager) {
srv := m.NewMockWindowsService()
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, false, nil)
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, false, nil)
m.ExpectCreateService(winsvc.WINDOWS_SERVICE_NAME, "nomad.exe",
winsvc.WindowsServiceConfiguration{}, srv, nil)
srv.ExpectEnableEventlog(nil)
srv.ExpectStop(nil)
srv.ExpectStart(nil)
}
upgradeInstallFn := func(m *winsvc.MockWindowsServiceManager) {
srv := m.NewMockWindowsService()
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, true, nil)
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, true, nil)
m.ExpectGetService(winsvc.WINDOWS_SERVICE_NAME, srv, nil)
m.ExpectGetService(winsvc.WINDOWS_SERVICE_NAME, srv, nil)
srv.ExpectIsRunning(false, nil)
srv.ExpectConfigure(winsvc.WindowsServiceConfiguration{}, nil)
srv.ExpectEnableEventlog(nil)
srv.ExpectStop(nil)
srv.ExpectStart(nil)
}
testCases := []struct {
desc string
args []string
privilegeFn func() bool
setup func(string, *winsvc.MockWindowsServiceManager)
after func(string)
output string
errOutput string
status int
}{
{
desc: "fresh install success",
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
freshInstallFn(m)
},
output: "Success",
status: 0,
},
{
desc: "fresh install writes config",
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
freshInstallFn(m)
},
after: func(dir string) {
must.FileExists(t, filepath.Join(dir, "programdata/HashiCorp/nomad/config/config.hcl"))
},
output: "initial configuration file",
},
{
desc: "fresh install binary file",
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
freshInstallFn(m)
},
after: func(dir string) {
must.FileExists(t, filepath.Join(dir, "programfiles/HashiCorp/nomad/bin/nomad.exe"))
},
output: "binary installed",
},
{
desc: "fresh install configuration already exists",
setup: func(dir string, m *winsvc.MockWindowsServiceManager) {
cdir := filepath.Join(dir, "programdata/HashiCorp/nomad/config")
err := os.MkdirAll(cdir, 0o755)
must.NoError(t, err)
f, err := os.Create(filepath.Join(cdir, "custom.hcl"))
must.NoError(t, err)
f.Close()
freshInstallFn(m)
},
after: func(dir string) {
must.FileNotExists(t, filepath.Join(dir, "programdata/HashiCorp/nomad/config/config.hcl"))
},
},
{
desc: "fresh install binary already exists",
setup: func(dir string, m *winsvc.MockWindowsServiceManager) {
cdir := filepath.Join(dir, "programfiles/HashiCorp/nomad/bin")
err := os.MkdirAll(cdir, 0o755)
must.NoError(t, err)
// create empty binary file
f, err := os.Create(filepath.Join(cdir, "nomad.exe"))
must.NoError(t, err)
f.Close()
freshInstallFn(m)
},
after: func(dir string) {
s, err := os.Stat(filepath.Join(dir, "programfiles/HashiCorp/nomad/bin/nomad.exe"))
must.NoError(t, err)
// ensure binary file is not empty
must.NonZero(t, s.Size())
},
},
{
desc: "upgrade install success",
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
upgradeInstallFn(m)
},
output: "Success",
status: 0,
},
{
desc: "with arguments",
args: []string{"any", "value"},
errOutput: "takes no arguments",
status: 1,
},
{
desc: "with -install-dir",
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
freshInstallFn(m)
},
args: []string{"-install-dir", "{{.ProgramFiles}}/custom/bin"},
after: func(dir string) {
_, err := os.Stat(filepath.Join(dir, "programfiles/custom/bin/nomad.exe"))
must.NoError(t, err)
},
},
{
desc: "with -config-dir",
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
freshInstallFn(m)
},
args: []string{"-config-dir", "{{.ProgramData}}/custom/nomad-configuration"},
after: func(dir string) {
_, err := os.Stat(filepath.Join(dir, "programdata/custom/nomad-configuration"))
must.NoError(t, err)
},
},
{
desc: "with -data-dir",
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
freshInstallFn(m)
},
args: []string{"-data-dir", "{{.ProgramData}}/custom/nomad-data"},
after: func(dir string) {
_, err := os.Stat(filepath.Join(dir, "programdata/custom/nomad-data"))
must.NoError(t, err)
},
},
{
desc: "service registered check failure",
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, false, errors.New("lookup failure"))
},
errOutput: "unable to check for existing service",
status: 1,
},
{
desc: "service registered check failure during service install",
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, false, nil)
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, false, errors.New("lookup failure"))
},
errOutput: "registration check failed",
status: 1,
},
{
desc: "get existing service to stop failure",
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, true, nil)
m.ExpectGetService(winsvc.WINDOWS_SERVICE_NAME, nil, errors.New("service get failure"))
},
errOutput: "could not get existing service",
status: 1,
},
{
desc: "stop existing service failure",
args: []string{"-reinstall"},
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
srv := m.NewMockWindowsService()
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, true, nil)
m.ExpectGetService(winsvc.WINDOWS_SERVICE_NAME, srv, nil)
srv.ExpectIsRunning(true, nil)
srv.ExpectStop(errors.New("cannot stop"))
},
errOutput: "unable to stop existing service",
status: 1,
},
{
desc: "get existing service to configure failure",
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
srv := m.NewMockWindowsService()
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, true, nil)
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, true, nil)
m.ExpectGetService(winsvc.WINDOWS_SERVICE_NAME, srv, nil)
srv.ExpectIsRunning(false, nil)
m.ExpectGetService(winsvc.WINDOWS_SERVICE_NAME, nil, errors.New("service get failure"))
},
errOutput: "unable to get existing service",
status: 1,
},
{
desc: "configure service failure",
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
srv := m.NewMockWindowsService()
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, true, nil)
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, true, nil)
srv.ExpectIsRunning(false, nil)
m.ExpectGetService(winsvc.WINDOWS_SERVICE_NAME, srv, nil)
m.ExpectGetService(winsvc.WINDOWS_SERVICE_NAME, srv, nil)
srv.ExpectConfigure(winsvc.WindowsServiceConfiguration{}, errors.New("configure failure"))
},
errOutput: "unable to configure service",
status: 1,
},
{
desc: "create service failure",
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, false, nil)
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, false, nil)
m.ExpectCreateService(winsvc.WINDOWS_SERVICE_NAME, "nomad.exe", winsvc.WindowsServiceConfiguration{}, nil, errors.New("create service failure"))
},
errOutput: "unable to create service",
status: 1,
},
{
desc: "eventlog setup failure",
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
srv := m.NewMockWindowsService()
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, false, nil)
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, false, nil)
m.ExpectCreateService(winsvc.WINDOWS_SERVICE_NAME, "nomad.exe", winsvc.WindowsServiceConfiguration{}, srv, nil)
srv.ExpectEnableEventlog(errors.New("eventlog configure failure"))
},
errOutput: "could not configure eventlog",
status: 1,
},
{
desc: "service stop pre-start failure",
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
srv := m.NewMockWindowsService()
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, false, nil)
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, false, nil)
m.ExpectCreateService(winsvc.WINDOWS_SERVICE_NAME, "nomad.exe", winsvc.WindowsServiceConfiguration{}, srv, nil)
srv.ExpectEnableEventlog(nil)
srv.ExpectStop(errors.New("service stop failure"))
},
errOutput: "could not stop service",
status: 1,
},
{
desc: "service start failure",
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
srv := m.NewMockWindowsService()
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, false, nil)
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, false, nil)
m.ExpectCreateService(winsvc.WINDOWS_SERVICE_NAME, "nomad.exe", winsvc.WindowsServiceConfiguration{}, srv, nil)
srv.ExpectEnableEventlog(nil)
srv.ExpectStop(nil)
srv.ExpectStart(errors.New("service start failure"))
},
errOutput: "could not start service",
status: 1,
},
{
desc: "upgrade without -reinstall and service running",
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
srv := m.NewMockWindowsService()
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, true, nil)
m.ExpectGetService(winsvc.WINDOWS_SERVICE_NAME, srv, nil)
srv.ExpectIsRunning(true, nil)
},
errOutput: "again with -reinstall",
status: 1,
},
{
desc: "upgrade with -reinstall and service running",
args: []string{"-reinstall"},
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
srv := m.NewMockWindowsService()
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, true, nil)
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, true, nil)
m.ExpectGetService(winsvc.WINDOWS_SERVICE_NAME, srv, nil)
m.ExpectGetService(winsvc.WINDOWS_SERVICE_NAME, srv, nil)
srv.ExpectIsRunning(true, nil)
srv.ExpectStop(nil)
srv.ExpectConfigure(winsvc.WindowsServiceConfiguration{}, nil)
srv.ExpectEnableEventlog(nil)
srv.ExpectStop(nil)
srv.ExpectStart(nil)
},
},
{
desc: "not running as administator",
privilegeFn: func() bool { return false },
errOutput: "must be run with Administator privileges",
status: 1,
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
t.Parallel()
testDir := t.TempDir()
ui := cli.NewMockUi()
mgr := winsvc.NewMockWindowsServiceManager(t)
if tc.setup != nil {
tc.setup(testDir, mgr)
}
pfn := tc.privilegeFn
if pfn == nil {
pfn = func() bool { return true }
}
cmd := &WindowsServiceInstallCommand{
Meta: Meta{Ui: ui},
serviceManagerFn: func() (winsvc.WindowsServiceManager, error) {
return mgr, nil
},
winPaths: createWinPaths(testDir),
privilegedCheckFn: pfn,
}
result := cmd.Run(tc.args)
out := ui.OutputWriter.String()
outErr := ui.ErrorWriter.String()
must.Eq(t, result, tc.status)
if tc.output != "" {
must.StrContains(t, out, tc.output)
}
if tc.errOutput != "" {
must.StrContains(t, outErr, tc.errOutput)
}
if tc.after != nil {
tc.after(testDir)
}
mgr.AssertExpectations()
})
}
}
func createWinPaths(rootDir string) winsvc.WindowsPaths {
return &testWindowsPaths{
SystemRoot: filepath.Join(rootDir, "systemroot"),
SystemDrive: rootDir,
ProgramData: filepath.Join(rootDir, "programdata"),
ProgramFiles: filepath.Join(rootDir, "programfiles"),
}
}
type testWindowsPaths struct {
SystemRoot string
SystemDrive string
ProgramData string
ProgramFiles string
}
func (t *testWindowsPaths) Expand(path string) (string, error) {
tmpl, err := template.New("expansion").Option("missingkey=error").Parse(path)
if err != nil {
return "", err
}
result := new(bytes.Buffer)
if err := tmpl.Execute(result, t); err != nil {
return "", err
}
return strings.ReplaceAll(result.String(), `\`, "/"), nil
}
func (t *testWindowsPaths) CreateDirectory(path string, _ bool) error {
return os.MkdirAll(path, 0o755)
}

View File

@@ -0,0 +1,121 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"fmt"
"strings"
"github.com/hashicorp/nomad/helper/winsvc"
"github.com/posener/complete"
)
type WindowsServiceUninstallCommand struct {
Meta
serviceManagerFn func() (winsvc.WindowsServiceManager, error)
privilegedCheckFn func() bool
}
func (c *WindowsServiceUninstallCommand) AutoCompleteFlags() complete.Flags {
return c.Meta.AutocompleteFlags(FlagSetDefault)
}
func (c *WindowsServiceUninstallCommand) Synopsis() string {
return "Uninstall the nomad Windows system service"
}
func (c *WindowsServiceUninstallCommand) Name() string { return "windows service uninstall" }
func (c *WindowsServiceUninstallCommand) Help() string {
helpText := `
Usage: nomad windows service uninstall [options]
This command uninstalls nomad as a Windows system service.
General Options:
` + generalOptionsUsage(usageOptsDefault)
return strings.TrimSpace(helpText)
}
func (c *WindowsServiceUninstallCommand) Run(args []string) int {
flags := c.Meta.FlagSet(c.Name(), FlagSetDefault)
flags.Usage = func() { c.Ui.Output(c.Help()) }
if err := flags.Parse(args); err != nil {
return 1
}
if args = flags.Args(); len(args) > 0 {
c.Ui.Error("This command takes no arguments")
c.Ui.Error(commandErrorText(c))
return 1
}
// Set helper functions to default if unset
if c.serviceManagerFn == nil {
c.serviceManagerFn = winsvc.NewWindowsServiceManager
}
if c.privilegedCheckFn == nil {
c.privilegedCheckFn = winsvc.IsPrivilegedProcess
}
// Check that command is being run with elevated permissions
if !c.privilegedCheckFn() {
c.Ui.Error("Service uninstall must be run with Administator privileges")
return 1
}
c.Ui.Output("Uninstalling nomad Windows service...")
m, err := c.serviceManagerFn()
if err != nil {
c.Ui.Error(fmt.Sprintf("Could not connect to Windows service manager - %s", err))
return 1
}
defer m.Close()
if err := c.performUninstall(m); err != nil {
c.Ui.Error(fmt.Sprintf("Service uninstall failed: %s", err))
return 1
}
c.Ui.Info("Successfully uninstalled nomad Windows service")
return 0
}
func (c *WindowsServiceUninstallCommand) performUninstall(m winsvc.WindowsServiceManager) error {
// Check that the nomad service is currently registered
exists, err := m.IsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME)
if err != nil {
return fmt.Errorf("unable to check for existing service - %w", err)
}
if !exists {
return nil
}
// Grab the service and ensure the service is stopped
srvc, err := m.GetService(winsvc.WINDOWS_SERVICE_NAME)
if err != nil {
return fmt.Errorf("could not get existing service - %w", err)
}
defer srvc.Close()
if err := srvc.Stop(); err != nil {
return fmt.Errorf("unable to stop service - %w", err)
}
// Remove the service from the event log
if err := srvc.DisableEventlog(); err != nil {
return fmt.Errorf("could not remove eventlog configuration - %w", err)
}
// Finally, delete the service
if err := srvc.Delete(); err != nil {
return fmt.Errorf("could not delete service - %w", err)
}
return nil
}

View File

@@ -0,0 +1,146 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"errors"
"testing"
"github.com/hashicorp/cli"
"github.com/hashicorp/nomad/helper/winsvc"
"github.com/shoenig/test/must"
)
func TestWindowsServiceUninstallCommand_Run(t *testing.T) {
testCases := []struct {
desc string
args []string
privilegeFn func() bool
setup func(*winsvc.MockWindowsServiceManager)
output string
errOutput string
status int
}{
{
desc: "service installed",
setup: func(m *winsvc.MockWindowsServiceManager) {
srv := m.NewMockWindowsService()
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, true, nil)
m.ExpectGetService(winsvc.WINDOWS_SERVICE_NAME, srv, nil)
srv.ExpectStop(nil)
srv.ExpectDisableEventlog(nil)
srv.ExpectDelete(nil)
},
output: "uninstalled nomad",
},
{
desc: "service not installed",
setup: func(m *winsvc.MockWindowsServiceManager) {
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, false, nil)
},
output: "uninstalled nomad",
},
{
desc: "service registration check failure",
setup: func(m *winsvc.MockWindowsServiceManager) {
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, false, errors.New("registered check failure"))
},
errOutput: "unable to check for existing service",
status: 1,
},
{
desc: "get service failure",
setup: func(m *winsvc.MockWindowsServiceManager) {
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, true, nil)
m.ExpectGetService(winsvc.WINDOWS_SERVICE_NAME, nil, errors.New("get service failure"))
},
errOutput: "could not get existing service",
status: 1,
},
{
desc: "service stop failure",
setup: func(m *winsvc.MockWindowsServiceManager) {
srv := m.NewMockWindowsService()
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, true, nil)
m.ExpectGetService(winsvc.WINDOWS_SERVICE_NAME, srv, nil)
srv.ExpectStop(errors.New("service stop failure"))
},
errOutput: "unable to stop service",
status: 1,
},
{
desc: "disable eventlog failure",
setup: func(m *winsvc.MockWindowsServiceManager) {
srv := m.NewMockWindowsService()
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, true, nil)
m.ExpectGetService(winsvc.WINDOWS_SERVICE_NAME, srv, nil)
srv.ExpectStop(nil)
srv.ExpectDisableEventlog(errors.New("disable eventlog failure"))
},
errOutput: "could not remove eventlog configuration",
status: 1,
},
{
desc: "delete service failure",
setup: func(m *winsvc.MockWindowsServiceManager) {
srv := m.NewMockWindowsService()
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, true, nil)
m.ExpectGetService(winsvc.WINDOWS_SERVICE_NAME, srv, nil)
srv.ExpectStop(nil)
srv.ExpectDisableEventlog(nil)
srv.ExpectDelete(errors.New("service delete failure"))
},
errOutput: "could not delete service",
status: 1,
},
{
desc: "with arguments",
args: []string{"any", "value"},
errOutput: "command takes no arguments",
status: 1,
},
{
desc: "not running as administator",
privilegeFn: func() bool { return false },
errOutput: "must be run with Administator privileges",
status: 1,
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
ui := cli.NewMockUi()
mgr := winsvc.NewMockWindowsServiceManager(t)
if tc.setup != nil {
tc.setup(mgr)
}
pfn := tc.privilegeFn
if pfn == nil {
pfn = func() bool { return true }
}
cmd := &WindowsServiceUninstallCommand{
Meta: Meta{Ui: ui},
serviceManagerFn: func() (winsvc.WindowsServiceManager, error) {
return mgr, nil
},
privilegedCheckFn: pfn,
}
result := cmd.Run(tc.args)
out := ui.OutputWriter.String()
outErr := ui.ErrorWriter.String()
must.Eq(t, result, tc.status)
if tc.output != "" {
must.StrContains(t, out, tc.output)
}
if tc.errOutput != "" {
must.StrContains(t, outErr, tc.errOutput)
}
mgr.AssertExpectations()
})
}
}

View File

@@ -0,0 +1,320 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package winsvc
import (
"reflect"
"testing"
"github.com/shoenig/test/must"
)
func NewMockWindowsServiceManager(t *testing.T) *MockWindowsServiceManager {
t.Helper()
m := &MockWindowsServiceManager{t: t}
return m
}
func NewMockWindowsService(t *testing.T) *MockWindowsService {
t.Helper()
m := &MockWindowsService{t: t}
return m
}
type MockWindowsServiceManager struct {
services []*MockWindowsService
isServiceRegistereds []isServiceRegistered
getServices []getService
createServices []createService
t *testing.T
}
type isServiceRegistered struct {
name string
result bool
err error
}
type getService struct {
name string
result WindowsService
err error
}
type createService struct {
name, binaryPath string
config WindowsServiceConfiguration
result WindowsService
err error
}
func (m *MockWindowsServiceManager) NewMockWindowsService() *MockWindowsService {
w := NewMockWindowsService(m.t)
m.services = append(m.services, w)
return w
}
func (m *MockWindowsServiceManager) ExpectIsServiceRegistered(name string, result bool, err error) {
m.isServiceRegistereds = append(m.isServiceRegistereds, isServiceRegistered{name, result, err})
}
func (m *MockWindowsServiceManager) ExpectGetService(name string, result WindowsService, err error) {
m.getServices = append(m.getServices, getService{name, result, err})
}
func (m *MockWindowsServiceManager) ExpectCreateService(name, binaryPath string, config WindowsServiceConfiguration, result WindowsService, err error) {
m.createServices = append(m.createServices, createService{name, binaryPath, config, result, err})
}
func (m *MockWindowsServiceManager) IsServiceRegistered(name string) (bool, error) {
m.t.Helper()
must.SliceNotEmpty(m.t, m.isServiceRegistereds,
must.Sprint("Unexpected call to IsServiceRegistered"))
call := m.isServiceRegistereds[0]
m.isServiceRegistereds = m.isServiceRegistereds[1:]
must.Eq(m.t, call.name, name,
must.Sprint("IsServiceRegistered received incorrect argument"))
return call.result, call.err
}
func (m *MockWindowsServiceManager) GetService(name string) (WindowsService, error) {
m.t.Helper()
must.SliceNotEmpty(m.t, m.getServices,
must.Sprint("Unexpected call to GetService"))
call := m.getServices[0]
m.getServices = m.getServices[1:]
must.Eq(m.t, call.name, name,
must.Sprint("GetService received incorrect argument"))
return call.result, call.err
}
func (m *MockWindowsServiceManager) CreateService(name, binaryPath string, config WindowsServiceConfiguration) (WindowsService, error) {
m.t.Helper()
must.SliceNotEmpty(m.t, m.createServices,
must.Sprint("Unexpected call to CreateService"))
call := m.createServices[0]
m.createServices = m.createServices[1:]
must.Eq(m.t, call.name, name,
must.Sprint("CreateService received incorrect argument"))
must.StrContains(m.t, binaryPath, call.binaryPath,
must.Sprint("CreateService received incorrect argument"))
if !reflect.ValueOf(call.config).IsZero() {
must.Eq(m.t, call.config, config,
must.Sprint("CreateService received incorrect argument"))
}
return call.result, call.err
}
func (m *MockWindowsServiceManager) Close() error { return nil }
func (m *MockWindowsServiceManager) AssertExpectations() {
m.t.Helper()
must.SliceEmpty(m.t, m.isServiceRegistereds,
must.Sprintf("IsServiceRegistered expecting %d more invocations", len(m.isServiceRegistereds)))
must.SliceEmpty(m.t, m.getServices,
must.Sprintf("GetService expecting %d more invocations", len(m.getServices)))
must.SliceEmpty(m.t, m.createServices,
must.Sprintf("CreateService expecting %d more invocations", len(m.createServices)))
for _, srv := range m.services {
srv.AssertExpectations()
}
}
type MockWindowsService struct {
names []string
configures []configure
starts []error
stops []error
deletes []error
isRunnings []iscall
isStoppeds []iscall
enableEventlogs []error
disableEventlogs []error
t *testing.T
}
type configure struct {
config WindowsServiceConfiguration
err error
}
type iscall struct {
result bool
err error
}
func (m *MockWindowsService) ExpectName(result string) {
m.names = append(m.names, result)
}
func (m *MockWindowsService) ExpectConfigure(config WindowsServiceConfiguration, err error) {
m.configures = append(m.configures, configure{config, err})
}
func (m *MockWindowsService) ExpectStart(err error) {
m.starts = append(m.starts, err)
}
func (m *MockWindowsService) ExpectStop(err error) {
m.stops = append(m.stops, err)
}
func (m *MockWindowsService) ExpectDelete(err error) {
m.deletes = append(m.deletes, err)
}
func (m *MockWindowsService) ExpectIsRunning(result bool, err error) {
m.isRunnings = append(m.isRunnings, iscall{result, err})
}
func (m *MockWindowsService) ExpectIsStopped(result bool, err error) {
m.isStoppeds = append(m.isStoppeds, iscall{result, err})
}
func (m *MockWindowsService) ExpectEnableEventlog(err error) {
m.enableEventlogs = append(m.enableEventlogs, err)
}
func (m *MockWindowsService) ExpectDisableEventlog(err error) {
m.disableEventlogs = append(m.disableEventlogs, err)
}
func (m *MockWindowsService) Name() string {
m.t.Helper()
must.SliceNotEmpty(m.t, m.names,
must.Sprint("Unexpected call to Name"))
name := m.names[0]
m.names = m.names[1:]
return name
}
func (m *MockWindowsService) Configure(config WindowsServiceConfiguration) error {
m.t.Helper()
must.SliceNotEmpty(m.t, m.configures,
must.Sprint("Unexpected call to Configure"))
call := m.configures[0]
m.configures = m.configures[1:]
if !reflect.ValueOf(call.config).IsZero() {
must.Eq(m.t, call.config, config,
must.Sprint("Configure received incorrect argument"))
}
return call.err
}
func (m *MockWindowsService) Start() error {
m.t.Helper()
must.SliceNotEmpty(m.t, m.starts,
must.Sprint("Unexpected call to Start"))
err := m.starts[0]
m.starts = m.starts[1:]
return err
}
func (m *MockWindowsService) Stop() error {
m.t.Helper()
must.SliceNotEmpty(m.t, m.stops,
must.Sprint("Unexpected call to Stop"))
err := m.stops[0]
m.stops = m.stops[1:]
return err
}
func (m *MockWindowsService) Delete() error {
m.t.Helper()
must.SliceNotEmpty(m.t, m.deletes,
must.Sprint("Unexpected call to Delete"))
err := m.deletes[0]
m.deletes = m.deletes[1:]
return err
}
func (m *MockWindowsService) IsRunning() (bool, error) {
m.t.Helper()
must.SliceNotEmpty(m.t, m.isRunnings,
must.Sprint("Unexpected call to IsRunning"))
call := m.isRunnings[0]
m.isRunnings = m.isRunnings[1:]
return call.result, call.err
}
func (m *MockWindowsService) IsStopped() (bool, error) {
m.t.Helper()
must.SliceNotEmpty(m.t, m.isStoppeds,
must.Sprint("Unexpected call to IsStopped"))
call := m.isStoppeds[0]
m.isStoppeds = m.isStoppeds[1:]
return call.result, call.err
}
func (m *MockWindowsService) EnableEventlog() error {
m.t.Helper()
must.SliceNotEmpty(m.t, m.enableEventlogs,
must.Sprint("Unexpected call to EnableEventlog"))
err := m.enableEventlogs[0]
m.enableEventlogs = m.enableEventlogs[1:]
return err
}
func (m *MockWindowsService) DisableEventlog() error {
m.t.Helper()
must.SliceNotEmpty(m.t, m.disableEventlogs,
must.Sprint("Unexpected call to DisableEventlog"))
err := m.disableEventlogs[0]
m.disableEventlogs = m.disableEventlogs[1:]
return err
}
func (m *MockWindowsService) Close() error { return nil }
func (m *MockWindowsService) AssertExpectations() {
m.t.Helper()
must.SliceEmpty(m.t, m.names,
must.Sprintf("Name expecting %d more invocations", len(m.names)))
must.SliceEmpty(m.t, m.configures,
must.Sprintf("Configure expecting %d more invocations", len(m.configures)))
must.SliceEmpty(m.t, m.starts,
must.Sprintf("Start expecting %d more invocations", len(m.starts)))
must.SliceEmpty(m.t, m.stops,
must.Sprintf("Stop expecting %d more invocations", len(m.stops)))
must.SliceEmpty(m.t, m.deletes,
must.Sprintf("Delete expecting %d more invocations", len(m.deletes)))
must.SliceEmpty(m.t, m.isRunnings,
must.Sprintf("IsRunning expecting %d more invocations", len(m.isRunnings)))
must.SliceEmpty(m.t, m.isStoppeds,
must.Sprintf("IsStopped expecting %d more invocations", len(m.isStoppeds)))
must.SliceEmpty(m.t, m.enableEventlogs,
must.Sprintf("EnableEventlog expecting %d more invocations", len(m.enableEventlogs)))
must.SliceEmpty(m.t, m.disableEventlogs,
must.Sprintf("DisableEventlog expecting %d more invocations", len(m.disableEventlogs)))
}

View File

@@ -129,6 +129,10 @@ You may, however, may pass the following configuration options as CLI arguments:
- `-encrypt`: Set the Serf encryption key. See the [Encryption Overview][] for
more details.
- `-eventlog`: Equivalent to the [eventlog.enabled][] config option.
- `-eventlog-level`: Equivalent to the [eventlog.level][] config option.
- `-join=<address>`: Address of another agent to join upon starting up. This can
be specified multiple times to specify multiple agents to join.
@@ -231,6 +235,8 @@ You may, however, may pass the following configuration options as CLI arguments:
[datacenter]: /nomad/docs/configuration#datacenter
[enabled]: /nomad/docs/configuration/acl#enabled
[encryption overview]: /nomad/docs/secure/traffic/gossip-encryption
[eventlog.enabled]: /nomad/docs/configuration#eventlog_enabled
[eventlog.level]: /nomad/docs/configuration#eventlog_level
[key_file]: /nomad/docs/configuration/consul#key_file
[log_include_location]: /nomad/docs/configuration#log_include_location
[log_json]: /nomad/docs/configuration#log_json

View File

@@ -0,0 +1,22 @@
---
layout: docs
page_title: 'nomad windows command reference'
description: |
The `nomad windows` command interacts with the Windows host platform. Install or uninstall Nomad as a Windows service.
---
# `nomad windows` command reference
Use the `windows` command to interact with Windows host platforms.
## Usage
Usage: `nomad windows <subcommand> [options]`
Run `nomad windows <subcommand> -h` for help on that subcommand. The following subcommands are available:
- [`windows service install`][install] - Install Nomad as a Windows service
- [`windows service uninstall`][uninstall] - Uninstall Nomad as a Windows service
[install]: /nomad/commands/windows/service-install
[uninstall]: /nomad/commands/windows/service-uninstall

View File

@@ -0,0 +1,50 @@
---
layout: docs
page_title: 'nomad windows service install command reference'
description: |
The `nomad windows service install` command installs the Nomad binary
and creates a Windows service.
---
# `nomad windows service install` command reference
The `windows service install` command installs the Nomad binary and
creates a Windows service.
## Usage
```plaintext
nomad windows service install
```
The `windows service install` command installs the `nomad` binary used to
run this command, creates a data and configuration directory, writes a basic
Nomad configuration file, creates a Windows service to run Nomad, and
registers the service with Windows Event Log.
If Nomad has been previously installed using this command, subsequent
executions will do the following:
1. Stop the service if it is running
1. Install the currently executing nomad binary
1. Ensure data and configuration directories exist
1. Write a configuration file if no configuration files are found
1. Update the service if needed
1. Update the Event Log configuration if needed.
## Options
- `-config-dir <dir>`: Directory to hold the Nomad agent configuration.
Defaults to "{{.ProgramFiles}}\HashiCorp\nomad\bin"
- `-data-dir <dir>`: Directory to hold the Nomad agent state. Defaults
to "{{.ProgramData}}\HashiCorp\nomad\data"
- `-install-dir <dir>`: Directory to install the Nomad binary. Defaults
to "{{.ProgramData}}\HashiCorp\nomad\config"
- `-reinstall`: Allow the nomad Windows service to be stopped during install.
## General options
@include 'general_options.mdx'

View File

@@ -0,0 +1,22 @@
---
layout: docs
page_title: 'nomad windows service uninstall command reference'
description: |
The `nomad windows service uninstall` command removes the Nomad
Windows service.
---
# `nomad windows service uninstall` command reference
The `windows service uninstall` command removes the Nomad Windows service.
## Usage
```plaintext
nomad windows service uninstall
```
The `windows service uninstall` command stops the Nomad service if
it is currently running, deregisters the service with the Windows Event Log,
and removes the Windows service. This command does not remove the installed
Nomad binary or the data and configuration directories.

View File

@@ -176,6 +176,16 @@ testing.
This option only works on Unix based systems. The log level inherits from
the Nomad agent log set in `log_level`
- `eventlog` - This is a nested object that configures the behavior with
with Windows Event Log. The following parameters are available:
- `enabled` - Enable sending Nomad agent logs to the Windows Event Log.
- `level` - `(string: "ERROR")` - Specifies the verbosity of logs the Nomad
agent outputs. Valid log levels include `ERROR`, `WARN`, or `INFO` in
increasing order of verbosity. Level must be of equal or less verbosity as
defined for the [`log_level`](#log_level) parameter.
- `http_api_response_headers` `(map<string|string>: nil)` - Specifies
user-defined headers to add to the HTTP API responses.

View File

@@ -7,9 +7,12 @@ description: |-
# Install Nomad as a Windows service
Nomad can be run as a native Windows service. In order to do this, you will need
to register the Nomad application with the Windows Service Control Manager using
[`sc.exe`], configure Nomad to log to a file, and then start the Nomad service.
You may run Nomad as a native Windows service. Use the [windows service install][]
command to install Nomad and create the Windows service.
You may also set up the Nomad Windows service manually. Use [`sc.exe`] to register
the Nomad application with the Windows Service Control Manager, configure Nomad to
log to a file, and then start the Nomad service.
~> **Note:** These steps should be run in a PowerShell session with Administrator
capabilities.
@@ -93,3 +96,4 @@ restart of Nomad service is not sufficient.
[`sc.exe`]: https://msdn.microsoft.com/en-us/library/windows/desktop/ms682107(v=vs.85).aspx
[download]: /nomad/downloads
[logging]: /nomad/docs/configuration#log_file
[windows service install]: /nomad/docs/commands/windows/service-install

View File

@@ -1053,5 +1053,22 @@
"path": "volume/status"
}
]
},
{
"title": "windows",
"routes": [
{
"title": "Overview",
"path": "windows"
},
{
"title": "service install",
"path": "windows/service-install"
},
{
"title": "service uninstall",
"path": "windows/service-uninstall"
}
]
}
]