Merge lp:~axwalk/juju-core/null-provider-take3 into lp:~go-bot/juju-core/trunk

Proposed by Andrew Wilkins
Status: Merged
Approved by: Andrew Wilkins
Approved revision: no longer in the source branch.
Merged at revision: 1889
Proposed branch: lp:~axwalk/juju-core/null-provider-take3
Merge into: lp:~go-bot/juju-core/trunk
Diff against target: 949 lines (+734/-12)
13 files modified
cmd/jujud/machine.go (+7/-1)
environs/cloudinit/cloudinit.go (+12/-2)
environs/cloudinit/cloudinit_test.go (+60/-4)
environs/manual/agent.go (+1/-1)
environs/manual/bootstrap.go (+9/-4)
environs/manual/bootstrap_test.go (+7/-0)
provider/all/all.go (+1/-0)
provider/null/config.go (+70/-0)
provider/null/config_test.go (+124/-0)
provider/null/environ.go (+173/-0)
provider/null/environ_test.go (+81/-0)
provider/null/instance.go (+68/-0)
provider/null/provider.go (+121/-0)
To merge this branch: bzr merge lp:~axwalk/juju-core/null-provider-take3
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+186954@code.launchpad.net

Commit message

Introduce the null provider

The null provider is a provider which
does, essentially, nothing. Instances
cannot be provisioned automatically;
they must be manually provisioned via
"add-machine ssh:...".

To bootstrap the environment, you name
the bootstrap node, which must be an
existing Ubuntu host, and it will be
"manually bootstrapped" via SSH. This
procedure is essentially the same as
manual provisioning.

Storage is managed by the bootstrap node.

TODO: storage is currently unauthenticated,
and the world can write to it. I will
address this next.

https://codereview.appspot.com/13255051/

Description of the change

Introduce the null provider

The null provider is a provider which
does, essentially, nothing. Instances
cannot be provisioned automatically;
they must be manually provisioned via
"add-machine ssh:...".

To bootstrap the environment, you name
the bootstrap node, which must be an
existing Ubuntu host, and it will be
"manually bootstrapped" via SSH. This
procedure is essentially the same as
manual provisioning.

Storage is managed by the bootstrap node.

TODO: storage is currently unauthenticated,
and the world can write to it. I will
address this next.

https://codereview.appspot.com/13255051/

To post a comment you must log in.
Revision history for this message
Andrew Wilkins (axwalk) wrote :

Reviewers: mp+186954_code.launchpad.net,

Message:
Please take a look.

Description:
Introduce the null provider

The null provider is a provider which
does, essentially, nothing. Instances
cannot be provisioned automatically;
they must be manually provisioned via
"add-machine ssh:...".

To bootstrap the environment, you name
the bootstrap node, which must be an
existing Ubuntu host, and it will be
"manually bootstrapped" via SSH. This
procedure is essentially the same as
manual provisioning.

Storage is managed by the bootstrap node.

TODO: storage is currently unauthenticated,
and the world can write to it. I will
address this next.

https://code.launchpad.net/~axwalk/juju-core/null-provider-take3/+merge/186954

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/13255051/

Affected files (+677, -16 lines):
   A [revision details]
   M cmd/juju/bootstrap.go
   M cmd/jujud/machine.go
   M environs/cloudinit/cloudinit.go
   M environs/manual/agent.go
   M environs/manual/bootstrap.go
   M environs/sshstorage/storage.go
   M provider/all/all.go
   A provider/null/config.go
   A provider/null/config_test.go
   A provider/null/environ.go
   A provider/null/environ_test.go
   A provider/null/instance.go
   A provider/null/provider.go

Revision history for this message
William Reade (fwereade) wrote :
Download full text (6.8 KiB)

OK, I am torn. There's some really great stuff here, and then there are
some really evil bits that are completely my fault because I didn't put
my foot down last time and demand that the local provider not get
special treatment.

So, not your fault, but we need to figure out the right path forward.
There are a few different problems, all of which need to be addressed at
some stage:

1) Tools data sources for both these environments really should be done
properly, with simplestreams, and without abusing the upload-tools
pathway.

2) JobHostEnvironStorage ought to exist, be set by the *provider* by
configuring the cloud-init (or equivalent) such that jujud
bootstrap-state injects a machine with the right jobs and we can
eliminate the special-casing in jujud/machine.go

3) JobManageEnvironFirewall, done as above. Along with JHES above, we'll
have interesting problems to solve come HA time, but it's a step in a
sane direction.

...but it's plainly ridiculous to block this CL on implementing those
three things. So.

(1) Please talk to ian to figure out the state of play here; it may be
that upload-tools does everything nicely and simplestreamsily already,
in which case my only quibble is the special-casing by provider type in
juju/bootstrap and that's a tech-debt bug for another day.

(2) This can be landed as-is, but must be resolved quickly. Write a bug,
assign it to yourself, and queue it behind the storage authentication
work.

(3) Eh, it's a tech-debt bug if you just return nil from all the ports
methods. It will become much easier to fix once (2) is resolved, though,
because there'll be a custom-jobs pathway from the provider into state.

I'll try to be online tonight so we can chat live -- I think this is
very close to landing if we can agree on the issue and responses.

https://codereview.appspot.com/13255051/diff/1/cmd/juju/bootstrap.go
File cmd/juju/bootstrap.go (left):

https://codereview.appspot.com/13255051/diff/1/cmd/juju/bootstrap.go#oldcode90
cmd/juju/bootstrap.go:90: c.UploadTools = true
This bit stresses me out every time I see it, and ISTM that we're
expanding the hack rather than fixing it properly. Why can't the null
provider get its tools from simplestreams as usual, and allow the user
to specify a tools url explicitly for isolated environments, like
openstack does?

(for that matter, while this is not your problem specifically, why can't
the local provider itself know how to look for suitable tools locally,
and expose *that* as a data source for simplestreams?)

Ian, I'd appreciate your input here.

https://codereview.appspot.com/13255051/diff/1/cmd/juju/bootstrap.go
File cmd/juju/bootstrap.go (right):

https://codereview.appspot.com/13255051/diff/1/cmd/juju/bootstrap.go#newcode92
cmd/juju/bootstrap.go:92: case provider.Local:
case provider.Null, provider.Local:
     ?

https://codereview.appspot.com/13255051/diff/1/cmd/jujud/machine.go
File cmd/jujud/machine.go (left):

https://codereview.appspot.com/13255051/diff/1/cmd/jujud/machine.go#oldcode240
cmd/jujud/machine.go:240: // the storage provider on one machine, and
that is the "bootstrap" node.
Oo, I don't like that comment. If we're going to be HA, storage is going
to ha...

Read more...

Revision history for this message
Andrew Wilkins (axwalk) wrote :
Download full text (7.0 KiB)

https://codereview.appspot.com/13255051/diff/1/cmd/juju/bootstrap.go
File cmd/juju/bootstrap.go (left):

https://codereview.appspot.com/13255051/diff/1/cmd/juju/bootstrap.go#oldcode90
cmd/juju/bootstrap.go:90: c.UploadTools = true
On 2013/09/23 16:32:25, fwereade wrote:
> This bit stresses me out every time I see it, and ISTM that we're
expanding the
> hack rather than fixing it properly. Why can't the null provider get
its tools
> from simplestreams as usual, and allow the user to specify a tools url
> explicitly for isolated environments, like openstack does?

No good reason. I did it this way during development (I think there
might have been some breakage with simplestreams along the way), and
never updated to do it properly. This file can safely be reverted.

> (for that matter, while this is not your problem specifically, why
can't the
> local provider itself know how to look for suitable tools locally, and
expose
> *that* as a data source for simplestreams?)

> Ian, I'd appreciate your input here.

https://codereview.appspot.com/13255051/diff/1/cmd/juju/bootstrap.go
File cmd/juju/bootstrap.go (right):

https://codereview.appspot.com/13255051/diff/1/cmd/juju/bootstrap.go#newcode92
cmd/juju/bootstrap.go:92: case provider.Local:
On 2013/09/23 16:32:25, fwereade wrote:
> case provider.Null, provider.Local:
> ?

Heh, yes. Excuse me while I take my C hat off.

