Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

juju run: relation context #705

Closed
wants to merge 26 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions api/facadeversions.go
Expand Up @@ -35,6 +35,7 @@ var facadeVersions = map[string]int{
"Rsyslog": 0,
"Uniter": 1,
"Actions": 0,
"RunCommand": 1,
}

// bestVersion tries to find the newest version in the version list that we can
Expand Down
47 changes: 47 additions & 0 deletions api/runcmd/client.go
@@ -0,0 +1,47 @@
package runcmd

import (
"github.com/juju/errors"

"github.com/juju/juju/api/base"
"github.com/juju/juju/apiserver/params"
)

const runcmdFacade = "RunCommand"

// Client provides access to the juju run command, used to execute
// commands for a given machine, unit, and/or service.
type Client struct {
base.ClientFacade
facade base.FacadeCaller
}

// NewClient returns a new RunCommand client.
func NewClient(caller base.APICallCloser) *Client {
frontend, backend := base.NewClientFacade(caller, runcmdFacade)
return &Client{ClientFacade: frontend, facade: backend}
}

// RunOnAllMachines runs the command on all the machines with the specified
// timeout.
func (c *Client) RunOnAllMachines(run params.RunParamsV1) ([]params.RunResult, error) {
var results params.RunResults
args := params.RunParams{Commands: run.Commands, Timeout: run.Timeout}
err := c.facade.FacadeCall("RunOnAllMachines", args, &results)
return results.Results, err
}

// Run the Commands specified on the machines identified through the ids
// provided in the machines, services and units slices.
func (c *Client) Run(run params.RunParamsV1) ([]params.RunResult, error) {
if c.facade.BestAPIVersion() < 1 {
if run.Context != nil {
if len(run.Context.Relation) > 0 || len(run.Context.RemoteUnit) > 0 {
return nil, errors.NotImplementedf("The server does not support the supplied option(s): --relation, --remote-unit. (apiversion: %d)", c.facade.BestAPIVersion())
}
}
}
var results params.RunResults
err := c.facade.FacadeCall("Run", run, &results)
return results.Results, err
}
206 changes: 206 additions & 0 deletions api/runcmd/client_test.go
@@ -0,0 +1,206 @@
// Copyright 2014 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package runcmd_test

import (
"fmt"
"io/ioutil"
"path/filepath"

jc "github.com/juju/testing/checkers"
"github.com/juju/utils/exec"
gc "gopkg.in/check.v1"

"github.com/juju/juju/api/runcmd"
"github.com/juju/juju/apiserver/params"
jujutesting "github.com/juju/juju/juju/testing"
"github.com/juju/juju/network"
"github.com/juju/juju/state"
"github.com/juju/juju/testing"
"github.com/juju/names"
)

type runcmdSuite struct {
jujutesting.JujuConnSuite
runcmd *runcmd.Client
}

var _ = gc.Suite(&runcmdSuite{})

func (s *runcmdSuite) SetUpTest(c *gc.C) {
s.JujuConnSuite.SetUpTest(c)
s.runcmd = runcmd.NewClient(s.APIState)
c.Assert(s.runcmd, gc.NotNil)
}

func (s *runcmdSuite) TestRunOnAllMachines(c *gc.C) {
// Make three machines.
s.addMachineWithAddress(c, "10.3.2.1")
s.addMachineWithAddress(c, "10.3.2.2")
s.addMachineWithAddress(c, "10.3.2.3")

s.mockSSH(c, echoInput)

runparams := params.RunParamsV1{
Commands: "hostname",
Timeout: testing.LongWait,
}
results, err := s.runcmd.RunOnAllMachines(runparams)
c.Assert(err, gc.IsNil)
c.Assert(results, gc.HasLen, 3)
var expectedResults []params.RunResult
for i := 0; i < 3; i++ {
expectedResults = append(expectedResults,
params.RunResult{
ExecResponse: exec.ExecResponse{Stdout: []byte("juju-run --no-context 'hostname'\n")},
MachineId: fmt.Sprint(i),
})
}

c.Assert(results, jc.DeepEquals, expectedResults)
}

func (s *runcmdSuite) TestRunMachineAndService(c *gc.C) {
// Make three machines.
s.addMachineWithAddress(c, "10.3.2.1")

charm := s.AddTestingCharm(c, "dummy")
owner := s.Factory.MakeUser(c, nil).Tag()
magic, err := s.State.AddService("magic", owner.String(), charm, nil)
c.Assert(err, gc.IsNil)
s.addUnit(c, magic)
s.addUnit(c, magic)

s.mockSSH(c, echoInput)

results, err := s.runcmd.Run(
params.RunParamsV1{
Commands: "hostname",
Timeout: testing.LongWait,
Targets: []string{names.NewMachineTag("0").String(), magic.Tag().String()},
})
c.Assert(err, gc.IsNil)
c.Assert(results, gc.HasLen, 3)
expectedResults := []params.RunResult{
params.RunResult{
ExecResponse: exec.ExecResponse{Stdout: []byte("juju-run --no-context 'hostname'\n")},
MachineId: "0",
},
params.RunResult{
ExecResponse: exec.ExecResponse{Stdout: []byte("juju-run magic/0 'hostname'\n")},
MachineId: "1",
UnitId: "magic/0",
},
params.RunResult{
ExecResponse: exec.ExecResponse{Stdout: []byte("juju-run magic/1 'hostname'\n")},
MachineId: "2",
UnitId: "magic/1",
},
}

c.Assert(results, jc.DeepEquals, expectedResults)
}

