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