mirror of
https://github.com/status-im/consul.git
synced 2025-01-09 05:23:04 +00:00
5fb9df1640
* 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>
297 lines
8.1 KiB
Go
297 lines
8.1 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package members
|
|
|
|
import (
|
|
"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.
|
|
`
|