mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
Merge pull request #26440 from hashicorp/f-winsvc-service
Add Windows service commands and Event Log support
This commit is contained in:
3
.changelog/26441.txt
Normal file
3
.changelog/26441.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
```release-note:improvement
|
||||
agent: Allow agent logging to the Windows Event Log
|
||||
```
|
||||
3
.changelog/26442.txt
Normal file
3
.changelog/26442.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
```release-note:improvement
|
||||
cli: Add commands for installing and uninstalling Windows system service
|
||||
```
|
||||
@@ -13,6 +13,7 @@ project {
|
||||
"ui/node_modules",
|
||||
"pnpm-workspace.yaml",
|
||||
"pnpm-lock.yaml",
|
||||
"helper/winsvc/strings_*.go",
|
||||
|
||||
// Enterprise files do not fall under the open source licensing. CE-ENT
|
||||
// merge conflicts might happen here, please be sure to put new CE
|
||||
|
||||
@@ -75,6 +75,7 @@ func (c *Command) readConfig() *Config {
|
||||
ACL: &ACLConfig{},
|
||||
Audit: &config.AuditConfig{},
|
||||
Reporting: &config.ReportingConfig{},
|
||||
Eventlog: &Eventlog{},
|
||||
}
|
||||
|
||||
flags := flag.NewFlagSet("agent", flag.ContinueOnError)
|
||||
@@ -132,6 +133,10 @@ func (c *Command) readConfig() *Config {
|
||||
flags.BoolVar(&cmdConfig.LogIncludeLocation, "log-include-location", false, "")
|
||||
flags.StringVar(&cmdConfig.NodeName, "node", "", "")
|
||||
|
||||
// Eventlog options
|
||||
flags.BoolVar(&cmdConfig.Eventlog.Enabled, "eventlog", false, "")
|
||||
flags.StringVar(&cmdConfig.Eventlog.Level, "eventlog-level", "", "")
|
||||
|
||||
// Consul options
|
||||
defaultConsul := cmdConfig.defaultConsul()
|
||||
flags.StringVar(&defaultConsul.Auth, "consul-auth", "", "")
|
||||
@@ -489,7 +494,12 @@ func (c *Command) IsValidConfig(config, cmdConfig *Config) bool {
|
||||
return false
|
||||
}
|
||||
if err := config.RPC.Validate(); err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("rpc block invalid: %v)", err))
|
||||
c.Ui.Error(fmt.Sprintf("rpc block invalid: %v", err))
|
||||
return false
|
||||
}
|
||||
|
||||
if err := config.Eventlog.Validate(); err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("eventlog block invalid: %v", err))
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -587,6 +597,7 @@ func SetupLoggers(ui cli.Ui, config *Config) (*gatedwriter.Writer, io.Writer) {
|
||||
if logLevel == "OFF" {
|
||||
config.EnableSyslog = false
|
||||
}
|
||||
|
||||
// Check if syslog is enabled
|
||||
if config.EnableSyslog {
|
||||
ui.Output(fmt.Sprintf("Config enable_syslog is `true` with log_level=%v", config.LogLevel))
|
||||
@@ -598,6 +609,17 @@ func SetupLoggers(ui cli.Ui, config *Config) (*gatedwriter.Writer, io.Writer) {
|
||||
writers = append(writers, newSyslogWriter(l, config.LogJson))
|
||||
}
|
||||
|
||||
// Check if eventlog is enabled
|
||||
if config.Eventlog != nil && config.Eventlog.Enabled {
|
||||
l, err := winsvc.NewEventLogger(config.Eventlog.Level)
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf("Windows event logger setup failed: %s", err))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
writers = append(writers, l)
|
||||
}
|
||||
|
||||
// Check if file logging is enabled
|
||||
if config.LogFile != "" {
|
||||
dir, fileName := filepath.Split(config.LogFile)
|
||||
@@ -773,6 +795,8 @@ func (c *Command) AutocompleteFlags() complete.Flags {
|
||||
"-vault-tls-server-name": complete.PredictAnything,
|
||||
"-acl-enabled": complete.PredictNothing,
|
||||
"-acl-replication-token": complete.PredictAnything,
|
||||
"-eventlog": complete.PredictNothing,
|
||||
"-eventlog-level": complete.PredictSet("INFO", "WARN", "ERROR"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -920,6 +944,10 @@ func (c *Command) Run(args []string) int {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Add events for the eventlog
|
||||
winsvc.SendEvent(winsvc.NewEvent(winsvc.EventServiceReady))
|
||||
defer func() { winsvc.SendEvent(winsvc.NewEvent(winsvc.EventServiceStopped)) }()
|
||||
|
||||
// Wait for exit
|
||||
return c.handleSignals()
|
||||
}
|
||||
@@ -1481,6 +1509,14 @@ General Options (clients and servers):
|
||||
-log-include-location
|
||||
Include file and line information in each log line. The default is false.
|
||||
|
||||
-eventlog
|
||||
Enable sending Nomad agent logs to the Windows Event Log.
|
||||
|
||||
-eventlog-level
|
||||
Specifies the verbosity of logs the Nomad agent outputs. Valid log levels
|
||||
include ERROR, WARN, or INFO in order of verbosity. Level must be
|
||||
of equal or less verbosity as defined for the -log-level parameter.
|
||||
|
||||
-node=<name>
|
||||
The name of the local agent. This name is used to identify the node
|
||||
in the cluster. The name must be unique per region. The default is
|
||||
|
||||
@@ -77,6 +77,10 @@ func TestCommand_Args(t *testing.T) {
|
||||
[]string{"-client", "-node-pool=not@valid"},
|
||||
"Invalid node pool",
|
||||
},
|
||||
{
|
||||
[]string{"-client", "-eventlog-level", "DEBUG"},
|
||||
"eventlog.level must be one of INFO, WARN, or ERROR",
|
||||
},
|
||||
}
|
||||
for _, tc := range tcases {
|
||||
// Make a new command. We preemptively close the shutdownCh
|
||||
@@ -502,6 +506,33 @@ func TestIsValidConfig(t *testing.T) {
|
||||
},
|
||||
err: "unknown keyring provider",
|
||||
},
|
||||
{
|
||||
name: "ValidEventlog",
|
||||
conf: Config{
|
||||
DataDir: "/tmp",
|
||||
Client: &ClientConfig{
|
||||
Enabled: true,
|
||||
},
|
||||
Eventlog: &Eventlog{
|
||||
Enabled: true,
|
||||
Level: "INFO",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "InvalidEventlog",
|
||||
conf: Config{
|
||||
DataDir: "/tmp",
|
||||
Client: &ClientConfig{
|
||||
Enabled: true,
|
||||
},
|
||||
Eventlog: &Eventlog{
|
||||
Enabled: true,
|
||||
Level: "DEBUG",
|
||||
},
|
||||
},
|
||||
err: "eventlog.level must be one of INFO, WARN, or ERROR",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/hashicorp/nomad/helper/ipaddr"
|
||||
"github.com/hashicorp/nomad/helper/pointer"
|
||||
"github.com/hashicorp/nomad/helper/users"
|
||||
"github.com/hashicorp/nomad/helper/winsvc"
|
||||
"github.com/hashicorp/nomad/nomad"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/hashicorp/nomad/nomad/structs/config"
|
||||
@@ -199,6 +200,9 @@ type Config struct {
|
||||
|
||||
// ExtraKeysHCL is used by hcl to surface unexpected keys
|
||||
ExtraKeysHCL []string `hcl:",unusedKeys" json:"-"`
|
||||
|
||||
// Configure logging to Windows eventlog
|
||||
Eventlog *Eventlog `hcl:"eventlog"`
|
||||
}
|
||||
|
||||
func (c *Config) defaultConsul() *config.ConsulConfig {
|
||||
@@ -1399,6 +1403,60 @@ func (t *Telemetry) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Eventlog is the configuration for the Windows Eventlog
|
||||
type Eventlog struct {
|
||||
// Enabled controls if Nomad agent logs are sent to the
|
||||
// Windows eventlog.
|
||||
Enabled bool `hcl:"enabled"`
|
||||
// Level of logs to send to eventlog. May be set to higher
|
||||
// severity than LogLevel but lower level will be ignored.
|
||||
Level string `hcl:"level"`
|
||||
}
|
||||
|
||||
// Copy is used to copy the Eventlog configuration
|
||||
func (e *Eventlog) Copy() *Eventlog {
|
||||
return &Eventlog{
|
||||
Enabled: e.Enabled,
|
||||
Level: e.Level,
|
||||
}
|
||||
}
|
||||
|
||||
// Merge is used to merge Eventlog configurations
|
||||
func (e *Eventlog) Merge(b *Eventlog) *Eventlog {
|
||||
if e == nil {
|
||||
return b
|
||||
}
|
||||
|
||||
result := *e
|
||||
|
||||
if b == nil {
|
||||
return &result
|
||||
}
|
||||
|
||||
if b.Enabled {
|
||||
result.Enabled = b.Enabled
|
||||
}
|
||||
|
||||
if b.Level != "" {
|
||||
result.Level = b.Level
|
||||
}
|
||||
|
||||
return &result
|
||||
}
|
||||
|
||||
// Validate validates the eventlog configuration
|
||||
func (e *Eventlog) Validate() error {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if winsvc.EventlogLevelFromString(e.Level) == winsvc.EVENTLOG_LEVEL_UNKNOWN {
|
||||
return errors.New("eventlog.level must be one of INFO, WARN, or ERROR")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ports encapsulates the various ports we bind to for network services. If any
|
||||
// are not specified then the defaults are used instead.
|
||||
type Ports struct {
|
||||
@@ -1732,6 +1790,10 @@ func DefaultConfig() *Config {
|
||||
collectionInterval: 1 * time.Second,
|
||||
DisableAllocationHookMetrics: pointer.Of(false),
|
||||
},
|
||||
Eventlog: &Eventlog{
|
||||
Enabled: false,
|
||||
Level: "error",
|
||||
},
|
||||
TLSConfig: &config.TLSConfig{},
|
||||
Sentinel: &config.SentinelConfig{},
|
||||
Version: version.GetVersion(),
|
||||
@@ -1844,6 +1906,13 @@ func (c *Config) Merge(b *Config) *Config {
|
||||
result.Telemetry = result.Telemetry.Merge(b.Telemetry)
|
||||
}
|
||||
|
||||
// Apply the eventlog config
|
||||
if result.Eventlog == nil && b.Eventlog != nil {
|
||||
result.Eventlog = b.Eventlog.Copy()
|
||||
} else if b.Eventlog != nil {
|
||||
result.Eventlog = result.Eventlog.Merge(b.Eventlog)
|
||||
}
|
||||
|
||||
// Apply the Reporting Config
|
||||
if result.Reporting == nil && b.Reporting != nil {
|
||||
result.Reporting = b.Reporting.Copy()
|
||||
|
||||
@@ -48,6 +48,7 @@ func TestConfig_Merge(t *testing.T) {
|
||||
AdvertiseAddrs: &AdvertiseAddrs{},
|
||||
Sentinel: &config.SentinelConfig{},
|
||||
Autopilot: &config.AutopilotConfig{},
|
||||
Eventlog: &Eventlog{},
|
||||
}
|
||||
|
||||
c2 := &Config{
|
||||
@@ -236,6 +237,10 @@ func TestConfig_Merge(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
Eventlog: &Eventlog{
|
||||
Enabled: true,
|
||||
Level: "INFO",
|
||||
},
|
||||
}
|
||||
|
||||
c3 := &Config{
|
||||
@@ -489,6 +494,10 @@ func TestConfig_Merge(t *testing.T) {
|
||||
Enabled: pointer.Of(true),
|
||||
},
|
||||
},
|
||||
Eventlog: &Eventlog{
|
||||
Enabled: true,
|
||||
Level: "ERROR",
|
||||
},
|
||||
}
|
||||
|
||||
result := c0.Merge(c1)
|
||||
@@ -2013,3 +2022,86 @@ func TestConfig_LoadClientNodeMaxAllocs(t *testing.T) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestEventlog_Merge(t *testing.T) {
|
||||
t.Run("nil rhs merge", func(t *testing.T) {
|
||||
var c1, c2 *Eventlog
|
||||
c1 = &Eventlog{
|
||||
Enabled: true,
|
||||
Level: "info",
|
||||
}
|
||||
result := c1.Merge(c2)
|
||||
must.Eq(t, result, c1)
|
||||
})
|
||||
|
||||
t.Run("nil lhs merge", func(t *testing.T) {
|
||||
var c1, c2 *Eventlog
|
||||
c2 = &Eventlog{
|
||||
Enabled: true,
|
||||
Level: "info",
|
||||
}
|
||||
result := c1.Merge(c2)
|
||||
must.Eq(t, result, c2)
|
||||
})
|
||||
|
||||
t.Run("full merge", func(t *testing.T) {
|
||||
c1 := &Eventlog{
|
||||
Enabled: false,
|
||||
Level: "info",
|
||||
}
|
||||
c2 := &Eventlog{
|
||||
Enabled: true,
|
||||
Level: "error",
|
||||
}
|
||||
result := c1.Merge(c2)
|
||||
must.True(t, result.Enabled)
|
||||
must.Eq(t, result.Level, "error")
|
||||
})
|
||||
|
||||
t.Run("enabled merge", func(t *testing.T) {
|
||||
// NOTE: Can only be enabled, not disabled
|
||||
c1 := &Eventlog{
|
||||
Enabled: true,
|
||||
}
|
||||
c2 := &Eventlog{
|
||||
Enabled: false,
|
||||
}
|
||||
result := c1.Merge(c2)
|
||||
must.True(t, result.Enabled)
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func TestEventlog_Validate(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
testCases := []struct {
|
||||
desc string
|
||||
eventlog *Eventlog
|
||||
shouldErr bool
|
||||
}{
|
||||
{
|
||||
desc: "valid level",
|
||||
eventlog: &Eventlog{Level: "info"},
|
||||
},
|
||||
{
|
||||
desc: "invalid level",
|
||||
eventlog: &Eventlog{Level: "debug"},
|
||||
shouldErr: true,
|
||||
},
|
||||
{
|
||||
desc: "nil eventlog",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
if tc.shouldErr {
|
||||
must.Error(t, tc.eventlog.Validate())
|
||||
} else {
|
||||
must.NoError(t, tc.eventlog.Validate())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
35
command/windows.go
Normal 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 }
|
||||
41
command/windows_service.go
Normal file
41
command/windows_service.go
Normal 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 }
|
||||
365
command/windows_service_install.go
Normal file
365
command/windows_service_install.go
Normal 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
|
||||
}
|
||||
391
command/windows_service_install_test.go
Normal file
391
command/windows_service_install_test.go
Normal 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)
|
||||
}
|
||||
121
command/windows_service_uninstall.go
Normal file
121
command/windows_service_uninstall.go
Normal 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
|
||||
}
|
||||
146
command/windows_service_uninstall_test.go
Normal file
146
command/windows_service_uninstall_test.go
Normal 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()
|
||||
})
|
||||
}
|
||||
}
|
||||
73
helper/winsvc/event.go
Normal file
73
helper/winsvc/event.go
Normal file
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package winsvc
|
||||
|
||||
type WindowsEventId uint32
|
||||
|
||||
//go:generate stringer -trimprefix=Event -output strings_eventid.go -linecomment -type=WindowsEventId
|
||||
const (
|
||||
EventUnknown WindowsEventId = iota // unknown event
|
||||
EventServiceStarting // service starting
|
||||
EventServiceReady // service ready
|
||||
EventServiceStopped // service stopped
|
||||
EventLogMessage // log message
|
||||
)
|
||||
|
||||
// NewEvent creates a new Event for the Windows Eventlog
|
||||
func NewEvent(kind WindowsEventId, opts ...EventOption) Event {
|
||||
evt := &event{
|
||||
kind: kind,
|
||||
level: EVENTLOG_LEVEL_INFO,
|
||||
}
|
||||
|
||||
for _, fn := range opts {
|
||||
fn(evt)
|
||||
}
|
||||
|
||||
return evt
|
||||
}
|
||||
|
||||
type Event interface {
|
||||
Kind() WindowsEventId
|
||||
Message() string
|
||||
Level() EventlogLevel
|
||||
}
|
||||
|
||||
type EventOption func(*event)
|
||||
|
||||
// WithEventMessage sets a custom message for the event
|
||||
func WithEventMessage(msg string) EventOption {
|
||||
return func(e *event) {
|
||||
e.message = msg
|
||||
}
|
||||
}
|
||||
|
||||
// WithEventLevel specifies the level used for the event
|
||||
func WithEventLevel(level EventlogLevel) EventOption {
|
||||
return func(e *event) {
|
||||
e.level = level
|
||||
}
|
||||
}
|
||||
|
||||
type event struct {
|
||||
kind WindowsEventId
|
||||
message string
|
||||
level EventlogLevel
|
||||
}
|
||||
|
||||
func (e *event) Kind() WindowsEventId {
|
||||
return e.kind
|
||||
}
|
||||
|
||||
func (e *event) Message() string {
|
||||
if e.message != "" {
|
||||
return e.message
|
||||
}
|
||||
|
||||
return e.kind.String()
|
||||
}
|
||||
|
||||
func (e *event) Level() EventlogLevel {
|
||||
return e.level
|
||||
}
|
||||
106
helper/winsvc/event_logger.go
Normal file
106
helper/winsvc/event_logger.go
Normal file
@@ -0,0 +1,106 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package winsvc
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type EventlogLevel uint8
|
||||
|
||||
//go:generate stringer -trimprefix=EVENTLOG_LEVEL_ -output strings_event_logger.go -linecomment -type=EventlogLevel
|
||||
const (
|
||||
EVENTLOG_LEVEL_UNKNOWN EventlogLevel = iota
|
||||
EVENTLOG_LEVEL_INFO
|
||||
EVENTLOG_LEVEL_WARN
|
||||
EVENTLOG_LEVEL_ERROR
|
||||
)
|
||||
|
||||
// EventlogLevelFromString converts a log level string to the correct constant
|
||||
func EventlogLevelFromString(level string) EventlogLevel {
|
||||
switch strings.ToUpper(level) {
|
||||
case EVENTLOG_LEVEL_INFO.String():
|
||||
return EVENTLOG_LEVEL_INFO
|
||||
case EVENTLOG_LEVEL_WARN.String():
|
||||
return EVENTLOG_LEVEL_WARN
|
||||
case EVENTLOG_LEVEL_ERROR.String():
|
||||
return EVENTLOG_LEVEL_ERROR
|
||||
}
|
||||
|
||||
return EVENTLOG_LEVEL_UNKNOWN
|
||||
}
|
||||
|
||||
var logPattern = regexp.MustCompile(`(?s)\[(ERROR|WARN|INFO)\] (.+)`)
|
||||
|
||||
type Eventlog interface {
|
||||
Info(uint32, string) error
|
||||
Warning(uint32, string) error
|
||||
Error(uint32, string) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
type eventLogger struct {
|
||||
evtLog Eventlog
|
||||
level EventlogLevel
|
||||
}
|
||||
|
||||
// Close closes the eventlog
|
||||
func (e *eventLogger) Close() error {
|
||||
return e.evtLog.Close()
|
||||
}
|
||||
|
||||
// Write writes logging message to the eventlog
|
||||
func (e *eventLogger) Write(p []byte) (int, error) {
|
||||
matches := logPattern.FindStringSubmatch(string(p))
|
||||
|
||||
// If no match was found, or the incorrect number of
|
||||
// elements detected then ignore
|
||||
if matches == nil || len(matches) != 3 {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
level := EventlogLevelFromString(matches[1])
|
||||
|
||||
// If the detected level of the message isn't currently
|
||||
// allowed then ignore
|
||||
if !e.allowed(level) {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// Still here so send the message to the eventlog
|
||||
switch level {
|
||||
case EVENTLOG_LEVEL_INFO:
|
||||
e.evtLog.Info(uint32(EventLogMessage), matches[2])
|
||||
case EVENTLOG_LEVEL_WARN:
|
||||
e.evtLog.Warning(uint32(EventLogMessage), matches[2])
|
||||
case EVENTLOG_LEVEL_ERROR:
|
||||
e.evtLog.Error(uint32(EventLogMessage), matches[2])
|
||||
}
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// Check if level is allowed
|
||||
func (e *eventLogger) allowed(level EventlogLevel) bool {
|
||||
return level >= e.level
|
||||
}
|
||||
|
||||
type nullEventlog struct{}
|
||||
|
||||
func (n *nullEventlog) Info(uint32, string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *nullEventlog) Warning(uint32, string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *nullEventlog) Error(uint32, string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *nullEventlog) Close() error {
|
||||
return nil
|
||||
}
|
||||
17
helper/winsvc/event_logger_nonwindows.go
Normal file
17
helper/winsvc/event_logger_nonwindows.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build !windows
|
||||
|
||||
package winsvc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
// NewEventLogger is a stub for non-Windows platforms to generate
|
||||
// and error when used.
|
||||
func NewEventLogger(_ string) (io.WriteCloser, error) {
|
||||
return nil, errors.New("EventLogger is not supported on this platform")
|
||||
}
|
||||
114
helper/winsvc/event_logger_test.go
Normal file
114
helper/winsvc/event_logger_test.go
Normal file
@@ -0,0 +1,114 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package winsvc
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/shoenig/test/must"
|
||||
)
|
||||
|
||||
func testEventLogger(e Eventlog, l EventlogLevel) io.WriteCloser {
|
||||
return &eventLogger{
|
||||
level: l,
|
||||
evtLog: e,
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventlogLevelFromString(t *testing.T) {
|
||||
t.Run("INFO", func(t *testing.T) {
|
||||
for _, val := range []string{"INFO", "info"} {
|
||||
l := EventlogLevelFromString(val)
|
||||
must.Eq(t, EVENTLOG_LEVEL_INFO, l)
|
||||
}
|
||||
})
|
||||
t.Run("WARN", func(t *testing.T) {
|
||||
for _, val := range []string{"WARN", "warn"} {
|
||||
l := EventlogLevelFromString(val)
|
||||
must.Eq(t, EVENTLOG_LEVEL_WARN, l)
|
||||
}
|
||||
})
|
||||
t.Run("ERROR", func(t *testing.T) {
|
||||
for _, val := range []string{"ERROR", "error"} {
|
||||
l := EventlogLevelFromString(val)
|
||||
must.Eq(t, EVENTLOG_LEVEL_ERROR, l)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestEventLogger(t *testing.T) {
|
||||
defaultmsgs := []string{
|
||||
"1970-01-01T16:27:16.116Z [INFO] Information line",
|
||||
"1970-01-01T16:27:16.116Z [WARN] Warning line",
|
||||
"1970-01-01T16:27:16.116Z [ERROR] Error line",
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
msgs []string
|
||||
level EventlogLevel
|
||||
setup func(*MockEventlog)
|
||||
}{
|
||||
{
|
||||
desc: "basic usage",
|
||||
level: EVENTLOG_LEVEL_INFO,
|
||||
setup: func(m *MockEventlog) {
|
||||
m.ExpectInfo(EventLogMessage, "Information line")
|
||||
m.ExpectWarning(EventLogMessage, "Warning line")
|
||||
m.ExpectError(EventLogMessage, "Error line")
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "higher level",
|
||||
level: EVENTLOG_LEVEL_ERROR,
|
||||
setup: func(m *MockEventlog) {
|
||||
m.ExpectError(EventLogMessage, "Error line")
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "debug and trace logs",
|
||||
level: EVENTLOG_LEVEL_INFO,
|
||||
setup: func(m *MockEventlog) {
|
||||
m.ExpectInfo(EventLogMessage, "Information line")
|
||||
m.ExpectWarning(EventLogMessage, "Warning line")
|
||||
m.ExpectError(EventLogMessage, "Error line")
|
||||
},
|
||||
msgs: append(defaultmsgs, []string{
|
||||
"[DEBUG] Debug line",
|
||||
"[TRACE] Trace line",
|
||||
}...),
|
||||
},
|
||||
{
|
||||
desc: "with multi-line logs",
|
||||
level: EVENTLOG_LEVEL_INFO,
|
||||
setup: func(m *MockEventlog) {
|
||||
m.ExpectInfo(EventLogMessage, "Information line")
|
||||
m.ExpectWarning(EventLogMessage, "Warning line")
|
||||
m.ExpectError(EventLogMessage, "Error line")
|
||||
m.ExpectInfo(EventLogMessage, "Information log\nthat includes\nmultiple lines")
|
||||
m.ExpectWarning(EventLogMessage, "Warning log\nthat includes second line")
|
||||
},
|
||||
msgs: append(defaultmsgs, []string{
|
||||
"[INFO] Information log\nthat includes\nmultiple lines",
|
||||
"[WARN] Warning log\nthat includes second line",
|
||||
}...),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
if len(tc.msgs) < 1 {
|
||||
tc.msgs = defaultmsgs
|
||||
}
|
||||
|
||||
el := NewMockEventlog(t)
|
||||
tc.setup(el)
|
||||
eventLogger := testEventLogger(el, tc.level)
|
||||
|
||||
for _, msg := range tc.msgs {
|
||||
eventLogger.Write([]byte(msg))
|
||||
}
|
||||
el.AssertExpectations()
|
||||
}
|
||||
}
|
||||
24
helper/winsvc/event_logger_windows.go
Normal file
24
helper/winsvc/event_logger_windows.go
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package winsvc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"golang.org/x/sys/windows/svc/eventlog"
|
||||
)
|
||||
|
||||
// NewEventLogger creates a new event logger instance
|
||||
func NewEventLogger(level string) (io.WriteCloser, error) {
|
||||
evtLog, err := eventlog.Open(WINDOWS_SERVICE_NAME)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to open Windows eventlog: %w", err)
|
||||
}
|
||||
|
||||
return &eventLogger{
|
||||
evtLog: evtLog,
|
||||
level: EventlogLevelFromString(level),
|
||||
}, nil
|
||||
}
|
||||
43
helper/winsvc/event_test.go
Normal file
43
helper/winsvc/event_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package winsvc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/shoenig/test/must"
|
||||
)
|
||||
|
||||
func TestNewEvent(t *testing.T) {
|
||||
t.Run("simple", func(t *testing.T) {
|
||||
event := NewEvent(EventServiceReady)
|
||||
must.Eq(t, EventServiceReady, event.Kind())
|
||||
must.Eq(t, EVENTLOG_LEVEL_INFO, event.Level())
|
||||
must.Eq(t, EventServiceReady.String(), event.Message())
|
||||
})
|
||||
|
||||
t.Run("WithEventMessage", func(t *testing.T) {
|
||||
event := NewEvent(EventServiceReady, WithEventMessage("Custom service ready message"))
|
||||
must.Eq(t, EventServiceReady, event.Kind())
|
||||
must.Eq(t, EVENTLOG_LEVEL_INFO, event.Level())
|
||||
must.Eq(t, "Custom service ready message", event.Message())
|
||||
})
|
||||
|
||||
t.Run("WithEventLevel", func(t *testing.T) {
|
||||
event := NewEvent(EventServiceReady, WithEventLevel(EVENTLOG_LEVEL_ERROR))
|
||||
must.Eq(t, EventServiceReady, event.Kind())
|
||||
must.Eq(t, EVENTLOG_LEVEL_ERROR, event.Level())
|
||||
must.Eq(t, EventServiceReady.String(), event.Message())
|
||||
})
|
||||
|
||||
t.Run("multiple options", func(t *testing.T) {
|
||||
event := NewEvent(EventServiceStopped,
|
||||
WithEventMessage("Custom service stopped message"),
|
||||
WithEventLevel(EVENTLOG_LEVEL_WARN),
|
||||
)
|
||||
must.Eq(t, EventServiceStopped, event.Kind())
|
||||
must.Eq(t, EVENTLOG_LEVEL_WARN, event.Level())
|
||||
must.Eq(t, "Custom service stopped message", event.Message())
|
||||
})
|
||||
}
|
||||
9
helper/winsvc/events_nonwindows.go
Normal file
9
helper/winsvc/events_nonwindows.go
Normal file
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build !windows
|
||||
|
||||
package winsvc
|
||||
|
||||
// SendEvent sends an event to the Windows eventlog
|
||||
func SendEvent(e Event) {}
|
||||
26
helper/winsvc/events_windows.go
Normal file
26
helper/winsvc/events_windows.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package winsvc
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/nomad/helper"
|
||||
)
|
||||
|
||||
var chanEvents = make(chan Event)
|
||||
|
||||
// SendEvent sends an event to the Windows eventlog
|
||||
func SendEvent(e Event) {
|
||||
timer, stop := helper.NewSafeTimer(100 * time.Millisecond)
|
||||
defer stop()
|
||||
|
||||
select {
|
||||
case chanEvents <- e:
|
||||
case <-timer.C:
|
||||
hclog.L().Error("failed to send event to windows eventlog, timed out",
|
||||
"event", e)
|
||||
}
|
||||
}
|
||||
84
helper/winsvc/mock_eventlog.go
Normal file
84
helper/winsvc/mock_eventlog.go
Normal file
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package winsvc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/shoenig/test/must"
|
||||
)
|
||||
|
||||
func NewMockEventlog(t *testing.T) *MockEventlog {
|
||||
return &MockEventlog{
|
||||
t: t,
|
||||
}
|
||||
}
|
||||
|
||||
type MockEventlog struct {
|
||||
infos []mockArgs
|
||||
warnings []mockArgs
|
||||
errors []mockArgs
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
type mockArgs struct {
|
||||
winId uint32
|
||||
msg string
|
||||
}
|
||||
|
||||
func (m *MockEventlog) ExpectInfo(v1 WindowsEventId, v2 string) {
|
||||
m.infos = append(m.infos, mockArgs{uint32(v1), v2})
|
||||
}
|
||||
|
||||
func (m *MockEventlog) ExpectWarning(v1 WindowsEventId, v2 string) {
|
||||
m.warnings = append(m.warnings, mockArgs{uint32(v1), v2})
|
||||
}
|
||||
|
||||
func (m *MockEventlog) ExpectError(v1 WindowsEventId, v2 string) {
|
||||
m.errors = append(m.errors, mockArgs{uint32(v1), v2})
|
||||
}
|
||||
|
||||
func (m *MockEventlog) Info(v1 uint32, v2 string) error {
|
||||
m.t.Helper()
|
||||
|
||||
expectedArgs := m.infos[0]
|
||||
m.infos = m.infos[1:]
|
||||
|
||||
must.Eq(m.t, expectedArgs.winId, v1, must.Sprint("Incorrect WindowsEventId value"))
|
||||
must.Eq(m.t, expectedArgs.msg, v2, must.Sprint("Incorrect message value"))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockEventlog) Warning(v1 uint32, v2 string) error {
|
||||
m.t.Helper()
|
||||
|
||||
expectedArgs := m.warnings[0]
|
||||
m.warnings = m.warnings[1:]
|
||||
|
||||
must.Eq(m.t, expectedArgs.winId, v1, must.Sprint("Incorrect WindowsEventId value"))
|
||||
must.Eq(m.t, expectedArgs.msg, v2, must.Sprint("Incorrect message value"))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockEventlog) Error(v1 uint32, v2 string) error {
|
||||
m.t.Helper()
|
||||
|
||||
expectedArgs := m.errors[0]
|
||||
m.errors = m.errors[1:]
|
||||
|
||||
must.Eq(m.t, expectedArgs.winId, v1, must.Sprint("Incorrect WindowsEventId value"))
|
||||
must.Eq(m.t, expectedArgs.msg, v2, must.Sprint("Incorrect message value"))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockEventlog) Close() error { return nil }
|
||||
|
||||
func (m *MockEventlog) AssertExpectations() {
|
||||
must.SliceEmpty(m.t, m.infos, must.Sprintf("Info expecting %d more invocations", len(m.infos)))
|
||||
must.SliceEmpty(m.t, m.warnings, must.Sprintf("Warning expecting %d more invocations", len(m.warnings)))
|
||||
must.SliceEmpty(m.t, m.errors, must.Sprintf("Error expecting %d more invocations", len(m.errors)))
|
||||
}
|
||||
320
helper/winsvc/mock_windows_service.go
Normal file
320
helper/winsvc/mock_windows_service.go
Normal 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)))
|
||||
}
|
||||
22
helper/winsvc/path_nonwindows.go
Normal file
22
helper/winsvc/path_nonwindows.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build !windows
|
||||
|
||||
package winsvc
|
||||
|
||||
import "errors"
|
||||
|
||||
func NewWindowsPaths() WindowsPaths {
|
||||
return &windowsPaths{}
|
||||
}
|
||||
|
||||
type windowsPaths struct{}
|
||||
|
||||
func (w *windowsPaths) Expand(string) (string, error) {
|
||||
return "", errors.New("Windows path expansion not supported on this platform")
|
||||
}
|
||||
|
||||
func (w *windowsPaths) CreateDirectory(string, bool) error {
|
||||
return errors.New("Windows directory creation not supported on this platform")
|
||||
}
|
||||
206
helper/winsvc/path_windows.go
Normal file
206
helper/winsvc/path_windows.go
Normal file
@@ -0,0 +1,206 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package winsvc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"text/template"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/registry"
|
||||
)
|
||||
|
||||
func NewWindowsPaths() WindowsPaths {
|
||||
return &windowsPaths{}
|
||||
}
|
||||
|
||||
type windowsPaths struct {
|
||||
SystemRoot string
|
||||
SystemDrive string
|
||||
ProgramData string
|
||||
ProgramFiles string
|
||||
loadErr error
|
||||
o sync.Once
|
||||
}
|
||||
|
||||
func (w *windowsPaths) Expand(path string) (string, error) {
|
||||
if err := w.load(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
tmpl := template.New("expansion").Option("missingkey=error")
|
||||
tmpl, err := tmpl.Parse(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
result := new(bytes.Buffer)
|
||||
if err := tmpl.Execute(result, w); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return result.String(), nil
|
||||
}
|
||||
|
||||
func (w *windowsPaths) CreateDirectory(path string, restrict_on_create bool) error {
|
||||
s, err := os.Stat(path)
|
||||
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
// Directory exists so nothing to do
|
||||
if s.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("path exists and is not directory - %s", path)
|
||||
}
|
||||
|
||||
// NOTE: mode ignored on Windows. If directory should
|
||||
// be restricted, an ACL will be applied below.
|
||||
if err := os.MkdirAll(path, 0o000); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Since the directory was just created, apply access
|
||||
// restrictions if requested
|
||||
if restrict_on_create {
|
||||
if err := setDirectoryPermissions(path); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getUserGroupSIDs() (usid *windows.SID, gsid *windows.SID, err error) {
|
||||
// NOTE: this token is a pseudo-token and does not
|
||||
// need to be closed
|
||||
token := windows.GetCurrentProcessToken()
|
||||
|
||||
userToken, err := token.GetTokenUser()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
usid = userToken.User.Sid
|
||||
|
||||
userGroup, err := token.GetTokenPrimaryGroup()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
gsid = userGroup.PrimaryGroup
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func setDirectoryPermissions(path string) error {
|
||||
// Grab the user and group SID for who is running the process
|
||||
userSid, groupSid, err := getUserGroupSIDs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Generate a SID for the administators group
|
||||
gsid, err := windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create an ACL with an ACE for user SID and an ACE for the
|
||||
// administrators group SID, both of which are granted full
|
||||
// access. No other ACEs are provided which restricts access
|
||||
// from non-administrators
|
||||
dacl, err := windows.ACLFromEntries(
|
||||
[]windows.EXPLICIT_ACCESS{
|
||||
{
|
||||
AccessPermissions: windows.GENERIC_ALL,
|
||||
AccessMode: windows.SET_ACCESS,
|
||||
Inheritance: windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT,
|
||||
Trustee: windows.TRUSTEE{
|
||||
MultipleTrusteeOperation: windows.NO_MULTIPLE_TRUSTEE,
|
||||
TrusteeForm: windows.TRUSTEE_IS_SID,
|
||||
TrusteeType: windows.TRUSTEE_IS_USER,
|
||||
TrusteeValue: windows.TrusteeValueFromSID(userSid),
|
||||
},
|
||||
},
|
||||
{
|
||||
AccessPermissions: windows.GENERIC_ALL,
|
||||
AccessMode: windows.SET_ACCESS,
|
||||
Inheritance: windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT,
|
||||
Trustee: windows.TRUSTEE{
|
||||
MultipleTrusteeOperation: windows.NO_MULTIPLE_TRUSTEE,
|
||||
TrusteeForm: windows.TRUSTEE_IS_SID,
|
||||
TrusteeType: windows.TRUSTEE_IS_WELL_KNOWN_GROUP,
|
||||
TrusteeValue: windows.TrusteeValueFromSID(gsid),
|
||||
},
|
||||
},
|
||||
}, nil,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Apply the ACL to the directory
|
||||
if err := windows.SetNamedSecurityInfo(path, windows.SE_FILE_OBJECT,
|
||||
windows.OWNER_SECURITY_INFORMATION|
|
||||
windows.GROUP_SECURITY_INFORMATION|
|
||||
windows.DACL_SECURITY_INFORMATION|
|
||||
windows.PROTECTED_DACL_SECURITY_INFORMATION,
|
||||
userSid, groupSid, dacl, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *windowsPaths) load() error {
|
||||
w.o.Do(func() {
|
||||
w.SystemDrive = os.Getenv("SystemDrive")
|
||||
if w.SystemDrive == "" {
|
||||
w.loadErr = fmt.Errorf("cannot detect Windows SystemDrive path")
|
||||
return
|
||||
}
|
||||
w.SystemRoot = strings.ReplaceAll(os.Getenv("SystemDrive"), "SystemDrive", w.SystemDrive)
|
||||
|
||||
w.ProgramData = os.Getenv("ProgramData")
|
||||
if w.ProgramData == "" {
|
||||
pdKey, err := registry.OpenKey(registry.LOCAL_MACHINE,
|
||||
`SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList`, registry.QUERY_VALUE)
|
||||
if err == nil {
|
||||
if pdVal, _, err := pdKey.GetStringValue("ProgramData"); err == nil {
|
||||
w.ProgramData = pdVal
|
||||
}
|
||||
}
|
||||
}
|
||||
if w.ProgramData == "" {
|
||||
w.loadErr = fmt.Errorf("cannot detect Windows ProgramData path")
|
||||
return
|
||||
}
|
||||
w.ProgramData = strings.ReplaceAll(w.ProgramData, "SystemDrive", w.SystemDrive)
|
||||
|
||||
w.ProgramFiles = os.Getenv("ProgramFiles")
|
||||
if w.ProgramFiles == "" {
|
||||
pdKey, err := registry.OpenKey(registry.LOCAL_MACHINE,
|
||||
`SOFTWARE\Microsoft\Windows\CurrentVersion`, registry.QUERY_VALUE)
|
||||
if err == nil {
|
||||
if pdVal, _, err := pdKey.GetStringValue("ProgramFilesDir"); err == nil {
|
||||
w.ProgramFiles = pdVal
|
||||
}
|
||||
}
|
||||
}
|
||||
if w.ProgramFiles == "" {
|
||||
w.loadErr = fmt.Errorf("cannot detect Windows ProgramFiles path")
|
||||
return
|
||||
}
|
||||
w.ProgramFiles = strings.ReplaceAll(w.ProgramFiles, "SystemDrive", w.SystemDrive)
|
||||
})
|
||||
|
||||
return w.loadErr
|
||||
}
|
||||
203
helper/winsvc/path_windows_test.go
Normal file
203
helper/winsvc/path_windows_test.go
Normal file
@@ -0,0 +1,203 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package winsvc
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/shoenig/test/must"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func TestCreateDirectory(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
testDir := t.TempDir()
|
||||
|
||||
t.Run("create", func(t *testing.T) {
|
||||
// NOTE: parallel is not set here to force parent
|
||||
// to wait for subtests to complete
|
||||
t.Run("unrestricted", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
path := filepath.Join(testDir, t.Name())
|
||||
|
||||
err := NewWindowsPaths().CreateDirectory(path, false)
|
||||
must.NoError(t, err)
|
||||
|
||||
dacl := getDirectoryDACL(t, path)
|
||||
|
||||
// When not applying restrictions on the new directory, all
|
||||
// ACEs will be inherited from the parent
|
||||
for i := range dacl.AceCount {
|
||||
ace := &windows.ACCESS_ALLOWED_ACE{}
|
||||
must.NoError(t, windows.GetAce(dacl, uint32(i), &ace), must.Sprint("failed to load ACE"))
|
||||
must.Eq(t, windows.INHERITED_ACCESS_ENTRY, ace.Header.AceFlags&windows.INHERITED_ACCESS_ENTRY,
|
||||
must.Sprint("ACE is not inherited"))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("restricted", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
path := filepath.Join(testDir, t.Name())
|
||||
|
||||
err := NewWindowsPaths().CreateDirectory(path, true)
|
||||
must.NoError(t, err)
|
||||
|
||||
dacl := getDirectoryDACL(t, path)
|
||||
matches := map[string]struct{}{}
|
||||
|
||||
// When restrictions are applied on the new directory, all
|
||||
// ACEs will be directly applied.
|
||||
for i := range dacl.AceCount {
|
||||
ace := &windows.ACCESS_ALLOWED_ACE{}
|
||||
must.NoError(t, windows.GetAce(dacl, uint32(i), &ace), must.Sprint("failed to load ACE"))
|
||||
must.NotEq(t, windows.INHERITED_ACCESS_ENTRY, ace.Header.AceFlags&windows.INHERITED_ACCESS_ENTRY,
|
||||
must.Sprint("ACE should not be inherited"))
|
||||
|
||||
if ace.Mask&windows.GENERIC_ALL == windows.GENERIC_ALL {
|
||||
sid := (*windows.SID)(unsafe.Pointer(&ace.SidStart))
|
||||
matches[sid.String()] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// All privileges should be set for user and administrators groups
|
||||
adminGroupSID, err := windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid)
|
||||
must.NoError(t, err, must.Sprint("failed to create well known administrators group SID"))
|
||||
userSID, _, err := getUserGroupSIDs()
|
||||
must.NoError(t, err, must.Sprint("failed to get user SID"))
|
||||
|
||||
must.NotNil(t, matches[userSID.String()], must.Sprint("missing user ACE with GENERIC_ALL"))
|
||||
must.NotNil(t, matches[adminGroupSID.String()],
|
||||
must.Sprint("missing administrators group ACE with GENERIC_ALL"))
|
||||
|
||||
must.Eq(t, 2, len(matches), must.Sprint("unexpected GENERIC_ALL ACEs found"))
|
||||
})
|
||||
|
||||
t.Run("unrestricted already exists", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
path := filepath.Join(testDir, t.Name())
|
||||
must.NoError(t, os.MkdirAll(path, 0o000))
|
||||
|
||||
err := NewWindowsPaths().CreateDirectory(path, false)
|
||||
must.NoError(t, err)
|
||||
|
||||
dacl := getDirectoryDACL(t, path)
|
||||
|
||||
// No restrictions are applied, so check that all ACEs
|
||||
// are inherited from parent
|
||||
for i := range dacl.AceCount {
|
||||
ace := &windows.ACCESS_ALLOWED_ACE{}
|
||||
must.NoError(t, windows.GetAce(dacl, uint32(i), &ace), must.Sprint("failed to load ACE"))
|
||||
must.Eq(t, windows.INHERITED_ACCESS_ENTRY, ace.Header.AceFlags&windows.INHERITED_ACCESS_ENTRY,
|
||||
must.Sprint("ACE is not inherited"))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("restricted already exists", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
path := filepath.Join(testDir, t.Name())
|
||||
must.NoError(t, os.MkdirAll(path, 0o000))
|
||||
|
||||
err := NewWindowsPaths().CreateDirectory(path, true)
|
||||
must.NoError(t, err)
|
||||
|
||||
dacl := getDirectoryDACL(t, path)
|
||||
|
||||
// When the directory already exists, restrictions should not
|
||||
// be applied so validate that all ACEs are inherited
|
||||
for i := range dacl.AceCount {
|
||||
ace := &windows.ACCESS_ALLOWED_ACE{}
|
||||
must.NoError(t, windows.GetAce(dacl, uint32(i), &ace), must.Sprint("failed to load ACE"))
|
||||
must.Eq(t, windows.INHERITED_ACCESS_ENTRY, ace.Header.AceFlags&windows.INHERITED_ACCESS_ENTRY,
|
||||
must.Sprint("ACE is not inherited"))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestExpand(t *testing.T) {
|
||||
t.Run("SystemDrive", func(t *testing.T) {
|
||||
t.Run("default", func(t *testing.T) {
|
||||
result, err := NewWindowsPaths().Expand(`{{.SystemDrive}}/testing`)
|
||||
must.NoError(t, err)
|
||||
must.StrNotContains(t, result, "{{.SystemDrive}}")
|
||||
})
|
||||
t.Run("custom environment variable", func(t *testing.T) {
|
||||
t.Setenv("SystemDrive", `z:`)
|
||||
result, err := NewWindowsPaths().Expand(`{{.SystemDrive}}\testing`)
|
||||
must.NoError(t, err)
|
||||
must.Eq(t, `z:\testing`, result)
|
||||
})
|
||||
t.Run("unset environment variable", func(t *testing.T) {
|
||||
t.Setenv("SystemDrive", "")
|
||||
_, err := NewWindowsPaths().Expand(`{{.SystemDrive}}\testing`)
|
||||
must.ErrorContains(t, err, "cannot detect Windows SystemDrive path")
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("ProgramData", func(t *testing.T) {
|
||||
t.Run("default", func(t *testing.T) {
|
||||
result, err := NewWindowsPaths().Expand(`{{.ProgramData}}/testing`)
|
||||
must.NoError(t, err)
|
||||
must.StrNotContains(t, result, "{{.ProgramData}}")
|
||||
})
|
||||
t.Run("custom environment variable", func(t *testing.T) {
|
||||
t.Setenv("ProgramData", `z:`)
|
||||
result, err := NewWindowsPaths().Expand(`{{.ProgramData}}\testing`)
|
||||
must.NoError(t, err)
|
||||
must.Eq(t, `z:\testing`, result)
|
||||
})
|
||||
t.Run("unset environment variable", func(t *testing.T) {
|
||||
t.Setenv("ProgramData", "")
|
||||
result, err := NewWindowsPaths().Expand(`{{.ProgramData}}\testing`)
|
||||
must.NoError(t, err)
|
||||
must.StrNotContains(t, result, "{{.ProgramData}}") // should be pulled from registry
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("ProgramFiles", func(t *testing.T) {
|
||||
t.Run("default", func(t *testing.T) {
|
||||
result, err := NewWindowsPaths().Expand(`{{.ProgramFiles}}/testing`)
|
||||
must.NoError(t, err)
|
||||
must.StrNotContains(t, result, "{{.ProgramFiles}}")
|
||||
})
|
||||
t.Run("custom environment variable", func(t *testing.T) {
|
||||
t.Setenv("ProgramFiles", `z:`)
|
||||
result, err := NewWindowsPaths().Expand(`{{.ProgramFiles}}\testing`)
|
||||
must.NoError(t, err)
|
||||
must.Eq(t, `z:\testing`, result)
|
||||
})
|
||||
t.Run("unset environment variable", func(t *testing.T) {
|
||||
t.Setenv("ProgramFiles", "")
|
||||
result, err := NewWindowsPaths().Expand(`{{.ProgramFiles}}\testing`)
|
||||
must.NoError(t, err)
|
||||
must.StrNotContains(t, result, "{{.ProgramFiles}}") // should be pulled from registry
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("missing key", func(t *testing.T) {
|
||||
_, err := NewWindowsPaths().Expand(`{{.Unknown}}\testing`)
|
||||
must.ErrorContains(t, err, "can't evaluate field")
|
||||
})
|
||||
}
|
||||
|
||||
func getDirectoryDACL(t *testing.T, path string) *windows.ACL {
|
||||
t.Helper()
|
||||
|
||||
s, err := os.Stat(path)
|
||||
must.NoError(t, err)
|
||||
must.True(t, s.IsDir(), must.Sprint("expected path to be a directory"))
|
||||
|
||||
info, err := windows.GetNamedSecurityInfo(path,
|
||||
windows.SE_FILE_OBJECT, windows.DACL_SECURITY_INFORMATION)
|
||||
must.NoError(t, err, must.Sprint("failed to get path security information"))
|
||||
|
||||
dacl, _, err := info.DACL()
|
||||
must.NoError(t, err, must.Sprint("failed to get path ACL"))
|
||||
|
||||
return dacl
|
||||
}
|
||||
11
helper/winsvc/privileged_nonwindows.go
Normal file
11
helper/winsvc/privileged_nonwindows.go
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build !windows
|
||||
|
||||
package winsvc
|
||||
|
||||
// IsPrivilegedProcess checks if current process is a privileged windows process
|
||||
func IsPrivilegedProcess() bool {
|
||||
return false
|
||||
}
|
||||
11
helper/winsvc/privileged_windows.go
Normal file
11
helper/winsvc/privileged_windows.go
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package winsvc
|
||||
|
||||
import "golang.org/x/sys/windows"
|
||||
|
||||
// IsPrivilegedProcess checks if current process is a privileged windows process
|
||||
func IsPrivilegedProcess() bool {
|
||||
return windows.GetCurrentProcessToken().IsElevated()
|
||||
}
|
||||
@@ -3,10 +3,22 @@
|
||||
|
||||
package winsvc
|
||||
|
||||
var chanGraceExit = make(chan int)
|
||||
const (
|
||||
WINDOWS_SERVICE_NAME = "nomad"
|
||||
WINDOWS_SERVICE_DISPLAY_NAME = "HashiCorp Nomad"
|
||||
WINDOWS_SERVICE_DESCRIPTION = "Workload scheduler and orchestrator - https://nomadproject.io"
|
||||
WINDOWS_INSTALL_BIN_DIRECTORY = `{{.ProgramFiles}}\HashiCorp\nomad\bin`
|
||||
WINDOWS_INSTALL_APPDATA_DIRECTORY = `{{.ProgramData}}\HashiCorp\nomad`
|
||||
|
||||
// Number of seconds to wait for a
|
||||
// service to reach a desired state
|
||||
WINDOWS_SERVICE_STATE_TIMEOUT = "1m"
|
||||
)
|
||||
|
||||
var chanGraceExit = make(chan struct{})
|
||||
|
||||
// ShutdownChannel returns a channel that sends a message that a shutdown
|
||||
// signal has been received for the service.
|
||||
func ShutdownChannel() <-chan int {
|
||||
func ShutdownChannel() <-chan struct{} {
|
||||
return chanGraceExit
|
||||
}
|
||||
|
||||
@@ -1,46 +1,84 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package winsvc
|
||||
|
||||
import (
|
||||
wsvc "golang.org/x/sys/windows/svc"
|
||||
"golang.org/x/sys/windows/svc"
|
||||
"golang.org/x/sys/windows/svc/eventlog"
|
||||
)
|
||||
|
||||
type serviceWindows struct{}
|
||||
// Commands that are currently supported
|
||||
const SERVICE_ACCEPTED_COMMANDS = svc.AcceptStop | svc.AcceptShutdown
|
||||
|
||||
type serviceWindows struct {
|
||||
evtLog Eventlog
|
||||
}
|
||||
|
||||
func init() {
|
||||
interactive, err := wsvc.IsAnInteractiveSession()
|
||||
isSvc, err := svc.IsWindowsService()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Cannot run as a service when running interactively
|
||||
if interactive {
|
||||
// This should only run when running
|
||||
// as a service
|
||||
if !isSvc {
|
||||
return
|
||||
}
|
||||
go wsvc.Run("", serviceWindows{})
|
||||
|
||||
go executeWindowsService()
|
||||
}
|
||||
|
||||
// Execute implements the Windows service Handler type. It will be
|
||||
// called at the start of the service, and the service will exit
|
||||
// once Execute completes.
|
||||
func (serviceWindows) Execute(args []string, r <-chan wsvc.ChangeRequest, s chan<- wsvc.Status) (svcSpecificEC bool, exitCode uint32) {
|
||||
const accCommands = wsvc.AcceptStop | wsvc.AcceptShutdown
|
||||
s <- wsvc.Status{State: wsvc.Running, Accepts: accCommands}
|
||||
func (srv serviceWindows) Execute(args []string, r <-chan svc.ChangeRequest, s chan<- svc.Status) (svcSpecificEC bool, exitCode uint32) {
|
||||
s <- svc.Status{State: svc.Running, Accepts: SERVICE_ACCEPTED_COMMANDS}
|
||||
srv.evtLog.Info(uint32(EventServiceStarting), "service starting")
|
||||
LOOP:
|
||||
for {
|
||||
c := <-r
|
||||
switch c.Cmd {
|
||||
case wsvc.Interrogate:
|
||||
s <- c.CurrentStatus
|
||||
case wsvc.Stop, wsvc.Shutdown:
|
||||
s <- wsvc.Status{State: wsvc.StopPending}
|
||||
chanGraceExit <- 1
|
||||
return false, 0
|
||||
select {
|
||||
case c := <-r:
|
||||
switch c.Cmd {
|
||||
case svc.Interrogate:
|
||||
s <- c.CurrentStatus
|
||||
case svc.Stop, svc.Shutdown:
|
||||
srv.evtLog.Info(uint32(EventLogMessage), "service stop requested")
|
||||
s <- svc.Status{State: svc.StopPending}
|
||||
close(chanGraceExit)
|
||||
}
|
||||
case e := <-chanEvents:
|
||||
switch e.Level() {
|
||||
case EVENTLOG_LEVEL_INFO:
|
||||
srv.evtLog.Info(uint32(e.Kind()), e.Message())
|
||||
case EVENTLOG_LEVEL_WARN:
|
||||
srv.evtLog.Warning(uint32(e.Kind()), e.Message())
|
||||
case EVENTLOG_LEVEL_ERROR:
|
||||
srv.evtLog.Error(uint32(e.Kind()), e.Message())
|
||||
}
|
||||
|
||||
if e.Kind() == EventServiceStopped {
|
||||
break LOOP
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false, 0
|
||||
}
|
||||
|
||||
func executeWindowsService() {
|
||||
var evtLog Eventlog
|
||||
evtLog, err := eventlog.Open(WINDOWS_SERVICE_NAME)
|
||||
if err != nil {
|
||||
// Eventlog will only be available if the
|
||||
// service was properly registered. If the
|
||||
// service was manually setup, it will likely
|
||||
// not have been registered with the eventlog
|
||||
// so it will not be available. In that case
|
||||
// just stub out the eventlog.
|
||||
evtLog = &nullEventlog{}
|
||||
}
|
||||
defer evtLog.Close()
|
||||
|
||||
svc.Run(WINDOWS_SERVICE_NAME, serviceWindows{evtLog: evtLog})
|
||||
}
|
||||
|
||||
26
helper/winsvc/strings_event_logger.go
Normal file
26
helper/winsvc/strings_event_logger.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// Code generated by "stringer -trimprefix=EVENTLOG_LEVEL_ -output strings_event_logger.go -linecomment -type=EventlogLevel"; DO NOT EDIT.
|
||||
|
||||
package winsvc
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[EVENTLOG_LEVEL_UNKNOWN-0]
|
||||
_ = x[EVENTLOG_LEVEL_INFO-1]
|
||||
_ = x[EVENTLOG_LEVEL_WARN-2]
|
||||
_ = x[EVENTLOG_LEVEL_ERROR-3]
|
||||
}
|
||||
|
||||
const _EventlogLevel_name = "UNKNOWNINFOWARNERROR"
|
||||
|
||||
var _EventlogLevel_index = [...]uint8{0, 7, 11, 15, 20}
|
||||
|
||||
func (i EventlogLevel) String() string {
|
||||
if i >= EventlogLevel(len(_EventlogLevel_index)-1) {
|
||||
return "EventlogLevel(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _EventlogLevel_name[_EventlogLevel_index[i]:_EventlogLevel_index[i+1]]
|
||||
}
|
||||
27
helper/winsvc/strings_eventid.go
Normal file
27
helper/winsvc/strings_eventid.go
Normal file
@@ -0,0 +1,27 @@
|
||||
// Code generated by "stringer -trimprefix=Event -output strings_eventid.go -linecomment -type=WindowsEventId"; DO NOT EDIT.
|
||||
|
||||
package winsvc
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[EventUnknown-0]
|
||||
_ = x[EventServiceStarting-1]
|
||||
_ = x[EventServiceReady-2]
|
||||
_ = x[EventServiceStopped-3]
|
||||
_ = x[EventLogMessage-4]
|
||||
}
|
||||
|
||||
const _WindowsEventId_name = "unknown eventservice startingservice readyservice stoppedlog message"
|
||||
|
||||
var _WindowsEventId_index = [...]uint8{0, 13, 29, 42, 57, 68}
|
||||
|
||||
func (i WindowsEventId) String() string {
|
||||
if i >= WindowsEventId(len(_WindowsEventId_index)-1) {
|
||||
return "WindowsEventId(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _WindowsEventId_name[_WindowsEventId_index[i]:_WindowsEventId_index[i+1]]
|
||||
}
|
||||
75
helper/winsvc/windows_service.go
Normal file
75
helper/winsvc/windows_service.go
Normal file
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package winsvc
|
||||
|
||||
type ServiceStartType uint32
|
||||
|
||||
// extracted from https://pkg.go.dev/golang.org/x/sys@v0.35.0/windows/svc/mgr#StartManual
|
||||
const (
|
||||
StartManual ServiceStartType = 3
|
||||
StartAutomatic ServiceStartType = 2
|
||||
StartDisabled ServiceStartType = 4
|
||||
)
|
||||
|
||||
type WindowsServiceConfiguration struct {
|
||||
StartType ServiceStartType
|
||||
DisplayName string
|
||||
Description string
|
||||
BinaryPathName string
|
||||
}
|
||||
|
||||
type WindowsPaths interface {
|
||||
// Expand expands the path defined by the template. Supports
|
||||
// values for:
|
||||
// - SystemDrive
|
||||
// - SystemRoot
|
||||
// - ProgramData
|
||||
// - ProgramFiles
|
||||
Expand(path string) (string, error)
|
||||
|
||||
// Creates a new directory if it does not exist. If directory
|
||||
// is created and restrict_on_create is true, a restrictive
|
||||
// ACL is applied.
|
||||
CreateDirectory(path string, restrict_on_create bool) error
|
||||
}
|
||||
|
||||
type WindowsService interface {
|
||||
// Name returns the name of the service
|
||||
Name() string
|
||||
// Configure applies the configuration to the Windows service.
|
||||
// NOTE: Full configuration applied so empty values will remove existing values.
|
||||
Configure(config WindowsServiceConfiguration) error
|
||||
// Start starts the Windows service and waits for the
|
||||
// service to be running.
|
||||
Start() error
|
||||
// Stop requests the service to stop and waits for the
|
||||
// service to stop.
|
||||
Stop() error
|
||||
// Close closes the connection to the Windows service.
|
||||
Close() error
|
||||
// Delete deletes the Windows service.
|
||||
Delete() error
|
||||
// IsRunning returns if the service is currently running.
|
||||
IsRunning() (bool, error)
|
||||
// IsStopped returns if the service is currently stopped.
|
||||
IsStopped() (bool, error)
|
||||
// EnableEventlog will add or update the Windows Eventlog
|
||||
// configuration for the service. It will set supported
|
||||
// events as info, warning, and error.
|
||||
EnableEventlog() error
|
||||
// DisableEventlog will remove the Windows Eventlog configuration
|
||||
// for the service.
|
||||
DisableEventlog() error
|
||||
}
|
||||
|
||||
type WindowsServiceManager interface {
|
||||
// IsServiceRegistered returns if the service is a registered Windows service.
|
||||
IsServiceRegistered(name string) (bool, error)
|
||||
// GetService opens and returns the named service.
|
||||
GetService(name string) (WindowsService, error)
|
||||
// CreateService creates a new Windows service.
|
||||
CreateService(name, binaryPath string, config WindowsServiceConfiguration) (WindowsService, error)
|
||||
// Close closes Windows service manager connection.
|
||||
Close() error
|
||||
}
|
||||
15
helper/winsvc/windows_service_nonwindows.go
Normal file
15
helper/winsvc/windows_service_nonwindows.go
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build !windows
|
||||
|
||||
package winsvc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
// NewWindowsServiceManager returns an error
|
||||
func NewWindowsServiceManager() (WindowsServiceManager, error) {
|
||||
return nil, errors.New("Windows service manager is not supported on this platform")
|
||||
}
|
||||
256
helper/winsvc/windows_service_windows.go
Normal file
256
helper/winsvc/windows_service_windows.go
Normal file
@@ -0,0 +1,256 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package winsvc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/signal"
|
||||
"reflect"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/nomad/helper"
|
||||
"golang.org/x/sys/windows/registry"
|
||||
"golang.org/x/sys/windows/svc"
|
||||
"golang.org/x/sys/windows/svc/eventlog"
|
||||
"golang.org/x/sys/windows/svc/mgr"
|
||||
)
|
||||
|
||||
// Base registry path for eventlog registrations
|
||||
const EVENTLOG_REGISTRY_PATH = `SYSTEM\CurrentControlSet\Services\EventLog\Application`
|
||||
|
||||
// Registry value name for supported event types
|
||||
const EVENTLOG_SUPPORTED_EVENTS_KEY = "TypesSupported"
|
||||
|
||||
// Event types registered as supported
|
||||
const EVENTLOG_SUPPORTED_EVENTS uint32 = eventlog.Error | eventlog.Warning | eventlog.Info
|
||||
|
||||
// NewWindowsServiceManager creates a new instance of the wrapper
|
||||
// to interact with the Windows service manager.
|
||||
func NewWindowsServiceManager() (WindowsServiceManager, error) {
|
||||
m, err := mgr.Connect()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &windowsServiceManager{manager: m}, nil
|
||||
}
|
||||
|
||||
type windowsServiceManager struct {
|
||||
manager *mgr.Mgr
|
||||
}
|
||||
|
||||
func (m *windowsServiceManager) IsServiceRegistered(name string) (bool, error) {
|
||||
list, err := m.manager.ListServices()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if slices.Contains(list, name) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (m *windowsServiceManager) GetService(name string) (WindowsService, error) {
|
||||
service, err := m.manager.OpenService(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &windowsService{service: service}, nil
|
||||
}
|
||||
|
||||
func (m *windowsServiceManager) CreateService(name, bin string, config WindowsServiceConfiguration) (WindowsService, error) {
|
||||
wsvc, err := m.manager.CreateService(name, bin, mgr.Config{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
service := &windowsService{service: wsvc}
|
||||
|
||||
// Only apply configuration if configuration is provided
|
||||
if !reflect.ValueOf(config).IsZero() {
|
||||
if err := service.Configure(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return service, nil
|
||||
}
|
||||
|
||||
func (m *windowsServiceManager) Close() error {
|
||||
return m.manager.Disconnect()
|
||||
}
|
||||
|
||||
type windowsService struct {
|
||||
service *mgr.Service
|
||||
}
|
||||
|
||||
func (s *windowsService) Name() string {
|
||||
return s.service.Name
|
||||
}
|
||||
|
||||
func (s *windowsService) Configure(config WindowsServiceConfiguration) error {
|
||||
serviceCfg, err := s.service.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
serviceCfg.StartType = uint32(config.StartType)
|
||||
serviceCfg.DisplayName = config.DisplayName
|
||||
serviceCfg.Description = config.Description
|
||||
serviceCfg.BinaryPathName = config.BinaryPathName
|
||||
|
||||
if err := s.service.UpdateConfig(serviceCfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *windowsService) Start() error {
|
||||
if running, _ := s.IsRunning(); running {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := s.service.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := waitFor(context.Background(), s.IsRunning); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *windowsService) Stop() error {
|
||||
if stopped, _ := s.IsStopped(); stopped {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := s.service.Control(svc.Stop); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := waitFor(context.Background(), s.IsStopped); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *windowsService) Close() error {
|
||||
return s.service.Close()
|
||||
}
|
||||
|
||||
func (s *windowsService) Delete() error {
|
||||
return s.service.Delete()
|
||||
}
|
||||
|
||||
func (s *windowsService) IsRunning() (bool, error) {
|
||||
return s.isService(svc.Running)
|
||||
}
|
||||
|
||||
func (s *windowsService) IsStopped() (bool, error) {
|
||||
return s.isService(svc.Stopped)
|
||||
}
|
||||
|
||||
func (s *windowsService) EnableEventlog() error {
|
||||
// Check if the service is already setup in the eventlog
|
||||
key, err := registry.OpenKey(registry.LOCAL_MACHINE,
|
||||
EVENTLOG_REGISTRY_PATH+`\`+s.Name(),
|
||||
registry.ALL_ACCESS,
|
||||
)
|
||||
|
||||
// If it could not be opened, assume error is caused
|
||||
// due to nonexistence. If it was for some other reason
|
||||
// the error will be encountered again when attempting to
|
||||
// create.
|
||||
if err != nil {
|
||||
if err := eventlog.InstallAsEventCreate(s.Name(), EVENTLOG_SUPPORTED_EVENTS); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
defer key.Close()
|
||||
|
||||
// Since the service is already registered, just
|
||||
// ensure it is properly configured. Currently
|
||||
// that just means the supported events.
|
||||
val, _, err := key.GetIntegerValue(EVENTLOG_SUPPORTED_EVENTS_KEY)
|
||||
if err != nil || uint32(val) != EVENTLOG_SUPPORTED_EVENTS {
|
||||
if err := key.SetDWordValue(EVENTLOG_SUPPORTED_EVENTS_KEY, EVENTLOG_SUPPORTED_EVENTS); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *windowsService) DisableEventlog() error {
|
||||
// Check if the service is currently enabled in the eventlog
|
||||
key, err := registry.OpenKey(registry.LOCAL_MACHINE,
|
||||
EVENTLOG_REGISTRY_PATH+`\`+s.Name(),
|
||||
registry.READ,
|
||||
)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
defer key.Close()
|
||||
|
||||
return eventlog.Remove(s.Name())
|
||||
}
|
||||
|
||||
func (s *windowsService) isService(state svc.State) (bool, error) {
|
||||
status, err := s.service.Query()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return status.State == state, nil
|
||||
}
|
||||
|
||||
func waitFor(ctx context.Context, condition func() (bool, error)) error {
|
||||
d, err := time.ParseDuration(WINDOWS_SERVICE_STATE_TIMEOUT)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Setup a deadline
|
||||
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(d))
|
||||
defer cancel()
|
||||
// Watch for any interrupts
|
||||
ctx, stop := signal.NotifyContext(ctx, os.Interrupt)
|
||||
defer stop()
|
||||
|
||||
pauseDur := time.Millisecond * 250
|
||||
t, timerStop := helper.NewSafeTimer(pauseDur)
|
||||
defer timerStop()
|
||||
|
||||
for {
|
||||
t.Reset(pauseDur)
|
||||
|
||||
complete, err := condition()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if complete {
|
||||
return nil
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("timeout exceeded waiting for process")
|
||||
case <-t.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
598
helper/winsvc/windows_service_windows_test.go
Normal file
598
helper/winsvc/windows_service_windows_test.go
Normal file
@@ -0,0 +1,598 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package winsvc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/fs"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/go-uuid"
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/shoenig/test/must"
|
||||
"golang.org/x/sys/windows/registry"
|
||||
"golang.org/x/sys/windows/svc"
|
||||
"golang.org/x/sys/windows/svc/mgr"
|
||||
)
|
||||
|
||||
func TestWindowsServiceManager(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
t.Run("IsServiceRegistered", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
t.Run("service does not exist", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
_, manager := makeManagers(t)
|
||||
|
||||
result, err := manager.IsServiceRegistered("fake-service-name")
|
||||
must.NoError(t, err, must.Sprint("check should not error"))
|
||||
must.False(t, result, must.Sprint("service should not exist"))
|
||||
})
|
||||
|
||||
t.Run("service does exist", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
m, manager := makeManagers(t)
|
||||
serviceName := generateStubService(t, m)
|
||||
|
||||
result, err := manager.IsServiceRegistered(serviceName)
|
||||
must.NoError(t, err, must.Sprint("check should not error"))
|
||||
must.True(t, result, must.Sprint("service should exist"))
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("GetService", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
t.Run("service does not exist", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
_, manager := makeManagers(t)
|
||||
_, err := manager.GetService("fake-service-name")
|
||||
must.ErrorContains(t, err, "specified service does not exist",
|
||||
must.Sprint("error should be generated when service does not exist"))
|
||||
})
|
||||
|
||||
t.Run("service does exist", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
m, manager := makeManagers(t)
|
||||
serviceName := generateStubService(t, m)
|
||||
|
||||
srv, err := manager.GetService(serviceName)
|
||||
must.NoError(t, err)
|
||||
defer srv.Close()
|
||||
must.Eq(t, serviceName, srv.Name(), must.Sprint("service name does not match"))
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("CreateService", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
t.Run("service does not exist", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
serviceName := generateServiceName()
|
||||
m, manager := makeManagers(t)
|
||||
|
||||
srv, err := manager.CreateService(serviceName, `c:\stub`, WindowsServiceConfiguration{})
|
||||
must.NoError(t, err)
|
||||
defer srv.Close()
|
||||
defer deleteStubService(t, m, serviceName)
|
||||
|
||||
must.Eq(t, serviceName, srv.Name(), must.Sprint("new service name is incorrect"))
|
||||
})
|
||||
|
||||
t.Run("service does exist", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
m, manager := makeManagers(t)
|
||||
serviceName := generateStubService(t, m)
|
||||
|
||||
_, err := manager.CreateService(serviceName, `c:\stub`, WindowsServiceConfiguration{})
|
||||
must.ErrorContains(t, err, "service already exists", must.Sprint("service creation should fail"))
|
||||
})
|
||||
|
||||
t.Run("with configuration", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
m, manager := makeManagers(t)
|
||||
serviceName := generateServiceName()
|
||||
srv, err := manager.CreateService(serviceName, `c:\stub`,
|
||||
WindowsServiceConfiguration{DisplayName: "testing service", StartType: StartDisabled})
|
||||
must.NoError(t, err, must.Sprint("service should be created"))
|
||||
defer srv.Close()
|
||||
defer deleteStubService(t, m, serviceName)
|
||||
|
||||
directSrv, err := m.OpenService(serviceName)
|
||||
must.NoError(t, err, must.Sprint("direct service connection should succeed"))
|
||||
defer directSrv.Close()
|
||||
|
||||
config, err := directSrv.Config()
|
||||
must.NoError(t, err, must.Sprint("configuration should be available from service"))
|
||||
must.Eq(t, "testing service", config.DisplayName, must.Sprint("new service name does not match"))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// This is a simple service available in Windows. It will
|
||||
// be used to locate the executable so a test service can
|
||||
// be created using it that will allow proper start/stop
|
||||
// testing.
|
||||
const TEST_WINDOWS_SERVICE = "SNMPTrap"
|
||||
|
||||
func TestWindowsService(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
mg, _ := makeManagers(t)
|
||||
snmpSvc, err := mg.OpenService(TEST_WINDOWS_SERVICE)
|
||||
must.NoError(t, err)
|
||||
defer snmpSvc.Close()
|
||||
snmpConfig, err := snmpSvc.Config()
|
||||
must.NoError(t, err)
|
||||
binPath := snmpConfig.BinaryPathName
|
||||
|
||||
t.Run("Name", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
m, manager := makeManagers(t)
|
||||
serviceName := generateStubService(t, m)
|
||||
|
||||
srv, err := manager.GetService(serviceName)
|
||||
must.NoError(t, err)
|
||||
defer srv.Close()
|
||||
|
||||
must.Eq(t, serviceName, srv.Name(), must.Sprint("service name does not match"))
|
||||
})
|
||||
|
||||
t.Run("Configure", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
t.Run("valid configuration", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
m, manager := makeManagers(t)
|
||||
serviceName := generateStubService(t, m)
|
||||
|
||||
srv, err := manager.GetService(serviceName)
|
||||
must.NoError(t, err, must.Sprint("service should be available"))
|
||||
err = srv.Configure(WindowsServiceConfiguration{
|
||||
StartType: StartDisabled,
|
||||
DisplayName: "testing display name",
|
||||
BinaryPathName: `c:\stub -with -arguments`,
|
||||
})
|
||||
must.NoError(t, err, must.Sprint("valid configuration should not error"))
|
||||
directSrv, err := m.OpenService(serviceName)
|
||||
must.NoError(t, err, must.Sprint("direct service should be available"))
|
||||
defer directSrv.Close()
|
||||
config, err := directSrv.Config()
|
||||
must.NoError(t, err, must.Sprint("direct service config should be available"))
|
||||
must.Eq(t, "testing display name", config.DisplayName, must.Sprint("display name does not match"))
|
||||
must.Eq(t, `c:\stub -with -arguments`, config.BinaryPathName, must.Sprint("binary path name does not match"))
|
||||
})
|
||||
|
||||
t.Run("invalid configuration", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
m, manager := makeManagers(t)
|
||||
serviceName := generateStubService(t, m)
|
||||
srv, err := manager.GetService(serviceName)
|
||||
|
||||
must.NoError(t, err, must.Sprint("service should be available"))
|
||||
err = srv.Configure(WindowsServiceConfiguration{
|
||||
DisplayName: "testing display name",
|
||||
BinaryPathName: `c:\stub -with -arguments`,
|
||||
})
|
||||
must.ErrorContains(t, err, "parameter is incorrect", must.Sprint("invalid configuration should error"))
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Start", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
t.Run("when stopped", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
m, manager := makeManagers(t)
|
||||
runnableSvc := runnableService(t, manager, binPath)
|
||||
|
||||
directSrv, err := m.OpenService(runnableSvc.Name())
|
||||
must.NoError(t, err, must.Sprint("direct service should be available"))
|
||||
defer directSrv.Close()
|
||||
|
||||
status, err := directSrv.Query()
|
||||
must.NoError(t, err, must.Sprint("direct service status should be available"))
|
||||
if status.State != svc.Stopped {
|
||||
_, err := directSrv.Control(svc.Stop)
|
||||
must.NoError(t, err, must.Sprint("direct stop should not fail"))
|
||||
err = waitFor(context.Background(), func() (bool, error) {
|
||||
status, err := directSrv.Query()
|
||||
must.NoError(t, err, must.Sprint("direct service should be queryable"))
|
||||
return status.State == svc.Stopped, nil
|
||||
})
|
||||
must.NoError(t, err, must.Sprint("service must be stopped"))
|
||||
}
|
||||
must.NoError(t, runnableSvc.Start(), must.Sprint("service should start without error"))
|
||||
status, err = directSrv.Query()
|
||||
must.NoError(t, err, must.Sprint("direct service status should be available"))
|
||||
must.Eq(t, status.State, svc.Running, must.Sprint("service should be running"))
|
||||
})
|
||||
|
||||
t.Run("when running", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
m, manager := makeManagers(t)
|
||||
runnableSvc := runnableService(t, manager, binPath)
|
||||
|
||||
directSrv, err := m.OpenService(runnableSvc.Name())
|
||||
must.NoError(t, err, must.Sprint("direct service should be available"))
|
||||
defer directSrv.Close()
|
||||
|
||||
status, err := directSrv.Query()
|
||||
must.NoError(t, err, must.Sprint("direct service status should be available"))
|
||||
if status.State != svc.Running {
|
||||
must.NoError(t, directSrv.Start(), must.Sprint("direct start should not fail"))
|
||||
err := waitFor(context.Background(), func() (bool, error) {
|
||||
status, err := directSrv.Query()
|
||||
must.NoError(t, err, must.Sprint("direct service should be queryable"))
|
||||
return status.State == svc.Running, nil
|
||||
})
|
||||
must.NoError(t, err, must.Sprint("service must be running"))
|
||||
}
|
||||
must.NoError(t, runnableSvc.Start(), must.Sprint("service should start without error"))
|
||||
status, err = directSrv.Query()
|
||||
must.NoError(t, err, must.Sprint("direct service status should be available"))
|
||||
must.Eq(t, status.State, svc.Running, must.Sprint("service should be running"))
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Stop", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
t.Run("when stopped", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
m, manager := makeManagers(t)
|
||||
runnableSvc := runnableService(t, manager, binPath)
|
||||
|
||||
directSrv, err := m.OpenService(runnableSvc.Name())
|
||||
must.NoError(t, err, must.Sprint("direct service should be available"))
|
||||
defer directSrv.Close()
|
||||
|
||||
status, err := directSrv.Query()
|
||||
must.NoError(t, err, must.Sprint("direct service status should be available"))
|
||||
if status.State != svc.Stopped {
|
||||
_, err := directSrv.Control(svc.Stop)
|
||||
must.NoError(t, err, must.Sprint("direct stop should not fail"))
|
||||
err = waitFor(context.Background(), func() (bool, error) {
|
||||
status, err := directSrv.Query()
|
||||
must.NoError(t, err, must.Sprint("direct service should be queryable"))
|
||||
return status.State == svc.Stopped, nil
|
||||
})
|
||||
must.NoError(t, err, must.Sprint("service must be stopped"))
|
||||
}
|
||||
must.NoError(t, runnableSvc.Stop(), must.Sprint("service should stop without error"))
|
||||
status, err = directSrv.Query()
|
||||
must.NoError(t, err, must.Sprint("direct service status should be available"))
|
||||
must.Eq(t, status.State, svc.Stopped, must.Sprint("service should be stopped"))
|
||||
})
|
||||
|
||||
t.Run("when running", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
m, manager := makeManagers(t)
|
||||
runnableSvc := runnableService(t, manager, binPath)
|
||||
|
||||
directSrv, err := m.OpenService(runnableSvc.Name())
|
||||
must.NoError(t, err, must.Sprint("direct service should be available"))
|
||||
defer directSrv.Close()
|
||||
|
||||
status, err := directSrv.Query()
|
||||
must.NoError(t, err, must.Sprint("direct service status should be available"))
|
||||
if status.State != svc.Running {
|
||||
must.NoError(t, directSrv.Start(), must.Sprint("direct start should not fail"))
|
||||
err := waitFor(context.Background(), func() (bool, error) {
|
||||
status, err := directSrv.Query()
|
||||
must.NoError(t, err, must.Sprint("direct service should be queryable"))
|
||||
return status.State == svc.Running, nil
|
||||
})
|
||||
must.NoError(t, err, must.Sprint("service must be running"))
|
||||
}
|
||||
must.NoError(t, runnableSvc.Stop(), must.Sprint("service should stop without error"))
|
||||
status, err = directSrv.Query()
|
||||
must.NoError(t, err, must.Sprint("direct service status should be available"))
|
||||
must.Eq(t, status.State, svc.Stopped, must.Sprint("service should be stopped"))
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
t.Run("when service exists", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
m, manager := makeManagers(t)
|
||||
|
||||
serviceName := generateStubService(t, m)
|
||||
srv, err := manager.GetService(serviceName)
|
||||
must.NoError(t, err, must.Sprint("service should be avaialble"))
|
||||
defer srv.Close()
|
||||
|
||||
must.NoError(t, srv.Delete(), must.Sprint("service should be deleted"))
|
||||
})
|
||||
|
||||
t.Run("when service does not exist", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
m, manager := makeManagers(t)
|
||||
|
||||
serviceName := generateStubService(t, m)
|
||||
srv, err := manager.GetService(serviceName)
|
||||
must.NoError(t, err, must.Sprint("service should be avaialble"))
|
||||
defer srv.Close()
|
||||
// Delete the service directly
|
||||
directSrv, err := m.OpenService(serviceName)
|
||||
must.NoError(t, err, must.Sprint("direct service should be available"))
|
||||
defer directSrv.Close()
|
||||
must.NoError(t, directSrv.Delete(), must.Sprint("service should be deleted"))
|
||||
|
||||
must.ErrorContains(t, srv.Delete(), "marked for deletion",
|
||||
must.Sprint("service should have already been deleted"))
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("IsRunning", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
t.Run("when service is not running", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
m, manager := makeManagers(t)
|
||||
runnableSvc := runnableService(t, manager, binPath)
|
||||
directSrv, err := m.OpenService(runnableSvc.Name())
|
||||
must.NoError(t, err, must.Sprint("direct service should be available"))
|
||||
defer directSrv.Close()
|
||||
|
||||
status, err := directSrv.Query()
|
||||
must.NoError(t, err, must.Sprint("direct service status should be available"))
|
||||
if status.State != svc.Stopped {
|
||||
_, err := directSrv.Control(svc.Stop)
|
||||
must.NoError(t, err, must.Sprint("direct stop should not fail"))
|
||||
err = waitFor(context.Background(), func() (bool, error) {
|
||||
status, err := directSrv.Query()
|
||||
must.NoError(t, err, must.Sprint("direct service should be queryable"))
|
||||
return status.State == svc.Stopped, nil
|
||||
})
|
||||
must.NoError(t, err, must.Sprint("service must be stopped"))
|
||||
}
|
||||
|
||||
srv, err := manager.GetService(directSrv.Name)
|
||||
must.NoError(t, err, must.Sprint("service should be available"))
|
||||
defer srv.Close()
|
||||
result, err := srv.IsRunning()
|
||||
must.NoError(t, err, must.Sprint("running check should not error"))
|
||||
must.False(t, result, must.Sprint("should not show service as running"))
|
||||
})
|
||||
|
||||
t.Run("when service is running", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
m, manager := makeManagers(t)
|
||||
runnableSvc := runnableService(t, manager, binPath)
|
||||
directSrv, err := m.OpenService(runnableSvc.Name())
|
||||
must.NoError(t, err, must.Sprint("direct service should be available"))
|
||||
defer directSrv.Close()
|
||||
|
||||
status, err := directSrv.Query()
|
||||
must.NoError(t, err, must.Sprint("direct service status should be available"))
|
||||
if status.State != svc.Running {
|
||||
must.NoError(t, directSrv.Start(), must.Sprint("direct start should not fail"))
|
||||
err := waitFor(context.Background(), func() (bool, error) {
|
||||
status, err := directSrv.Query()
|
||||
must.NoError(t, err, must.Sprint("direct service should be queryable"))
|
||||
return status.State == svc.Running, nil
|
||||
})
|
||||
must.NoError(t, err, must.Sprint("service must be running"))
|
||||
}
|
||||
srv, err := manager.GetService(directSrv.Name)
|
||||
must.NoError(t, err, must.Sprint("service should be available"))
|
||||
defer srv.Close()
|
||||
result, err := srv.IsRunning()
|
||||
must.NoError(t, err, must.Sprint("running check should not error"))
|
||||
must.True(t, result, must.Sprint("should show service as running"))
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("IsStopped", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
t.Run("when service is not running", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
m, manager := makeManagers(t)
|
||||
runnableSvc := runnableService(t, manager, binPath)
|
||||
directSrv, err := m.OpenService(runnableSvc.Name())
|
||||
must.NoError(t, err, must.Sprint("direct service should be available"))
|
||||
defer directSrv.Close()
|
||||
|
||||
status, err := directSrv.Query()
|
||||
must.NoError(t, err, must.Sprint("direct service status should be available"))
|
||||
if status.State != svc.Stopped {
|
||||
_, err := directSrv.Control(svc.Stop)
|
||||
must.NoError(t, err, must.Sprint("direct stop should not fail"))
|
||||
err = waitFor(context.Background(), func() (bool, error) {
|
||||
status, err := directSrv.Query()
|
||||
must.NoError(t, err, must.Sprint("direct service should be queryable"))
|
||||
return status.State == svc.Stopped, nil
|
||||
})
|
||||
must.NoError(t, err, must.Sprint("service must be stopped"))
|
||||
}
|
||||
|
||||
srv, err := manager.GetService(directSrv.Name)
|
||||
must.NoError(t, err, must.Sprint("service should be available"))
|
||||
defer srv.Close()
|
||||
result, err := srv.IsStopped()
|
||||
must.NoError(t, err, must.Sprint("running check should not error"))
|
||||
must.True(t, result, must.Sprint("should show service as stopped"))
|
||||
})
|
||||
|
||||
t.Run("when service is running", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
m, manager := makeManagers(t)
|
||||
runnableSvc := runnableService(t, manager, binPath)
|
||||
directSrv, err := m.OpenService(runnableSvc.Name())
|
||||
must.NoError(t, err, must.Sprint("direct service should be available"))
|
||||
defer directSrv.Close()
|
||||
|
||||
status, err := directSrv.Query()
|
||||
must.NoError(t, err, must.Sprint("direct service status should be available"))
|
||||
if status.State != svc.Running {
|
||||
must.NoError(t, directSrv.Start(), must.Sprint("direct start should not fail"))
|
||||
err := waitFor(context.Background(), func() (bool, error) {
|
||||
status, err := directSrv.Query()
|
||||
must.NoError(t, err, must.Sprint("direct service should be queryable"))
|
||||
return status.State == svc.Running, nil
|
||||
})
|
||||
must.NoError(t, err, must.Sprint("service must be running"))
|
||||
}
|
||||
srv, err := manager.GetService(directSrv.Name)
|
||||
must.NoError(t, err, must.Sprint("service should be available"))
|
||||
defer srv.Close()
|
||||
result, err := srv.IsStopped()
|
||||
must.NoError(t, err, must.Sprint("running check should not error"))
|
||||
must.False(t, result, must.Sprint("should not show service as stopped"))
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("EnableEventLog", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
t.Run("when service is not registered", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
m, manager := makeManagers(t)
|
||||
serviceName := generateStubService(t, m)
|
||||
|
||||
srv, err := manager.GetService(serviceName)
|
||||
must.NoError(t, err, must.Sprint("service should be available"))
|
||||
defer srv.Close()
|
||||
|
||||
must.NoError(t, srv.EnableEventlog(), must.Sprint("could not enable eventlog"))
|
||||
key, err := registry.OpenKey(registry.LOCAL_MACHINE,
|
||||
EVENTLOG_REGISTRY_PATH+`\`+serviceName,
|
||||
registry.READ,
|
||||
)
|
||||
must.NoError(t, err, must.Sprint("registry key should be available"))
|
||||
defer key.Close()
|
||||
val, _, err := key.GetIntegerValue(EVENTLOG_SUPPORTED_EVENTS_KEY)
|
||||
must.NoError(t, err, must.Sprint("registry key value should be available"))
|
||||
must.Eq(t, EVENTLOG_SUPPORTED_EVENTS, uint32(val), must.Sprint("registry value should match"))
|
||||
})
|
||||
|
||||
t.Run("when service is already registered", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
m, manager := makeManagers(t)
|
||||
serviceName := generateStubService(t, m)
|
||||
|
||||
srv, err := manager.GetService(serviceName)
|
||||
must.NoError(t, err, must.Sprint("service should be available"))
|
||||
defer srv.Close()
|
||||
must.NoError(t, srv.EnableEventlog(), must.Sprint("could not enable eventlog"))
|
||||
// Modify value in registry
|
||||
key, err := registry.OpenKey(registry.LOCAL_MACHINE,
|
||||
EVENTLOG_REGISTRY_PATH+`\`+serviceName,
|
||||
registry.ALL_ACCESS,
|
||||
)
|
||||
err = key.SetDWordValue(EVENTLOG_SUPPORTED_EVENTS_KEY, 1)
|
||||
must.NoError(t, err, must.Sprint("could not modify registry value"))
|
||||
|
||||
// Now enable and verify value is correct
|
||||
must.NoError(t, srv.EnableEventlog(), must.Sprint("failed to enable eventlog"))
|
||||
val, _, err := key.GetIntegerValue(EVENTLOG_SUPPORTED_EVENTS_KEY)
|
||||
must.NoError(t, err, must.Sprint("registry value should be available"))
|
||||
must.Eq(t, EVENTLOG_SUPPORTED_EVENTS, uint32(val), must.Sprint("registry value should match"))
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("DisableEventLog", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
t.Run("when service is not registered", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
m, manager := makeManagers(t)
|
||||
serviceName := generateStubService(t, m)
|
||||
|
||||
srv, err := manager.GetService(serviceName)
|
||||
must.NoError(t, err, must.Sprint("service should be available"))
|
||||
defer srv.Close()
|
||||
|
||||
must.NoError(t, srv.DisableEventlog(), must.Sprint("eventlog disable should not error"))
|
||||
})
|
||||
|
||||
t.Run("when service is registered", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
m, manager := makeManagers(t)
|
||||
serviceName := generateStubService(t, m)
|
||||
|
||||
srv, err := manager.GetService(serviceName)
|
||||
must.NoError(t, err, must.Sprint("service should be available"))
|
||||
defer srv.Close()
|
||||
must.NoError(t, srv.EnableEventlog(), must.Sprint("eventlog enable should not error"))
|
||||
|
||||
must.NoError(t, srv.DisableEventlog(), must.Sprint("eventlog disable should not error"))
|
||||
_, err = registry.OpenKey(registry.LOCAL_MACHINE,
|
||||
EVENTLOG_REGISTRY_PATH+`\`+serviceName,
|
||||
registry.READ,
|
||||
)
|
||||
must.ErrorIs(t, err, fs.ErrNotExist, must.Sprint("registry key should no longer exist"))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func generateServiceName() string {
|
||||
id, err := uuid.GenerateUUID()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return id[:5]
|
||||
}
|
||||
|
||||
func generateStubService(t *testing.T, m *mgr.Mgr) string {
|
||||
t.Helper()
|
||||
|
||||
id := generateServiceName()
|
||||
_, err := m.CreateService(id, `c:\stub`, mgr.Config{})
|
||||
must.NoError(t, err, must.Sprint("failed to generate stub service"))
|
||||
|
||||
t.Cleanup(func() { deleteStubService(t, m, id) })
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
func deleteStubService(t *testing.T, m *mgr.Mgr, svcId string) {
|
||||
t.Helper()
|
||||
|
||||
srvc, err := m.OpenService(svcId)
|
||||
if err != nil {
|
||||
// If the service doesn't exist, then deletion is done so not
|
||||
// an error. Otherwise, force an error.
|
||||
must.ErrorContains(t, err, "service does not exist", must.Sprint("failed to open service"))
|
||||
return
|
||||
}
|
||||
status, err := srvc.Query()
|
||||
must.NoError(t, err, must.Sprint("failed to query service"))
|
||||
if status.State != svc.Stopped {
|
||||
status, err = srvc.Control(svc.Stop)
|
||||
must.NoError(t, err, must.Sprint("failed to stop service"))
|
||||
err := waitFor(context.Background(), func() (bool, error) {
|
||||
status, err := srvc.Query()
|
||||
must.NoError(t, err, must.Sprint("failed to query service"))
|
||||
return status.State == svc.Stopped, nil
|
||||
})
|
||||
must.NoError(t, err, must.Sprintf("could not stop service for deletion - %s", svcId))
|
||||
}
|
||||
if err := srvc.Delete(); err != nil {
|
||||
must.ErrorContains(t, err, "service has been marked for deletion", must.Sprint("failed to delete service"))
|
||||
}
|
||||
}
|
||||
|
||||
func makeManagers(t *testing.T) (*mgr.Mgr, WindowsServiceManager) {
|
||||
t.Helper()
|
||||
|
||||
winM, err := NewWindowsServiceManager()
|
||||
must.NoError(t, err, must.Sprint("failed to create service manager"))
|
||||
|
||||
m, err := mgr.Connect()
|
||||
must.NoError(t, err, must.Sprint("failed to connect to windows service manager"))
|
||||
|
||||
t.Cleanup(func() {
|
||||
winM.Close()
|
||||
m.Disconnect()
|
||||
})
|
||||
|
||||
return m, winM
|
||||
}
|
||||
|
||||
func runnableService(t *testing.T, m WindowsServiceManager, binPath string) WindowsService {
|
||||
t.Helper()
|
||||
|
||||
runnableSvc, err := m.CreateService(generateServiceName(), binPath,
|
||||
WindowsServiceConfiguration{StartType: StartManual, BinaryPathName: binPath})
|
||||
must.NoError(t, err, must.Sprint("failed to create runnable service"))
|
||||
|
||||
t.Cleanup(func() { runnableSvc.Close() })
|
||||
|
||||
return runnableSvc
|
||||
}
|
||||
@@ -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
|
||||
|
||||
22
website/content/commands/windows/index.mdx
Normal file
22
website/content/commands/windows/index.mdx
Normal 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
|
||||
50
website/content/commands/windows/service-install.mdx
Normal file
50
website/content/commands/windows/service-install.mdx
Normal 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'
|
||||
22
website/content/commands/windows/service-uninstall.mdx
Normal file
22
website/content/commands/windows/service-uninstall.mdx
Normal 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.
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user