github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/api/client.go (about)

     1  // Copyright 2013, 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package api
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"net/http"
    11  	"net/url"
    12  	"os"
    13  	"strings"
    14  
    15  	"github.com/juju/errors"
    16  	"github.com/juju/loggo"
    17  	"github.com/juju/names"
    18  	"github.com/juju/version"
    19  	"golang.org/x/net/websocket"
    20  	"gopkg.in/juju/charm.v6-unstable"
    21  	csparams "gopkg.in/juju/charmrepo.v2-unstable/csclient/params"
    22  	"gopkg.in/macaroon.v1"
    23  
    24  	"github.com/juju/juju/api/base"
    25  	"github.com/juju/juju/apiserver/params"
    26  	"github.com/juju/juju/constraints"
    27  	"github.com/juju/juju/downloader"
    28  	"github.com/juju/juju/network"
    29  	"github.com/juju/juju/tools"
    30  )
    31  
    32  // Client represents the client-accessible part of the state.
    33  type Client struct {
    34  	base.ClientFacade
    35  	facade base.FacadeCaller
    36  	st     *state
    37  }
    38  
    39  // Status returns the status of the juju model.
    40  func (c *Client) Status(patterns []string) (*params.FullStatus, error) {
    41  	var result params.FullStatus
    42  	p := params.StatusParams{Patterns: patterns}
    43  	if err := c.facade.FacadeCall("FullStatus", p, &result); err != nil {
    44  		return nil, err
    45  	}
    46  	return &result, nil
    47  }
    48  
    49  // StatusHistory retrieves the last <size> results of
    50  // <kind:combined|agent|workload|machine|machineinstance|container|containerinstance> status
    51  // for <name> unit
    52  func (c *Client) StatusHistory(kind params.HistoryKind, name string, size int) (*params.StatusHistoryResults, error) {
    53  	var results params.StatusHistoryResults
    54  	args := params.StatusHistoryArgs{
    55  		Kind: kind,
    56  		Size: size,
    57  		Name: name,
    58  	}
    59  	err := c.facade.FacadeCall("StatusHistory", args, &results)
    60  	if err != nil {
    61  		return &params.StatusHistoryResults{}, errors.Trace(err)
    62  	}
    63  	return &results, nil
    64  }
    65  
    66  // Resolved clears errors on a unit.
    67  func (c *Client) Resolved(unit string, retry bool) error {
    68  	p := params.Resolved{
    69  		UnitName: unit,
    70  		Retry:    retry,
    71  	}
    72  	return c.facade.FacadeCall("Resolved", p, nil)
    73  }
    74  
    75  // RetryProvisioning updates the provisioning status of a machine allowing the
    76  // provisioner to retry.
    77  func (c *Client) RetryProvisioning(machines ...names.MachineTag) ([]params.ErrorResult, error) {
    78  	p := params.Entities{}
    79  	p.Entities = make([]params.Entity, len(machines))
    80  	for i, machine := range machines {
    81  		p.Entities[i] = params.Entity{Tag: machine.String()}
    82  	}
    83  	var results params.ErrorResults
    84  	err := c.facade.FacadeCall("RetryProvisioning", p, &results)
    85  	return results.Results, err
    86  }
    87  
    88  // PublicAddress returns the public address of the specified
    89  // machine or unit. For a machine, target is an id not a tag.
    90  func (c *Client) PublicAddress(target string) (string, error) {
    91  	var results params.PublicAddressResults
    92  	p := params.PublicAddress{Target: target}
    93  	err := c.facade.FacadeCall("PublicAddress", p, &results)
    94  	return results.PublicAddress, err
    95  }
    96  
    97  // PrivateAddress returns the private address of the specified
    98  // machine or unit.
    99  func (c *Client) PrivateAddress(target string) (string, error) {
   100  	var results params.PrivateAddressResults
   101  	p := params.PrivateAddress{Target: target}
   102  	err := c.facade.FacadeCall("PrivateAddress", p, &results)
   103  	return results.PrivateAddress, err
   104  }
   105  
   106  // AddMachines adds new machines with the supplied parameters.
   107  func (c *Client) AddMachines(machineParams []params.AddMachineParams) ([]params.AddMachinesResult, error) {
   108  	args := params.AddMachines{
   109  		MachineParams: machineParams,
   110  	}
   111  	results := new(params.AddMachinesResults)
   112  	err := c.facade.FacadeCall("AddMachinesV2", args, results)
   113  	return results.Machines, err
   114  }
   115  
   116  // ProvisioningScript returns a shell script that, when run,
   117  // provisions a machine agent on the machine executing the script.
   118  func (c *Client) ProvisioningScript(args params.ProvisioningScriptParams) (script string, err error) {
   119  	var result params.ProvisioningScriptResult
   120  	if err = c.facade.FacadeCall("ProvisioningScript", args, &result); err != nil {
   121  		return "", err
   122  	}
   123  	return result.Script, nil
   124  }
   125  
   126  // DestroyMachines removes a given set of machines.
   127  func (c *Client) DestroyMachines(machines ...string) error {
   128  	params := params.DestroyMachines{MachineNames: machines}
   129  	return c.facade.FacadeCall("DestroyMachines", params, nil)
   130  }
   131  
   132  // ForceDestroyMachines removes a given set of machines and all associated units.
   133  func (c *Client) ForceDestroyMachines(machines ...string) error {
   134  	params := params.DestroyMachines{Force: true, MachineNames: machines}
   135  	return c.facade.FacadeCall("DestroyMachines", params, nil)
   136  }
   137  
   138  // GetModelConstraints returns the constraints for the model.
   139  func (c *Client) GetModelConstraints() (constraints.Value, error) {
   140  	results := new(params.GetConstraintsResults)
   141  	err := c.facade.FacadeCall("GetModelConstraints", nil, results)
   142  	return results.Constraints, err
   143  }
   144  
   145  // SetModelConstraints specifies the constraints for the model.
   146  func (c *Client) SetModelConstraints(constraints constraints.Value) error {
   147  	params := params.SetConstraints{
   148  		Constraints: constraints,
   149  	}
   150  	return c.facade.FacadeCall("SetModelConstraints", params, nil)
   151  }
   152  
   153  // CharmInfo holds information about a charm.
   154  type CharmInfo struct {
   155  	Revision int
   156  	URL      string
   157  	Config   *charm.Config
   158  	Meta     *charm.Meta
   159  	Actions  *charm.Actions
   160  }
   161  
   162  // CharmInfo returns information about the requested charm.
   163  func (c *Client) CharmInfo(charmURL string) (*CharmInfo, error) {
   164  	args := params.CharmInfo{CharmURL: charmURL}
   165  	info := new(CharmInfo)
   166  	if err := c.facade.FacadeCall("CharmInfo", args, info); err != nil {
   167  		return nil, err
   168  	}
   169  	return info, nil
   170  }
   171  
   172  // ModelInfo returns details about the Juju model.
   173  func (c *Client) ModelInfo() (params.ModelInfo, error) {
   174  	var info params.ModelInfo
   175  	err := c.facade.FacadeCall("ModelInfo", nil, &info)
   176  	return info, err
   177  }
   178  
   179  // ModelUUID returns the model UUID from the client connection.
   180  func (c *Client) ModelUUID() string {
   181  	tag, err := c.st.ModelTag()
   182  	if err != nil {
   183  		logger.Warningf("model tag not an model: %v", err)
   184  		return ""
   185  	}
   186  	return tag.Id()
   187  }
   188  
   189  // ModelUserInfo returns information on all users in the model.
   190  func (c *Client) ModelUserInfo() ([]params.ModelUserInfo, error) {
   191  	var results params.ModelUserInfoResults
   192  	err := c.facade.FacadeCall("ModelUserInfo", nil, &results)
   193  	if err != nil {
   194  		return nil, errors.Trace(err)
   195  	}
   196  
   197  	info := []params.ModelUserInfo{}
   198  	for i, result := range results.Results {
   199  		if result.Result == nil {
   200  			return nil, errors.Errorf("unexpected nil result at position %d", i)
   201  		}
   202  		info = append(info, *result.Result)
   203  	}
   204  	return info, nil
   205  }
   206  
   207  // WatchAll holds the id of the newly-created AllWatcher/AllModelWatcher.
   208  type WatchAll struct {
   209  	AllWatcherId string
   210  }
   211  
   212  // WatchAll returns an AllWatcher, from which you can request the Next
   213  // collection of Deltas.
   214  func (c *Client) WatchAll() (*AllWatcher, error) {
   215  	info := new(WatchAll)
   216  	if err := c.facade.FacadeCall("WatchAll", nil, info); err != nil {
   217  		return nil, err
   218  	}
   219  	return NewAllWatcher(c.st, &info.AllWatcherId), nil
   220  }
   221  
   222  // Close closes the Client's underlying State connection
   223  // Client is unique among the api.State facades in closing its own State
   224  // connection, but it is conventional to use a Client object without any access
   225  // to its underlying state connection.
   226  func (c *Client) Close() error {
   227  	return c.st.Close()
   228  }
   229  
   230  // ModelGet returns all model settings.
   231  func (c *Client) ModelGet() (map[string]interface{}, error) {
   232  	result := params.ModelConfigResults{}
   233  	err := c.facade.FacadeCall("ModelGet", nil, &result)
   234  	return result.Config, err
   235  }
   236  
   237  // ModelSet sets the given key-value pairs in the model.
   238  func (c *Client) ModelSet(config map[string]interface{}) error {
   239  	args := params.ModelSet{Config: config}
   240  	return c.facade.FacadeCall("ModelSet", args, nil)
   241  }
   242  
   243  // ModelUnset sets the given key-value pairs in the model.
   244  func (c *Client) ModelUnset(keys ...string) error {
   245  	args := params.ModelUnset{Keys: keys}
   246  	return c.facade.FacadeCall("ModelUnset", args, nil)
   247  }
   248  
   249  // SetModelAgentVersion sets the model agent-version setting
   250  // to the given value.
   251  func (c *Client) SetModelAgentVersion(version version.Number) error {
   252  	args := params.SetModelAgentVersion{Version: version}
   253  	return c.facade.FacadeCall("SetModelAgentVersion", args, nil)
   254  }
   255  
   256  // AbortCurrentUpgrade aborts and archives the current upgrade
   257  // synchronisation record, if any.
   258  func (c *Client) AbortCurrentUpgrade() error {
   259  	return c.facade.FacadeCall("AbortCurrentUpgrade", nil, nil)
   260  }
   261  
   262  // FindTools returns a List containing all tools matching the specified parameters.
   263  func (c *Client) FindTools(majorVersion, minorVersion int, series, arch string) (result params.FindToolsResult, err error) {
   264  	args := params.FindToolsParams{
   265  		MajorVersion: majorVersion,
   266  		MinorVersion: minorVersion,
   267  		Arch:         arch,
   268  		Series:       series,
   269  	}
   270  	err = c.facade.FacadeCall("FindTools", args, &result)
   271  	return result, err
   272  }
   273  
   274  // DestroyModel puts the model into a "dying" state,
   275  // and removes all non-manager machine instances. DestroyModel
   276  // will fail if there are any manually-provisioned non-manager machines
   277  // in state.
   278  func (c *Client) DestroyModel() error {
   279  	return c.facade.FacadeCall("DestroyModel", nil, nil)
   280  }
   281  
   282  // AddLocalCharm prepares the given charm with a local: schema in its
   283  // URL, and uploads it via the API server, returning the assigned
   284  // charm URL.
   285  func (c *Client) AddLocalCharm(curl *charm.URL, ch charm.Charm) (*charm.URL, error) {
   286  	if curl.Schema != "local" {
   287  		return nil, errors.Errorf("expected charm URL with local: schema, got %q", curl.String())
   288  	}
   289  
   290  	if err := c.validateCharmVersion(ch); err != nil {
   291  		return nil, errors.Trace(err)
   292  	}
   293  
   294  	// Package the charm for uploading.
   295  	var archive *os.File
   296  	switch ch := ch.(type) {
   297  	case *charm.CharmDir:
   298  		var err error
   299  		if archive, err = ioutil.TempFile("", "charm"); err != nil {
   300  			return nil, errors.Annotate(err, "cannot create temp file")
   301  		}
   302  		defer os.Remove(archive.Name())
   303  		defer archive.Close()
   304  		if err := ch.ArchiveTo(archive); err != nil {
   305  			return nil, errors.Annotate(err, "cannot repackage charm")
   306  		}
   307  		if _, err := archive.Seek(0, 0); err != nil {
   308  			return nil, errors.Annotate(err, "cannot rewind packaged charm")
   309  		}
   310  	case *charm.CharmArchive:
   311  		var err error
   312  		if archive, err = os.Open(ch.Path); err != nil {
   313  			return nil, errors.Annotate(err, "cannot read charm archive")
   314  		}
   315  		defer archive.Close()
   316  	default:
   317  		return nil, errors.Errorf("unknown charm type %T", ch)
   318  	}
   319  
   320  	curl, err := c.UploadCharm(curl, archive)
   321  	if err != nil {
   322  		return nil, errors.Trace(err)
   323  	}
   324  	return curl, nil
   325  }
   326  
   327  // UploadCharm sends the content to the API server using an HTTP post.
   328  func (c *Client) UploadCharm(curl *charm.URL, content io.ReadSeeker) (*charm.URL, error) {
   329  	endpoint := "/charms?series=" + curl.Series
   330  	contentType := "application/zip"
   331  	var resp params.CharmsResponse
   332  	if err := c.httpPost(content, endpoint, contentType, &resp); err != nil {
   333  		return nil, errors.Trace(err)
   334  	}
   335  
   336  	curl, err := charm.ParseURL(resp.CharmURL)
   337  	if err != nil {
   338  		return nil, errors.Annotatef(err, "bad charm URL in response")
   339  	}
   340  	return curl, nil
   341  }
   342  
   343  type minJujuVersionErr struct {
   344  	*errors.Err
   345  }
   346  
   347  func minVersionError(minver, jujuver version.Number) error {
   348  	err := errors.NewErr("charm's min version (%s) is higher than this juju environment's version (%s)",
   349  		minver, jujuver)
   350  	err.SetLocation(1)
   351  	return minJujuVersionErr{&err}
   352  }
   353  
   354  func (c *Client) validateCharmVersion(ch charm.Charm) error {
   355  	minver := ch.Meta().MinJujuVersion
   356  	if minver != version.Zero {
   357  		agentver, err := c.AgentVersion()
   358  		if err != nil {
   359  			return errors.Trace(err)
   360  		}
   361  
   362  		if minver.Compare(agentver) > 0 {
   363  			return minVersionError(minver, agentver)
   364  		}
   365  	}
   366  	return nil
   367  }
   368  
   369  // TODO(ericsnow) Use charmstore.CharmID for AddCharm() & AddCharmWithAuth().
   370  
   371  // AddCharm adds the given charm URL (which must include revision) to
   372  // the model, if it does not exist yet. Local charms are not
   373  // supported, only charm store URLs. See also AddLocalCharm() in the
   374  // client-side API.
   375  //
   376  // If the AddCharm API call fails because of an authorization error
   377  // when retrieving the charm from the charm store, an error
   378  // satisfying params.IsCodeUnauthorized will be returned.
   379  func (c *Client) AddCharm(curl *charm.URL, channel csparams.Channel) error {
   380  	args := params.AddCharm{
   381  		URL:     curl.String(),
   382  		Channel: string(channel),
   383  	}
   384  	if err := c.facade.FacadeCall("AddCharm", args, nil); err != nil {
   385  		return errors.Trace(err)
   386  	}
   387  	return nil
   388  }
   389  
   390  // AddCharmWithAuthorization is like AddCharm except it also provides
   391  // the given charmstore macaroon for the juju server to use when
   392  // obtaining the charm from the charm store. The macaroon is
   393  // conventionally obtained from the /delegatable-macaroon endpoint in
   394  // the charm store.
   395  //
   396  // If the AddCharmWithAuthorization API call fails because of an
   397  // authorization error when retrieving the charm from the charm store,
   398  // an error satisfying params.IsCodeUnauthorized will be returned.
   399  func (c *Client) AddCharmWithAuthorization(curl *charm.URL, channel csparams.Channel, csMac *macaroon.Macaroon) error {
   400  	args := params.AddCharmWithAuthorization{
   401  		URL:                curl.String(),
   402  		Channel:            string(channel),
   403  		CharmStoreMacaroon: csMac,
   404  	}
   405  	if err := c.facade.FacadeCall("AddCharmWithAuthorization", args, nil); err != nil {
   406  		return errors.Trace(err)
   407  	}
   408  	return nil
   409  }
   410  
   411  // ResolveCharm resolves the best available charm URLs with series, for charm
   412  // locations without a series specified.
   413  func (c *Client) ResolveCharm(ref *charm.URL) (*charm.URL, error) {
   414  	args := params.ResolveCharms{References: []charm.URL{*ref}}
   415  	result := new(params.ResolveCharmResults)
   416  	if err := c.facade.FacadeCall("ResolveCharms", args, result); err != nil {
   417  		return nil, err
   418  	}
   419  	if len(result.URLs) == 0 {
   420  		return nil, errors.New("unexpected empty response")
   421  	}
   422  	urlInfo := result.URLs[0]
   423  	if urlInfo.Error != "" {
   424  		return nil, errors.New(urlInfo.Error)
   425  	}
   426  	return urlInfo.URL, nil
   427  }
   428  
   429  // OpenCharm streams out the identified charm from the controller via
   430  // the API.
   431  func (c *Client) OpenCharm(curl *charm.URL) (io.ReadCloser, error) {
   432  	// The returned httpClient sets the base url to /model/<uuid> if it can.
   433  	httpClient, err := c.st.HTTPClient()
   434  	if err != nil {
   435  		return nil, errors.Trace(err)
   436  	}
   437  	blob, err := openCharm(httpClient, curl)
   438  	if err != nil {
   439  		return nil, errors.Trace(err)
   440  	}
   441  	return blob, nil
   442  }
   443  
   444  // openCharm streams out the identified charm from the controller via
   445  // the API.
   446  func openCharm(httpClient HTTPDoer, curl *charm.URL) (io.ReadCloser, error) {
   447  	query := make(url.Values)
   448  	query.Add("url", curl.String())
   449  	query.Add("file", "*")
   450  	blob, err := openBlob(httpClient, "/charms", query)
   451  	if err != nil {
   452  		return nil, errors.Trace(err)
   453  	}
   454  	return blob, nil
   455  }
   456  
   457  // NewCharmDownloader returns a new charm downloader that wraps the
   458  // provided API client.
   459  func NewCharmDownloader(client *Client) *downloader.Downloader {
   460  	dlr := &downloader.Downloader{
   461  		OpenBlob: func(url *url.URL) (io.ReadCloser, error) {
   462  			curl, err := charm.ParseURL(url.String())
   463  			if err != nil {
   464  				return nil, errors.Annotate(err, "did not receive a valid charm URL")
   465  			}
   466  			reader, err := client.OpenCharm(curl)
   467  			if err != nil {
   468  				return nil, errors.Trace(err)
   469  			}
   470  			return reader, nil
   471  		},
   472  	}
   473  	return dlr
   474  }
   475  
   476  // UploadTools uploads tools at the specified location to the API server over HTTPS.
   477  func (c *Client) UploadTools(r io.ReadSeeker, vers version.Binary, additionalSeries ...string) (tools.List, error) {
   478  	endpoint := fmt.Sprintf("/tools?binaryVersion=%s&series=%s", vers, strings.Join(additionalSeries, ","))
   479  	contentType := "application/x-tar-gz"
   480  	var resp params.ToolsResult
   481  	if err := c.httpPost(r, endpoint, contentType, &resp); err != nil {
   482  		return nil, errors.Trace(err)
   483  	}
   484  	return resp.ToolsList, nil
   485  }
   486  
   487  func (c *Client) httpPost(content io.ReadSeeker, endpoint, contentType string, response interface{}) error {
   488  	req, err := http.NewRequest("POST", endpoint, nil)
   489  	if err != nil {
   490  		return errors.Annotate(err, "cannot create upload request")
   491  	}
   492  	req.Header.Set("Content-Type", contentType)
   493  
   494  	// The returned httpClient sets the base url to /model/<uuid> if it can.
   495  	httpClient, err := c.st.HTTPClient()
   496  	if err != nil {
   497  		return errors.Trace(err)
   498  	}
   499  
   500  	if err := httpClient.Do(req, content, response); err != nil {
   501  		return errors.Trace(err)
   502  	}
   503  	return nil
   504  }
   505  
   506  // APIHostPorts returns a slice of network.HostPort for each API server.
   507  func (c *Client) APIHostPorts() ([][]network.HostPort, error) {
   508  	var result params.APIHostPortsResult
   509  	if err := c.facade.FacadeCall("APIHostPorts", nil, &result); err != nil {
   510  		return nil, err
   511  	}
   512  	return result.NetworkHostsPorts(), nil
   513  }
   514  
   515  // AgentVersion reports the version number of the api server.
   516  func (c *Client) AgentVersion() (version.Number, error) {
   517  	var result params.AgentVersionResult
   518  	if err := c.facade.FacadeCall("AgentVersion", nil, &result); err != nil {
   519  		return version.Number{}, err
   520  	}
   521  	return result.Version, nil
   522  }
   523  
   524  // websocketDialConfig is called instead of websocket.DialConfig so we can
   525  // override it in tests.
   526  var websocketDialConfig = func(config *websocket.Config) (base.Stream, error) {
   527  	c, err := websocket.DialConfig(config)
   528  	if err != nil {
   529  		return nil, errors.Trace(err)
   530  	}
   531  	return websocketStream{c}, nil
   532  }
   533  
   534  type websocketStream struct {
   535  	*websocket.Conn
   536  }
   537  
   538  func (c websocketStream) ReadJSON(v interface{}) error {
   539  	return websocket.JSON.Receive(c.Conn, v)
   540  }
   541  
   542  func (c websocketStream) WriteJSON(v interface{}) error {
   543  	return websocket.JSON.Send(c.Conn, v)
   544  }
   545  
   546  // DebugLogParams holds parameters for WatchDebugLog that control the
   547  // filtering of the log messages. If the structure is zero initialized, the
   548  // entire log file is sent back starting from the end, and until the user
   549  // closes the connection.
   550  type DebugLogParams struct {
   551  	// IncludeEntity lists entity tags to include in the response. Tags may
   552  	// finish with a '*' to match a prefix e.g.: unit-mysql-*, machine-2. If
   553  	// none are set, then all lines are considered included.
   554  	IncludeEntity []string
   555  	// IncludeModule lists logging modules to include in the response. If none
   556  	// are set all modules are considered included.  If a module is specified,
   557  	// all the submodules also match.
   558  	IncludeModule []string
   559  	// ExcludeEntity lists entity tags to exclude from the response. As with
   560  	// IncludeEntity the values may finish with a '*'.
   561  	ExcludeEntity []string
   562  	// ExcludeModule lists logging modules to exclude from the resposne. If a
   563  	// module is specified, all the submodules are also excluded.
   564  	ExcludeModule []string
   565  	// Limit defines the maximum number of lines to return. Once this many
   566  	// have been sent, the socket is closed.  If zero, all filtered lines are
   567  	// sent down the connection until the client closes the connection.
   568  	Limit uint
   569  	// Backlog tells the server to try to go back this many lines before
   570  	// starting filtering. If backlog is zero and replay is false, then there
   571  	// may be an initial delay until the next matching log message is written.
   572  	Backlog uint
   573  	// Level specifies the minimum logging level to be sent back in the response.
   574  	Level loggo.Level
   575  	// Replay tells the server to start at the start of the log file rather
   576  	// than the end. If replay is true, backlog is ignored.
   577  	Replay bool
   578  	// NoTail tells the server to only return the logs it has now, and not
   579  	// to wait for new logs to arrive.
   580  	NoTail bool
   581  }
   582  
   583  // WatchDebugLog returns a ReadCloser that the caller can read the log
   584  // lines from. Only log lines that match the filtering specified in
   585  // the DebugLogParams are returned. It returns an error that satisfies
   586  // errors.IsNotImplemented when the API server does not support the
   587  // end-point.
   588  func (c *Client) WatchDebugLog(args DebugLogParams) (io.ReadCloser, error) {
   589  	// The websocket connection just hangs if the server doesn't have the log
   590  	// end point. So do a version check, as version was added at the same time
   591  	// as the remote end point.
   592  	_, err := c.AgentVersion()
   593  	if err != nil {
   594  		return nil, errors.NotSupportedf("WatchDebugLog")
   595  	}
   596  	// Prepare URL query attributes.
   597  	attrs := url.Values{
   598  		"includeEntity": args.IncludeEntity,
   599  		"includeModule": args.IncludeModule,
   600  		"excludeEntity": args.ExcludeEntity,
   601  		"excludeModule": args.ExcludeModule,
   602  	}
   603  	if args.Replay {
   604  		attrs.Set("replay", fmt.Sprint(args.Replay))
   605  	}
   606  	if args.NoTail {
   607  		attrs.Set("noTail", fmt.Sprint(args.NoTail))
   608  	}
   609  	if args.Limit > 0 {
   610  		attrs.Set("maxLines", fmt.Sprint(args.Limit))
   611  	}
   612  	if args.Backlog > 0 {
   613  		attrs.Set("backlog", fmt.Sprint(args.Backlog))
   614  	}
   615  	if args.Level != loggo.UNSPECIFIED {
   616  		attrs.Set("level", fmt.Sprint(args.Level))
   617  	}
   618  
   619  	connection, err := c.st.ConnectStream("/log", attrs)
   620  	if err != nil {
   621  		return nil, errors.Trace(err)
   622  	}
   623  	return connection, nil
   624  }