blob: d47a092bb8e7b015742f330dc59b9fbe1805ed62 [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 (
19 "encoding/json"
Scott Baker9173ed82020-05-19 08:30:12 -070020 "errors"
Zack Williamse940c7a2019-08-21 14:25:39 -070021 "fmt"
Scott Baker9173ed82020-05-19 08:30:12 -070022 "github.com/golang/protobuf/jsonpb"
23 "github.com/golang/protobuf/proto"
Scott Baker2b0ad652019-08-21 14:57:07 -070024 "github.com/opencord/voltctl/pkg/filter"
25 "github.com/opencord/voltctl/pkg/format"
26 "github.com/opencord/voltctl/pkg/order"
Zack Williamse940c7a2019-08-21 14:25:39 -070027 "google.golang.org/grpc"
28 "gopkg.in/yaml.v2"
29 "io/ioutil"
30 "log"
divyadesai19009132020-03-04 12:58:08 +000031 "net"
Zack Williamse940c7a2019-08-21 14:25:39 -070032 "os"
33 "path/filepath"
Scott Baker9173ed82020-05-19 08:30:12 -070034 "reflect"
divyadesai19009132020-03-04 12:58:08 +000035 "strconv"
Zack Williamse940c7a2019-08-21 14:25:39 -070036 "strings"
37 "time"
38)
39
40type OutputType uint8
41
42const (
43 OUTPUT_TABLE OutputType = iota
44 OUTPUT_JSON
45 OUTPUT_YAML
divyadesai19009132020-03-04 12:58:08 +000046
47 defaultApiHost = "localhost"
48 defaultApiPort = 55555
49
50 defaultKafkaHost = "localhost"
51 defaultKafkaPort = 9092
52
53 supportedKvStoreType = "etcd"
54 defaultKvHost = "localhost"
55 defaultKvPort = 2379
56 defaultKvTimeout = time.Second * 5
57
58 defaultGrpcTimeout = time.Minute * 5
Zack Williamse940c7a2019-08-21 14:25:39 -070059)
60
61type GrpcConfigSpec struct {
62 Timeout time.Duration `yaml:"timeout"`
63}
64
divyadesai19009132020-03-04 12:58:08 +000065type KvStoreConfigSpec struct {
66 Timeout time.Duration `yaml:"timeout"`
67}
68
Zack Williamse940c7a2019-08-21 14:25:39 -070069type TlsConfigSpec struct {
70 UseTls bool `yaml:"useTls"`
71 CACert string `yaml:"caCert"`
72 Cert string `yaml:"cert"`
73 Key string `yaml:"key"`
74 Verify string `yaml:"verify"`
75}
76
77type GlobalConfigSpec struct {
divyadesai19009132020-03-04 12:58:08 +000078 ApiVersion string `yaml:"apiVersion"`
79 Server string `yaml:"server"`
80 Kafka string `yaml:"kafka"`
81 KvStore string `yaml:"kvstore"`
82 Tls TlsConfigSpec `yaml:"tls"`
83 Grpc GrpcConfigSpec `yaml:"grpc"`
84 KvStoreConfig KvStoreConfigSpec `yaml:"kvstoreconfig"`
85 K8sConfig string `yaml:"-"`
Zack Williamse940c7a2019-08-21 14:25:39 -070086}
87
88var (
89 ParamNames = map[string]map[string]string{
90 "v1": {
91 "ID": "voltha.ID",
92 },
93 "v2": {
94 "ID": "common.ID",
95 },
kesavand12cd8eb2020-01-20 22:25:22 -050096 "v3": {
Dinesh Belwalkarc9aa6d82020-03-04 15:22:17 -080097 "ID": "common.ID",
98 "port": "voltha.Port",
99 "ValueSpecifier": "common.ValueSpecifier",
kesavand12cd8eb2020-01-20 22:25:22 -0500100 },
Zack Williamse940c7a2019-08-21 14:25:39 -0700101 }
102
103 CharReplacer = strings.NewReplacer("\\t", "\t", "\\n", "\n")
104
105 GlobalConfig = GlobalConfigSpec{
kesavand12cd8eb2020-01-20 22:25:22 -0500106 ApiVersion: "v3",
David Bainbridge0f758d42019-10-26 05:17:48 +0000107 Server: "localhost:55555",
Scott Bakered4efab2020-01-13 19:12:25 -0800108 Kafka: "",
divyadesai19009132020-03-04 12:58:08 +0000109 KvStore: "localhost:2379",
Zack Williamse940c7a2019-08-21 14:25:39 -0700110 Tls: TlsConfigSpec{
111 UseTls: false,
112 },
113 Grpc: GrpcConfigSpec{
divyadesai19009132020-03-04 12:58:08 +0000114 Timeout: defaultGrpcTimeout,
115 },
116 KvStoreConfig: KvStoreConfigSpec{
117 Timeout: defaultKvTimeout,
Zack Williamse940c7a2019-08-21 14:25:39 -0700118 },
119 }
120
David Bainbridgea6722342019-10-24 23:55:53 +0000121 GlobalCommandOptions = make(map[string]map[string]string)
122
Zack Williamse940c7a2019-08-21 14:25:39 -0700123 GlobalOptions struct {
divyadesai19009132020-03-04 12:58:08 +0000124 Config string `short:"c" long:"config" env:"VOLTCONFIG" value-name:"FILE" default:"" description:"Location of client config file"`
125 Server string `short:"s" long:"server" default:"" value-name:"SERVER:PORT" description:"IP/Host and port of VOLTHA"`
126 Kafka string `short:"k" long:"kafka" default:"" value-name:"SERVER:PORT" description:"IP/Host and port of Kafka"`
127 KvStore string `short:"e" long:"kvstore" env:"KVSTORE" value-name:"SERVER:PORT" description:"IP/Host and port of KV store (etcd)"`
128
David Bainbridgec4029aa2019-09-26 18:56:39 +0000129 // Do not set the default for the API version here, else it will override the value read in the config
David K. Bainbridge2b627612020-02-18 14:50:13 -0800130 // nolint: staticcheck
kesavand12cd8eb2020-01-20 22:25:22 -0500131 ApiVersion string `short:"a" long:"apiversion" description:"API version" value-name:"VERSION" choice:"v1" choice:"v2" choice:"v3"`
David Bainbridgea6722342019-10-24 23:55:53 +0000132 Debug bool `short:"d" long:"debug" description:"Enable debug mode"`
David K. Bainbridge402f8482020-02-26 17:14:46 -0800133 Timeout string `short:"t" long:"timeout" description:"API call timeout duration" value-name:"DURATION" default:""`
David Bainbridgea6722342019-10-24 23:55:53 +0000134 UseTLS bool `long:"tls" description:"Use TLS"`
135 CACert string `long:"tlscacert" value-name:"CA_CERT_FILE" description:"Trust certs signed only by this CA"`
136 Cert string `long:"tlscert" value-name:"CERT_FILE" description:"Path to TLS vertificate file"`
137 Key string `long:"tlskey" value-name:"KEY_FILE" description:"Path to TLS key file"`
138 Verify bool `long:"tlsverify" description:"Use TLS and verify the remote"`
139 K8sConfig string `short:"8" long:"k8sconfig" env:"KUBECONFIG" value-name:"FILE" default:"" description:"Location of Kubernetes config file"`
divyadesai19009132020-03-04 12:58:08 +0000140 KvStoreTimeout string `long:"kvstoretimeout" env:"KVSTORE_TIMEOUT" value-name:"DURATION" default:"" description:"timeout for calls to KV store"`
David Bainbridgea6722342019-10-24 23:55:53 +0000141 CommandOptions string `short:"o" long:"command-options" env:"VOLTCTL_COMMAND_OPTIONS" value-name:"FILE" default:"" description:"Location of command options default configuration file"`
Zack Williamse940c7a2019-08-21 14:25:39 -0700142 }
David Bainbridgea6722342019-10-24 23:55:53 +0000143
144 Debug = log.New(os.Stdout, "DEBUG: ", 0)
145 Info = log.New(os.Stdout, "INFO: ", 0)
146 Warn = log.New(os.Stderr, "WARN: ", 0)
147 Error = log.New(os.Stderr, "ERROR: ", 0)
Zack Williamse940c7a2019-08-21 14:25:39 -0700148)
149
150type OutputOptions struct {
David K. Bainbridge2b627612020-02-18 14:50:13 -0800151 Format string `long:"format" value-name:"FORMAT" default:"" description:"Format to use to output structured data"`
152 Quiet bool `short:"q" long:"quiet" description:"Output only the IDs of the objects"`
153 // nolint: staticcheck
Zack Williamse940c7a2019-08-21 14:25:39 -0700154 OutputAs string `short:"o" long:"outputas" default:"table" choice:"table" choice:"json" choice:"yaml" description:"Type of output to generate"`
155 NameLimit int `short:"l" long:"namelimit" default:"-1" value-name:"LIMIT" description:"Limit the depth (length) in the table column name"`
156}
157
158type ListOutputOptions struct {
159 OutputOptions
160 Filter string `short:"f" long:"filter" default:"" value-name:"FILTER" description:"Only display results that match filter"`
161 OrderBy string `short:"r" long:"orderby" default:"" value-name:"ORDER" description:"Specify the sort order of the results"`
162}
163
164type OutputOptionsJson struct {
David K. Bainbridge2b627612020-02-18 14:50:13 -0800165 Format string `long:"format" value-name:"FORMAT" default:"" description:"Format to use to output structured data"`
166 Quiet bool `short:"q" long:"quiet" description:"Output only the IDs of the objects"`
167 // nolint: staticcheck
Zack Williamse940c7a2019-08-21 14:25:39 -0700168 OutputAs string `short:"o" long:"outputas" default:"json" choice:"table" choice:"json" choice:"yaml" description:"Type of output to generate"`
169 NameLimit int `short:"l" long:"namelimit" default:"-1" value-name:"LIMIT" description:"Limit the depth (length) in the table column name"`
170}
171
172type ListOutputOptionsJson struct {
173 OutputOptionsJson
174 Filter string `short:"f" long:"filter" default:"" value-name:"FILTER" description:"Only display results that match filter"`
175 OrderBy string `short:"r" long:"orderby" default:"" value-name:"ORDER" description:"Specify the sort order of the results"`
176}
177
178func toOutputType(in string) OutputType {
179 switch in {
180 case "table":
181 fallthrough
182 default:
183 return OUTPUT_TABLE
184 case "json":
185 return OUTPUT_JSON
186 case "yaml":
187 return OUTPUT_YAML
188 }
189}
190
divyadesai19009132020-03-04 12:58:08 +0000191func splitEndpoint(ep, defaultHost string, defaultPort int) (string, int, error) {
192 port := defaultPort
193 host, sPort, err := net.SplitHostPort(ep)
194 if err != nil {
195 if addrErr, ok := err.(*net.AddrError); ok {
196 if addrErr.Err != "missing port in address" {
197 return "", 0, err
198 }
199 host = ep
200 } else {
201 return "", 0, err
202 }
203 } else if len(strings.TrimSpace(sPort)) > 0 {
204 val, err := strconv.Atoi(sPort)
205 if err != nil {
206 return "", 0, err
207 }
208 port = val
209 }
210 if len(strings.TrimSpace(host)) == 0 {
211 host = defaultHost
212 }
213 return strings.Trim(host, "]["), port, nil
214}
215
Zack Williamse940c7a2019-08-21 14:25:39 -0700216type CommandResult struct {
217 Format format.Format
218 Filter string
219 OrderBy string
220 OutputAs OutputType
221 NameLimit int
222 Data interface{}
223}
224
David Bainbridgea6722342019-10-24 23:55:53 +0000225func GetCommandOptionWithDefault(name, option, defaultValue string) string {
226 if cmd, ok := GlobalCommandOptions[name]; ok {
227 if val, ok := cmd[option]; ok {
228 return CharReplacer.Replace(val)
229 }
230 }
231 return defaultValue
232}
233
Zack Williamse940c7a2019-08-21 14:25:39 -0700234func ProcessGlobalOptions() {
235 if len(GlobalOptions.Config) == 0 {
236 home, err := os.UserHomeDir()
237 if err != nil {
David Bainbridgea6722342019-10-24 23:55:53 +0000238 Warn.Printf("Unable to discover the user's home directory: %s", err)
Zack Williamse940c7a2019-08-21 14:25:39 -0700239 home = "~"
240 }
241 GlobalOptions.Config = filepath.Join(home, ".volt", "config")
242 }
243
David Bainbridgea6722342019-10-24 23:55:53 +0000244 if info, err := os.Stat(GlobalOptions.Config); err == nil && !info.IsDir() {
Zack Williamse940c7a2019-08-21 14:25:39 -0700245 configFile, err := ioutil.ReadFile(GlobalOptions.Config)
246 if err != nil {
David Bainbridgea6722342019-10-24 23:55:53 +0000247 Error.Fatalf("Unable to read the configuration file '%s': %s",
248 GlobalOptions.Config, err.Error())
Zack Williamse940c7a2019-08-21 14:25:39 -0700249 }
David Bainbridgea6722342019-10-24 23:55:53 +0000250 if err = yaml.Unmarshal(configFile, &GlobalConfig); err != nil {
251 Error.Fatalf("Unable to parse the configuration file '%s': %s",
252 GlobalOptions.Config, err.Error())
Zack Williamse940c7a2019-08-21 14:25:39 -0700253 }
254 }
255
256 // Override from command line
257 if GlobalOptions.Server != "" {
258 GlobalConfig.Server = GlobalOptions.Server
259 }
divyadesai19009132020-03-04 12:58:08 +0000260 host, port, err := splitEndpoint(GlobalConfig.Server, defaultApiHost, defaultApiPort)
261 if err != nil {
262 Error.Fatalf("voltha API endport incorrectly specified '%s':%s",
263 GlobalConfig.Server, err)
264 }
265 GlobalConfig.Server = net.JoinHostPort(host, strconv.Itoa(port))
266
Scott Bakered4efab2020-01-13 19:12:25 -0800267 if GlobalOptions.Kafka != "" {
268 GlobalConfig.Kafka = GlobalOptions.Kafka
269 }
divyadesai19009132020-03-04 12:58:08 +0000270 host, port, err = splitEndpoint(GlobalConfig.Kafka, defaultKafkaHost, defaultKafkaPort)
271 if err != nil {
272 Error.Fatalf("Kafka endport incorrectly specified '%s':%s",
273 GlobalConfig.Kafka, err)
274 }
275 GlobalConfig.Kafka = net.JoinHostPort(host, strconv.Itoa(port))
276
277 if GlobalOptions.KvStore != "" {
278 GlobalConfig.KvStore = GlobalOptions.KvStore
279 }
280 host, port, err = splitEndpoint(GlobalConfig.KvStore, defaultKvHost, defaultKvPort)
281 if err != nil {
282 Error.Fatalf("KV store endport incorrectly specified '%s':%s",
283 GlobalConfig.KvStore, err)
284 }
285 GlobalConfig.KvStore = net.JoinHostPort(host, strconv.Itoa(port))
286
Zack Williamse940c7a2019-08-21 14:25:39 -0700287 if GlobalOptions.ApiVersion != "" {
288 GlobalConfig.ApiVersion = GlobalOptions.ApiVersion
289 }
290
divyadesai19009132020-03-04 12:58:08 +0000291 if GlobalOptions.KvStoreTimeout != "" {
292 timeout, err := time.ParseDuration(GlobalOptions.KvStoreTimeout)
293 if err != nil {
294 Error.Fatalf("Unable to parse specified KV strore timeout duration '%s': %s",
295 GlobalOptions.KvStoreTimeout, err.Error())
296 }
297 GlobalConfig.KvStoreConfig.Timeout = timeout
298 }
299
David K. Bainbridge402f8482020-02-26 17:14:46 -0800300 if GlobalOptions.Timeout != "" {
301 timeout, err := time.ParseDuration(GlobalOptions.Timeout)
302 if err != nil {
303 Error.Fatalf("Unable to parse specified timeout duration '%s': %s",
304 GlobalOptions.Timeout, err.Error())
305 }
306 GlobalConfig.Grpc.Timeout = timeout
307 }
308
Zack Williamse940c7a2019-08-21 14:25:39 -0700309 // If a k8s cert/key were not specified, then attempt to read it from
310 // any $HOME/.kube/config if it exists
311 if len(GlobalOptions.K8sConfig) == 0 {
312 home, err := os.UserHomeDir()
313 if err != nil {
David Bainbridgea6722342019-10-24 23:55:53 +0000314 Warn.Printf("Unable to discover the user's home directory: %s", err)
Zack Williamse940c7a2019-08-21 14:25:39 -0700315 home = "~"
316 }
317 GlobalOptions.K8sConfig = filepath.Join(home, ".kube", "config")
318 }
David Bainbridgea6722342019-10-24 23:55:53 +0000319
320 if len(GlobalOptions.CommandOptions) == 0 {
321 home, err := os.UserHomeDir()
322 if err != nil {
323 Warn.Printf("Unable to discover the user's home directory: %s", err)
324 home = "~"
325 }
326 GlobalOptions.CommandOptions = filepath.Join(home, ".volt", "command_options")
327 }
328
329 if info, err := os.Stat(GlobalOptions.CommandOptions); err == nil && !info.IsDir() {
330 optionsFile, err := ioutil.ReadFile(GlobalOptions.CommandOptions)
331 if err != nil {
332 Error.Fatalf("Unable to read command options configuration file '%s' : %s",
333 GlobalOptions.CommandOptions, err.Error())
334 }
335 if err = yaml.Unmarshal(optionsFile, &GlobalCommandOptions); err != nil {
336 Error.Fatalf("Unable to parse the command line options configuration file '%s': %s",
337 GlobalOptions.CommandOptions, err.Error())
338 }
339 }
Zack Williamse940c7a2019-08-21 14:25:39 -0700340}
341
342func NewConnection() (*grpc.ClientConn, error) {
343 ProcessGlobalOptions()
344 return grpc.Dial(GlobalConfig.Server, grpc.WithInsecure())
345}
346
Scott Baker9173ed82020-05-19 08:30:12 -0700347func ConvertJsonProtobufArray(data_in interface{}) (string, error) {
348 result := ""
349
350 slice := reflect.ValueOf(data_in)
351 if slice.Kind() != reflect.Slice {
352 return "", errors.New("Not a slice")
353 }
354
355 result = result + "["
356
357 marshaler := jsonpb.Marshaler{EmitDefaults: true}
358 for i := 0; i < slice.Len(); i++ {
359 item := slice.Index(i).Interface()
360 protoMessage, okay := item.(proto.Message)
361 if !okay {
362 return "", errors.New("Failed to convert item to a proto.Message")
363 }
364 asJson, err := marshaler.MarshalToString(protoMessage)
365 if err != nil {
366 return "", fmt.Errorf("Failed to marshal the json: %s", err)
367 }
368
369 result = result + asJson
370
371 if i < slice.Len()-1 {
372 result = result + ","
373 }
374 }
375
376 result = result + "]"
377
378 return result, nil
379}
380
Zack Williamse940c7a2019-08-21 14:25:39 -0700381func GenerateOutput(result *CommandResult) {
382 if result != nil && result.Data != nil {
383 data := result.Data
384 if result.Filter != "" {
385 f, err := filter.Parse(result.Filter)
386 if err != nil {
David Bainbridge0f758d42019-10-26 05:17:48 +0000387 Error.Fatalf("Unable to parse specified output filter '%s': %s", result.Filter, err.Error())
Zack Williamse940c7a2019-08-21 14:25:39 -0700388 }
389 data, err = f.Process(data)
390 if err != nil {
David Bainbridge0f758d42019-10-26 05:17:48 +0000391 Error.Fatalf("Unexpected error while filtering command results: %s", err.Error())
Zack Williamse940c7a2019-08-21 14:25:39 -0700392 }
393 }
394 if result.OrderBy != "" {
395 s, err := order.Parse(result.OrderBy)
396 if err != nil {
David Bainbridge0f758d42019-10-26 05:17:48 +0000397 Error.Fatalf("Unable to parse specified sort specification '%s': %s", result.OrderBy, err.Error())
Zack Williamse940c7a2019-08-21 14:25:39 -0700398 }
399 data, err = s.Process(data)
400 if err != nil {
David Bainbridge0f758d42019-10-26 05:17:48 +0000401 Error.Fatalf("Unexpected error while sorting command result: %s", err.Error())
Zack Williamse940c7a2019-08-21 14:25:39 -0700402 }
403 }
404 if result.OutputAs == OUTPUT_TABLE {
405 tableFormat := format.Format(result.Format)
David Bainbridge12f036f2019-10-15 22:09:04 +0000406 if err := tableFormat.Execute(os.Stdout, true, result.NameLimit, data); err != nil {
David Bainbridge0f758d42019-10-26 05:17:48 +0000407 Error.Fatalf("Unexpected error while attempting to format results as table : %s", err.Error())
David Bainbridge12f036f2019-10-15 22:09:04 +0000408 }
Zack Williamse940c7a2019-08-21 14:25:39 -0700409 } else if result.OutputAs == OUTPUT_JSON {
Scott Baker9173ed82020-05-19 08:30:12 -0700410 // first try to convert it as an array of protobufs
411 asJson, err := ConvertJsonProtobufArray(data)
Zack Williamse940c7a2019-08-21 14:25:39 -0700412 if err != nil {
Scott Baker9173ed82020-05-19 08:30:12 -0700413 // if that fails, then just do a standard json conversion
414 asJsonB, err := json.Marshal(&data)
415 if err != nil {
416 Error.Fatalf("Unexpected error while processing command results to JSON: %s", err.Error())
417 }
418 asJson = string(asJsonB)
Zack Williamse940c7a2019-08-21 14:25:39 -0700419 }
420 fmt.Printf("%s", asJson)
421 } else if result.OutputAs == OUTPUT_YAML {
422 asYaml, err := yaml.Marshal(&data)
423 if err != nil {
David Bainbridge0f758d42019-10-26 05:17:48 +0000424 Error.Fatalf("Unexpected error while processing command results to YAML: %s", err.Error())
Zack Williamse940c7a2019-08-21 14:25:39 -0700425 }
426 fmt.Printf("%s", asYaml)
427 }
428 }
429}