diff --git a/.changelog/26441.txt b/.changelog/26441.txt new file mode 100644 index 000000000..436afbf2f --- /dev/null +++ b/.changelog/26441.txt @@ -0,0 +1,3 @@ +```release-note:improvement +agent: Allow agent logging to the Windows Event Log +``` diff --git a/.changelog/26442.txt b/.changelog/26442.txt new file mode 100644 index 000000000..91691cf61 --- /dev/null +++ b/.changelog/26442.txt @@ -0,0 +1,3 @@ +```release-note:improvement +cli: Add commands for installing and uninstalling Windows system service +``` diff --git a/command/commands.go b/command/commands.go index 14ec45b67..62f62b3e3 100644 --- a/command/commands.go +++ b/command/commands.go @@ -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{ diff --git a/command/windows.go b/command/windows.go new file mode 100644 index 000000000..f86166367 --- /dev/null +++ b/command/windows.go @@ -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 [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 } diff --git a/command/windows_service.go b/command/windows_service.go new file mode 100644 index 000000000..f67df7de4 --- /dev/null +++ b/command/windows_service.go @@ -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 [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 } diff --git a/command/windows_service_install.go b/command/windows_service_install.go new file mode 100644 index 000000000..612c5011b --- /dev/null +++ b/command/windows_service_install.go @@ -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 + Directory to hold the Nomad agent configuration. Defaults + to "{{.ProgramFiles}}\HashiCorp\nomad\bin" + + -data-dir + Directory to hold the Nomad agent state. Defaults + to "{{.ProgramData}}\HashiCorp\nomad\data" + + -install-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 +} diff --git a/command/windows_service_install_test.go b/command/windows_service_install_test.go new file mode 100644 index 000000000..a1120be40 --- /dev/null +++ b/command/windows_service_install_test.go @@ -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) +} diff --git a/command/windows_service_uninstall.go b/command/windows_service_uninstall.go new file mode 100644 index 000000000..a0573cdf4 --- /dev/null +++ b/command/windows_service_uninstall.go @@ -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 +} diff --git a/command/windows_service_uninstall_test.go b/command/windows_service_uninstall_test.go new file mode 100644 index 000000000..f927982b6 --- /dev/null +++ b/command/windows_service_uninstall_test.go @@ -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() + }) + } +} diff --git a/helper/winsvc/mock_windows_service.go b/helper/winsvc/mock_windows_service.go new file mode 100644 index 000000000..74f046a88 --- /dev/null +++ b/helper/winsvc/mock_windows_service.go @@ -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))) +} diff --git a/website/content/commands/agent.mdx b/website/content/commands/agent.mdx index 94f6b5fd7..ebc840ab1 100644 --- a/website/content/commands/agent.mdx +++ b/website/content/commands/agent.mdx @@ -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 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 diff --git a/website/content/commands/windows/index.mdx b/website/content/commands/windows/index.mdx new file mode 100644 index 000000000..ea165c3f3 --- /dev/null +++ b/website/content/commands/windows/index.mdx @@ -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 [options]` + +Run `nomad windows -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 diff --git a/website/content/commands/windows/service-install.mdx b/website/content/commands/windows/service-install.mdx new file mode 100644 index 000000000..d10904bac --- /dev/null +++ b/website/content/commands/windows/service-install.mdx @@ -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 `: Directory to hold the Nomad agent configuration. + Defaults to "{{.ProgramFiles}}\HashiCorp\nomad\bin" + +- `-data-dir `: Directory to hold the Nomad agent state. Defaults + to "{{.ProgramData}}\HashiCorp\nomad\data" + +- `-install-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' diff --git a/website/content/commands/windows/service-uninstall.mdx b/website/content/commands/windows/service-uninstall.mdx new file mode 100644 index 000000000..f2f560f65 --- /dev/null +++ b/website/content/commands/windows/service-uninstall.mdx @@ -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. diff --git a/website/content/docs/configuration/index.mdx b/website/content/docs/configuration/index.mdx index fce0cb238..5842eb6f7 100644 --- a/website/content/docs/configuration/index.mdx +++ b/website/content/docs/configuration/index.mdx @@ -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: nil)` - Specifies user-defined headers to add to the HTTP API responses. diff --git a/website/content/docs/deploy/production/windows-service.mdx b/website/content/docs/deploy/production/windows-service.mdx index 1b8fa6eac..a56c7ab56 100644 --- a/website/content/docs/deploy/production/windows-service.mdx +++ b/website/content/docs/deploy/production/windows-service.mdx @@ -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. @@ -23,7 +26,7 @@ argument should include the fully qualified path to the Nomad executable and any arguments to the nomad command: agent, -config, etc. ```plaintext -sc.exe create "Nomad" binPath="«full path to nomad.exe» agent -config=«path to config file or directory»" start= auto +sc.exe create "Nomad" binPath="«full path to nomad.exe» agent -config=«path to config file or directory»" start=auto [SC] CreateService SUCCESS ``` @@ -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 diff --git a/website/data/commands-nav-data.json b/website/data/commands-nav-data.json index d0c37d599..6e9b0a794 100644 --- a/website/data/commands-nav-data.json +++ b/website/data/commands-nav-data.json @@ -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" + } + ] } ]