Permalink
Cannot retrieve contributors at this time
Fetching contributors…
| // Copyright 2013, 2014 Canonical Ltd. | |
| // Licensed under the AGPLv3, see LICENCE file for details. | |
| package api | |
| import ( | |
| "fmt" | |
| "io" | |
| "io/ioutil" | |
| "net/http" | |
| "net/url" | |
| "os" | |
| "strconv" | |
| "strings" | |
| "github.com/gorilla/websocket" | |
| "github.com/juju/errors" | |
| "github.com/juju/version" | |
| "gopkg.in/juju/charm.v6-unstable" | |
| csparams "gopkg.in/juju/charmrepo.v2-unstable/csclient/params" | |
| "gopkg.in/juju/names.v2" | |
| "gopkg.in/macaroon.v1" | |
| "github.com/juju/juju/api/base" | |
| "github.com/juju/juju/api/common" | |
| "github.com/juju/juju/apiserver/params" | |
| "github.com/juju/juju/constraints" | |
| "github.com/juju/juju/downloader" | |
| "github.com/juju/juju/network" | |
| "github.com/juju/juju/status" | |
| "github.com/juju/juju/tools" | |
| ) | |
| // Client represents the client-accessible part of the state. | |
| type Client struct { | |
| base.ClientFacade | |
| facade base.FacadeCaller | |
| st *state | |
| } | |
| // Status returns the status of the juju model. | |
| func (c *Client) Status(patterns []string) (*params.FullStatus, error) { | |
| var result params.FullStatus | |
| p := params.StatusParams{Patterns: patterns} | |
| if err := c.facade.FacadeCall("FullStatus", p, &result); err != nil { | |
| return nil, err | |
| } | |
| return &result, nil | |
| } | |
| // StatusHistory retrieves the last <size> results of | |
| // <kind:combined|agent|workload|machine|machineinstance|container|containerinstance> status | |
| // for <name> unit | |
| func (c *Client) StatusHistory(kind status.HistoryKind, tag names.Tag, filter status.StatusHistoryFilter) (status.History, error) { | |
| var results params.StatusHistoryResults | |
| args := params.StatusHistoryRequest{ | |
| Kind: string(kind), | |
| Filter: params.StatusHistoryFilter{ | |
| Size: filter.Size, | |
| Date: filter.FromDate, | |
| Delta: filter.Delta, | |
| Exclude: filter.Exclude.Values(), | |
| }, | |
| Tag: tag.String(), | |
| } | |
| bulkArgs := params.StatusHistoryRequests{Requests: []params.StatusHistoryRequest{args}} | |
| err := c.facade.FacadeCall("StatusHistory", bulkArgs, &results) | |
| if err != nil { | |
| return status.History{}, errors.Trace(err) | |
| } | |
| if len(results.Results) != 1 { | |
| return status.History{}, errors.Errorf("expected 1 result got %d", len(results.Results)) | |
| } | |
| if results.Results[0].Error != nil { | |
| return status.History{}, errors.Annotatef(results.Results[0].Error, "while processing the request") | |
| } | |
| history := make(status.History, len(results.Results[0].History.Statuses)) | |
| if results.Results[0].History.Error != nil { | |
| return status.History{}, results.Results[0].History.Error | |
| } | |
| for i, h := range results.Results[0].History.Statuses { | |
| history[i] = status.DetailedStatus{ | |
| Status: status.Status(h.Status), | |
| Info: h.Info, | |
| Data: h.Data, | |
| Since: h.Since, | |
| Kind: status.HistoryKind(h.Kind), | |
| Version: h.Version, | |
| // TODO(perrito666) make sure these are still used. | |
| Life: h.Life, | |
| Err: h.Err, | |
| } | |
| // TODO(perrito666) https://launchpad.net/bugs/1577589 | |
| if !history[i].Kind.Valid() { | |
| logger.Errorf("history returned an unknown status kind %q", h.Kind) | |
| } | |
| } | |
| return history, nil | |
| } | |
| // Resolved clears errors on a unit. | |
| func (c *Client) Resolved(unit string, retry bool) error { | |
| p := params.Resolved{ | |
| UnitName: unit, | |
| Retry: retry, | |
| } | |
| return c.facade.FacadeCall("Resolved", p, nil) | |
| } | |
| // RetryProvisioning updates the provisioning status of a machine allowing the | |
| // provisioner to retry. | |
| func (c *Client) RetryProvisioning(machines ...names.MachineTag) ([]params.ErrorResult, error) { | |
| p := params.Entities{} | |
| p.Entities = make([]params.Entity, len(machines)) | |
| for i, machine := range machines { | |
| p.Entities[i] = params.Entity{Tag: machine.String()} | |
| } | |
| var results params.ErrorResults | |
| err := c.facade.FacadeCall("RetryProvisioning", p, &results) | |
| return results.Results, err | |
| } | |
| // PublicAddress returns the public address of the specified | |
| // machine or unit. For a machine, target is an id not a tag. | |
| func (c *Client) PublicAddress(target string) (string, error) { | |
| var results params.PublicAddressResults | |
| p := params.PublicAddress{Target: target} | |
| err := c.facade.FacadeCall("PublicAddress", p, &results) | |
| return results.PublicAddress, err | |
| } | |
| // PrivateAddress returns the private address of the specified | |
| // machine or unit. | |
| func (c *Client) PrivateAddress(target string) (string, error) { | |
| var results params.PrivateAddressResults | |
| p := params.PrivateAddress{Target: target} | |
| err := c.facade.FacadeCall("PrivateAddress", p, &results) | |
| return results.PrivateAddress, err | |
| } | |
| // AddMachines adds new machines with the supplied parameters. | |
| func (c *Client) AddMachines(machineParams []params.AddMachineParams) ([]params.AddMachinesResult, error) { | |
| args := params.AddMachines{ | |
| MachineParams: machineParams, | |
| } | |
| results := new(params.AddMachinesResults) | |
| err := c.facade.FacadeCall("AddMachinesV2", args, results) | |
| return results.Machines, err | |
| } | |
| // ProvisioningScript returns a shell script that, when run, | |
| // provisions a machine agent on the machine executing the script. | |
| func (c *Client) ProvisioningScript(args params.ProvisioningScriptParams) (script string, err error) { | |
| var result params.ProvisioningScriptResult | |
| if err = c.facade.FacadeCall("ProvisioningScript", args, &result); err != nil { | |
| return "", err | |
| } | |
| return result.Script, nil | |
| } | |
| // DestroyMachines removes a given set of machines. | |
| // | |
| // NOTE(axw) this exists only for backwards compatibility, when MachineManager | |
| // facade v3 is not available. The MachineManager.DestroyMachines method should | |
| // be preferred. | |
| // | |
| // TODO(axw) 2017-03-16 #1673323 | |
| // Drop this in Juju 3.0. | |
| func (c *Client) DestroyMachines(machines ...string) error { | |
| params := params.DestroyMachines{MachineNames: machines} | |
| return c.facade.FacadeCall("DestroyMachines", params, nil) | |
| } | |
| // ForceDestroyMachines removes a given set of machines and all associated units. | |
| // | |
| // NOTE(axw) this exists only for backwards compatibility, when MachineManager | |
| // facade v3 is not available. The MachineManager.ForceDestroyMachines method | |
| // should be preferred. | |
| // | |
| // TODO(axw) 2017-03-16 #1673323 | |
| // Drop this in Juju 3.0. | |
| func (c *Client) ForceDestroyMachines(machines ...string) error { | |
| params := params.DestroyMachines{Force: true, MachineNames: machines} | |
| return c.facade.FacadeCall("DestroyMachines", params, nil) | |
| } | |
| // GetModelConstraints returns the constraints for the model. | |
| func (c *Client) GetModelConstraints() (constraints.Value, error) { | |
| results := new(params.GetConstraintsResults) | |
| err := c.facade.FacadeCall("GetModelConstraints", nil, results) | |
| return results.Constraints, err | |
| } | |
| // SetModelConstraints specifies the constraints for the model. | |
| func (c *Client) SetModelConstraints(constraints constraints.Value) error { | |
| params := params.SetConstraints{ | |
| Constraints: constraints, | |
| } | |
| return c.facade.FacadeCall("SetModelConstraints", params, nil) | |
| } | |
| // ModelUUID returns the model UUID from the client connection | |
| // and reports whether it is valud. | |
| func (c *Client) ModelUUID() (string, bool) { | |
| tag, ok := c.st.ModelTag() | |
| if !ok { | |
| return "", false | |
| } | |
| return tag.Id(), true | |
| } | |
| // ModelUserInfo returns information on all users in the model. | |
| func (c *Client) ModelUserInfo() ([]params.ModelUserInfo, error) { | |
| var results params.ModelUserInfoResults | |
| err := c.facade.FacadeCall("ModelUserInfo", nil, &results) | |
| if err != nil { | |
| return nil, errors.Trace(err) | |
| } | |
| info := []params.ModelUserInfo{} | |
| for i, result := range results.Results { | |
| if result.Result == nil { | |
| return nil, errors.Errorf("unexpected nil result at position %d", i) | |
| } | |
| info = append(info, *result.Result) | |
| } | |
| return info, nil | |
| } | |
| // WatchAll returns an AllWatcher, from which you can request the Next | |
| // collection of Deltas. | |
| func (c *Client) WatchAll() (*AllWatcher, error) { | |
| var info params.AllWatcherId | |
| if err := c.facade.FacadeCall("WatchAll", nil, &info); err != nil { | |
| return nil, err | |
| } | |
| return NewAllWatcher(c.st, &info.AllWatcherId), nil | |
| } | |
| // Close closes the Client's underlying State connection | |
| // Client is unique among the api.State facades in closing its own State | |
| // connection, but it is conventional to use a Client object without any access | |
| // to its underlying state connection. | |
| func (c *Client) Close() error { | |
| return c.st.Close() | |
| } | |
| // SetModelAgentVersion sets the model agent-version setting | |
| // to the given value. | |
| func (c *Client) SetModelAgentVersion(version version.Number) error { | |
| args := params.SetModelAgentVersion{Version: version} | |
| return c.facade.FacadeCall("SetModelAgentVersion", args, nil) | |
| } | |
| // AbortCurrentUpgrade aborts and archives the current upgrade | |
| // synchronisation record, if any. | |
| func (c *Client) AbortCurrentUpgrade() error { | |
| return c.facade.FacadeCall("AbortCurrentUpgrade", nil, nil) | |
| } | |
| // FindTools returns a List containing all tools matching the specified parameters. | |
| func (c *Client) FindTools(majorVersion, minorVersion int, series, arch string) (result params.FindToolsResult, err error) { | |
| args := params.FindToolsParams{ | |
| MajorVersion: majorVersion, | |
| MinorVersion: minorVersion, | |
| Arch: arch, | |
| Series: series, | |
| } | |
| err = c.facade.FacadeCall("FindTools", args, &result) | |
| return result, err | |
| } | |
| // AddLocalCharm prepares the given charm with a local: schema in its | |
| // URL, and uploads it via the API server, returning the assigned | |
| // charm URL. | |
| func (c *Client) AddLocalCharm(curl *charm.URL, ch charm.Charm) (*charm.URL, error) { | |
| if curl.Schema != "local" { | |
| return nil, errors.Errorf("expected charm URL with local: schema, got %q", curl.String()) | |
| } | |
| if err := c.validateCharmVersion(ch); err != nil { | |
| return nil, errors.Trace(err) | |
| } | |
| // Package the charm for uploading. | |
| var archive *os.File | |
| switch ch := ch.(type) { | |
| case *charm.CharmDir: | |
| var err error | |
| if archive, err = ioutil.TempFile("", "charm"); err != nil { | |
| return nil, errors.Annotate(err, "cannot create temp file") | |
| } | |
| defer os.Remove(archive.Name()) | |
| defer archive.Close() | |
| if err := ch.ArchiveTo(archive); err != nil { | |
| return nil, errors.Annotate(err, "cannot repackage charm") | |
| } | |
| if _, err := archive.Seek(0, 0); err != nil { | |
| return nil, errors.Annotate(err, "cannot rewind packaged charm") | |
| } | |
| case *charm.CharmArchive: | |
| var err error | |
| if archive, err = os.Open(ch.Path); err != nil { | |
| return nil, errors.Annotate(err, "cannot read charm archive") | |
| } | |
| defer archive.Close() | |
| default: | |
| return nil, errors.Errorf("unknown charm type %T", ch) | |
| } | |
| curl, err := c.UploadCharm(curl, archive) | |
| if err != nil { | |
| return nil, errors.Trace(err) | |
| } | |
| return curl, nil | |
| } | |
| // UploadCharm sends the content to the API server using an HTTP post. | |
| func (c *Client) UploadCharm(curl *charm.URL, content io.ReadSeeker) (*charm.URL, error) { | |
| args := url.Values{} | |
| args.Add("series", curl.Series) | |
| args.Add("schema", curl.Schema) | |
| args.Add("revision", strconv.Itoa(curl.Revision)) | |
| apiURI := url.URL{Path: "/charms", RawQuery: args.Encode()} | |
| contentType := "application/zip" | |
| var resp params.CharmsResponse | |
| if err := c.httpPost(content, apiURI.String(), contentType, &resp); err != nil { | |
| return nil, errors.Trace(err) | |
| } | |
| curl, err := charm.ParseURL(resp.CharmURL) | |
| if err != nil { | |
| return nil, errors.Annotatef(err, "bad charm URL in response") | |
| } | |
| return curl, nil | |
| } | |
| type minJujuVersionErr struct { | |
| *errors.Err | |
| } | |
| func minVersionError(minver, jujuver version.Number) error { | |
| err := errors.NewErr("charm's min version (%s) is higher than this juju environment's version (%s)", | |
| minver, jujuver) | |
| err.SetLocation(1) | |
| return minJujuVersionErr{&err} | |
| } | |
| func (c *Client) validateCharmVersion(ch charm.Charm) error { | |
| minver := ch.Meta().MinJujuVersion | |
| if minver != version.Zero { | |
| agentver, err := c.AgentVersion() | |
| if err != nil { | |
| return errors.Trace(err) | |
| } | |
| if minver.Compare(agentver) > 0 { | |
| return minVersionError(minver, agentver) | |
| } | |
| } | |
| return nil | |
| } | |
| // TODO(ericsnow) Use charmstore.CharmID for AddCharm() & AddCharmWithAuth(). | |
| // AddCharm adds the given charm URL (which must include revision) to | |
| // the model, if it does not exist yet. Local charms are not | |
| // supported, only charm store URLs. See also AddLocalCharm() in the | |
| // client-side API. | |
| // | |
| // If the AddCharm API call fails because of an authorization error | |
| // when retrieving the charm from the charm store, an error | |
| // satisfying params.IsCodeUnauthorized will be returned. | |
| func (c *Client) AddCharm(curl *charm.URL, channel csparams.Channel) error { | |
| args := params.AddCharm{ | |
| URL: curl.String(), | |
| Channel: string(channel), | |
| } | |
| if err := c.facade.FacadeCall("AddCharm", args, nil); err != nil { | |
| return errors.Trace(err) | |
| } | |
| return nil | |
| } | |
| // AddCharmWithAuthorization is like AddCharm except it also provides | |
| // the given charmstore macaroon for the juju server to use when | |
| // obtaining the charm from the charm store. The macaroon is | |
| // conventionally obtained from the /delegatable-macaroon endpoint in | |
| // the charm store. | |
| // | |
| // If the AddCharmWithAuthorization API call fails because of an | |
| // authorization error when retrieving the charm from the charm store, | |
| // an error satisfying params.IsCodeUnauthorized will be returned. | |
| func (c *Client) AddCharmWithAuthorization(curl *charm.URL, channel csparams.Channel, csMac *macaroon.Macaroon) error { | |
| args := params.AddCharmWithAuthorization{ | |
| URL: curl.String(), | |
| Channel: string(channel), | |
| CharmStoreMacaroon: csMac, | |
| } | |
| if err := c.facade.FacadeCall("AddCharmWithAuthorization", args, nil); err != nil { | |
| return errors.Trace(err) | |
| } | |
| return nil | |
| } | |
| // ResolveCharm resolves the best available charm URLs with series, for charm | |
| // locations without a series specified. | |
| func (c *Client) ResolveCharm(ref *charm.URL) (*charm.URL, error) { | |
| args := params.ResolveCharms{References: []string{ref.String()}} | |
| result := new(params.ResolveCharmResults) | |
| if err := c.facade.FacadeCall("ResolveCharms", args, result); err != nil { | |
| return nil, err | |
| } | |
| if len(result.URLs) == 0 { | |
| return nil, errors.New("unexpected empty response") | |
| } | |
| urlInfo := result.URLs[0] | |
| if urlInfo.Error != "" { | |
| return nil, errors.New(urlInfo.Error) | |
| } | |
| url, err := charm.ParseURL(urlInfo.URL) | |
| if err != nil { | |
| return nil, errors.Trace(err) | |
| } | |
| return url, nil | |
| } | |
| // OpenCharm streams out the identified charm from the controller via | |
| // the API. | |
| func (c *Client) OpenCharm(curl *charm.URL) (io.ReadCloser, error) { | |
| query := make(url.Values) | |
| query.Add("url", curl.String()) | |
| query.Add("file", "*") | |
| return c.OpenURI("/charms", query) | |
| } | |
| // OpenURI performs a GET on a Juju HTTP endpoint returning the | |
| func (c *Client) OpenURI(uri string, query url.Values) (io.ReadCloser, error) { | |
| // The returned httpClient sets the base url to /model/<uuid> if it can. | |
| httpClient, err := c.st.HTTPClient() | |
| if err != nil { | |
| return nil, errors.Trace(err) | |
| } | |
| blob, err := openBlob(httpClient, uri, query) | |
| if err != nil { | |
| return nil, errors.Trace(err) | |
| } | |
| return blob, nil | |
| } | |
| // NewCharmDownloader returns a new charm downloader that wraps the | |
| // provided API client. | |
| func NewCharmDownloader(client *Client) *downloader.Downloader { | |
| dlr := &downloader.Downloader{ | |
| OpenBlob: func(url *url.URL) (io.ReadCloser, error) { | |
| curl, err := charm.ParseURL(url.String()) | |
| if err != nil { | |
| return nil, errors.Annotate(err, "did not receive a valid charm URL") | |
| } | |
| reader, err := client.OpenCharm(curl) | |
| if err != nil { | |
| return nil, errors.Trace(err) | |
| } | |
| return reader, nil | |
| }, | |
| } | |
| return dlr | |
| } | |
| // UploadTools uploads tools at the specified location to the API server over HTTPS. | |
| func (c *Client) UploadTools(r io.ReadSeeker, vers version.Binary, additionalSeries ...string) (tools.List, error) { | |
| endpoint := fmt.Sprintf("/tools?binaryVersion=%s&series=%s", vers, strings.Join(additionalSeries, ",")) | |
| contentType := "application/x-tar-gz" | |
| var resp params.ToolsResult | |
| if err := c.httpPost(r, endpoint, contentType, &resp); err != nil { | |
| return nil, errors.Trace(err) | |
| } | |
| return resp.ToolsList, nil | |
| } | |
| func (c *Client) httpPost(content io.ReadSeeker, endpoint, contentType string, response interface{}) error { | |
| req, err := http.NewRequest("POST", endpoint, nil) | |
| if err != nil { | |
| return errors.Annotate(err, "cannot create upload request") | |
| } | |
| req.Header.Set("Content-Type", contentType) | |
| // The returned httpClient sets the base url to /model/<uuid> if it can. | |
| httpClient, err := c.st.HTTPClient() | |
| if err != nil { | |
| return errors.Trace(err) | |
| } | |
| if err := httpClient.Do(req, content, response); err != nil { | |
| return errors.Trace(err) | |
| } | |
| return nil | |
| } | |
| // APIHostPorts returns a slice of network.HostPort for each API server. | |
| func (c *Client) APIHostPorts() ([][]network.HostPort, error) { | |
| var result params.APIHostPortsResult | |
| if err := c.facade.FacadeCall("APIHostPorts", nil, &result); err != nil { | |
| return nil, err | |
| } | |
| return result.NetworkHostsPorts(), nil | |
| } | |
| // AgentVersion reports the version number of the api server. | |
| func (c *Client) AgentVersion() (version.Number, error) { | |
| var result params.AgentVersionResult | |
| if err := c.facade.FacadeCall("AgentVersion", nil, &result); err != nil { | |
| return version.Number{}, err | |
| } | |
| return result.Version, nil | |
| } | |
| // websocketDial is called instead of dialer.Dial so we can override it in | |
| // tests. | |
| var websocketDial = func(dialer *websocket.Dialer, urlStr string, requestHeader http.Header) (base.Stream, error) { | |
| c, _, err := dialer.Dial(urlStr, requestHeader) | |
| if err != nil { | |
| return nil, errors.Trace(err) | |
| } | |
| return c, nil | |
| } | |
| // WatchDebugLog returns a channel of structured Log Messages. Only log entries | |
| // that match the filtering specified in the DebugLogParams are returned. | |
| func (c *Client) WatchDebugLog(args common.DebugLogParams) (<-chan common.LogMessage, error) { | |
| return common.StreamDebugLog(c.st, args) | |
| } |