├── .github ├── dependabot.yaml └── workflows │ ├── check.yml │ └── website.yml ├── .gitignore ├── .golangci.yml ├── .release └── release-metadata.hcl ├── CHANGELOG.md ├── CODEOWNERS ├── GNUmakefile ├── LICENSE ├── README.md ├── client ├── README.md ├── const.go └── rpc_client.go ├── cmd └── serf │ ├── .gitignore │ ├── command │ ├── agent │ │ ├── agent.go │ │ ├── agent_test.go │ │ ├── command.go │ │ ├── command_test.go │ │ ├── config.go │ │ ├── config_test.go │ │ ├── event_handler.go │ │ ├── event_handler_mock.go │ │ ├── event_handler_test.go │ │ ├── flag_slice_value.go │ │ ├── flag_slice_value_test.go │ │ ├── gated_writer.go │ │ ├── gated_writer_test.go │ │ ├── invoke.go │ │ ├── ipc.go │ │ ├── ipc_event_stream.go │ │ ├── ipc_event_stream_test.go │ │ ├── ipc_log_stream.go │ │ ├── ipc_log_stream_test.go │ │ ├── ipc_query_response_stream.go │ │ ├── log_levels.go │ │ ├── log_writer.go │ │ ├── log_writer_test.go │ │ ├── mdns.go │ │ ├── rpc_client_test.go │ │ ├── syslog.go │ │ ├── syslog_test.go │ │ ├── util.go │ │ └── util_test.go │ ├── event.go │ ├── event_test.go │ ├── force_leave.go │ ├── force_leave_test.go │ ├── info.go │ ├── info_test.go │ ├── join.go │ ├── join_test.go │ ├── keygen.go │ ├── keygen_test.go │ ├── keys.go │ ├── keys_test.go │ ├── leave.go │ ├── leave_test.go │ ├── members.go │ ├── members_test.go │ ├── monitor.go │ ├── output.go │ ├── output_test.go │ ├── query.go │ ├── query_test.go │ ├── reachability.go │ ├── reachability_test.go │ ├── rpc.go │ ├── rtt.go │ ├── rtt_test.go │ ├── tags.go │ ├── tags_test.go │ ├── util_test.go │ └── version.go │ ├── commands.go │ └── main.go ├── coordinate ├── client.go ├── client_test.go ├── config.go ├── coordinate.go ├── coordinate_test.go ├── performance_test.go ├── phantom.go └── util_test.go ├── demo ├── vagrant-cluster │ ├── README.md │ └── Vagrantfile └── web-load-balancer │ ├── README.md │ ├── cloudformation.json │ ├── setup_load_balancer.sh │ ├── setup_serf.sh │ ├── setup_web_server.sh │ └── user_data │ ├── README.md │ ├── dump_user_data.py │ ├── load_balancer_user_data.sh │ └── web_user_data.sh ├── docs ├── agent │ ├── basics.html.markdown │ ├── encryption.html.markdown │ ├── event-handlers.html.markdown │ ├── logging.html.markdown │ ├── options.html.markdown │ ├── rpc.html.markdown │ └── telemetry.html.markdown ├── commands │ ├── agent.html.markdown │ ├── event.html.markdown │ ├── force-leave.html.markdown │ ├── index.html.markdown │ ├── info.html.markdown │ ├── join.html.markdown │ ├── keygen.html.markdown │ ├── keys.html.markdown │ ├── leave.html.markdown │ ├── members.html.markdown │ ├── monitor.html.markdown │ ├── query.html.markdown │ ├── reachability.html.markdown │ ├── rtt.html.markdown │ └── tags.html.markdown ├── compatibility.html.markdown ├── index.html.markdown ├── internals │ ├── coordinates.html.markdown │ ├── gossip.html.markdown │ ├── index.html.markdown │ ├── security.html.markdown │ └── simulator.html.erb ├── intro │ ├── getting-started │ │ ├── agent.html.markdown │ │ ├── event-handlers.html.markdown │ │ ├── install.html.markdown │ │ ├── join.html.markdown │ │ ├── next-steps.html.markdown │ │ ├── queries.html.markdown │ │ └── user-events.html.markdown │ ├── index.html.markdown │ ├── use-cases.html.markdown │ ├── vs-chef-puppet.html.markdown │ ├── vs-consul.html.markdown │ ├── vs-custom.html.markdown │ ├── vs-fabric.html.markdown │ ├── vs-other-sw.html.markdown │ └── vs-zookeeper.html.markdown ├── recipes.html.markdown ├── recipes │ ├── agent-uptime.html.markdown │ └── event-handler-router.html.markdown └── upgrading.html.markdown ├── go.mod ├── go.sum ├── ops-misc ├── README.md ├── debian │ ├── changelog │ ├── compat │ ├── control │ ├── copyright │ ├── dirs │ ├── install │ ├── postinst │ ├── postrm │ ├── rules │ ├── serf.init │ ├── serf.json.example │ └── serf.upstart ├── io.serfdom.serf.plist ├── serf.sysv.init └── systemd.conf ├── scripts ├── build.sh ├── dist.sh ├── dist_build.sh ├── serf-builder │ └── Dockerfile ├── setup_test_subnet.sh └── start_cluster.sh ├── serf ├── broadcast.go ├── broadcast_test.go ├── coalesce.go ├── coalesce_member.go ├── coalesce_member_test.go ├── coalesce_test.go ├── coalesce_user.go ├── coalesce_user_test.go ├── config.go ├── config_test.go ├── conflict_delegate.go ├── delegate.go ├── delegate_test.go ├── event.go ├── event_delegate.go ├── event_test.go ├── internal │ └── race │ │ ├── race_disabled.go │ │ └── race_enabled.go ├── internal_query.go ├── internal_query_test.go ├── keymanager.go ├── keymanager_test.go ├── lamport.go ├── lamport_test.go ├── merge_delegate.go ├── merge_delegate_test.go ├── messages.go ├── messages_test.go ├── ping_delegate.go ├── query.go ├── query_test.go ├── serf.go ├── serf_internals_test.go ├── serf_test.go ├── snapshot.go └── snapshot_test.go ├── testutil ├── addr.go ├── retry │ ├── retry.go │ └── retry_test.go ├── testlog.go ├── testutil_test.go └── yield.go └── version └── version.go /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | version: 2 5 | 6 | updates: 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | - package-ecosystem: "gomod" 12 | directory: "/" 13 | schedule: 14 | interval: "weekly" 15 | -------------------------------------------------------------------------------- /.github/workflows/website.yml: -------------------------------------------------------------------------------- 1 | name: website 2 | 3 | on: [workflow_dispatch] 4 | 5 | permissions: 6 | contents: none 7 | 8 | jobs: 9 | build-website: 10 | runs-on: ubuntu-20.04 11 | container: 12 | image: docker.mirror.hashicorp.services/hashicorp/middleman-hashicorp:0.3.47 13 | env: 14 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 15 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 16 | FASTLY_API_TOKEN: ${{ secrets.FASTLY_API_TOKEN }} 17 | defaults: 18 | run: 19 | working-directory: website 20 | steps: 21 | - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 22 | - name: install gems 23 | run: bundle install --path vendor/bundle --retry=3 24 | - name: middleman build 25 | run: bundle exec middleman build 26 | - name: website deploy 27 | run: ./scripts/deploy.sh 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Platform 2 | .DS_Store 3 | /.idea 4 | *.iml 5 | 6 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 7 | *.o 8 | *.a 9 | *.so 10 | 11 | # Folders 12 | /bin/ 13 | /pkg/ 14 | 15 | # Architecture specific extensions/prefixes 16 | *.[568vq] 17 | [568vq].out 18 | 19 | *.cgo1.go 20 | *.cgo2.c 21 | _cgo_defun.c 22 | _cgo_gotypes.go 23 | _cgo_export.* 24 | 25 | _testmain.go 26 | 27 | *.exe 28 | *.test 29 | 30 | # Website 31 | website/build/ 32 | website/.bundle 33 | website/vendor 34 | .vagrant 35 | /coverage.out 36 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | linters: 5 | disable-all: true 6 | enable: 7 | # (TODO) uncomment after fixing the lint issues 8 | # - gofmt 9 | - govet 10 | # - unconvert 11 | # - staticcheck 12 | # - ineffassign 13 | # - unparam 14 | - forbidigo 15 | 16 | issues: 17 | # Disable the default exclude list so that all excludes are explicitly 18 | # defined in this file. 19 | exclude-use-default: false 20 | 21 | exclude-rules: 22 | # Temp Ignore SA9004: only the first constant in this group has an explicit type 23 | # https://staticcheck.io/docs/checks#SA9004 24 | - linters: [staticcheck] 25 | text: 'SA9004:' 26 | 27 | - linters: [staticcheck] 28 | text: 'SA1019: Package github.com/golang/protobuf/jsonpb is deprecated' 29 | 30 | - linters: [staticcheck] 31 | text: 'SA1019: Package github.com/golang/protobuf/proto is deprecated' 32 | 33 | - linters: [staticcheck] 34 | text: 'SA1019: ptypes.MarshalAny is deprecated' 35 | 36 | - linters: [staticcheck] 37 | text: 'SA1019: ptypes.UnmarshalAny is deprecated' 38 | 39 | - linters: [staticcheck] 40 | text: 'SA1019: package github.com/golang/protobuf/ptypes is deprecated' 41 | 42 | # An argument that always receives the same value is often not a problem. 43 | - linters: [unparam] 44 | text: 'always receives' 45 | 46 | # Often functions will implement an interface that returns an error without 47 | # needing to return an error. Sometimes the error return value is unnecessary 48 | # but a linter can not tell the difference. 49 | - linters: [unparam] 50 | text: 'result \d+ \(error\) is always nil' 51 | 52 | # Allow unused parameters to start with an underscore. Arguments with a name 53 | # of '_' are already ignored. 54 | # Ignoring longer names that start with underscore allow for better 55 | # self-documentation than a single underscore by itself. Underscore arguments 56 | # should generally only be used when a function is implementing an interface. 57 | - linters: [unparam] 58 | text: '`_[^`]*` is unused' 59 | 60 | # Temp ignore some common unused parameters so that unparam can be added 61 | # incrementally. 62 | - linters: [unparam] 63 | text: '`(t|resp|req|entMeta)` is unused' 64 | 65 | linters-settings: 66 | gofmt: 67 | simplify: true 68 | forbidigo: 69 | # Forbid the following identifiers (list of regexp). 70 | forbid: 71 | - '\brequire\.New\b(# Use package-level functions with explicit TestingT)?' 72 | - '\bassert\.New\b(# Use package-level functions with explicit TestingT)?' 73 | - '\bmetrics\.IncrCounter\b(# Use labeled metrics)?' 74 | - '\bmetrics\.AddSample\b(# Use labeled metrics)?' 75 | - '\bmetrics\.MeasureSince\b(# Use labeled metrics)?' 76 | - '\bmetrics\.SetGauge\b(# Use labeled metrics)?' 77 | # Exclude godoc examples from forbidigo checks. 78 | # Default: true 79 | exclude_godoc_examples: false 80 | 81 | run: 82 | timeout: 10m 83 | concurrency: 4 84 | -------------------------------------------------------------------------------- /.release/release-metadata.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | url_license = "https://github.com/hashicorp/serf/blob/master/LICENSE" 5 | url_project_website = "https://github.com/hashicorp/serf" 6 | url_source_repository = "https://github.com/hashicorp/serf" -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Each line is a file pattern followed by one or more owners. 2 | # More on CODEOWNERS files: https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners 3 | 4 | # Default owner 5 | * @hashicorp/team-ip-compliance @hashicorp/consul-core-reviewers 6 | 7 | # Add override rules below. Each line is a file/folder pattern followed by one or more owners. 8 | # Being an owner means those groups or individuals will be added as reviewers to PRs affecting 9 | # those areas of the code. 10 | # Examples: 11 | # /docs/ @docs-team 12 | # *.js @js-team 13 | # *.go @go-team 14 | -------------------------------------------------------------------------------- /GNUmakefile: -------------------------------------------------------------------------------- 1 | SHELL := bash 2 | 3 | GOTOOLS := github.com/mitchellh/gox 4 | VERSION := $(shell awk -F\" '/Version = "(.*)"/ { print $$2; exit }' version/version.go) 5 | GITSHA:=$(shell git rev-parse HEAD) 6 | GITBRANCH:=$(shell git symbolic-ref --short HEAD 2>/dev/null) 7 | 8 | GOFILES ?= $(shell go list ./...) 9 | 10 | default: test 11 | 12 | # bin generates the releasable binaries 13 | bin: tools 14 | @sh -c "'$(CURDIR)/scripts/build.sh'" 15 | 16 | cov: 17 | go test ./... -coverprofile=coverage.out 18 | go tool cover -html=coverage.out 19 | 20 | format: 21 | @echo "--> Running go fmt" 22 | @go fmt $(GOFILES) 23 | 24 | # dev creates binaries for testing locally - these are put into ./bin and 25 | # $GOPATH 26 | dev: 27 | @SERF_DEV=1 sh -c "'$(CURDIR)/scripts/build.sh'" 28 | 29 | # dist creates the binaries for distibution 30 | dist: 31 | @sh -c "'$(CURDIR)/scripts/dist.sh' $(VERSION)" 32 | 33 | get-tools: 34 | go get -u -v $(GOTOOLS) 35 | 36 | # subnet sets up the require subnet for testing on darwin (osx) - you must run 37 | # this before running other tests if you are on osx. 38 | subnet: 39 | @sh -c "'$(CURDIR)/scripts/setup_test_subnet.sh'" 40 | 41 | # test runs the test suite 42 | test: subnet vet 43 | go test ./... 44 | 45 | # testrace runs the race checker 46 | testrace: subnet vet 47 | go test -race ./... $(TESTARGS) 48 | 49 | tools: 50 | @which gox 2>/dev/null ; if [ $$? -eq 1 ]; then \ 51 | $(MAKE) get-tools; \ 52 | fi 53 | 54 | vet: 55 | @echo "--> Running go vet" 56 | @go vet -tags '$(GOTAGS)' $(GOFILES); if [ $$? -eq 1 ]; then \ 57 | echo ""; \ 58 | echo "Vet found suspicious constructs. Please check the reported constructs"; \ 59 | echo "and fix them if necessary before submitting the code for review."; \ 60 | exit 1; \ 61 | fi 62 | 63 | .PHONY: default bin cov format dev dist get-tools subnet test testrace tools vet 64 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # Serf Client 2 | 3 | This repo provide the `client` package, which is used to interact with 4 | a Serf agent using the msgpack RPC system it supports. This is the official 5 | reference implementation, and is used inside the Serf CLI to support the various 6 | commands. 7 | 8 | Full documentation can be found on [godoc here](https://godoc.org/github.com/hashicorp/serf/client). 9 | -------------------------------------------------------------------------------- /cmd/serf/.gitignore: -------------------------------------------------------------------------------- 1 | /serf 2 | -------------------------------------------------------------------------------- /cmd/serf/command/agent/event_handler_mock.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package agent 5 | 6 | import ( 7 | "sync" 8 | 9 | "github.com/hashicorp/serf/serf" 10 | ) 11 | 12 | // MockEventHandler is an EventHandler implementation that can be used 13 | // for tests. 14 | type MockEventHandler struct { 15 | Events []serf.Event 16 | sync.Mutex 17 | } 18 | 19 | func (h *MockEventHandler) HandleEvent(e serf.Event) { 20 | h.Lock() 21 | defer h.Unlock() 22 | h.Events = append(h.Events, e) 23 | } 24 | 25 | // MockQueryHandler is an EventHandler implementation used for tests, 26 | // it always responds to a query with a given response 27 | type MockQueryHandler struct { 28 | Response []byte 29 | Queries []*serf.Query 30 | sync.Mutex 31 | } 32 | 33 | func (h *MockQueryHandler) HandleEvent(e serf.Event) { 34 | query, ok := e.(*serf.Query) 35 | if !ok { 36 | return 37 | } 38 | 39 | h.Lock() 40 | h.Queries = append(h.Queries, query) 41 | h.Unlock() 42 | 43 | query.Respond(h.Response) 44 | } 45 | -------------------------------------------------------------------------------- /cmd/serf/command/agent/flag_slice_value.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package agent 5 | 6 | import ( 7 | "flag" 8 | "strings" 9 | ) 10 | 11 | // AppendSliceValue implements the flag.Value interface and allows multiple 12 | // calls to the same variable to append a list. 13 | type AppendSliceValue []string 14 | 15 | var _ flag.Value = new(AppendSliceValue) 16 | 17 | func (s *AppendSliceValue) String() string { 18 | return strings.Join(*s, ",") 19 | } 20 | 21 | func (s *AppendSliceValue) Set(value string) error { 22 | if *s == nil { 23 | *s = make([]string, 0, 1) 24 | } 25 | 26 | *s = append(*s, value) 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /cmd/serf/command/agent/flag_slice_value_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package agent 5 | 6 | import ( 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func TestAppendSliceValueSet(t *testing.T) { 12 | sv := new(AppendSliceValue) 13 | err := sv.Set("foo") 14 | if err != nil { 15 | t.Fatalf("err: %v", err) 16 | } 17 | 18 | err = sv.Set("bar") 19 | if err != nil { 20 | t.Fatalf("err: %v", err) 21 | } 22 | 23 | expected := []string{"foo", "bar"} 24 | if !reflect.DeepEqual([]string(*sv), expected) { 25 | t.Fatalf("Bad: %#v", sv) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /cmd/serf/command/agent/gated_writer.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package agent 5 | 6 | import ( 7 | "io" 8 | "sync" 9 | ) 10 | 11 | // GatedWriter is an io.Writer implementation that buffers all of its 12 | // data into an internal buffer until it is told to let data through. 13 | type GatedWriter struct { 14 | Writer io.Writer 15 | 16 | buf [][]byte 17 | flush bool 18 | lock sync.RWMutex 19 | } 20 | 21 | var _ io.Writer = &GatedWriter{} 22 | 23 | // Flush tells the GatedWriter to flush any buffered data and to stop 24 | // buffering. 25 | func (w *GatedWriter) Flush() { 26 | w.lock.Lock() 27 | w.flush = true 28 | w.lock.Unlock() 29 | 30 | for _, p := range w.buf { 31 | w.Write(p) 32 | } 33 | w.buf = nil 34 | } 35 | 36 | func (w *GatedWriter) Write(p []byte) (n int, err error) { 37 | w.lock.RLock() 38 | defer w.lock.RUnlock() 39 | 40 | if w.flush { 41 | return w.Writer.Write(p) 42 | } 43 | 44 | p2 := make([]byte, len(p)) 45 | copy(p2, p) 46 | w.buf = append(w.buf, p2) 47 | return len(p), nil 48 | } 49 | -------------------------------------------------------------------------------- /cmd/serf/command/agent/gated_writer_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package agent 5 | 6 | import ( 7 | "bytes" 8 | "testing" 9 | ) 10 | 11 | func TestGatedWriter(t *testing.T) { 12 | buf := new(bytes.Buffer) 13 | w := &GatedWriter{Writer: buf} 14 | w.Write([]byte("foo\n")) 15 | w.Write([]byte("bar\n")) 16 | 17 | if buf.String() != "" { 18 | t.Fatalf("bad: %s", buf.String()) 19 | } 20 | 21 | w.Flush() 22 | 23 | if buf.String() != "foo\nbar\n" { 24 | t.Fatalf("bad: %s", buf.String()) 25 | } 26 | 27 | w.Write([]byte("baz\n")) 28 | 29 | if buf.String() != "foo\nbar\nbaz\n" { 30 | t.Fatalf("bad: %s", buf.String()) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /cmd/serf/command/agent/ipc_log_stream.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package agent 5 | 6 | import ( 7 | "log" 8 | 9 | "github.com/hashicorp/logutils" 10 | ) 11 | 12 | // logStream is used to stream logs to a client over IPC 13 | type logStream struct { 14 | client streamClient 15 | filter *logutils.LevelFilter 16 | logCh chan string 17 | logger *log.Logger 18 | seq uint64 19 | } 20 | 21 | func newLogStream(client streamClient, filter *logutils.LevelFilter, 22 | seq uint64, logger *log.Logger) *logStream { 23 | ls := &logStream{ 24 | client: client, 25 | filter: filter, 26 | logCh: make(chan string, 512), 27 | logger: logger, 28 | seq: seq, 29 | } 30 | go ls.stream() 31 | return ls 32 | } 33 | 34 | func (ls *logStream) HandleLog(l string) { 35 | // Check the log level 36 | if !ls.filter.Check([]byte(l)) { 37 | return 38 | } 39 | 40 | // Do a non-blocking send 41 | select { 42 | case ls.logCh <- l: 43 | default: 44 | // We can't log syncronously, since we are already being invoked 45 | // from the logWriter, and a log will need to invoke Write() which 46 | // already holds the lock. We must therefor do the log async, so 47 | // as to not deadlock 48 | go ls.logger.Printf("[WARN] agent.ipc: Dropping logs to %v", ls.client) 49 | } 50 | } 51 | 52 | func (ls *logStream) Stop() { 53 | close(ls.logCh) 54 | } 55 | 56 | func (ls *logStream) stream() { 57 | header := responseHeader{Seq: ls.seq, Error: ""} 58 | rec := logRecord{Log: ""} 59 | 60 | for line := range ls.logCh { 61 | rec.Log = line 62 | if err := ls.client.Send(&header, &rec); err != nil { 63 | ls.logger.Printf("[ERR] agent.ipc: Failed to stream log to %v: %v", 64 | ls.client, err) 65 | return 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /cmd/serf/command/agent/ipc_log_stream_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package agent 5 | 6 | import ( 7 | "log" 8 | "os" 9 | "testing" 10 | "time" 11 | 12 | "github.com/hashicorp/logutils" 13 | ) 14 | 15 | func TestIPCLogStream(t *testing.T) { 16 | sc := &MockStreamClient{} 17 | filter := LevelFilter() 18 | filter.MinLevel = logutils.LogLevel("INFO") 19 | 20 | ls := newLogStream(sc, filter, 42, log.New(os.Stderr, "", log.LstdFlags)) 21 | defer ls.Stop() 22 | 23 | log := "[DEBUG] this is a test log" 24 | log2 := "[INFO] This should pass" 25 | ls.HandleLog(log) 26 | ls.HandleLog(log2) 27 | 28 | time.Sleep(5 * time.Millisecond) 29 | 30 | if len(sc.headers) != 1 { 31 | t.Fatalf("expected 1 messages!") 32 | } 33 | for _, h := range sc.headers { 34 | if h.Seq != 42 { 35 | t.Fatalf("bad seq") 36 | } 37 | if h.Error != "" { 38 | t.Fatalf("bad err") 39 | } 40 | } 41 | 42 | obj1 := sc.objs[0].(*logRecord) 43 | if obj1.Log != log2 { 44 | t.Fatalf("bad event %#v", obj1) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /cmd/serf/command/agent/ipc_query_response_stream.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package agent 5 | 6 | import ( 7 | "log" 8 | "time" 9 | 10 | "github.com/hashicorp/serf/serf" 11 | ) 12 | 13 | // queryResponseStream is used to stream the query results back to a client 14 | type queryResponseStream struct { 15 | client streamClient 16 | logger *log.Logger 17 | seq uint64 18 | } 19 | 20 | func newQueryResponseStream(client streamClient, seq uint64, logger *log.Logger) *queryResponseStream { 21 | qs := &queryResponseStream{ 22 | client: client, 23 | logger: logger, 24 | seq: seq, 25 | } 26 | return qs 27 | } 28 | 29 | // Stream is a long running routine used to stream the results of a query back to a client 30 | func (qs *queryResponseStream) Stream(resp *serf.QueryResponse) { 31 | // Setup a timer for the query ending 32 | remaining := resp.Deadline().Sub(time.Now()) 33 | done := time.After(remaining) 34 | 35 | ackCh := resp.AckCh() 36 | respCh := resp.ResponseCh() 37 | for { 38 | select { 39 | case a := <-ackCh: 40 | if err := qs.sendAck(a); err != nil { 41 | qs.logger.Printf("[ERR] agent.ipc: Failed to stream ack to %v: %v", qs.client, err) 42 | return 43 | } 44 | case r := <-respCh: 45 | if err := qs.sendResponse(r.From, r.Payload); err != nil { 46 | qs.logger.Printf("[ERR] agent.ipc: Failed to stream response to %v: %v", qs.client, err) 47 | return 48 | } 49 | case <-done: 50 | if err := qs.sendDone(); err != nil { 51 | qs.logger.Printf("[ERR] agent.ipc: Failed to stream query end to %v: %v", qs.client, err) 52 | } 53 | return 54 | } 55 | } 56 | } 57 | 58 | // sendAck is used to send a single ack 59 | func (qs *queryResponseStream) sendAck(from string) error { 60 | header := responseHeader{ 61 | Seq: qs.seq, 62 | Error: "", 63 | } 64 | rec := queryRecord{ 65 | Type: queryRecordAck, 66 | From: from, 67 | } 68 | return qs.client.Send(&header, &rec) 69 | } 70 | 71 | // sendResponse is used to send a single response 72 | func (qs *queryResponseStream) sendResponse(from string, payload []byte) error { 73 | header := responseHeader{ 74 | Seq: qs.seq, 75 | Error: "", 76 | } 77 | rec := queryRecord{ 78 | Type: queryRecordResponse, 79 | From: from, 80 | Payload: payload, 81 | } 82 | return qs.client.Send(&header, &rec) 83 | } 84 | 85 | // sendDone is used to signal the end 86 | func (qs *queryResponseStream) sendDone() error { 87 | header := responseHeader{ 88 | Seq: qs.seq, 89 | Error: "", 90 | } 91 | rec := queryRecord{ 92 | Type: queryRecordDone, 93 | } 94 | return qs.client.Send(&header, &rec) 95 | } 96 | -------------------------------------------------------------------------------- /cmd/serf/command/agent/log_levels.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package agent 5 | 6 | import ( 7 | "io/ioutil" 8 | 9 | "github.com/hashicorp/logutils" 10 | ) 11 | 12 | // LevelFilter returns a LevelFilter that is configured with the log 13 | // levels that we use. 14 | func LevelFilter() *logutils.LevelFilter { 15 | return &logutils.LevelFilter{ 16 | Levels: []logutils.LogLevel{"TRACE", "DEBUG", "INFO", "WARN", "ERR"}, 17 | MinLevel: "INFO", 18 | Writer: ioutil.Discard, 19 | } 20 | } 21 | 22 | // ValidateLevelFilter verifies that the log levels within the filter 23 | // are valid. 24 | func ValidateLevelFilter(minLevel logutils.LogLevel, filter *logutils.LevelFilter) bool { 25 | for _, level := range filter.Levels { 26 | if level == minLevel { 27 | return true 28 | } 29 | } 30 | return false 31 | } 32 | -------------------------------------------------------------------------------- /cmd/serf/command/agent/log_writer.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package agent 5 | 6 | import ( 7 | "sync" 8 | ) 9 | 10 | // LogHandler interface is used for clients that want to subscribe 11 | // to logs, for example to stream them over an IPC mechanism 12 | type LogHandler interface { 13 | HandleLog(string) 14 | } 15 | 16 | // logWriter implements io.Writer so it can be used as a log sink. 17 | // It maintains a circular buffer of logs, and a set of handlers to 18 | // which it can stream the logs to. 19 | type logWriter struct { 20 | sync.Mutex 21 | logs []string 22 | index int 23 | handlers map[LogHandler]struct{} 24 | } 25 | 26 | // NewLogWriter creates a logWriter with the given buffer capacity 27 | func NewLogWriter(buf int) *logWriter { 28 | return &logWriter{ 29 | logs: make([]string, buf), 30 | index: 0, 31 | handlers: make(map[LogHandler]struct{}), 32 | } 33 | } 34 | 35 | // RegisterHandler adds a log handler to receive logs, and sends 36 | // the last buffered logs to the handler 37 | func (l *logWriter) RegisterHandler(lh LogHandler) { 38 | l.Lock() 39 | defer l.Unlock() 40 | 41 | // Do nothing if already registered 42 | if _, ok := l.handlers[lh]; ok { 43 | return 44 | } 45 | 46 | // Register 47 | l.handlers[lh] = struct{}{} 48 | 49 | // Send the old logs 50 | if l.logs[l.index] != "" { 51 | for i := l.index; i < len(l.logs); i++ { 52 | lh.HandleLog(l.logs[i]) 53 | } 54 | } 55 | for i := 0; i < l.index; i++ { 56 | lh.HandleLog(l.logs[i]) 57 | } 58 | } 59 | 60 | // DeregisterHandler removes a LogHandler and prevents more invocations 61 | func (l *logWriter) DeregisterHandler(lh LogHandler) { 62 | l.Lock() 63 | defer l.Unlock() 64 | delete(l.handlers, lh) 65 | } 66 | 67 | // Write is used to accumulate new logs 68 | func (l *logWriter) Write(p []byte) (n int, err error) { 69 | l.Lock() 70 | defer l.Unlock() 71 | 72 | // Strip off newlines at the end if there are any since we store 73 | // individual log lines in the agent. 74 | n = len(p) 75 | if p[n-1] == '\n' { 76 | p = p[:n-1] 77 | } 78 | 79 | l.logs[l.index] = string(p) 80 | l.index = (l.index + 1) % len(l.logs) 81 | 82 | for lh, _ := range l.handlers { 83 | lh.HandleLog(string(p)) 84 | } 85 | return 86 | } 87 | -------------------------------------------------------------------------------- /cmd/serf/command/agent/log_writer_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package agent 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | type MockLogHandler struct { 11 | logs []string 12 | } 13 | 14 | func (m *MockLogHandler) HandleLog(l string) { 15 | m.logs = append(m.logs, l) 16 | } 17 | 18 | func TestLogWriter(t *testing.T) { 19 | h := &MockLogHandler{} 20 | w := NewLogWriter(4) 21 | 22 | // Write some logs 23 | w.Write([]byte("one")) // Gets dropped! 24 | w.Write([]byte("two")) 25 | w.Write([]byte("three")) 26 | w.Write([]byte("four")) 27 | w.Write([]byte("five")) 28 | 29 | // Register a handler, sends old! 30 | w.RegisterHandler(h) 31 | 32 | w.Write([]byte("six")) 33 | w.Write([]byte("seven")) 34 | 35 | // Deregister 36 | w.DeregisterHandler(h) 37 | 38 | w.Write([]byte("eight")) 39 | w.Write([]byte("nine")) 40 | 41 | out := []string{ 42 | "two", 43 | "three", 44 | "four", 45 | "five", 46 | "six", 47 | "seven", 48 | } 49 | for idx := range out { 50 | if out[idx] != h.logs[idx] { 51 | t.Fatalf("mismatch %v", h.logs) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /cmd/serf/command/agent/syslog.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package agent 5 | 6 | import ( 7 | "bytes" 8 | 9 | "github.com/hashicorp/go-syslog" 10 | "github.com/hashicorp/logutils" 11 | ) 12 | 13 | // levelPriority is used to map a log level to a 14 | // syslog priority level 15 | var levelPriority = map[string]gsyslog.Priority{ 16 | "TRACE": gsyslog.LOG_DEBUG, 17 | "DEBUG": gsyslog.LOG_INFO, 18 | "INFO": gsyslog.LOG_NOTICE, 19 | "WARN": gsyslog.LOG_WARNING, 20 | "ERR": gsyslog.LOG_ERR, 21 | "CRIT": gsyslog.LOG_CRIT, 22 | } 23 | 24 | // SyslogWrapper is used to cleaup log messages before 25 | // writing them to a Syslogger. Implements the io.Writer 26 | // interface. 27 | type SyslogWrapper struct { 28 | l gsyslog.Syslogger 29 | filt *logutils.LevelFilter 30 | } 31 | 32 | // Write is used to implement io.Writer 33 | func (s *SyslogWrapper) Write(p []byte) (int, error) { 34 | // Skip syslog if the log level doesn't apply 35 | if !s.filt.Check(p) { 36 | return 0, nil 37 | } 38 | 39 | // Extract log level 40 | var level string 41 | afterLevel := p 42 | x := bytes.IndexByte(p, '[') 43 | if x >= 0 { 44 | y := bytes.IndexByte(p[x:], ']') 45 | if y >= 0 { 46 | level = string(p[x+1 : x+y]) 47 | afterLevel = p[x+y+2:] 48 | } 49 | } 50 | 51 | // Each log level will be handled by a specific syslog priority 52 | priority, ok := levelPriority[level] 53 | if !ok { 54 | priority = gsyslog.LOG_NOTICE 55 | } 56 | 57 | // Attempt the write 58 | err := s.l.WriteLevel(priority, afterLevel) 59 | return len(p), err 60 | } 61 | -------------------------------------------------------------------------------- /cmd/serf/command/agent/syslog_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package agent 5 | 6 | import ( 7 | "runtime" 8 | "testing" 9 | 10 | "github.com/hashicorp/go-syslog" 11 | "github.com/hashicorp/logutils" 12 | ) 13 | 14 | func TestSyslogFilter(t *testing.T) { 15 | if runtime.GOOS == "windows" { 16 | t.SkipNow() 17 | } 18 | l, err := gsyslog.NewLogger(gsyslog.LOG_NOTICE, "LOCAL0", "serf") 19 | if err != nil { 20 | t.Fatalf("err: %v", err) 21 | } 22 | 23 | filt := LevelFilter() 24 | filt.MinLevel = logutils.LogLevel("INFO") 25 | 26 | s := &SyslogWrapper{l, filt} 27 | n, err := s.Write([]byte("[INFO] test")) 28 | if err != nil { 29 | t.Fatalf("err: %v", err) 30 | } 31 | if n == 0 { 32 | t.Fatalf("should have logged") 33 | } 34 | 35 | n, err = s.Write([]byte("[DEBUG] test")) 36 | if err != nil { 37 | t.Fatalf("err: %v", err) 38 | } 39 | if n != 0 { 40 | t.Fatalf("should not have logged") 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /cmd/serf/command/agent/util.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package agent 5 | 6 | import ( 7 | "runtime" 8 | "strconv" 9 | ) 10 | 11 | // runtimeStats is used to return various runtime information 12 | func runtimeStats() map[string]string { 13 | return map[string]string{ 14 | "os": runtime.GOOS, 15 | "arch": runtime.GOARCH, 16 | "version": runtime.Version(), 17 | "max_procs": strconv.FormatInt(int64(runtime.GOMAXPROCS(0)), 10), 18 | "goroutines": strconv.FormatInt(int64(runtime.NumGoroutine()), 10), 19 | "cpu_count": strconv.FormatInt(int64(runtime.NumCPU()), 10), 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /cmd/serf/command/agent/util_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package agent 5 | 6 | import ( 7 | "io" 8 | "math/rand" 9 | "net" 10 | "testing" 11 | "time" 12 | 13 | "github.com/hashicorp/serf/serf" 14 | "github.com/hashicorp/serf/testutil" 15 | ) 16 | 17 | func init() { 18 | // Seed the random number generator 19 | rand.Seed(time.Now().UnixNano()) 20 | } 21 | 22 | func drainEventCh(ch <-chan string) { 23 | for { 24 | select { 25 | case <-ch: 26 | default: 27 | return 28 | } 29 | } 30 | } 31 | 32 | func testAgent(t *testing.T, ip net.IP, logOutput io.Writer) *Agent { 33 | return testAgentWithConfig(t, ip, DefaultConfig(), serf.DefaultConfig(), logOutput) 34 | } 35 | 36 | func testAgentWithConfig(t *testing.T, ip net.IP, agentConfig *Config, serfConfig *serf.Config, logOutput io.Writer) *Agent { 37 | serfConfig.MemberlistConfig.ProbeInterval = 100 * time.Millisecond 38 | serfConfig.MemberlistConfig.BindAddr = ip.String() 39 | serfConfig.NodeName = serfConfig.MemberlistConfig.BindAddr 40 | 41 | // Activate the strictest version of memberlist validation to ensure 42 | // we properly pass node names through the serf layer. 43 | serfConfig.MemberlistConfig.RequireNodeNames = true 44 | 45 | if logOutput == nil { 46 | logOutput = testutil.TestWriter(t) 47 | } 48 | 49 | agent, err := Create(agentConfig, serfConfig, logOutput) 50 | if err != nil { 51 | t.Fatalf("err: %v", err) 52 | } 53 | return agent 54 | } 55 | -------------------------------------------------------------------------------- /cmd/serf/command/event.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package command 5 | 6 | import ( 7 | "flag" 8 | "fmt" 9 | "strings" 10 | 11 | "github.com/mitchellh/cli" 12 | ) 13 | 14 | // EventCommand is a Command implementation that queries a running 15 | // Serf agent what members are part of the cluster currently. 16 | type EventCommand struct { 17 | Ui cli.Ui 18 | } 19 | 20 | var _ cli.Command = &EventCommand{} 21 | 22 | func (c *EventCommand) Help() string { 23 | helpText := ` 24 | Usage: serf event [options] name payload 25 | 26 | Dispatches a custom event across the Serf cluster. 27 | 28 | Options: 29 | 30 | -coalesce=true/false Whether this event can be coalesced. This means 31 | that repeated events of the same name within a 32 | short period of time are ignored, except the last 33 | one received. Default is true. 34 | -rpc-addr=127.0.0.1:7373 RPC address of the Serf agent. 35 | -rpc-auth="" RPC auth token of the Serf agent. 36 | ` 37 | return strings.TrimSpace(helpText) 38 | } 39 | 40 | func (c *EventCommand) Run(args []string) int { 41 | var coalesce bool 42 | 43 | cmdFlags := flag.NewFlagSet("event", flag.ContinueOnError) 44 | cmdFlags.Usage = func() { c.Ui.Output(c.Help()) } 45 | cmdFlags.BoolVar(&coalesce, "coalesce", true, "coalesce") 46 | rpcAddr := RPCAddrFlag(cmdFlags) 47 | rpcAuth := RPCAuthFlag(cmdFlags) 48 | if err := cmdFlags.Parse(args); err != nil { 49 | return 1 50 | } 51 | 52 | args = cmdFlags.Args() 53 | if len(args) < 1 { 54 | c.Ui.Error("An event name must be specified.") 55 | c.Ui.Error("") 56 | c.Ui.Error(c.Help()) 57 | return 1 58 | } else if len(args) > 2 { 59 | c.Ui.Error("Too many command line arguments. Only a name and payload must be specified.") 60 | c.Ui.Error("") 61 | c.Ui.Error(c.Help()) 62 | return 1 63 | } 64 | 65 | event := args[0] 66 | var payload []byte 67 | if len(args) == 2 { 68 | payload = []byte(args[1]) 69 | } 70 | 71 | client, err := RPCClient(*rpcAddr, *rpcAuth) 72 | if err != nil { 73 | c.Ui.Error(fmt.Sprintf("Error connecting to Serf agent: %s", err)) 74 | return 1 75 | } 76 | defer client.Close() 77 | 78 | if err := client.UserEvent(event, payload, coalesce); err != nil { 79 | c.Ui.Error(fmt.Sprintf("Error sending event: %s", err)) 80 | return 1 81 | } 82 | 83 | c.Ui.Output(fmt.Sprintf("Event '%s' dispatched! Coalescing enabled: %#v", 84 | event, coalesce)) 85 | return 0 86 | } 87 | 88 | func (c *EventCommand) Synopsis() string { 89 | return "Send a custom event through the Serf cluster" 90 | } 91 | -------------------------------------------------------------------------------- /cmd/serf/command/event_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package command 5 | 6 | import ( 7 | "strings" 8 | "testing" 9 | 10 | "github.com/mitchellh/cli" 11 | ) 12 | 13 | func TestEventCommandRun_noEvent(t *testing.T) { 14 | ui := new(cli.MockUi) 15 | c := &EventCommand{Ui: ui} 16 | args := []string{"-rpc-addr=foo"} 17 | 18 | code := c.Run(args) 19 | if code != 1 { 20 | t.Fatalf("bad: %d", code) 21 | } 22 | 23 | if !strings.Contains(ui.ErrorWriter.String(), "event name") { 24 | t.Fatalf("bad: %#v", ui.ErrorWriter.String()) 25 | } 26 | } 27 | 28 | func TestEventCommandRun_tooMany(t *testing.T) { 29 | ui := new(cli.MockUi) 30 | c := &EventCommand{Ui: ui} 31 | args := []string{"-rpc-addr=foo", "foo", "bar", "baz"} 32 | 33 | code := c.Run(args) 34 | if code != 1 { 35 | t.Fatalf("bad: %d", code) 36 | } 37 | 38 | if !strings.Contains(ui.ErrorWriter.String(), "Too many") { 39 | t.Fatalf("bad: %#v", ui.ErrorWriter.String()) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /cmd/serf/command/force_leave.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package command 5 | 6 | import ( 7 | "flag" 8 | "fmt" 9 | "strings" 10 | 11 | "github.com/mitchellh/cli" 12 | ) 13 | 14 | // ForceLeaveCommand is a Command implementation that tells a running Serf 15 | // to force a member to enter the "left" state. 16 | type ForceLeaveCommand struct { 17 | Ui cli.Ui 18 | } 19 | 20 | var _ cli.Command = &ForceLeaveCommand{} 21 | 22 | func (c *ForceLeaveCommand) Run(args []string) int { 23 | var prune bool 24 | 25 | cmdFlags := flag.NewFlagSet("join", flag.ContinueOnError) 26 | cmdFlags.Usage = func() { c.Ui.Output(c.Help()) } 27 | cmdFlags.BoolVar(&prune, "prune", false, "Remove agent forcibly from list of members") 28 | rpcAddr := RPCAddrFlag(cmdFlags) 29 | rpcAuth := RPCAuthFlag(cmdFlags) 30 | if err := cmdFlags.Parse(args); err != nil { 31 | return 1 32 | } 33 | 34 | nodes := cmdFlags.Args() 35 | if len(nodes) != 1 { 36 | c.Ui.Error("A node name must be specified to force leave.") 37 | c.Ui.Error("") 38 | c.Ui.Error(c.Help()) 39 | return 1 40 | } 41 | 42 | client, err := RPCClient(*rpcAddr, *rpcAuth) 43 | if err != nil { 44 | c.Ui.Error(fmt.Sprintf("Error connecting to Serf agent: %s", err)) 45 | return 1 46 | } 47 | defer client.Close() 48 | 49 | if prune { 50 | err = client.ForceLeavePrune(nodes[0]) 51 | if err != nil { 52 | c.Ui.Error(fmt.Sprintf("Error force leaving: %s", err)) 53 | return 1 54 | } 55 | return 0 56 | 57 | } 58 | 59 | err = client.ForceLeave(nodes[0]) 60 | if err != nil { 61 | c.Ui.Error(fmt.Sprintf("Error force leaving: %s", err)) 62 | return 1 63 | } 64 | 65 | return 0 66 | } 67 | 68 | func (c *ForceLeaveCommand) Synopsis() string { 69 | return "Forces a member of the cluster to enter the \"left\" state" 70 | } 71 | 72 | func (c *ForceLeaveCommand) Help() string { 73 | helpText := ` 74 | Usage: serf force-leave [options] name 75 | 76 | Forces a member of a Serf cluster to enter the "left" state. Note 77 | that if the member is still actually alive, it will eventually rejoin 78 | the cluster. This command is most useful for cleaning out "failed" nodes 79 | that are never coming back. If you do not force leave a failed node, 80 | Serf will attempt to reconnect to those failed nodes for some period of 81 | time before eventually reaping them. 82 | 83 | Options: 84 | 85 | -rpc-addr=127.0.0.1:7373 RPC address of the Serf agent. 86 | -rpc-auth="" RPC auth token of the Serf agent. 87 | -prune Remove agent forcibly from list of members 88 | ` 89 | return strings.TrimSpace(helpText) 90 | } 91 | -------------------------------------------------------------------------------- /cmd/serf/command/info.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package command 5 | 6 | import ( 7 | "bytes" 8 | "flag" 9 | "fmt" 10 | "sort" 11 | "strings" 12 | 13 | "github.com/mitchellh/cli" 14 | ) 15 | 16 | // InfoCommand is a Command implementation that queries a running 17 | // Serf agent for various debugging statistics for operators 18 | type InfoCommand struct { 19 | Ui cli.Ui 20 | } 21 | 22 | var _ cli.Command = &InfoCommand{} 23 | 24 | func (i *InfoCommand) Help() string { 25 | helpText := ` 26 | Usage: serf info [options] 27 | 28 | Provides debugging information for operators 29 | 30 | Options: 31 | 32 | -format If provided, output is returned in the specified 33 | format. Valid formats are 'json', and 'text' (default) 34 | 35 | -rpc-addr=127.0.0.1:7373 RPC address of the Serf agent. 36 | 37 | -rpc-auth="" RPC auth token of the Serf agent. 38 | ` 39 | return strings.TrimSpace(helpText) 40 | } 41 | 42 | func (i *InfoCommand) Run(args []string) int { 43 | var format string 44 | cmdFlags := flag.NewFlagSet("info", flag.ContinueOnError) 45 | cmdFlags.Usage = func() { i.Ui.Output(i.Help()) } 46 | cmdFlags.StringVar(&format, "format", "text", "output format") 47 | rpcAddr := RPCAddrFlag(cmdFlags) 48 | rpcAuth := RPCAuthFlag(cmdFlags) 49 | if err := cmdFlags.Parse(args); err != nil { 50 | return 1 51 | } 52 | 53 | client, err := RPCClient(*rpcAddr, *rpcAuth) 54 | if err != nil { 55 | i.Ui.Error(fmt.Sprintf("Error connecting to Serf agent: %s", err)) 56 | return 1 57 | } 58 | defer client.Close() 59 | 60 | stats, err := client.Stats() 61 | if err != nil { 62 | i.Ui.Error(fmt.Sprintf("Error querying agent: %s", err)) 63 | return 1 64 | } 65 | 66 | output, err := formatOutput(StatsContainer(stats), format) 67 | if err != nil { 68 | i.Ui.Error(fmt.Sprintf("Encoding error: %s", err)) 69 | return 1 70 | } 71 | 72 | i.Ui.Output(string(output)) 73 | return 0 74 | } 75 | 76 | func (i *InfoCommand) Synopsis() string { 77 | return "Provides debugging information for operators" 78 | } 79 | 80 | type StatsContainer map[string]map[string]string 81 | 82 | func (s StatsContainer) String() string { 83 | var buf bytes.Buffer 84 | 85 | // Get the keys in sorted order 86 | keys := make([]string, 0, len(s)) 87 | for key := range s { 88 | keys = append(keys, key) 89 | } 90 | sort.Strings(keys) 91 | 92 | // Iterate over each top-level key 93 | for _, key := range keys { 94 | buf.WriteString(fmt.Sprintf("%s:\n", key)) 95 | 96 | // Sort the sub-keys 97 | subvals := s[key] 98 | subkeys := make([]string, 0, len(subvals)) 99 | for k := range subvals { 100 | subkeys = append(subkeys, k) 101 | } 102 | sort.Strings(subkeys) 103 | 104 | // Iterate over the subkeys 105 | for _, subkey := range subkeys { 106 | val := subvals[subkey] 107 | buf.WriteString(fmt.Sprintf("\t%s = %s\n", subkey, val)) 108 | } 109 | } 110 | return buf.String() 111 | } 112 | -------------------------------------------------------------------------------- /cmd/serf/command/info_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package command 5 | 6 | import ( 7 | "strings" 8 | "testing" 9 | 10 | "github.com/hashicorp/serf/testutil" 11 | "github.com/mitchellh/cli" 12 | ) 13 | 14 | func TestInfoCommandRun(t *testing.T) { 15 | ip1, returnFn1 := testutil.TakeIP() 16 | defer returnFn1() 17 | 18 | ip2, returnFn2 := testutil.TakeIP() 19 | defer returnFn2() 20 | 21 | a1 := testAgent(t, ip1) 22 | defer a1.Shutdown() 23 | 24 | rpcAddr, ipc := testIPC(t, ip2, a1) 25 | defer ipc.Shutdown() 26 | 27 | ui := new(cli.MockUi) 28 | c := &InfoCommand{Ui: ui} 29 | args := []string{"-rpc-addr=" + rpcAddr} 30 | 31 | code := c.Run(args) 32 | if code != 0 { 33 | t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) 34 | } 35 | 36 | if !strings.Contains(ui.OutputWriter.String(), "runtime") { 37 | t.Fatalf("bad: %#v", ui.OutputWriter.String()) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /cmd/serf/command/join.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package command 5 | 6 | import ( 7 | "flag" 8 | "fmt" 9 | "strings" 10 | 11 | "github.com/mitchellh/cli" 12 | ) 13 | 14 | // JoinCommand is a Command implementation that tells a running Serf 15 | // agent to join another. 16 | type JoinCommand struct { 17 | Ui cli.Ui 18 | } 19 | 20 | var _ cli.Command = &JoinCommand{} 21 | 22 | func (c *JoinCommand) Help() string { 23 | helpText := ` 24 | Usage: serf join [options] address ... 25 | 26 | Tells a running Serf agent (with "serf agent") to join the cluster 27 | by specifying at least one existing member. 28 | 29 | Options: 30 | 31 | -replay Replay past user events. 32 | -rpc-addr=127.0.0.1:7373 RPC address of the Serf agent. 33 | -rpc-auth="" RPC auth token of the Serf agent. 34 | ` 35 | return strings.TrimSpace(helpText) 36 | } 37 | 38 | func (c *JoinCommand) Run(args []string) int { 39 | var replayEvents bool 40 | 41 | cmdFlags := flag.NewFlagSet("join", flag.ContinueOnError) 42 | cmdFlags.Usage = func() { c.Ui.Output(c.Help()) } 43 | cmdFlags.BoolVar(&replayEvents, "replay", false, "replay") 44 | rpcAddr := RPCAddrFlag(cmdFlags) 45 | rpcAuth := RPCAuthFlag(cmdFlags) 46 | if err := cmdFlags.Parse(args); err != nil { 47 | return 1 48 | } 49 | 50 | addrs := cmdFlags.Args() 51 | if len(addrs) == 0 { 52 | c.Ui.Error("At least one address to join must be specified.") 53 | c.Ui.Error("") 54 | c.Ui.Error(c.Help()) 55 | return 1 56 | } 57 | 58 | client, err := RPCClient(*rpcAddr, *rpcAuth) 59 | if err != nil { 60 | c.Ui.Error(fmt.Sprintf("Error connecting to Serf agent: %s", err)) 61 | return 1 62 | } 63 | defer client.Close() 64 | 65 | n, err := client.Join(addrs, replayEvents) 66 | if err != nil { 67 | c.Ui.Error(fmt.Sprintf("Error joining the cluster: %s", err)) 68 | return 1 69 | } 70 | 71 | c.Ui.Output(fmt.Sprintf( 72 | "Successfully joined cluster by contacting %d nodes.", n)) 73 | return 0 74 | } 75 | 76 | func (c *JoinCommand) Synopsis() string { 77 | return "Tell Serf agent to join cluster" 78 | } 79 | -------------------------------------------------------------------------------- /cmd/serf/command/join_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package command 5 | 6 | import ( 7 | "strings" 8 | "testing" 9 | 10 | "github.com/hashicorp/serf/testutil" 11 | "github.com/mitchellh/cli" 12 | ) 13 | 14 | func TestJoinCommandRun(t *testing.T) { 15 | ip1, returnFn1 := testutil.TakeIP() 16 | defer returnFn1() 17 | 18 | ip2, returnFn2 := testutil.TakeIP() 19 | defer returnFn2() 20 | 21 | ip3, returnFn3 := testutil.TakeIP() 22 | defer returnFn3() 23 | 24 | a1 := testAgent(t, ip1) 25 | defer a1.Shutdown() 26 | 27 | a2 := testAgent(t, ip2) 28 | defer a2.Shutdown() 29 | 30 | rpcAddr, ipc := testIPC(t, ip3, a1) 31 | defer ipc.Shutdown() 32 | 33 | ui := new(cli.MockUi) 34 | c := &JoinCommand{Ui: ui} 35 | args := []string{ 36 | "-rpc-addr=" + rpcAddr, 37 | a2.SerfConfig().NodeName + "/" + a2.SerfConfig().MemberlistConfig.BindAddr, 38 | } 39 | 40 | code := c.Run(args) 41 | if code != 0 { 42 | t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) 43 | } 44 | 45 | if len(a1.Serf().Members()) != 2 { 46 | t.Fatalf("bad: %#v", a1.Serf().Members()) 47 | } 48 | } 49 | 50 | func TestJoinCommandRun_noAddrs(t *testing.T) { 51 | ui := new(cli.MockUi) 52 | c := &JoinCommand{Ui: ui} 53 | args := []string{"-rpc-addr=foo"} 54 | 55 | code := c.Run(args) 56 | if code != 1 { 57 | t.Fatalf("bad: %d", code) 58 | } 59 | 60 | if !strings.Contains(ui.ErrorWriter.String(), "one address") { 61 | t.Fatalf("bad: %#v", ui.ErrorWriter.String()) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /cmd/serf/command/keygen.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package command 5 | 6 | import ( 7 | "crypto/rand" 8 | "encoding/base64" 9 | "fmt" 10 | "strings" 11 | 12 | "github.com/mitchellh/cli" 13 | ) 14 | 15 | // KeygenCommand is a Command implementation that generates an encryption 16 | // key for use in `serf agent`. 17 | type KeygenCommand struct { 18 | Ui cli.Ui 19 | } 20 | 21 | var _ cli.Command = &KeygenCommand{} 22 | 23 | func (c *KeygenCommand) Run(_ []string) int { 24 | key := make([]byte, 32) 25 | n, err := rand.Reader.Read(key) 26 | if err != nil { 27 | c.Ui.Error(fmt.Sprintf("Error reading random data: %s", err)) 28 | return 1 29 | } 30 | if n != 32 { 31 | c.Ui.Error(fmt.Sprintf("Couldn't read enough entropy. Generate more entropy!")) 32 | return 1 33 | } 34 | 35 | c.Ui.Output(base64.StdEncoding.EncodeToString(key)) 36 | return 0 37 | } 38 | 39 | func (c *KeygenCommand) Synopsis() string { 40 | return "Generates a new encryption key" 41 | } 42 | 43 | func (c *KeygenCommand) Help() string { 44 | helpText := ` 45 | Usage: serf keygen 46 | 47 | Generates a new encryption key that can be used to configure the 48 | agent to encrypt traffic. The output of this command is already 49 | in the proper format that the agent expects. 50 | ` 51 | return strings.TrimSpace(helpText) 52 | } 53 | -------------------------------------------------------------------------------- /cmd/serf/command/keygen_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package command 5 | 6 | import ( 7 | "encoding/base64" 8 | "testing" 9 | 10 | "github.com/mitchellh/cli" 11 | ) 12 | 13 | func TestKeygenCommand(t *testing.T) { 14 | ui := new(cli.MockUi) 15 | c := &KeygenCommand{Ui: ui} 16 | code := c.Run(nil) 17 | if code != 0 { 18 | t.Fatalf("bad: %d", code) 19 | } 20 | 21 | output := ui.OutputWriter.String() 22 | result, err := base64.StdEncoding.DecodeString(output) 23 | if err != nil { 24 | t.Fatalf("err: %v", err) 25 | } 26 | 27 | if len(result) != 32 { 28 | t.Fatalf("bad: %#v", result) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /cmd/serf/command/leave.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package command 5 | 6 | import ( 7 | "flag" 8 | "fmt" 9 | "strings" 10 | 11 | "github.com/mitchellh/cli" 12 | ) 13 | 14 | // LeaveCommand is a Command implementation that instructs 15 | // the Serf agent to gracefully leave the cluster 16 | type LeaveCommand struct { 17 | Ui cli.Ui 18 | } 19 | 20 | var _ cli.Command = &LeaveCommand{} 21 | 22 | func (c *LeaveCommand) Help() string { 23 | helpText := ` 24 | Usage: serf leave 25 | 26 | Causes the agent to gracefully leave the Serf cluster and shutdown. 27 | 28 | Options: 29 | 30 | -rpc-addr=127.0.0.1:7373 RPC address of the Serf agent. 31 | -rpc-auth="" RPC auth token of the Serf agent. 32 | ` 33 | return strings.TrimSpace(helpText) 34 | } 35 | 36 | func (c *LeaveCommand) Run(args []string) int { 37 | cmdFlags := flag.NewFlagSet("leave", flag.ContinueOnError) 38 | cmdFlags.Usage = func() { c.Ui.Output(c.Help()) } 39 | rpcAddr := RPCAddrFlag(cmdFlags) 40 | rpcAuth := RPCAuthFlag(cmdFlags) 41 | if err := cmdFlags.Parse(args); err != nil { 42 | return 1 43 | } 44 | 45 | client, err := RPCClient(*rpcAddr, *rpcAuth) 46 | if err != nil { 47 | c.Ui.Error(fmt.Sprintf("Error connecting to Serf agent: %s", err)) 48 | return 1 49 | } 50 | defer client.Close() 51 | 52 | if err := client.Leave(); err != nil { 53 | c.Ui.Error(fmt.Sprintf("Error leaving: %s", err)) 54 | return 1 55 | } 56 | 57 | c.Ui.Output("Graceful leave complete") 58 | return 0 59 | } 60 | 61 | func (c *LeaveCommand) Synopsis() string { 62 | return "Gracefully leaves the Serf cluster and shuts down" 63 | } 64 | -------------------------------------------------------------------------------- /cmd/serf/command/leave_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package command 5 | 6 | import ( 7 | "strings" 8 | "testing" 9 | 10 | "github.com/hashicorp/serf/testutil" 11 | "github.com/mitchellh/cli" 12 | ) 13 | 14 | func TestLeaveCommandRun(t *testing.T) { 15 | ip1, returnFn1 := testutil.TakeIP() 16 | defer returnFn1() 17 | 18 | ip2, returnFn2 := testutil.TakeIP() 19 | defer returnFn2() 20 | 21 | a1 := testAgent(t, ip1) 22 | defer a1.Shutdown() 23 | 24 | rpcAddr, ipc := testIPC(t, ip2, a1) 25 | defer ipc.Shutdown() 26 | 27 | ui := new(cli.MockUi) 28 | c := &LeaveCommand{Ui: ui} 29 | args := []string{"-rpc-addr=" + rpcAddr} 30 | 31 | code := c.Run(args) 32 | if code != 0 { 33 | t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) 34 | } 35 | 36 | if !strings.Contains(ui.OutputWriter.String(), "leave complete") { 37 | t.Fatalf("bad: %#v", ui.OutputWriter.String()) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /cmd/serf/command/monitor.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package command 5 | 6 | import ( 7 | "flag" 8 | "fmt" 9 | "strings" 10 | "sync" 11 | 12 | "github.com/hashicorp/logutils" 13 | "github.com/mitchellh/cli" 14 | ) 15 | 16 | // MonitorCommand is a Command implementation that queries a running 17 | // Serf agent what members are part of the cluster currently. 18 | type MonitorCommand struct { 19 | ShutdownCh <-chan struct{} 20 | Ui cli.Ui 21 | 22 | lock sync.Mutex 23 | quitting bool 24 | } 25 | 26 | func (c *MonitorCommand) Help() string { 27 | helpText := ` 28 | Usage: serf monitor [options] 29 | 30 | Shows recent log messages of a Serf agent, and attaches to the agent, 31 | outputting log messages as they occur in real time. The monitor lets you 32 | listen for log levels that may be filtered out of the Serf agent. For 33 | example your agent may only be logging at INFO level, but with the monitor 34 | you can see the DEBUG level logs. 35 | 36 | Options: 37 | 38 | -log-level=info Log level of the agent. 39 | -rpc-addr=127.0.0.1:7373 RPC address of the Serf agent. 40 | -rpc-auth="" RPC auth token of the Serf agent. 41 | ` 42 | return strings.TrimSpace(helpText) 43 | } 44 | 45 | func (c *MonitorCommand) Run(args []string) int { 46 | var logLevel string 47 | cmdFlags := flag.NewFlagSet("monitor", flag.ContinueOnError) 48 | cmdFlags.Usage = func() { c.Ui.Output(c.Help()) } 49 | cmdFlags.StringVar(&logLevel, "log-level", "INFO", "log level") 50 | rpcAddr := RPCAddrFlag(cmdFlags) 51 | rpcAuth := RPCAuthFlag(cmdFlags) 52 | if err := cmdFlags.Parse(args); err != nil { 53 | return 1 54 | } 55 | 56 | client, err := RPCClient(*rpcAddr, *rpcAuth) 57 | if err != nil { 58 | c.Ui.Error(fmt.Sprintf("Error connecting to Serf agent: %s", err)) 59 | return 1 60 | } 61 | defer client.Close() 62 | 63 | eventCh := make(chan map[string]interface{}, 1024) 64 | streamHandle, err := client.Stream("*", eventCh) 65 | if err != nil { 66 | c.Ui.Error(fmt.Sprintf("Error starting stream: %s", err)) 67 | return 1 68 | } 69 | defer client.Stop(streamHandle) 70 | 71 | logCh := make(chan string, 1024) 72 | monHandle, err := client.Monitor(logutils.LogLevel(logLevel), logCh) 73 | if err != nil { 74 | c.Ui.Error(fmt.Sprintf("Error starting monitor: %s", err)) 75 | return 1 76 | } 77 | defer client.Stop(monHandle) 78 | 79 | eventDoneCh := make(chan struct{}) 80 | go func() { 81 | defer close(eventDoneCh) 82 | OUTER: 83 | for { 84 | select { 85 | case log := <-logCh: 86 | if log == "" { 87 | break OUTER 88 | } 89 | c.Ui.Info(log) 90 | case event := <-eventCh: 91 | if event == nil { 92 | break OUTER 93 | } 94 | c.Ui.Info("Event Info:") 95 | for key, val := range event { 96 | c.Ui.Info(fmt.Sprintf("\t%s: %#v", key, val)) 97 | } 98 | } 99 | } 100 | 101 | c.lock.Lock() 102 | defer c.lock.Unlock() 103 | if !c.quitting { 104 | c.Ui.Info("") 105 | c.Ui.Output("Remote side ended the monitor! This usually means that the\n" + 106 | "remote side has exited or crashed.") 107 | } 108 | }() 109 | 110 | select { 111 | case <-eventDoneCh: 112 | return 1 113 | case <-c.ShutdownCh: 114 | c.lock.Lock() 115 | c.quitting = true 116 | c.lock.Unlock() 117 | } 118 | 119 | return 0 120 | } 121 | 122 | func (c *MonitorCommand) Synopsis() string { 123 | return "Stream logs from a Serf agent" 124 | } 125 | -------------------------------------------------------------------------------- /cmd/serf/command/output.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package command 5 | 6 | import ( 7 | "encoding/json" 8 | "fmt" 9 | "strings" 10 | ) 11 | 12 | // Format some raw data for output. For better or worse, this currently forces 13 | // the passed data object to implement fmt.Stringer, since it's pretty hard to 14 | // implement a canonical *-to-string function. 15 | func formatOutput(data interface{}, format string) ([]byte, error) { 16 | var out string 17 | 18 | switch format { 19 | 20 | case "json": 21 | jsonout, err := json.MarshalIndent(data, "", " ") 22 | if err != nil { 23 | return nil, err 24 | } 25 | out = string(jsonout) 26 | 27 | case "text": 28 | out = data.(fmt.Stringer).String() 29 | 30 | default: 31 | return nil, fmt.Errorf("Invalid output format \"%s\"", format) 32 | 33 | } 34 | return []byte(prepareOutput(out)), nil 35 | } 36 | 37 | // Apply some final formatting to make sure we don't end up with extra newlines 38 | func prepareOutput(in string) string { 39 | return strings.TrimSpace(string(in)) 40 | } 41 | -------------------------------------------------------------------------------- /cmd/serf/command/output_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package command 5 | 6 | import ( 7 | "fmt" 8 | "testing" 9 | ) 10 | 11 | type OutputTest struct { 12 | XMLName string `json:"-"` 13 | TestString string `json:"test_string"` 14 | TestInt int `json:"test_int"` 15 | TestNil []byte `json:"test_nil"` 16 | TestNested OutputTestNested `json:"nested"` 17 | } 18 | 19 | type OutputTestNested struct { 20 | NestKey string `json:"nest_key"` 21 | } 22 | 23 | func (o OutputTest) String() string { 24 | return fmt.Sprintf("%s %d %s", o.TestString, o.TestInt, o.TestNil) 25 | } 26 | 27 | func TestCommandOutput(t *testing.T) { 28 | var formatted []byte 29 | result := OutputTest{ 30 | TestString: "woooo a string", 31 | TestInt: 77, 32 | TestNil: nil, 33 | TestNested: OutputTestNested{ 34 | NestKey: "nest_value", 35 | }, 36 | } 37 | 38 | json_expected := `{ 39 | "test_string": "woooo a string", 40 | "test_int": 77, 41 | "test_nil": null, 42 | "nested": { 43 | "nest_key": "nest_value" 44 | } 45 | }` 46 | formatted, _ = formatOutput(result, "json") 47 | if string(formatted) != json_expected { 48 | t.Fatalf("bad json:\n%s\n\nexpected:\n%s", formatted, json_expected) 49 | } 50 | 51 | text_expected := "woooo a string 77" 52 | formatted, _ = formatOutput(result, "text") 53 | if string(formatted) != text_expected { 54 | t.Fatalf("bad output:\n\"%s\"\n\nexpected:\n\"%s\"", formatted, text_expected) 55 | } 56 | 57 | error_expected := `Invalid output format "boo"` 58 | _, err := formatOutput(result, "boo") 59 | if err.Error() != error_expected { 60 | t.Fatalf("bad output:\n\"%s\"\n\nexpected:\n\"%s\"", err.Error(), error_expected) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /cmd/serf/command/reachability_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package command 5 | 6 | import ( 7 | "strings" 8 | "testing" 9 | 10 | "github.com/hashicorp/serf/testutil" 11 | "github.com/mitchellh/cli" 12 | ) 13 | 14 | func TestReachabilityCommand_Run(t *testing.T) { 15 | ip1, returnFn1 := testutil.TakeIP() 16 | defer returnFn1() 17 | 18 | ip2, returnFn2 := testutil.TakeIP() 19 | defer returnFn2() 20 | 21 | a1 := testAgent(t, ip1) 22 | defer a1.Shutdown() 23 | 24 | rpcAddr, ipc := testIPC(t, ip2, a1) 25 | defer ipc.Shutdown() 26 | 27 | ui := new(cli.MockUi) 28 | c := &ReachabilityCommand{Ui: ui} 29 | args := []string{"-rpc-addr=" + rpcAddr, "-verbose"} 30 | 31 | code := c.Run(args) 32 | if code != 0 { 33 | t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) 34 | } 35 | 36 | if !strings.Contains(ui.OutputWriter.String(), a1.SerfConfig().NodeName) { 37 | t.Fatalf("bad: %#v", ui.OutputWriter.String()) 38 | } 39 | if !strings.Contains(ui.OutputWriter.String(), "Successfully") { 40 | t.Fatalf("bad: %#v", ui.OutputWriter.String()) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /cmd/serf/command/rpc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package command 5 | 6 | import ( 7 | "flag" 8 | "os" 9 | 10 | "github.com/hashicorp/serf/client" 11 | ) 12 | 13 | // RPCAddrFlag returns a pointer to a string that will be populated 14 | // when the given flagset is parsed with the RPC address of the Serf. 15 | func RPCAddrFlag(f *flag.FlagSet) *string { 16 | defaultRpcAddr := os.Getenv("SERF_RPC_ADDR") 17 | if defaultRpcAddr == "" { 18 | defaultRpcAddr = "127.0.0.1:7373" 19 | } 20 | 21 | return f.String("rpc-addr", defaultRpcAddr, 22 | "RPC address of the Serf agent") 23 | } 24 | 25 | // RPCAuthFlag returns a pointer to a string that will be populated 26 | // when the given flagset is parsed with the RPC auth token of the Serf. 27 | func RPCAuthFlag(f *flag.FlagSet) *string { 28 | rpcAuth := os.Getenv("SERF_RPC_AUTH") 29 | return f.String("rpc-auth", rpcAuth, 30 | "RPC auth token of the Serf agent") 31 | } 32 | 33 | // RPCClient returns a new Serf RPC client with the given address. 34 | func RPCClient(addr, auth string) (*client.RPCClient, error) { 35 | config := client.Config{Addr: addr, AuthKey: auth} 36 | return client.ClientFromConfig(&config) 37 | } 38 | -------------------------------------------------------------------------------- /cmd/serf/command/rtt.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package command 5 | 6 | import ( 7 | "flag" 8 | "fmt" 9 | "strings" 10 | 11 | "github.com/mitchellh/cli" 12 | ) 13 | 14 | // RTTCommand is a Command implementation that allows users to query the 15 | // estimated round trip time between nodes using network coordinates. 16 | type RTTCommand struct { 17 | Ui cli.Ui 18 | } 19 | 20 | func (c *RTTCommand) Help() string { 21 | helpText := ` 22 | Usage: serf rtt [options] node1 [node2] 23 | 24 | Estimates the round trip time between two nodes using Serf's network 25 | coordinate model of the cluster. 26 | 27 | At least one node name is required. If the second node name isn't given, it 28 | is set to the agent's node name. Note that these are node names as known to 29 | Serf as "serf members" would show, not IP addresses. 30 | 31 | Options: 32 | 33 | -rpc-addr=127.0.0.1:7373 RPC address of the Serf agent. 34 | 35 | -rpc-auth="" RPC auth token of the Serf agent. 36 | ` 37 | return strings.TrimSpace(helpText) 38 | } 39 | 40 | func (c *RTTCommand) Run(args []string) int { 41 | cmdFlags := flag.NewFlagSet("rtt", flag.ContinueOnError) 42 | cmdFlags.Usage = func() { c.Ui.Output(c.Help()) } 43 | rpcAddr := RPCAddrFlag(cmdFlags) 44 | rpcAuth := RPCAuthFlag(cmdFlags) 45 | if err := cmdFlags.Parse(args); err != nil { 46 | return 1 47 | } 48 | 49 | // Create the RPC client. 50 | client, err := RPCClient(*rpcAddr, *rpcAuth) 51 | if err != nil { 52 | c.Ui.Error(fmt.Sprintf("Error connecting to Serf agent: %s", err)) 53 | return 1 54 | } 55 | defer client.Close() 56 | 57 | // They must provide at least one node. 58 | nodes := cmdFlags.Args() 59 | if len(nodes) == 1 { 60 | stats, err := client.Stats() 61 | if err != nil { 62 | c.Ui.Error(fmt.Sprintf("Error querying agent: %s", err)) 63 | return 1 64 | } 65 | nodes = append(nodes, stats["agent"]["name"]) 66 | } else if len(nodes) != 2 { 67 | c.Ui.Error("One or two node names must be specified") 68 | c.Ui.Error("") 69 | c.Ui.Error(c.Help()) 70 | return 1 71 | } 72 | 73 | // Get the coordinates. 74 | coord1, err := client.GetCoordinate(nodes[0]) 75 | if err != nil { 76 | c.Ui.Error(fmt.Sprintf("Error getting coordinates: %s", err)) 77 | return 1 78 | } 79 | if coord1 == nil { 80 | c.Ui.Error(fmt.Sprintf("Could not find a coordinate for node %q", nodes[0])) 81 | return 1 82 | } 83 | coord2, err := client.GetCoordinate(nodes[1]) 84 | if err != nil { 85 | c.Ui.Error(fmt.Sprintf("Error getting coordinates: %s", err)) 86 | return 1 87 | } 88 | if coord2 == nil { 89 | c.Ui.Error(fmt.Sprintf("Could not find a coordinate for node %q", nodes[1])) 90 | return 1 91 | } 92 | 93 | // Report the round trip time. 94 | dist := fmt.Sprintf("%.3f ms", coord1.DistanceTo(coord2).Seconds()*1000.0) 95 | c.Ui.Output(fmt.Sprintf("Estimated %s <-> %s rtt: %s", nodes[0], nodes[1], dist)) 96 | return 0 97 | } 98 | 99 | func (c *RTTCommand) Synopsis() string { 100 | return "Estimates network round trip time between nodes" 101 | } 102 | -------------------------------------------------------------------------------- /cmd/serf/command/rtt_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package command 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/hashicorp/serf/testutil" 12 | "github.com/mitchellh/cli" 13 | ) 14 | 15 | func TestRTTCommand_Implements(t *testing.T) { 16 | var _ cli.Command = &RTTCommand{} 17 | } 18 | 19 | func TestRTTCommand_Run_BadArgs(t *testing.T) { 20 | ip1, returnFn1 := testutil.TakeIP() 21 | defer returnFn1() 22 | 23 | ip2, returnFn2 := testutil.TakeIP() 24 | defer returnFn2() 25 | 26 | a1 := testAgent(t, ip1) 27 | defer a1.Shutdown() 28 | 29 | _, ipc := testIPC(t, ip2, a1) 30 | defer ipc.Shutdown() 31 | 32 | ui := new(cli.MockUi) 33 | c := &RTTCommand{Ui: ui} 34 | 35 | code := c.Run([]string{}) 36 | if code != 1 { 37 | t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) 38 | } 39 | } 40 | 41 | func TestRTTCommand_Run(t *testing.T) { 42 | ip1, returnFn1 := testutil.TakeIP() 43 | defer returnFn1() 44 | 45 | ip2, returnFn2 := testutil.TakeIP() 46 | defer returnFn2() 47 | 48 | a1 := testAgent(t, ip1) 49 | defer a1.Shutdown() 50 | 51 | rpcAddr, ipc := testIPC(t, ip2, a1) 52 | defer ipc.Shutdown() 53 | 54 | coord, ok := a1.Serf().GetCachedCoordinate(a1.SerfConfig().NodeName) 55 | if !ok { 56 | t.Fatalf("should have a coordinate for the agent") 57 | } 58 | dist_str := fmt.Sprintf("%.3f ms", coord.DistanceTo(coord).Seconds()*1000.0) 59 | 60 | // Try with the default of the agent's node. 61 | args := []string{"-rpc-addr=" + rpcAddr, a1.SerfConfig().NodeName} 62 | { 63 | ui := new(cli.MockUi) 64 | c := &RTTCommand{Ui: ui} 65 | code := c.Run(args) 66 | if code != 0 { 67 | t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) 68 | } 69 | 70 | // Make sure the proper RTT was reported in the output. 71 | expected := fmt.Sprintf("rtt: %s", dist_str) 72 | if !strings.Contains(ui.OutputWriter.String(), expected) { 73 | t.Fatalf("bad: %#v", ui.OutputWriter.String()) 74 | } 75 | } 76 | 77 | // Explicitly set the agent's node twice. 78 | args = append(args, a1.SerfConfig().NodeName) 79 | { 80 | ui := new(cli.MockUi) 81 | c := &RTTCommand{Ui: ui} 82 | code := c.Run(args) 83 | if code != 0 { 84 | t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) 85 | } 86 | 87 | // Make sure the proper RTT was reported in the output. 88 | expected := fmt.Sprintf("rtt: %s", dist_str) 89 | if !strings.Contains(ui.OutputWriter.String(), expected) { 90 | t.Fatalf("bad: %#v", ui.OutputWriter.String()) 91 | } 92 | } 93 | 94 | // Try an unknown node. 95 | args = []string{"nope"} 96 | { 97 | ui := new(cli.MockUi) 98 | c := &RTTCommand{Ui: ui} 99 | code := c.Run(args) 100 | if code != 1 { 101 | t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /cmd/serf/command/tags.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package command 5 | 6 | import ( 7 | "flag" 8 | "fmt" 9 | "strings" 10 | 11 | "github.com/hashicorp/serf/cmd/serf/command/agent" 12 | "github.com/mitchellh/cli" 13 | ) 14 | 15 | // TagsCommand is an interface to dynamically add or otherwise modify a 16 | // running serf agent's tags. 17 | type TagsCommand struct { 18 | Ui cli.Ui 19 | } 20 | 21 | var _ cli.Command = &TagsCommand{} 22 | 23 | func (c *TagsCommand) Help() string { 24 | helpText := ` 25 | Usage: serf tags [options] ... 26 | 27 | Modifies tags on a running Serf agent. 28 | 29 | Options: 30 | 31 | -rpc-addr=127.0.0.1:7373 RPC Address of the Serf agent. 32 | -rpc-auth="" RPC auth token of the Serf agent. 33 | -set key=value Creates or modifies the value of a tag 34 | -delete key Removes a tag, if present 35 | ` 36 | return strings.TrimSpace(helpText) 37 | } 38 | 39 | func (c *TagsCommand) Run(args []string) int { 40 | var tagPairs []string 41 | var delTags []string 42 | cmdFlags := flag.NewFlagSet("tags", flag.ContinueOnError) 43 | cmdFlags.Usage = func() { c.Ui.Output(c.Help()) } 44 | cmdFlags.Var((*agent.AppendSliceValue)(&tagPairs), "set", 45 | "tag pairs, specified as key=value") 46 | cmdFlags.Var((*agent.AppendSliceValue)(&delTags), "delete", 47 | "tag keys to unset") 48 | rpcAddr := RPCAddrFlag(cmdFlags) 49 | rpcAuth := RPCAuthFlag(cmdFlags) 50 | if err := cmdFlags.Parse(args); err != nil { 51 | return 1 52 | } 53 | 54 | if len(tagPairs) == 0 && len(delTags) == 0 { 55 | c.Ui.Output(c.Help()) 56 | return 1 57 | } 58 | 59 | client, err := RPCClient(*rpcAddr, *rpcAuth) 60 | if err != nil { 61 | c.Ui.Error(fmt.Sprintf("Error connecting to Serf agent: %s", err)) 62 | return 1 63 | } 64 | defer client.Close() 65 | 66 | tags, err := agent.UnmarshalTags(tagPairs) 67 | if err != nil { 68 | c.Ui.Error(fmt.Sprintf("Error: %s", err)) 69 | return 1 70 | } 71 | 72 | if err := client.UpdateTags(tags, delTags); err != nil { 73 | c.Ui.Error(fmt.Sprintf("Error setting tags: %s", err)) 74 | return 1 75 | } 76 | 77 | c.Ui.Output("Successfully updated agent tags") 78 | return 0 79 | } 80 | 81 | func (c *TagsCommand) Synopsis() string { 82 | return "Modify tags of a running Serf agent" 83 | } 84 | -------------------------------------------------------------------------------- /cmd/serf/command/tags_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package command 5 | 6 | import ( 7 | "strings" 8 | "testing" 9 | 10 | "github.com/hashicorp/serf/client" 11 | "github.com/hashicorp/serf/testutil" 12 | "github.com/mitchellh/cli" 13 | ) 14 | 15 | func TestTagsCommandRun(t *testing.T) { 16 | ip1, returnFn1 := testutil.TakeIP() 17 | defer returnFn1() 18 | 19 | ip2, returnFn2 := testutil.TakeIP() 20 | defer returnFn2() 21 | 22 | a1 := testAgent(t, ip1) 23 | defer a1.Shutdown() 24 | 25 | rpcAddr, ipc := testIPC(t, ip2, a1) 26 | defer ipc.Shutdown() 27 | 28 | ui := new(cli.MockUi) 29 | c := &TagsCommand{Ui: ui} 30 | args := []string{ 31 | "-rpc-addr=" + rpcAddr, 32 | "-delete", "tag2", 33 | "-set", "a=1", 34 | "-set", "b=2", 35 | } 36 | 37 | code := c.Run(args) 38 | if code != 0 { 39 | t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) 40 | } 41 | 42 | if !strings.Contains(ui.OutputWriter.String(), "Successfully updated agent tags") { 43 | t.Fatalf("bad: %#v", ui.OutputWriter.String()) 44 | } 45 | 46 | rpcClient, err := client.NewRPCClient(rpcAddr) 47 | if err != nil { 48 | t.Fatalf("err: %v", err) 49 | } 50 | 51 | mem, err := rpcClient.Members() 52 | if err != nil { 53 | t.Fatalf("err: %v", err) 54 | } 55 | 56 | if len(mem) != 1 { 57 | t.Fatalf("bad: %v", mem) 58 | } 59 | 60 | m0 := mem[0] 61 | if _, ok := m0.Tags["tag2"]; ok { 62 | t.Fatalf("bad: %v", m0.Tags) 63 | } 64 | if _, ok := m0.Tags["a"]; !ok { 65 | t.Fatalf("bad: %v", m0.Tags) 66 | } 67 | if _, ok := m0.Tags["b"]; !ok { 68 | t.Fatalf("bad: %v", m0.Tags) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /cmd/serf/command/util_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package command 5 | 6 | import ( 7 | "io" 8 | "math/rand" 9 | "net" 10 | "testing" 11 | "time" 12 | 13 | "github.com/hashicorp/serf/cmd/serf/command/agent" 14 | "github.com/hashicorp/serf/serf" 15 | "github.com/hashicorp/serf/testutil" 16 | ) 17 | 18 | func init() { 19 | // Seed the random number generator 20 | rand.Seed(time.Now().UnixNano()) 21 | } 22 | 23 | func testAgent(t *testing.T, ip net.IP) *agent.Agent { 24 | agentConfig := agent.DefaultConfig() 25 | serfConfig := serf.DefaultConfig() 26 | return testAgentWithConfig(t, ip, agentConfig, serfConfig) 27 | } 28 | 29 | func testAgentWithConfig(t *testing.T, ip net.IP, agentConfig *agent.Config, serfConfig *serf.Config) *agent.Agent { 30 | serfConfig.MemberlistConfig.BindAddr = ip.String() 31 | serfConfig.MemberlistConfig.ProbeInterval = 50 * time.Millisecond 32 | serfConfig.MemberlistConfig.ProbeTimeout = 25 * time.Millisecond 33 | serfConfig.MemberlistConfig.SuspicionMult = 1 34 | serfConfig.NodeName = serfConfig.MemberlistConfig.BindAddr 35 | serfConfig.Tags = map[string]string{"role": "test", "tag1": "foo", "tag2": "bar"} 36 | 37 | serfConfig.MemberlistConfig.RequireNodeNames = true 38 | 39 | agent, err := agent.Create(agentConfig, serfConfig, testutil.TestWriter(t)) 40 | if err != nil { 41 | t.Fatalf("err: %v", err) 42 | } 43 | 44 | if err := agent.Start(); err != nil { 45 | t.Fatalf("err: %v", err) 46 | } 47 | 48 | return agent 49 | } 50 | 51 | func testIPC(t *testing.T, ip net.IP, a *agent.Agent) (string, *agent.AgentIPC) { 52 | rpcAddr := ip.String() + ":11111" 53 | 54 | l, err := net.Listen("tcp", rpcAddr) 55 | if err != nil { 56 | t.Fatalf("err: %v", err) 57 | } 58 | 59 | tw := testutil.TestWriter(t) 60 | 61 | lw := agent.NewLogWriter(512) 62 | mult := io.MultiWriter(tw, lw) 63 | ipc := agent.NewAgentIPC(a, "", l, mult, lw, false) 64 | return rpcAddr, ipc 65 | } 66 | -------------------------------------------------------------------------------- /cmd/serf/command/version.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package command 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/hashicorp/serf/serf" 10 | "github.com/mitchellh/cli" 11 | ) 12 | 13 | // VersionCommand is a Command implementation prints the version. 14 | type VersionCommand struct { 15 | Version string 16 | UI cli.Ui 17 | } 18 | 19 | func (c *VersionCommand) Help() string { 20 | return "" 21 | } 22 | 23 | func (c *VersionCommand) Run(_ []string) int { 24 | c.UI.Output(c.Version) 25 | c.UI.Output(fmt.Sprintf("Agent Protocol: %d (Understands back to: %d)", 26 | serf.ProtocolVersionMax, serf.ProtocolVersionMin)) 27 | return 0 28 | } 29 | 30 | func (c *VersionCommand) Synopsis() string { 31 | return "Prints the Serf version" 32 | } 33 | -------------------------------------------------------------------------------- /cmd/serf/commands.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "os" 8 | "os/signal" 9 | 10 | "github.com/hashicorp/serf/cmd/serf/command" 11 | "github.com/hashicorp/serf/cmd/serf/command/agent" 12 | "github.com/hashicorp/serf/version" 13 | "github.com/mitchellh/cli" 14 | ) 15 | 16 | // Commands is the mapping of all the available Serf commands. 17 | var Commands map[string]cli.CommandFactory 18 | 19 | func init() { 20 | ui := &cli.BasicUi{Writer: os.Stdout} 21 | 22 | Commands = map[string]cli.CommandFactory{ 23 | "agent": func() (cli.Command, error) { 24 | return &agent.Command{ 25 | Ui: ui, 26 | ShutdownCh: make(chan struct{}), 27 | }, nil 28 | }, 29 | 30 | "event": func() (cli.Command, error) { 31 | return &command.EventCommand{ 32 | Ui: ui, 33 | }, nil 34 | }, 35 | 36 | "query": func() (cli.Command, error) { 37 | return &command.QueryCommand{ 38 | ShutdownCh: makeShutdownCh(), 39 | Ui: ui, 40 | }, nil 41 | }, 42 | 43 | "force-leave": func() (cli.Command, error) { 44 | return &command.ForceLeaveCommand{ 45 | Ui: ui, 46 | }, nil 47 | }, 48 | 49 | "join": func() (cli.Command, error) { 50 | return &command.JoinCommand{ 51 | Ui: ui, 52 | }, nil 53 | }, 54 | 55 | "keygen": func() (cli.Command, error) { 56 | return &command.KeygenCommand{ 57 | Ui: ui, 58 | }, nil 59 | }, 60 | 61 | "keys": func() (cli.Command, error) { 62 | return &command.KeysCommand{ 63 | Ui: ui, 64 | }, nil 65 | }, 66 | 67 | "leave": func() (cli.Command, error) { 68 | return &command.LeaveCommand{ 69 | Ui: ui, 70 | }, nil 71 | }, 72 | 73 | "members": func() (cli.Command, error) { 74 | return &command.MembersCommand{ 75 | Ui: ui, 76 | }, nil 77 | }, 78 | 79 | "monitor": func() (cli.Command, error) { 80 | return &command.MonitorCommand{ 81 | ShutdownCh: makeShutdownCh(), 82 | Ui: ui, 83 | }, nil 84 | }, 85 | 86 | "tags": func() (cli.Command, error) { 87 | return &command.TagsCommand{ 88 | Ui: ui, 89 | }, nil 90 | }, 91 | 92 | "reachability": func() (cli.Command, error) { 93 | return &command.ReachabilityCommand{ 94 | ShutdownCh: makeShutdownCh(), 95 | Ui: ui, 96 | }, nil 97 | }, 98 | 99 | "rtt": func() (cli.Command, error) { 100 | return &command.RTTCommand{ 101 | Ui: ui, 102 | }, nil 103 | }, 104 | 105 | "info": func() (cli.Command, error) { 106 | return &command.InfoCommand{ 107 | Ui: ui, 108 | }, nil 109 | }, 110 | 111 | "version": func() (cli.Command, error) { 112 | return &command.VersionCommand{ 113 | UI: ui, 114 | Version: version.GetHumanVersion(), 115 | }, nil 116 | }, 117 | } 118 | } 119 | 120 | // makeShutdownCh returns a channel that can be used for shutdown 121 | // notifications for commands. This channel will send a message for every 122 | // interrupt received. 123 | func makeShutdownCh() <-chan struct{} { 124 | resultCh := make(chan struct{}) 125 | 126 | signalCh := make(chan os.Signal, 4) 127 | signal.Notify(signalCh, os.Interrupt) 128 | go func() { 129 | for { 130 | <-signalCh 131 | resultCh <- struct{}{} 132 | } 133 | }() 134 | 135 | return resultCh 136 | } 137 | -------------------------------------------------------------------------------- /cmd/serf/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "os" 11 | 12 | "github.com/mitchellh/cli" 13 | ) 14 | 15 | func main() { 16 | log.SetOutput(ioutil.Discard) 17 | 18 | // Get the command line args. We shortcut "--version" and "-v" to 19 | // just show the version. 20 | args := os.Args[1:] 21 | for _, arg := range args { 22 | if arg == "-v" || arg == "--version" { 23 | newArgs := make([]string, len(args)+1) 24 | newArgs[0] = "version" 25 | copy(newArgs[1:], args) 26 | args = newArgs 27 | break 28 | } 29 | } 30 | 31 | cli := &cli.CLI{ 32 | Args: args, 33 | Commands: Commands, 34 | HelpFunc: cli.BasicHelpFunc("serf"), 35 | } 36 | 37 | exitCode, err := cli.Run() 38 | if err != nil { 39 | fmt.Fprintf(os.Stderr, "Error executing CLI: %s\n", err.Error()) 40 | os.Exit(1) 41 | } 42 | 43 | os.Exit(exitCode) 44 | } 45 | -------------------------------------------------------------------------------- /coordinate/config.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package coordinate 5 | 6 | import ( 7 | "github.com/hashicorp/go-metrics/compat" 8 | ) 9 | 10 | // Config is used to set the parameters of the Vivaldi-based coordinate mapping 11 | // algorithm. 12 | // 13 | // The following references are called out at various points in the documentation 14 | // here: 15 | // 16 | // [1] Dabek, Frank, et al. "Vivaldi: A decentralized network coordinate system." 17 | // 18 | // ACM SIGCOMM Computer Communication Review. Vol. 34. No. 4. ACM, 2004. 19 | // 20 | // [2] Ledlie, Jonathan, Paul Gardner, and Margo I. Seltzer. "Network Coordinates 21 | // 22 | // in the Wild." NSDI. Vol. 7. 2007. 23 | // 24 | // [3] Lee, Sanghwan, et al. "On suitability of Euclidean embedding for 25 | // 26 | // host-based network coordinate systems." Networking, IEEE/ACM Transactions 27 | // on 18.1 (2010): 27-40. 28 | type Config struct { 29 | // The dimensionality of the coordinate system. As discussed in [2], more 30 | // dimensions improves the accuracy of the estimates up to a point. Per [2] 31 | // we chose 8 dimensions plus a non-Euclidean height. 32 | Dimensionality uint 33 | 34 | // VivaldiErrorMax is the default error value when a node hasn't yet made 35 | // any observations. It also serves as an upper limit on the error value in 36 | // case observations cause the error value to increase without bound. 37 | VivaldiErrorMax float64 38 | 39 | // VivaldiCE is a tuning factor that controls the maximum impact an 40 | // observation can have on a node's confidence. See [1] for more details. 41 | VivaldiCE float64 42 | 43 | // VivaldiCC is a tuning factor that controls the maximum impact an 44 | // observation can have on a node's coordinate. See [1] for more details. 45 | VivaldiCC float64 46 | 47 | // AdjustmentWindowSize is a tuning factor that determines how many samples 48 | // we retain to calculate the adjustment factor as discussed in [3]. Setting 49 | // this to zero disables this feature. 50 | AdjustmentWindowSize uint 51 | 52 | // HeightMin is the minimum value of the height parameter. Since this 53 | // always must be positive, it will introduce a small amount error, so 54 | // the chosen value should be relatively small compared to "normal" 55 | // coordinates. 56 | HeightMin float64 57 | 58 | // LatencyFilterSamples is the maximum number of samples that are retained 59 | // per node, in order to compute a median. The intent is to ride out blips 60 | // but still keep the delay low, since our time to probe any given node is 61 | // pretty infrequent. See [2] for more details. 62 | LatencyFilterSize uint 63 | 64 | // GravityRho is a tuning factor that sets how much gravity has an effect 65 | // to try to re-center coordinates. See [2] for more details. 66 | GravityRho float64 67 | 68 | // metricLabels is the slice of labels to put on all emitted metrics 69 | MetricLabels []metrics.Label 70 | } 71 | 72 | // DefaultConfig returns a Config that has some default values suitable for 73 | // basic testing of the algorithm, but not tuned to any particular type of cluster. 74 | func DefaultConfig() *Config { 75 | return &Config{ 76 | Dimensionality: 8, 77 | VivaldiErrorMax: 1.5, 78 | VivaldiCE: 0.25, 79 | VivaldiCC: 0.25, 80 | AdjustmentWindowSize: 20, 81 | HeightMin: 10.0e-6, 82 | LatencyFilterSize: 3, 83 | GravityRho: 150.0, 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /coordinate/util_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package coordinate 5 | 6 | import ( 7 | "math" 8 | "testing" 9 | ) 10 | 11 | // verifyEqualFloats will compare f1 and f2 and fail if they are not 12 | // "equal" within a threshold. 13 | func verifyEqualFloats(t *testing.T, f1 float64, f2 float64) { 14 | const zeroThreshold = 1.0e-6 15 | if math.Abs(f1-f2) > zeroThreshold { 16 | t.Fatalf("equal assertion fail, %9.6f != %9.6f", f1, f2) 17 | } 18 | } 19 | 20 | // verifyEqualVectors will compare vec1 and vec2 and fail if they are not 21 | // "equal" within a threshold. 22 | func verifyEqualVectors(t *testing.T, vec1 []float64, vec2 []float64) { 23 | if len(vec1) != len(vec2) { 24 | t.Fatalf("vector length mismatch, %d != %d", len(vec1), len(vec2)) 25 | } 26 | 27 | for i := range vec1 { 28 | verifyEqualFloats(t, vec1[i], vec2[i]) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /demo/vagrant-cluster/README.md: -------------------------------------------------------------------------------- 1 | # Vagrant Serf Demo 2 | 3 | This demo provides a very simple Vagrantfile that creates two nodes, 4 | one at "172.20.20.10" and another at "172.20.20.11". Both are running 5 | a standard Ubuntu 12.04 distribution, with Serf pre-installed under 6 | `/usr/bin/serf`. 7 | 8 | To get started, you can start the cluster by just doing: 9 | 10 | $ vagrant up 11 | 12 | Once it is finished, you should be able to see the following: 13 | 14 | $ vagrant status 15 | Current machine states: 16 | n1 running (vmware_fusion) 17 | n2 running (vmware_fusion) 18 | 19 | At this point the two nodes are running and you can SSH in to play with them: 20 | 21 | $ vagrant ssh n1 22 | ... 23 | $ vagrant ssh n2 24 | ... 25 | 26 | To learn more about starting serf, joining nodes and interacting with the agent, 27 | checkout the [getting started guide](/docs/intro/getting-started/install.html.markdown). 28 | -------------------------------------------------------------------------------- /demo/vagrant-cluster/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | $script = <