func (s *runcmdSuite) TestRunWithContext(c *gc.C) {
// Make three machines.
s.addMachineWithAddress(c, "10.3.2.1")

owner := s.Factory.MakeUser(c, nil).Tag()
charm := s.AddTestingCharm(c, "wordpress")
wordpress, err := s.State.AddService("wordpress", owner.String(), charm, nil)
c.Assert(err, gc.IsNil)
unit := s.addUnit(c, wordpress)

s.addRelatedService(c, "wordpress", "mysql", unit)
s.mockSSH(c, echoInput)

results, err := s.runcmd.Run(
params.RunParamsV1{
Commands: "hostname",
Timeout: testing.LongWait,
Targets: []string{wordpress.Tag().String()},
Context: &params.RunContext{Relation: "mysql"},
})
c.Assert(err, gc.IsNil)
c.Assert(results, gc.HasLen, 1)
expectedResults := []params.RunResult{
params.RunResult{
ExecResponse: exec.ExecResponse{Stdout: []byte("juju-run --relation 0 wordpress/0 'hostname'\n")},
MachineId: "1",
UnitId: "wordpress/0",
},
}

c.Assert(results, jc.DeepEquals, expectedResults)
}

func (s *runcmdSuite) addMachine(c *gc.C) *state.Machine {
machine, err := s.State.AddMachine("quantal", state.JobHostUnits)
c.Assert(err, gc.IsNil)
return machine
}

func (s *runcmdSuite) mockSSH(c *gc.C, cmd string) {
testbin := c.MkDir()
fakessh := filepath.Join(testbin, "ssh")
s.PatchEnvPathPrepend(testbin)
err := ioutil.WriteFile(fakessh, []byte(cmd), 0755)
c.Assert(err, gc.IsNil)
}

func (s *runcmdSuite) addMachineWithAddress(c *gc.C, address string) *state.Machine {
machine := s.addMachine(c)
machine.SetAddresses(network.NewAddress(address, network.ScopeUnknown))
return machine
}

func (s *runcmdSuite) addUnit(c *gc.C, service *state.Service) *state.Unit {
unit, err := service.AddUnit()
c.Assert(err, gc.IsNil)
err = unit.AssignToNewMachine()
c.Assert(err, gc.IsNil)
mId, err := unit.AssignedMachineId()
c.Assert(err, gc.IsNil)
machine, err := s.State.Machine(mId)
c.Assert(err, gc.IsNil)
machine.SetAddresses(network.NewAddress("10.3.2.1", network.ScopeUnknown))
return unit
}

func (s *runcmdSuite) addRelatedService(c *gc.C, firstSvc, relatedSvc string, unit *state.Unit) (*state.Relation, *state.Service, *state.Unit) {
relatedService := s.AddTestingService(c, relatedSvc, s.AddTestingCharm(c, relatedSvc))
rel := s.addRelation(c, firstSvc, relatedSvc)
relUnit, err := rel.Unit(unit)
c.Assert(err, gc.IsNil)
err = relUnit.EnterScope(nil)
c.Assert(err, gc.IsNil)
s.addUnit(c, relatedService)
relatedUnit, err := s.State.Unit(relatedSvc + "/0")
c.Assert(err, gc.IsNil)
return rel, relatedService, relatedUnit
}

func (s *runcmdSuite) addRelation(c *gc.C, first, second string) *state.Relation {
eps, err := s.State.InferEndpoints(first, second)
c.Assert(err, gc.IsNil)
rel, err := s.State.AddRelation(eps...)
c.Assert(err, gc.IsNil)
return rel
}

var echoInputShowArgs = `#!/bin/bash
# Write the args to stderr
echo "$*" >&2
# And echo stdin to stdout
while read line
do echo $line
done <&0
`

var echoInput = `#!/bin/bash
# And echo stdin to stdout
while read line
do echo $line
done <&0
`
14 changes: 14 additions & 0 deletions api/runcmd/package_test.go
@@ -0,0 +1,14 @@
// Copyright 2014 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package runcmd_test

import (
stdtesting "testing"

"github.com/juju/juju/testing"
)

func TestAll(t *stdtesting.T) {
testing.MgoTestPackage(t)
}
1 change: 1 addition & 0 deletions apiserver/allfacades.go
Expand Up @@ -24,6 +24,7 @@ import (
_ "github.com/juju/juju/apiserver/provisioner"
_ "github.com/juju/juju/apiserver/reboot"
_ "github.com/juju/juju/apiserver/rsyslog"
_ "github.com/juju/juju/apiserver/runcmd"
_ "github.com/juju/juju/apiserver/uniter"
_ "github.com/juju/juju/apiserver/upgrader"
_ "github.com/juju/juju/apiserver/usermanager"
Expand Down
17 changes: 17 additions & 0 deletions apiserver/params/internal.go
Expand Up @@ -614,6 +614,23 @@ type RunParams struct {
Units []string
}

// RunCommands holds the information for a `juju run` command using
// version 1 of the runcmd API
type RunParamsV1 struct {
Commands string
Targets []string
Context *RunContext
Timeout time.Duration
}

// RunContext holds the information for a `juju-run` command
// that was provided the --relation option. Used with V1 of the
// runcmd API.
type RunContext struct {
Relation string
RemoteUnit string
}

// RunResult contains the result from an individual run call on a machine.
// UnitId is populated if the command was run inside the unit context.
type RunResult struct {
Expand Down
14 changes: 14 additions & 0 deletions apiserver/runcmd/package_test.go
@@ -0,0 +1,14 @@
// Copyright 2014 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package runcmd_test

import (
stdtesting "testing"

"github.com/juju/juju/testing"
)

func TestAll(t *stdtesting.T) {
testing.MgoTestPackage(t)
}