github.com/tompreston/snapd@v0.0.0-20210817193607-954edfcb9611/cmd/snap/cmd_model.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2019 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 main 21 22 import ( 23 "errors" 24 "fmt" 25 "strings" 26 "time" 27 28 "github.com/jessevdk/go-flags" 29 30 "github.com/snapcore/snapd/asserts" 31 "github.com/snapcore/snapd/client" 32 "github.com/snapcore/snapd/i18n" 33 ) 34 35 var ( 36 shortModelHelp = i18n.G("Get the active model for this device") 37 longModelHelp = i18n.G(` 38 The model command returns the active model assertion information for this 39 device. 40 41 By default, only the essential model identification information is 42 included in the output, but this can be expanded to include all of an 43 assertion's non-meta headers. 44 45 The verbose output is presented in a structured, yaml-like format. 46 47 Similarly, the active serial assertion can be used for the output instead of the 48 model assertion. 49 `) 50 51 invalidTypeMessage = i18n.G("invalid type for %q header") 52 errNoMainAssertion = errors.New(i18n.G("device not ready yet (no assertions found)")) 53 errNoSerial = errors.New(i18n.G("device not registered yet (no serial assertion found)")) 54 errNoVerboseAssertion = errors.New(i18n.G("cannot use --verbose with --assertion")) 55 56 // this list is a "nice" "human" "readable" "ordering" of headers to print 57 // off, sorted in lexical order with meta headers and primary key headers 58 // removed, and big nasty keys such as device-key-sha3-384 and 59 // device-key at the bottom 60 // it also contains both serial and model assertion headers, but we 61 // follow the same code path for both assertion types and some of the 62 // headers are shared between the two, so it still works out correctly 63 niceOrdering = [...]string{ 64 "architecture", 65 "base", 66 "classic", 67 "display-name", 68 "gadget", 69 "kernel", 70 "revision", 71 "store", 72 "system-user-authority", 73 "timestamp", 74 "required-snaps", // for uc16 and uc18 models 75 "snaps", // for uc20 models 76 "device-key-sha3-384", 77 "device-key", 78 } 79 ) 80 81 type cmdModel struct { 82 clientMixin 83 timeMixin 84 colorMixin 85 86 Serial bool `long:"serial"` 87 Verbose bool `long:"verbose"` 88 Assertion bool `long:"assertion"` 89 } 90 91 func init() { 92 addCommand("model", 93 shortModelHelp, 94 longModelHelp, 95 func() flags.Commander { 96 return &cmdModel{} 97 }, colorDescs.also(timeDescs).also(map[string]string{ 98 "assertion": i18n.G("Print the raw assertion."), 99 "verbose": i18n.G("Print all specific assertion fields."), 100 "serial": i18n.G( 101 "Print the serial assertion instead of the model assertion."), 102 }), 103 []argDesc{}, 104 ) 105 } 106 107 func (x *cmdModel) Execute(args []string) error { 108 if x.Verbose && x.Assertion { 109 // can't do a verbose mode for the assertion 110 return errNoVerboseAssertion 111 } 112 113 var mainAssertion asserts.Assertion 114 serialAssertion, serialErr := x.client.CurrentSerialAssertion() 115 modelAssertion, modelErr := x.client.CurrentModelAssertion() 116 117 // if we didn't get a model assertion bail early 118 if modelErr != nil { 119 if client.IsAssertionNotFoundError(modelErr) { 120 // device is not registered yet - use specific error message 121 return errNoMainAssertion 122 } 123 return modelErr 124 } 125 126 // if the serial assertion error is anything other than not found, also 127 // bail early 128 // the serial assertion not being found may not be fatal 129 if serialErr != nil && !client.IsAssertionNotFoundError(serialErr) { 130 return serialErr 131 } 132 133 if x.Serial { 134 mainAssertion = serialAssertion 135 } else { 136 mainAssertion = modelAssertion 137 } 138 139 if x.Assertion { 140 // if we are using the serial assertion and we specifically didn't find the 141 // serial assertion, bail with specific error 142 if x.Serial && client.IsAssertionNotFoundError(serialErr) { 143 return errNoMainAssertion 144 } 145 146 _, err := Stdout.Write(asserts.Encode(mainAssertion)) 147 return err 148 } 149 150 termWidth, _ := termSize() 151 termWidth -= 3 152 if termWidth > 100 { 153 // any wider than this and it gets hard to read 154 termWidth = 100 155 } 156 157 esc := x.getEscapes() 158 159 w := tabWriter() 160 161 if x.Serial && client.IsAssertionNotFoundError(serialErr) { 162 // for serial assertion, the primary keys are output (model and 163 // brand-id), but if we didn't find the serial assertion then we still 164 // output the brand-id and model from the model assertion, but also 165 // return a devNotReady error 166 fmt.Fprintf(w, "brand-id:\t%s\n", modelAssertion.HeaderString("brand-id")) 167 fmt.Fprintf(w, "model:\t%s\n", modelAssertion.HeaderString("model")) 168 w.Flush() 169 return errNoSerial 170 } 171 172 // the rest of this function is the main flow for outputting either the 173 // model or serial assertion in normal or verbose mode 174 175 // for the `snap model` case with no options, we don't want colons, we want 176 // to be like `snap version` 177 separator := ":" 178 if !x.Verbose && !x.Serial { 179 separator = "" 180 } 181 182 // ordering of the primary keys for model: brand, model, serial 183 // ordering of primary keys for serial is brand-id, model, serial 184 185 // output brand/brand-id 186 brandIDHeader := mainAssertion.HeaderString("brand-id") 187 modelHeader := mainAssertion.HeaderString("model") 188 // for the serial header, if there's no serial yet, it's not an error for 189 // model (and we already handled the serial error above) but need to add a 190 // parenthetical about the device not being registered yet 191 var serial string 192 if client.IsAssertionNotFoundError(serialErr) { 193 if x.Verbose || x.Serial { 194 // verbose and serial are yamlish, so we need to escape the dash 195 serial = esc.dash 196 } else { 197 serial = "-" 198 } 199 serial += " (device not registered yet)" 200 } else { 201 serial = serialAssertion.HeaderString("serial") 202 } 203 204 // handle brand/brand-id and model/model + display-name differently on just 205 // `snap model` w/o opts 206 if x.Serial || x.Verbose { 207 fmt.Fprintf(w, "brand-id:\t%s\n", brandIDHeader) 208 fmt.Fprintf(w, "model:\t%s\n", modelHeader) 209 } else { 210 // for the model command (not --serial) we want to show a publisher 211 // style display of "brand" instead of just "brand-id" 212 storeAccount, err := x.client.StoreAccount(brandIDHeader) 213 if err != nil { 214 return err 215 } 216 // use the longPublisher helper to format the brand store account 217 // like we do in `snap info` 218 fmt.Fprintf(w, "brand%s\t%s\n", separator, longPublisher(x.getEscapes(), storeAccount)) 219 220 // for model, if there's a display-name, we show that first with the 221 // real model in parenthesis 222 if displayName := modelAssertion.HeaderString("display-name"); displayName != "" { 223 modelHeader = fmt.Sprintf("%s (%s)", displayName, modelHeader) 224 } 225 fmt.Fprintf(w, "model%s\t%s\n", separator, modelHeader) 226 } 227 228 // only output the grade if it is non-empty, either it is not in the model 229 // assertion for all non-uc20 model assertions, or it is non-empty and 230 // required for uc20 model assertions 231 grade := modelAssertion.HeaderString("grade") 232 if grade != "" { 233 fmt.Fprintf(w, "grade%s\t%s\n", separator, grade) 234 } 235 236 storageSafety := modelAssertion.HeaderString("storage-safety") 237 if storageSafety != "" { 238 fmt.Fprintf(w, "storage-safety%s\t%s\n", separator, storageSafety) 239 } 240 241 // serial is same for all variants 242 fmt.Fprintf(w, "serial%s\t%s\n", separator, serial) 243 244 // --verbose means output more information 245 if x.Verbose { 246 allHeadersMap := mainAssertion.Headers() 247 248 for _, headerName := range niceOrdering { 249 invalidTypeErr := fmt.Errorf(invalidTypeMessage, headerName) 250 251 headerValue, ok := allHeadersMap[headerName] 252 // make sure the header is in the map 253 if !ok { 254 continue 255 } 256 257 // switch on which header it is to handle some special cases 258 switch headerName { 259 // list of scalars 260 case "required-snaps", "system-user-authority": 261 headerIfaceList, ok := headerValue.([]interface{}) 262 if !ok { 263 return invalidTypeErr 264 } 265 if len(headerIfaceList) == 0 { 266 continue 267 } 268 fmt.Fprintf(w, "%s:\t\n", headerName) 269 for _, elem := range headerIfaceList { 270 headerStringElem, ok := elem.(string) 271 if !ok { 272 return invalidTypeErr 273 } 274 // note we don't wrap these, since for now this is 275 // specifically just required-snaps and so all of these 276 // will be snap names which are required to be short 277 fmt.Fprintf(w, " - %s\n", headerStringElem) 278 } 279 280 //timestamp needs to be formatted with fmtTime from the timeMixin 281 case "timestamp": 282 timestamp, ok := headerValue.(string) 283 if !ok { 284 return invalidTypeErr 285 } 286 287 // parse the time string as RFC3339, which is what the format is 288 // always in for assertions 289 t, err := time.Parse(time.RFC3339, timestamp) 290 if err != nil { 291 return err 292 } 293 fmt.Fprintf(w, "timestamp:\t%s\n", x.fmtTime(t)) 294 295 // long string key we don't want to rewrap but can safely handle 296 // on "reasonable" width terminals 297 case "device-key-sha3-384": 298 // also flush the writer before continuing so the previous keys 299 // don't try to align with this key 300 w.Flush() 301 headerString, ok := headerValue.(string) 302 if !ok { 303 return invalidTypeErr 304 } 305 306 switch { 307 case termWidth > 86: 308 fmt.Fprintf(w, "device-key-sha3-384: %s\n", headerString) 309 case termWidth <= 86 && termWidth > 66: 310 fmt.Fprintln(w, "device-key-sha3-384: |") 311 wrapLine(w, []rune(headerString), " ", termWidth) 312 } 313 case "snaps": 314 // also flush the writer before continuing so the previous keys 315 // don't try to align with this key 316 w.Flush() 317 snapsHeader, ok := headerValue.([]interface{}) 318 if !ok { 319 return invalidTypeErr 320 } 321 if len(snapsHeader) == 0 { 322 // unexpected why this is an empty list, but just ignore for 323 // now 324 continue 325 } 326 fmt.Fprintf(w, "snaps:\n") 327 for _, sn := range snapsHeader { 328 snMap, ok := sn.(map[string]interface{}) 329 if !ok { 330 return invalidTypeErr 331 } 332 // iterate over all keys in the map in a stable, visually 333 // appealing ordering 334 // first do snap name, which will always be present since we 335 // parsed a valid assertion 336 name := snMap["name"].(string) 337 fmt.Fprintf(w, " - name:\t%s\n", name) 338 339 // the rest of these may be absent, but they are all still 340 // simple strings 341 for _, snKey := range []string{"id", "type", "default-channel", "presence"} { 342 snValue, ok := snMap[snKey] 343 if !ok { 344 continue 345 } 346 snStrValue, ok := snValue.(string) 347 if !ok { 348 return invalidTypeErr 349 } 350 if snStrValue != "" { 351 fmt.Fprintf(w, " %s:\t%s\n", snKey, snStrValue) 352 } 353 } 354 355 // finally handle "modes" which is a list 356 modes, ok := snMap["modes"] 357 if !ok { 358 continue 359 } 360 modesSlice, ok := modes.([]interface{}) 361 if !ok { 362 return invalidTypeErr 363 } 364 if len(modesSlice) == 0 { 365 continue 366 } 367 368 modeStrSlice := make([]string, 0, len(modesSlice)) 369 for _, mode := range modesSlice { 370 modeStr, ok := mode.(string) 371 if !ok { 372 return invalidTypeErr 373 } 374 modeStrSlice = append(modeStrSlice, modeStr) 375 } 376 modesSliceYamlStr := "[" + strings.Join(modeStrSlice, ", ") + "]" 377 fmt.Fprintf(w, " modes:\t%s\n", modesSliceYamlStr) 378 } 379 380 // long base64 key we can rewrap safely 381 case "device-key": 382 headerString, ok := headerValue.(string) 383 if !ok { 384 return invalidTypeErr 385 } 386 // the string value here has newlines inserted as part of the 387 // raw assertion, but base64 doesn't care about whitespace, so 388 // it's safe to split by newlines and re-wrap to make it 389 // prettier 390 headerString = strings.Join( 391 strings.Split(headerString, "\n"), 392 "") 393 fmt.Fprintln(w, "device-key: |") 394 wrapLine(w, []rune(headerString), " ", termWidth) 395 396 // the default is all the rest of short scalar values, which all 397 // should be strings 398 default: 399 headerString, ok := headerValue.(string) 400 if !ok { 401 return invalidTypeErr 402 } 403 fmt.Fprintf(w, "%s:\t%s\n", headerName, headerString) 404 } 405 } 406 } 407 408 return w.Flush() 409 }