Beispiel #1
0
// 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
}
Beispiel #2
0
// 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
}
Beispiel #3
0
// 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)
}
Beispiel #4
0
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)
	}
}
Beispiel #5
0
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)
	}
}
Beispiel #6
0
// 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
}
Beispiel #7
0
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())
}
Beispiel #8
0
// 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
	}
}
Beispiel #9
0
// 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)
}
Beispiel #10
0
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
}
Beispiel #11
0
// 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
}
Beispiel #12
0
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: &params.Action{Tag: validActionTagString}},
			{Action: &params.Action{Tag: validActionTagString}},
		},
		expectedErr: "illegal number of results returned",
	}, {
		should:   "fail with API error",
		withArgs: []string{validUnitId, "some-action"},
		withActionResults: []params.ActionResult{{
			Action: &params.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: &params.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: &params.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: &params.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: &params.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: &params.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: &params.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: &params.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)
			}
		}()
	}
}
Beispiel #13
0
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)
}
Beispiel #14
0
// entityToActionTag converts the params.Entity type to a names.ActionTag
func entityToActionTag(entity params.Entity) (names.ActionTag, error) {
	return names.ParseActionTag(entity.Tag)
}
Beispiel #15
0
Datei: run.go Projekt: makyo/juju
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)
}