diff --git a/api/agent.go b/api/agent.go index 08b1441333c6..e2d1ad543c15 100644 --- a/api/agent.go +++ b/api/agent.go @@ -276,9 +276,10 @@ func (a *Agent) ForceLeave(node string) error { // EnableServiceMaintenance toggles service maintenance mode on // for the given service ID. -func (a *Agent) EnableServiceMaintenance(serviceID string) error { +func (a *Agent) EnableServiceMaintenance(serviceID, reason string) error { r := a.c.newRequest("PUT", "/v1/agent/service/maintenance/"+serviceID) r.params.Set("enable", "true") + r.params.Set("reason", reason) _, resp, err := requireOK(a.c.doRequest(r)) if err != nil { return err @@ -302,9 +303,10 @@ func (a *Agent) DisableServiceMaintenance(serviceID string) error { // EnableNodeMaintenance toggles node maintenance mode on for the // agent we are connected to. -func (a *Agent) EnableNodeMaintenance() error { +func (a *Agent) EnableNodeMaintenance(reason string) error { r := a.c.newRequest("PUT", "/v1/agent/maintenance") r.params.Set("enable", "true") + r.params.Set("reason", reason) _, resp, err := requireOK(a.c.doRequest(r)) if err != nil { return err diff --git a/api/agent_test.go b/api/agent_test.go index e27887d6f85d..37a00c573f35 100644 --- a/api/agent_test.go +++ b/api/agent_test.go @@ -272,7 +272,7 @@ func TestServiceMaintenance(t *testing.T) { } // Enable maintenance mode - if err := agent.EnableServiceMaintenance("redis"); err != nil { + if err := agent.EnableServiceMaintenance("redis", "broken"); err != nil { t.Fatalf("err: %s", err) } @@ -285,7 +285,7 @@ func TestServiceMaintenance(t *testing.T) { for _, check := range checks { if strings.Contains(check.CheckID, "maintenance") { found = true - if check.Status != "critical" { + if check.Status != "critical" || check.Notes != "broken" { t.Fatalf("bad: %#v", checks) } } @@ -318,7 +318,7 @@ func TestNodeMaintenance(t *testing.T) { agent := c.Agent() // Enable maintenance mode - if err := agent.EnableNodeMaintenance(); err != nil { + if err := agent.EnableNodeMaintenance("broken"); err != nil { t.Fatalf("err: %s", err) } @@ -331,7 +331,7 @@ func TestNodeMaintenance(t *testing.T) { for _, check := range checks { if strings.Contains(check.CheckID, "maintenance") { found = true - if check.Status != "critical" { + if check.Status != "critical" || check.Notes != "broken" { t.Fatalf("bad: %#v", checks) } } diff --git a/command/agent/agent.go b/command/agent/agent.go index 5ec4409f1da6..5aeb783a80c7 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -34,6 +34,12 @@ const ( // The ID of the faux health checks for maintenance mode serviceMaintCheckPrefix = "_service_maintenance" nodeMaintCheckID = "_node_maintenance" + + // Default reasons for node/service maintenance mode + defaultNodeMaintReason = "Maintenance mode is enabled for this node, " + + "but no reason was provided. This is a default message." + defaultServiceMaintReason = "Maintenance mode is enabled for this " + + "service, but no reason was provided. This is a default message." ) /* @@ -1027,7 +1033,7 @@ func serviceMaintCheckID(serviceID string) string { // EnableServiceMaintenance will register a false health check against the given // service ID with critical status. This will exclude the service from queries. -func (a *Agent) EnableServiceMaintenance(serviceID string) error { +func (a *Agent) EnableServiceMaintenance(serviceID, reason string) error { service, ok := a.state.Services()[serviceID] if !ok { return fmt.Errorf("No service registered with ID %q", serviceID) @@ -1039,18 +1045,23 @@ func (a *Agent) EnableServiceMaintenance(serviceID string) error { return nil } + // Use default notes if no reason provided + if reason == "" { + reason = defaultServiceMaintReason + } + // Create and register the critical health check check := &structs.HealthCheck{ Node: a.config.NodeName, CheckID: checkID, Name: "Service Maintenance Mode", - Notes: "Maintenance mode is enabled for this service", + Notes: reason, ServiceID: service.ID, ServiceName: service.Service, Status: structs.HealthCritical, } a.AddCheck(check, nil, true) - a.logger.Printf("[INFO] agent: service %q entered maintenance mode", serviceID) + a.logger.Printf("[INFO] agent: Service %q entered maintenance mode", serviceID) return nil } @@ -1070,28 +1081,33 @@ func (a *Agent) DisableServiceMaintenance(serviceID string) error { // Deregister the maintenance check a.RemoveCheck(checkID, true) - a.logger.Printf("[INFO] agent: service %q left maintenance mode", serviceID) + a.logger.Printf("[INFO] agent: Service %q left maintenance mode", serviceID) return nil } // EnableNodeMaintenance places a node into maintenance mode. -func (a *Agent) EnableNodeMaintenance() { +func (a *Agent) EnableNodeMaintenance(reason string) { // Ensure node maintenance is not already enabled if _, ok := a.state.Checks()[nodeMaintCheckID]; ok { return } + // Use a default notes value + if reason == "" { + reason = defaultNodeMaintReason + } + // Create and register the node maintenance check check := &structs.HealthCheck{ Node: a.config.NodeName, CheckID: nodeMaintCheckID, Name: "Node Maintenance Mode", - Notes: "Maintenance mode is enabled for this node", + Notes: reason, Status: structs.HealthCritical, } a.AddCheck(check, nil, true) - a.logger.Printf("[INFO] agent: node entered maintenance mode") + a.logger.Printf("[INFO] agent: Node entered maintenance mode") } // DisableNodeMaintenance removes a node from maintenance mode @@ -1100,5 +1116,5 @@ func (a *Agent) DisableNodeMaintenance() { return } a.RemoveCheck(nodeMaintCheckID, true) - a.logger.Printf("[INFO] agent: node left maintenance mode") + a.logger.Printf("[INFO] agent: Node left maintenance mode") } diff --git a/command/agent/agent_endpoint.go b/command/agent/agent_endpoint.go index 817426286bec..d7cfeb9e7f48 100644 --- a/command/agent/agent_endpoint.go +++ b/command/agent/agent_endpoint.go @@ -220,17 +220,21 @@ func (s *HTTPServer) AgentServiceMaintenance(resp http.ResponseWriter, req *http } if enable { - if err = s.agent.EnableServiceMaintenance(serviceID); err != nil { + reason := params.Get("reason") + if err = s.agent.EnableServiceMaintenance(serviceID, reason); err != nil { resp.WriteHeader(404) resp.Write([]byte(err.Error())) + return nil, nil } } else { if err = s.agent.DisableServiceMaintenance(serviceID); err != nil { resp.WriteHeader(404) resp.Write([]byte(err.Error())) + return nil, nil } } - return nil, err + + return nil, nil } func (s *HTTPServer) AgentNodeMaintenance(resp http.ResponseWriter, req *http.Request) (interface{}, error) { @@ -257,7 +261,7 @@ func (s *HTTPServer) AgentNodeMaintenance(resp http.ResponseWriter, req *http.Re } if enable { - s.agent.EnableNodeMaintenance() + s.agent.EnableNodeMaintenance(params.Get("reason")) } else { s.agent.DisableNodeMaintenance() } diff --git a/command/agent/agent_endpoint_test.go b/command/agent/agent_endpoint_test.go index 189b90a3c1cc..0387f38901ba 100644 --- a/command/agent/agent_endpoint_test.go +++ b/command/agent/agent_endpoint_test.go @@ -542,8 +542,8 @@ func TestHTTPAgent_ServiceMaintenanceEndpoint_BadRequest(t *testing.T) { // Fails when bad service ID provided req, _ = http.NewRequest("PUT", "/v1/agent/service/maintenance/_nope_?enable=true", nil) resp = httptest.NewRecorder() - if _, err := srv.AgentServiceMaintenance(resp, req); err == nil { - t.Fatalf("should have errored") + if _, err := srv.AgentServiceMaintenance(resp, req); err != nil { + t.Fatalf("err: %s", err) } if resp.Code != 404 { t.Fatalf("expected 404, got %d", resp.Code) @@ -566,7 +566,7 @@ func TestHTTPAgent_EnableServiceMaintenance(t *testing.T) { } // Force the service into maintenance mode - req, _ := http.NewRequest("PUT", "/v1/agent/service/maintenance/test?enable=true", nil) + req, _ := http.NewRequest("PUT", "/v1/agent/service/maintenance/test?enable=true&reason=broken", nil) resp := httptest.NewRecorder() if _, err := srv.AgentServiceMaintenance(resp, req); err != nil { t.Fatalf("err: %s", err) @@ -577,9 +577,15 @@ func TestHTTPAgent_EnableServiceMaintenance(t *testing.T) { // Ensure the maintenance check was registered checkID := serviceMaintCheckID("test") - if _, ok := srv.agent.state.Checks()[checkID]; !ok { + check, ok := srv.agent.state.Checks()[checkID] + if !ok { t.Fatalf("should have registered maintenance check") } + + // Ensure the reason was set in notes + if check.Notes != "broken" { + t.Fatalf("bad: %#v", check) + } } func TestHTTPAgent_DisableServiceMaintenance(t *testing.T) { @@ -598,7 +604,7 @@ func TestHTTPAgent_DisableServiceMaintenance(t *testing.T) { } // Force the service into maintenance mode - if err := srv.agent.EnableServiceMaintenance("test"); err != nil { + if err := srv.agent.EnableServiceMaintenance("test", ""); err != nil { t.Fatalf("err: %s", err) } @@ -653,7 +659,8 @@ func TestHTTPAgent_EnableNodeMaintenance(t *testing.T) { defer srv.agent.Shutdown() // Force the node into maintenance mode - req, _ := http.NewRequest("PUT", "/v1/agent/self/maintenance?enable=true", nil) + req, _ := http.NewRequest( + "PUT", "/v1/agent/self/maintenance?enable=true&reason=broken", nil) resp := httptest.NewRecorder() if _, err := srv.AgentNodeMaintenance(resp, req); err != nil { t.Fatalf("err: %s", err) @@ -663,9 +670,15 @@ func TestHTTPAgent_EnableNodeMaintenance(t *testing.T) { } // Ensure the maintenance check was registered - if _, ok := srv.agent.state.Checks()[nodeMaintCheckID]; !ok { + check, ok := srv.agent.state.Checks()[nodeMaintCheckID] + if !ok { t.Fatalf("should have registered maintenance check") } + + // Ensure the reason was set in notes + if check.Notes != "broken" { + t.Fatalf("bad: %#v", check) + } } func TestHTTPAgent_DisableNodeMaintenance(t *testing.T) { @@ -675,7 +688,7 @@ func TestHTTPAgent_DisableNodeMaintenance(t *testing.T) { defer srv.agent.Shutdown() // Force the node into maintenance mode - srv.agent.EnableNodeMaintenance() + srv.agent.EnableNodeMaintenance("") // Leave maintenance mode req, _ := http.NewRequest("PUT", "/v1/agent/self/maintenance?enable=false", nil) diff --git a/command/agent/agent_test.go b/command/agent/agent_test.go index 5279b25c47bf..0a702263ffd2 100644 --- a/command/agent/agent_test.go +++ b/command/agent/agent_test.go @@ -916,16 +916,22 @@ func TestAgent_ServiceMaintenanceMode(t *testing.T) { } // Enter maintenance mode for the service - if err := agent.EnableServiceMaintenance("redis"); err != nil { + if err := agent.EnableServiceMaintenance("redis", "broken"); err != nil { t.Fatalf("err: %s", err) } // Make sure the critical health check was added checkID := serviceMaintCheckID("redis") - if _, ok := agent.state.Checks()[checkID]; !ok { + check, ok := agent.state.Checks()[checkID] + if !ok { t.Fatalf("should have registered critical maintenance check") } + // Ensure the reason was set in notes + if check.Notes != "broken" { + t.Fatalf("bad: %#v", check) + } + // Leave maintenance mode if err := agent.DisableServiceMaintenance("redis"); err != nil { t.Fatalf("err: %s", err) @@ -935,6 +941,20 @@ func TestAgent_ServiceMaintenanceMode(t *testing.T) { if _, ok := agent.state.Checks()[checkID]; ok { t.Fatalf("should have deregistered maintenance check") } + + // Enter service maintenance mode without providing a reason + if err := agent.EnableServiceMaintenance("redis", ""); err != nil { + t.Fatalf("err: %s", err) + } + + // Ensure the check was registered with the default notes + check, ok = agent.state.Checks()[checkID] + if !ok { + t.Fatalf("should have registered critical check") + } + if check.Notes != defaultServiceMaintReason { + t.Fatalf("bad: %#v", check) + } } func TestAgent_NodeMaintenanceMode(t *testing.T) { @@ -944,13 +964,19 @@ func TestAgent_NodeMaintenanceMode(t *testing.T) { defer agent.Shutdown() // Enter maintenance mode for the node - agent.EnableNodeMaintenance() + agent.EnableNodeMaintenance("broken") // Make sure the critical health check was added - if _, ok := agent.state.Checks()[nodeMaintCheckID]; !ok { + check, ok := agent.state.Checks()[nodeMaintCheckID] + if !ok { t.Fatalf("should have registered critical node check") } + // Ensure the reason was set in notes + if check.Notes != "broken" { + t.Fatalf("bad: %#v", check) + } + // Leave maintenance mode agent.DisableNodeMaintenance() @@ -958,4 +984,16 @@ func TestAgent_NodeMaintenanceMode(t *testing.T) { if _, ok := agent.state.Checks()[nodeMaintCheckID]; ok { t.Fatalf("should have deregistered critical node check") } + + // Enter maintenance mode without passing a reason + agent.EnableNodeMaintenance("") + + // Make sure the check was registered with the default note + check, ok = agent.state.Checks()[nodeMaintCheckID] + if !ok { + t.Fatalf("should have registered critical node check") + } + if check.Notes != defaultNodeMaintReason { + t.Fatalf("bad: %#v", check) + } } diff --git a/command/maint.go b/command/maint.go new file mode 100644 index 000000000000..e1319db5267d --- /dev/null +++ b/command/maint.go @@ -0,0 +1,176 @@ +package command + +import ( + "flag" + "fmt" + "strings" + + "github.com/hashicorp/consul/api" + "github.com/mitchellh/cli" +) + +// MaintCommand is a Command implementation that enables or disables +// node or service maintenance mode. +type MaintCommand struct { + Ui cli.Ui +} + +func (c *MaintCommand) Help() string { + helpText := ` +Usage: consul maint [options] + + Places a node or service into maintenance mode. During maintenance mode, + the node or service will be excluded from all queries through the DNS + or API interfaces, effectively taking it out of the pool of available + nodes. This is done by registering an additional critical health check. + + When enabling maintenance mode for a node or service, you may optionally + specify a reason string. This string will appear in the "Notes" field + of the critical health check which is registered against the node or + service. If no reason is provided, a default value will be used. + + Maintenance mode is persistent, and will be restored in the event of an + agent restart. It is therefore required to disable maintenance mode on + a given node or service before it will be placed back into the pool. + + By default, we operate on the node as a whole. By specifying the + "-service" argument, this behavior can be changed to enable or disable + only a specific service. + + If no arguments are given, the agent's maintenance status will be shown. + This will return blank if nothing is currently under maintenance. + +Options: + + -enable Enable maintenance mode. + -disable Disable maintenance mode. + -reason= Text string describing the maintenance reason + -service= Control maintenance mode for a specific service ID + -token="" ACL token to use. Defaults to that of agent. + -http-addr=127.0.0.1:8500 HTTP address of the Consul agent. +` + return strings.TrimSpace(helpText) +} + +func (c *MaintCommand) Run(args []string) int { + var enable bool + var disable bool + var reason string + var serviceID string + var token string + + cmdFlags := flag.NewFlagSet("maint", flag.ContinueOnError) + cmdFlags.Usage = func() { c.Ui.Output(c.Help()) } + + cmdFlags.BoolVar(&enable, "enable", false, "enable maintenance mode") + cmdFlags.BoolVar(&disable, "disable", false, "disable maintenance mode") + cmdFlags.StringVar(&reason, "reason", "", "maintenance reason") + cmdFlags.StringVar(&serviceID, "service", "", "service maintenance") + cmdFlags.StringVar(&token, "token", "", "") + httpAddr := HTTPAddrFlag(cmdFlags) + + if err := cmdFlags.Parse(args); err != nil { + return 1 + } + + // Ensure we don't have conflicting args + if enable && disable { + c.Ui.Error("Only one of -enable or -disable may be provided") + return 1 + } + if !enable && reason != "" { + c.Ui.Error("Reason may only be provided with -enable") + return 1 + } + if !enable && !disable && serviceID != "" { + c.Ui.Error("Service requires either -enable or -disable") + return 1 + } + + // Create and test the HTTP client + conf := api.DefaultConfig() + conf.Address = *httpAddr + conf.Token = token + client, err := api.NewClient(conf) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err)) + return 1 + } + a := client.Agent() + nodeName, err := a.NodeName() + if err != nil { + c.Ui.Error(fmt.Sprintf("Error querying Consul agent: %s", err)) + return 1 + } + + if !enable && !disable { + // List mode - list nodes/services in maintenance mode + checks, err := a.Checks() + if err != nil { + c.Ui.Error(fmt.Sprintf("Error getting checks: %s", err)) + return 1 + } + + for _, check := range checks { + if check.CheckID == "_node_maintenance" { + c.Ui.Output("Node:") + c.Ui.Output(" Name: " + nodeName) + c.Ui.Output(" Reason: " + check.Notes) + c.Ui.Output("") + } else if strings.HasPrefix(check.CheckID, "_service_maintenance:") { + c.Ui.Output("Service:") + c.Ui.Output(" ID: " + check.ServiceID) + c.Ui.Output(" Reason: " + check.Notes) + c.Ui.Output("") + } + } + + return 0 + } + + if enable { + // Enable node maintenance + if serviceID == "" { + if err := a.EnableNodeMaintenance(reason); err != nil { + c.Ui.Error(fmt.Sprintf("Error enabling node maintenance: %s", err)) + return 1 + } + c.Ui.Output("Node maintenance is now enabled") + return 0 + } + + // Enable service maintenance + if err := a.EnableServiceMaintenance(serviceID, reason); err != nil { + c.Ui.Error(fmt.Sprintf("Error enabling service maintenance: %s", err)) + return 1 + } + c.Ui.Output(fmt.Sprintf("Service maintenance is now enabled for %q", serviceID)) + return 0 + } + + if disable { + // Disable node maintenance + if serviceID == "" { + if err := a.DisableNodeMaintenance(); err != nil { + c.Ui.Error(fmt.Sprintf("Error disabling node maintenance: %s", err)) + return 1 + } + c.Ui.Output("Node maintenance is now disabled") + return 0 + } + + // Disable service maintenance + if err := a.DisableServiceMaintenance(serviceID); err != nil { + c.Ui.Error(fmt.Sprintf("Error disabling service maintenance: %s", err)) + return 1 + } + c.Ui.Output(fmt.Sprintf("Service maintenance is now disabled for %q", serviceID)) + return 0 + } + + return 0 +} + +func (c *MaintCommand) Synopsis() string { + return "Controls node or service maintenance mode" +} diff --git a/command/maint_test.go b/command/maint_test.go new file mode 100644 index 000000000000..92bf02b7679e --- /dev/null +++ b/command/maint_test.go @@ -0,0 +1,210 @@ +package command + +import ( + "strings" + "testing" + + "github.com/hashicorp/consul/consul/structs" + "github.com/mitchellh/cli" +) + +func TestMaintCommand_implements(t *testing.T) { + var _ cli.Command = &MaintCommand{} +} + +func TestMaintCommandRun_ConflictingArgs(t *testing.T) { + ui := new(cli.MockUi) + c := &MaintCommand{Ui: ui} + + if code := c.Run([]string{"-enable", "-disable"}); code != 1 { + t.Fatalf("expected return code 1, got %d", code) + } + + if code := c.Run([]string{"-disable", "-reason=broken"}); code != 1 { + t.Fatalf("expected return code 1, got %d", code) + } + + if code := c.Run([]string{"-reason=broken"}); code != 1 { + t.Fatalf("expected return code 1, got %d", code) + } + + if code := c.Run([]string{"-service=redis"}); code != 1 { + t.Fatalf("expected return code 1, got %d", code) + } +} + +func TestMaintCommandRun_NoArgs(t *testing.T) { + a1 := testAgent(t) + defer a1.Shutdown() + + // Register the service and put it into maintenance mode + service := &structs.NodeService{ + ID: "test", + Service: "test", + } + if err := a1.agent.AddService(service, nil, false); err != nil { + t.Fatalf("err: %v", err) + } + if err := a1.agent.EnableServiceMaintenance("test", "broken 1"); err != nil { + t.Fatalf("err: %s", err) + } + + // Enable node maintenance + a1.agent.EnableNodeMaintenance("broken 2") + + // Run consul maint with no args (list mode) + ui := new(cli.MockUi) + c := &MaintCommand{Ui: ui} + + args := []string{"-http-addr=" + a1.httpAddr} + code := c.Run(args) + if code != 0 { + t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) + } + + // Ensure the service shows up in the list + out := ui.OutputWriter.String() + if !strings.Contains(out, "test") { + t.Fatalf("bad:\n%s", out) + } + if !strings.Contains(out, "broken 1") { + t.Fatalf("bad:\n%s", out) + } + + // Ensure the node shows up in the list + if !strings.Contains(out, a1.config.NodeName) { + t.Fatalf("bad:\n%s", out) + } + if !strings.Contains(out, "broken 2") { + t.Fatalf("bad:\n%s", out) + } +} + +func TestMaintCommandRun_EnableNodeMaintenance(t *testing.T) { + a1 := testAgent(t) + defer a1.Shutdown() + + ui := new(cli.MockUi) + c := &MaintCommand{Ui: ui} + + args := []string{ + "-http-addr=" + a1.httpAddr, + "-enable", + "-reason=broken", + } + code := c.Run(args) + if code != 0 { + t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) + } + + if !strings.Contains(ui.OutputWriter.String(), "now enabled") { + t.Fatalf("bad: %#v", ui.OutputWriter.String()) + } +} + +func TestMaintCommandRun_DisableNodeMaintenance(t *testing.T) { + a1 := testAgent(t) + defer a1.Shutdown() + + ui := new(cli.MockUi) + c := &MaintCommand{Ui: ui} + + args := []string{ + "-http-addr=" + a1.httpAddr, + "-disable", + } + code := c.Run(args) + if code != 0 { + t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) + } + + if !strings.Contains(ui.OutputWriter.String(), "now disabled") { + t.Fatalf("bad: %#v", ui.OutputWriter.String()) + } +} + +func TestMaintCommandRun_EnableServiceMaintenance(t *testing.T) { + a1 := testAgent(t) + defer a1.Shutdown() + + // Register the service + service := &structs.NodeService{ + ID: "test", + Service: "test", + } + if err := a1.agent.AddService(service, nil, false); err != nil { + t.Fatalf("err: %v", err) + } + + ui := new(cli.MockUi) + c := &MaintCommand{Ui: ui} + + args := []string{ + "-http-addr=" + a1.httpAddr, + "-enable", + "-service=test", + "-reason=broken", + } + code := c.Run(args) + if code != 0 { + t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) + } + + if !strings.Contains(ui.OutputWriter.String(), "now enabled") { + t.Fatalf("bad: %#v", ui.OutputWriter.String()) + } +} + +func TestMaintCommandRun_DisableServiceMaintenance(t *testing.T) { + a1 := testAgent(t) + defer a1.Shutdown() + + // Register the service + service := &structs.NodeService{ + ID: "test", + Service: "test", + } + if err := a1.agent.AddService(service, nil, false); err != nil { + t.Fatalf("err: %v", err) + } + + ui := new(cli.MockUi) + c := &MaintCommand{Ui: ui} + + args := []string{ + "-http-addr=" + a1.httpAddr, + "-disable", + "-service=test", + } + code := c.Run(args) + if code != 0 { + t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) + } + + if !strings.Contains(ui.OutputWriter.String(), "now disabled") { + t.Fatalf("bad: %#v", ui.OutputWriter.String()) + } +} + +func TestMaintCommandRun_ServiceMaintenance_NoService(t *testing.T) { + a1 := testAgent(t) + defer a1.Shutdown() + + ui := new(cli.MockUi) + c := &MaintCommand{Ui: ui} + + args := []string{ + "-http-addr=" + a1.httpAddr, + "-enable", + "-service=redis", + "-reason=broken", + } + code := c.Run(args) + if code != 1 { + t.Fatalf("expected response code 1, got %d", code) + } + + if !strings.Contains(ui.ErrorWriter.String(), "No service registered") { + t.Fatalf("bad: %#v", ui.ErrorWriter.String()) + } +} diff --git a/commands.go b/commands.go index 13721975ea7e..d4e9aa0599ee 100644 --- a/commands.go +++ b/commands.go @@ -76,6 +76,12 @@ func init() { }, nil }, + "maint": func() (cli.Command, error) { + return &command.MaintCommand{ + Ui: ui, + }, nil + }, + "members": func() (cli.Command, error) { return &command.MembersCommand{ Ui: ui, diff --git a/website/source/docs/agent/http/agent.html.markdown b/website/source/docs/agent/http/agent.html.markdown index ca15adebbba3..1fa3f5a5cd23 100644 --- a/website/source/docs/agent/http/agent.html.markdown +++ b/website/source/docs/agent/http/agent.html.markdown @@ -195,6 +195,10 @@ persistent and will be automatically restored on agent restart. The `?enable` flag is required, and its value must be `true` (to enter maintenance mode), or `false` (to resume normal operation). +The `?reason` flag is optional, and can contain a text string explaining the +reason for placing the node into maintenance mode. If no reason is provided, +a default value will be used instead. + The return code is 200 on success. ### /v1/agent/join/\ @@ -355,4 +359,8 @@ on agent restart. The `?enable` flag is required, and its value must be `true` (to enter maintenance mode), or `false` (to resume normal operation). +The `?reason` flag is optional, and can contain a text string explaining the +reason for placing the service into maintenance mode. If no reason is provided, +a default value will be used instead. + The return code is 200 on success. diff --git a/website/source/docs/commands/maint.html.markdown b/website/source/docs/commands/maint.html.markdown new file mode 100644 index 000000000000..f906332e235e --- /dev/null +++ b/website/source/docs/commands/maint.html.markdown @@ -0,0 +1,68 @@ +--- +layout: "docs" +page_title: "Commands: Maint" +sidebar_current: "docs-commands-maint" +description: > + The `maint` command provides control of both service and node maintenance mode +--- + +# Consul Maint + +Command: `consul maint` + +The `maint` command provides control of both service and node maintenance mode. +Using the command, it is possible to mark a service provided by a node or the +node as a whole as "under maintenance". In this mode of operation, the service +or node will not appear in DNS query results, or API results. This effectively +takes the service or node out of the pool of available "healthy" nodes. + +Under the hood, maintenance mode is activated by registering a health check in +critical status against a node or service, and deactivated by deregistering the +health check. + +## Usage + +Usage: `consul maint [options]` + +All of the command line arguments are optional. + +The list of available flags are: + +* `-enable` - Enable maintenance mode on a given service or node. If + combined with the `-service` flag, we operate on a specific service ID. + Otherwise, node maintenance mode is enabled. + +* `-disable` - Disable maintenance mode on a given service or node. If + combined with the `-service` flag, we operate on a specific service ID. + Otherwise, node maintenance mode is disabled. + +* `-reason` - An optional reason for placing the node or service into + maintenance mode. If provided, this reason will be visible in the newly- + registered critical check's "Notes" field. + +* `-service` - An optional service ID to control node maintenance mode for. By + providing this flag, the `-enable` and `-disable` flags functionality is + modified to operate on the given service ID. + +* `-token` - ACL token to use. Defaults to that of agent. + +* `-http-addr` - Address to the HTTP server of the agent you want to contact + to send this command. If this isn't specified, the command will contact + "127.0.0.1:8500" which is the default HTTP address of a Consul agent. + +## List mode + +If neither `-enable` nor `-disable` are passed, the `maint` command will +switch to "list mode", displaying any current maintenances. This may return +blank if nothing is currently under maintenance. The output will look like: + +``` +$ consul maint +Node: + Name: node1.local + Reason: This node is broken. + +Service: + ID: redis + Reason: Redis is currently offline. +``` diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 87be2f79df91..7b99f15fae3c 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -91,6 +91,10 @@ lock + > + maint + + > members