blob: acdc115da5b79fb09c428ae52ffbf5bddaee26ea [file] [log] [blame]
Scott Bakerd69e4222019-08-21 16:46:05 -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 "context"
divyadesai19009132020-03-04 12:58:08 +000020 "errors"
Scott Bakerd69e4222019-08-21 16:46:05 -070021 "fmt"
Scott Bakerd69e4222019-08-21 16:46:05 -070022 flags "github.com/jessevdk/go-flags"
Scott Bakerd69e4222019-08-21 16:46:05 -070023 "github.com/opencord/voltctl/pkg/format"
24 "github.com/opencord/voltctl/pkg/model"
divyadesai19009132020-03-04 12:58:08 +000025 "github.com/opencord/voltha-lib-go/v3/pkg/config"
26 "github.com/opencord/voltha-lib-go/v3/pkg/db/kvstore"
27 "github.com/opencord/voltha-lib-go/v3/pkg/log"
28 "io/ioutil"
29 "os"
Scott Bakerd69e4222019-08-21 16:46:05 -070030 "strings"
31)
32
divyadesai19009132020-03-04 12:58:08 +000033const (
34 defaultComponentName = "global"
35 defaultPackageName = "default"
36)
37
38// LogLevelOutput represents the output structure for the loglevel
39type LogLevelOutput struct {
Scott Bakerd69e4222019-08-21 16:46:05 -070040 ComponentName string
41 Status string
42 Error string
43}
44
divyadesai19009132020-03-04 12:58:08 +000045// SetLogLevelOpts represents the supported CLI arguments for the loglevel set command
Scott Bakerd69e4222019-08-21 16:46:05 -070046type SetLogLevelOpts struct {
47 OutputOptions
divyadesai19009132020-03-04 12:58:08 +000048 Args struct {
Scott Bakerd69e4222019-08-21 16:46:05 -070049 Level string
50 Component []string
51 } `positional-args:"yes" required:"yes"`
52}
53
divyadesai19009132020-03-04 12:58:08 +000054// ListLogLevelOpts represents the supported CLI arguments for the loglevel list command
55type ListLogLevelsOpts struct {
Scott Bakerd69e4222019-08-21 16:46:05 -070056 ListOutputOptions
57 Args struct {
58 Component []string
59 } `positional-args:"yes" required:"yes"`
60}
61
divyadesai19009132020-03-04 12:58:08 +000062// ClearLogLevelOpts represents the supported CLI arguments for the loglevel clear command
63type ClearLogLevelsOpts struct {
64 OutputOptions
65 Args struct {
66 Component []string
67 } `positional-args:"yes" required:"yes"`
Scott Bakerd69e4222019-08-21 16:46:05 -070068}
69
divyadesai19009132020-03-04 12:58:08 +000070// LogLevelOpts represents the loglevel commands
Scott Bakerd69e4222019-08-21 16:46:05 -070071type LogLevelOpts struct {
divyadesai19009132020-03-04 12:58:08 +000072 SetLogLevel SetLogLevelOpts `command:"set"`
73 ListLogLevels ListLogLevelsOpts `command:"list"`
74 ClearLogLevels ClearLogLevelsOpts `command:"clear"`
Scott Bakerd69e4222019-08-21 16:46:05 -070075}
76
77var logLevelOpts = LogLevelOpts{}
78
79const (
divyadesai19009132020-03-04 12:58:08 +000080 DEFAULT_LOGLEVELS_FORMAT = "table{{ .ComponentName }}\t{{.PackageName}}\t{{.Level}}"
81 DEFAULT_LOGLEVEL_RESULT_FORMAT = "table{{ .ComponentName }}\t{{.Status}}\t{{.Error}}"
Scott Bakerd69e4222019-08-21 16:46:05 -070082)
83
divyadesai19009132020-03-04 12:58:08 +000084// RegisterLogLevelCommands is used to register set,list and clear loglevel of components
Scott Bakerd69e4222019-08-21 16:46:05 -070085func RegisterLogLevelCommands(parent *flags.Parser) {
divyadesai19009132020-03-04 12:58:08 +000086 _, err := parent.AddCommand("loglevel", "loglevel commands", "list, set, clear log levels of components", &logLevelOpts)
Scott Bakerd69e4222019-08-21 16:46:05 -070087 if err != nil {
David Bainbridge0f758d42019-10-26 05:17:48 +000088 Error.Fatalf("Unable to register log level commands with voltctl command parser: %s", err.Error())
Scott Bakerd69e4222019-08-21 16:46:05 -070089 }
90}
91
divyadesai19009132020-03-04 12:58:08 +000092// processComponentListArgs stores the component name and package names given in command arguments to LogLevel
93// It checks the given argument has # key or not, if # is present then split the argument for # then stores first part as component name
94// and second part as package name
95func processComponentListArgs(Components []string) ([]model.LogLevel, error) {
Scott Bakerd69e4222019-08-21 16:46:05 -070096
divyadesai19009132020-03-04 12:58:08 +000097 var logLevelConfig []model.LogLevel
Scott Bakerd69e4222019-08-21 16:46:05 -070098
divyadesai19009132020-03-04 12:58:08 +000099 if len(Components) == 0 {
100 Components = append(Components, defaultComponentName)
Scott Bakerd69e4222019-08-21 16:46:05 -0700101 }
102
divyadesai19009132020-03-04 12:58:08 +0000103 for _, component := range Components {
104 logConfig := model.LogLevel{}
105 val := strings.SplitN(component, "#", 2)
divyadesai6e28d302020-03-16 08:39:16 +0000106
107 if strings.Contains(val[0], "/") {
108 return nil, errors.New("the component name '" + val[0] + "' contains an invalid character '/'")
109 }
110
divyadesai19009132020-03-04 12:58:08 +0000111 if len(val) > 1 {
112 if val[0] == defaultComponentName {
113 return nil, errors.New("global level doesn't support packageName")
Scott Bakerd69e4222019-08-21 16:46:05 -0700114 }
divyadesai19009132020-03-04 12:58:08 +0000115 logConfig.ComponentName = val[0]
116 logConfig.PackageName = strings.ReplaceAll(val[1], "/", "#")
117 } else {
118 logConfig.ComponentName = component
119 logConfig.PackageName = defaultPackageName
Scott Bakerd69e4222019-08-21 16:46:05 -0700120 }
divyadesai19009132020-03-04 12:58:08 +0000121 logLevelConfig = append(logLevelConfig, logConfig)
Scott Bakerd69e4222019-08-21 16:46:05 -0700122 }
divyadesai19009132020-03-04 12:58:08 +0000123 return logLevelConfig, nil
Scott Bakerd69e4222019-08-21 16:46:05 -0700124}
125
divyadesai19009132020-03-04 12:58:08 +0000126// This method set loglevel for components.
127// For example, using below command loglevel can be set for specific component with default packageName
128// voltctl loglevel set level <componentName>
129// For example, using below command loglevel can be set for specific component with specific packageName
130// voltctl loglevel set level <componentName#packageName>
131// For example, using below command loglevel can be set for more than one component for default package and other component for specific packageName
132// voltctl loglevel set level <componentName1#packageName> <componentName2>
Scott Bakerd69e4222019-08-21 16:46:05 -0700133func (options *SetLogLevelOpts) Execute(args []string) error {
divyadesai19009132020-03-04 12:58:08 +0000134 var (
135 logLevelConfig []model.LogLevel
136 err error
137 )
138 ProcessGlobalOptions()
139
140 /*
141 * TODO: VOL-2738
142 * EVIL HACK ALERT
143 * ===============
144 * It would be nice if we could squelch all but fatal log messages from
145 * the underlying libraries because as a CLI client we don't want a
146 * bunch of logs and stack traces output and instead want to deal with
147 * simple error propagation. To work around this, voltha-lib-go logging
148 * is set to fatal and we redirect etcd client logging to a temp file.
149 *
150 * Replacing os.Stderr is used here as opposed to Dup2 because we want
151 * low level panic to be displayed if they occurr. A temp file is used
152 * as opposed to /dev/null because it can't be assumed that /dev/null
153 * exists on all platforms and thus a temp file seems more portable.
154 */
155 log.SetAllLogLevel(log.FatalLevel)
156 saveStderr := os.Stderr
157 if tmpStderr, err := ioutil.TempFile("", ""); err == nil {
158 os.Stderr = tmpStderr
159 defer func() {
160 os.Stderr = saveStderr
161 // Ignore errors on clean up because we can't do
162 // anything anyway.
163 _ = tmpStderr.Close()
164 _ = os.Remove(tmpStderr.Name())
165 }()
Scott Bakerd69e4222019-08-21 16:46:05 -0700166 }
167
divyadesai19009132020-03-04 12:58:08 +0000168 if options.Args.Level != "" {
169 if _, err := log.StringToLogLevel(options.Args.Level); err != nil {
divyadesai6e28d302020-03-16 08:39:16 +0000170 return fmt.Errorf("Unknown log level '%s'. Allowed values are DEBUG, INFO, WARN, ERROR, FATAL", options.Args.Level)
Scott Bakerd69e4222019-08-21 16:46:05 -0700171 }
172 }
173
divyadesai19009132020-03-04 12:58:08 +0000174 logLevelConfig, err = processComponentListArgs(options.Args.Component)
175 if err != nil {
176 return fmt.Errorf(err.Error())
177 }
178
179 client, err := kvstore.NewEtcdClient(GlobalConfig.KvStore, int(GlobalConfig.KvStoreConfig.Timeout.Seconds()))
180 if err != nil {
181 return fmt.Errorf("Unable to create kvstore client %s", err)
182 }
183 defer client.Close()
184
185 // Already error checked during option processing
186 host, port, _ := splitEndpoint(GlobalConfig.KvStore, defaultKvHost, defaultKvPort)
187 cm := config.NewConfigManager(client, supportedKvStoreType, host, port, int(GlobalConfig.KvStoreConfig.Timeout.Seconds()))
188
189 var output []LogLevelOutput
190
191 ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.KvStoreConfig.Timeout)
192 defer cancel()
193
194 for _, lConfig := range logLevelConfig {
195
196 logConfig := cm.InitComponentConfig(lConfig.ComponentName, config.ConfigTypeLogLevel)
197 err := logConfig.Save(ctx, lConfig.PackageName, strings.ToUpper(options.Args.Level))
198 if err != nil {
199 output = append(output, LogLevelOutput{ComponentName: lConfig.ComponentName, Status: "Failure", Error: err.Error()})
200 } else {
201 output = append(output, LogLevelOutput{ComponentName: lConfig.ComponentName, Status: "Success"})
202 }
203
204 }
205
Scott Bakerd69e4222019-08-21 16:46:05 -0700206 outputFormat := CharReplacer.Replace(options.Format)
207 if outputFormat == "" {
divyadesai19009132020-03-04 12:58:08 +0000208 outputFormat = GetCommandOptionWithDefault("loglevel-set", "format", DEFAULT_LOGLEVEL_RESULT_FORMAT)
Scott Bakerd69e4222019-08-21 16:46:05 -0700209 }
Scott Bakerd69e4222019-08-21 16:46:05 -0700210 result := CommandResult{
211 Format: format.Format(outputFormat),
212 OutputAs: toOutputType(options.OutputAs),
213 NameLimit: options.NameLimit,
214 Data: output,
215 }
216
217 GenerateOutput(&result)
218 return nil
219}
220
divyadesai19009132020-03-04 12:58:08 +0000221// This method list loglevel for components.
222// For example, using below command loglevel can be list for specific component
223// voltctl loglevel list <componentName>
224// For example, using below command loglevel can be list for all the components with all the packageName
225// voltctl loglevel list
226func (options *ListLogLevelsOpts) Execute(args []string) error {
227
228 var (
David K. Bainbridge4bbad142020-03-11 11:55:39 -0700229 // Initialize to empty as opposed to nil so that -o json will
230 // display empty list and not null VOL-2742
231 data []model.LogLevel = []model.LogLevel{}
divyadesai19009132020-03-04 12:58:08 +0000232 componentList []string
233 logLevelConfig map[string]string
234 err error
235 )
236 ProcessGlobalOptions()
237
238 /*
239 * TODO: VOL-2738
240 * EVIL HACK ALERT
241 * ===============
242 * It would be nice if we could squelch all but fatal log messages from
243 * the underlying libraries because as a CLI client we don't want a
244 * bunch of logs and stack traces output and instead want to deal with
245 * simple error propagation. To work around this, voltha-lib-go logging
246 * is set to fatal and we redirect etcd client logging to a temp file.
247 *
248 * Replacing os.Stderr is used here as opposed to Dup2 because we want
249 * low level panic to be displayed if they occurr. A temp file is used
250 * as opposed to /dev/null because it can't be assumed that /dev/null
251 * exists on all platforms and thus a temp file seems more portable.
252 */
253 log.SetAllLogLevel(log.FatalLevel)
254 saveStderr := os.Stderr
255 if tmpStderr, err := ioutil.TempFile("", ""); err == nil {
256 os.Stderr = tmpStderr
257 defer func() {
258 os.Stderr = saveStderr
259 // Ignore errors on clean up because we can't do
260 // anything anyway.
261 _ = tmpStderr.Close()
262 _ = os.Remove(tmpStderr.Name())
263 }()
264 }
265
266 client, err := kvstore.NewEtcdClient(GlobalConfig.KvStore, int(GlobalConfig.KvStoreConfig.Timeout.Seconds()))
267 if err != nil {
268 return fmt.Errorf("Unable to create kvstore client %s", err)
269 }
270 defer client.Close()
271
272 // Already error checked during option processing
273 host, port, _ := splitEndpoint(GlobalConfig.KvStore, defaultKvHost, defaultKvPort)
274 cm := config.NewConfigManager(client, supportedKvStoreType, host, port, int(GlobalConfig.KvStoreConfig.Timeout.Seconds()))
275
276 ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.KvStoreConfig.Timeout)
277 defer cancel()
278
Scott Bakerd69e4222019-08-21 16:46:05 -0700279 if len(options.Args.Component) == 0 {
divyadesai19009132020-03-04 12:58:08 +0000280 componentList, err = cm.RetrieveComponentList(ctx, config.ConfigTypeLogLevel)
281 if err != nil {
282 return fmt.Errorf("Unable to retrieve list of voltha components : %s ", err)
283 }
284 } else {
285 componentList = options.Args.Component
Scott Bakerd69e4222019-08-21 16:46:05 -0700286 }
287
divyadesai19009132020-03-04 12:58:08 +0000288 for _, componentName := range componentList {
289 logConfig := cm.InitComponentConfig(componentName, config.ConfigTypeLogLevel)
Scott Bakerd69e4222019-08-21 16:46:05 -0700290
divyadesai19009132020-03-04 12:58:08 +0000291 logLevelConfig, err = logConfig.RetrieveAll(ctx)
292 if err != nil {
293 return fmt.Errorf("Unable to retrieve loglevel configuration for component %s : %s", componentName, err)
294 }
Scott Bakerd69e4222019-08-21 16:46:05 -0700295
divyadesai19009132020-03-04 12:58:08 +0000296 for packageName, level := range logLevelConfig {
297 logLevel := model.LogLevel{}
298 if packageName == "" {
299 continue
Scott Bakerd69e4222019-08-21 16:46:05 -0700300 }
301
divyadesai19009132020-03-04 12:58:08 +0000302 pName := strings.ReplaceAll(packageName, "#", "/")
303 logLevel.PopulateFrom(componentName, pName, level)
304 data = append(data, logLevel)
Scott Bakerd69e4222019-08-21 16:46:05 -0700305 }
306 }
307
308 outputFormat := CharReplacer.Replace(options.Format)
309 if outputFormat == "" {
divyadesai19009132020-03-04 12:58:08 +0000310 outputFormat = GetCommandOptionWithDefault("loglevel-list", "format", DEFAULT_LOGLEVELS_FORMAT)
David Bainbridgea6722342019-10-24 23:55:53 +0000311 }
312 orderBy := options.OrderBy
313 if orderBy == "" {
divyadesai19009132020-03-04 12:58:08 +0000314 orderBy = GetCommandOptionWithDefault("loglevel-list", "order", "")
Scott Bakerd69e4222019-08-21 16:46:05 -0700315 }
316
317 result := CommandResult{
318 Format: format.Format(outputFormat),
319 Filter: options.Filter,
David Bainbridgea6722342019-10-24 23:55:53 +0000320 OrderBy: orderBy,
Scott Bakerd69e4222019-08-21 16:46:05 -0700321 OutputAs: toOutputType(options.OutputAs),
322 NameLimit: options.NameLimit,
323 Data: data,
324 }
325 GenerateOutput(&result)
326 return nil
327}
328
divyadesai19009132020-03-04 12:58:08 +0000329// This method clear loglevel for components.
330// For example, using below command loglevel can be clear for specific component with default packageName
331// voltctl loglevel clear <componentName>
332// For example, using below command loglevel can be clear for specific component with specific packageName
333// voltctl loglevel clear <componentName#packageName>
334func (options *ClearLogLevelsOpts) Execute(args []string) error {
David Bainbridgea6722342019-10-24 23:55:53 +0000335
divyadesai19009132020-03-04 12:58:08 +0000336 var (
337 logLevelConfig []model.LogLevel
338 err error
339 )
340 ProcessGlobalOptions()
Scott Bakerd69e4222019-08-21 16:46:05 -0700341
divyadesai19009132020-03-04 12:58:08 +0000342 /*
343 * TODO: VOL-2738
344 * EVIL HACK ALERT
345 * ===============
346 * It would be nice if we could squelch all but fatal log messages from
347 * the underlying libraries because as a CLI client we don't want a
348 * bunch of logs and stack traces output and instead want to deal with
349 * simple error propagation. To work around this, voltha-lib-go logging
350 * is set to fatal and we redirect etcd client logging to a temp file.
351 *
352 * Replacing os.Stderr is used here as opposed to Dup2 because we want
353 * low level panic to be displayed if they occurr. A temp file is used
354 * as opposed to /dev/null because it can't be assumed that /dev/null
355 * exists on all platforms and thus a temp file seems more portable.
356 */
357 log.SetAllLogLevel(log.FatalLevel)
358 saveStderr := os.Stderr
359 if tmpStderr, err := ioutil.TempFile("", ""); err == nil {
360 os.Stderr = tmpStderr
361 defer func() {
362 os.Stderr = saveStderr
363 // Ignore errors on clean up because we can't do
364 // anything anyway.
365 _ = tmpStderr.Close()
366 _ = os.Remove(tmpStderr.Name())
367 }()
368 }
369
370 logLevelConfig, err = processComponentListArgs(options.Args.Component)
Scott Bakerd69e4222019-08-21 16:46:05 -0700371 if err != nil {
divyadesai19009132020-03-04 12:58:08 +0000372 return fmt.Errorf("%s", err)
Scott Bakerd69e4222019-08-21 16:46:05 -0700373 }
374
divyadesai19009132020-03-04 12:58:08 +0000375 client, err := kvstore.NewEtcdClient(GlobalConfig.KvStore, int(GlobalConfig.KvStoreConfig.Timeout.Seconds()))
376 if err != nil {
377 return fmt.Errorf("Unable to create kvstore client %s", err)
378 }
379 defer client.Close()
380
381 // Already error checked during option processing
382 host, port, _ := splitEndpoint(GlobalConfig.KvStore, defaultKvHost, defaultKvPort)
383 cm := config.NewConfigManager(client, supportedKvStoreType, host, port, int(GlobalConfig.KvStoreConfig.Timeout.Seconds()))
384
385 var output []LogLevelOutput
386
387 ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.KvStoreConfig.Timeout)
388 defer cancel()
389
390 for _, lConfig := range logLevelConfig {
391
divyadesai6e28d302020-03-16 08:39:16 +0000392 if lConfig.ComponentName == defaultComponentName {
393 return fmt.Errorf("The global default loglevel cannot be cleared.")
394 }
395
divyadesai19009132020-03-04 12:58:08 +0000396 logConfig := cm.InitComponentConfig(lConfig.ComponentName, config.ConfigTypeLogLevel)
397
398 err := logConfig.Delete(ctx, lConfig.PackageName)
399 if err != nil {
400 output = append(output, LogLevelOutput{ComponentName: lConfig.ComponentName, Status: "Failure", Error: err.Error()})
401 } else {
402 output = append(output, LogLevelOutput{ComponentName: lConfig.ComponentName, Status: "Success"})
403 }
Scott Bakerd69e4222019-08-21 16:46:05 -0700404 }
405
divyadesai19009132020-03-04 12:58:08 +0000406 outputFormat := CharReplacer.Replace(options.Format)
407 if outputFormat == "" {
408 outputFormat = GetCommandOptionWithDefault("loglevel-clear", "format", DEFAULT_LOGLEVEL_RESULT_FORMAT)
409 }
Scott Bakerd69e4222019-08-21 16:46:05 -0700410
divyadesai19009132020-03-04 12:58:08 +0000411 result := CommandResult{
412 Format: format.Format(outputFormat),
413 OutputAs: toOutputType(options.OutputAs),
414 NameLimit: options.NameLimit,
415 Data: output,
416 }
Scott Bakerd69e4222019-08-21 16:46:05 -0700417
divyadesai19009132020-03-04 12:58:08 +0000418 GenerateOutput(&result)
419 return nil
Scott Bakerd69e4222019-08-21 16:46:05 -0700420}