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 ¶ms.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 }