github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/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 }