github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/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", 75 "device-key-sha3-384", 76 "device-key", 77 } 78 ) 79 80 type cmdModel struct { 81 clientMixin 82 timeMixin 83 colorMixin 84 85 Serial bool `long:"serial"` 86 Verbose bool `long:"verbose"` 87 Assertion bool `long:"assertion"` 88 } 89 90 func init() { 91 addCommand("model", 92 shortModelHelp, 93 longModelHelp, 94 func() flags.Commander { 95 return &cmdModel{} 96 }, colorDescs.also(timeDescs).also(map[string]string{ 97 "assertion": i18n.G("Print the raw assertion."), 98 "verbose": i18n.G("Print all specific assertion fields."), 99 "serial": i18n.G( 100 "Print the serial assertion instead of the model assertion."), 101 }), 102 []argDesc{}, 103 ) 104 } 105 106 func (x *cmdModel) Execute(args []string) error { 107 if x.Verbose && x.Assertion { 108 // can't do a verbose mode for the assertion 109 return errNoVerboseAssertion 110 } 111 112 var mainAssertion asserts.Assertion 113 serialAssertion, serialErr := x.client.CurrentSerialAssertion() 114 modelAssertion, modelErr := x.client.CurrentModelAssertion() 115 116 // if we didn't get a model assertion bail early 117 if modelErr != nil { 118 if client.IsAssertionNotFoundError(modelErr) { 119 // device is not registered yet - use specific error message 120 return errNoMainAssertion 121 } 122 return modelErr 123 } 124 125 // if the serial assertion error is anything other than not found, also 126 // bail early 127 // the serial assertion not being found may not be fatal 128 if serialErr != nil && !client.IsAssertionNotFoundError(serialErr) { 129 return serialErr 130 } 131 132 if x.Serial { 133 mainAssertion = serialAssertion 134 } else { 135 mainAssertion = modelAssertion 136 } 137 138 if x.Assertion { 139 // if we are using the serial assertion and we specifically didn't find the 140 // serial assertion, bail with specific error 141 if x.Serial && client.IsAssertionNotFoundError(serialErr) { 142 return errNoMainAssertion 143 } 144 145 _, err := Stdout.Write(asserts.Encode(mainAssertion)) 146 return err 147 } 148 149 termWidth, _ := termSize() 150 termWidth -= 3 151 if termWidth > 100 { 152 // any wider than this and it gets hard to read 153 termWidth = 100 154 } 155 156 esc := x.getEscapes() 157 158 w := tabWriter() 159 160 if x.Serial && client.IsAssertionNotFoundError(serialErr) { 161 // for serial assertion, the primary keys are output (model and 162 // brand-id), but if we didn't find the serial assertion then we still 163 // output the brand-id and model from the model assertion, but also 164 // return a devNotReady error 165 fmt.Fprintf(w, "brand-id:\t%s\n", modelAssertion.HeaderString("brand-id")) 166 fmt.Fprintf(w, "model:\t%s\n", modelAssertion.HeaderString("model")) 167 w.Flush() 168 return errNoSerial 169 } 170 171 // the rest of this function is the main flow for outputting either the 172 // model or serial assertion in normal or verbose mode 173 174 // for the `snap model` case with no options, we don't want colons, we want 175 // to be like `snap version` 176 separator := ":" 177 if !x.Verbose && !x.Serial { 178 separator = "" 179 } 180 181 // ordering of the primary keys for model: brand, model, serial 182 // ordering of primary keys for serial is brand-id, model, serial 183 184 // output brand/brand-id 185 brandIDHeader := mainAssertion.HeaderString("brand-id") 186 modelHeader := mainAssertion.HeaderString("model") 187 // for the serial header, if there's no serial yet, it's not an error for 188 // model (and we already handled the serial error above) but need to add a 189 // parenthetical about the device not being registered yet 190 var serial string 191 if client.IsAssertionNotFoundError(serialErr) { 192 if x.Verbose || x.Serial { 193 // verbose and serial are yamlish, so we need to escape the dash 194 serial = esc.dash 195 } else { 196 serial = "-" 197 } 198 serial += " (device not registered yet)" 199 } else { 200 serial = serialAssertion.HeaderString("serial") 201 } 202 203 // handle brand/brand-id and model/model + display-name differently on just 204 // `snap model` w/o opts 205 if x.Serial || x.Verbose { 206 fmt.Fprintf(w, "brand-id:\t%s\n", brandIDHeader) 207 fmt.Fprintf(w, "model:\t%s\n", modelHeader) 208 } else { 209 // for the model command (not --serial) we want to show a publisher 210 // style display of "brand" instead of just "brand-id" 211 storeAccount, err := x.client.StoreAccount(brandIDHeader) 212 if err != nil { 213 return err 214 } 215 // use the longPublisher helper to format the brand store account 216 // like we do in `snap info` 217 fmt.Fprintf(w, "brand%s\t%s\n", separator, longPublisher(x.getEscapes(), storeAccount)) 218 219 // for model, if there's a display-name, we show that first with the 220 // real model in parenthesis 221 if displayName := modelAssertion.HeaderString("display-name"); displayName != "" { 222 modelHeader = fmt.Sprintf("%s (%s)", displayName, modelHeader) 223 } 224 fmt.Fprintf(w, "model%s\t%s\n", separator, modelHeader) 225 } 226 227 // only output the grade if it is non-empty, either it is not in the model 228 // assertion for all non-uc20 model assertions, or it is non-empty and 229 // required for uc20 model assertions 230 grade := modelAssertion.HeaderString("grade") 231 if grade != "" { 232 fmt.Fprintf(w, "grade%s\t%s\n", separator, grade) 233 } 234 235 // serial is same for all variants 236 fmt.Fprintf(w, "serial%s\t%s\n", separator, serial) 237 238 // --verbose means output more information 239 if x.Verbose { 240 allHeadersMap := mainAssertion.Headers() 241 242 for _, headerName := range niceOrdering { 243 invalidTypeErr := fmt.Errorf(invalidTypeMessage, headerName) 244 245 headerValue, ok := allHeadersMap[headerName] 246 // make sure the header is in the map 247 if !ok { 248 continue 249 } 250 251 // switch on which header it is to handle some special cases 252 switch headerName { 253 // list of scalars 254 case "required-snaps", "system-user-authority": 255 headerIfaceList, ok := headerValue.([]interface{}) 256 if !ok { 257 return invalidTypeErr 258 } 259 if len(headerIfaceList) == 0 { 260 continue 261 } 262 fmt.Fprintf(w, "%s:\t\n", headerName) 263 for _, elem := range headerIfaceList { 264 headerStringElem, ok := elem.(string) 265 if !ok { 266 return invalidTypeErr 267 } 268 // note we don't wrap these, since for now this is 269 // specifically just required-snaps and so all of these 270 // will be snap names which are required to be short 271 fmt.Fprintf(w, " - %s\n", headerStringElem) 272 } 273 274 //timestamp needs to be formatted with fmtTime from the timeMixin 275 case "timestamp": 276 timestamp, ok := headerValue.(string) 277 if !ok { 278 return invalidTypeErr 279 } 280 281 // parse the time string as RFC3339, which is what the format is 282 // always in for assertions 283 t, err := time.Parse(time.RFC3339, timestamp) 284 if err != nil { 285 return err 286 } 287 fmt.Fprintf(w, "timestamp:\t%s\n", x.fmtTime(t)) 288 289 // long string key we don't want to rewrap but can safely handle 290 // on "reasonable" width terminals 291 case "device-key-sha3-384": 292 // also flush the writer before continuing so the previous keys 293 // don't try to align with this key 294 w.Flush() 295 headerString, ok := headerValue.(string) 296 if !ok { 297 return invalidTypeErr 298 } 299 300 switch { 301 case termWidth > 86: 302 fmt.Fprintf(w, "device-key-sha3-384: %s\n", headerString) 303 case termWidth <= 86 && termWidth > 66: 304 fmt.Fprintln(w, "device-key-sha3-384: |") 305 wrapLine(w, []rune(headerString), " ", termWidth) 306 } 307 308 // long base64 key we can rewrap safely 309 case "device-key": 310 headerString, ok := headerValue.(string) 311 if !ok { 312 return invalidTypeErr 313 } 314 // the string value here has newlines inserted as part of the 315 // raw assertion, but base64 doesn't care about whitespace, so 316 // it's safe to split by newlines and re-wrap to make it 317 // prettier 318 headerString = strings.Join( 319 strings.Split(headerString, "\n"), 320 "") 321 fmt.Fprintln(w, "device-key: |") 322 wrapLine(w, []rune(headerString), " ", termWidth) 323 324 // the default is all the rest of short scalar values, which all 325 // should be strings 326 default: 327 headerString, ok := headerValue.(string) 328 if !ok { 329 return invalidTypeErr 330 } 331 fmt.Fprintf(w, "%s:\t%s\n", headerName, headerString) 332 } 333 } 334 } 335 336 return w.Flush() 337 }