You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
consul/command/members/members.go

297 lines
8.1 KiB

// Copyright (c) HashiCorp, Inc.
[COMPLIANCE] License changes (#18443) * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at <Blog URL>, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * add missing license headers * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
1 year ago
// SPDX-License-Identifier: BUSL-1.1
package members
import (
7 years ago
"flag"
"fmt"
"net"
"regexp"
"sort"
"strings"
"github.com/hashicorp/serf/serf"
"github.com/mitchellh/cli"
"github.com/ryanuber/columnize"
"github.com/hashicorp/consul/acl"
consulapi "github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/flags"
)
// cmd is a Command implementation that queries a running
// Consul agent what members are part of the cluster currently.
type cmd struct {
UI cli.Ui
help string
flags *flag.FlagSet
http *flags.HTTPFlags
// flags
detailed bool
wan bool
statusFilter string
segment string
filter string
}
func New(ui cli.Ui) *cmd {
c := &cmd{UI: ui}
c.init()
return c
}
func (c *cmd) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
c.flags.BoolVar(&c.detailed, "detailed", false,
"Provides detailed information about nodes.")
c.flags.BoolVar(&c.wan, "wan", false,
"If the agent is in server mode, this can be used to return the other "+
"peers in the WAN pool.")
c.flags.StringVar(&c.statusFilter, "status", ".*",
"If provided, output is filtered to only nodes matching the regular "+
"expression for status.")
c.flags.StringVar(&c.segment, "segment", consulapi.AllSegments,
"(Enterprise-only) If provided, output is filtered to only nodes in"+
"the given segment.")
c.flags.StringVar(&c.filter, "filter", "", "Filter to use with the request")
c.http = &flags.HTTPFlags{}
flags.Merge(c.flags, c.http.ClientFlags())
flags.Merge(c.flags, c.http.PartitionFlag())
c.help = flags.Usage(help, c.flags)
}
func (c *cmd) Run(args []string) int {
if err := c.flags.Parse(args); err != nil {
return 1
}
// Compile the regexp
statusRe, err := regexp.Compile(c.statusFilter)
if err != nil {
c.UI.Error(fmt.Sprintf("Failed to compile status regexp: %v", err))
return 1
}
client, err := c.http.APIClient()
if err != nil {
c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
}
// Make the request.
opts := consulapi.MembersOpts{
Segment: c.segment,
WAN: c.wan,
Filter: c.filter,
}
members, err := client.Agent().MembersOpts(opts)
if err != nil {
c.UI.Error(fmt.Sprintf("Error retrieving members: %s", err))
return 1
}
// Filter the results
n := len(members)
for i := 0; i < n; i++ {
member := members[i]
if member.Tags[consulapi.MemberTagKeyPartition] == "" {
member.Tags[consulapi.MemberTagKeyPartition] = "default"
}
if acl.IsDefaultPartition(member.Tags[consulapi.MemberTagKeyPartition]) {
if c.segment == consulapi.AllSegments && member.Tags[consulapi.MemberTagKeyRole] == consulapi.MemberTagValueRoleServer {
member.Tags[consulapi.MemberTagKeySegment] = "<all>"
} else if member.Tags[consulapi.MemberTagKeySegment] == "" {
member.Tags[consulapi.MemberTagKeySegment] = "<default>"
}
} else {
member.Tags[consulapi.MemberTagKeySegment] = ""
}
statusString := serf.MemberStatus(member.Status).String()
if !statusRe.MatchString(statusString) {
members[i], members[n-1] = members[n-1], members[i]
i--
n--
continue
}
}
members = members[:n]
// No matching members
if len(members) == 0 {
return 2
}
sort.Sort(ByMemberNamePartitionAndSegment(members))
// Generate the output
var result []string
if c.detailed {
result = c.detailedOutput(members)
} else {
result = c.standardOutput(members)
}
// Generate the columnized version
output := columnize.Format(result, &columnize.Config{Delim: string([]byte{0x1f})})
c.UI.Output(output)
return 0
}
// ByMemberNamePartitionAndSegment sorts members by name with a stable sort.
//
// 1. servers go at the top
// 2. members of the default partition go next (including segments)
// 3. members of partitions follow
type ByMemberNamePartitionAndSegment []*consulapi.AgentMember
func (m ByMemberNamePartitionAndSegment) Len() int { return len(m) }
func (m ByMemberNamePartitionAndSegment) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
func (m ByMemberNamePartitionAndSegment) Less(i, j int) bool {
tags_i := parseTags(m[i].Tags)
tags_j := parseTags(m[j].Tags)
// put role=consul first
switch {
case tags_i.role == consulapi.MemberTagValueRoleServer && tags_j.role != consulapi.MemberTagValueRoleServer:
return true
case tags_i.role != consulapi.MemberTagValueRoleServer && tags_j.role == consulapi.MemberTagValueRoleServer:
return false
}
// then the default partitions
switch {
case isDefault(tags_i.partition) && !isDefault(tags_j.partition):
return true
case !isDefault(tags_i.partition) && isDefault(tags_j.partition):
return false
}
// then by segments within the default
switch {
case tags_i.segment < tags_j.segment:
return true
case tags_i.segment > tags_j.segment:
return false
}
// then by partitions
switch {
case tags_i.partition < tags_j.partition:
return true
case tags_i.partition > tags_j.partition:
return false
}
// finally by name
return m[i].Name < m[j].Name
}
func isDefault(s string) bool {
// NOTE: we can't use structs.IsDefaultPartition since that discards the input
return s == "" || s == "default"
}
// standardOutput is used to dump the most useful information about nodes
// in a more human-friendly format
func (c *cmd) standardOutput(members []*consulapi.AgentMember) []string {
result := make([]string, 0, len(members))
header := "Node\x1fAddress\x1fStatus\x1fType\x1fBuild\x1fProtocol\x1fDC\x1fPartition\x1fSegment"
result = append(result, header)
for _, member := range members {
tags := parseTags(member.Tags)
addr := net.TCPAddr{IP: net.ParseIP(member.Addr), Port: int(member.Port)}
protocol := member.Tags["vsn"]
build := member.Tags["build"]
if build == "" {
build = "< 0.3"
} else if idx := strings.Index(build, ":"); idx != -1 {
build = build[:idx]
}
statusString := serf.MemberStatus(member.Status).String()
switch tags.role {
case consulapi.MemberTagValueRoleClient:
line := fmt.Sprintf("%s\x1f%s\x1f%s\x1fclient\x1f%s\x1f%s\x1f%s\x1f%s\x1f%s",
member.Name, addr.String(), statusString, build, protocol, tags.datacenter, tags.partition, tags.segment)
result = append(result, line)
case consulapi.MemberTagValueRoleServer:
line := fmt.Sprintf("%s\x1f%s\x1f%s\x1fserver\x1f%s\x1f%s\x1f%s\x1f%s\x1f%s",
member.Name, addr.String(), statusString, build, protocol, tags.datacenter, tags.partition, tags.segment)
result = append(result, line)
default:
line := fmt.Sprintf("%s\x1f%s\x1f%s\x1funknown\x1f\x1f\x1f\x1f\x1f",
member.Name, addr.String(), statusString)
result = append(result, line)
}
}
return result
}
type decodedTags struct {
role string
segment string
partition string
datacenter string
}
func parseTags(tags map[string]string) decodedTags {
return decodedTags{
role: tags[consulapi.MemberTagKeyRole],
segment: tags[consulapi.MemberTagKeySegment],
partition: tags[consulapi.MemberTagKeyPartition],
datacenter: tags[consulapi.MemberTagKeyDatacenter],
}
}
// detailedOutput is used to dump all known information about nodes in
// their raw format
func (c *cmd) detailedOutput(members []*consulapi.AgentMember) []string {
result := make([]string, 0, len(members))
header := "Node\x1fAddress\x1fStatus\x1fTags"
result = append(result, header)
for _, member := range members {
// Get the tags sorted by key
tagKeys := make([]string, 0, len(member.Tags))
for key := range member.Tags {
tagKeys = append(tagKeys, key)
}
sort.Strings(tagKeys)
// Format the tags as tag1=v1,tag2=v2,...
var tagPairs []string
for _, key := range tagKeys {
tagPairs = append(tagPairs, fmt.Sprintf("%s=%s", key, member.Tags[key]))
}
tags := strings.Join(tagPairs, ",")
addr := net.TCPAddr{IP: net.ParseIP(member.Addr), Port: int(member.Port)}
line := fmt.Sprintf("%s\x1f%s\x1f%s\x1f%s",
member.Name, addr.String(), serf.MemberStatus(member.Status).String(), tags)
result = append(result, line)
}
return result
}
func (c *cmd) Synopsis() string {
return synopsis
}
func (c *cmd) Help() string {
return c.help
}
const synopsis = "Lists the members of a Consul cluster"
const help = `
Usage: consul members [options]
Outputs the members of a running Consul agent.
`