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  }