github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/overlord/hookstate/ctlcmd/set.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/jsonutil"
    29  	"github.com/snapcore/snapd/overlord/configstate"
    30  	"github.com/snapcore/snapd/overlord/configstate/config"
    31  	"github.com/snapcore/snapd/overlord/hookstate"
    32  )
    33  
    34  type setCommand struct {
    35  	baseCommand
    36  
    37  	Positional struct {
    38  		PlugOrSlotSpec string   `positional-arg-name:":<plug|slot>"`
    39  		ConfValues     []string `positional-arg-name:"key=value"`
    40  	} `positional-args:"yes"`
    41  }
    42  
    43  var shortSetHelp = i18n.G("Changes configuration options")
    44  var longSetHelp = i18n.G(`
    45  The set command changes the provided configuration options as requested.
    46  
    47      $ snapctl set username=frank password=$PASSWORD
    48  
    49  All configuration changes are persisted at once, and only after the hook
    50  returns successfully.
    51  
    52  Nested values may be modified via a dotted path:
    53  
    54      $ snapctl set author.name=frank
    55  
    56  Configuration option may be unset with exclamation mark:
    57      $ snapctl set author!
    58  
    59  Plug and slot attributes may be set in the respective prepare and connect hooks by
    60  naming the respective plug or slot:
    61  
    62      $ snapctl set :myplug path=/dev/ttyS0
    63  `)
    64  
    65  func init() {
    66  	addCommand("set", shortSetHelp, longSetHelp, func() command { return &setCommand{} })
    67  }
    68  
    69  func (s *setCommand) Execute(args []string) error {
    70  	if s.Positional.PlugOrSlotSpec == "" && len(s.Positional.ConfValues) == 0 {
    71  		return fmt.Errorf(i18n.G("set which option?"))
    72  	}
    73  
    74  	context := s.context()
    75  	if context == nil {
    76  		return fmt.Errorf("cannot set without a context")
    77  	}
    78  
    79  	// treat PlugOrSlotSpec argument as key=value if it contans '=' or doesn't contain ':' - this is to support
    80  	// values such as "device-service.url=192.168.0.1:5555" and error out on invalid key=value if only "key" is given.
    81  	if strings.Contains(s.Positional.PlugOrSlotSpec, "=") || !strings.Contains(s.Positional.PlugOrSlotSpec, ":") {
    82  		s.Positional.ConfValues = append([]string{s.Positional.PlugOrSlotSpec}, s.Positional.ConfValues[0:]...)
    83  		s.Positional.PlugOrSlotSpec = ""
    84  		return s.setConfigSetting(context)
    85  	}
    86  
    87  	parts := strings.SplitN(s.Positional.PlugOrSlotSpec, ":", 2)
    88  	snap, name := parts[0], parts[1]
    89  	if name == "" {
    90  		return fmt.Errorf("plug or slot name not provided")
    91  	}
    92  	if snap != "" {
    93  		return fmt.Errorf(`"snapctl set %s" not supported, use "snapctl set :%s" instead`, s.Positional.PlugOrSlotSpec, parts[1])
    94  	}
    95  	return s.setInterfaceSetting(context, name)
    96  }
    97  
    98  func (s *setCommand) setConfigSetting(context *hookstate.Context) error {
    99  	context.Lock()
   100  	tr := configstate.ContextTransaction(context)
   101  	context.Unlock()
   102  
   103  	for _, patchValue := range s.Positional.ConfValues {
   104  		parts := strings.SplitN(patchValue, "=", 2)
   105  		if len(parts) == 1 && strings.HasSuffix(patchValue, "!") {
   106  			key := strings.TrimSuffix(patchValue, "!")
   107  			tr.Set(s.context().InstanceName(), key, nil)
   108  			continue
   109  		}
   110  		if len(parts) != 2 {
   111  			return fmt.Errorf(i18n.G("invalid parameter: %q (want key=value)"), patchValue)
   112  		}
   113  		key := parts[0]
   114  		var value interface{}
   115  		if err := jsonutil.DecodeWithNumber(strings.NewReader(parts[1]), &value); err != nil {
   116  			// Not valid JSON-- just save the string as-is.
   117  			value = parts[1]
   118  		}
   119  
   120  		tr.Set(s.context().InstanceName(), key, value)
   121  	}
   122  
   123  	return nil
   124  }
   125  
   126  func setInterfaceAttribute(context *hookstate.Context, staticAttrs map[string]interface{}, dynamicAttrs map[string]interface{}, key string, value interface{}) error {
   127  	data, err := json.Marshal(value)
   128  	if err != nil {
   129  		return fmt.Errorf("cannot marshal snap %q option %q: %s", context.InstanceName(), key, err)
   130  	}
   131  	raw := json.RawMessage(data)
   132  
   133  	subkeys, err := config.ParseKey(key)
   134  	if err != nil {
   135  		return err
   136  	}
   137  
   138  	// We're called from setInterfaceSetting, subkeys is derived from key
   139  	// part of key=value argument and is guaranteed to be non-empty at this
   140  	// point.
   141  	if len(subkeys) == 0 {
   142  		return fmt.Errorf("internal error: unexpected empty subkeys for key %q", key)
   143  	}
   144  	var existing interface{}
   145  	err = getAttribute(context.InstanceName(), subkeys[:1], 0, staticAttrs, &existing)
   146  	if err == nil {
   147  		return fmt.Errorf(i18n.G("attribute %q cannot be overwritten"), key)
   148  	}
   149  	// we expect NoAttributeError here, any other error is unexpected (a real error)
   150  	if !isNoAttribute(err) {
   151  		return err
   152  	}
   153  
   154  	_, err = config.PatchConfig(context.InstanceName(), subkeys, 0, dynamicAttrs, &raw)
   155  	return err
   156  }
   157  
   158  func (s *setCommand) setInterfaceSetting(context *hookstate.Context, plugOrSlot string) error {
   159  	// Make sure set :<plug|slot> is only supported during the execution of prepare-[plug|slot] hooks
   160  	hookType, _ := interfaceHookType(context.HookName())
   161  	if hookType != preparePlugHook && hookType != prepareSlotHook {
   162  		return fmt.Errorf(i18n.G("interface attributes can only be set during the execution of prepare hooks"))
   163  	}
   164  
   165  	attrsTask, err := attributesTask(context)
   166  	if err != nil {
   167  		return err
   168  	}
   169  
   170  	// check if the requested plug or slot is correct for this hook.
   171  	if err := validatePlugOrSlot(attrsTask, hookType == preparePlugHook, plugOrSlot); err != nil {
   172  		return err
   173  	}
   174  
   175  	var which string
   176  	if hookType == preparePlugHook {
   177  		which = "plug"
   178  	} else {
   179  		which = "slot"
   180  	}
   181  
   182  	context.Lock()
   183  	defer context.Unlock()
   184  
   185  	var staticAttrs, dynamicAttrs map[string]interface{}
   186  	if err = attrsTask.Get(which+"-static", &staticAttrs); err != nil {
   187  		return fmt.Errorf(i18n.G("internal error: cannot get %s from appropriate task, %s"), which, err)
   188  	}
   189  
   190  	dynKey := which + "-dynamic"
   191  	if err = attrsTask.Get(dynKey, &dynamicAttrs); err != nil {
   192  		return fmt.Errorf(i18n.G("internal error: cannot get %s from appropriate task, %s"), which, err)
   193  	}
   194  
   195  	for _, attrValue := range s.Positional.ConfValues {
   196  		parts := strings.SplitN(attrValue, "=", 2)
   197  		if len(parts) != 2 {
   198  			return fmt.Errorf(i18n.G("invalid parameter: %q (want key=value)"), attrValue)
   199  		}
   200  
   201  		var value interface{}
   202  		if err := jsonutil.DecodeWithNumber(strings.NewReader(parts[1]), &value); err != nil {
   203  			// Not valid JSON, save the string as-is
   204  			value = parts[1]
   205  		}
   206  		err = setInterfaceAttribute(context, staticAttrs, dynamicAttrs, parts[0], value)
   207  		if err != nil {
   208  			return fmt.Errorf(i18n.G("cannot set attribute: %v"), err)
   209  		}
   210  	}
   211  
   212  	attrsTask.Set(dynKey, dynamicAttrs)
   213  	return nil
   214  }