// authAndActionFromTagFn first authenticates the request, and then returns // a function with which to authenticate and retrieve each action in the // request. func (u *UniterAPI) authAndActionFromTagFn() (func(string) (*state.Action, error), error) { canAccess, err := u.accessUnit() if err != nil { return nil, err } unit, ok := u.auth.GetAuthTag().(names.UnitTag) if !ok { return nil, fmt.Errorf("calling entity is not a unit") } return func(tag string) (*state.Action, error) { actionTag, err := names.ParseActionTag(tag) if err != nil { return nil, err } unitTag := actionTag.PrefixTag() if unitTag != unit { return nil, common.ErrPerm } if !canAccess(unitTag) { return nil, common.ErrPerm } return u.st.ActionByTag(actionTag) }, nil }
// authAndActionFromTagFn first authenticates the request, and then returns // a function with which to authenticate and retrieve each action in the // request. func (u *UniterAPIV3) authAndActionFromTagFn() (func(string) (*state.Action, error), error) { canAccess, err := u.accessUnit() if err != nil { return nil, err } unit, ok := u.auth.GetAuthTag().(names.UnitTag) if !ok { return nil, fmt.Errorf("calling entity is not a unit") } return func(tag string) (*state.Action, error) { actionTag, err := names.ParseActionTag(tag) if err != nil { return nil, err } action, err := u.st.ActionByTag(actionTag) if err != nil { return nil, err } receiverTag, err := names.ActionReceiverTag(action.Receiver()) if err != nil { return nil, err } if unit != receiverTag { return nil, common.ErrPerm } if !canAccess(receiverTag) { return nil, common.ErrPerm } return action, nil }, nil }
// actionIfPermitted returns an action, only if canAccess permits, // returns common.ErrPerm if not permitted func (u *UniterAPI) actionIfPermitted(tag string) (*state.Action, error) { canAccess, err := u.accessUnit() if err != nil { return nil, err } // Use the currently authenticated unit to get the endpoint. whichUnit, ok := u.auth.GetAuthTag().(names.UnitTag) if !ok { return nil, fmt.Errorf("entity is not a unit") } // this Unit must match the Action's prefix. actionTag, err := names.ParseActionTag(tag) if err != nil { return nil, err } unitTag := actionTag.PrefixTag() // The Unit is querying for another Unit's Action. if unitTag != whichUnit { return nil, common.ErrPerm } // The Unit does not have access. if !canAccess(unitTag) { return nil, common.ErrPerm } return u.st.ActionByTag(actionTag) }
func (s *actionSuite) TestParseActionTag(c *gc.C) { for i, t := range parseActionTagTests { c.Logf("test %d: %s", i, t.tag) got, err := names.ParseActionTag(t.tag) if err != nil || t.err != nil { c.Check(err, gc.DeepEquals, t.err) continue } c.Check(got, gc.FitsTypeOf, t.expected) c.Check(got, gc.Equals, t.expected) } }
func (s *actionSuite) TestParseActionTag(c *gc.C) { parseActionTagTests := []struct { tag string expected names.Tag err error }{ { tag: "", expected: nil, err: names.InvalidTagError("", ""), }, { tag: "action-good" + names.ActionMarker + "123", expected: names.NewActionTag("good" + names.ActionMarker + "123"), err: nil, }, { tag: "action-good/0" + names.ActionMarker + "123", expected: names.NewActionTag("good/0" + names.ActionMarker + "123"), err: nil, }, { tag: "action-bad/00" + names.ActionMarker + "123", expected: nil, err: names.InvalidTagError("action-bad/00"+names.ActionMarker+"123", names.ActionTagKind), }, { tag: "dave", expected: nil, err: names.InvalidTagError("dave", ""), }, { tag: "action-dave/0", expected: nil, err: names.InvalidTagError("action-dave/0", names.ActionTagKind), }, { tag: "action", expected: nil, err: names.InvalidTagError("action", ""), }, { tag: "user-dave", expected: nil, err: names.InvalidTagError("user-dave", names.ActionTagKind), }} for i, t := range parseActionTagTests { c.Logf("test %d: %s", i, t.tag) got, err := names.ParseActionTag(t.tag) if err != nil || t.err != nil { c.Check(err, gc.DeepEquals, t.err) continue } c.Check(got, gc.FitsTypeOf, t.expected) c.Check(got, gc.Equals, t.expected) } }
// Actions returns the Actions by Tags passed and ensures that the Unit asking // for them is the same Unit that has the Actions. func (u *UniterAPI) Actions(args params.Entities) (params.ActionsQueryResults, error) { nothing := params.ActionsQueryResults{} canAccess, err := u.accessUnit() if err != nil { return nothing, err } results := params.ActionsQueryResults{ ActionsQueryResults: make([]params.ActionsQueryResult, len(args.Entities)), } for i, actionQuery := range args.Entities { // Use the currently authenticated unit to get the endpoint. whichUnit, ok := u.auth.GetAuthTag().(names.UnitTag) if !ok { return nothing, fmt.Errorf("entity is not a unit") } // this Unit must match the Action's prefix. actionTag, err := names.ParseActionTag(actionQuery.Tag) if err != nil { return nothing, err } unitTag := actionTag.PrefixTag() // The Unit is querying for another Unit's Action. if unitTag != whichUnit { return nothing, common.ErrPerm } // The Unit does not have access. if !canAccess(unitTag) { return nothing, common.ErrPerm } actionQueryResult, err := u.getOneActionByTag(actionTag) if err == nil { results.ActionsQueryResults[i] = actionQueryResult } results.ActionsQueryResults[i].Error = common.ServerError(err) } return results, nil }
func (s *actionSuite) TestFindActionTagsByPrefix(c *gc.C) { // NOTE: full testing with multiple matches has been moved to state package. arg := params.Actions{Actions: []params.Action{{Receiver: s.wordpressUnit.Tag().String(), Name: "fakeaction", Parameters: map[string]interface{}{}}}} r, err := s.action.Enqueue(arg) c.Assert(err, gc.Equals, nil) c.Assert(r.Results, gc.HasLen, len(arg.Actions)) actionTag, err := names.ParseActionTag(r.Results[0].Action.Tag) c.Assert(err, gc.Equals, nil) prefix := actionTag.Id()[:7] tags, err := s.action.FindActionTagsByPrefix(params.FindTags{Prefixes: []string{prefix}}) c.Assert(err, gc.Equals, nil) entities, ok := tags.Matches[prefix] c.Assert(ok, gc.Equals, true) c.Assert(len(entities), gc.Equals, 1) c.Assert(entities[0].Tag, gc.Equals, actionTag.String()) }
// AuthAndActionFromTagFn takes in an authorizer function and a function that can fetch action by tags from state // and returns a function that can fetch an action from state by id and check the authorization. func AuthAndActionFromTagFn(canAccess AuthFunc, getActionByTag func(names.ActionTag) (state.Action, error)) func(string) (state.Action, error) { return func(tag string) (state.Action, error) { actionTag, err := names.ParseActionTag(tag) if err != nil { return nil, errors.Trace(err) } action, err := getActionByTag(actionTag) if err != nil { return nil, errors.Trace(err) } receiverTag, err := names.ActionReceiverTag(action.Receiver()) if err != nil { return nil, errors.Trace(err) } if !canAccess(receiverTag) { return nil, ErrPerm } return action, nil } }
// SetUp is part of the watcher.StringsHandler interface. func (h *handler) SetUp() (watcher.StringsWatcher, error) { actions, err := h.config.Facade.RunningActions(h.config.MachineTag) if err != nil { return nil, errors.Trace(err) } // We try to cancel any running action before starting up so actions don't linger around // We *should* really have only one action coming up here if the execution is serial but // this is best effort anyway. for _, action := range actions { tag, err := names.ParseActionTag(action.Action.Tag) if err != nil { logger.Infof("tried to cancel action %s but failed with error %v", action.Action.Tag, err) continue } err = h.config.Facade.ActionFinish(tag, params.ActionFailed, nil, "action cancelled") if err != nil { logger.Infof("tried to cancel action %s but failed with error %v", action.Action.Tag, err) } } return h.config.Facade.WatchActionNotifications(h.config.MachineTag) }
func resultToMap(result params.ActionResult) map[string]interface{} { item := map[string]interface{}{} if result.Error != nil { item["error"] = result.Error.Error() } if result.Action != nil { atag, err := names.ParseActionTag(result.Action.Tag) if err != nil { item["id"] = result.Action.Tag } else { item["id"] = atag.Id() } rtag, err := names.ParseUnitTag(result.Action.Receiver) if err != nil { item["unit"] = result.Action.Receiver } else { item["unit"] = rtag.Id() } } item["status"] = result.Status return item }
// Run implements Command.Run. func (c *collectMetricsCommand) Run(ctx *cmd.Context) error { runnerClient, err := newRunClient(c.ModelCommandBase) if err != nil { return errors.Trace(err) } defer runnerClient.Close() runParams := params.RunParams{ Timeout: commandTimeout, Units: c.units, Services: c.services, Commands: "nc -U ../metrics-collect.socket", } // trigger metrics collection runResults, err := runnerClient.Run(runParams) if err != nil { return errors.Trace(err) } // We want to wait for the action results indefinitely. Discard the tick. wait := time.NewTimer(0 * time.Second) _ = <-wait.C // trigger sending metrics in parallel resultChannel := make(chan string, len(runResults)) for _, result := range runResults { r := result if r.Error != nil { fmt.Fprintf(ctx.Stdout, "failed to collect metrics: %v\n", err) resultChannel <- "invalid id" continue } tag, err := names.ParseActionTag(r.Action.Tag) if err != nil { fmt.Fprintf(ctx.Stdout, "failed to collect metrics: %v\n", err) resultChannel <- "invalid id" continue } actionResult, err := getActionResult(runnerClient, tag.Id(), wait) if err != nil { fmt.Fprintf(ctx.Stdout, "failed to collect metrics: %v\n", err) resultChannel <- "invalid id" continue } unitId, err := parseActionResult(actionResult) if err != nil { fmt.Fprintf(ctx.Stdout, "failed to collect metrics: %v\n", err) resultChannel <- "invalid id" continue } go func() { defer func() { resultChannel <- unitId }() sendParams := params.RunParams{ Timeout: commandTimeout, Units: []string{unitId}, Commands: "nc -U ../metrics-send.socket", } sendResults, err := runnerClient.Run(sendParams) if err != nil { fmt.Fprintf(ctx.Stdout, "failed to send metrics for unit %v: %v\n", unitId, err) return } if len(sendResults) != 1 { fmt.Fprintf(ctx.Stdout, "failed to send metrics for unit %v\n", unitId) return } if sendResults[0].Error != nil { fmt.Fprintf(ctx.Stdout, "failed to send metrics for unit %v: %v\n", unitId, sendResults[0].Error) return } tag, err := names.ParseActionTag(sendResults[0].Action.Tag) if err != nil { fmt.Fprintf(ctx.Stdout, "failed to send metrics for unit %v: %v\n", unitId, err) return } actionResult, err := getActionResult(runnerClient, tag.Id(), wait) if err != nil { fmt.Fprintf(ctx.Stdout, "failed to send metrics for unit %v: %v\n", unitId, err) return } stdout, stderr, err := parseRunOutput(actionResult) if err != nil { fmt.Fprintf(ctx.Stdout, "failed to send metrics for unit %v: %v\n", unitId, err) return } if stdout != "ok" { fmt.Fprintf(ctx.Stdout, "failed to send metrics for unit %v: %v\n", unitId, errors.New(stderr)) } }() } for _ = range runResults { // The default is to wait forever for the command to finish. select { case <-resultChannel: } } return nil }
func (s *DoSuite) TestRun(c *gc.C) { tests := []struct { should string withArgs []string withAPIErr string withActionResults []params.ActionResult expectedActionEnqueued params.Action expectedErr string }{{ should: "fail with multiple results", withArgs: []string{validUnitId, "some-action"}, withActionResults: []params.ActionResult{ {Action: ¶ms.Action{Tag: validActionTagString}}, {Action: ¶ms.Action{Tag: validActionTagString}}, }, expectedErr: "illegal number of results returned", }, { should: "fail with API error", withArgs: []string{validUnitId, "some-action"}, withActionResults: []params.ActionResult{{ Action: ¶ms.Action{Tag: validActionTagString}}, }, withAPIErr: "something wrong in API", expectedErr: "something wrong in API", }, { should: "fail with error in result", withArgs: []string{validUnitId, "some-action"}, withActionResults: []params.ActionResult{{ Action: ¶ms.Action{Tag: validActionTagString}, Error: common.ServerError(errors.New("database error")), }}, expectedErr: "database error", }, { should: "fail with invalid tag in result", withArgs: []string{validUnitId, "some-action"}, withActionResults: []params.ActionResult{{ Action: ¶ms.Action{Tag: invalidActionTagString}, }}, expectedErr: "\"" + invalidActionTagString + "\" is not a valid action tag", }, { should: "fail with missing file passed", withArgs: []string{validUnitId, "some-action", "--params", s.dir + "/" + "missing.yml", }, expectedErr: "open .*missing.yml: " + utils.NoSuchFileErrRegexp, }, { should: "fail with invalid yaml in file", withArgs: []string{validUnitId, "some-action", "--params", s.dir + "/" + "invalidParams.yml", }, expectedErr: "yaml: line 3: mapping values are not allowed in this context", }, { should: "fail with invalid UTF in file", withArgs: []string{validUnitId, "some-action", "--params", s.dir + "/" + "invalidUTF.yml", }, expectedErr: "yaml: invalid leading UTF-8 octet", }, { should: "fail with invalid YAML passed as arg and no --string-args", withArgs: []string{validUnitId, "some-action", "foo.bar=\""}, expectedErr: "yaml: found unexpected end of stream", }, { should: "enqueue a basic action with no params", withArgs: []string{validUnitId, "some-action"}, withActionResults: []params.ActionResult{{ Action: ¶ms.Action{Tag: validActionTagString}, }}, expectedActionEnqueued: params.Action{ Name: "some-action", Parameters: map[string]interface{}{}, Receiver: names.NewUnitTag(validUnitId).String(), }, }, { should: "enqueue an action with some explicit params", withArgs: []string{validUnitId, "some-action", "out.name=bar", "out.kind=tmpfs", "out.num=3", "out.boolval=y", }, withActionResults: []params.ActionResult{{ Action: ¶ms.Action{Tag: validActionTagString}, }}, expectedActionEnqueued: params.Action{ Name: "some-action", Receiver: names.NewUnitTag(validUnitId).String(), Parameters: map[string]interface{}{ "out": map[string]interface{}{ "name": "bar", "kind": "tmpfs", "num": 3, "boolval": true, }, }, }, }, { should: "enqueue an action with some raw string params", withArgs: []string{validUnitId, "some-action", "--string-args", "out.name=bar", "out.kind=tmpfs", "out.num=3", "out.boolval=y", }, withActionResults: []params.ActionResult{{ Action: ¶ms.Action{Tag: validActionTagString}, }}, expectedActionEnqueued: params.Action{ Name: "some-action", Receiver: names.NewUnitTag(validUnitId).String(), Parameters: map[string]interface{}{ "out": map[string]interface{}{ "name": "bar", "kind": "tmpfs", "num": "3", "boolval": "y", }, }, }, }, { should: "enqueue an action with file params plus CLI args", withArgs: []string{validUnitId, "some-action", "--params", s.dir + "/" + "validParams.yml", "compression.kind=gz", "compression.fast=true", }, withActionResults: []params.ActionResult{{ Action: ¶ms.Action{Tag: validActionTagString}, }}, expectedActionEnqueued: params.Action{ Name: "some-action", Receiver: names.NewUnitTag(validUnitId).String(), Parameters: map[string]interface{}{ "out": "name", "compression": map[string]interface{}{ "kind": "gz", "quality": "high", "fast": true, }, }, }, }, { should: "enqueue an action with file params and explicit params", withArgs: []string{validUnitId, "some-action", "out.name=bar", "out.kind=tmpfs", "compression.quality.speed=high", "compression.quality.size=small", "--params", s.dir + "/" + "validParams.yml", }, withActionResults: []params.ActionResult{{ Action: ¶ms.Action{Tag: validActionTagString}, }}, expectedActionEnqueued: params.Action{ Name: "some-action", Receiver: names.NewUnitTag(validUnitId).String(), Parameters: map[string]interface{}{ "out": map[string]interface{}{ "name": "bar", "kind": "tmpfs", }, "compression": map[string]interface{}{ "kind": "xz", "quality": map[string]interface{}{ "speed": "high", "size": "small", }, }, }, }, }} for i, t := range tests { func() { c.Logf("test %d: should %s:\n$ juju actions do %s\n", i, t.should, strings.Join(t.withArgs, " ")) fakeClient := &fakeAPIClient{ actionResults: t.withActionResults, } if t.withAPIErr != "" { fakeClient.apiErr = errors.New(t.withAPIErr) } restore := s.patchAPIClient(fakeClient) defer restore() wrappedCommand, _ := action.NewDoCommand() args := append([]string{"-e", "dummyenv"}, t.withArgs...) ctx, err := testing.RunCommand(c, wrappedCommand, args...) if t.expectedErr != "" || t.withAPIErr != "" { c.Check(err, gc.ErrorMatches, t.expectedErr) } else { c.Assert(err, gc.IsNil) // Before comparing, double-check to avoid // panics in malformed tests. c.Assert(len(t.withActionResults), gc.Equals, 1) // Make sure the test's expected Action was // non-nil and correct. c.Assert(t.withActionResults[0].Action, gc.NotNil) expectedTag, err := names.ParseActionTag(t.withActionResults[0].Action.Tag) c.Assert(err, gc.IsNil) // Make sure the CLI responded with the expected tag keyToCheck := "Action queued with id" expectedMap := map[string]string{keyToCheck: expectedTag.Id()} outputResult := ctx.Stdout.(*bytes.Buffer).Bytes() resultMap := make(map[string]string) err = yaml.Unmarshal(outputResult, &resultMap) c.Assert(err, gc.IsNil) c.Check(resultMap, jc.DeepEquals, expectedMap) // Make sure the Action sent to the API to be // enqueued was indeed the expected map enqueued := fakeClient.EnqueuedActions() c.Assert(enqueued.Actions, gc.HasLen, 1) c.Check(enqueued.Actions[0], jc.DeepEquals, t.expectedActionEnqueued) } }() } }
func (c *doCommand) Run(ctx *cmd.Context) error { api, err := c.NewActionAPIClient() if err != nil { return err } defer api.Close() actionParams := map[string]interface{}{} if c.paramsYAML.Path != "" { b, err := c.paramsYAML.Read(ctx) if err != nil { return err } err = yaml.Unmarshal(b, &actionParams) if err != nil { return err } conformantParams, err := common.ConformYAML(actionParams) if err != nil { return err } betterParams, ok := conformantParams.(map[string]interface{}) if !ok { return errors.New("params must contain a YAML map with string keys") } actionParams = betterParams } // If we had explicit args {..., [key, key, key, key, value], ...} // then iterate and set params ..., key.key.key.key=value, ... for _, argSlice := range c.args { valueIndex := len(argSlice) - 1 keys := argSlice[:valueIndex] value := argSlice[valueIndex] cleansedValue := interface{}(value) if !c.parseStrings { err := yaml.Unmarshal([]byte(value), &cleansedValue) if err != nil { return err } } // Insert the value in the map. addValueToMap(keys, cleansedValue, actionParams) } conformantParams, err := common.ConformYAML(actionParams) if err != nil { return err } typedConformantParams, ok := conformantParams.(map[string]interface{}) if !ok { return errors.Errorf("params must be a map, got %T", typedConformantParams) } actionParam := params.Actions{ Actions: []params.Action{{ Receiver: c.unitTag.String(), Name: c.actionName, Parameters: actionParams, }}, } results, err := api.Enqueue(actionParam) if err != nil { return err } if len(results.Results) != 1 { return errors.New("illegal number of results returned") } result := results.Results[0] if result.Error != nil { return result.Error } if result.Action == nil { return errors.New("action failed to enqueue") } tag, err := names.ParseActionTag(result.Action.Tag) if err != nil { return err } output := map[string]string{"Action queued with id": tag.Id()} return c.out.Write(ctx, output) }
// entityToActionTag converts the params.Entity type to a names.ActionTag func entityToActionTag(entity params.Entity) (names.ActionTag, error) { return names.ParseActionTag(entity.Tag) }
func (c *runCommand) Run(ctx *cmd.Context) error { client, err := getRunAPIClient(c) if err != nil { return err } defer client.Close() var runResults []params.ActionResult if c.all { runResults, err = client.RunOnAllMachines(c.commands, c.timeout) } else { params := params.RunParams{ Commands: c.commands, Timeout: c.timeout, Machines: c.machines, Services: c.services, Units: c.units, } runResults, err = client.Run(params) } if err != nil { return block.ProcessBlockedError(err, block.BlockChange) } actionsToQuery := []actionQuery{} for _, result := range runResults { if result.Error != nil { fmt.Fprintf(ctx.GetStderr(), "couldn't queue one action: %v", result.Error) continue } actionTag, err := names.ParseActionTag(result.Action.Tag) if err != nil { fmt.Fprintf(ctx.GetStderr(), "got invalid action tag %v for receiver %v", result.Action.Tag, result.Action.Receiver) continue } receiverTag, err := names.ActionReceiverFromTag(result.Action.Receiver) if err != nil { fmt.Fprintf(ctx.GetStderr(), "got invalid action receiver tag %v for action %v", result.Action.Receiver, result.Action.Tag) continue } var receiverType string switch receiverTag.(type) { case names.UnitTag: receiverType = "UnitId" case names.MachineTag: receiverType = "MachineId" default: receiverType = "ReceiverId" } actionsToQuery = append(actionsToQuery, actionQuery{ actionTag: actionTag, receiver: actionReceiver{ receiverType: receiverType, tag: receiverTag, }}) } if len(actionsToQuery) == 0 { return errors.New("no actions were successfully enqueued, aborting") } values := []interface{}{} for len(actionsToQuery) > 0 { actionResults, err := client.Actions(entities(actionsToQuery)) if err != nil { return errors.Trace(err) } newActionsToQuery := []actionQuery{} for i, result := range actionResults.Results { if result.Error == nil { switch result.Status { case params.ActionRunning, params.ActionPending: newActionsToQuery = append(newActionsToQuery, actionsToQuery[i]) continue } } values = append(values, ConvertActionResults(result, actionsToQuery[i])) } actionsToQuery = newActionsToQuery // TODO: use a watcher instead of sleeping // this should be easier once we implement action grouping <-afterFunc(1 * time.Second) } // If we are just dealing with one result, AND we are using the smart // format, then pretend we were running it locally. if len(values) == 1 && c.out.Name() == "smart" { result, ok := values[0].(map[string]interface{}) if !ok { return errors.New("couldn't read action output") } if res, ok := result["Error"].(string); ok { return errors.New(res) } ctx.Stdout.Write(formatOutput(result, "Stdout")) ctx.Stderr.Write(formatOutput(result, "Stderr")) if code, ok := result["ReturnCode"].(int); ok && code != 0 { return cmd.NewRcPassthroughError(code) } // Message should always contain only errors. if res, ok := result["Message"].(string); ok && res != "" { ctx.Stderr.Write([]byte(res)) } return nil } return c.out.Write(ctx, values) }