github.com/Lephar/snapd@v0.0.0-20210825215435-c7fba9cef4d2/client/client.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2015-2020 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 client
    21  
    22  import (
    23  	"bytes"
    24  	"context"
    25  	"encoding/json"
    26  	"fmt"
    27  	"io"
    28  	"io/ioutil"
    29  	"net"
    30  	"net/http"
    31  	"net/url"
    32  	"os"
    33  	"path"
    34  	"strconv"
    35  	"time"
    36  
    37  	"github.com/snapcore/snapd/dirs"
    38  	"github.com/snapcore/snapd/jsonutil"
    39  )
    40  
    41  func unixDialer(socketPath string) func(string, string) (net.Conn, error) {
    42  	if socketPath == "" {
    43  		socketPath = dirs.SnapdSocket
    44  	}
    45  	return func(_, _ string) (net.Conn, error) {
    46  		return net.Dial("unix", socketPath)
    47  	}
    48  }
    49  
    50  type doer interface {
    51  	Do(*http.Request) (*http.Response, error)
    52  }
    53  
    54  // Config allows to customize client behavior.
    55  type Config struct {
    56  	// BaseURL contains the base URL where snappy daemon is expected to be.
    57  	// It can be empty for a default behavior of talking over a unix socket.
    58  	BaseURL string
    59  
    60  	// DisableAuth controls whether the client should send an
    61  	// Authorization header from reading the auth.json data.
    62  	DisableAuth bool
    63  
    64  	// Interactive controls whether the client runs in interactive mode.
    65  	// At present, this only affects whether interactive polkit
    66  	// authorisation is requested.
    67  	Interactive bool
    68  
    69  	// Socket is the path to the unix socket to use
    70  	Socket string
    71  
    72  	// DisableKeepAlive indicates whether the connections should not be kept
    73  	// alive for later reuse
    74  	DisableKeepAlive bool
    75  
    76  	// User-Agent to sent to the snapd daemon
    77  	UserAgent string
    78  }
    79  
    80  // A Client knows how to talk to the snappy daemon.
    81  type Client struct {
    82  	baseURL url.URL
    83  	doer    doer
    84  
    85  	disableAuth bool
    86  	interactive bool
    87  
    88  	maintenance error
    89  
    90  	warningCount     int
    91  	warningTimestamp time.Time
    92  
    93  	userAgent string
    94  }
    95  
    96  // New returns a new instance of Client
    97  func New(config *Config) *Client {
    98  	if config == nil {
    99  		config = &Config{}
   100  	}
   101  
   102  	// By default talk over an UNIX socket.
   103  	if config.BaseURL == "" {
   104  		transport := &http.Transport{Dial: unixDialer(config.Socket), DisableKeepAlives: config.DisableKeepAlive}
   105  		return &Client{
   106  			baseURL: url.URL{
   107  				Scheme: "http",
   108  				Host:   "localhost",
   109  			},
   110  			doer:        &http.Client{Transport: transport},
   111  			disableAuth: config.DisableAuth,
   112  			interactive: config.Interactive,
   113  			userAgent:   config.UserAgent,
   114  		}
   115  	}
   116  
   117  	baseURL, err := url.Parse(config.BaseURL)
   118  	if err != nil {
   119  		panic(fmt.Sprintf("cannot parse server base URL: %q (%v)", config.BaseURL, err))
   120  	}
   121  	return &Client{
   122  		baseURL:     *baseURL,
   123  		doer:        &http.Client{Transport: &http.Transport{DisableKeepAlives: config.DisableKeepAlive}},
   124  		disableAuth: config.DisableAuth,
   125  		interactive: config.Interactive,
   126  		userAgent:   config.UserAgent,
   127  	}
   128  }
   129  
   130  // Maintenance returns an error reflecting the daemon maintenance status or nil.
   131  func (client *Client) Maintenance() error {
   132  	return client.maintenance
   133  }
   134  
   135  // WarningsSummary returns the number of warnings that are ready to be shown to
   136  // the user, and the timestamp of the most recently added warning (useful for
   137  // silencing the warning alerts, and OKing the returned warnings).
   138  func (client *Client) WarningsSummary() (count int, timestamp time.Time) {
   139  	return client.warningCount, client.warningTimestamp
   140  }
   141  
   142  func (client *Client) WhoAmI() (string, error) {
   143  	user, err := readAuthData()
   144  	if os.IsNotExist(err) {
   145  		return "", nil
   146  	}
   147  	if err != nil {
   148  		return "", err
   149  	}
   150  
   151  	return user.Email, nil
   152  }
   153  
   154  func (client *Client) setAuthorization(req *http.Request) error {
   155  	user, err := readAuthData()
   156  	if os.IsNotExist(err) {
   157  		return nil
   158  	}
   159  	if err != nil {
   160  		return err
   161  	}
   162  
   163  	var buf bytes.Buffer
   164  	fmt.Fprintf(&buf, `Macaroon root="%s"`, user.Macaroon)
   165  	for _, discharge := range user.Discharges {
   166  		fmt.Fprintf(&buf, `, discharge="%s"`, discharge)
   167  	}
   168  	req.Header.Set("Authorization", buf.String())
   169  	return nil
   170  }
   171  
   172  type RequestError struct{ error }
   173  
   174  func (e RequestError) Error() string {
   175  	return fmt.Sprintf("cannot build request: %v", e.error)
   176  }
   177  
   178  type AuthorizationError struct{ error }
   179  
   180  func (e AuthorizationError) Error() string {
   181  	return fmt.Sprintf("cannot add authorization: %v", e.error)
   182  }
   183  
   184  type ConnectionError struct{ Err error }
   185  
   186  func (e ConnectionError) Error() string {
   187  	var errStr string
   188  	switch e.Err {
   189  	case context.DeadlineExceeded:
   190  		errStr = "timeout exceeded while waiting for response"
   191  	case context.Canceled:
   192  		errStr = "request canceled"
   193  	default:
   194  		errStr = e.Err.Error()
   195  	}
   196  	return fmt.Sprintf("cannot communicate with server: %s", errStr)
   197  }
   198  
   199  func (e ConnectionError) Unwrap() error {
   200  	return e.Err
   201  }
   202  
   203  // AllowInteractionHeader is the HTTP request header used to indicate
   204  // that the client is willing to allow interaction.
   205  const AllowInteractionHeader = "X-Allow-Interaction"
   206  
   207  // raw performs a request and returns the resulting http.Response and
   208  // error. You usually only need to call this directly if you expect the
   209  // response to not be JSON, otherwise you'd call Do(...) instead.
   210  func (client *Client) raw(ctx context.Context, method, urlpath string, query url.Values, headers map[string]string, body io.Reader) (*http.Response, error) {
   211  	// fake a url to keep http.Client happy
   212  	u := client.baseURL
   213  	u.Path = path.Join(client.baseURL.Path, urlpath)
   214  	u.RawQuery = query.Encode()
   215  	req, err := http.NewRequest(method, u.String(), body)
   216  	if err != nil {
   217  		return nil, RequestError{err}
   218  	}
   219  	if client.userAgent != "" {
   220  		req.Header.Set("User-Agent", client.userAgent)
   221  	}
   222  
   223  	for key, value := range headers {
   224  		req.Header.Set(key, value)
   225  	}
   226  	// Content-length headers are special and need to be set
   227  	// directly to the request. Just setting it to the header
   228  	// will be ignored by go http.
   229  	if clStr := req.Header.Get("Content-Length"); clStr != "" {
   230  		cl, err := strconv.ParseInt(clStr, 10, 64)
   231  		if err != nil {
   232  			return nil, err
   233  		}
   234  		req.ContentLength = cl
   235  	}
   236  
   237  	if !client.disableAuth {
   238  		// set Authorization header if there are user's credentials
   239  		err = client.setAuthorization(req)
   240  		if err != nil {
   241  			return nil, AuthorizationError{err}
   242  		}
   243  	}
   244  
   245  	if client.interactive {
   246  		req.Header.Set(AllowInteractionHeader, "true")
   247  	}
   248  
   249  	if ctx != nil {
   250  		req = req.WithContext(ctx)
   251  	}
   252  
   253  	rsp, err := client.doer.Do(req)
   254  	if err != nil {
   255  		return nil, ConnectionError{err}
   256  	}
   257  
   258  	return rsp, nil
   259  }
   260  
   261  // rawWithTimeout is like raw(), but sets a timeout based on opts for
   262  // the whole of request and response (including rsp.Body() read) round
   263  // trip. If opts is nil the default doTimeout is used.
   264  // The caller is responsible for canceling the internal context
   265  // to release the resources associated with the request by calling the
   266  // returned cancel function.
   267  func (client *Client) rawWithTimeout(ctx context.Context, method, urlpath string, query url.Values, headers map[string]string, body io.Reader, opts *doOptions) (*http.Response, context.CancelFunc, error) {
   268  	opts = ensureDoOpts(opts)
   269  	if opts.Timeout <= 0 {
   270  		return nil, nil, fmt.Errorf("internal error: timeout not set in options for rawWithTimeout")
   271  	}
   272  
   273  	ctx, cancel := context.WithTimeout(ctx, opts.Timeout)
   274  	rsp, err := client.raw(ctx, method, urlpath, query, headers, body)
   275  	if err != nil && ctx.Err() != nil {
   276  		cancel()
   277  		return nil, nil, ConnectionError{ctx.Err()}
   278  	}
   279  
   280  	return rsp, cancel, err
   281  }
   282  
   283  var (
   284  	doRetry = 250 * time.Millisecond
   285  	// snapd may need to reach out to the store, where it uses a fixed 10s
   286  	// timeout for the whole of a single request to complete, requests are
   287  	// retried for up to 38s in total, make sure that the client timeout is
   288  	// not shorter than that
   289  	doTimeout = 120 * time.Second
   290  )
   291  
   292  // MockDoTimings mocks the delay used by the do retry loop and request timeout.
   293  func MockDoTimings(retry, timeout time.Duration) (restore func()) {
   294  	oldRetry := doRetry
   295  	oldTimeout := doTimeout
   296  	doRetry = retry
   297  	doTimeout = timeout
   298  	return func() {
   299  		doRetry = oldRetry
   300  		doTimeout = oldTimeout
   301  	}
   302  }
   303  
   304  type hijacked struct {
   305  	do func(*http.Request) (*http.Response, error)
   306  }
   307  
   308  func (h hijacked) Do(req *http.Request) (*http.Response, error) {
   309  	return h.do(req)
   310  }
   311  
   312  // Hijack lets the caller take over the raw http request
   313  func (client *Client) Hijack(f func(*http.Request) (*http.Response, error)) {
   314  	client.doer = hijacked{f}
   315  }
   316  
   317  type doOptions struct {
   318  	// Timeout is the overall request timeout
   319  	Timeout time.Duration
   320  	// Retry interval.
   321  	// Note for a request with a Timeout but without a retry, Retry should just
   322  	// be set to something larger than the Timeout.
   323  	Retry time.Duration
   324  }
   325  
   326  func ensureDoOpts(opts *doOptions) *doOptions {
   327  	if opts == nil {
   328  		// defaults
   329  		opts = &doOptions{
   330  			Timeout: doTimeout,
   331  			Retry:   doRetry,
   332  		}
   333  	}
   334  	return opts
   335  }
   336  
   337  // doNoTimeoutAndRetry can be passed to the do family to not have timeout
   338  // nor retries.
   339  var doNoTimeoutAndRetry = &doOptions{
   340  	Timeout: time.Duration(-1),
   341  }
   342  
   343  // do performs a request and decodes the resulting json into the given
   344  // value. It's low-level, for testing/experimenting only; you should
   345  // usually use a higher level interface that builds on this.
   346  func (client *Client) do(method, path string, query url.Values, headers map[string]string, body io.Reader, v interface{}, opts *doOptions) (statusCode int, err error) {
   347  	opts = ensureDoOpts(opts)
   348  
   349  	client.checkMaintenanceJSON()
   350  
   351  	var rsp *http.Response
   352  	var ctx context.Context = context.Background()
   353  	if opts.Timeout <= 0 {
   354  		// no timeout and retries
   355  		rsp, err = client.raw(ctx, method, path, query, headers, body)
   356  	} else {
   357  		if opts.Retry <= 0 {
   358  			return 0, fmt.Errorf("internal error: retry setting %s invalid", opts.Retry)
   359  		}
   360  		retry := time.NewTicker(opts.Retry)
   361  		defer retry.Stop()
   362  		timeout := time.NewTimer(opts.Timeout)
   363  		defer timeout.Stop()
   364  
   365  		for {
   366  			var cancel context.CancelFunc
   367  			// use the same timeout as for the whole of the retry
   368  			// loop to error out the whole do() call when a single
   369  			// request exceeds the deadline
   370  			rsp, cancel, err = client.rawWithTimeout(ctx, method, path, query, headers, body, opts)
   371  			if err == nil {
   372  				defer cancel()
   373  			}
   374  			if err == nil || method != "GET" {
   375  				break
   376  			}
   377  			select {
   378  			case <-retry.C:
   379  				continue
   380  			case <-timeout.C:
   381  			}
   382  			break
   383  		}
   384  	}
   385  	if err != nil {
   386  		return 0, err
   387  	}
   388  	defer rsp.Body.Close()
   389  
   390  	if v != nil {
   391  		if err := decodeInto(rsp.Body, v); err != nil {
   392  			return rsp.StatusCode, err
   393  		}
   394  	}
   395  
   396  	return rsp.StatusCode, nil
   397  }
   398  
   399  func decodeInto(reader io.Reader, v interface{}) error {
   400  	dec := json.NewDecoder(reader)
   401  	if err := dec.Decode(v); err != nil {
   402  		r := dec.Buffered()
   403  		buf, err1 := ioutil.ReadAll(r)
   404  		if err1 != nil {
   405  			buf = []byte(fmt.Sprintf("error reading buffered response body: %s", err1))
   406  		}
   407  		return fmt.Errorf("cannot decode %q: %s", buf, err)
   408  	}
   409  	return nil
   410  }
   411  
   412  // doSync performs a request to the given path using the specified HTTP method.
   413  // It expects a "sync" response from the API and on success decodes the JSON
   414  // response payload into the given value using the "UseNumber" json decoding
   415  // which produces json.Numbers instead of float64 types for numbers.
   416  func (client *Client) doSync(method, path string, query url.Values, headers map[string]string, body io.Reader, v interface{}) (*ResultInfo, error) {
   417  	return client.doSyncWithOpts(method, path, query, headers, body, v, nil)
   418  }
   419  
   420  // checkMaintenanceJSON checks if there is a maintenance.json file written by
   421  // snapd the daemon that positively identifies snapd as being unavailable due to
   422  // maintenance, either for snapd restarting itself to update, or rebooting the
   423  // system to update the kernel or base snap, etc. If there is ongoing
   424  // maintenance, then the maintenance object on the client is set appropriately.
   425  // note that currently checkMaintenanceJSON does not return errors, such that
   426  // if the file is missing or corrupt or empty, nothing will happen and it will
   427  // be silently ignored
   428  func (client *Client) checkMaintenanceJSON() {
   429  	f, err := os.Open(dirs.SnapdMaintenanceFile)
   430  	// just continue if we can't read the maintenance file
   431  	if err != nil {
   432  		return
   433  	}
   434  	defer f.Close()
   435  
   436  	// we have a maintenance file, try to read it
   437  	maintenance := &Error{}
   438  
   439  	if err := json.NewDecoder(f).Decode(&maintenance); err != nil {
   440  		// if the json is malformed, just ignore it for now, we only use it for
   441  		// positive identification of snapd down for maintenance
   442  		return
   443  	}
   444  
   445  	if maintenance != nil {
   446  		switch maintenance.Kind {
   447  		case ErrorKindDaemonRestart:
   448  			client.maintenance = maintenance
   449  		case ErrorKindSystemRestart:
   450  			client.maintenance = maintenance
   451  		}
   452  		// don't set maintenance for other kinds, as we don't know what it
   453  		// is yet
   454  
   455  		// this also means an empty json object in maintenance.json doesn't get
   456  		// treated as a real maintenance downtime for example
   457  	}
   458  }
   459  
   460  func (client *Client) doSyncWithOpts(method, path string, query url.Values, headers map[string]string, body io.Reader, v interface{}, opts *doOptions) (*ResultInfo, error) {
   461  	// first check maintenance.json to see if snapd is down for a restart, and
   462  	// set cli.maintenance as appropriate, then perform the request
   463  	// TODO: it would be a nice thing to skip the request if we know that snapd
   464  	// won't respond and return a specific error, but that's a big behavior
   465  	// change we probably shouldn't make right now, not to mention it probably
   466  	// requires adjustments in other areas too
   467  	client.checkMaintenanceJSON()
   468  
   469  	var rsp response
   470  	statusCode, err := client.do(method, path, query, headers, body, &rsp, opts)
   471  	if err != nil {
   472  		return nil, err
   473  	}
   474  	if err := rsp.err(client, statusCode); err != nil {
   475  		return nil, err
   476  	}
   477  	if rsp.Type != "sync" {
   478  		return nil, fmt.Errorf("expected sync response, got %q", rsp.Type)
   479  	}
   480  
   481  	if v != nil {
   482  		if err := jsonutil.DecodeWithNumber(bytes.NewReader(rsp.Result), v); err != nil {
   483  			return nil, fmt.Errorf("cannot unmarshal: %v", err)
   484  		}
   485  	}
   486  
   487  	client.warningCount = rsp.WarningCount
   488  	client.warningTimestamp = rsp.WarningTimestamp
   489  
   490  	return &rsp.ResultInfo, nil
   491  }
   492  
   493  func (client *Client) doAsync(method, path string, query url.Values, headers map[string]string, body io.Reader) (changeID string, err error) {
   494  	_, changeID, err = client.doAsyncFull(method, path, query, headers, body, nil)
   495  	return
   496  }
   497  
   498  func (client *Client) doAsyncFull(method, path string, query url.Values, headers map[string]string, body io.Reader, opts *doOptions) (result json.RawMessage, changeID string, err error) {
   499  	var rsp response
   500  	statusCode, err := client.do(method, path, query, headers, body, &rsp, opts)
   501  	if err != nil {
   502  		return nil, "", err
   503  	}
   504  	if err := rsp.err(client, statusCode); err != nil {
   505  		return nil, "", err
   506  	}
   507  	if rsp.Type != "async" {
   508  		return nil, "", fmt.Errorf("expected async response for %q on %q, got %q", method, path, rsp.Type)
   509  	}
   510  	if statusCode != 202 {
   511  		return nil, "", fmt.Errorf("operation not accepted")
   512  	}
   513  	if rsp.Change == "" {
   514  		return nil, "", fmt.Errorf("async response without change reference")
   515  	}
   516  
   517  	return rsp.Result, rsp.Change, nil
   518  }
   519  
   520  type ServerVersion struct {
   521  	Version     string
   522  	Series      string
   523  	OSID        string
   524  	OSVersionID string
   525  	OnClassic   bool
   526  
   527  	KernelVersion  string
   528  	Architecture   string
   529  	Virtualization string
   530  }
   531  
   532  func (client *Client) ServerVersion() (*ServerVersion, error) {
   533  	sysInfo, err := client.SysInfo()
   534  	if err != nil {
   535  		return nil, err
   536  	}
   537  
   538  	return &ServerVersion{
   539  		Version:     sysInfo.Version,
   540  		Series:      sysInfo.Series,
   541  		OSID:        sysInfo.OSRelease.ID,
   542  		OSVersionID: sysInfo.OSRelease.VersionID,
   543  		OnClassic:   sysInfo.OnClassic,
   544  
   545  		KernelVersion:  sysInfo.KernelVersion,
   546  		Architecture:   sysInfo.Architecture,
   547  		Virtualization: sysInfo.Virtualization,
   548  	}, nil
   549  }
   550  
   551  // A response produced by the REST API will usually fit in this
   552  // (exceptions are the icons/ endpoints obvs)
   553  type response struct {
   554  	Result json.RawMessage `json:"result"`
   555  	Type   string          `json:"type"`
   556  	Change string          `json:"change"`
   557  
   558  	WarningCount     int       `json:"warning-count"`
   559  	WarningTimestamp time.Time `json:"warning-timestamp"`
   560  
   561  	ResultInfo
   562  
   563  	Maintenance *Error `json:"maintenance"`
   564  }
   565  
   566  // Error is the real value of response.Result when an error occurs.
   567  type Error struct {
   568  	Kind    ErrorKind   `json:"kind"`
   569  	Value   interface{} `json:"value"`
   570  	Message string      `json:"message"`
   571  
   572  	StatusCode int
   573  }
   574  
   575  func (e *Error) Error() string {
   576  	return e.Message
   577  }
   578  
   579  // IsRetryable returns true if the given error is an error
   580  // that can be retried later.
   581  func IsRetryable(err error) bool {
   582  	switch e := err.(type) {
   583  	case *Error:
   584  		return e.Kind == ErrorKindSnapChangeConflict
   585  	}
   586  	return false
   587  }
   588  
   589  // IsTwoFactorError returns whether the given error is due to problems
   590  // in two-factor authentication.
   591  func IsTwoFactorError(err error) bool {
   592  	e, ok := err.(*Error)
   593  	if !ok || e == nil {
   594  		return false
   595  	}
   596  
   597  	return e.Kind == ErrorKindTwoFactorFailed || e.Kind == ErrorKindTwoFactorRequired
   598  }
   599  
   600  // IsInterfacesUnchangedError returns whether the given error means the requested
   601  // change to interfaces was not made, because there was nothing to do.
   602  func IsInterfacesUnchangedError(err error) bool {
   603  	e, ok := err.(*Error)
   604  	if !ok || e == nil {
   605  		return false
   606  	}
   607  	return e.Kind == ErrorKindInterfacesUnchanged
   608  }
   609  
   610  // IsAssertionNotFoundError returns whether the given error means that the
   611  // assertion wasn't found and thus the device isn't ready/seeded.
   612  func IsAssertionNotFoundError(err error) bool {
   613  	e, ok := err.(*Error)
   614  	if !ok || e == nil {
   615  		return false
   616  	}
   617  
   618  	return e.Kind == ErrorKindAssertionNotFound
   619  }
   620  
   621  // OSRelease contains information about the system extracted from /etc/os-release.
   622  type OSRelease struct {
   623  	ID        string `json:"id"`
   624  	VersionID string `json:"version-id,omitempty"`
   625  }
   626  
   627  // RefreshInfo contains information about refreshes.
   628  type RefreshInfo struct {
   629  	// Timer contains the refresh.timer setting.
   630  	Timer string `json:"timer,omitempty"`
   631  	// Schedule contains the legacy refresh.schedule setting.
   632  	Schedule string `json:"schedule,omitempty"`
   633  	Last     string `json:"last,omitempty"`
   634  	Hold     string `json:"hold,omitempty"`
   635  	Next     string `json:"next,omitempty"`
   636  }
   637  
   638  // SysInfo holds system information
   639  type SysInfo struct {
   640  	Series    string    `json:"series,omitempty"`
   641  	Version   string    `json:"version,omitempty"`
   642  	BuildID   string    `json:"build-id"`
   643  	OSRelease OSRelease `json:"os-release"`
   644  	OnClassic bool      `json:"on-classic"`
   645  	Managed   bool      `json:"managed"`
   646  
   647  	KernelVersion  string `json:"kernel-version,omitempty"`
   648  	Architecture   string `json:"architecture,omitempty"`
   649  	Virtualization string `json:"virtualization,omitempty"`
   650  
   651  	Refresh         RefreshInfo         `json:"refresh,omitempty"`
   652  	Confinement     string              `json:"confinement"`
   653  	SandboxFeatures map[string][]string `json:"sandbox-features,omitempty"`
   654  }
   655  
   656  func (rsp *response) err(cli *Client, statusCode int) error {
   657  	if cli != nil {
   658  		maintErr := rsp.Maintenance
   659  		// avoid setting to (*client.Error)(nil)
   660  		if maintErr != nil {
   661  			cli.maintenance = maintErr
   662  		} else {
   663  			cli.maintenance = nil
   664  		}
   665  	}
   666  	if rsp.Type != "error" {
   667  		return nil
   668  	}
   669  	var resultErr Error
   670  	err := json.Unmarshal(rsp.Result, &resultErr)
   671  	if err != nil || resultErr.Message == "" {
   672  		return fmt.Errorf("server error: %q", http.StatusText(statusCode))
   673  	}
   674  	resultErr.StatusCode = statusCode
   675  
   676  	return &resultErr
   677  }
   678  
   679  func parseError(r *http.Response) error {
   680  	var rsp response
   681  	if r.Header.Get("Content-Type") != "application/json" {
   682  		return fmt.Errorf("server error: %q", r.Status)
   683  	}
   684  
   685  	dec := json.NewDecoder(r.Body)
   686  	if err := dec.Decode(&rsp); err != nil {
   687  		return fmt.Errorf("cannot unmarshal error: %v", err)
   688  	}
   689  
   690  	err := rsp.err(nil, r.StatusCode)
   691  	if err == nil {
   692  		return fmt.Errorf("server error: %q", r.Status)
   693  	}
   694  	return err
   695  }
   696  
   697  // SysInfo gets system information from the REST API.
   698  func (client *Client) SysInfo() (*SysInfo, error) {
   699  	var sysInfo SysInfo
   700  
   701  	opts := &doOptions{
   702  		Timeout: 25 * time.Second,
   703  		Retry:   doRetry,
   704  	}
   705  	if _, err := client.doSyncWithOpts("GET", "/v2/system-info", nil, nil, nil, &sysInfo, opts); err != nil {
   706  		return nil, fmt.Errorf("cannot obtain system details: %v", err)
   707  	}
   708  
   709  	return &sysInfo, nil
   710  }
   711  
   712  type debugAction struct {
   713  	Action string      `json:"action"`
   714  	Params interface{} `json:"params,omitempty"`
   715  }
   716  
   717  // Debug is only useful when writing test code, it will trigger
   718  // an internal action with the given parameters.
   719  func (client *Client) Debug(action string, params interface{}, result interface{}) error {
   720  	body, err := json.Marshal(debugAction{
   721  		Action: action,
   722  		Params: params,
   723  	})
   724  	if err != nil {
   725  		return err
   726  	}
   727  
   728  	_, err = client.doSync("POST", "/v2/debug", nil, nil, bytes.NewReader(body), result)
   729  	return err
   730  }
   731  
   732  func (client *Client) DebugGet(aspect string, result interface{}, params map[string]string) error {
   733  	urlParams := url.Values{"aspect": []string{aspect}}
   734  	for k, v := range params {
   735  		urlParams.Set(k, v)
   736  	}
   737  	_, err := client.doSync("GET", "/v2/debug", urlParams, nil, nil, &result)
   738  	return err
   739  }
   740  
   741  type SystemRecoveryKeysResponse struct {
   742  	RecoveryKey  string `json:"recovery-key"`
   743  	ReinstallKey string `json:"reinstall-key"`
   744  }
   745  
   746  func (client *Client) SystemRecoveryKeys(result interface{}) error {
   747  	_, err := client.doSync("GET", "/v2/system-recovery-keys", nil, nil, nil, &result)
   748  	return err
   749  }