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