Files
nomad/command/windows_service_install.go
Chris Roberts c3dcdb5413 [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.
2025-09-02 16:40:35 -07:00

366 lines
11 KiB
Go

// 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
}