https://codereview.appspot.com/13255051/diff/1/cmd/jujud/machine.go
File cmd/jujud/machine.go (left):

https://codereview.appspot.com/13255051/diff/1/cmd/jujud/machine.go#oldcode240
cmd/jujud/machine.go:240: // the storage provider on one machine, and
that is the "bootstrap" node.
On 2013/09/23 16:32:25, fwereade wrote:
> Oo, I don't like that comment. If we're going to be HA, storage is
going to have
> to be HA too. Not one for today though.

Is the null provider ever going to need to be HA? I thought this was
purely for "scale down", though I suppose we *might* want to support a
fully fledged scale-out environment, just without the automatic
provisioning.

https://codereview.appspot.com/13255051/diff/1/cmd/jujud/machine.go
File cmd/jujud/machine.go (right):

https://codereview.appspot.com/13255051/diff/1/cmd/jujud/machine.go#newcode241
cmd/jujud/machine.go:241: if (providerType == provider.Local ||
providerType == provider.Null) && m.Id() == bootstrapMachineId {
On 2013/09/23 16:32:25, fwereade wrote:
> This *really* should not be expressed like this. Special-casing one
provider
> type was a pretty bad layering violation, and two exceptions feel
uncomfortably
> like a trend.

> I think the only way we can reasonably do this is to tweak the
bootstrap
> pathways for the local and null providers, such that they inject an
additional
> machine job (say, JobHostEnvironStorage) on the bootstrap node.

That makes sense. There's a bunch of things to touch, so I have created:
https://bugs.launchpad.net/juju-core/+bug/1229507
and will follow up. If you prefer I do this now, though, I can.

https://codereview.appspot.com/13255051/diff/1/provider/null/config.go
File provider/null/config.go (right):

https://codereview.appspot.com/13255051/diff/1/provider/null/config.go#newcode23
provider/nul...

Read more...

Revision history for this message
Andrew Wilkins (axwalk) wrote :
Revision history for this message
William Reade (fwereade) wrote :
Download full text (3.9 KiB)

We need to do more work wrt "shutting down" manually provisioned
instances -- ie removing juju therefrom -- but most of that I'm happy to
leave to a followup.

I'm not quite sure whether the complete non-functioning of Destroy is a
good thing or not. Given that destroying doesn't actually destroy
anything, maybe it's good that we don't trash storage either... but in
that case I think we should just return a not-implemented sort of error,
and keep going to provide a decent shutdown experience. Thoughts? Quick
discussion/confirmation here is the only thing delaying approval.

https://codereview.appspot.com/13255051/diff/1/cmd/jujud/machine.go
File cmd/jujud/machine.go (left):

https://codereview.appspot.com/13255051/diff/1/cmd/jujud/machine.go#oldcode240
cmd/jujud/machine.go:240: // the storage provider on one machine, and
that is the "bootstrap" node.
On 2013/09/24 03:05:57, axw1 wrote:
> On 2013/09/23 16:32:25, fwereade wrote:
> > Oo, I don't like that comment. If we're going to be HA, storage is
going to
> have
> > to be HA too. Not one for today though.

> Is the null provider ever going to need to be HA? I thought this was
purely for
> "scale down", though I suppose we *might* want to support a fully
fledged
> scale-out environment, just without the automatic provisioning.

One day it will. Happy to defer consideration of that for now though.

https://codereview.appspot.com/13255051/diff/11001/cmd/jujud/machine.go
File cmd/jujud/machine.go (right):

https://codereview.appspot.com/13255051/diff/11001/cmd/jujud/machine.go#newcode259
cmd/jujud/machine.go:259: // are capable of managing ports at a global
level.
FWIW the firewaller handles instance-level provisioning too.

https://codereview.appspot.com/13255051/diff/11001/provider/null/environ.go
File provider/null/environ.go (right):

https://codereview.appspot.com/13255051/diff/11001/provider/null/environ.go#newcode76
provider/null/environ.go:76: func (e *nullEnviron) Bootstrap(_
constraints.Value, possibleTools tools.List, machineID string) error {
I really think we ought to drop machineID (which would be more
conventionally spelled machineId, I think) from Bootstrap, and the id
should always be "0". But that's not a job for today.

https://codereview.appspot.com/13255051/diff/11001/provider/null/environ.go#newcode142
provider/null/environ.go:142: return fmt.Errorf("null provider cannot
destroy instances: %v", insts)
Is there *any* way to destroy an environment then? It's always got the
bootstrap machine in it, regardless of whether it's passed those
instances.

https://codereview.appspot.com/13255051/diff/11001/provider/null/environ_test.go
File provider/null/environ_test.go (right):

https://codereview.appspot.com/13255051/diff/11001/provider/null/environ_test.go#newcode72
provider/null/environ_test.go:72: c.Assert(s.env.Destroy(nil), gc.IsNil)
Yeah, it bothers me that this can complete without either errors or
impact. It's a deeper issue than can be managed in this CL, though -- it
kinda comes down to:

1) destroy-environment should probably involve setting every machine
except the manager to Dead (possibly breaking expectations in order to
do so -- eg killing them with active units in ...

Read more...

Revision history for this message
Andrew Wilkins (axwalk) wrote :

https://codereview.appspot.com/13255051/diff/11001/cmd/jujud/machine.go
File cmd/jujud/machine.go (right):

https://codereview.appspot.com/13255051/diff/11001/cmd/jujud/machine.go#newcode259
cmd/jujud/machine.go:259: // are capable of managing ports at a global
level.
On 2013/09/26 08:03:41, fwereade wrote:
> FWIW the firewaller handles instance-level provisioning too.

Probably a better way of writing the comment would be "centrally",
rather than "at a global level".

https://codereview.appspot.com/13255051/diff/11001/provider/null/environ_test.go
File provider/null/environ_test.go (right):

https://codereview.appspot.com/13255051/diff/11001/provider/null/environ_test.go#newcode72
provider/null/environ_test.go:72: c.Assert(s.env.Destroy(nil), gc.IsNil)
On 2013/09/26 08:03:41, fwereade wrote:
> Yeah, it bothers me that this can complete without either errors or
impact. It's
> a deeper issue than can be managed in this CL, though -- it kinda
comes down to:

> 1) destroy-environment should probably involve setting every machine
except the
> manager to Dead (possibly breaking expectations in order to do so --
eg killing
> them with active units in play), allowing the provisioner to clean
them all up
> neatly, before setting the provisioner's own machine to Dead. Not
clear how much
> work this is in practice.

I wasn't really sure where to draw the line for null/manual. This sounds
reasonable, though. I think this shouldn't be *too* much work.

> 2) manually provisioned machine agents should be able to clean
themselves up
> completely once they've set themselves to Dead.

This still works, from the previous work on manual provisioning.

> Separately, remind me: what cleans up environment storage? That might
be our
> responsibility in this method.

Currently not done for the null-provider. I think the simplest thing to
do would be for the machine agent to remove its data-dir on
uninstallation (storage lives under there).

https://codereview.appspot.com/13255051/diff/11001/provider/null/instance.go
File provider/null/instance.go (right):

https://codereview.appspot.com/13255051/diff/11001/provider/null/instance.go#newcode41
provider/null/instance.go:41: // If the user specified bootsrap-host as
an IP address,
On 2013/09/26 08:03:41, fwereade wrote:
> s/bootsrap/bootstrap/

Thanks, will fix.

https://codereview.appspot.com/13255051/

Revision history for this message
William Reade (fwereade) wrote :

On 2013/09/26 08:23:00, axw1 wrote:
> > 1) destroy-environment should probably involve setting every machine
except
> the
> > manager to Dead (possibly breaking expectations in order to do so --
eg
> killing
> > them with active units in play), allowing the provisioner to clean
them all up
> > neatly, before setting the provisioner's own machine to Dead. Not
clear how
> much
> > work this is in practice.

> I wasn't really sure where to draw the line for null/manual. This
sounds
> reasonable, though. I think this shouldn't be *too* much work.

Excellent. You'll probably want a reasonably detailed chat with me about
state soon, then.

> > 2) manually provisioned machine agents should be able to clean
themselves up
> > completely once they've set themselves to Dead.

> This still works, from the previous work on manual provisioning.

