github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/overlord/hookstate/ctlcmd/get.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2016 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package ctlcmd
    21  
    22  import (
    23  	"encoding/json"
    24  	"fmt"
    25  	"strings"
    26  
    27  	"github.com/snapcore/snapd/i18n"
    28  	"github.com/snapcore/snapd/interfaces"
    29  	"github.com/snapcore/snapd/overlord/configstate"
    30  	"github.com/snapcore/snapd/overlord/configstate/config"
    31  	"github.com/snapcore/snapd/overlord/hookstate"
    32  	"github.com/snapcore/snapd/overlord/state"
    33  )
    34  
    35  type getCommand struct {
    36  	baseCommand
    37  
    38  	// these two options are mutually exclusive
    39  	ForceSlotSide bool `long:"slot" description:"return attribute values from the slot side of the connection"`
    40  	ForcePlugSide bool `long:"plug" description:"return attribute values from the plug side of the connection"`
    41  
    42  	Positional struct {
    43  		PlugOrSlotSpec string   `positional-args:"true" positional-arg-name:":<plug|slot>"`
    44  		Keys           []string `positional-arg-name:"<keys>" description:"option keys"`
    45  	} `positional-args:"yes"`
    46  
    47  	Document bool `short:"d" description:"always return document, even with single key"`
    48  	Typed    bool `short:"t" description:"strict typing with nulls and quoted strings"`
    49  }
    50  
    51  var shortGetHelp = i18n.G("The get command prints configuration and interface connection settings.")
    52  var longGetHelp = i18n.G(`
    53  The get command prints configuration options for the current snap.
    54  
    55      $ snapctl get username
    56      frank
    57  
    58  If multiple option names are provided, a document is returned:
    59  
    60      $ snapctl get username password
    61      {
    62          "username": "frank",
    63          "password": "..."
    64      }
    65  
    66  Nested values may be retrieved via a dotted path:
    67  
    68      $ snapctl get author.name
    69      frank
    70  
    71  Values of interface connection settings may be printed with:
    72  
    73      $ snapctl get :myplug usb-vendor
    74      $ snapctl get :myslot path
    75  
    76  This will return the named setting from the local interface endpoint, whether a plug
    77  or a slot. Returning the setting from the connected snap's endpoint is also possible
    78  by explicitly requesting that via the --plug and --slot command line options:
    79  
    80      $ snapctl get :myplug --slot usb-vendor
    81  
    82  This requests the "usb-vendor" setting from the slot that is connected to "myplug".
    83  `)
    84  
    85  func init() {
    86  	addCommand("get", shortGetHelp, longGetHelp, func() command {
    87  		return &getCommand{}
    88  	})
    89  }
    90  
    91  func (c *getCommand) printValues(getByKey func(string) (interface{}, bool, error)) error {
    92  	patch := make(map[string]interface{})
    93  	for _, key := range c.Positional.Keys {
    94  		value, output, err := getByKey(key)
    95  		if err == nil {
    96  			if output {
    97  				patch[key] = value
    98  			} // else skip this value
    99  		} else {
   100  			return err
   101  		}
   102  	}
   103  
   104  	var confToPrint interface{} = patch
   105  	if !c.Document && len(c.Positional.Keys) == 1 {
   106  		confToPrint = patch[c.Positional.Keys[0]]
   107  	}
   108  
   109  	if c.Typed && confToPrint == nil {
   110  		c.printf("null\n")
   111  		return nil
   112  	}
   113  
   114  	if s, ok := confToPrint.(string); ok && !c.Typed {
   115  		c.printf("%s\n", s)
   116  		return nil
   117  	}
   118  
   119  	var bytes []byte
   120  	if confToPrint != nil {
   121  		var err error
   122  		bytes, err = json.MarshalIndent(confToPrint, "", "\t")
   123  		if err != nil {
   124  			return err
   125  		}
   126  	}
   127  
   128  	c.printf("%s\n", string(bytes))
   129  
   130  	return nil
   131  }
   132  
   133  func (c *getCommand) Execute(args []string) error {
   134  	if len(c.Positional.Keys) == 0 && c.Positional.PlugOrSlotSpec == "" {
   135  		return fmt.Errorf(i18n.G("get which option?"))
   136  	}
   137  
   138  	context := c.context()
   139  	if context == nil {
   140  		return fmt.Errorf("cannot get without a context")
   141  	}
   142  
   143  	if c.Typed && c.Document {
   144  		return fmt.Errorf("cannot use -d and -t together")
   145  	}
   146  
   147  	if strings.Contains(c.Positional.PlugOrSlotSpec, ":") {
   148  		parts := strings.SplitN(c.Positional.PlugOrSlotSpec, ":", 2)
   149  		snap, name := parts[0], parts[1]
   150  		if name == "" {
   151  			return fmt.Errorf("plug or slot name not provided")
   152  		}
   153  		if snap != "" {
   154  			return fmt.Errorf(`"snapctl get %s" not supported, use "snapctl get :%s" instead`, c.Positional.PlugOrSlotSpec, parts[1])
   155  		}
   156  		if len(c.Positional.Keys) == 0 {
   157  			return fmt.Errorf(i18n.G("get which attribute?"))
   158  		}
   159  
   160  		return c.getInterfaceSetting(context, name)
   161  	}
   162  
   163  	// PlugOrSlotSpec is actually a configuration key.
   164  	c.Positional.Keys = append([]string{c.Positional.PlugOrSlotSpec}, c.Positional.Keys[0:]...)
   165  	c.Positional.PlugOrSlotSpec = ""
   166  
   167  	return c.getConfigSetting(context)
   168  }
   169  
   170  func (c *getCommand) getConfigSetting(context *hookstate.Context) error {
   171  	if c.ForcePlugSide || c.ForceSlotSide {
   172  		return fmt.Errorf("cannot use --plug or --slot without <snap>:<plug|slot> argument")
   173  	}
   174  
   175  	context.Lock()
   176  	transaction := configstate.ContextTransaction(context)
   177  	context.Unlock()
   178  
   179  	return c.printValues(func(key string) (interface{}, bool, error) {
   180  		var value interface{}
   181  		err := transaction.Get(c.context().InstanceName(), key, &value)
   182  		if err == nil {
   183  			return value, true, nil
   184  		}
   185  		if config.IsNoOption(err) {
   186  			if !c.Typed {
   187  				value = ""
   188  			}
   189  			return value, false, nil
   190  		}
   191  		return value, false, err
   192  	})
   193  }
   194  
   195  type ifaceHookType int
   196  
   197  const (
   198  	preparePlugHook ifaceHookType = iota
   199  	prepareSlotHook
   200  	unpreparePlugHook
   201  	unprepareSlotHook
   202  	connectPlugHook
   203  	connectSlotHook
   204  	disconnectPlugHook
   205  	disconnectSlotHook
   206  	unknownHook
   207  )
   208  
   209  func interfaceHookType(hookName string) (ifaceHookType, error) {
   210  	switch {
   211  	case strings.HasPrefix(hookName, "prepare-plug-"):
   212  		return preparePlugHook, nil
   213  	case strings.HasPrefix(hookName, "connect-plug-"):
   214  		return connectPlugHook, nil
   215  	case strings.HasPrefix(hookName, "prepare-slot-"):
   216  		return prepareSlotHook, nil
   217  	case strings.HasPrefix(hookName, "connect-slot-"):
   218  		return connectSlotHook, nil
   219  	case strings.HasPrefix(hookName, "disconnect-plug-"):
   220  		return disconnectPlugHook, nil
   221  	case strings.HasPrefix(hookName, "disconnect-slot-"):
   222  		return disconnectSlotHook, nil
   223  	case strings.HasPrefix(hookName, "unprepare-slot-"):
   224  		return unprepareSlotHook, nil
   225  	case strings.HasPrefix(hookName, "unprepare-plug-"):
   226  		return unpreparePlugHook, nil
   227  	default:
   228  		return unknownHook, fmt.Errorf("unknown hook type")
   229  	}
   230  }
   231  
   232  func validatePlugOrSlot(attrsTask *state.Task, plugSide bool, plugOrSlot string) error {
   233  	// check if the requested plug or slot is correct for given hook.
   234  	attrsTask.State().Lock()
   235  	defer attrsTask.State().Unlock()
   236  
   237  	var name string
   238  	var err error
   239  	if plugSide {
   240  		var plugRef interfaces.PlugRef
   241  		if err = attrsTask.Get("plug", &plugRef); err == nil {
   242  			name = plugRef.Name
   243  		}
   244  	} else {
   245  		var slotRef interfaces.SlotRef
   246  		if err = attrsTask.Get("slot", &slotRef); err == nil {
   247  			name = slotRef.Name
   248  		}
   249  	}
   250  	if err != nil {
   251  		return fmt.Errorf(i18n.G("internal error: cannot find plug or slot data in the appropriate task"))
   252  	}
   253  	if name != plugOrSlot {
   254  		return fmt.Errorf(i18n.G("unknown plug or slot %q"), plugOrSlot)
   255  	}
   256  	return nil
   257  }
   258  
   259  func attributesTask(context *hookstate.Context) (*state.Task, error) {
   260  	var attrsTaskID string
   261  	context.Lock()
   262  	defer context.Unlock()
   263  
   264  	if err := context.Get("attrs-task", &attrsTaskID); err != nil {
   265  		return nil, err
   266  	}
   267  
   268  	st := context.State()
   269  
   270  	attrsTask := st.Task(attrsTaskID)
   271  	if attrsTask == nil {
   272  		return nil, fmt.Errorf(i18n.G("internal error: cannot find attrs task"))
   273  	}
   274  
   275  	return attrsTask, nil
   276  }
   277  
   278  func (c *getCommand) getInterfaceSetting(context *hookstate.Context, plugOrSlot string) error {
   279  	// Make sure get :<plug|slot> is only supported during the execution of interface hooks
   280  	hookType, err := interfaceHookType(context.HookName())
   281  	if err != nil {
   282  		return fmt.Errorf(i18n.G("interface attributes can only be read during the execution of interface hooks"))
   283  	}
   284  
   285  	var attrsTask *state.Task
   286  	attrsTask, err = attributesTask(context)
   287  	if err != nil {
   288  		return err
   289  	}
   290  
   291  	if c.ForcePlugSide && c.ForceSlotSide {
   292  		return fmt.Errorf("cannot use --plug and --slot together")
   293  	}
   294  
   295  	isPlugSide := (hookType == preparePlugHook || hookType == unpreparePlugHook || hookType == connectPlugHook || hookType == disconnectPlugHook)
   296  	if err = validatePlugOrSlot(attrsTask, isPlugSide, plugOrSlot); err != nil {
   297  		return err
   298  	}
   299  
   300  	var which string
   301  	if c.ForcePlugSide || (isPlugSide && !c.ForceSlotSide) {
   302  		which = "plug"
   303  	} else {
   304  		which = "slot"
   305  	}
   306  
   307  	st := context.State()
   308  	st.Lock()
   309  	defer st.Unlock()
   310  
   311  	var staticAttrs, dynamicAttrs map[string]interface{}
   312  	if err = attrsTask.Get(which+"-static", &staticAttrs); err != nil {
   313  		return fmt.Errorf(i18n.G("internal error: cannot get %s from appropriate task"), which)
   314  	}
   315  	if err = attrsTask.Get(which+"-dynamic", &dynamicAttrs); err != nil {
   316  		return fmt.Errorf(i18n.G("internal error: cannot get %s from appropriate task"), which)
   317  	}
   318  
   319  	return c.printValues(func(key string) (interface{}, bool, error) {
   320  		subkeys, err := config.ParseKey(key)
   321  		if err != nil {
   322  			return nil, false, err
   323  		}
   324  
   325  		var value interface{}
   326  		err = getAttribute(context.InstanceName(), subkeys, 0, staticAttrs, &value)
   327  		if err == nil {
   328  			return value, true, nil
   329  		}
   330  		if isNoAttribute(err) {
   331  			err = getAttribute(context.InstanceName(), subkeys, 0, dynamicAttrs, &value)
   332  			if err == nil {
   333  				return value, true, nil
   334  			}
   335  		}
   336  		return nil, false, err
   337  	})
   338  }