blob: 3e1beb458191c7b6da96816df581c3f5cc0850ef [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"
Scott Baker2b0ad652019-08-21 14:57:07 -070038 "github.com/opencord/voltctl/pkg/filter"
39 "github.com/opencord/voltctl/pkg/format"
40 "github.com/opencord/voltctl/pkg/order"
Zack Williamse940c7a2019-08-21 14:25:39 -070041 "google.golang.org/grpc"
David K. Bainbridge1d946442021-03-19 16:45:52 +000042 "google.golang.org/grpc/credentials"
43 yaml "gopkg.in/yaml.v2"
Zack Williamse940c7a2019-08-21 14:25:39 -070044)
45
46type OutputType uint8
47
48const (
49 OUTPUT_TABLE OutputType = iota
50 OUTPUT_JSON
51 OUTPUT_YAML
divyadesai19009132020-03-04 12:58:08 +000052
divyadesai19009132020-03-04 12:58:08 +000053 supportedKvStoreType = "etcd"
Zack Williamse940c7a2019-08-21 14:25:39 -070054)
55
Zack Williamse940c7a2019-08-21 14:25:39 -070056var (
57 ParamNames = map[string]map[string]string{
58 "v1": {
59 "ID": "voltha.ID",
60 },
61 "v2": {
62 "ID": "common.ID",
63 },
kesavand12cd8eb2020-01-20 22:25:22 -050064 "v3": {
Dinesh Belwalkarc9aa6d82020-03-04 15:22:17 -080065 "ID": "common.ID",
66 "port": "voltha.Port",
67 "ValueSpecifier": "common.ValueSpecifier",
kesavand12cd8eb2020-01-20 22:25:22 -050068 },
Zack Williamse940c7a2019-08-21 14:25:39 -070069 }
70
71 CharReplacer = strings.NewReplacer("\\t", "\t", "\\n", "\n")
72
David K. Bainbridge1d946442021-03-19 16:45:52 +000073 GlobalConfig = configv2.NewDefaultConfig()
Zack Williamse940c7a2019-08-21 14:25:39 -070074
David Bainbridgea6722342019-10-24 23:55:53 +000075 GlobalCommandOptions = make(map[string]map[string]string)
76
Zack Williamse940c7a2019-08-21 14:25:39 -070077 GlobalOptions struct {
divyadesai19009132020-03-04 12:58:08 +000078 Config string `short:"c" long:"config" env:"VOLTCONFIG" value-name:"FILE" default:"" description:"Location of client config file"`
79 Server string `short:"s" long:"server" default:"" value-name:"SERVER:PORT" description:"IP/Host and port of VOLTHA"`
80 Kafka string `short:"k" long:"kafka" default:"" value-name:"SERVER:PORT" description:"IP/Host and port of Kafka"`
81 KvStore string `short:"e" long:"kvstore" env:"KVSTORE" value-name:"SERVER:PORT" description:"IP/Host and port of KV store (etcd)"`
82
David K. Bainbridge2b627612020-02-18 14:50:13 -080083 // nolint: staticcheck
serkant.uluderyad3daa902020-05-21 22:49:25 -070084 Debug bool `short:"d" long:"debug" description:"Enable debug mode"`
85 Timeout string `short:"t" long:"timeout" description:"API call timeout duration" value-name:"DURATION" default:""`
86 UseTLS bool `long:"tls" description:"Use TLS"`
87 CACert string `long:"tlscacert" value-name:"CA_CERT_FILE" description:"Trust certs signed only by this CA"`
88 Cert string `long:"tlscert" value-name:"CERT_FILE" description:"Path to TLS vertificate file"`
89 Key string `long:"tlskey" value-name:"KEY_FILE" description:"Path to TLS key file"`
90 Verify bool `long:"tlsverify" description:"Use TLS and verify the remote"`
91 K8sConfig string `short:"8" long:"k8sconfig" env:"KUBECONFIG" value-name:"FILE" default:"" description:"Location of Kubernetes config file"`
92 KvStoreTimeout string `long:"kvstoretimeout" env:"KVSTORE_TIMEOUT" value-name:"DURATION" default:"" description:"timeout for calls to KV store"`
93 CommandOptions string `short:"o" long:"command-options" env:"VOLTCTL_COMMAND_OPTIONS" value-name:"FILE" default:"" description:"Location of command options default configuration file"`
94 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 -070095 }
David Bainbridgea6722342019-10-24 23:55:53 +000096
97 Debug = log.New(os.Stdout, "DEBUG: ", 0)
98 Info = log.New(os.Stdout, "INFO: ", 0)
99 Warn = log.New(os.Stderr, "WARN: ", 0)
100 Error = log.New(os.Stderr, "ERROR: ", 0)
Zack Williamse940c7a2019-08-21 14:25:39 -0700101)
102
103type OutputOptions struct {
David K. Bainbridge2b627612020-02-18 14:50:13 -0800104 Format string `long:"format" value-name:"FORMAT" default:"" description:"Format to use to output structured data"`
105 Quiet bool `short:"q" long:"quiet" description:"Output only the IDs of the objects"`
106 // nolint: staticcheck
Zack Williamse940c7a2019-08-21 14:25:39 -0700107 OutputAs string `short:"o" long:"outputas" default:"table" choice:"table" choice:"json" choice:"yaml" description:"Type of output to generate"`
108 NameLimit int `short:"l" long:"namelimit" default:"-1" value-name:"LIMIT" description:"Limit the depth (length) in the table column name"`
109}
110
Maninder045921e2020-09-29 16:46:02 +0530111type FlowIdOptions struct {
112 HexId bool `short:"x" long:"hex-id" description:"Output Ids in hex format"`
113}
114
Zack Williamse940c7a2019-08-21 14:25:39 -0700115type ListOutputOptions struct {
116 OutputOptions
117 Filter string `short:"f" long:"filter" default:"" value-name:"FILTER" description:"Only display results that match filter"`
118 OrderBy string `short:"r" long:"orderby" default:"" value-name:"ORDER" description:"Specify the sort order of the results"`
119}
120
121type OutputOptionsJson struct {
David K. Bainbridge2b627612020-02-18 14:50:13 -0800122 Format string `long:"format" value-name:"FORMAT" default:"" description:"Format to use to output structured data"`
123 Quiet bool `short:"q" long:"quiet" description:"Output only the IDs of the objects"`
124 // nolint: staticcheck
Zack Williamse940c7a2019-08-21 14:25:39 -0700125 OutputAs string `short:"o" long:"outputas" default:"json" choice:"table" choice:"json" choice:"yaml" description:"Type of output to generate"`
126 NameLimit int `short:"l" long:"namelimit" default:"-1" value-name:"LIMIT" description:"Limit the depth (length) in the table column name"`
127}
128
129type ListOutputOptionsJson struct {
130 OutputOptionsJson
131 Filter string `short:"f" long:"filter" default:"" value-name:"FILTER" description:"Only display results that match filter"`
132 OrderBy string `short:"r" long:"orderby" default:"" value-name:"ORDER" description:"Specify the sort order of the results"`
133}
134
135func toOutputType(in string) OutputType {
136 switch in {
137 case "table":
138 fallthrough
139 default:
140 return OUTPUT_TABLE
141 case "json":
142 return OUTPUT_JSON
143 case "yaml":
144 return OUTPUT_YAML
145 }
146}
147
148type CommandResult struct {
149 Format format.Format
150 Filter string
151 OrderBy string
152 OutputAs OutputType
153 NameLimit int
154 Data interface{}
155}
156
David Bainbridgea6722342019-10-24 23:55:53 +0000157func GetCommandOptionWithDefault(name, option, defaultValue string) string {
158 if cmd, ok := GlobalCommandOptions[name]; ok {
159 if val, ok := cmd[option]; ok {
160 return CharReplacer.Replace(val)
161 }
162 }
163 return defaultValue
164}
165
serkant.uluderyad3daa902020-05-21 22:49:25 -0700166var sizeParser = regexp.MustCompile(`^([0-9]+)([PETGMK]?)I?B?$`)
167
168func parseSize(size string) (uint64, error) {
169
170 parts := sizeParser.FindAllStringSubmatch(strings.ToUpper(size), -1)
171 if len(parts) == 0 {
172 return 0, fmt.Errorf("size: invalid size '%s'", size)
173 }
174 value, err := strconv.ParseUint(parts[0][1], 10, 64)
175 if err != nil {
176 return 0, fmt.Errorf("size: invalid size '%s'", size)
177 }
178 switch parts[0][2] {
179 case "E":
180 value = value * 1024 * 1024 * 1024 * 1024 * 1024 * 1024
181 case "P":
182 value = value * 1024 * 1024 * 1024 * 1024 * 1024
183 case "T":
184 value = value * 1024 * 1024 * 1024 * 1024
185 case "G":
186 value = value * 1024 * 1024 * 1024
187 case "M":
188 value = value * 1024 * 1024
189 case "K":
190 value = value * 1024
191 default:
192 }
193 return value, nil
194}
195
Zack Williamse940c7a2019-08-21 14:25:39 -0700196func ProcessGlobalOptions() {
197 if len(GlobalOptions.Config) == 0 {
198 home, err := os.UserHomeDir()
199 if err != nil {
David Bainbridgea6722342019-10-24 23:55:53 +0000200 Warn.Printf("Unable to discover the user's home directory: %s", err)
Zack Williamse940c7a2019-08-21 14:25:39 -0700201 home = "~"
202 }
203 GlobalOptions.Config = filepath.Join(home, ".volt", "config")
204 }
205
David Bainbridgea6722342019-10-24 23:55:53 +0000206 if info, err := os.Stat(GlobalOptions.Config); err == nil && !info.IsDir() {
Zack Williamse940c7a2019-08-21 14:25:39 -0700207 configFile, err := ioutil.ReadFile(GlobalOptions.Config)
208 if err != nil {
David Bainbridgea6722342019-10-24 23:55:53 +0000209 Error.Fatalf("Unable to read the configuration file '%s': %s",
210 GlobalOptions.Config, err.Error())
Zack Williamse940c7a2019-08-21 14:25:39 -0700211 }
David K. Bainbridge1d946442021-03-19 16:45:52 +0000212 // First try the latest version of the config api then work
213 // backwards
David Bainbridgea6722342019-10-24 23:55:53 +0000214 if err = yaml.Unmarshal(configFile, &GlobalConfig); err != nil {
David K. Bainbridge1d946442021-03-19 16:45:52 +0000215 GlobalConfigV1 := configv1.NewDefaultConfig()
216 if err = yaml.Unmarshal(configFile, &GlobalConfigV1); err != nil {
217 Error.Fatalf("Unable to parse the configuration file '%s': %s",
218 GlobalOptions.Config, err.Error())
219 }
220 GlobalConfig = configv2.FromConfigV1(GlobalConfigV1)
Zack Williamse940c7a2019-08-21 14:25:39 -0700221 }
222 }
223
224 // Override from command line
225 if GlobalOptions.Server != "" {
226 GlobalConfig.Server = GlobalOptions.Server
227 }
David K. Bainbridge1d946442021-03-19 16:45:52 +0000228
229 if GlobalOptions.UseTLS {
230 GlobalConfig.Tls.UseTls = true
divyadesai19009132020-03-04 12:58:08 +0000231 }
David K. Bainbridge1d946442021-03-19 16:45:52 +0000232
233 if GlobalOptions.Verify {
234 GlobalConfig.Tls.Verify = true
235 }
divyadesai19009132020-03-04 12:58:08 +0000236
Scott Bakered4efab2020-01-13 19:12:25 -0800237 if GlobalOptions.Kafka != "" {
238 GlobalConfig.Kafka = GlobalOptions.Kafka
239 }
divyadesai19009132020-03-04 12:58:08 +0000240
241 if GlobalOptions.KvStore != "" {
242 GlobalConfig.KvStore = GlobalOptions.KvStore
243 }
divyadesai19009132020-03-04 12:58:08 +0000244
divyadesai19009132020-03-04 12:58:08 +0000245 if GlobalOptions.KvStoreTimeout != "" {
246 timeout, err := time.ParseDuration(GlobalOptions.KvStoreTimeout)
247 if err != nil {
248 Error.Fatalf("Unable to parse specified KV strore timeout duration '%s': %s",
249 GlobalOptions.KvStoreTimeout, err.Error())
250 }
251 GlobalConfig.KvStoreConfig.Timeout = timeout
252 }
253
David K. Bainbridge402f8482020-02-26 17:14:46 -0800254 if GlobalOptions.Timeout != "" {
255 timeout, err := time.ParseDuration(GlobalOptions.Timeout)
256 if err != nil {
257 Error.Fatalf("Unable to parse specified timeout duration '%s': %s",
258 GlobalOptions.Timeout, err.Error())
259 }
260 GlobalConfig.Grpc.Timeout = timeout
261 }
262
serkant.uluderyad3daa902020-05-21 22:49:25 -0700263 if GlobalOptions.MaxCallRecvMsgSize != "" {
264 GlobalConfig.Grpc.MaxCallRecvMsgSize = GlobalOptions.MaxCallRecvMsgSize
265 }
266
Zack Williamse940c7a2019-08-21 14:25:39 -0700267 // If a k8s cert/key were not specified, then attempt to read it from
268 // any $HOME/.kube/config if it exists
269 if len(GlobalOptions.K8sConfig) == 0 {
270 home, err := os.UserHomeDir()
271 if err != nil {
David Bainbridgea6722342019-10-24 23:55:53 +0000272 Warn.Printf("Unable to discover the user's home directory: %s", err)
Zack Williamse940c7a2019-08-21 14:25:39 -0700273 home = "~"
274 }
275 GlobalOptions.K8sConfig = filepath.Join(home, ".kube", "config")
276 }
David Bainbridgea6722342019-10-24 23:55:53 +0000277
278 if len(GlobalOptions.CommandOptions) == 0 {
279 home, err := os.UserHomeDir()
280 if err != nil {
281 Warn.Printf("Unable to discover the user's home directory: %s", err)
282 home = "~"
283 }
284 GlobalOptions.CommandOptions = filepath.Join(home, ".volt", "command_options")
285 }
286
287 if info, err := os.Stat(GlobalOptions.CommandOptions); err == nil && !info.IsDir() {
288 optionsFile, err := ioutil.ReadFile(GlobalOptions.CommandOptions)
289 if err != nil {
290 Error.Fatalf("Unable to read command options configuration file '%s' : %s",
291 GlobalOptions.CommandOptions, err.Error())
292 }
293 if err = yaml.Unmarshal(optionsFile, &GlobalCommandOptions); err != nil {
294 Error.Fatalf("Unable to parse the command line options configuration file '%s': %s",
295 GlobalOptions.CommandOptions, err.Error())
296 }
297 }
Zack Williamse940c7a2019-08-21 14:25:39 -0700298}
299
300func NewConnection() (*grpc.ClientConn, error) {
301 ProcessGlobalOptions()
serkant.uluderyad3daa902020-05-21 22:49:25 -0700302
303 // convert grpc.msgSize into bytes
304 n, err := parseSize(GlobalConfig.Grpc.MaxCallRecvMsgSize)
305 if err != nil {
306 Error.Fatalf("Cannot convert msgSize %s to bytes", GlobalConfig.Grpc.MaxCallRecvMsgSize)
307 }
308
David K. Bainbridge1d946442021-03-19 16:45:52 +0000309 var opts []grpc.DialOption
310
311 opts = append(opts,
312 grpc.WithDisableRetry(),
313 grpc.WithBlock(),
314 grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(int(n))))
315
316 if GlobalConfig.Tls.UseTls {
317 creds := credentials.NewTLS(&tls.Config{
318 InsecureSkipVerify: !GlobalConfig.Tls.Verify})
319 opts = append(opts, grpc.WithTransportCredentials(creds))
320 } else {
321 opts = append(opts, grpc.WithInsecure())
322 }
323 ctx, cancel := context.WithTimeout(context.TODO(),
324 GlobalConfig.Grpc.ConnectTimeout)
325 defer cancel()
326 return grpc.DialContext(ctx, GlobalConfig.Server, opts...)
Zack Williamse940c7a2019-08-21 14:25:39 -0700327}
328
Scott Baker9173ed82020-05-19 08:30:12 -0700329func ConvertJsonProtobufArray(data_in interface{}) (string, error) {
330 result := ""
331
332 slice := reflect.ValueOf(data_in)
333 if slice.Kind() != reflect.Slice {
334 return "", errors.New("Not a slice")
335 }
336
337 result = result + "["
338
339 marshaler := jsonpb.Marshaler{EmitDefaults: true}
340 for i := 0; i < slice.Len(); i++ {
341 item := slice.Index(i).Interface()
342 protoMessage, okay := item.(proto.Message)
343 if !okay {
344 return "", errors.New("Failed to convert item to a proto.Message")
345 }
346 asJson, err := marshaler.MarshalToString(protoMessage)
347 if err != nil {
348 return "", fmt.Errorf("Failed to marshal the json: %s", err)
349 }
350
351 result = result + asJson
352
353 if i < slice.Len()-1 {
354 result = result + ","
355 }
356 }
357
358 result = result + "]"
359
360 return result, nil
361}
362
Zack Williamse940c7a2019-08-21 14:25:39 -0700363func GenerateOutput(result *CommandResult) {
364 if result != nil && result.Data != nil {
365 data := result.Data
366 if result.Filter != "" {
367 f, err := filter.Parse(result.Filter)
368 if err != nil {
David Bainbridge0f758d42019-10-26 05:17:48 +0000369 Error.Fatalf("Unable to parse specified output filter '%s': %s", result.Filter, err.Error())
Zack Williamse940c7a2019-08-21 14:25:39 -0700370 }
371 data, err = f.Process(data)
372 if err != nil {
David Bainbridge0f758d42019-10-26 05:17:48 +0000373 Error.Fatalf("Unexpected error while filtering command results: %s", err.Error())
Zack Williamse940c7a2019-08-21 14:25:39 -0700374 }
375 }
376 if result.OrderBy != "" {
377 s, err := order.Parse(result.OrderBy)
378 if err != nil {
David Bainbridge0f758d42019-10-26 05:17:48 +0000379 Error.Fatalf("Unable to parse specified sort specification '%s': %s", result.OrderBy, err.Error())
Zack Williamse940c7a2019-08-21 14:25:39 -0700380 }
381 data, err = s.Process(data)
382 if err != nil {
David Bainbridge0f758d42019-10-26 05:17:48 +0000383 Error.Fatalf("Unexpected error while sorting command result: %s", err.Error())
Zack Williamse940c7a2019-08-21 14:25:39 -0700384 }
385 }
386 if result.OutputAs == OUTPUT_TABLE {
387 tableFormat := format.Format(result.Format)
David Bainbridge12f036f2019-10-15 22:09:04 +0000388 if err := tableFormat.Execute(os.Stdout, true, result.NameLimit, data); err != nil {
David Bainbridge0f758d42019-10-26 05:17:48 +0000389 Error.Fatalf("Unexpected error while attempting to format results as table : %s", err.Error())
David Bainbridge12f036f2019-10-15 22:09:04 +0000390 }
Zack Williamse940c7a2019-08-21 14:25:39 -0700391 } else if result.OutputAs == OUTPUT_JSON {
Scott Baker9173ed82020-05-19 08:30:12 -0700392 // first try to convert it as an array of protobufs
393 asJson, err := ConvertJsonProtobufArray(data)
Zack Williamse940c7a2019-08-21 14:25:39 -0700394 if err != nil {
Scott Baker9173ed82020-05-19 08:30:12 -0700395 // if that fails, then just do a standard json conversion
396 asJsonB, err := json.Marshal(&data)
397 if err != nil {
398 Error.Fatalf("Unexpected error while processing command results to JSON: %s", err.Error())
399 }
400 asJson = string(asJsonB)
Zack Williamse940c7a2019-08-21 14:25:39 -0700401 }
402 fmt.Printf("%s", asJson)
403 } else if result.OutputAs == OUTPUT_YAML {
404 asYaml, err := yaml.Marshal(&data)
405 if err != nil {
David Bainbridge0f758d42019-10-26 05:17:48 +0000406 Error.Fatalf("Unexpected error while processing command results to YAML: %s", err.Error())
Zack Williamse940c7a2019-08-21 14:25:39 -0700407 }
408 fmt.Printf("%s", asYaml)
409 }
410 }
411}