Sweet! I couldn't remember if we'd done that.

> > Separately, remind me: what cleans up environment storage? That
might be our
> > responsibility in this method.

> Currently not done for the null-provider. I think the simplest thing
to do would
> be for the machine agent to remove its data-dir on uninstallation
(storage lives
> under there).

OK, that sounds pretty reasonable, but let's not set it in stone. We can
figure out what works best in the context of how we actually do it.

What do you think about returning an error from Destroy now, though? We
really don't have any sensible reason to claim the operation worked...

https://codereview.appspot.com/13255051/

Revision history for this message
Andrew Wilkins (axwalk) wrote :

https://codereview.appspot.com/13255051/diff/11001/provider/null/environ.go
File provider/null/environ.go (right):

https://codereview.appspot.com/13255051/diff/11001/provider/null/environ.go#newcode142
provider/null/environ.go:142: return fmt.Errorf("null provider cannot
destroy instances: %v", insts)
On 2013/09/26 08:03:41, fwereade wrote:
> Is there *any* way to destroy an environment then? It's always got the
bootstrap
> machine in it, regardless of whether it's passed those instances.

No, not entirely at the moment. Agreed, this should return an error for
now while the rest is worked out. I will do that.

https://codereview.appspot.com/13255051/

Revision history for this message
Andrew Wilkins (axwalk) wrote :
Revision history for this message
William Reade (fwereade) wrote :

LGTM then, destroying properly can come in followups.

https://codereview.appspot.com/13255051/

Revision history for this message
Go Bot (go-bot) wrote :
Download full text (38.1 KiB)

The attempt to merge lp:~axwalk/juju-core/null-provider-take3 into lp:juju-core failed. Below is the output from the failed tests.

ok launchpad.net/juju-core/agent 0.743s
ok launchpad.net/juju-core/agent/tools 0.225s
ok launchpad.net/juju-core/bzr 6.571s
ok launchpad.net/juju-core/cert 3.240s
ok launchpad.net/juju-core/charm 0.546s
? launchpad.net/juju-core/charm/hooks [no test files]
ok launchpad.net/juju-core/cloudinit 0.025s
ok launchpad.net/juju-core/cmd 0.222s
? launchpad.net/juju-core/cmd/builddb [no test files]
? launchpad.net/juju-core/cmd/charmd [no test files]
? launchpad.net/juju-core/cmd/charmload [no test files]
listing available tools
built 1.15.0.1-precise-amd64 (3244kB)
listing available tools
found 2 tools
listing target bucket
found 0 tools in target; 2 tools to be copied
copying 1.15.0.1-precise-amd64 from file:///tmp/419770343/tools/releases/juju-1.15.0.1-precise-amd64.tgz
copying tools/releases/juju-1.15.0.1-precise-amd64.tgz
downloaded tools/releases/juju-1.15.0.1-precise-amd64.tgz (3244kB), uploading
download 3244kB, uploading
copying 1.15.0.1-raring-amd64 from file:///tmp/419770343/tools/releases/juju-1.15.0.1-raring-amd64.tgz
copying tools/releases/juju-1.15.0.1-raring-amd64.tgz
downloaded tools/releases/juju-1.15.0.1-raring-amd64.tgz (3244kB), uploading
download 3244kB, uploading
copied 2 tools
generating tools metadata
tools metadata written

Juju cannot bootstrap because no tools are available for your environment.
An attempt was made to build and upload appropriate tools but this was unsuccessful.

listing available tools

Juju cannot bootstrap because no tools are available for your environment.
In addition, no tools could be located to upload.
You may want to use the 'tools-url' configuration setting to specify the tools location.

listing available tools
listing available tools

Juju cannot bootstrap because no tools are available for your environment.
In addition, no tools could be located to upload.
You may want to use the 'tools-url' configuration setting to specify the tools location.

listing available tools
found 4 tools
found 4 recent tools (version 1.2.0)
listing target bucket
found 0 tools in target; 4 tools to be copied
copying 1.2.0-precise-amd64 from http://127.0.0.1:37907/tools/juju-1.2.0-precise-amd64.tgz
copying tools/releases/juju-1.2.0-precise-amd64.tgz
downloaded tools/releases/juju-1.2.0-precise-amd64.tgz (0kB), uploading
download 0kB, uploading
copying 1.2.0-quantal-amd64 from http://127.0.0.1:37907/tools/juju-1.2.0-quantal-amd64.tgz
copying tools/releases/juju-1.2.0-quantal-amd64.tgz
downloaded tools/releases/juju-1.2.0-quantal-amd64.tgz (0kB), uploading
download 0kB, uploading
copying 1.2.0-quantal-i386 from http://127.0.0.1:37907/tools/juju-1.2.0-quantal-i386.tgz
copying tools/releases/juju-1.2.0-quantal-i386.tgz
downloaded tools/releases/juju-1.2.0-quantal-i386.tgz (0kB), uploading
download 0kB, uploading
copying 1.2.0-raring-amd64 from http://127.0.0.1:37907/tools/juju-1.2.0-raring-amd64.tgz
copying tools/releases/juju-1.2.0-raring-amd64.tgz
downloaded tools/releases/juju-1.2.0-raring-amd64.tgz (0kB), uploading
download 0kB, uploading
copied 4 tools
generating...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cmd/jujud/machine.go'
2--- cmd/jujud/machine.go 2013-09-25 16:18:20 +0000
3+++ cmd/jujud/machine.go 2013-09-26 11:22:31 +0000
4@@ -246,8 +246,11 @@
5 // Take advantage of special knowledge here in that we will only ever want
6 // the storage provider on one machine, and that is the "bootstrap" node.
7 providerType := agentConfig.Value(agent.ProviderType)
8- if providerType == provider.Local && m.Id() == bootstrapMachineId {
9+ if (providerType == provider.Local || providerType == provider.Null) && m.Id() == bootstrapMachineId {
10 runner.StartWorker("local-storage", func() (worker.Worker, error) {
11+ // TODO(axw) 2013-09-24 bug #1229507
12+ // Make another job to enable storage.
13+ // There's nothing special about this.
14 return localstorage.NewWorker(agentConfig), nil
15 })
16 }
17@@ -256,6 +259,9 @@
18 case state.JobHostUnits:
19 // Implemented in APIWorker.
20 case state.JobManageEnviron:
21+ // TODO(axw) 2013-09-24 bug #1229506
22+ // Make another job to enable the firewaller. Not all environments
23+ // are capable of managing ports centrally.
24 runner.StartWorker("firewaller", func() (worker.Worker, error) {
25 return firewaller.NewFirewaller(st), nil
26 })
27
28=== modified file 'environs/cloudinit/cloudinit.go'
29--- environs/cloudinit/cloudinit.go 2013-09-26 06:34:26 +0000
30+++ environs/cloudinit/cloudinit.go 2013-09-26 11:22:31 +0000
31@@ -8,6 +8,7 @@
32 "encoding/json"
33 "fmt"
34 "path"
35+ "strings"
36
37 "launchpad.net/goyaml"
38
39@@ -32,6 +33,9 @@
40 // is bootstrapping.
41 const BootstrapStateURLFile = "/tmp/provider-state-url"
42
43+// fileSchemePrefix is the prefix for file:// URLs.
44+const fileSchemePrefix = "file://"
45+
46 // MachineConfig represents initialization information for a new juju machine.
47 type MachineConfig struct {
48 // StateServer specifies whether the new machine will run the
49@@ -141,6 +145,12 @@
50
51 // Make a directory for the tools to live in, then fetch the
52 // tools and unarchive them into it.
53+ var copyCmd string
54+ if strings.HasPrefix(cfg.Tools.URL, fileSchemePrefix) {
55+ copyCmd = fmt.Sprintf("cp %s $bin/tools.tar.gz", shquote(cfg.Tools.URL[len(fileSchemePrefix):]))
56+ } else {
57+ copyCmd = fmt.Sprintf("wget --no-verbose -O $bin/tools.tar.gz %s", shquote(cfg.Tools.URL))
58+ }
59 toolsJson, err := json.Marshal(cfg.Tools)
60 if err != nil {
61 return nil, err
62@@ -148,8 +158,8 @@
63 c.AddScripts(
64 "bin="+shquote(cfg.jujuTools()),
65 "mkdir -p $bin",
66- fmt.Sprintf("wget --no-verbose -O - %s | tee $bin/tools.tar.gz | sha256sum > $bin/juju%s.sha256",
67- shquote(cfg.Tools.URL), cfg.Tools.Version),
68+ copyCmd,
69+ fmt.Sprintf("sha256sum $bin/tools.tar.gz > $bin/juju%s.sha256", cfg.Tools.Version),
70 fmt.Sprintf(`grep '%s' $bin/juju%s.sha256 || (echo "Tools checksum mismatch"; exit 1)`,
71 cfg.Tools.SHA256, cfg.Tools.Version),
72 fmt.Sprintf("tar zxf $bin/tools.tar.gz -C $bin"),
73
74=== modified file 'environs/cloudinit/cloudinit_test.go'
75--- environs/cloudinit/cloudinit_test.go 2013-09-26 06:34:26 +0000
76+++ environs/cloudinit/cloudinit_test.go 2013-09-26 11:22:31 +0000
77@@ -84,7 +84,8 @@
78 mkdir -p /var/log/juju
79 bin='/var/lib/juju/tools/1\.2\.3-precise-amd64'
80 mkdir -p \$bin
81-wget --no-verbose -O - 'http://foo\.com/tools/juju1\.2\.3-precise-amd64\.tgz' \| tee \$bin/tools\.tar\.gz \| sha256sum > \$bin/juju1\.2\.3-precise-amd64\.sha256
82+wget --no-verbose -O \$bin/tools\.tar\.gz 'http://foo\.com/tools/juju1\.2\.3-precise-amd64\.tgz'
83+sha256sum \$bin/tools\.tar\.gz > \$bin/juju1\.2\.3-precise-amd64\.sha256
84 grep '1234' \$bin/juju1\.2\.3-precise-amd64.sha256 \|\| \(echo "Tools checksum mismatch"; exit 1\)
85 tar zxf \$bin/tools.tar.gz -C \$bin
86 rm \$bin/tools\.tar\.gz && rm \$bin/juju1\.2\.3-precise-amd64\.sha256
87@@ -152,7 +153,8 @@
88 mkdir -p /var/log/juju
89 bin='/var/lib/juju/tools/1\.2\.3-raring-amd64'
90 mkdir -p \$bin
91-wget --no-verbose -O - 'http://foo\.com/tools/juju1\.2\.3-raring-amd64\.tgz' \| tee \$bin/tools\.tar\.gz \| sha256sum > \$bin/juju1\.2\.3-raring-amd64\.sha256
92+wget --no-verbose -O \$bin/tools\.tar\.gz 'http://foo\.com/tools/juju1\.2\.3-raring-amd64\.tgz'
93+sha256sum \$bin/tools\.tar\.gz > \$bin/juju1\.2\.3-raring-amd64\.sha256
94 grep '1234' \$bin/juju1\.2\.3-raring-amd64.sha256 \|\| \(echo "Tools checksum mismatch"; exit 1\)
95 tar zxf \$bin/tools.tar.gz -C \$bin
96 rm \$bin/tools\.tar\.gz && rm \$bin/juju1\.2\.3-raring-amd64\.sha256
97@@ -214,7 +216,8 @@
98 mkdir -p /var/log/juju
99 bin='/var/lib/juju/tools/1\.2\.3-linux-amd64'
100 mkdir -p \$bin
101-wget --no-verbose -O - 'http://foo\.com/tools/juju1\.2\.3-linux-amd64\.tgz' \| tee \$bin/tools\.tar\.gz \| sha256sum > \$bin/juju1\.2\.3-linux-amd64\.sha256
102+wget --no-verbose -O \$bin/tools\.tar\.gz 'http://foo\.com/tools/juju1\.2\.3-linux-amd64\.tgz'
103+sha256sum \$bin/tools\.tar\.gz > \$bin/juju1\.2\.3-linux-amd64\.sha256
104 grep '1234' \$bin/juju1\.2\.3-linux-amd64.sha256 \|\| \(echo "Tools checksum mismatch"; exit 1\)
105 tar zxf \$bin/tools.tar.gz -C \$bin
106 rm \$bin/tools\.tar\.gz && rm \$bin/juju1\.2\.3-linux-amd64\.sha256
107@@ -260,7 +263,8 @@
108 mkdir -p /var/log/juju
109 bin='/var/lib/juju/tools/1\.2\.3-linux-amd64'
110 mkdir -p \$bin
111-wget --no-verbose -O - 'http://foo\.com/tools/juju1\.2\.3-linux-amd64\.tgz' \| tee \$bin/tools\.tar\.gz \| sha256sum > \$bin/juju1\.2\.3-linux-amd64\.sha256
112+wget --no-verbose -O \$bin/tools\.tar\.gz 'http://foo\.com/tools/juju1\.2\.3-linux-amd64\.tgz'
113+sha256sum \$bin/tools\.tar\.gz > \$bin/juju1\.2\.3-linux-amd64\.sha256
114 grep '1234' \$bin/juju1\.2\.3-linux-amd64.sha256 \|\| \(echo "Tools checksum mismatch"; exit 1\)
115 tar zxf \$bin/tools.tar.gz -C \$bin
116 rm \$bin/tools\.tar\.gz && rm \$bin/juju1\.2\.3-linux-amd64\.sha256
117@@ -277,6 +281,52 @@
118 cat >> /etc/init/jujud-machine-2-lxc-1\.conf << 'EOF'\\ndescription "juju machine-2-lxc-1 agent"\\nauthor "Juju Team <juju@lists\.ubuntu\.com>"\\nstart on runlevel \[2345\]\\nstop on runlevel \[!2345\]\\nrespawn\\nnormal exit 0\\n\\nlimit nofile 20000 20000\\n\\nexec /var/lib/juju/tools/machine-2-lxc-1/jujud machine --data-dir '/var/lib/juju' --machine-id 2/lxc/1 --debug >> /var/log/juju/machine-2-lxc-1\.log 2>&1\\nEOF\\n
119 start jujud-machine-2-lxc-1
120 `,
121+ }, {
122+ cfg: cloudinit.MachineConfig{
123+ MachineId: "123",
124+ MachineContainerType: "lxc",
125+ AuthorizedKeys: "sshkey1",
126+ AgentEnvironment: map[string]string{agent.ProviderType: "dummy"},
127+ DataDir: environs.DataDir,
128+ Tools: newFileTools("1.2.3-linux-amd64", "/var/lib/juju/storage/juju1.2.3-linux-amd64.tgz"),
129+ MachineNonce: "FAKE_NONCE",
130+ StateInfo: &state.Info{
131+ Addrs: []string{"state-addr.testing.invalid:12345"},
132+ Tag: "machine-123",
133+ Password: "arble",
134+ CACert: []byte("CA CERT\n" + testing.CACert),
135+ },
136+ APIInfo: &api.Info{
137+ Addrs: []string{"state-addr.testing.invalid:54321"},
138+ Tag: "machine-123",
139+ Password: "bletch",
140+ CACert: []byte("CA CERT\n" + testing.CACert),
141+ },
142+ },
143+ expectScripts: `
144+set -xe
145+mkdir -p /var/lib/juju
146+mkdir -p /var/log/juju
147+bin='/var/lib/juju/tools/1\.2\.3-linux-amd64'
148+mkdir -p \$bin
149+cp '/var/lib/juju/storage/juju1.2.3-linux-amd64\.tgz' \$bin/tools\.tar\.gz
150+sha256sum \$bin/tools\.tar\.gz > \$bin/juju1\.2\.3-linux-amd64\.sha256
151+grep '1234' \$bin/juju1\.2\.3-linux-amd64.sha256 \|\| \(echo "Tools checksum mismatch"; exit 1\)
152+tar zxf \$bin/tools.tar.gz -C \$bin
153+rm \$bin/tools\.tar\.gz && rm \$bin/juju1\.2\.3-linux-amd64\.sha256
154+printf %s '{"version":"1\.2\.3-linux-amd64","url":"file:///var/lib/juju/storage/juju1\.2\.3-linux-amd64\.tgz","sha256":"1234","size":10}' > \$bin/downloaded-tools\.txt
155+install -m 600 /dev/null '/etc/rsyslog\.d/25-juju\.conf'
156+printf '%s\\n' '\\n\$ModLoad imfile\\n\\n\$InputFileStateFile /var/spool/rsyslog/juju-machine-123-state\\n\$InputFilePersistStateInterval 50\\n\$InputFilePollInterval 5\\n\$InputFileName /var/log/juju/machine-123.log\\n\$InputFileTag juju-machine-123:\\n\$InputFileStateFile machine-123\\n\$InputRunFileMonitor\\n\\n:syslogtag, startswith, \"juju-\" @state-addr.testing.invalid:514\\n& ~\\n' > '/etc/rsyslog\.d/25-juju\.conf'
157+restart rsyslog
158+mkdir -p '/var/lib/juju/agents/machine-123'
159+install -m 644 /dev/null '/var/lib/juju/agents/machine-123/format'
160+printf '%s\\n' '.*' > '/var/lib/juju/agents/machine-123/format'
161+install -m 600 /dev/null '/var/lib/juju/agents/machine-123/agent\.conf'
162+printf '%s\\n' '.*' > '/var/lib/juju/agents/machine-123/agent\.conf'
163+ln -s 1\.2\.3-linux-amd64 '/var/lib/juju/tools/machine-123'
164+cat >> /etc/init/jujud-machine-123\.conf << 'EOF'\\ndescription "juju machine-123 agent"\\nauthor "Juju Team <juju@lists\.ubuntu\.com>"\\nstart on runlevel \[2345\]\\nstop on runlevel \[!2345\]\\nrespawn\\nnormal exit 0\\n\\nlimit nofile 20000 20000\\n\\nexec /var/lib/juju/tools/machine-123/jujud machine --data-dir '/var/lib/juju' --machine-id 123 --debug >> /var/log/juju/machine-123\.log 2>&1\\nEOF\\n
165+start jujud-machine-123
166+`,
167 },
168 }
169
170@@ -289,6 +339,12 @@
171 }
172 }
173
174+func newFileTools(vers, path string) *tools.Tools {
175+ tools := newSimpleTools(vers)
176+ tools.URL = "file://" + path
177+ return tools
178+}
179+
180 // check that any --env-config $base64 is valid and matches t.cfg.Config
181 func checkEnvConfig(c *gc.C, cfg *config.Config, x map[interface{}]interface{}, scripts []string) {
182 re := regexp.MustCompile(`--env-config '([^']+)'`)
183
184=== modified file 'environs/manual/agent.go'
185--- environs/manual/agent.go 2013-09-16 04:55:45 +0000
186+++ environs/manual/agent.go 2013-09-26 11:22:31 +0000
187@@ -69,7 +69,7 @@
188 // we'll just generate a shell script.
189 var mcfg *cloudinit.MachineConfig
190 if args.bootstrap {
191- mcfg = environs.NewBootstrapMachineConfig(args.machineId, args.nonce)
192+ mcfg = environs.NewBootstrapMachineConfig(args.machineId, args.stateFileURL)
193 } else {
194 mcfg = environs.NewMachineConfig(args.machineId, args.nonce, args.stateInfo, args.apiInfo)
195 }
196
197=== modified file 'environs/manual/bootstrap.go'
198--- environs/manual/bootstrap.go 2013-09-16 04:55:45 +0000
199+++ environs/manual/bootstrap.go 2013-09-26 11:22:31 +0000
200@@ -30,14 +30,12 @@
201
202 type BootstrapArgs struct {
203 Host string
204+ DataDir string
205 Environ LocalStorageEnviron
206 MachineId string
207 PossibleTools tools.List
208 }
209
210-// TODO(axw) make this configurable?
211-const dataDir = "/var/lib/juju"
212-
213 func errMachineIdInvalid(machineId string) error {
214 return fmt.Errorf("%q is not a valid machine ID", machineId)
215 }
216@@ -52,6 +50,9 @@
217 if args.Environ == nil {
218 return errors.New("environ argument is nil")
219 }
220+ if args.DataDir == "" {
221+ return errors.New("data-dir argument is empty")
222+ }
223 if !names.IsMachine(args.MachineId) {
224 return errMachineIdInvalid(args.MachineId)
225 }
226@@ -103,6 +104,10 @@
227 }
228 }()
229
230+ // Set the new tools prefix so StorageName returns the right thing.
231+ restore := envtools.SetToolPrefix(envtools.NewToolPrefix)
232+ defer restore()
233+
234 // Get a file:// scheme tools URL for the tools, which will have been
235 // copied to the remote machine's storage directory.
236 tools := *possibleTools[0]
237@@ -122,7 +127,7 @@
238 stateFileURL := fmt.Sprintf("file://%s/%s", storageDir, provider.StateFile)
239 err = provisionMachineAgent(provisionMachineAgentArgs{
240 host: args.Host,
241- dataDir: dataDir,
242+ dataDir: args.DataDir,
243 environConfig: args.Environ.Config(),
244 stateFileURL: stateFileURL,
245 machineId: args.MachineId,
246
247=== modified file 'environs/manual/bootstrap_test.go'
248--- environs/manual/bootstrap_test.go 2013-09-20 05:04:42 +0000
249+++ environs/manual/bootstrap_test.go 2013-09-26 11:22:31 +0000
250@@ -73,6 +73,7 @@
251 c.Assert(err, gc.IsNil)
252 return BootstrapArgs{
253 Host: hostname,
254+ DataDir: "/var/lib/juju",
255 Environ: s.env,
256 MachineId: "0",
257 PossibleTools: toolsList,
258@@ -122,6 +123,12 @@
259 c.Assert(err, jc.Satisfies, coreerrors.IsNotBootstrapped)
260 }
261
262+func (s *bootstrapSuite) TestBootstrapEmptyDataDir(c *gc.C) {
263+ args := s.getArgs(c)
264+ args.DataDir = ""
265+ c.Assert(Bootstrap(args), gc.ErrorMatches, "data-dir argument is empty")
266+}
267+
268 func (s *bootstrapSuite) TestBootstrapEmptyHost(c *gc.C) {
269 args := s.getArgs(c)
270 args.Host = ""
271
272=== modified file 'provider/all/all.go'
273--- provider/all/all.go 2013-08-19 13:29:56 +0000
274+++ provider/all/all.go 2013-09-26 11:22:31 +0000
275@@ -9,5 +9,6 @@
276 _ "launchpad.net/juju-core/provider/ec2"
277 _ "launchpad.net/juju-core/provider/local"
278 _ "launchpad.net/juju-core/provider/maas"
279+ _ "launchpad.net/juju-core/provider/null"
280 _ "launchpad.net/juju-core/provider/openstack"
281 )
282
283=== added directory 'provider/null'
284=== added file 'provider/null/config.go'
285--- provider/null/config.go 1970-01-01 00:00:00 +0000
286+++ provider/null/config.go 2013-09-26 11:22:31 +0000
287@@ -0,0 +1,70 @@
288+// Copyright 2013 Canonical Ltd.
289+// Licensed under the AGPLv3, see LICENCE file for details.
290+
291+package null
292+
293+import (
294+ "fmt"
295+
296+ "launchpad.net/juju-core/environs/config"
297+ "launchpad.net/juju-core/schema"
298+)
299+
300+var (
301+ configFields = schema.Fields{
302+ "bootstrap-host": schema.String(),
303+ "bootstrap-user": schema.String(),
304+ "storage-listen-ip": schema.String(),
305+ "storage-port": schema.Int(),
306+ }
307+ configDefaults = schema.Defaults{
308+ "bootstrap-user": "",
309+ "storage-listen-ip": "",
310+ "storage-port": 8040,
311+ }
312+)
313+
314+type environConfig struct {
315+ *config.Config
316+ attrs map[string]interface{}
317+}
318+
319+func newEnvironConfig(config *config.Config, attrs map[string]interface{}) *environConfig {
320+ return &environConfig{Config: config, attrs: attrs}
321+}
322+
323+func (c *environConfig) bootstrapHost() string {
324+ return c.attrs["bootstrap-host"].(string)
325+}
326+
327+func (c *environConfig) bootstrapUser() string {
328+ return c.attrs["bootstrap-user"].(string)
329+}
330+
331+func (c *environConfig) sshHost() string {
332+ host := c.bootstrapHost()
333+ if user := c.bootstrapUser(); user != "" {
334+ host = user + "@" + host
335+ }
336+ return host
337+}
338+
339+func (c *environConfig) storageListenIPAddress() string {
340+ return c.attrs["storage-listen-ip"].(string)
341+}
342+
343+func (c *environConfig) storagePort() int {
344+ return int(c.attrs["storage-port"].(int64))
345+}
346+
347+// storageAddr returns an address for connecting to the
348+// bootstrap machine's localstorage.
349+func (c *environConfig) storageAddr() string {
350+ return fmt.Sprintf("%s:%d", c.bootstrapHost(), c.storagePort())
351+}
352+
353+// storageListenAddr returns an address for the bootstrap
354+// machine to listen on for its localstorage.
355+func (c *environConfig) storageListenAddr() string {
356+ return fmt.Sprintf("%s:%d", c.storageListenIPAddress(), c.storagePort())
357+}
358
359=== added file 'provider/null/config_test.go'
360--- provider/null/config_test.go 1970-01-01 00:00:00 +0000
361+++ provider/null/config_test.go 2013-09-26 11:22:31 +0000
362@@ -0,0 +1,124 @@
363+// Copyright 2013 Canonical Ltd.
364+// Licensed under the AGPLv3, see LICENCE file for details.
365+
366+package null
367+
368+import (
369+ "fmt"
370+ "regexp"
371+ stdtesting "testing"
372+
373+ gc "launchpad.net/gocheck"
374+
375+ "launchpad.net/juju-core/environs/config"
376+ "launchpad.net/juju-core/provider"
377+ coretesting "launchpad.net/juju-core/testing"
378+ "launchpad.net/juju-core/testing/testbase"
379+)
380+
381+type configSuite struct {
382+ testbase.LoggingSuite
383+}
384+
385+var _ = gc.Suite(&configSuite{})
386+
387+func Test(t *stdtesting.T) {
388+ gc.TestingT(t)
389+}
390+
391+func minimalConfigValues() map[string]interface{} {
392+ return map[string]interface{}{
393+ "name": "test",
394+ "type": provider.Null,
395+ "bootstrap-host": "hostname",
396+ // While the ca-cert bits aren't entirely minimal, they avoid the need
397+ // to set up a fake home.
398+ "ca-cert": coretesting.CACert,
399+ "ca-private-key": coretesting.CAKey,
400+ }
401+}
402+
403+func minimalConfig(c *gc.C) *config.Config {
404+ minimal := minimalConfigValues()
405+ testConfig, err := config.New(config.UseDefaults, minimal)
406+ c.Assert(err, gc.IsNil)
407+ return testConfig
408+}
409+
410+func getEnvironConfig(c *gc.C, attrs map[string]interface{}) *environConfig {
411+ testConfig, err := config.New(config.UseDefaults, attrs)
412+ c.Assert(err, gc.IsNil)
413+ envConfig, err := nullProvider{}.validate(testConfig, nil)
414+ c.Assert(err, gc.IsNil)
415+ return envConfig
416+}
417+
418+func (s *configSuite) TestValidateConfig(c *gc.C) {
419+ testConfig := minimalConfig(c)
420+ testConfig, err := testConfig.Apply(map[string]interface{}{"bootstrap-host": ""})
421+ c.Assert(err, gc.IsNil)
422+ _, err = nullProvider{}.Validate(testConfig, nil)
423+ c.Assert(err, gc.ErrorMatches, "bootstrap-host must be specified")
424+
425+ testConfig = minimalConfig(c)
426+ valid, err := nullProvider{}.Validate(testConfig, nil)
427+ c.Assert(err, gc.IsNil)
428+
429+ unknownAttrs := valid.UnknownAttrs()
430+ c.Assert(unknownAttrs["bootstrap-host"], gc.Equals, "hostname")
431+ c.Assert(unknownAttrs["bootstrap-user"], gc.Equals, "")
432+ c.Assert(unknownAttrs["storage-listen-ip"], gc.Equals, "")
433+ c.Assert(unknownAttrs["storage-port"], gc.Equals, int64(8040))
434+}
435+
436+func (s *configSuite) TestConfigMutability(c *gc.C) {
437+ testConfig := minimalConfig(c)
438+ valid, err := nullProvider{}.Validate(testConfig, nil)
439+ c.Assert(err, gc.IsNil)
440+ unknownAttrs := valid.UnknownAttrs()
441+
442+ // Make sure the immutable values can't be changed. It'd be nice to be
443+ // able to change these, but that would involve somehow updating the
444+ // machine agent's config/upstart config.
445+ oldConfig := testConfig
446+ for k, v := range map[string]interface{}{
447+ "bootstrap-host": "new-hostname",
448+ "bootstrap-user": "new-username",
449+ "storage-listen-ip": "10.0.0.123",
450+ "storage-port": int64(1234),
451+ } {
452+ testConfig = minimalConfig(c)
453+ testConfig, err = testConfig.Apply(map[string]interface{}{k: v})
454+ c.Assert(err, gc.IsNil)
455+ _, err := nullProvider{}.Validate(testConfig, oldConfig)
456+ oldv := unknownAttrs[k]
457+ errmsg := fmt.Sprintf("cannot change %s from %q to %q", k, oldv, v)
458+ c.Assert(err, gc.ErrorMatches, regexp.QuoteMeta(errmsg))
459+ }
460+}
461+
462+func (s *configSuite) TestBootstrapHostUser(c *gc.C) {
463+ values := minimalConfigValues()
464+ testConfig := getEnvironConfig(c, values)
465+ c.Assert(testConfig.bootstrapHost(), gc.Equals, "hostname")
466+ c.Assert(testConfig.bootstrapUser(), gc.Equals, "")
467+ c.Assert(testConfig.sshHost(), gc.Equals, "hostname")
468+ values["bootstrap-host"] = "127.0.0.1"
469+ values["bootstrap-user"] = "ubuntu"
470+ testConfig = getEnvironConfig(c, values)
471+ c.Assert(testConfig.bootstrapHost(), gc.Equals, "127.0.0.1")
472+ c.Assert(testConfig.bootstrapUser(), gc.Equals, "ubuntu")
473+ c.Assert(testConfig.sshHost(), gc.Equals, "ubuntu@127.0.0.1")
474+}
475+
476+func (s *configSuite) TestStorageParams(c *gc.C) {
477+ values := minimalConfigValues()
478+ testConfig := getEnvironConfig(c, values)
479+ c.Assert(testConfig.storageAddr(), gc.Equals, "hostname:8040")
480+ c.Assert(testConfig.storageListenAddr(), gc.Equals, ":8040")
481+ values["storage-listen-ip"] = "10.0.0.123"
482+ values["storage-port"] = int64(1234)
483+ testConfig = getEnvironConfig(c, values)
484+ c.Assert(testConfig.storageAddr(), gc.Equals, "hostname:1234")
485+ c.Assert(testConfig.storageListenAddr(), gc.Equals, "10.0.0.123:1234")
486+}
487
488=== added file 'provider/null/environ.go'
489--- provider/null/environ.go 1970-01-01 00:00:00 +0000
490+++ provider/null/environ.go 2013-09-26 11:22:31 +0000
491@@ -0,0 +1,173 @@
492+// Copyright 2013 Canonical Ltd.
493+// Licensed under the AGPLv3, see LICENCE file for details.
494+
495+package null
496+
497+import (
498+ "errors"
499+ "path"
500+ "sync"
501+
502+ "launchpad.net/juju-core/constraints"
503+ "launchpad.net/juju-core/environs"
504+ "launchpad.net/juju-core/environs/cloudinit"
505+ "launchpad.net/juju-core/environs/config"
506+ "launchpad.net/juju-core/environs/httpstorage"
507+ "launchpad.net/juju-core/environs/manual"
508+ "launchpad.net/juju-core/environs/sshstorage"
509+ "launchpad.net/juju-core/environs/storage"
510+ "launchpad.net/juju-core/instance"
511+ "launchpad.net/juju-core/provider"
512+ "launchpad.net/juju-core/state"
513+ "launchpad.net/juju-core/state/api"
514+ "launchpad.net/juju-core/tools"
515+)
516+
517+const (
518+ // TODO(axw) make this configurable?
519+ dataDir = "/var/lib/juju"
520+
521+ // storageSubdir is the subdirectory of
522+ // dataDir in which storage will be located.
523+ storageSubdir = "storage"
524+
525+ // storageTmpSubdir is the subdirectory of
526+ // dataDir in which temporary storage will
527+ // be located.
528+ storageTmpSubdir = "storage-tmp"
529+)
530+
531+type nullEnviron struct {
532+ cfg *environConfig
533+ cfgmutex sync.Mutex
534+}
535+
536+var errNoStartInstance = errors.New("null provider cannot start instances")
537+var errNoStopInstance = errors.New("null provider cannot stop instances")
538+
539+func (*nullEnviron) StartInstance(constraints.Value, tools.List, *cloudinit.MachineConfig) (instance.Instance, *instance.HardwareCharacteristics, error) {
540+ return nil, nil, errNoStartInstance
541+}
542+
543+func (*nullEnviron) StopInstances([]instance.Instance) error {
544+ return errNoStopInstance
545+}
546+
547+func (e *nullEnviron) AllInstances() ([]instance.Instance, error) {
548+ return e.Instances([]instance.Id{manual.BootstrapInstanceId})
549+}
550+
551+func (e *nullEnviron) envConfig() (cfg *environConfig) {
552+ e.cfgmutex.Lock()
553+ cfg = e.cfg
554+ e.cfgmutex.Unlock()
555+ return cfg
556+}
557+
558+func (e *nullEnviron) Config() *config.Config {
559+ return e.envConfig().Config
560+}
561+
562+func (e *nullEnviron) Name() string {
563+ return e.envConfig().Name()
564+}
565+
566+func (e *nullEnviron) Bootstrap(_ constraints.Value, possibleTools tools.List, machineID string) error {
567+ return manual.Bootstrap(manual.BootstrapArgs{
568+ Host: e.envConfig().sshHost(),
569+ DataDir: dataDir,
570+ Environ: e,
571+ MachineId: machineID,
572+ PossibleTools: possibleTools,
573+ })
574+}
575+
576+func (e *nullEnviron) StateInfo() (*state.Info, *api.Info, error) {
577+ return provider.StateInfo(e)
578+}
579+
580+func (e *nullEnviron) SetConfig(cfg *config.Config) error {
581+ e.cfgmutex.Lock()
582+ defer e.cfgmutex.Unlock()
583+ envConfig, err := nullProvider{}.validate(cfg, e.cfg.Config)
584+ if err != nil {
585+ return err
586+ }
587+ e.cfg = envConfig
588+ return nil
589+}
590+
591+// Implements environs.Environ.
592+//
593+// This method will only ever return an Instance for the Id
594+// environ/manual.BootstrapInstanceId. If any others are
595+// specified, then ErrPartialInstances or ErrNoInstances
596+// will result.
597+func (e *nullEnviron) Instances(ids []instance.Id) (instances []instance.Instance, err error) {
598+ instances = make([]instance.Instance, len(ids))
599+ var found bool
600+ for i, id := range ids {
601+ if id == manual.BootstrapInstanceId {
602+ instances[i] = nullBootstrapInstance{e.envConfig().bootstrapHost()}
603+ found = true
604+ } else {
605+ err = environs.ErrPartialInstances
606+ }
607+ }
608+ if !found {
609+ err = environs.ErrNoInstances
610+ }
611+ return instances, err
612+}
613+
614+// Implements environs/bootstrap.BootstrapStorage.
615+func (e *nullEnviron) BootstrapStorage() (storage.Storage, error) {
616+ cfg := e.envConfig()
617+ storageDir := e.StorageDir()
618+ storageTmpdir := path.Join(dataDir, storageTmpSubdir)
619+ return sshstorage.NewSSHStorage(cfg.sshHost(), storageDir, storageTmpdir)
620+}
621+
622+func (e *nullEnviron) Storage() storage.Storage {
623+ return httpstorage.Client(e.envConfig().storageAddr())
624+}
625+
626+func (e *nullEnviron) PublicStorage() storage.StorageReader {
627+ return environs.EmptyStorage
628+}
629+
630+func (e *nullEnviron) Destroy() error {
631+ return errors.New("null provider destruction is not implemented yet")
632+}
633+
634+func (e *nullEnviron) OpenPorts(ports []instance.Port) error {
635+ return nil
636+}
637+
638+func (e *nullEnviron) ClosePorts(ports []instance.Port) error {
639+ return nil
640+}
641+
642+func (e *nullEnviron) Ports() ([]instance.Port, error) {
643+ return []instance.Port{}, nil
644+}
645+
646+func (*nullEnviron) Provider() environs.EnvironProvider {
647+ return nullProvider{}
648+}
649+
650+func (e *nullEnviron) StorageAddr() string {
651+ return e.envConfig().storageListenAddr()
652+}
653+
654+func (e *nullEnviron) StorageDir() string {
655+ return path.Join(dataDir, storageSubdir)
656+}
657+
658+func (e *nullEnviron) SharedStorageAddr() string {
659+ return ""
660+}
661+
662+func (e *nullEnviron) SharedStorageDir() string {
663+ return ""
664+}
665
666=== added file 'provider/null/environ_test.go'
667--- provider/null/environ_test.go 1970-01-01 00:00:00 +0000
668+++ provider/null/environ_test.go 2013-09-26 11:22:31 +0000
669@@ -0,0 +1,81 @@
670+// Copyright 2013 Canonical Ltd.
671+// Licensed under the AGPLv3, see LICENCE file for details.
672+
673+package null
674+
675+import (
676+ gc "launchpad.net/gocheck"
677+
678+ "launchpad.net/juju-core/environs"
679+ "launchpad.net/juju-core/environs/manual"
680+ "launchpad.net/juju-core/instance"
681+)
682+
683+type environSuite struct {
684+ env *nullEnviron
685+}
686+
687+var _ = gc.Suite(&environSuite{})
688+
689+func (s *environSuite) SetUpTest(c *gc.C) {
690+ envConfig := getEnvironConfig(c, minimalConfigValues())
691+ s.env = &nullEnviron{cfg: envConfig}
692+}
693+
694+func (s *environSuite) TestSetConfig(c *gc.C) {
695+ err := s.env.SetConfig(minimalConfig(c))
696+ c.Assert(err, gc.IsNil)
697+
698+ testConfig := minimalConfig(c)
699+ testConfig, err = testConfig.Apply(map[string]interface{}{"bootstrap-host": ""})
700+ c.Assert(err, gc.IsNil)
701+ err = s.env.SetConfig(testConfig)
702+ c.Assert(err, gc.ErrorMatches, "bootstrap-host must be specified")
703+}
704+
705+func (s *environSuite) TestInstances(c *gc.C) {
706+ var ids []instance.Id
707+
708+ instances, err := s.env.Instances(ids)
709+ c.Assert(err, gc.Equals, environs.ErrNoInstances)
710+ c.Assert(instances, gc.HasLen, 0)
711+
712+ ids = append(ids, manual.BootstrapInstanceId)
713+ instances, err = s.env.Instances(ids)
714+ c.Assert(err, gc.IsNil)
715+ c.Assert(instances, gc.HasLen, 1)
716+ c.Assert(instances[0], gc.NotNil)
717+
718+ ids = append(ids, manual.BootstrapInstanceId)
719+ instances, err = s.env.Instances(ids)
720+ c.Assert(err, gc.IsNil)
721+ c.Assert(instances, gc.HasLen, 2)
722+ c.Assert(instances[0], gc.NotNil)
723+ c.Assert(instances[1], gc.NotNil)
724+
725+ ids = append(ids, instance.Id("invalid"))
726+ instances, err = s.env.Instances(ids)
727+ c.Assert(err, gc.Equals, environs.ErrPartialInstances)
728+ c.Assert(instances, gc.HasLen, 3)
729+ c.Assert(instances[0], gc.NotNil)
730+ c.Assert(instances[1], gc.NotNil)
731+ c.Assert(instances[2], gc.IsNil)
732+
733+ ids = []instance.Id{instance.Id("invalid")}
734+ instances, err = s.env.Instances(ids)
735+ c.Assert(err, gc.Equals, environs.ErrNoInstances)
736+ c.Assert(instances, gc.HasLen, 1)
737+ c.Assert(instances[0], gc.IsNil)
738+}
739+
740+func (s *environSuite) TestDestroy(c *gc.C) {
741+ c.Assert(s.env.Destroy(), gc.ErrorMatches, "null provider destruction is not implemented yet")
742+}
743+
744+func (s *environSuite) TestLocalStorageConfig(c *gc.C) {
745+ c.Assert(s.env.StorageDir(), gc.Equals, "/var/lib/juju/storage")
746+ c.Assert(s.env.cfg.storageListenAddr(), gc.Equals, ":8040")
747+ c.Assert(s.env.StorageAddr(), gc.Equals, s.env.cfg.storageListenAddr())
748+ c.Assert(s.env.SharedStorageAddr(), gc.Equals, "")
749+ c.Assert(s.env.SharedStorageDir(), gc.Equals, "")
750+}
751
752=== added file 'provider/null/instance.go'
753--- provider/null/instance.go 1970-01-01 00:00:00 +0000
754+++ provider/null/instance.go 2013-09-26 11:22:31 +0000
755@@ -0,0 +1,68 @@
756+// Copyright 2013 Canonical Ltd.
757+// Licensed under the AGPLv3, see LICENCE file for details.
758+
759+package null
760+
761+import (
762+ "net"
763+
764+ "launchpad.net/juju-core/environs/manual"
765+ "launchpad.net/juju-core/instance"
766+)
767+
768+type nullBootstrapInstance struct {
769+ host string
770+}
771+
772+func (_ nullBootstrapInstance) Id() instance.Id {
773+ // The only way to bootrap is via manual bootstrap.
774+ return manual.BootstrapInstanceId
775+}
776+
777+func (_ nullBootstrapInstance) Status() string {
778+ return ""
779+}
780+
781+func (inst nullBootstrapInstance) Addresses() (addresses []instance.Address, err error) {
782+ host, err := inst.DNSName()
783+ if err != nil {
784+ return nil, err
785+ }
786+ addresses, err = instance.HostAddresses(host)
787+ if err != nil {
788+ return nil, err
789+ }
790+ // Add a HostName type address.
791+ addresses = append(addresses, instance.NewAddress(host))
792+ return addresses, nil
793+}
794+
795+func (inst nullBootstrapInstance) DNSName() (string, error) {
796+ // If the user specified bootstrap-host as an IP address,
797+ // do a reverse lookup.
798+ host := inst.host
799+ if ip := net.ParseIP(host); ip != nil {
800+ names, err := net.LookupAddr(ip.String())
801+ if err != nil {
802+ return "", err
803+ }
804+ host = names[0]
805+ }
806+ return host, nil
807+}
808+
809+func (i nullBootstrapInstance) WaitDNSName() (string, error) {
810+ return i.DNSName()
811+}
812+
813+func (_ nullBootstrapInstance) OpenPorts(machineId string, ports []instance.Port) error {
814+ return nil
815+}
816+
817+func (_ nullBootstrapInstance) ClosePorts(machineId string, ports []instance.Port) error {
818+ return nil
819+}
820+
821+func (_ nullBootstrapInstance) Ports(machineId string) ([]instance.Port, error) {
822+ return []instance.Port{}, nil
823+}
824
825=== added file 'provider/null/provider.go'
826--- provider/null/provider.go 1970-01-01 00:00:00 +0000
827+++ provider/null/provider.go 2013-09-26 11:22:31 +0000
828@@ -0,0 +1,121 @@
829+// Copyright 2013 Canonical Ltd.
830+// Licensed under the AGPLv3, see LICENCE file for details.
831+
832+package null
833+
834+import (
835+ "errors"
836+ "fmt"
837+
838+ "launchpad.net/juju-core/environs"
839+ "launchpad.net/juju-core/environs/config"
840+ "launchpad.net/juju-core/provider"
841+ "launchpad.net/juju-core/utils"
842+)
843+
844+type nullProvider struct{}
845+
846+func init() {
847+ environs.RegisterProvider(provider.Null, nullProvider{})
848+}
849+
850+var errNoBootstrapHost = errors.New("bootstrap-host must be specified")
851+
852+func (p nullProvider) Prepare(cfg *config.Config) (environs.Environ, error) {
853+ return p.Open(cfg)
854+}
855+
856+func (p nullProvider) Open(cfg *config.Config) (environs.Environ, error) {
857+ envConfig, err := p.validate(cfg, nil)
858+ if err != nil {
859+ return nil, err
860+ }
861+ return &nullEnviron{cfg: envConfig}, nil
862+}
863+
864+func checkImmutableString(cfg, old *environConfig, key string) error {
865+ if old.attrs[key] != cfg.attrs[key] {
866+ return fmt.Errorf("cannot change %s from %q to %q", key, old.attrs[key], cfg.attrs[key])
867+ }
868+ return nil
869+}
870+
871+func (p nullProvider) validate(cfg, old *config.Config) (*environConfig, error) {
872+ // Check for valid changes for the base config values.
873+ if err := config.Validate(cfg, old); err != nil {
874+ return nil, err
875+ }
876+ validated, err := cfg.ValidateUnknownAttrs(configFields, configDefaults)
877+ if err != nil {
878+ return nil, err
879+ }
880+ envConfig := newEnvironConfig(cfg, validated)
881+ if envConfig.bootstrapHost() == "" {
882+ return nil, errNoBootstrapHost
883+ }
884+ // Check various immutable attributes.
885+ if old != nil {
886+ oldEnvConfig, err := p.validate(old, nil)
887+ if err != nil {
888+ return nil, err
889+ }
890+ for _, key := range [...]string{
891+ "bootstrap-user",
892+ "bootstrap-host",
893+ "storage-listen-ip",
894+ } {
895+ if err = checkImmutableString(envConfig, oldEnvConfig, key); err != nil {
896+ return nil, err
897+ }
898+ }
899+ oldPort, newPort := oldEnvConfig.storagePort(), envConfig.storagePort()
900+ if oldPort != newPort {
901+ return nil, fmt.Errorf("cannot change storage-port from %q to %q", oldPort, newPort)
902+ }
903+ }
904+ return envConfig, nil
905+}
906+
907+func (p nullProvider) Validate(cfg, old *config.Config) (valid *config.Config, err error) {
908+ envConfig, err := p.validate(cfg, old)
909+ if err != nil {
910+ return nil, err
911+ }
912+ return cfg.Apply(envConfig.attrs)
913+}
914+
915+func (_ nullProvider) BoilerplateConfig() string {
916+ return `"null":
917+ type: "null"
918+ admin-secret: {{rand}}
919+ ## set bootstrap-host to the host where the bootstrap machine agent
920+ ## should be provisioned.
921+ bootstrap-host:
922+ ## set the login user to bootstrap the machine as. If left blank,
923+ ## juju will connect to the bootstrap machine as the current user.
924+ # bootstrap-user:
925+ ## set the IP address for the bootstrap machine to listen on for
926+ ## storage requests. If left blank, storage will be served on all
927+ ## network interfaces.
928+ # storage-listen-ip:
929+ # storage-port: 8040
930+
931+`
932+}
933+
934+func (_ nullProvider) SecretAttrs(cfg *config.Config) (map[string]string, error) {
935+ return make(map[string]string), nil
936+}
937+
938+func (_ nullProvider) PublicAddress() (string, error) {
939+ // TODO(axw) 2013-09-10 bug #1222643
940+ //
941+ // eth0 may not be the desired interface for traffic to route
942+ // through. We should somehow make this configurable, and
943+ // possibly also record the IP resolved during manual bootstrap.
944+ return utils.GetAddressForInterface("eth0")
945+}
946+
947+func (p nullProvider) PrivateAddress() (string, error) {
948+ return p.PublicAddress()
949+}

Subscribers

People subscribed via source and target branches

to status/vote changes: