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  }