From 7707f36aac197da14f393cadd974dc49a6f55c48 Mon Sep 17 00:00:00 2001 From: Ondrej Fabry Date: Thu, 27 Aug 2020 09:53:31 +0200 Subject: [PATCH] feat(agentctl): Add config get/update commands (#1709) --- client/client_api.go | 8 +- cmd/agentctl/api/types/client.go | 21 +- cmd/agentctl/api/types/types.go | 23 +- cmd/agentctl/cli/cli.go | 16 +- cmd/agentctl/cli/flags.go | 2 - cmd/agentctl/client/api.go | 9 +- cmd/agentctl/client/client.go | 37 +- cmd/agentctl/client/http.go | 9 - cmd/agentctl/client/infra.go | 35 +- cmd/agentctl/client/model.go | 40 +-- cmd/agentctl/client/options.go | 4 - cmd/agentctl/client/scheduler.go | 22 +- cmd/agentctl/client/vpp.go | 6 +- .../commands/{models.go => all_models.go} | 0 cmd/agentctl/commands/commands.go | 1 + cmd/agentctl/commands/config.go | 319 +++++++++++++++++- cmd/agentctl/commands/dump.go | 199 ++++++----- cmd/agentctl/commands/errors.go | 3 +- cmd/agentctl/commands/formatter.go | 58 ++-- cmd/agentctl/commands/import.go | 75 ++-- cmd/agentctl/commands/model.go | 27 +- cmd/agentctl/commands/root.go | 6 +- cmd/agentctl/commands/util.go | 45 +++ cmd/agentctl/{agentctl.go => main.go} | 14 +- go.mod | 7 +- go.sum | 41 ++- pkg/version/version.go | 16 + plugins/kvscheduler/api/txn_record.go | 26 +- plugins/kvscheduler/txn_exec.go | 11 + plugins/kvscheduler/txn_record.go | 5 + proto/ligato/vpp/vpp_types.go | 20 +- tests/e2e/100_agentctl_test.go | 5 + 32 files changed, 783 insertions(+), 327 deletions(-) rename cmd/agentctl/commands/{models.go => all_models.go} (100%) create mode 100644 cmd/agentctl/commands/util.go rename cmd/agentctl/{agentctl.go => main.go} (80%) diff --git a/client/client_api.go b/client/client_api.go index 7e8c3a6158..65e35793e2 100644 --- a/client/client_api.go +++ b/client/client_api.go @@ -26,8 +26,12 @@ type ModelInfo = generic.ModelDetail type StateItem = generic.StateItem -// ConfigClient defines the client-side interface for config. -type ConfigClient interface { +// ConfigClient ... +// Deprecated: use GenericClient instead +type ConfigClient = GenericClient + +// GenericClient is the client-side interface for generic handler. +type GenericClient interface { // KnownModels retrieves list of known modules. KnownModels(class string) ([]*ModelInfo, error) diff --git a/cmd/agentctl/api/types/client.go b/cmd/agentctl/api/types/client.go index 2b977f3133..1ebf75caf3 100644 --- a/cmd/agentctl/api/types/client.go +++ b/cmd/agentctl/api/types/client.go @@ -14,22 +14,9 @@ package types -type Model struct { - Name string - Class string - Module string - Type string - Version string - KeyPrefix string - NameTemplate string `json:",omitempty"` - ProtoName string - ProtoFile string `json:",omitempty"` - GoType string `json:",omitempty"` - PkgPath string `json:",omitempty"` -} - type ModelListOptions struct { - Class string + Class string + Module string } type SchedulerDumpOptions struct { @@ -45,3 +32,7 @@ type SchedulerResyncOptions struct { Retry bool Verbose bool } + +type SchedulerHistoryOptions struct { + Count int +} diff --git a/cmd/agentctl/api/types/types.go b/cmd/agentctl/api/types/types.go index c1343dced2..f73e3fb922 100644 --- a/cmd/agentctl/api/types/types.go +++ b/cmd/agentctl/api/types/types.go @@ -26,22 +26,35 @@ type Version struct { Version string GitCommit string GitBranch string + BuildUser string BuildHost string BuildTime int64 + GoVersion string OS string Arch string -} -// Ping contains response of Engine API: -// GET "/_ping" -type Ping struct { APIVersion string OSType string } type Logger struct { - Logger string `json:"logger,omitempty"` + Logger string Level string `json:"level,omitempty"` } + +// Model provides info about registered model. +type Model struct { + Name string + Class string + Module string + Type string + Version string + KeyPrefix string + NameTemplate string `json:",omitempty"` + ProtoName string + ProtoFile string `json:",omitempty"` + GoType string `json:",omitempty"` + PkgPath string `json:",omitempty"` +} diff --git a/cmd/agentctl/cli/cli.go b/cmd/agentctl/cli/cli.go index cffae99915..137f01c4a4 100644 --- a/cmd/agentctl/cli/cli.go +++ b/cmd/agentctl/cli/cli.go @@ -147,14 +147,12 @@ func (cli *AgentCli) Initialize(opts *ClientOptions, ops ...InitializeOpt) error return err } } - if opts.Debug { debug.Enable() SetLogLevel("debug") } else { SetLogLevel(opts.LogLevel) } - cfg, err := MakeConfig() if err != nil { return err @@ -162,7 +160,6 @@ func (cli *AgentCli) Initialize(opts *ClientOptions, ops ...InitializeOpt) error if opts.Debug { logging.Debug(cfg.DebugOutput()) } - if cli.client == nil { clientOptions := buildClientOptions(cfg) cli.client, err = client.NewClientWithOpts(clientOptions...) @@ -189,7 +186,6 @@ func buildClientOptions(cfg *Config) []client.Opt { client.WithEtcdEndpoints(cfg.EtcdEndpoints), client.WithEtcdDialTimeout(cfg.EtcdDialTimeout), } - if cfg.ShouldUseSecureGRPC() { clientOpts = append(clientOpts, client.WithGrpcTLS( cfg.GRPCSecure.CertFile, @@ -214,26 +210,24 @@ func buildClientOptions(cfg *Config) []client.Opt { cfg.KVDBSecure.SkipVerify, )) } - return clientOpts } func (cli *AgentCli) initializeFromClient() { logging.Debugf("initializeFromClient (DefaultVersion: %v)", cli.DefaultVersion()) - ping, err := cli.client.Ping(context.Background()) + version, err := cli.client.AgentVersion(context.Background()) if err != nil { // Default to true if we fail to connect to daemon cli.serverInfo = ServerInfo{} - if ping.APIVersion != "" { - cli.client.NegotiateAPIVersionPing(ping) + if version != nil && version.APIVersion != "" { + cli.client.NegotiateAPIVersionPing(version) } return } - cli.serverInfo = ServerInfo{ - OSType: ping.OSType, + OSType: version.OSType, } - cli.client.NegotiateAPIVersionPing(ping) + cli.client.NegotiateAPIVersionPing(version) } diff --git a/cmd/agentctl/cli/flags.go b/cmd/agentctl/cli/flags.go index 7bcb8265d7..9d33bb22e9 100644 --- a/cmd/agentctl/cli/flags.go +++ b/cmd/agentctl/cli/flags.go @@ -84,13 +84,11 @@ func SetLogLevel(logLevel string) { logging.DefaultLogger.SetLevel(logging.WarnLevel) return } - lvl, err := logrus.ParseLevel(logLevel) if err != nil { fmt.Fprintf(os.Stderr, "Unable to parse logging level: %s\n", logLevel) os.Exit(1) } - logrus.SetLevel(lvl) logging.DefaultLogger.SetLevel(logging.ParseLogLevel(logLevel)) } diff --git a/cmd/agentctl/client/api.go b/cmd/agentctl/client/api.go index e412f55160..94d333c613 100644 --- a/cmd/agentctl/client/api.go +++ b/cmd/agentctl/client/api.go @@ -12,6 +12,7 @@ import ( "go.ligato.io/vpp-agent/v3/client" "go.ligato.io/vpp-agent/v3/cmd/agentctl/api/types" "go.ligato.io/vpp-agent/v3/plugins/kvscheduler/api" + "go.ligato.io/vpp-agent/v3/proto/ligato/configurator" "go.ligato.io/vpp-agent/v3/proto/ligato/kvscheduler" ) @@ -23,7 +24,8 @@ type APIClient interface { VppAPIClient MetricsAPIClient - ConfigClient() (client.ConfigClient, error) + GenericClient() (client.GenericClient, error) + ConfiguratorClient() (configurator.ConfiguratorServiceClient, error) AgentHost() string Version() string @@ -32,14 +34,13 @@ type APIClient interface { HTTPClient() *http.Client AgentVersion(ctx context.Context) (*types.Version, error) NegotiateAPIVersion(ctx context.Context) - NegotiateAPIVersionPing(types.Ping) + NegotiateAPIVersionPing(version *types.Version) Close() error } // InfraAPIClient defines API client methods for the system type InfraAPIClient interface { Status(ctx context.Context) (*probe.ExposedStatus, error) - Ping(ctx context.Context) (types.Ping, error) LoggerList(ctx context.Context) ([]types.Logger, error) LoggerSet(ctx context.Context, logger, level string) error } @@ -54,11 +55,13 @@ type SchedulerAPIClient interface { SchedulerDump(ctx context.Context, opts types.SchedulerDumpOptions) ([]api.KVWithMetadata, error) SchedulerValues(ctx context.Context, opts types.SchedulerValuesOptions) ([]*kvscheduler.BaseValueStatus, error) SchedulerResync(ctx context.Context, opts types.SchedulerResyncOptions) (*api.RecordedTxn, error) + SchedulerHistory(ctx context.Context, opts types.SchedulerHistoryOptions) (api.RecordedTxns, error) } // VppAPIClient defines API client methods for the VPP type VppAPIClient interface { VppRunCli(ctx context.Context, cmd string) (reply string, err error) + VppGetStats(ctx context.Context, typ string) error } type MetricsAPIClient interface { diff --git a/cmd/agentctl/client/client.go b/cmd/agentctl/client/client.go index e1a0eaf987..1aab581c40 100644 --- a/cmd/agentctl/client/client.go +++ b/cmd/agentctl/client/client.go @@ -28,19 +28,19 @@ import ( "github.com/coreos/etcd/clientv3" "github.com/docker/docker/api/types/versions" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials" - "go.ligato.io/cn-infra/v2/db/keyval" "go.ligato.io/cn-infra/v2/db/keyval/etcd" "go.ligato.io/cn-infra/v2/logging" "go.ligato.io/cn-infra/v2/logging/logrus" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" "go.ligato.io/vpp-agent/v3/client" "go.ligato.io/vpp-agent/v3/client/remoteclient" "go.ligato.io/vpp-agent/v3/cmd/agentctl/api" "go.ligato.io/vpp-agent/v3/cmd/agentctl/api/types" "go.ligato.io/vpp-agent/v3/pkg/debug" + "go.ligato.io/vpp-agent/v3/proto/ligato/configurator" ) const ( @@ -107,16 +107,13 @@ func NewClientWithOpts(ops ...Opt) (*Client, error) { grpcPort: DefaultPortGRPC, httpPort: DefaultPortHTTP, } - for _, op := range ops { if err := op(c); err != nil { return nil, err } } - c.grpcAddr = net.JoinHostPort(c.host, strconv.Itoa(c.grpcPort)) c.httpAddr = net.JoinHostPort(c.host, strconv.Itoa(c.httpPort)) - return c, nil } @@ -155,8 +152,7 @@ func (c *Client) GRPCConn() (*grpc.ClientConn, error) { return c.grpcClient, nil } -// ConfigClient returns "remoteclient" with gRPC connection. -func (c *Client) ConfigClient() (client.ConfigClient, error) { +func (c *Client) GenericClient() (client.GenericClient, error) { conn, err := c.GRPCConn() if err != nil { return nil, err @@ -164,6 +160,15 @@ func (c *Client) ConfigClient() (client.ConfigClient, error) { return remoteclient.NewClientGRPC(conn), nil } +// ConfiguratorClient returns "confi" with gRPC connection. +func (c *Client) ConfiguratorClient() (configurator.ConfiguratorServiceClient, error) { + conn, err := c.GRPCConn() + if err != nil { + return nil, err + } + return configurator.NewConfiguratorServiceClient(conn), nil +} + // HTTPClient returns configured HTTP client. func (c *Client) HTTPClient() *http.Client { if c.httpClient == nil { @@ -231,12 +236,12 @@ func (c *Client) getAPIPath(ctx context.Context, p string, query url.Values) str func (c *Client) NegotiateAPIVersion(ctx context.Context) { if !c.manualOverride { - ping, _ := c.Ping(ctx) - c.negotiateAPIVersionPing(ping) + version, _ := c.AgentVersion(ctx) + c.negotiateAPIVersionPing(version) } } -func (c *Client) NegotiateAPIVersionPing(p types.Ping) { +func (c *Client) NegotiateAPIVersionPing(p *types.Version) { if !c.manualOverride { c.negotiateAPIVersionPing(p) } @@ -244,22 +249,19 @@ func (c *Client) NegotiateAPIVersionPing(p types.Ping) { // negotiateAPIVersionPing queries the API and updates the version to match the // API version. Any errors are silently ignored. -func (c *Client) negotiateAPIVersionPing(p types.Ping) { +func (c *Client) negotiateAPIVersionPing(p *types.Version) { // try the latest version before versioning headers existed if p.APIVersion == "" { p.APIVersion = "0.1" } - // if the client is not initialized with a version, start with the latest supported version if c.version == "" { c.version = api.DefaultVersion } - // if server version is lower than the client version, downgrade if versions.LessThan(p.APIVersion, c.version) { c.version = p.APIVersion } - // Store the results, so that automatic API version negotiation (if enabled) // won't be performed on the next request. if c.negotiateVersion { @@ -272,9 +274,7 @@ func connectGrpc(addr string, tc *tls.Config) (*grpc.ClientConn, error) { if tc != nil { dialOpt = grpc.WithTransportCredentials(credentials.NewTLS(tc)) } - logging.Debugf("dialing grpc address: %v", addr) - return grpc.Dial(addr, dialOpt) } @@ -285,12 +285,10 @@ func connectEtcd(endpoints []string, dialTimeout time.Duration, tc *tls.Config) } else { log.SetLevel(logging.WarnLevel) } - dt := defaultEtcdDialTimeout if dialTimeout != 0 { dt = dialTimeout } - cfg := etcd.ClientConfig{ Config: &clientv3.Config{ Endpoints: endpoints, @@ -299,7 +297,6 @@ func connectEtcd(endpoints []string, dialTimeout time.Duration, tc *tls.Config) }, OpTimeout: defaultEtcdOpTimeout, } - kvdb, err := etcd.NewEtcdConnectionWithBytes(cfg, log) if err != nil { return nil, err diff --git a/cmd/agentctl/client/http.go b/cmd/agentctl/client/http.go index acf06b4541..87ba2314c7 100644 --- a/cmd/agentctl/client/http.go +++ b/cmd/agentctl/client/http.go @@ -98,12 +98,10 @@ func (c *Client) sendRequest(ctx context.Context, method, path string, query url if err != nil { return serverResponse{}, err } - resp, err := c.doRequest(ctx, req) if err != nil { return resp, err } - err = c.checkResponseErr(resp) return resp, err } @@ -113,7 +111,6 @@ func (c *Client) doRequest(ctx context.Context, req *http.Request) (serverRespon statusCode: -1, reqURL: req.URL, } - var ( err error resp *http.Response @@ -138,7 +135,6 @@ func (c *Client) doRequest(ctx context.Context, req *http.Request) (serverRespon if c.scheme != "https" && strings.Contains(err.Error(), "malformed HTTP response") { return serverResp, fmt.Errorf("%v.\n* Are you trying to connect to a TLS-enabled daemon without TLS?", err) } - if c.scheme == "https" && strings.Contains(err.Error(), "bad certificate") { return serverResp, errors.Wrap(err, "The server probably has client authentication (--tlsverify) enabled. Please check your TLS client certification settings") } @@ -166,7 +162,6 @@ func (c *Client) doRequest(ctx context.Context, req *http.Request) (serverRespon } } } - return serverResp, errors.Wrap(err, "error during connect") } if logrus.IsLevelEnabled(logrus.DebugLevel) { @@ -191,7 +186,6 @@ func (c *Client) checkResponseErr(serverResp serverResponse) error { if serverResp.statusCode >= 200 && serverResp.statusCode < 400 { return nil } - var body []byte var err error if serverResp.body != nil { @@ -213,12 +207,10 @@ func (c *Client) checkResponseErr(serverResp serverResponse) error { return fmt.Errorf("request returned %s for API route and version %s, check if the server supports the requested API version", http.StatusText(serverResp.statusCode), serverResp.reqURL) } - var ct string if serverResp.header != nil { ct = serverResp.header.Get("Content-Type") } - var errorMsg string if ct == "application/json" { var errorResponse types.ErrorResponse @@ -229,7 +221,6 @@ func (c *Client) checkResponseErr(serverResp serverResponse) error { } else { errorMsg = string(body) } - errorMsg = fmt.Sprintf("[%d] %s", serverResp.statusCode, strings.TrimSpace(errorMsg)) return errors.Wrap(errors.New(errorMsg), "Error response from daemon") diff --git a/cmd/agentctl/client/infra.go b/cmd/agentctl/client/infra.go index 7b9c57aac6..8069cd49ec 100644 --- a/cmd/agentctl/client/infra.go +++ b/cmd/agentctl/client/infra.go @@ -18,44 +18,12 @@ import ( "context" "encoding/json" "fmt" - "path" - "github.com/sirupsen/logrus" "go.ligato.io/cn-infra/v2/health/probe" "go.ligato.io/vpp-agent/v3/cmd/agentctl/api/types" ) -// Ping pings the server and returns the value of the "API-Version" headers. -func (c *Client) Ping(ctx context.Context) (types.Ping, error) { - var ping types.Ping - logrus.Debugf("sending ping request") - // Using cli.buildRequest() + cli.doRequest() instead of cli.sendRequest() - // because ping requests are used during API version negotiation, so we want - // to hit the non-versioned /_ping endpoint, not /v1.xx/_ping - req, err := c.buildRequest("GET", path.Join(c.basePath, "/ping"), nil, nil) - if err != nil { - return ping, err - } - serverResp, err := c.doRequest(ctx, req) - defer ensureReaderClosed(serverResp) - if err != nil { - return ping, err - } - return parsePingResponse(c, serverResp) -} - -func parsePingResponse(cli *Client, resp serverResponse) (types.Ping, error) { - var ping types.Ping - if resp.header == nil { - err := cli.checkResponseErr(resp) - return ping, err - } - ping.APIVersion = resp.header.Get("API-Version") - err := cli.checkResponseErr(resp) - return ping, err -} - // AgentVersion returns information about Agent. func (c *Client) AgentVersion(ctx context.Context) (*types.Version, error) { resp, err := c.get(ctx, "/info/version", nil, nil) @@ -63,8 +31,9 @@ func (c *Client) AgentVersion(ctx context.Context) (*types.Version, error) { if err != nil { return nil, err } - var v types.Version + v.APIVersion = resp.header.Get("API-Version") + err = json.NewDecoder(resp.body).Decode(&v) return &v, err } diff --git a/cmd/agentctl/client/model.go b/cmd/agentctl/client/model.go index dcbf48eb1c..d6d57a93b9 100644 --- a/cmd/agentctl/client/model.go +++ b/cmd/agentctl/client/model.go @@ -14,7 +14,7 @@ import ( ) func (c *Client) ModelList(ctx context.Context, opts types.ModelListOptions) ([]types.Model, error) { - cfgClient, err := c.ConfigClient() + cfgClient, err := c.GenericClient() if err != nil { return nil, err } @@ -22,18 +22,30 @@ func (c *Client) ModelList(ctx context.Context, opts types.ModelListOptions) ([] if err != nil { return nil, err } - logrus.Debugf("retrieved %d known models", len(knownModels)) if debug.IsEnabledFor("models") { for _, m := range knownModels { - logrus.Debug(" - ", proto.CompactTextString(m)) + logrus.Trace(" - ", proto.CompactTextString(m)) } } - allModels := convertModels(knownModels) - sort.Sort(modelsByName(allModels)) - return allModels, nil + return sortModels(allModels), nil +} + +// sortModels sorts models in this order: +// Class > Name > Version +func sortModels(list []types.Model) []types.Model { + sort.Slice(list, func(i, j int) bool { + if list[i].Class != list[j].Class { + return list[i].Class < list[j].Class + } + if list[i].Name != list[j].Name { + return list[i].Name < list[j].Name + } + return list[i].Version < list[j].Version + }) + return list } func convertModels(knownModels []*generic.ModelDetail) []types.Model { @@ -64,7 +76,6 @@ func convertModels(knownModels []*generic.ModelDetail) []types.Model { protoFile = o.Values[0] } } - // fix key prefixes for models with no template if nameTemplate == "" { km, err := models.GetModel(spec.ModelName()) @@ -73,7 +84,6 @@ func convertModels(knownModels []*generic.ModelDetail) []types.Model { keyPrefix = km.KeyPrefix() } } - model := types.Model{ Name: spec.ModelName(), Module: spec.Module, @@ -91,17 +101,3 @@ func convertModels(knownModels []*generic.ModelDetail) []types.Model { } return allModels } - -type modelsByName []types.Model - -func (s modelsByName) Len() int { - return len(s) -} - -func (s modelsByName) Less(i, j int) bool { - return s[i].Name < s[j].Name -} - -func (s modelsByName) Swap(i, j int) { - s[i], s[j] = s[j], s[i] -} diff --git a/cmd/agentctl/client/options.go b/cmd/agentctl/client/options.go index 47207257a6..4985c3c628 100644 --- a/cmd/agentctl/client/options.go +++ b/cmd/agentctl/client/options.go @@ -66,7 +66,6 @@ func WithEtcdDialTimeout(t time.Duration) Opt { func withTLS(cert, key, ca string, skipVerify bool) (*tls.Config, error) { var options []tlsconfig.Option - if cert != "" && key != "" { options = append(options, tlsconfig.CertKey(cert, key)) } @@ -76,7 +75,6 @@ func withTLS(cert, key, ca string, skipVerify bool) (*tls.Config, error) { if skipVerify { options = append(options, tlsconfig.SkipServerVerification()) } - return tlsconfig.New(options...) } @@ -153,7 +151,6 @@ func WithHTTPHeader(k, v string) Opt { if c.customHTTPHeaders == nil { c.customHTTPHeaders = make(map[string]string) } - c.customHTTPHeaders[k] = v return nil } @@ -165,7 +162,6 @@ func WithHTTPBasicAuth(s string) Opt { if s == "" { return func(c *Client) error { return nil } } - auth := base64.StdEncoding.EncodeToString([]byte(s)) return WithHTTPHeader("Authorization", "Basic "+auth) } diff --git a/cmd/agentctl/client/scheduler.go b/cmd/agentctl/client/scheduler.go index e39a20ffcf..1eed6af89f 100644 --- a/cmd/agentctl/client/scheduler.go +++ b/cmd/agentctl/client/scheduler.go @@ -24,7 +24,6 @@ func (c *Client) SchedulerDump(ctx context.Context, opts types.SchedulerDumpOpti api.KVWithMetadata Value ProtoWithName } - query := url.Values{} query.Set("key-prefix", opts.KeyPrefix) query.Set("view", opts.View) @@ -33,12 +32,10 @@ func (c *Client) SchedulerDump(ctx context.Context, opts types.SchedulerDumpOpti if err != nil { return nil, err } - var kvdump []KVWithMetadata if err := json.NewDecoder(resp.body).Decode(&kvdump); err != nil { return nil, fmt.Errorf("decoding reply failed: %v", err) } - var dump []api.KVWithMetadata for _, kvd := range kvdump { d := kvd.KVWithMetadata @@ -50,13 +47,14 @@ func (c *Client) SchedulerDump(ctx context.Context, opts types.SchedulerDumpOpti return nil, fmt.Errorf("unknown proto message defined for key %s", d.Key) } d.Value = reflect.New(valueType.Elem()).Interface().(proto.Message) + if len(kvd.Value.ProtoMsgData) > 0 && kvd.Value.ProtoMsgData[0] == '{' { err = jsonpb.UnmarshalString(kvd.Value.ProtoMsgData, d.Value) } else { err = proto.UnmarshalText(kvd.Value.ProtoMsgData, d.Value) } if err != nil { - return nil, fmt.Errorf("decoding reply failed: %v", err) + return nil, fmt.Errorf("decoding dump reply for %v failed: %v", valueType, err) } dump = append(dump, d) } @@ -71,7 +69,6 @@ func (c *Client) SchedulerValues(ctx context.Context, opts types.SchedulerValues if err != nil { return nil, err } - var status []*kvscheduler.BaseValueStatus if err := json.NewDecoder(resp.body).Decode(&status); err != nil { return nil, fmt.Errorf("decoding reply failed: %v", err) @@ -101,3 +98,18 @@ func (c *Client) SchedulerResync(ctx context.Context, opts types.SchedulerResync return &rectxn, nil } + +func (c *Client) SchedulerHistory(ctx context.Context, opts types.SchedulerHistoryOptions) (api.RecordedTxns, error) { + query := url.Values{} + + resp, err := c.get(ctx, "/scheduler/txn-history", query, nil) + if err != nil { + return nil, err + } + + var rectxn api.RecordedTxns + if err := json.NewDecoder(resp.body).Decode(&rectxn); err != nil { + return nil, fmt.Errorf("decoding reply failed: %v", err) + } + return rectxn, nil +} diff --git a/cmd/agentctl/client/vpp.go b/cmd/agentctl/client/vpp.go index 438bc93a2e..27d978812c 100644 --- a/cmd/agentctl/client/vpp.go +++ b/cmd/agentctl/client/vpp.go @@ -28,9 +28,13 @@ func (c *Client) VppRunCli(ctx context.Context, cmd string) (reply string, err e if err != nil { return "", fmt.Errorf("HTTP POST request failed: %v", err) } - if err := json.NewDecoder(resp.body).Decode(&reply); err != nil { return "", fmt.Errorf("decoding reply failed: %v", err) } return reply, nil } + +func (c *Client) VppGetStats(ctx context.Context, typ string) error { + // TODO: implement this + return nil +} diff --git a/cmd/agentctl/commands/models.go b/cmd/agentctl/commands/all_models.go similarity index 100% rename from cmd/agentctl/commands/models.go rename to cmd/agentctl/commands/all_models.go diff --git a/cmd/agentctl/commands/commands.go b/cmd/agentctl/commands/commands.go index f9d00968ef..2fcbe071a0 100644 --- a/cmd/agentctl/commands/commands.go +++ b/cmd/agentctl/commands/commands.go @@ -51,6 +51,7 @@ func NewRoot(agentCli *cli.AgentCli) *Root { func AddBaseCommands(cmd *cobra.Command, cli cli.Cli) { cmd.AddCommand( NewConfigCommand(cli), + newModelsCommand(cli), NewModelCommand(cli), NewLogCommand(cli), NewImportCommand(cli), diff --git a/cmd/agentctl/commands/config.go b/cmd/agentctl/commands/config.go index f8149c5e3d..bd2ebf7c78 100644 --- a/cmd/agentctl/commands/config.go +++ b/cmd/agentctl/commands/config.go @@ -16,11 +16,21 @@ package commands import ( "context" + "fmt" + "io" + "io/ioutil" + "time" + yaml2 "github.com/ghodss/yaml" + "github.com/olekukonko/tablewriter" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "google.golang.org/protobuf/encoding/protojson" "go.ligato.io/vpp-agent/v3/cmd/agentctl/api/types" agentcli "go.ligato.io/vpp-agent/v3/cmd/agentctl/cli" + kvs "go.ligato.io/vpp-agent/v3/plugins/kvscheduler/api" + "go.ligato.io/vpp-agent/v3/proto/ligato/configurator" ) func NewConfigCommand(cli agentcli.Cli) *cobra.Command { @@ -29,18 +39,175 @@ func NewConfigCommand(cli agentcli.Cli) *cobra.Command { Short: "Manage agent configuration", } cmd.AddCommand( + newConfigGetCommand(cli), + newConfigRetrieveCommand(cli), + newConfigUpdateCommand(cli), newConfigResyncCommand(cli), + newConfigHistoryCommand(cli), ) return cmd } +func newConfigGetCommand(cli agentcli.Cli) *cobra.Command { + var ( + opts ConfigGetOptions + ) + cmd := &cobra.Command{ + Use: "get", + Short: "Get config from agent", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runConfigGet(cli, opts) + }, + } + flags := cmd.Flags() + flags.StringVarP(&opts.Format, "format", "f", "", "Format output") + return cmd +} + +type ConfigGetOptions struct { + Format string +} + +func runConfigGet(cli agentcli.Cli, opts ConfigGetOptions) error { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + client, err := cli.Client().ConfiguratorClient() + if err != nil { + return err + } + resp, err := client.Get(ctx, &configurator.GetRequest{}) + if err != nil { + return err + } + + format := opts.Format + if len(format) == 0 { + format = `yaml` + } + if err := formatAsTemplate(cli.Out(), format, resp.Config); err != nil { + return err + } + + return nil +} + +func newConfigUpdateCommand(cli agentcli.Cli) *cobra.Command { + var ( + opts ConfigUpdateOptions + ) + cmd := &cobra.Command{ + Use: "update", + Short: "Update config in agent", + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runConfigUpdate(cli, opts, args) + }, + } + flags := cmd.Flags() + flags.StringVarP(&opts.Format, "format", "f", "", "Format output") + flags.BoolVar(&opts.Replace, "replace", false, "Replaces entire config in agent") + return cmd +} + +type ConfigUpdateOptions struct { + Format string + Replace bool +} + +func runConfigUpdate(cli agentcli.Cli, opts ConfigUpdateOptions, args []string) error { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + client, err := cli.Client().ConfiguratorClient() + if err != nil { + return err + } + + if len(args) == 0 { + return fmt.Errorf("missing file argument") + } + file := args[0] + b, err := ioutil.ReadFile(file) + if err != nil { + return fmt.Errorf("reading file %s: %w", file, err) + } + + var update = &configurator.Config{} + bj, err := yaml2.YAMLToJSON(b) + if err != nil { + return fmt.Errorf("converting to JSON: %w", err) + } + err = protojson.Unmarshal(bj, update) + if err != nil { + return err + } + logrus.Infof("loaded config update:\n%s", update) + + if _, err := client.Update(ctx, &configurator.UpdateRequest{ + Update: update, + FullResync: opts.Replace, + }); err != nil { + return err + } + + return nil +} + +func newConfigRetrieveCommand(cli agentcli.Cli) *cobra.Command { + var ( + opts ConfigRetrieveOptions + ) + cmd := &cobra.Command{ + Use: "retrieve", + Aliases: []string{"ret", "read"}, + Short: "Retrieve currently running config", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runConfigRetrieve(cli, opts) + }, + } + flags := cmd.Flags() + flags.StringVarP(&opts.Format, "format", "f", "", "Format output") + return cmd +} + +type ConfigRetrieveOptions struct { + Format string +} + +func runConfigRetrieve(cli agentcli.Cli, opts ConfigRetrieveOptions) error { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + client, err := cli.Client().ConfiguratorClient() + if err != nil { + return err + } + resp, err := client.Dump(ctx, &configurator.DumpRequest{}) + if err != nil { + return err + } + + format := opts.Format + if len(format) == 0 { + format = `yaml` + } + if err := formatAsTemplate(cli.Out(), format, resp); err != nil { + return err + } + + return nil +} + func newConfigResyncCommand(cli agentcli.Cli) *cobra.Command { var ( opts ConfigResyncOptions ) cmd := &cobra.Command{ Use: "resync", - Short: "Run config resync in agent", + Short: "Run config resync", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { return runConfigResync(cli, opts) @@ -59,6 +226,9 @@ type ConfigResyncOptions struct { Retry bool } +// TODO: define default format with go template +const defaultFormatConfigResync = `json` + func runConfigResync(cli agentcli.Cli, opts ConfigResyncOptions) error { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -70,12 +240,10 @@ func runConfigResync(cli agentcli.Cli, opts ConfigResyncOptions) error { if err != nil { return err } - format := opts.Format if len(format) == 0 { format = defaultFormatConfigResync } - if err := formatAsTemplate(cli.Out(), format, rectxn); err != nil { return err } @@ -83,5 +251,146 @@ func runConfigResync(cli agentcli.Cli, opts ConfigResyncOptions) error { return nil } -// TODO: define default format with go template -const defaultFormatConfigResync = `json` +func newConfigHistoryCommand(cli agentcli.Cli) *cobra.Command { + var ( + opts ConfigHistoryOptions + ) + cmd := &cobra.Command{ + Use: "history", + Short: "Retrieve config history", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runConfigHistory(cli, opts) + }, + } + flags := cmd.Flags() + flags.StringVarP(&opts.Format, "format", "f", "", "Format output") + return cmd +} + +type ConfigHistoryOptions struct { + Format string + Verbose bool + Retry bool +} + +func runConfigHistory(cli agentcli.Cli, opts ConfigHistoryOptions) error { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + txns, err := cli.Client().SchedulerHistory(ctx, types.SchedulerHistoryOptions{}) + if err != nil { + return err + } + format := opts.Format + if len(format) == 0 { + printHistoryTable(cli.Out(), txns) + } + if err := formatAsTemplate(cli.Out(), format, txns); err != nil { + return err + } + + return nil +} + +func printHistoryTable(out io.Writer, txns kvs.RecordedTxns) { + table := tablewriter.NewWriter(out) + table.SetHeader([]string{ + "Seq", "", "Type", "", "Age", "Summary", "Result", + }) + table.SetAutoWrapText(false) + table.SetAutoFormatHeaders(true) + table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) + table.SetAlignment(tablewriter.ALIGN_LEFT) + table.SetCenterSeparator("") + table.SetColumnSeparator("") + table.SetRowSeparator("") + table.SetHeaderLine(false) + table.SetBorder(false) + table.SetTablePadding("\t") + + for _, txn := range txns { + typ := kvs.TxnTypeToString(txn.TxnType) + info := txn.Description + if txn.TxnType == kvs.NBTransaction && txn.ResyncType != kvs.NotResync { + info = fmt.Sprintf("%s", kvs.ResyncTypeToString(txn.ResyncType)) + } + elapsed := txn.Stop.Sub(txn.Start).Round(time.Millisecond / 10) + took := elapsed.String() + if elapsed < time.Millisecond/10 { + took = "<.1ms" + } else if elapsed > time.Millisecond*100 { + took = elapsed.Round(time.Millisecond).String() + } + _ = took + result := txnErrors(txn) + resClr := tablewriter.FgGreenColor + if result != "" { + resClr = tablewriter.FgHiRedColor + } else { + result = "ok" + } + var typClr int + switch txn.TxnType { + case kvs.NBTransaction: + typClr = tablewriter.FgYellowColor + case kvs.SBNotification: + typClr = tablewriter.FgCyanColor + case kvs.RetryFailedOps: + typClr = tablewriter.FgMagentaColor + } + age := shortHumanDuration(time.Since(txn.Start)) + summary := fmt.Sprintf("%d executed", len(txn.Executed)) + row := []string{ + fmt.Sprintf("%3v", txn.SeqNum), + fmt.Sprintf("%v", txnIcon(txn)), + typ, + info, + fmt.Sprintf("%-3s", age), + //fmt.Sprintf("%-3s (took %v)", age, took), + fmt.Sprintf("values: %2d -> %s", len(txn.Values), summary), + result, + } + clrs := []tablewriter.Colors{ + {tablewriter.Normal, typClr}, + {tablewriter.Bold, typClr + 60}, + {tablewriter.Normal, typClr}, + {}, + {}, + {}, + {resClr}, + } + table.Rich(row, clrs) + } + table.Render() +} + +func txnErrors(txn *kvs.RecordedTxn) string { + var errs Errors + for _, r := range txn.Executed { + if r.NewErrMsg != "" { + r.NewErr = fmt.Errorf("%v", r.NewErrMsg) + errs = append(errs, r.NewErr) + } + } + if errs != nil { + word := "error" + if len(errs) > 1 { + word = fmt.Sprintf("%d errors", len(errs)) + } + return fmt.Sprintf("%s: %v", word, errs.Error()) + } + return "" +} + +func txnIcon(txn *kvs.RecordedTxn) string { + switch txn.TxnType { + case kvs.SBNotification: + return "⇧" + case kvs.NBTransaction: + return "⟱" + case kvs.RetryFailedOps: + return "↻" + } + return "?" +} diff --git a/cmd/agentctl/commands/dump.go b/cmd/agentctl/commands/dump.go index ced2a45827..9ae4217e08 100644 --- a/cmd/agentctl/commands/dump.go +++ b/cmd/agentctl/commands/dump.go @@ -15,71 +15,113 @@ package commands import ( - "bytes" "context" "fmt" "io" "sort" "strings" - "text/tabwriter" "github.com/golang/protobuf/proto" + "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" "go.ligato.io/cn-infra/v2/logging" "go.ligato.io/vpp-agent/v3/cmd/agentctl/api/types" agentcli "go.ligato.io/vpp-agent/v3/cmd/agentctl/cli" + "go.ligato.io/vpp-agent/v3/pkg/models" "go.ligato.io/vpp-agent/v3/plugins/kvscheduler/api" ) func NewDumpCommand(cli agentcli.Cli) *cobra.Command { - var opts DumpOptions - + var ( + opts DumpOptions + ) cmd := &cobra.Command{ - Use: "dump MODEL", + Use: "dump MODEL [MODEL...]", Short: "Dump running state", + Long: "Dumps actual running state", Example: ` - To dump all data: - $ {{.CommandPath}} all + Dump everything + {{.CommandPath}} all - To dump all VPP data in json format run: - $ {{.CommandPath}} -f json vpp.* + Dump VPP interfaces & routes + {{.CommandPath}} vpp.interfaces vpp.l3.routes - To use different dump view use --view flag: - $ {{.CommandPath}} --view=NB vpp.interfaces`, - Args: cobra.MinimumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { + Dump all VPP data in JSON format + {{.CommandPath}} -f json vpp.* + + Dump only VPP memif interfaces + {{.CommandPath}} -f '{{` + "`{{range .}}{{if eq .Value.Type.String \"MEMIF\" }}{{json .}}{{end}}{{end}}`" + `}}' vpp.interfaces + + Dump everything currently defined at northbound + {{.CommandPath}} --view=NB all + + Dump all VPP & Linux data directly from southband + {{.CommandPath}} --view=SB vpp.* linux.* + + Dump all VPP & Linux data directly from southband + {{.CommandPath}} --view=SB all +`, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + fmt.Fprintf(cli.Err(), "You must specify models to dump. Use \"%s models\" for a complete list of known models.\n", cmd.Root().Name()) + return fmt.Errorf("no models specified") + } opts.Models = args + return opts.Validate() + }, + RunE: func(cmd *cobra.Command, args []string) error { return runDump(cli, opts) }, } flags := cmd.Flags() flags.StringVar(&opts.View, "view", "cached", "Dump view type: cached, NB, SB") - flags.StringVarP(&opts.Format, "format", "f", "", "Format output") + flags.StringVar(&opts.Origin, "origin", "", "Show only data with specific origin: NB, SB, unknown") + flags.StringVarP(&opts.Format, "format", "f", "", "Format output (json|yaml|go-template|proto)") return cmd } type DumpOptions struct { Models []string View string + Origin string Format string } -func runDump(cli agentcli.Cli, opts DumpOptions) error { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - var dumpView string +func (opts *DumpOptions) Validate() error { + // models + if opts.Models[0] == "all" { + opts.Models = []string{"*"} + } + // view switch strings.ToLower(opts.View) { case "cached", "cache", "": - dumpView = "cached" - case "nb", "northbound": - dumpView = "NB" - case "sb", "southbound": - dumpView = "SB" + opts.View = "cached" + case "nb", "north", "northbound": + opts.View = "NB" + case "sb", "south", "southbound": + opts.View = "SB" default: return fmt.Errorf("invalid view type: %q", opts.View) } + // origin + switch strings.ToLower(opts.Origin) { + case "": + case "unknown": + opts.Origin = api.UnknownOrigin.String() + case "from-nb", "nb", "north", "northbound": + opts.Origin = api.FromNB.String() + case "from-sb", "sb", "south", "southbound": + opts.Origin = api.FromSB.String() + default: + return fmt.Errorf("invalid origin: %q", opts.Origin) + } + return nil +} + +func runDump(cli agentcli.Cli, opts DumpOptions) error { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() allModels, err := cli.Client().ModelList(ctx, types.ModelListOptions{ Class: "config", @@ -87,26 +129,21 @@ func runDump(cli agentcli.Cli, opts DumpOptions) error { if err != nil { return err } - - refs := opts.Models - if opts.Models[0] == "all" { - refs = []string{"*"} - } - var keyPrefixes []string - for _, m := range filterModelsByRefs(allModels, refs) { + for _, m := range filterModelsByRefs(allModels, opts.Models) { keyPrefixes = append(keyPrefixes, m.KeyPrefix) } if len(keyPrefixes) == 0 { - return fmt.Errorf("no models found for %q", opts.Models) + return fmt.Errorf("no matching models found for %q", opts.Models) } - - var errs Errors - var dumps []api.KVWithMetadata + var ( + errs Errors + dumps []api.KVWithMetadata + ) for _, keyPrefix := range keyPrefixes { dump, err := cli.Client().SchedulerDump(ctx, types.SchedulerDumpOptions{ KeyPrefix: keyPrefix, - View: dumpView, + View: opts.View, }) if err != nil { errs = append(errs, fmt.Errorf("dump for %s failed: %v", keyPrefix, err)) @@ -115,10 +152,13 @@ func runDump(cli agentcli.Cli, opts DumpOptions) error { dumps = append(dumps, dump...) } if errs != nil { - logging.Debugf("dumped with %d errors\n%v", len(errs), errs) + logging.Debugf("dump finished with %d errors\n%v", len(errs), errs) } - sort.Sort(dumpByKey(dumps)) + dumps = filterDumpByOrigin(dumps, opts.Origin) + sort.Slice(dumps, func(i, j int) bool { + return dumps[i].Key < dumps[j].Key + }) format := opts.Format if len(format) == 0 { @@ -128,54 +168,59 @@ func runDump(cli agentcli.Cli, opts DumpOptions) error { return err } } - return nil } -// printDumpTable prints dump data using table format -// -// KEY VALUE ORIGIN METADATA -// config/vpp/v2/interfaces/UNTAGGED-local0 [vpp.interfaces.Interface] from-SB map[IPAddresses: SwIfIndex:0 TAPHostIfName: Vrf:0] -// name: "UNTAGGED-local0" -// type: SOFTWARE_LOOPBACK -// -// config/vpp/v2/interfaces/loop1 [vpp.interfaces.Interface] from-NB map[IPAddresses: SwIfIndex:1 TAPHostIfName: Vrf:0] -// name: "loop1" -// type: SOFTWARE_LOOPBACK -// +func filterDumpByOrigin(dumps []api.KVWithMetadata, origin string) []api.KVWithMetadata { + if origin == "" { + return dumps + } + var filtered []api.KVWithMetadata + for _, d := range dumps { + if !strings.EqualFold(d.Origin.String(), origin) { + continue + } + filtered = append(filtered, d) + } + return filtered +} + func printDumpTable(out io.Writer, dump []api.KVWithMetadata) { - var buf bytes.Buffer - w := tabwriter.NewWriter(&buf, 0, 0, 3, ' ', 0) - fmt.Fprintf(w, "KEY\tVALUE\tORIGIN\tMETADATA\t\n") + table := tablewriter.NewWriter(out) + table.SetHeader([]string{ + "Model", "Origin", "Value", "Metadata", "Key", + }) + table.SetAutoMergeCells(true) + table.SetRowLine(true) for _, d := range dump { val := proto.MarshalTextString(d.Value) - val = strings.ReplaceAll(val, "\n", "\t\t\t\n\t") var meta string if d.Metadata != nil { - meta = fmt.Sprintf("%+v", d.Metadata) + meta = yamlTmpl(d.Metadata) } - - fmt.Fprintf(w, "%s\t[%s]\t%s\t%s\t\n", - d.Key, proto.MessageName(d.Value), d.Origin, meta) - fmt.Fprintf(w, "\t%s\t\t\n", val) - } - if err := w.Flush(); err != nil { - panic(err) + var ( + name = "-" + model string + orig = d.Origin + ) + if m, err := models.GetModelForKey(d.Key); err == nil { + name, _ = m.ParseKey(d.Key) + model = m.Name() + if name == "" { + name = d.Key + } + } + val = fmt.Sprintf("[%s]\n%s", proto.MessageName(d.Value), val) + var row []string + row = []string{ + model, + orig.String(), + val, + meta, + name, + } + table.Append(row) } - fmt.Fprint(out, buf.String()) -} - -type dumpByKey []api.KVWithMetadata - -func (s dumpByKey) Len() int { - return len(s) -} - -func (s dumpByKey) Less(i, j int) bool { - return s[i].Key < s[j].Key -} - -func (s dumpByKey) Swap(i, j int) { - s[i], s[j] = s[j], s[i] + table.Render() } diff --git a/cmd/agentctl/commands/errors.go b/cmd/agentctl/commands/errors.go index 4308997a69..187f55f479 100644 --- a/cmd/agentctl/commands/errors.go +++ b/cmd/agentctl/commands/errors.go @@ -25,10 +25,9 @@ import ( type Errors []error func (errList Errors) Error() string { - if len(errList) < 1 { + if len(errList) == 0 { return "" } - out := make([]string, len(errList)) for i := range errList { out[i] = errList[i].Error() diff --git a/cmd/agentctl/commands/formatter.go b/cmd/agentctl/commands/formatter.go index cd59f3f504..fa9fc6edc8 100644 --- a/cmd/agentctl/commands/formatter.go +++ b/cmd/agentctl/commands/formatter.go @@ -23,8 +23,10 @@ import ( "text/template" "time" - "github.com/ghodss/yaml" - "github.com/golang/protobuf/proto" + "github.com/goccy/go-yaml" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/encoding/prototext" + "google.golang.org/protobuf/proto" ) var tmplFuncs = template.FuncMap{ @@ -36,11 +38,7 @@ var tmplFuncs = template.FuncMap{ } func formatAsTemplate(w io.Writer, format string, data interface{}) error { - t := template.New("format") - t.Funcs(tmplFuncs) - var b bytes.Buffer - switch strings.ToLower(format) { case "json": b.WriteString(jsonTmpl(data)) @@ -49,6 +47,8 @@ func formatAsTemplate(w io.Writer, format string, data interface{}) error { case "proto": b.WriteString(protoTmpl(data)) default: + t := template.New("format") + t.Funcs(tmplFuncs) if _, err := t.Parse(format); err != nil { return fmt.Errorf("parsing format template failed: %v", err) } @@ -56,32 +56,51 @@ func formatAsTemplate(w io.Writer, format string, data interface{}) error { return fmt.Errorf("executing format template failed: %v", err) } } - _, err := b.WriteTo(w) return err } +func jsonTmpl(data interface{}) string { + b := encodeJson(data, " ") + return string(b) +} + func yamlTmpl(data interface{}) string { - var b bytes.Buffer - encoder := json.NewEncoder(&b) - if err := encoder.Encode(data); err != nil { - panic(err) - } - bb, err := yaml.JSONToYAML(b.Bytes()) + out := encodeJson(data, "") + bb, err := jsonToYaml(out) if err != nil { panic(err) } return string(bb) } -func jsonTmpl(data interface{}) string { +func encodeJson(data interface{}, ident string) []byte { + if msg, ok := data.(proto.Message); ok { + m := protojson.MarshalOptions{ + Indent: ident, + } + b, err := m.Marshal(msg) + if err != nil { + panic(err) + } + return b + } var b bytes.Buffer encoder := json.NewEncoder(&b) - encoder.SetIndent("", " ") + encoder.SetIndent("", ident) if err := encoder.Encode(data); err != nil { panic(err) } - return b.String() + return b.Bytes() +} + +func jsonToYaml(j []byte) ([]byte, error) { + var jsonObj interface{} + err := yaml.UnmarshalWithOptions(j, &jsonObj, yaml.UseOrderedMap()) + if err != nil { + return nil, err + } + return yaml.Marshal(jsonObj) } func protoTmpl(data interface{}) string { @@ -89,12 +108,11 @@ func protoTmpl(data interface{}) string { if !ok { panic(fmt.Sprintf("%T is not a proto message", data)) } - var b bytes.Buffer - m := proto.TextMarshaler{} - if err := m.Marshal(&b, pb); err != nil { + out, err := prototext.Marshal(pb) + if err != nil { panic(err) } - return b.String() + return string(out) } func epochTmpl(s int64) time.Time { diff --git a/cmd/agentctl/commands/import.go b/cmd/agentctl/commands/import.go index 3ec0225e3f..29378f6f32 100644 --- a/cmd/agentctl/commands/import.go +++ b/cmd/agentctl/commands/import.go @@ -35,56 +35,59 @@ import ( "go.ligato.io/vpp-agent/v3/pkg/models" ) +const ( + defaultTimeout = time.Second * 30 + defaultTxOps = 128 +) + func NewImportCommand(cli agentcli.Cli) *cobra.Command { var ( opts ImportOptions timeout uint ) cmd := &cobra.Command{ - Use: "import file", + Use: "import FILE", Args: cobra.ExactArgs(1), Short: "Import config data from file", - Example: ` - To import file contents into Etcd, run: - $ cat input.txt - config/vpp/v2/interfaces/loop1 {"name":"loop1","type":"SOFTWARE_LOOPBACK"} - config/vpp/l2/v2/bridge-domain/bd1 {"name":"bd1"} - - $ {{.CommandPath}} input.txt - - To import it via gRPC, include --grpc flag: - $ {{.CommandPath}} --grpc=localhost:9111 input.txt + Long: `Import config data from file into Etcd or via gRPC. +FILE FORMAT + Contents of the import file must contain single key-value pair per line: - FILE FORMAT - Contents of the import file must contain single key-value pair per line: - - + ... - Empty lines and lines starting with '#' are ignored. + NOTE: Empty lines and lines starting with '#' are ignored. + + Sample file: + config/vpp/v2/interfaces/loop1 {"name":"loop1","type":"SOFTWARE_LOOPBACK"} + config/vpp/l2/v2/bridge-domain/bd1 {"name":"bd1"} - KEY FORMAT +KEY FORMAT Keys can be defined in two ways: - - full: /vnf-agent/vpp1/config/vpp/v2/interfaces/iface1 - - short: config/vpp/v2/interfaces/iface1 + - Full - /vnf-agent/vpp1/config/vpp/v2/interfaces/iface1 + - Short - config/vpp/v2/interfaces/iface1 - For short keys, the import command uses microservice label defined with --service-label.`, - + When using short keys, import will use configured microservice label (e.g. --service-label flag).`, + Example: ` + Import data into Etcd: + {{.CommandPath}} input.txt + + Import data directly into agent via gRPC:: + {{.CommandPath}} --grpc input.txt +`, RunE: func(cmd *cobra.Command, args []string) error { opts.InputFile = args[0] - opts.Timeout = time.Second * time.Duration(timeout) + opts.Timeout = time.Duration(timeout) return RunImport(cli, opts) }, } - flags := cmd.Flags() - flags.UintVar(&opts.TxOps, "txops", 128, "Number of ops per transaction") - flags.UintVarP(&timeout, "time", "t", 30, "Timeout (in seconds) to wait for server response") - flags.BoolVar(&opts.ViaGrpc, "grpc", false, "Enable to import config via gRPC") - + flags.UintVar(&opts.TxOps, "txops", defaultTxOps, "Number of ops per transaction") + flags.DurationVarP(&opts.Timeout, "time", "t", defaultTimeout, "Timeout to wait for server response") + flags.BoolVar(&opts.ViaGrpc, "grpc", false, "Import config directly to agent via gRPC") return cmd } @@ -100,22 +103,19 @@ func RunImport(cli agentcli.Cli, opts ImportOptions) error { if err != nil { return fmt.Errorf("parsing import data failed: %v", err) } + fmt.Printf("importing %d key-value pairs\n", len(keyVals)) if opts.ViaGrpc { // Set up a connection to the server. - c, err := cli.Client().ConfigClient() + c, err := cli.Client().GenericClient() if err != nil { return err } - - fmt.Printf("importing %d key vals\n", len(keyVals)) - req := c.ChangeRequest() for _, keyVal := range keyVals { fmt.Printf(" - %s\n", keyVal.Key) req.Update(keyVal.Val) } - fmt.Printf("sending via gRPC\n") ctx, cancel := context.WithTimeout(context.Background(), opts.Timeout) @@ -124,16 +124,12 @@ func RunImport(cli agentcli.Cli, opts ImportOptions) error { if err := req.Send(ctx); err != nil { return fmt.Errorf("send failed: %v", err) } - } else { c, err := cli.Client().KVDBClient() if err != nil { return fmt.Errorf("KVDB error: %v", err) } db := c.ProtoBroker() - - fmt.Printf("importing %d key vals\n", len(keyVals)) - var txn = db.NewTxn() ops := 0 for i := 0; i < len(keyVals); i++ { @@ -142,21 +138,18 @@ func RunImport(cli agentcli.Cli, opts ImportOptions) error { if err != nil { return fmt.Errorf("key processing failed: %v", err) } - fmt.Printf(" - %s\n", key) txn.Put(key, keyVal.Val) ops++ if ops == int(opts.TxOps) || i+1 == len(keyVals) { fmt.Printf("commiting tx with %d ops\n", ops) - ctx, cancel := context.WithTimeout(context.Background(), opts.Timeout) err = txn.Commit(ctx) cancel() if err != nil { return fmt.Errorf("commit failed: %v", err) } - ops = 0 txn = db.NewTxn() } @@ -184,7 +177,6 @@ func parseImportFile(importFile string) (keyVals []keyVal, err error) { if bytes.HasPrefix(line, []byte("#")) { continue } - parts := bytes.SplitN(line, []byte(" "), 2) if len(parts) < 2 { continue @@ -194,16 +186,13 @@ func parseImportFile(importFile string) (keyVals []keyVal, err error) { if key == "" || data == "" { continue } - logrus.Debugf("parse line: %s %s\n", key, data) //key = completeFullKey(key) - val, err := unmarshalKeyVal(key, data) if err != nil { return nil, fmt.Errorf("decoding value failed: %v", err) } - logrus.Debugf("KEY: %s - %v\n", key, val) keyVals = append(keyVals, keyVal{key, val}) } diff --git a/cmd/agentctl/commands/model.go b/cmd/agentctl/commands/model.go index 5d0f24b9ba..d407e3f3c9 100644 --- a/cmd/agentctl/commands/model.go +++ b/cmd/agentctl/commands/model.go @@ -42,15 +42,26 @@ func NewModelCommand(cli agentcli.Cli) *cobra.Command { return cmd } +func newModelsCommand(cli agentcli.Cli) *cobra.Command { + cmd := newModelListCommand(cli) + cmd.Use = "models" + cmd.Aliases = nil + cmd.Hidden = true + return cmd +} + func newModelListCommand(cli agentcli.Cli) *cobra.Command { var opts ModelListOptions cmd := &cobra.Command{ - Use: "ls [PATTERN]", - Aliases: []string{"list", "l"}, - Short: "List models", + Use: "list PATTERN", + Aliases: []string{"ls", "l"}, + Short: "List models mathing pattern(s)", Args: cobra.ArbitraryArgs, RunE: func(cmd *cobra.Command, args []string) error { opts.Refs = args + if strings.ToLower(opts.Class) == "all" { + opts.Class = "" + } return runModelList(cli, opts) }, } @@ -70,17 +81,12 @@ func runModelList(cli agentcli.Cli, opts ModelListOptions) error { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - if strings.ToLower(opts.Class) == "all" { - opts.Class = "" - } - allModels, err := cli.Client().ModelList(ctx, types.ModelListOptions{ Class: opts.Class, }) if err != nil { return err } - models := filterModelsByRefs(allModels, opts.Refs) format := opts.Format @@ -98,7 +104,7 @@ func runModelList(cli agentcli.Cli, opts ModelListOptions) error { func printModelTable(out io.Writer, models []types.Model) { var buf bytes.Buffer w := tabwriter.NewWriter(&buf, 0, 0, 2, ' ', 0) - fmt.Fprintf(w, "MODEL\tCLASS\tPROTO MESSAGE\tKEY PREFIX\t\n") + fmt.Fprintf(w, "MODEL NAME\tCLASS\tPROTO MESSAGE\tKEY PREFIX\t\n") for _, model := range models { fmt.Fprintf(w, "%s\t%s\t%s\t%s\t\n", model.Name, model.Class, model.ProtoName, model.KeyPrefix) @@ -188,19 +194,16 @@ func runModelInspect(cli agentcli.Cli, opts ModelInspectOptions) error { if err != nil { return err } - models, err := filterModelsByPrefix(allModels, opts.Names) if err != nil { return err } - logrus.Debugf("models: %+v", models) format := opts.Format if len(format) == 0 { format = "json" } - if err := formatAsTemplate(cli.Out(), format, models); err != nil { return err } diff --git a/cmd/agentctl/commands/root.go b/cmd/agentctl/commands/root.go index 1f3b554445..62388dc6eb 100644 --- a/cmd/agentctl/commands/root.go +++ b/cmd/agentctl/commands/root.go @@ -22,6 +22,7 @@ import ( "os" "strings" + "github.com/common-nighthawk/go-figure" "github.com/docker/docker/pkg/term" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -61,6 +62,9 @@ func NewRootNamed(name string, agentCli *cli.AgentCli) *Root { Version: fmt.Sprintf("%s, commit %s", agent.BuildVersion, agent.CommitHash), } + asciiLogo := figure.NewFigure(name, "slant", true) + cmd.Long = asciiLogo.String() + opts, flags, helpCmd = SetupRootCommand(cmd) flags.BoolP("version", "v", false, "Print version info and quit") @@ -68,7 +72,7 @@ func NewRootNamed(name string, agentCli *cli.AgentCli) *Root { flags.StringVarP(&opts.LogLevel, "log-level", "l", "", `Set the logging level ("debug"|"info"|"warn"|"error"|"fatal")`) cmd.SetHelpCommand(helpCmd) - cmd.SetOutput(agentCli.Out()) + cmd.SetOut(agentCli.Out()) AddBaseCommands(cmd, agentCli) diff --git a/cmd/agentctl/commands/util.go b/cmd/agentctl/commands/util.go new file mode 100644 index 0000000000..f6a26ace9f --- /dev/null +++ b/cmd/agentctl/commands/util.go @@ -0,0 +1,45 @@ +// Copyright (c) 2020 Cisco and/or its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commands + +import ( + "fmt" + "time" +) + +type Colorer interface { + Code() string +} + +func escapeClr(c Colorer, s interface{}) string { + return fmt.Sprintf("\xff\x1b[%sm\xff%v\xff\x1b[0m\xff", c.Code(), s) +} + +func shortHumanDuration(d time.Duration) string { + if seconds := int(d.Seconds()); seconds < -1 { + return fmt.Sprintf("") + } else if seconds < 0 { + return fmt.Sprintf("0s") + } else if seconds < 60 { + return fmt.Sprintf("%ds", seconds) + } else if minutes := int(d.Minutes()); minutes < 60 { + return fmt.Sprintf("%dm", minutes) + } else if hours := int(d.Hours()); hours < 24 { + return fmt.Sprintf("%dh", hours) + } else if hours < 24*365 { + return fmt.Sprintf("%dd", hours/24) + } + return fmt.Sprintf("%dy", int(d.Hours()/24/365)) +} diff --git a/cmd/agentctl/agentctl.go b/cmd/agentctl/main.go similarity index 80% rename from cmd/agentctl/agentctl.go rename to cmd/agentctl/main.go index 4b05ea679b..7478b44a64 100644 --- a/cmd/agentctl/agentctl.go +++ b/cmd/agentctl/main.go @@ -24,13 +24,11 @@ import ( ) const logo = ` - ___ __ ________ __ - / | ____ ____ ____ / /_/ ____/ /_/ / - / /| |/ __ '/ _ \/ __ \/ __/ / / __/ / - / ___ / /_/ / __/ / / / /_/ /___/ /_/ / - /_/ |_\__, /\___/_/ /_/\__/\____/\__/_/ - /____/ - + __ __ __ + ___ ____ ____ ___ / /_____/ /_/ / + / _ '/ _ '/ -_) _ \/ __/ __/ __/ / + \_,_/\_, /\__/_//_/\__/\__/\__/_/ + /___/ ` func runAgentctl(cli *agentcli.AgentCli) error { @@ -47,7 +45,7 @@ func main() { cli := commands.NewAgentCli() if err := runAgentctl(cli); err != nil { - fmt.Fprintln(cli.Err(), err) + fmt.Fprintf(cli.Err(), "\nERROR: %v\n", err) os.Exit(commands.ExitCode(err)) } } diff --git a/go.mod b/go.mod index c169243c90..e1ecd5f5d3 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/Shopify/sarama v1.20.1 // indirect github.com/alicebob/miniredis v2.5.0+incompatible // indirect + github.com/common-nighthawk/go-figure v0.0.0-20200609044655-c4b36f998cf2 github.com/containerd/continuity v0.0.0-20171215195539-b2b946a77f59 // indirect github.com/coreos/bbolt v1.3.3 // indirect github.com/coreos/etcd v3.3.13+incompatible @@ -24,6 +25,7 @@ require ( github.com/ghodss/yaml v1.0.0 github.com/go-errors/errors v1.0.1 github.com/goccy/go-graphviz v0.0.6 + github.com/goccy/go-yaml v1.8.0 github.com/golang/protobuf v1.4.2 github.com/gorilla/mux v1.6.2 github.com/gorilla/websocket v1.4.1 // indirect @@ -35,6 +37,7 @@ require ( github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936 github.com/mitchellh/mapstructure v1.1.2 github.com/namsral/flag v1.7.4-pre + github.com/olekukonko/tablewriter v0.0.4 github.com/onsi/gomega v1.4.3 github.com/opencontainers/go-digest v1.0.0-rc1 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect @@ -52,8 +55,8 @@ require ( github.com/yuin/gopher-lua v0.0.0-20190514113301-1cd887cd7036 // indirect go.ligato.io/cn-infra/v2 v2.5.0-alpha.0.20200313154441-b0d4c1b11c73 go.uber.org/multierr v1.2.0 // indirect - golang.org/x/net v0.0.0-20200226121028-0de0cce0169b - golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3 + golang.org/x/net v0.0.0-20200528225125-3c3fba18258b + golang.org/x/sys v0.0.0-20200523222454-059865788121 golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0 // indirect google.golang.org/genproto v0.0.0-20200601130524-0f60399e6634 // indirect google.golang.org/grpc v1.29.1 diff --git a/go.sum b/go.sum index 972ee27db1..68b86cc666 100644 --- a/go.sum +++ b/go.sum @@ -54,6 +54,8 @@ github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6D github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/common-nighthawk/go-figure v0.0.0-20200609044655-c4b36f998cf2 h1:tjT4Jp4gxECvsJcYpAMtW2I3YqzBTPuB67OejxXs86s= +github.com/common-nighthawk/go-figure v0.0.0-20200609044655-c4b36f998cf2/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w= github.com/containerd/continuity v0.0.0-20171215195539-b2b946a77f59 h1:PP8ffsrKAQ6u4Yq63J+g5HJTeka1tgsmaGMZuHzXm1g= github.com/containerd/continuity v0.0.0-20171215195539-b2b946a77f59/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/coreos/bbolt v1.3.1-etcd.8/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= @@ -109,6 +111,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evalphobia/logrus_fluent v0.4.0 h1:uYIgSLezcopy6V7Epr5yIvsnzGqH+bE/62b32xIEe+A= github.com/evalphobia/logrus_fluent v0.4.0/go.mod h1:hasyj+CXm3BDP1YhFk/rnTcjlegyqvkokV9A25cQsaA= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fluent/fluent-logger-golang v1.3.0 h1:oBolFKS9fY9HReChzaX1RQF5GkdNdByrledPTfUWoGA= github.com/fluent/fluent-logger-golang v1.3.0/go.mod h1:2/HCT/jTy78yGyeNGQLGQsjF3zzzAuy6Xlk6FCMV5eU= github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= @@ -131,11 +135,17 @@ github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-redis/redis v6.14.2+incompatible h1:UE9pLhzmWf+xHNmZsoccjXosPicuiNaInPgym8nzfg0= github.com/go-redis/redis v6.14.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/goccy/go-graphviz v0.0.6 h1:sCT69fmH2KKsObVfsozYyKXxrqmIfo3SyHZs72xkgxs= github.com/goccy/go-graphviz v0.0.6/go.mod h1:wXVsXxmyMQU6TN3zGRttjNn3h+iCAS7xQFC6TlNvLhk= +github.com/goccy/go-yaml v1.8.0 h1:WCe9sBiI0oZb6EC6f3kq3dv0+aEiNdstT7b4xxq4MJQ= +github.com/goccy/go-yaml v1.8.0/go.mod h1:wS4gNoLalDSJxo/SpngzPQ2BN4uuZVLCmbM4S3vd4+Y= github.com/gocql/gocql v0.0.0-20181030013202-a84ce58083d3/go.mod h1:4Fw1eo5iaEhDUs8XyuhSVCVy52Jq3L+/3GJgYkwc+/0= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= @@ -250,12 +260,21 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/lunixbochs/struc v0.0.0-20190916212049-a5c72983bc42 h1:PzBD7QuxXSgSu61TKXxRwVGzWO5d9QZ0HxFFpndZMCg= github.com/lunixbochs/struc v0.0.0-20190916212049-a5c72983bc42/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/maraino/go-mock v0.0.0-20180321183845-4c74c434cd3a/go.mod h1:KpdDhCgE2rvPhsnLbGZ8Uf1QORj6v92FOgFKnCz5CXM= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14 h1:9jZdLNd/P4+SfEJ0TNyxYpsK8N4GtfylBLqtbYN1sbA= @@ -281,6 +300,8 @@ github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5 h1:BvoENQQU+fZ9uukda/R github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/nishanths/predeclared v0.0.0-20200524104333-86fad755b4d3/go.mod h1:nt3d53pc1VYcphSCIaYAJtnPYnr3Zyn8fMq2wvPGPso= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= +github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -370,6 +391,8 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/tinylib/msgp v1.0.2 h1:DfdQrzQa7Yh2es9SuLkixqxuXS2SxsdYn0KbdrOGWD8= github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= @@ -441,8 +464,9 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200528225125-3c3fba18258b h1:IYiJPiJfzktmDAO1HQiwjMjwjlYKHAL7KzeD544RJPs= +golang.org/x/net v0.0.0-20200528225125-3c3fba18258b/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -467,19 +491,26 @@ golang.org/x/sys v0.0.0-20190204203706-41f3e6584952 h1:FDfvYgoVsA7TTZSbgiqjAbfPb golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3 h1:5B6i6EAiSYyejWfvc5Rc9BbI3rzIsrrXfAQBWnYfn+w= -golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121 h1:rITEj+UZHYC927n8GT97eC3zrpzXdb/voyeOuVKS46o= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0 h1:xQwXv67TxFo9nC1GJFyab5eq/5B590r6RlnL/G8Sz7w= golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -533,6 +564,10 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v9 v9.30.0 h1:Wk0Z37oBmKj9/n+tPyBHZmeL19LaCoK3Qq48VwYENss= +gopkg.in/go-playground/validator.v9 v9.30.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= diff --git a/pkg/version/version.go b/pkg/version/version.go index 633f71687b..4d322de4a5 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -17,6 +17,7 @@ package version import ( "fmt" + "os" "runtime" "strconv" "time" @@ -36,6 +37,9 @@ var buildTime time.Time var revision string func init() { + if buildDate == "" { + buildDate = getBuildDate() + } if buildDate != "" { buildstampInt64, _ := strconv.ParseInt(buildDate, 10, 64) buildTime = time.Unix(buildstampInt64, 0) @@ -49,6 +53,18 @@ func init() { } } +func getBuildDate() string { + bin, err := os.Executable() + if err != nil { + return "" + } + info, err := os.Stat(bin) + if err != nil { + return "" + } + return fmt.Sprint(info.ModTime().Unix()) +} + // App returns app name. func App() string { return app diff --git a/plugins/kvscheduler/api/txn_record.go b/plugins/kvscheduler/api/txn_record.go index 78bab247f3..2217737c16 100644 --- a/plugins/kvscheduler/api/txn_record.go +++ b/plugins/kvscheduler/api/txn_record.go @@ -85,7 +85,7 @@ func (t *TxnType) UnmarshalJSON(b []byte) error { return nil } -func txnTypeToString(t TxnType) string { +func TxnTypeToString(t TxnType) string { switch t { case NBTransaction: return "NB Transaction" @@ -97,7 +97,7 @@ func txnTypeToString(t TxnType) string { return t.String() } -func resyncTypeToString(t ResyncType) string { +func ResyncTypeToString(t ResyncType) string { switch t { case NotResync: return "Not Resync" @@ -141,13 +141,15 @@ type RecordedTxnOp struct { Key string // changes - NewState kvscheduler.ValueState `json:",omitempty"` - NewValue *utils.RecordedProtoMessage `json:",omitempty"` - NewErr error `json:",omitempty"` - PrevState kvscheduler.ValueState `json:",omitempty"` - PrevValue *utils.RecordedProtoMessage `json:",omitempty"` - PrevErr error `json:",omitempty"` - NOOP bool `json:",omitempty"` + NewState kvscheduler.ValueState `json:",omitempty"` + NewValue *utils.RecordedProtoMessage `json:",omitempty"` + NewErr error `json:"-"` + NewErrMsg string `json:",omitempty"` + PrevState kvscheduler.ValueState `json:",omitempty"` + PrevValue *utils.RecordedProtoMessage `json:",omitempty"` + PrevErr error `json:"-"` + PrevErrMsg string `json:",omitempty"` + NOOP bool `json:",omitempty"` // flags IsDerived bool `json:",omitempty"` @@ -187,13 +189,13 @@ func (txn *RecordedTxn) StringWithOpts(resultOnly, verbose bool, indent int) str str += indent1 + "* transaction arguments:\n" str += indent2 + fmt.Sprintf("- seqNum: %d\n", txn.SeqNum) if txn.TxnType == NBTransaction && txn.ResyncType != NotResync { - str += indent2 + fmt.Sprintf("- type: %s, %s\n", txnTypeToString(txn.TxnType), resyncTypeToString(txn.ResyncType)) + str += indent2 + fmt.Sprintf("- type: %s, %s\n", TxnTypeToString(txn.TxnType), ResyncTypeToString(txn.ResyncType)) } else { if txn.TxnType == RetryFailedOps { str += indent2 + fmt.Sprintf("- type: %s (for txn %d, attempt #%d)\n", - txnTypeToString(txn.TxnType), txn.RetryForTxn, txn.RetryAttempt) + TxnTypeToString(txn.TxnType), txn.RetryForTxn, txn.RetryAttempt) } else { - str += indent2 + fmt.Sprintf("- type: %s\n", txnTypeToString(txn.TxnType)) + str += indent2 + fmt.Sprintf("- type: %s\n", TxnTypeToString(txn.TxnType)) } } if txn.Description != "" { diff --git a/plugins/kvscheduler/txn_exec.go b/plugins/kvscheduler/txn_exec.go index ee77bf47f1..04111c1069 100644 --- a/plugins/kvscheduler/txn_exec.go +++ b/plugins/kvscheduler/txn_exec.go @@ -300,6 +300,7 @@ func (s *Scheduler) applyDelete(node graph.NodeRW, txnOp *kvs.RecordedTxnOp, arg } } else { txnOp.NewErr = err + txnOp.NewErrMsg = err.Error() txnOp.NewState = s.markFailedValue(node, args, err, retriableErr) if !args.applied.Has(getNodeBaseKey(node)) { // value removal not originating from this transaction @@ -409,6 +410,7 @@ func (s *Scheduler) applyCreate(node graph.NodeRW, txnOp *kvs.RecordedTxnOp, arg if err != nil { node.SetFlags(&UnavailValueFlag{}) txnOp.NewErr = err + txnOp.NewErrMsg = err.Error() txnOp.NewState = kvscheduler.ValueState_INVALID txnOp.NOOP = true s.updateNodeState(node, txnOp.NewState, args) @@ -456,6 +458,7 @@ func (s *Scheduler) applyCreate(node graph.NodeRW, txnOp *kvs.RecordedTxnOp, arg node.SetFlags(&UnavailValueFlag{}) retriableErr := handler.isRetriableFailure(err) txnOp.NewErr = err + txnOp.NewErrMsg = err.Error() txnOp.NewState = s.markFailedValue(node, args, err, retriableErr) if !args.applied.Has(getNodeBaseKey(node)) { // value not originating from this transaction @@ -525,6 +528,7 @@ func (s *Scheduler) applyUpdate(node graph.NodeRW, txnOp *kvs.RecordedTxnOp, arg node.SetValue(args.kv.value) // save the invalid value node.SetFlags(&UnavailValueFlag{}) txnOp.NewErr = err + txnOp.NewErrMsg = err.Error() txnOp.NewState = kvscheduler.ValueState_INVALID txnOp.NOOP = true s.updateNodeState(node, txnOp.NewState, args) @@ -611,6 +615,7 @@ func (s *Scheduler) applyUpdate(node graph.NodeRW, txnOp *kvs.RecordedTxnOp, arg if err != nil { retriableErr := handler.isRetriableFailure(err) txnOp.NewErr = err + txnOp.NewErrMsg = err.Error() txnOp.NewState = s.markFailedValue(node, args, err, retriableErr) executed = append(executed, txnOp) if !args.applied.Has(getNodeBaseKey(node)) { @@ -826,6 +831,9 @@ func (s *Scheduler) compressTxnOps(executed kvs.RecordedTxnOps) kvs.RecordedTxnO compressedOp = true executed[j].PrevValue = op.PrevValue executed[j].PrevErr = op.PrevErr + if op.PrevErr!=nil { + executed[j].PrevErrMsg = op.PrevErr.Error() + } executed[j].PrevState = op.PrevState } break @@ -850,6 +858,9 @@ func (s *Scheduler) compressTxnOps(executed kvs.RecordedTxnOps) kvs.RecordedTxnO compressedOp = true compressed[j].NewValue = op.NewValue compressed[j].NewErr = op.NewErr + if op.NewErr != nil { + compressed[j].NewErrMsg = op.NewErr.Error() + } compressed[j].NewState = op.NewState } break diff --git a/plugins/kvscheduler/txn_record.go b/plugins/kvscheduler/txn_record.go index 82b323c87e..33f3374c3a 100644 --- a/plugins/kvscheduler/txn_record.go +++ b/plugins/kvscheduler/txn_record.go @@ -87,12 +87,17 @@ func (s *Scheduler) preRecordTxnOp(args *applyValueArgs, node graph.Node) *kvs.R prevOrigin = args.kv.origin } _, prevErr := getNodeError(node) + var prevErrMsg string + if prevErr != nil { + prevErrMsg = prevErr.Error() + } return &kvs.RecordedTxnOp{ Key: args.kv.key, PrevValue: prevValue, NewValue: utils.RecordProtoMessage(args.kv.value), PrevState: getNodeState(node), PrevErr: prevErr, + PrevErrMsg: prevErrMsg, IsDerived: args.isDerived, IsProperty: args.isDerived && s.registry.GetDescriptorForKey(args.kv.key) == nil, IsRevert: args.kv.isRevert, diff --git a/proto/ligato/vpp/vpp_types.go b/proto/ligato/vpp/vpp_types.go index 3744565a04..fe2de042cb 100644 --- a/proto/ligato/vpp/vpp_types.go +++ b/proto/ligato/vpp/vpp_types.go @@ -29,7 +29,7 @@ import ( type ( // Interface Interface = vpp_interfaces.Interface - Span = vpp_interfaces.Span + Span = vpp_interfaces.Span // ACL & ABF ACL = vpp_acl.ACL @@ -46,14 +46,14 @@ type ( ProxyARP = vpp_l3.ProxyARP IPScanNeigh = vpp_l3.IPScanNeighbor VRFTable = vpp_l3.VrfTable - L3XConnect = vpp_l3.L3XConnect - DHCPProxy = vpp_l3.DHCPProxy + L3XConnect = vpp_l3.L3XConnect + DHCPProxy = vpp_l3.DHCPProxy // NAT - NAT44Global = vpp_nat.Nat44Global - DNAT44 = vpp_nat.DNat44 - Nat44AddressPool = vpp_nat.Nat44AddressPool - Nat44Interface = vpp_nat.Nat44Interface + NAT44Global = vpp_nat.Nat44Global + DNAT44 = vpp_nat.DNat44 + Nat44AddressPool = vpp_nat.Nat44AddressPool + Nat44Interface = vpp_nat.Nat44Interface // IPSec IPSecSPD = vpp_ipsec.SecurityPolicyDatabase @@ -63,11 +63,11 @@ type ( // Punt PuntIPRedirect = vpp_punt.IPRedirect PuntToHost = vpp_punt.ToHost - PuntException = vpp_punt.Exception + PuntException = vpp_punt.Exception // SRv6 - SRv6Global = vpp_srv6.SRv6Global + SRv6Global = vpp_srv6.SRv6Global SRv6LocalSID = vpp_srv6.LocalSID - SRv6Policy = vpp_srv6.Policy + SRv6Policy = vpp_srv6.Policy SRv6Steering = vpp_srv6.Steering ) diff --git a/tests/e2e/100_agentctl_test.go b/tests/e2e/100_agentctl_test.go index 993499708e..8b860e2c2a 100644 --- a/tests/e2e/100_agentctl_test.go +++ b/tests/e2e/100_agentctl_test.go @@ -172,6 +172,11 @@ func TestAgentCtlCommands(t *testing.T) { cmd: "model ls", expectReStdout: `linux.interfaces.interface\s+config\s+ligato.linux.interfaces.Interface`, }, + { + name: "Test `models` action", + cmd: "models", + expectReStdout: `linux.interfaces.interface\s+config\s+ligato.linux.interfaces.Interface`, + }, { name: "Test `model inspect` action", cmd: "model inspect vpp.interfaces",