blob: 8e3ced7e25779ce4a3c0ea59023af3a4b664294a [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"
20 "fmt"
Scott Baker2b0ad652019-08-21 14:57:07 -070021 "github.com/opencord/voltctl/pkg/filter"
22 "github.com/opencord/voltctl/pkg/format"
23 "github.com/opencord/voltctl/pkg/order"
Zack Williamse940c7a2019-08-21 14:25:39 -070024 "google.golang.org/grpc"
25 "gopkg.in/yaml.v2"
26 "io/ioutil"
27 "log"
divyadesai19009132020-03-04 12:58:08 +000028 "net"
Zack Williamse940c7a2019-08-21 14:25:39 -070029 "os"
30 "path/filepath"
divyadesai19009132020-03-04 12:58:08 +000031 "strconv"
Zack Williamse940c7a2019-08-21 14:25:39 -070032 "strings"
33 "time"
34)
35
36type OutputType uint8
37
38const (
39 OUTPUT_TABLE OutputType = iota
40 OUTPUT_JSON
41 OUTPUT_YAML
divyadesai19009132020-03-04 12:58:08 +000042
43 defaultApiHost = "localhost"
44 defaultApiPort = 55555
45
46 defaultKafkaHost = "localhost"
47 defaultKafkaPort = 9092
48
49 supportedKvStoreType = "etcd"
50 defaultKvHost = "localhost"
51 defaultKvPort = 2379
52 defaultKvTimeout = time.Second * 5
53
54 defaultGrpcTimeout = time.Minute * 5
Zack Williamse940c7a2019-08-21 14:25:39 -070055)
56
57type GrpcConfigSpec struct {
58 Timeout time.Duration `yaml:"timeout"`
59}
60
divyadesai19009132020-03-04 12:58:08 +000061type KvStoreConfigSpec struct {
62 Timeout time.Duration `yaml:"timeout"`
63}
64
Zack Williamse940c7a2019-08-21 14:25:39 -070065type TlsConfigSpec struct {
66 UseTls bool `yaml:"useTls"`
67 CACert string `yaml:"caCert"`
68 Cert string `yaml:"cert"`
69 Key string `yaml:"key"`
70 Verify string `yaml:"verify"`
71}
72
73type GlobalConfigSpec struct {
divyadesai19009132020-03-04 12:58:08 +000074 ApiVersion string `yaml:"apiVersion"`
75 Server string `yaml:"server"`
76 Kafka string `yaml:"kafka"`
77 KvStore string `yaml:"kvstore"`
78 Tls TlsConfigSpec `yaml:"tls"`
79 Grpc GrpcConfigSpec `yaml:"grpc"`
80 KvStoreConfig KvStoreConfigSpec `yaml:"kvstoreconfig"`
81 K8sConfig string `yaml:"-"`
Zack Williamse940c7a2019-08-21 14:25:39 -070082}
83
84var (
85 ParamNames = map[string]map[string]string{
86 "v1": {
87 "ID": "voltha.ID",
88 },
89 "v2": {
90 "ID": "common.ID",
91 },
kesavand12cd8eb2020-01-20 22:25:22 -050092 "v3": {
93 "ID": "common.ID",
94 "port": "voltha.Port",
95 },
Zack Williamse940c7a2019-08-21 14:25:39 -070096 }
97
98 CharReplacer = strings.NewReplacer("\\t", "\t", "\\n", "\n")
99
100 GlobalConfig = GlobalConfigSpec{
kesavand12cd8eb2020-01-20 22:25:22 -0500101 ApiVersion: "v3",
David Bainbridge0f758d42019-10-26 05:17:48 +0000102 Server: "localhost:55555",
Scott Bakered4efab2020-01-13 19:12:25 -0800103 Kafka: "",
divyadesai19009132020-03-04 12:58:08 +0000104 KvStore: "localhost:2379",
Zack Williamse940c7a2019-08-21 14:25:39 -0700105 Tls: TlsConfigSpec{
106 UseTls: false,
107 },
108 Grpc: GrpcConfigSpec{
divyadesai19009132020-03-04 12:58:08 +0000109 Timeout: defaultGrpcTimeout,
110 },
111 KvStoreConfig: KvStoreConfigSpec{
112 Timeout: defaultKvTimeout,
Zack Williamse940c7a2019-08-21 14:25:39 -0700113 },
114 }
115
David Bainbridgea6722342019-10-24 23:55:53 +0000116 GlobalCommandOptions = make(map[string]map[string]string)
117
Zack Williamse940c7a2019-08-21 14:25:39 -0700118 GlobalOptions struct {
divyadesai19009132020-03-04 12:58:08 +0000119 Config string `short:"c" long:"config" env:"VOLTCONFIG" value-name:"FILE" default:"" description:"Location of client config file"`
120 Server string `short:"s" long:"server" default:"" value-name:"SERVER:PORT" description:"IP/Host and port of VOLTHA"`
121 Kafka string `short:"k" long:"kafka" default:"" value-name:"SERVER:PORT" description:"IP/Host and port of Kafka"`
122 KvStore string `short:"e" long:"kvstore" env:"KVSTORE" value-name:"SERVER:PORT" description:"IP/Host and port of KV store (etcd)"`
123
David Bainbridgec4029aa2019-09-26 18:56:39 +0000124 // 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 -0800125 // nolint: staticcheck
kesavand12cd8eb2020-01-20 22:25:22 -0500126 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 +0000127 Debug bool `short:"d" long:"debug" description:"Enable debug mode"`
David K. Bainbridge402f8482020-02-26 17:14:46 -0800128 Timeout string `short:"t" long:"timeout" description:"API call timeout duration" value-name:"DURATION" default:""`
David Bainbridgea6722342019-10-24 23:55:53 +0000129 UseTLS bool `long:"tls" description:"Use TLS"`
130 CACert string `long:"tlscacert" value-name:"CA_CERT_FILE" description:"Trust certs signed only by this CA"`
131 Cert string `long:"tlscert" value-name:"CERT_FILE" description:"Path to TLS vertificate file"`
132 Key string `long:"tlskey" value-name:"KEY_FILE" description:"Path to TLS key file"`
133 Verify bool `long:"tlsverify" description:"Use TLS and verify the remote"`
134 K8sConfig string `short:"8" long:"k8sconfig" env:"KUBECONFIG" value-name:"FILE" default:"" description:"Location of Kubernetes config file"`
divyadesai19009132020-03-04 12:58:08 +0000135 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 +0000136 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 -0700137 }
David Bainbridgea6722342019-10-24 23:55:53 +0000138
139 Debug = log.New(os.Stdout, "DEBUG: ", 0)
140 Info = log.New(os.Stdout, "INFO: ", 0)
141 Warn = log.New(os.Stderr, "WARN: ", 0)
142 Error = log.New(os.Stderr, "ERROR: ", 0)
Zack Williamse940c7a2019-08-21 14:25:39 -0700143)
144
145type OutputOptions struct {
David K. Bainbridge2b627612020-02-18 14:50:13 -0800146 Format string `long:"format" value-name:"FORMAT" default:"" description:"Format to use to output structured data"`
147 Quiet bool `short:"q" long:"quiet" description:"Output only the IDs of the objects"`
148 // nolint: staticcheck
Zack Williamse940c7a2019-08-21 14:25:39 -0700149 OutputAs string `short:"o" long:"outputas" default:"table" choice:"table" choice:"json" choice:"yaml" description:"Type of output to generate"`
150 NameLimit int `short:"l" long:"namelimit" default:"-1" value-name:"LIMIT" description:"Limit the depth (length) in the table column name"`
151}
152
153type ListOutputOptions struct {
154 OutputOptions
155 Filter string `short:"f" long:"filter" default:"" value-name:"FILTER" description:"Only display results that match filter"`
156 OrderBy string `short:"r" long:"orderby" default:"" value-name:"ORDER" description:"Specify the sort order of the results"`
157}
158
159type OutputOptionsJson struct {
David K. Bainbridge2b627612020-02-18 14:50:13 -0800160 Format string `long:"format" value-name:"FORMAT" default:"" description:"Format to use to output structured data"`
161 Quiet bool `short:"q" long:"quiet" description:"Output only the IDs of the objects"`
162 // nolint: staticcheck
Zack Williamse940c7a2019-08-21 14:25:39 -0700163 OutputAs string `short:"o" long:"outputas" default:"json" choice:"table" choice:"json" choice:"yaml" description:"Type of output to generate"`
164 NameLimit int `short:"l" long:"namelimit" default:"-1" value-name:"LIMIT" description:"Limit the depth (length) in the table column name"`
165}
166
167type ListOutputOptionsJson struct {
168 OutputOptionsJson
169 Filter string `short:"f" long:"filter" default:"" value-name:"FILTER" description:"Only display results that match filter"`
170 OrderBy string `short:"r" long:"orderby" default:"" value-name:"ORDER" description:"Specify the sort order of the results"`
171}
172
173func toOutputType(in string) OutputType {
174 switch in {
175 case "table":
176 fallthrough
177 default:
178 return OUTPUT_TABLE
179 case "json":
180 return OUTPUT_JSON
181 case "yaml":
182 return OUTPUT_YAML
183 }
184}
185
divyadesai19009132020-03-04 12:58:08 +0000186func splitEndpoint(ep, defaultHost string, defaultPort int) (string, int, error) {
187 port := defaultPort
188 host, sPort, err := net.SplitHostPort(ep)
189 if err != nil {
190 if addrErr, ok := err.(*net.AddrError); ok {
191 if addrErr.Err != "missing port in address" {
192 return "", 0, err
193 }
194 host = ep
195 } else {
196 return "", 0, err
197 }
198 } else if len(strings.TrimSpace(sPort)) > 0 {
199 val, err := strconv.Atoi(sPort)
200 if err != nil {
201 return "", 0, err
202 }
203 port = val
204 }
205 if len(strings.TrimSpace(host)) == 0 {
206 host = defaultHost
207 }
208 return strings.Trim(host, "]["), port, nil
209}
210
Zack Williamse940c7a2019-08-21 14:25:39 -0700211type CommandResult struct {
212 Format format.Format
213 Filter string
214 OrderBy string
215 OutputAs OutputType
216 NameLimit int
217 Data interface{}
218}
219
David Bainbridgea6722342019-10-24 23:55:53 +0000220func GetCommandOptionWithDefault(name, option, defaultValue string) string {
221 if cmd, ok := GlobalCommandOptions[name]; ok {
222 if val, ok := cmd[option]; ok {
223 return CharReplacer.Replace(val)
224 }
225 }
226 return defaultValue
227}
228
Zack Williamse940c7a2019-08-21 14:25:39 -0700229func ProcessGlobalOptions() {
230 if len(GlobalOptions.Config) == 0 {
231 home, err := os.UserHomeDir()
232 if err != nil {
David Bainbridgea6722342019-10-24 23:55:53 +0000233 Warn.Printf("Unable to discover the user's home directory: %s", err)
Zack Williamse940c7a2019-08-21 14:25:39 -0700234 home = "~"
235 }
236 GlobalOptions.Config = filepath.Join(home, ".volt", "config")
237 }
238
David Bainbridgea6722342019-10-24 23:55:53 +0000239 if info, err := os.Stat(GlobalOptions.Config); err == nil && !info.IsDir() {
Zack Williamse940c7a2019-08-21 14:25:39 -0700240 configFile, err := ioutil.ReadFile(GlobalOptions.Config)
241 if err != nil {
David Bainbridgea6722342019-10-24 23:55:53 +0000242 Error.Fatalf("Unable to read the configuration file '%s': %s",
243 GlobalOptions.Config, err.Error())
Zack Williamse940c7a2019-08-21 14:25:39 -0700244 }
David Bainbridgea6722342019-10-24 23:55:53 +0000245 if err = yaml.Unmarshal(configFile, &GlobalConfig); err != nil {
246 Error.Fatalf("Unable to parse the configuration file '%s': %s",
247 GlobalOptions.Config, err.Error())
Zack Williamse940c7a2019-08-21 14:25:39 -0700248 }
249 }
250
251 // Override from command line
252 if GlobalOptions.Server != "" {
253 GlobalConfig.Server = GlobalOptions.Server
254 }
divyadesai19009132020-03-04 12:58:08 +0000255 host, port, err := splitEndpoint(GlobalConfig.Server, defaultApiHost, defaultApiPort)
256 if err != nil {
257 Error.Fatalf("voltha API endport incorrectly specified '%s':%s",
258 GlobalConfig.Server, err)
259 }
260 GlobalConfig.Server = net.JoinHostPort(host, strconv.Itoa(port))
261
Scott Bakered4efab2020-01-13 19:12:25 -0800262 if GlobalOptions.Kafka != "" {
263 GlobalConfig.Kafka = GlobalOptions.Kafka
264 }
divyadesai19009132020-03-04 12:58:08 +0000265 host, port, err = splitEndpoint(GlobalConfig.Kafka, defaultKafkaHost, defaultKafkaPort)
266 if err != nil {
267 Error.Fatalf("Kafka endport incorrectly specified '%s':%s",
268 GlobalConfig.Kafka, err)
269 }
270 GlobalConfig.Kafka = net.JoinHostPort(host, strconv.Itoa(port))
271
272 if GlobalOptions.KvStore != "" {
273 GlobalConfig.KvStore = GlobalOptions.KvStore
274 }
275 host, port, err = splitEndpoint(GlobalConfig.KvStore, defaultKvHost, defaultKvPort)
276 if err != nil {
277 Error.Fatalf("KV store endport incorrectly specified '%s':%s",
278 GlobalConfig.KvStore, err)
279 }
280 GlobalConfig.KvStore = net.JoinHostPort(host, strconv.Itoa(port))
281
Zack Williamse940c7a2019-08-21 14:25:39 -0700282 if GlobalOptions.ApiVersion != "" {
283 GlobalConfig.ApiVersion = GlobalOptions.ApiVersion
284 }
285
divyadesai19009132020-03-04 12:58:08 +0000286 if GlobalOptions.KvStoreTimeout != "" {
287 timeout, err := time.ParseDuration(GlobalOptions.KvStoreTimeout)
288 if err != nil {
289 Error.Fatalf("Unable to parse specified KV strore timeout duration '%s': %s",
290 GlobalOptions.KvStoreTimeout, err.Error())
291 }
292 GlobalConfig.KvStoreConfig.Timeout = timeout
293 }
294
David K. Bainbridge402f8482020-02-26 17:14:46 -0800295 if GlobalOptions.Timeout != "" {
296 timeout, err := time.ParseDuration(GlobalOptions.Timeout)
297 if err != nil {
298 Error.Fatalf("Unable to parse specified timeout duration '%s': %s",
299 GlobalOptions.Timeout, err.Error())
300 }
301 GlobalConfig.Grpc.Timeout = timeout
302 }
303
Zack Williamse940c7a2019-08-21 14:25:39 -0700304 // If a k8s cert/key were not specified, then attempt to read it from
305 // any $HOME/.kube/config if it exists
306 if len(GlobalOptions.K8sConfig) == 0 {
307 home, err := os.UserHomeDir()
308 if err != nil {
David Bainbridgea6722342019-10-24 23:55:53 +0000309 Warn.Printf("Unable to discover the user's home directory: %s", err)
Zack Williamse940c7a2019-08-21 14:25:39 -0700310 home = "~"
311 }
312 GlobalOptions.K8sConfig = filepath.Join(home, ".kube", "config")
313 }
David Bainbridgea6722342019-10-24 23:55:53 +0000314
315 if len(GlobalOptions.CommandOptions) == 0 {
316 home, err := os.UserHomeDir()
317 if err != nil {
318 Warn.Printf("Unable to discover the user's home directory: %s", err)
319 home = "~"
320 }
321 GlobalOptions.CommandOptions = filepath.Join(home, ".volt", "command_options")
322 }
323
324 if info, err := os.Stat(GlobalOptions.CommandOptions); err == nil && !info.IsDir() {
325 optionsFile, err := ioutil.ReadFile(GlobalOptions.CommandOptions)
326 if err != nil {
327 Error.Fatalf("Unable to read command options configuration file '%s' : %s",
328 GlobalOptions.CommandOptions, err.Error())
329 }
330 if err = yaml.Unmarshal(optionsFile, &GlobalCommandOptions); err != nil {
331 Error.Fatalf("Unable to parse the command line options configuration file '%s': %s",
332 GlobalOptions.CommandOptions, err.Error())
333 }
334 }
Zack Williamse940c7a2019-08-21 14:25:39 -0700335}
336
337func NewConnection() (*grpc.ClientConn, error) {
338 ProcessGlobalOptions()
339 return grpc.Dial(GlobalConfig.Server, grpc.WithInsecure())
340}
341
342func GenerateOutput(result *CommandResult) {
343 if result != nil && result.Data != nil {
344 data := result.Data
345 if result.Filter != "" {
346 f, err := filter.Parse(result.Filter)
347 if err != nil {
David Bainbridge0f758d42019-10-26 05:17:48 +0000348 Error.Fatalf("Unable to parse specified output filter '%s': %s", result.Filter, err.Error())
Zack Williamse940c7a2019-08-21 14:25:39 -0700349 }
350 data, err = f.Process(data)
351 if err != nil {
David Bainbridge0f758d42019-10-26 05:17:48 +0000352 Error.Fatalf("Unexpected error while filtering command results: %s", err.Error())
Zack Williamse940c7a2019-08-21 14:25:39 -0700353 }
354 }
355 if result.OrderBy != "" {
356 s, err := order.Parse(result.OrderBy)
357 if err != nil {
David Bainbridge0f758d42019-10-26 05:17:48 +0000358 Error.Fatalf("Unable to parse specified sort specification '%s': %s", result.OrderBy, err.Error())
Zack Williamse940c7a2019-08-21 14:25:39 -0700359 }
360 data, err = s.Process(data)
361 if err != nil {
David Bainbridge0f758d42019-10-26 05:17:48 +0000362 Error.Fatalf("Unexpected error while sorting command result: %s", err.Error())
Zack Williamse940c7a2019-08-21 14:25:39 -0700363 }
364 }
365 if result.OutputAs == OUTPUT_TABLE {
366 tableFormat := format.Format(result.Format)
David Bainbridge12f036f2019-10-15 22:09:04 +0000367 if err := tableFormat.Execute(os.Stdout, true, result.NameLimit, data); err != nil {
David Bainbridge0f758d42019-10-26 05:17:48 +0000368 Error.Fatalf("Unexpected error while attempting to format results as table : %s", err.Error())
David Bainbridge12f036f2019-10-15 22:09:04 +0000369 }
Zack Williamse940c7a2019-08-21 14:25:39 -0700370 } else if result.OutputAs == OUTPUT_JSON {
371 asJson, err := json.Marshal(&data)
372 if err != nil {
David Bainbridge0f758d42019-10-26 05:17:48 +0000373 Error.Fatalf("Unexpected error while processing command results to JSON: %s", err.Error())
Zack Williamse940c7a2019-08-21 14:25:39 -0700374 }
375 fmt.Printf("%s", asJson)
376 } else if result.OutputAs == OUTPUT_YAML {
377 asYaml, err := yaml.Marshal(&data)
378 if err != nil {
David Bainbridge0f758d42019-10-26 05:17:48 +0000379 Error.Fatalf("Unexpected error while processing command results to YAML: %s", err.Error())
Zack Williamse940c7a2019-08-21 14:25:39 -0700380 }
381 fmt.Printf("%s", asYaml)
382 }
383 }
384}