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