Merge lp:~axwalk/juju-core/null-provider-take3 into lp:~go-bot/juju-core/trunk
- null-provider-take3
- Merge into trunk
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 |
Related bugs: |
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.
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.
Andrew Wilkins (axwalk) wrote : | # |
William Reade (fwereade) wrote : | # |
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) JobHostEnvironS
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) JobManageEnviro
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:/
File cmd/juju/
https:/
cmd/juju/
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:/
File cmd/juju/
https:/
cmd/juju/
case provider.Null, provider.Local:
?
https:/
File cmd/jujud/
https:/
cmd/jujud/
that is the "bootstrap" node.
Oo, I don't like that comment. If we're going to be HA, storage is going
to ha...
Andrew Wilkins (axwalk) wrote : | # |
https:/
File cmd/juju/
https:/
cmd/juju/
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:/
File cmd/juju/
https:/
cmd/juju/
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:/
File cmd/jujud/
https:/
cmd/jujud/
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:/
File cmd/jujud/
https:/
cmd/jujud/
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, JobHostEnvironS
That makes sense. There's a bunch of things to touch, so I have created:
https:/
and will follow up. If you prefer I do this now, though, I can.
https:/
File provider/
https:/
provider/nul...
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
William Reade (fwereade) wrote : | # |
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/
https:/
File cmd/jujud/
https:/
cmd/jujud/
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:/
File cmd/jujud/
https:/
cmd/jujud/
level.
FWIW the firewaller handles instance-level provisioning too.
https:/
File provider/
https:/
provider/
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:/
provider/
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:/
File provider/
https:/
provider/
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 ...
Andrew Wilkins (axwalk) wrote : | # |
https:/
File cmd/jujud/
https:/
cmd/jujud/
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:/
File provider/
https:/
provider/
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:/
File provider/
https:/
provider/
an IP address,
On 2013/09/26 08:03:41, fwereade wrote:
> s/bootsrap/
Thanks, will fix.
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...
Andrew Wilkins (axwalk) wrote : | # |
https:/
File provider/
https:/
provider/
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.
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
William Reade (fwereade) wrote : | # |
LGTM then, destroying properly can come in followups.
Go Bot (go-bot) wrote : | # |
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.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
? launchpad.
? launchpad.
? launchpad.
listing available tools
built 1.15.0.
listing available tools
found 2 tools
listing target bucket
found 0 tools in target; 2 tools to be copied
copying 1.15.0.
copying tools/releases/
downloaded tools/releases/
download 3244kB, uploading
copying 1.15.0.
copying tools/releases/
downloaded tools/releases/
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://
copying tools/releases/
downloaded tools/releases/
download 0kB, uploading
copying 1.2.0-quantal-amd64 from http://
copying tools/releases/
downloaded tools/releases/
download 0kB, uploading
copying 1.2.0-quantal-i386 from http://
copying tools/releases/
downloaded tools/releases/
download 0kB, uploading
copying 1.2.0-raring-amd64 from http://
copying tools/releases/
downloaded tools/releases/
download 0kB, uploading
copied 4 tools
generating...
Preview Diff
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 | +} |
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): bootstrap. go machine. go cloudinit/ cloudinit. go manual/ agent.go manual/ bootstrap. go sshstorage/ storage. go null/config. go null/config_ test.go null/environ. go null/environ_ test.go null/instance. go null/provider. go
A [revision details]
M cmd/juju/
M cmd/jujud/
M environs/
M environs/
M environs/
M environs/
M provider/all/all.go
A provider/
A provider/
A provider/
A provider/
A provider/
A provider/