github.com/google/fleetspeak@v0.1.15-0.20240426164851-4f31f62c1aea/fleetspeak/src/admin/cli/cli.go (about)

     1  // Copyright 2017 Google Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     https://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package cli contains methods useful for implementing administrative command
    16  // line utilities.
    17  package cli
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"os"
    23  	"path"
    24  	"sort"
    25  	"strconv"
    26  	"strings"
    27  	"time"
    28  
    29  	log "github.com/golang/glog"
    30  	"google.golang.org/grpc"
    31  
    32  	"github.com/google/fleetspeak/fleetspeak/src/admin/history"
    33  	"github.com/google/fleetspeak/fleetspeak/src/common"
    34  
    35  	sgrpc "github.com/google/fleetspeak/fleetspeak/src/server/proto/fleetspeak_server"
    36  	spb "github.com/google/fleetspeak/fleetspeak/src/server/proto/fleetspeak_server"
    37  )
    38  
    39  // dateFmt is a fairly dense, 23 character date format string, suitable for
    40  // tabular date information.
    41  const dateFmt = "15:04:05.000 2006.01.02"
    42  
    43  // Usage prints usage information describing the command line flags and behavior
    44  // of programs based on Execute.
    45  func Usage() {
    46  	n := path.Base(os.Args[0])
    47  	fmt.Fprintf(os.Stderr,
    48  		"Usage:\n"+
    49  			"    %s listclients [<client_id>...]\n"+
    50  			"    %s listcontacts <client_id> [limit]\n"+
    51  			"    %s analysehistory <client_id>\n"+
    52  			"    %s blacklistclient <client_id>\n"+
    53  			"\n", n, n, n, n)
    54  }
    55  
    56  // Execute examines command line flags and executes one of the standard command line
    57  // actions, as summarized by Usage. It requires a grpc connection to an admin server
    58  // and the command line parameters to interpret.
    59  func Execute(conn *grpc.ClientConn, args ...string) {
    60  	admin := sgrpc.NewAdminClient(conn)
    61  
    62  	if len(args) == 0 {
    63  		fmt.Fprint(os.Stderr, "A command is required.\n")
    64  		Usage()
    65  		os.Exit(1)
    66  	}
    67  
    68  	switch args[0] {
    69  	case "listclients":
    70  		ListClients(admin, args[1:]...)
    71  	case "listcontacts":
    72  		ListContacts(admin, args[1:]...)
    73  	case "analysehistory":
    74  		AnalyseHistory(admin, args[1:]...)
    75  	case "blacklistclient":
    76  		BlacklistClient(admin, args[1:]...)
    77  	default:
    78  		fmt.Fprintf(os.Stderr, "Unknown command: %v\n", args[0])
    79  		Usage()
    80  		os.Exit(1)
    81  	}
    82  }
    83  
    84  // ListClients prints a list of all clients in the fleetspeak system.
    85  func ListClients(c sgrpc.AdminClient, args ...string) {
    86  	var ids [][]byte
    87  	for i, arg := range args {
    88  		id, err := common.StringToClientID(arg)
    89  		if err != nil {
    90  			log.Exitf("Unable to convert [%s] (index %d) to client id: %v", arg, i, err)
    91  		}
    92  		ids = append(ids, id.Bytes())
    93  	}
    94  	ctx := context.Background()
    95  	res, err := c.ListClients(ctx, &spb.ListClientsRequest{ClientIds: ids}, grpc.MaxCallRecvMsgSize(1024*1024*1024))
    96  	if err != nil {
    97  		log.Exitf("ListClients RPC failed: %v", err)
    98  	}
    99  	if len(res.Clients) == 0 {
   100  		fmt.Println("No clients found.")
   101  		return
   102  	}
   103  	sort.Sort(byContactTime(res.Clients))
   104  	fmt.Printf("%-16s %-23s %s\n", "Client ID:", "Last Seen:", "Labels:")
   105  	for _, cl := range res.Clients {
   106  		id, err := common.BytesToClientID(cl.ClientId)
   107  		if err != nil {
   108  			log.Errorf("Ignoring invalid client id [%v], %v", cl.ClientId, err)
   109  			continue
   110  		}
   111  		var ls []string
   112  		for _, l := range cl.Labels {
   113  			ls = append(ls, l.ServiceName+":"+l.Label)
   114  		}
   115  		ts := cl.LastContactTime.AsTime()
   116  		tag := ""
   117  		if cl.Blacklisted {
   118  			tag = " *blacklisted*"
   119  		}
   120  		fmt.Printf("%v %v [%v]%s\n", id, ts.Format(dateFmt), strings.Join(ls, ","), tag)
   121  	}
   122  }
   123  
   124  // byContactTime adapts []*spb.Client for use by sort.Sort.
   125  type byContactTime []*spb.Client
   126  
   127  func (b byContactTime) Len() int           { return len(b) }
   128  func (b byContactTime) Swap(i, j int)      { b[i], b[j] = b[j], b[i] }
   129  func (b byContactTime) Less(i, j int) bool { return contactTime(b[i]).Before(contactTime(b[j])) }
   130  
   131  func contactTime(c *spb.Client) time.Time {
   132  	return time.Unix(c.LastContactTime.Seconds, int64(c.LastContactTime.Nanos))
   133  }
   134  
   135  // ListContacts prints a list contacts that the system has recorded for a
   136  // client. args[0] must be a client id. If present, args[1] limits to the most
   137  // recent args[1] contacts.
   138  func ListContacts(c sgrpc.AdminClient, args ...string) {
   139  	if len(args) == 0 || len(args) > 2 {
   140  		Usage()
   141  		os.Exit(1)
   142  	}
   143  	id, err := common.StringToClientID(args[0])
   144  	if err != nil {
   145  		log.Exitf("Unable to parse %s as client id: %v", args[0], err)
   146  	}
   147  	var lim int
   148  	if len(args) == 2 {
   149  		lim, err = strconv.Atoi(args[1])
   150  		if err != nil {
   151  			log.Exitf("Unable to parse %s as a limit: %v", args[1], err)
   152  		}
   153  	}
   154  
   155  	ctx := context.Background()
   156  	res, err := c.ListClientContacts(ctx, &spb.ListClientContactsRequest{ClientId: id.Bytes()}, grpc.MaxCallRecvMsgSize(1024*1024*1024))
   157  	if err != nil {
   158  		log.Exitf("ListClientContacts RPC failed: %v", err)
   159  	}
   160  	if len(res.Contacts) == 0 {
   161  		fmt.Println("No contacts found.")
   162  		return
   163  	}
   164  
   165  	fmt.Printf("Found %d contacts.\n", len(res.Contacts))
   166  
   167  	sort.Sort(byTimestamp(res.Contacts))
   168  	fmt.Printf("%-23s %s", "Timestamp:", "Observed IP:\n")
   169  	for i, con := range res.Contacts {
   170  		if lim > 0 && i > lim {
   171  			break
   172  		}
   173  		if err := con.Timestamp.CheckValid(); err != nil {
   174  			log.Errorf("Unable to parse timestamp for contact: %v", err)
   175  			continue
   176  		}
   177  		ts := con.Timestamp.AsTime()
   178  		fmt.Printf("%s %s\n", ts.Format(dateFmt), con.ObservedAddress)
   179  	}
   180  }
   181  
   182  // byTimestamp adapts []*spb.ClientContact for use by sort.Sort. Places most
   183  // recent contacts first.
   184  type byTimestamp []*spb.ClientContact
   185  
   186  func (b byTimestamp) Len() int           { return len(b) }
   187  func (b byTimestamp) Swap(i, j int)      { b[i], b[j] = b[j], b[i] }
   188  func (b byTimestamp) Less(i, j int) bool { return timestamp(b[i]).After(timestamp(b[j])) }
   189  
   190  func timestamp(c *spb.ClientContact) time.Time {
   191  	return time.Unix(c.Timestamp.Seconds, int64(c.Timestamp.Nanos))
   192  }
   193  
   194  // AnalyseHistory prints a summary analysis of a client's history. args[0] must
   195  // be a client id.
   196  func AnalyseHistory(c sgrpc.AdminClient, args ...string) {
   197  	if len(args) != 1 {
   198  		Usage()
   199  		os.Exit(1)
   200  	}
   201  	id, err := common.StringToClientID(args[0])
   202  	if err != nil {
   203  		log.Exitf("Unable to parse %s as client id: %v", args[0], err)
   204  	}
   205  	ctx := context.Background()
   206  	res, err := c.ListClientContacts(ctx, &spb.ListClientContactsRequest{ClientId: id.Bytes()}, grpc.MaxCallRecvMsgSize(1024*1024*1024))
   207  	if err != nil {
   208  		log.Exitf("ListClientContacts RPC failed: %v", err)
   209  	}
   210  	if len(res.Contacts) == 0 {
   211  		fmt.Println("No contacts found.")
   212  		return
   213  	}
   214  	s, err := history.Summarize(res.Contacts)
   215  	if err != nil {
   216  		log.Exitf("Error creating summary: %v", err)
   217  	}
   218  	fmt.Printf(`Raw Summary:
   219    First Recorded Contact: %v
   220    Last Recorded Contact: %v
   221    Contact Count: %d
   222    Observed IP Count: %d
   223    Split Points: %d
   224    Splits: %d
   225    Skips: %d
   226  `, s.Start, s.End, s.Count, s.IPCount, s.SplitPoints, s.Splits, s.Skips)
   227  	if s.Splits > 0 {
   228  		fmt.Printf("This client appears to have be restored %d times from %d different backup images.\n", s.Splits, s.SplitPoints)
   229  	}
   230  	if s.Skips > s.Splits {
   231  		fmt.Printf("Observed %d Skips, but only %d splits. The machine may have been cloned.\n", s.Skips, s.Splits)
   232  	}
   233  }
   234  
   235  // BlacklistClient blacklists a client id, forcing any client(s) using it to
   236  // rekey. args[0] must be a client id.
   237  func BlacklistClient(c sgrpc.AdminClient, args ...string) {
   238  	if len(args) != 1 {
   239  		Usage()
   240  		os.Exit(1)
   241  	}
   242  	id, err := common.StringToClientID(args[0])
   243  	if err != nil {
   244  		log.Exitf("Unable to parse %s as client id: %v", args[0], err)
   245  	}
   246  	ctx := context.Background()
   247  	if _, err := c.BlacklistClient(ctx, &spb.BlacklistClientRequest{ClientId: id.Bytes()}); err != nil {
   248  		log.Exitf("BlacklistClient RPC failed: %v", err)
   249  	}
   250  }