blob: 8cefc14f33e173d596b42cb925237b0963994c7a [file] [log] [blame]
Zack Williamse940c7a2019-08-21 14:25:39 -07001/*
2 * Copyright 2019-present Ciena Corporation
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package commands
17
18import (
David K. Bainbridge1d946442021-03-19 16:45:52 +000019 "context"
20 "crypto/tls"
Zack Williamse940c7a2019-08-21 14:25:39 -070021 "encoding/json"
Scott Baker9173ed82020-05-19 08:30:12 -070022 "errors"
Zack Williamse940c7a2019-08-21 14:25:39 -070023 "fmt"
serkant.uluderyad3daa902020-05-21 22:49:25 -070024 "io/ioutil"
25 "log"
serkant.uluderyad3daa902020-05-21 22:49:25 -070026 "os"
27 "path/filepath"
28 "reflect"
29 "regexp"
30 "strconv"
31 "strings"
32 "time"
33
Scott Baker9173ed82020-05-19 08:30:12 -070034 "github.com/golang/protobuf/jsonpb"
35 "github.com/golang/protobuf/proto"
David K. Bainbridge1d946442021-03-19 16:45:52 +000036 configv1 "github.com/opencord/voltctl/internal/pkg/apis/config/v1"
37 configv2 "github.com/opencord/voltctl/internal/pkg/apis/config/v2"
David K. Bainbridge9189c632021-03-26 21:52:21 +000038 configv3 "github.com/opencord/voltctl/internal/pkg/apis/config/v3"
Scott Baker2b0ad652019-08-21 14:57:07 -070039 "github.com/opencord/voltctl/pkg/filter"
40 "github.com/opencord/voltctl/pkg/format"
41 "github.com/opencord/voltctl/pkg/order"
Zack Williamse940c7a2019-08-21 14:25:39 -070042 "google.golang.org/grpc"
David K. Bainbridge1d946442021-03-19 16:45:52 +000043 "google.golang.org/grpc/credentials"
44 yaml "gopkg.in/yaml.v2"
Zack Williamse940c7a2019-08-21 14:25:39 -070045)
46
47type OutputType uint8
48
49const (
50 OUTPUT_TABLE OutputType = iota
51 OUTPUT_JSON
52 OUTPUT_YAML
divyadesai19009132020-03-04 12:58:08 +000053
divyadesai19009132020-03-04 12:58:08 +000054 supportedKvStoreType = "etcd"
Zack Williamse940c7a2019-08-21 14:25:39 -070055)
56
Zack Williamse940c7a2019-08-21 14:25:39 -070057var (
58 ParamNames = map[string]map[string]string{
59 "v1": {
60 "ID": "voltha.ID",
61 },
62 "v2": {
63 "ID": "common.ID",
64 },
kesavand12cd8eb2020-01-20 22:25:22 -050065 "v3": {
Dinesh Belwalkarc9aa6d82020-03-04 15:22:17 -080066 "ID": "common.ID",
67 "port": "voltha.Port",
68 "ValueSpecifier": "common.ValueSpecifier",
kesavand12cd8eb2020-01-20 22:25:22 -050069 },
Zack Williamse940c7a2019-08-21 14:25:39 -070070 }
71
72 CharReplacer = strings.NewReplacer("\\t", "\t", "\\n", "\n")
73
David K. Bainbridge9189c632021-03-26 21:52:21 +000074 GlobalConfig = configv3.NewDefaultConfig()
Zack Williamse940c7a2019-08-21 14:25:39 -070075
David Bainbridgea6722342019-10-24 23:55:53 +000076 GlobalCommandOptions = make(map[string]map[string]string)
77
Zack Williamse940c7a2019-08-21 14:25:39 -070078 GlobalOptions struct {
divyadesai19009132020-03-04 12:58:08 +000079 Config string `short:"c" long:"config" env:"VOLTCONFIG" value-name:"FILE" default:"" description:"Location of client config file"`
David K. Bainbridge9189c632021-03-26 21:52:21 +000080 Stack string `short:"v" long:"stack" env:"STACK" value-name:"STACK" default:"" description:"Name of stack to use in multistack deployment"`
divyadesai19009132020-03-04 12:58:08 +000081 Server string `short:"s" long:"server" default:"" value-name:"SERVER:PORT" description:"IP/Host and port of VOLTHA"`
82 Kafka string `short:"k" long:"kafka" default:"" value-name:"SERVER:PORT" description:"IP/Host and port of Kafka"`
83 KvStore string `short:"e" long:"kvstore" env:"KVSTORE" value-name:"SERVER:PORT" description:"IP/Host and port of KV store (etcd)"`
84
David K. Bainbridge2b627612020-02-18 14:50:13 -080085 // nolint: staticcheck
serkant.uluderyad3daa902020-05-21 22:49:25 -070086 Debug bool `short:"d" long:"debug" description:"Enable debug mode"`
87 Timeout string `short:"t" long:"timeout" description:"API call timeout duration" value-name:"DURATION" default:""`
88 UseTLS bool `long:"tls" description:"Use TLS"`
89 CACert string `long:"tlscacert" value-name:"CA_CERT_FILE" description:"Trust certs signed only by this CA"`
90 Cert string `long:"tlscert" value-name:"CERT_FILE" description:"Path to TLS vertificate file"`
91 Key string `long:"tlskey" value-name:"KEY_FILE" description:"Path to TLS key file"`
92 Verify bool `long:"tlsverify" description:"Use TLS and verify the remote"`
93 K8sConfig string `short:"8" long:"k8sconfig" env:"KUBECONFIG" value-name:"FILE" default:"" description:"Location of Kubernetes config file"`
94 KvStoreTimeout string `long:"kvstoretimeout" env:"KVSTORE_TIMEOUT" value-name:"DURATION" default:"" description:"timeout for calls to KV store"`
95 CommandOptions string `short:"o" long:"command-options" env:"VOLTCTL_COMMAND_OPTIONS" value-name:"FILE" default:"" description:"Location of command options default configuration file"`
96 MaxCallRecvMsgSize string `short:"m" long:"maxcallrecvmsgsize" description:"Max GRPC Client request size limit in bytes (eg: 4MB)" value-name:"SIZE" default:"4M"`
Zack Williamse940c7a2019-08-21 14:25:39 -070097 }
David Bainbridgea6722342019-10-24 23:55:53 +000098
99 Debug = log.New(os.Stdout, "DEBUG: ", 0)
100 Info = log.New(os.Stdout, "INFO: ", 0)
101 Warn = log.New(os.Stderr, "WARN: ", 0)
102 Error = log.New(os.Stderr, "ERROR: ", 0)
Zack Williamse940c7a2019-08-21 14:25:39 -0700103)
104
105type OutputOptions struct {
David K. Bainbridge2b627612020-02-18 14:50:13 -0800106 Format string `long:"format" value-name:"FORMAT" default:"" description:"Format to use to output structured data"`
107 Quiet bool `short:"q" long:"quiet" description:"Output only the IDs of the objects"`
108 // nolint: staticcheck
Zack Williamse940c7a2019-08-21 14:25:39 -0700109 OutputAs string `short:"o" long:"outputas" default:"table" choice:"table" choice:"json" choice:"yaml" description:"Type of output to generate"`
110 NameLimit int `short:"l" long:"namelimit" default:"-1" value-name:"LIMIT" description:"Limit the depth (length) in the table column name"`
111}
112
Maninder045921e2020-09-29 16:46:02 +0530113type FlowIdOptions struct {
114 HexId bool `short:"x" long:"hex-id" description:"Output Ids in hex format"`
115}
116
Himani Chawla3c161c62021-05-13 16:36:51 +0530117type GroupListOptions struct {
118 Bucket bool `short:"b" long:"buckets" description:"Display Buckets"`
119}
Zack Williamse940c7a2019-08-21 14:25:39 -0700120type ListOutputOptions struct {
121 OutputOptions
122 Filter string `short:"f" long:"filter" default:"" value-name:"FILTER" description:"Only display results that match filter"`
123 OrderBy string `short:"r" long:"orderby" default:"" value-name:"ORDER" description:"Specify the sort order of the results"`
124}
125
126type OutputOptionsJson struct {
David K. Bainbridge2b627612020-02-18 14:50:13 -0800127 Format string `long:"format" value-name:"FORMAT" default:"" description:"Format to use to output structured data"`
128 Quiet bool `short:"q" long:"quiet" description:"Output only the IDs of the objects"`
129 // nolint: staticcheck
Zack Williamse940c7a2019-08-21 14:25:39 -0700130 OutputAs string `short:"o" long:"outputas" default:"json" choice:"table" choice:"json" choice:"yaml" description:"Type of output to generate"`
131 NameLimit int `short:"l" long:"namelimit" default:"-1" value-name:"LIMIT" description:"Limit the depth (length) in the table column name"`
132}
133
134type ListOutputOptionsJson struct {
135 OutputOptionsJson
136 Filter string `short:"f" long:"filter" default:"" value-name:"FILTER" description:"Only display results that match filter"`
137 OrderBy string `short:"r" long:"orderby" default:"" value-name:"ORDER" description:"Specify the sort order of the results"`
138}
139
140func toOutputType(in string) OutputType {
141 switch in {
142 case "table":
143 fallthrough
144 default:
145 return OUTPUT_TABLE
146 case "json":
147 return OUTPUT_JSON
148 case "yaml":
149 return OUTPUT_YAML
150 }
151}
152
153type CommandResult struct {
154 Format format.Format
155 Filter string
156 OrderBy string
157 OutputAs OutputType
158 NameLimit int
159 Data interface{}
160}
161
David Bainbridgea6722342019-10-24 23:55:53 +0000162func GetCommandOptionWithDefault(name, option, defaultValue string) string {
163 if cmd, ok := GlobalCommandOptions[name]; ok {
164 if val, ok := cmd[option]; ok {
165 return CharReplacer.Replace(val)
166 }
167 }
168 return defaultValue
169}
170
serkant.uluderyad3daa902020-05-21 22:49:25 -0700171var sizeParser = regexp.MustCompile(`^([0-9]+)([PETGMK]?)I?B?$`)
172
173func parseSize(size string) (uint64, error) {
174
175 parts := sizeParser.FindAllStringSubmatch(strings.ToUpper(size), -1)
176 if len(parts) == 0 {
177 return 0, fmt.Errorf("size: invalid size '%s'", size)
178 }
179 value, err := strconv.ParseUint(parts[0][1], 10, 64)
180 if err != nil {
181 return 0, fmt.Errorf("size: invalid size '%s'", size)
182 }
183 switch parts[0][2] {
184 case "E":
185 value = value * 1024 * 1024 * 1024 * 1024 * 1024 * 1024
186 case "P":
187 value = value * 1024 * 1024 * 1024 * 1024 * 1024
188 case "T":
189 value = value * 1024 * 1024 * 1024 * 1024
190 case "G":
191 value = value * 1024 * 1024 * 1024
192 case "M":
193 value = value * 1024 * 1024
194 case "K":
195 value = value * 1024
196 default:
197 }
198 return value, nil
199}
200
Zack Williamse940c7a2019-08-21 14:25:39 -0700201func ProcessGlobalOptions() {
David K. Bainbridge9189c632021-03-26 21:52:21 +0000202 ReadConfig()
203
204 // If a stack is selected via command line set it
205 if GlobalOptions.Stack != "" {
206 if GlobalConfig.StackByName(GlobalOptions.Stack) == nil {
207 Error.Fatalf("stack specified, '%s', not found in configuration",
208 GlobalOptions.Stack)
Zack Williamse940c7a2019-08-21 14:25:39 -0700209 }
David K. Bainbridge9189c632021-03-26 21:52:21 +0000210 GlobalConfig.CurrentStack = GlobalOptions.Stack
Zack Williamse940c7a2019-08-21 14:25:39 -0700211 }
212
David K. Bainbridge9189c632021-03-26 21:52:21 +0000213 ApplyOptionOverrides(GlobalConfig.Current())
serkant.uluderyad3daa902020-05-21 22:49:25 -0700214
Zack Williamse940c7a2019-08-21 14:25:39 -0700215 // If a k8s cert/key were not specified, then attempt to read it from
216 // any $HOME/.kube/config if it exists
217 if len(GlobalOptions.K8sConfig) == 0 {
218 home, err := os.UserHomeDir()
219 if err != nil {
David Bainbridgea6722342019-10-24 23:55:53 +0000220 Warn.Printf("Unable to discover the user's home directory: %s", err)
Zack Williamse940c7a2019-08-21 14:25:39 -0700221 home = "~"
222 }
223 GlobalOptions.K8sConfig = filepath.Join(home, ".kube", "config")
224 }
David Bainbridgea6722342019-10-24 23:55:53 +0000225
226 if len(GlobalOptions.CommandOptions) == 0 {
227 home, err := os.UserHomeDir()
228 if err != nil {
229 Warn.Printf("Unable to discover the user's home directory: %s", err)
230 home = "~"
231 }
232 GlobalOptions.CommandOptions = filepath.Join(home, ".volt", "command_options")
233 }
234
235 if info, err := os.Stat(GlobalOptions.CommandOptions); err == nil && !info.IsDir() {
236 optionsFile, err := ioutil.ReadFile(GlobalOptions.CommandOptions)
237 if err != nil {
238 Error.Fatalf("Unable to read command options configuration file '%s' : %s",
239 GlobalOptions.CommandOptions, err.Error())
240 }
241 if err = yaml.Unmarshal(optionsFile, &GlobalCommandOptions); err != nil {
242 Error.Fatalf("Unable to parse the command line options configuration file '%s': %s",
243 GlobalOptions.CommandOptions, err.Error())
244 }
245 }
Zack Williamse940c7a2019-08-21 14:25:39 -0700246}
247
David K. Bainbridge9189c632021-03-26 21:52:21 +0000248func ApplyOptionOverrides(stack *configv3.StackConfigSpec) {
249
250 if stack == nil {
251 // nothing to do
252 return
253 }
254 // Override from command line
255 if GlobalOptions.Server != "" {
256 stack.Server = GlobalOptions.Server
257 }
258
259 if GlobalOptions.UseTLS {
260 stack.Tls.UseTls = true
261 }
262
263 if GlobalOptions.Verify {
264 stack.Tls.Verify = true
265 }
266
267 if GlobalOptions.Kafka != "" {
268 stack.Kafka = GlobalOptions.Kafka
269 }
270
271 if GlobalOptions.KvStore != "" {
272 stack.KvStore = GlobalOptions.KvStore
273 }
274
275 if GlobalOptions.KvStoreTimeout != "" {
276 timeout, err := time.ParseDuration(GlobalOptions.KvStoreTimeout)
277 if err != nil {
278 Error.Fatalf("Unable to parse specified KV strore timeout duration '%s': %s",
279 GlobalOptions.KvStoreTimeout, err.Error())
280 }
281 stack.KvStoreConfig.Timeout = timeout
282 }
283
284 if GlobalOptions.Timeout != "" {
285 timeout, err := time.ParseDuration(GlobalOptions.Timeout)
286 if err != nil {
287 Error.Fatalf("Unable to parse specified timeout duration '%s': %s",
288 GlobalOptions.Timeout, err.Error())
289 }
290 stack.Grpc.Timeout = timeout
291 }
292
293 if GlobalOptions.MaxCallRecvMsgSize != "" {
294 stack.Grpc.MaxCallRecvMsgSize = GlobalOptions.MaxCallRecvMsgSize
295 }
296}
297
298func ReadConfig() {
299 if len(GlobalOptions.Config) == 0 {
300 home, err := os.UserHomeDir()
301 if err != nil {
302 Warn.Printf("Unable to discover the user's home directory: %s", err)
303 home = "~"
304 }
305 GlobalOptions.Config = filepath.Join(home, ".volt", "config")
306 }
307
308 if info, err := os.Stat(GlobalOptions.Config); err == nil && !info.IsDir() {
309 configFile, err := ioutil.ReadFile(GlobalOptions.Config)
310 if err != nil {
311 Error.Fatalf("Unable to read the configuration file '%s': %s",
312 GlobalOptions.Config, err.Error())
313 }
314 // First try the latest version of the config api then work
315 // backwards
316 if err = yaml.Unmarshal(configFile, &GlobalConfig); err != nil {
317 GlobalConfigV2 := configv2.NewDefaultConfig()
318 if err = yaml.Unmarshal(configFile, &GlobalConfigV2); err != nil {
319 GlobalConfigV1 := configv1.NewDefaultConfig()
320 if err = yaml.Unmarshal(configFile, &GlobalConfigV1); err != nil {
321 Error.Fatalf("Unable to parse the configuration file '%s': %s",
322 GlobalOptions.Config, err.Error())
323 }
324 GlobalConfig = configv3.FromConfigV1(GlobalConfigV1)
325 } else {
326 GlobalConfig = configv3.FromConfigV2(GlobalConfigV2)
327 }
328 }
329 }
330
331}
332
Zack Williamse940c7a2019-08-21 14:25:39 -0700333func NewConnection() (*grpc.ClientConn, error) {
334 ProcessGlobalOptions()
serkant.uluderyad3daa902020-05-21 22:49:25 -0700335
336 // convert grpc.msgSize into bytes
David K. Bainbridge9189c632021-03-26 21:52:21 +0000337 n, err := parseSize(GlobalConfig.Current().Grpc.MaxCallRecvMsgSize)
serkant.uluderyad3daa902020-05-21 22:49:25 -0700338 if err != nil {
David K. Bainbridge9189c632021-03-26 21:52:21 +0000339 Error.Fatalf("Cannot convert msgSize %s to bytes", GlobalConfig.Current().Grpc.MaxCallRecvMsgSize)
serkant.uluderyad3daa902020-05-21 22:49:25 -0700340 }
341
David K. Bainbridge1d946442021-03-19 16:45:52 +0000342 var opts []grpc.DialOption
343
344 opts = append(opts,
345 grpc.WithDisableRetry(),
David K. Bainbridge1d946442021-03-19 16:45:52 +0000346 grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(int(n))))
347
David K. Bainbridge9189c632021-03-26 21:52:21 +0000348 if GlobalConfig.Current().Tls.UseTls {
David K. Bainbridge1d946442021-03-19 16:45:52 +0000349 creds := credentials.NewTLS(&tls.Config{
David K. Bainbridge9189c632021-03-26 21:52:21 +0000350 InsecureSkipVerify: !GlobalConfig.Current().Tls.Verify})
David K. Bainbridge1d946442021-03-19 16:45:52 +0000351 opts = append(opts, grpc.WithTransportCredentials(creds))
352 } else {
353 opts = append(opts, grpc.WithInsecure())
354 }
355 ctx, cancel := context.WithTimeout(context.TODO(),
David K. Bainbridge9189c632021-03-26 21:52:21 +0000356 GlobalConfig.Current().Grpc.ConnectTimeout)
David K. Bainbridge1d946442021-03-19 16:45:52 +0000357 defer cancel()
David K. Bainbridge9189c632021-03-26 21:52:21 +0000358 return grpc.DialContext(ctx, GlobalConfig.Current().Server, opts...)
Zack Williamse940c7a2019-08-21 14:25:39 -0700359}
360
Scott Baker9173ed82020-05-19 08:30:12 -0700361func ConvertJsonProtobufArray(data_in interface{}) (string, error) {
362 result := ""
363
364 slice := reflect.ValueOf(data_in)
365 if slice.Kind() != reflect.Slice {
366 return "", errors.New("Not a slice")
367 }
368
369 result = result + "["
370
371 marshaler := jsonpb.Marshaler{EmitDefaults: true}
372 for i := 0; i < slice.Len(); i++ {
373 item := slice.Index(i).Interface()
374 protoMessage, okay := item.(proto.Message)
375 if !okay {
376 return "", errors.New("Failed to convert item to a proto.Message")
377 }
378 asJson, err := marshaler.MarshalToString(protoMessage)
379 if err != nil {
380 return "", fmt.Errorf("Failed to marshal the json: %s", err)
381 }
382
383 result = result + asJson
384
385 if i < slice.Len()-1 {
386 result = result + ","
387 }
388 }
389
390 result = result + "]"
391
392 return result, nil
393}
394
Zack Williamse940c7a2019-08-21 14:25:39 -0700395func GenerateOutput(result *CommandResult) {
396 if result != nil && result.Data != nil {
397 data := result.Data
398 if result.Filter != "" {
399 f, err := filter.Parse(result.Filter)
400 if err != nil {
David Bainbridge0f758d42019-10-26 05:17:48 +0000401 Error.Fatalf("Unable to parse specified output filter '%s': %s", result.Filter, err.Error())
Zack Williamse940c7a2019-08-21 14:25:39 -0700402 }
403 data, err = f.Process(data)
404 if err != nil {
David Bainbridge0f758d42019-10-26 05:17:48 +0000405 Error.Fatalf("Unexpected error while filtering command results: %s", err.Error())
Zack Williamse940c7a2019-08-21 14:25:39 -0700406 }
407 }
408 if result.OrderBy != "" {
409 s, err := order.Parse(result.OrderBy)
410 if err != nil {
David Bainbridge0f758d42019-10-26 05:17:48 +0000411 Error.Fatalf("Unable to parse specified sort specification '%s': %s", result.OrderBy, err.Error())
Zack Williamse940c7a2019-08-21 14:25:39 -0700412 }
413 data, err = s.Process(data)
414 if err != nil {
David Bainbridge0f758d42019-10-26 05:17:48 +0000415 Error.Fatalf("Unexpected error while sorting command result: %s", err.Error())
Zack Williamse940c7a2019-08-21 14:25:39 -0700416 }
417 }
418 if result.OutputAs == OUTPUT_TABLE {
419 tableFormat := format.Format(result.Format)
David Bainbridge12f036f2019-10-15 22:09:04 +0000420 if err := tableFormat.Execute(os.Stdout, true, result.NameLimit, data); err != nil {
David Bainbridge0f758d42019-10-26 05:17:48 +0000421 Error.Fatalf("Unexpected error while attempting to format results as table : %s", err.Error())
David Bainbridge12f036f2019-10-15 22:09:04 +0000422 }
Zack Williamse940c7a2019-08-21 14:25:39 -0700423 } else if result.OutputAs == OUTPUT_JSON {
Scott Baker9173ed82020-05-19 08:30:12 -0700424 // first try to convert it as an array of protobufs
425 asJson, err := ConvertJsonProtobufArray(data)
Zack Williamse940c7a2019-08-21 14:25:39 -0700426 if err != nil {
Scott Baker9173ed82020-05-19 08:30:12 -0700427 // if that fails, then just do a standard json conversion
428 asJsonB, err := json.Marshal(&data)
429 if err != nil {
430 Error.Fatalf("Unexpected error while processing command results to JSON: %s", err.Error())
431 }
432 asJson = string(asJsonB)
Zack Williamse940c7a2019-08-21 14:25:39 -0700433 }
434 fmt.Printf("%s", asJson)
435 } else if result.OutputAs == OUTPUT_YAML {
436 asYaml, err := yaml.Marshal(&data)
437 if err != nil {
David Bainbridge0f758d42019-10-26 05:17:48 +0000438 Error.Fatalf("Unexpected error while processing command results to YAML: %s", err.Error())
Zack Williamse940c7a2019-08-21 14:25:39 -0700439 }
440 fmt.Printf("%s", asYaml)
441 }
442 }
443}