Merge lp:~axwalk/juju-core/lp1258240-bootstrap-refresh-dnsname-take2 into lp:~go-bot/juju-core/trunk
- lp1258240-bootstrap-refresh-dnsname-take2
- Merge into trunk
Status: | Merged | ||||||||
---|---|---|---|---|---|---|---|---|---|
Approved by: | Andrew Wilkins | ||||||||
Approved revision: | no longer in the source branch. | ||||||||
Merged at revision: | 2158 | ||||||||
Proposed branch: | lp:~axwalk/juju-core/lp1258240-bootstrap-refresh-dnsname-take2 | ||||||||
Merge into: | lp:~go-bot/juju-core/trunk | ||||||||
Diff against target: |
1303 lines (+486/-171) 26 files modified
cloudinit/cloudinit_test.go (+1/-1) cloudinit/options.go (+1/-1) cloudinit/sshinit/configure.go (+0/-11) cmd/jujud/machine_test.go (+1/-1) container/kvm/instance.go (+4/-0) container/lxc/instance.go (+4/-0) container/lxc/lxc_test.go (+1/-1) environs/cloudinit/cloudinit.go (+17/-0) environs/cloudinit/cloudinit_test.go (+7/-3) environs/cloudinit_test.go (+5/-1) instance/instance.go (+3/-0) provider/azure/instance.go (+7/-0) provider/common/bootstrap.go (+209/-59) provider/common/bootstrap_test.go (+95/-37) provider/common/export_test.go (+1/-0) provider/dummy/environs.go (+4/-0) provider/ec2/ec2.go (+37/-17) provider/ec2/local_test.go (+4/-0) provider/joyent/instance.go (+4/-0) provider/local/instance.go (+4/-0) provider/maas/environ.go (+1/-1) provider/maas/environ_whitebox_test.go (+2/-2) provider/maas/instance.go (+22/-7) provider/maas/instance_test.go (+10/-10) provider/null/instance.go (+15/-11) provider/openstack/provider.go (+27/-8) |
||||||||
To merge this branch: | bzr merge lp:~axwalk/juju-core/lp1258240-bootstrap-refresh-dnsname-take2 | ||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju Engineering | Pending | ||
Review via email: mp+198867@code.launchpad.net |
Commit message
provider/common: attempt all addresses in waitSSH
We now attempt all addresses during waitSSH, and
refresh the addresses periodically. Providers may
return private addresses; for some clouds this is
what we want to use, and for others it is not.
As private addresses are relative, one may be able
to connect to the private address and yet be
connecting to a machine other than the intended
one. For this reason, we emit the machine's nonce
during cloud-init to a file (/var/lib/
and verify it during waitSSH.
The nonce verification also serves to synchronise
cloud-init and sshinit. Since the file is written
as the last thing cloud-init does, we can safely
run sshinit once it has been verified.
Fixes #1258240
Fixes #1259942
Description of the change
provider/common: attempt all addresses in waitSSH
We now attempt all addresses during waitSSH, and
refresh the addresses periodically. Providers may
return private addresses; for some clouds this is
what we want to use, and for others it is not.
As private addresses are relative, one may be able
to connect to the private address and yet be
connecting to a machine other than the intended
one. For this reason, we emit the machine's nonce
during cloud-init to a file (/var/lib/
and verify it during waitSSH.
The nonce verification also serves to synchronise
cloud-init and sshinit. Since the file is written
as the last thing cloud-init does, we can safely
run sshinit once it has been verified.
Fixes #1258240
Fixes #1259942
Andrew Wilkins (axwalk) wrote : | # |
Andrew Wilkins (axwalk) wrote : | # |
On 2013/12/13 04:13:23, axw wrote:
> Please take a look.
Hm, actually, I think this may break when a floating/public IP comes
along that we can't connect to from an internal network.
Andrew Wilkins (axwalk) wrote : | # |
On 2013/12/13 04:25:05, axw wrote:
> On 2013/12/13 04:13:23, axw wrote:
> > Please take a look.
> Hm, actually, I think this may break when a floating/public IP comes
along that
> we can't connect to from an internal network.
I played around with a dynamic select (reflect.Select) allowing the loop
to receive results from old/ongoing attempts. I think combined with a
check for some file we write to disk in cloud-init this could be
workable. I'll continue on with it next week.
John A Meinel (jameinel) wrote : | # |
https:/
File provider/
https:/
provider/
(string, error) {
Doing this trick of reloading the Instance so that we don't cache sounds
like something we would do if we didn't control the whole library.
Is there a reason we can't just change the behavior. Either add a method
to indicate we want the uncached DNSName or a flag or something else to
Instance? Adding a proxy instance just to create them on the fly seems
like a strange workaround to me.
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
https:/
File provider/
https:/
provider/
(string, error) {
On 2013/12/15 12:36:38, jameinel wrote:
> Doing this trick of reloading the Instance so that we don't cache
sounds like
> something we would do if we didn't control the whole library.
> Is there a reason we can't just change the behavior. Either add a
method to
> indicate we want the uncached DNSName or a flag or something else to
Instance?
> Adding a proxy instance just to create them on the fly seems like a
strange
> workaround to me.
I will investigate this. In the mean time, I have updated the CL so that
it:
- works on HP Cloud
- works on Canonistack (i.e. where you really do want to use the
private IP)
- verifies hosts by checking a file created by cloud-init contains the
machine's nonce
It works by attempting *all* addresses in parallel. I did away with the
initial port 22 check, and went straight to SSH; it's not slow relative
to the rest of the bootstrap process, and it's necessary anyway to
verify the nonce.
Andrew Wilkins (axwalk) wrote : | # |
https:/
File provider/
https:/
provider/
utils.ShQuote(
The bootstrap machine's nonce is static, so it's conceivable that the
private address points to another machine that has Juju on it and is
also a bootstrap node. Unlikely, perhaps, but conceivable.
If this CL is okay otherwise, I'll change Juju to use a UUID for the
bootstrap nonce too.
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
Andrew Wilkins (axwalk) wrote : | # |
On 2013/12/16 09:56:56, axw wrote:
> Please take a look.
I had intended to branch and prereq, but I forgot the vital branch step.
Oh well. If it's too much, let me know and I can try to fix it.
Otherwise... there's a Refresh method on instance.Instance now.
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
Roger Peppe (rogpeppe) wrote : | # |
Looks great in general, with some comments and suggestions below.
https:/
File provider/
https:/
provider/
not
s/not/no/
https:/
File environs/
https:/
environs/
NonceFile), cfg.MachineNonce, 0644)
Is this really the last thing that happens in cloudinit?
What if there are some scripts in the cloudinit.Config?
I think this should probably be an added script.
https:/
File provider/
https:/
provider/
This should probably allow itself to be stopped when closed.j
https:/
provider/
I was wondering if some of this code might look nicer in a separate type
that wraps the Try.
type parallelHostChecker struct {
active make(map[
*parallel.Try
}
func newParallelHost
*parallelHostCh
func (g *parallelHostCh
func (g *parallelHostCh
g.Try.Close()
for _, ch := range active {
close(ch)
}
return nil
}
https:/
File provider/
https:/
provider/
Might be worth running go test -race just to make sure that it's ok to
use this, assuming you haven't already.
https:/
File provider/ec2/ec2.go (right):
https:/
provider/
just "mu" should be fine here.
conventionally it's situated above the fields that it guards.
https:/
provider/
shouldn't this use the mutex?
https:/
provider/
ditto
https:/
provider/
ditto
https:/
File provider/
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
https:/
File environs/
https:/
environs/
NonceFile), cfg.MachineNonce, 0644)
On 2013/12/18 09:01:05, rog wrote:
> Is this really the last thing that happens in cloudinit?
> What if there are some scripts in the cloudinit.Config?
It really is. ConfigureBasic is the only thing called to set up
cloud-init for the bootstrap node.
> I think this should probably be an added script.
Added where though? All of the providers call environs.
and so there's no nice place to do that.
I could move environs.
script in that function, but I don't think we'll have gained anything.
https:/
File provider/
https:/
provider/
On 2013/12/18 09:01:05, rog wrote:
> This should probably allow itself to be stopped when closed.j
Done.
https:/
provider/
On 2013/12/18 09:01:05, rog wrote:
> I was wondering if some of this code might look nicer in a separate
type that
> wraps the Try.
> type parallelHostChecker struct {
> active make(map[
> *parallel.Try
> }
> func newParallelHost
*parallelHostCh
> func (g *parallelHostCh
> func (g *parallelHostCh
> g.Try.Close()
> for _, ch := range active {
> close(ch)
> }
> return nil
> }
Done.
https:/
File provider/
https:/
provider/
On 2013/12/18 09:01:05, rog wrote:
> Might be worth running go test -race just to make sure that it's ok to
use this,
> assuming you haven't already.
The printing to stderr is all done within one goroutine (the one that
calls WaitSSH), so there's no potential for race.
However, go test -race has shown up something else :)
In bootstrap.go, hostChecker.loop spawns a goroutine that may outlive
loop (and everything that calls that). That goroutine calls connectSSH,
a function var that is replaced in the test setup/teardown.
https:/
File provider/ec2/ec2.go (right):
https:/
provider/
On 2013/12/18 09:01:05, rog wrote:
> just "mu" should be fine here.
> conventionally it's situated above the fields tha...
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
Roger Peppe (rogpeppe) wrote : | # |
LGTM, thanks, except I'd like to know a bit more about the AddFile vs
AddRunCmd issue.
https:/
File environs/
https:/
environs/
NonceFile), cfg.MachineNonce, 0644)
On 2013/12/18 10:22:10, axw wrote:
> On 2013/12/18 09:01:05, rog wrote:
> > Is this really the last thing that happens in cloudinit?
> > What if there are some scripts in the cloudinit.Config?
> It really is. ConfigureBasic is the only thing called to set up
cloud-init for
> the bootstrap node.
> > I think this should probably be an added script.
> Added where though? All of the providers call
environs.
> there's no nice place to do that.
I was thinking that instead of this AddFile we'd just
call c.AddRunCmd(
If we do things as above and one of the providers uses the
additionalScripts argument to ComposeUserData, won't we
write the nonce file before running any of those scripts?
But I'm probably misunderstanding the issues around this.
https:/
File provider/ec2/ec2.go (right):
https:/
provider/
On 2013/12/18 10:22:10, axw wrote:
> On 2013/12/18 09:01:05, rog wrote:
> > shouldn't this use the mutex?
> No, the mutex only guards ec2Instance.
> *ec2.Instance). I've put the mutex above that field and separated them
from "e"
> to make it more obvious.
Ahem. Discussed online.
https:/
provider/
On 2013/12/18 10:22:10, axw wrote:
> Oops, there's a potential double lock on the mutex here. Fixed that.
Since that evidently wasn't covered by the tests, perhaps we could have
a test that did cover that?
https:/
File provider/
https:/
provider/
idUnlocked() instance.Id {
On 2013/12/18 10:22:10, axw wrote:
> On 2013/12/18 09:01:05, rog wrote:
> > I'm not sure this pulls its weight.
> >
> > How about just:
> >
> > func idFromDetail(d *nova.ServerDetail) instance.Id {
> > return instance.
> > }
> > then Id can just become:
> >
> > func (inst *openstackInstance) Id() instance.Id {
> > return idFromDetail(
> > }
> Good point. I went a step further and inlined the use if
inst.serverDeta
> Refresh, and then:
> func (inst *openstackInstance) Id() instance.Id {
> return instance.
> }
Cool - I almost suggested that.
Andrew Wilkins (axwalk) wrote : | # |
On 2013/12/18 12:49:08, rog wrote:
> LGTM, thanks, except I'd like to know a bit more about the AddFile vs
AddRunCmd
> issue.
https:/
> File environs/
https:/
> environs/
> NonceFile), cfg.MachineNonce, 0644)
> On 2013/12/18 10:22:10, axw wrote:
> > On 2013/12/18 09:01:05, rog wrote:
> > > Is this really the last thing that happens in cloudinit?
> > > What if there are some scripts in the cloudinit.Config?
> >
> > It really is. ConfigureBasic is the only thing called to set up
cloud-init for
> > the bootstrap node.
> >
> > > I think this should probably be an added script.
> >
> > Added where though? All of the providers call
environs.
> > there's no nice place to do that.
> I was thinking that instead of this AddFile we'd just
> call c.AddRunCmd(
> If we do things as above and one of the providers uses the
additionalScripts
> argument to ComposeUserData, won't we
> write the nonce file before running any of those scripts?
> But I'm probably misunderstanding the issues around this.
Ah, I see what you mean. Well, AddFile is actually a runcmd underneath,
so it's not a problem. The additionalScripts are added before
ConfigureBasic is run, so the AddFile runcmd comes last.
https:/
> File provider/ec2/ec2.go (right):
https:/
> provider/
> On 2013/12/18 10:22:10, axw wrote:
> > On 2013/12/18 09:01:05, rog wrote:
> > > shouldn't this use the mutex?
> >
> > No, the mutex only guards ec2Instance.
> > *ec2.Instance). I've put the mutex above that field and separated
them from
> "e"
> > to make it more obvious.
> Ahem. Discussed online.
https:/
> provider/
> On 2013/12/18 10:22:10, axw wrote:
> > Oops, there's a potential double lock on the mutex here. Fixed that.
> Since that evidently wasn't covered by the tests, perhaps we could
have a test
> that did cover that?
Yes, I've just modified goamz/ec2/ec2test to set DNSName to "" the first
time it returns an instance. This showed up a bug. I'll repropose the
fix here, then propose the change to goamz later.
https:/
> File provider/
https:/
> provider/
idUnlocked()
> instance.Id {
> On 2013/12/18 10:22:10, axw wrote:
> > On 2013/12/18 09:01:05, rog wrote:
> > > I'm not sure this pulls its weight.
> > >
> > > How about just:
> > >
> > > func idFromDetail(d *nova.ServerDetail) instance.Id {
> > > return instance.
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
https:/
File environs/
https:/
environs/
NonceFile), cfg.MachineNonce, 0644)
On 2013/12/18 12:49:09, rog wrote:
> On 2013/12/18 10:22:10, axw wrote:
> > On 2013/12/18 09:01:05, rog wrote:
> > > Is this really the last thing that happens in cloudinit?
> > > What if there are some scripts in the cloudinit.Config?
> >
> > It really is. ConfigureBasic is the only thing called to set up
cloud-init for
> > the bootstrap node.
> >
> > > I think this should probably be an added script.
> >
> > Added where though? All of the providers call
environs.
> > there's no nice place to do that.
> I was thinking that instead of this AddFile we'd just
> call c.AddRunCmd(
> If we do things as above and one of the providers uses the
additionalScripts
> argument to ComposeUserData, won't we
> write the nonce file before running any of those scripts?
> But I'm probably misunderstanding the issues around this.
As discussed on IRC, I've made this explicitly use AddScripts (which
generates multiple runcmds) in place of AddFile, in the event AddFile
gets changed.
Preview Diff
1 | === modified file 'cloudinit/cloudinit_test.go' |
2 | --- cloudinit/cloudinit_test.go 2013-12-12 23:00:22 +0000 |
3 | +++ cloudinit/cloudinit_test.go 2013-12-18 15:16:49 +0000 |
4 | @@ -244,7 +244,7 @@ |
5 | const ( |
6 | header = "#cloud-config\n" |
7 | addFileExpected = `runcmd: |
8 | -- install -m 644 /dev/null '/etc/apt/apt.conf.d/99proxy' |
9 | +- install -D -m 644 /dev/null '/etc/apt/apt.conf.d/99proxy' |
10 | - printf '%s\n' '"Acquire::http::Proxy "http://10.0.3.1:3142";' > '/etc/apt/apt.conf.d/99proxy' |
11 | ` |
12 | ) |
13 | |
14 | === modified file 'cloudinit/options.go' |
15 | --- cloudinit/options.go 2013-12-11 07:18:02 +0000 |
16 | +++ cloudinit/options.go 2013-12-18 15:16:49 +0000 |
17 | @@ -316,7 +316,7 @@ |
18 | // of escape sequences differs between shells, namely bash and |
19 | // dash. Instead, we use printf (or we could use /bin/echo). |
20 | cfg.AddScripts( |
21 | - fmt.Sprintf("install -m %o /dev/null %s", mode, p), |
22 | + fmt.Sprintf("install -D -m %o /dev/null %s", mode, p), |
23 | fmt.Sprintf(`printf '%%s\n' %s > %s`, shquote(data), p), |
24 | ) |
25 | } |
26 | |
27 | === modified file 'cloudinit/sshinit/configure.go' |
28 | --- cloudinit/sshinit/configure.go 2013-12-10 14:43:21 +0000 |
29 | +++ cloudinit/sshinit/configure.go 2013-12-18 15:16:49 +0000 |
30 | @@ -75,17 +75,6 @@ |
31 | // but added here to avoid relying on that to be |
32 | // invariant. |
33 | script := []string{"#!/bin/bash", "set -e"} |
34 | - // Wait for cloud-init to complete. pkill will return non-zero |
35 | - // if there are no matching processes. |
36 | - script = append(script, ` |
37 | - if pkill -0 cloud-init; then |
38 | - printf "Waiting for cloud-init to finish" |
39 | - while pkill -0 cloud-init; do |
40 | - printf "." |
41 | - sleep 2s |
42 | - done |
43 | - printf "\n" |
44 | - fi`) |
45 | // We must initialise progress reporting before entering |
46 | // the subshell and redirecting stderr. |
47 | script = append(script, cloudinit.InitProgressCmd()) |
48 | |
49 | === modified file 'cmd/jujud/machine_test.go' |
50 | --- cmd/jujud/machine_test.go 2013-12-18 13:33:12 +0000 |
51 | +++ cmd/jujud/machine_test.go 2013-12-18 15:16:49 +0000 |
52 | @@ -26,8 +26,8 @@ |
53 | "launchpad.net/juju-core/state/api/params" |
54 | statetesting "launchpad.net/juju-core/state/testing" |
55 | "launchpad.net/juju-core/state/watcher" |
56 | + "launchpad.net/juju-core/testing" |
57 | coretesting "launchpad.net/juju-core/testing" |
58 | - "launchpad.net/juju-core/testing" |
59 | jc "launchpad.net/juju-core/testing/checkers" |
60 | "launchpad.net/juju-core/testing/testbase" |
61 | "launchpad.net/juju-core/tools" |
62 | |
63 | === modified file 'container/kvm/instance.go' |
64 | --- container/kvm/instance.go 2013-11-12 21:59:49 +0000 |
65 | +++ container/kvm/instance.go 2013-12-18 15:16:49 +0000 |
66 | @@ -29,6 +29,10 @@ |
67 | return "stopped" |
68 | } |
69 | |
70 | +func (*kvmInstance) Refresh() error { |
71 | + return nil |
72 | +} |
73 | + |
74 | func (kvm *kvmInstance) Addresses() ([]instance.Address, error) { |
75 | logger.Errorf("kvmInstance.Addresses not implemented") |
76 | return nil, nil |
77 | |
78 | === modified file 'container/lxc/instance.go' |
79 | --- container/lxc/instance.go 2013-10-03 12:08:15 +0000 |
80 | +++ container/lxc/instance.go 2013-12-18 15:16:49 +0000 |
81 | @@ -31,6 +31,10 @@ |
82 | return string(state) |
83 | } |
84 | |
85 | +func (*lxcInstance) Refresh() error { |
86 | + return nil |
87 | +} |
88 | + |
89 | func (lxc *lxcInstance) Addresses() ([]instance.Address, error) { |
90 | return nil, errors.NewNotImplementedError("lxcInstance.Addresses") |
91 | } |
92 | |
93 | === modified file 'container/lxc/lxc_test.go' |
94 | --- container/lxc/lxc_test.go 2013-12-04 04:28:17 +0000 |
95 | +++ container/lxc/lxc_test.go 2013-12-18 15:16:49 +0000 |
96 | @@ -88,7 +88,7 @@ |
97 | |
98 | c.Assert(scripts[len(scripts)-4:], gc.DeepEquals, []string{ |
99 | "start jujud-machine-1-lxc-0", |
100 | - "install -m 644 /dev/null '/etc/apt/apt.conf.d/99proxy-extra'", |
101 | + "install -D -m 644 /dev/null '/etc/apt/apt.conf.d/99proxy-extra'", |
102 | fmt.Sprintf(`printf '%%s\n' '%s' > '/etc/apt/apt.conf.d/99proxy-extra'`, configProxyExtra), |
103 | "ifconfig", |
104 | }) |
105 | |
106 | === modified file 'environs/cloudinit/cloudinit.go' |
107 | --- environs/cloudinit/cloudinit.go 2013-12-13 04:40:41 +0000 |
108 | +++ environs/cloudinit/cloudinit.go 2013-12-18 15:16:49 +0000 |
109 | @@ -140,6 +140,11 @@ |
110 | |
111 | const cloudInitOutputLog = "/var/log/cloud-init-output.log" |
112 | |
113 | +// NonceFile is written by cloud-init as the last thing it does. |
114 | +// The file will contain the machine's nonce. The filename is |
115 | +// relative to the Juju data-dir. |
116 | +const NonceFile = "nonce.txt" |
117 | + |
118 | // ConfigureBasic updates the provided cloudinit.Config with |
119 | // basic configuration to initialise an OS image, such that it can |
120 | // be connected to via SSH, and log to a standard location. |
121 | @@ -155,6 +160,18 @@ |
122 | func ConfigureBasic(cfg *MachineConfig, c *cloudinit.Config) error { |
123 | c.AddSSHAuthorizedKeys(cfg.AuthorizedKeys) |
124 | c.SetOutput(cloudinit.OutAll, "| tee -a "+cloudInitOutputLog, "") |
125 | + // Create a file in a well-defined location containing the machine's |
126 | + // nonce. The presence and contents of this file will be verified |
127 | + // during bootstrap. |
128 | + // |
129 | + // Note: this must be the last runcmd we do in ConfigureBasic, as |
130 | + // the presence of the nonce file is used to gate the remainder |
131 | + // of synchronous bootstrap. |
132 | + noncefile := shquote(path.Join(cfg.DataDir, NonceFile)) |
133 | + c.AddScripts( |
134 | + fmt.Sprintf("install -D -m %o /dev/null %s", 0644, noncefile), |
135 | + fmt.Sprintf(`printf '%%s\n' %s > %s`, shquote(cfg.MachineNonce), noncefile), |
136 | + ) |
137 | return nil |
138 | } |
139 | |
140 | |
141 | === modified file 'environs/cloudinit/cloudinit_test.go' |
142 | --- environs/cloudinit/cloudinit_test.go 2013-12-13 03:45:46 +0000 |
143 | +++ environs/cloudinit/cloudinit_test.go 2013-12-18 15:16:49 +0000 |
144 | @@ -88,6 +88,8 @@ |
145 | setEnvConfig: true, |
146 | expectScripts: ` |
147 | echo ENABLE_MONGODB="no" > /etc/default/mongodb |
148 | +install -D -m 644 /dev/null '/var/lib/juju/nonce.txt' |
149 | +printf '%s\\n' 'FAKE_NONCE' > '/var/lib/juju/nonce.txt' |
150 | test -e /proc/self/fd/9 \|\| exec 9>&2 |
151 | set -xe |
152 | mkdir -p /var/lib/juju |
153 | @@ -101,7 +103,7 @@ |
154 | tar zxf \$bin/tools.tar.gz -C \$bin |
155 | rm \$bin/tools\.tar\.gz && rm \$bin/juju1\.2\.3-precise-amd64\.sha256 |
156 | printf %s '{"version":"1\.2\.3-precise-amd64","url":"http://foo\.com/tools/releases/juju1\.2\.3-precise-amd64\.tgz","sha256":"1234","size":10}' > \$bin/downloaded-tools\.txt |
157 | -install -m 600 /dev/null '/etc/rsyslog\.d/25-juju\.conf' |
158 | +install -D -m 600 /dev/null '/etc/rsyslog\.d/25-juju\.conf' |
159 | printf '%s\\n' '.*' > '/etc/rsyslog.d/25-juju.conf' |
160 | restart rsyslog |
161 | mkdir -p '/var/lib/juju/agents/machine-0' |
162 | @@ -110,7 +112,7 @@ |
163 | install -m 600 /dev/null '/var/lib/juju/agents/machine-0/agent\.conf' |
164 | printf '%s\\n' '.*' > '/var/lib/juju/agents/machine-0/agent\.conf' |
165 | ln -s /var/lib/juju/tools/machine-0/jujud /usr/local/bin/juju-run |
166 | -install -m 600 /dev/null '/var/lib/juju/server\.pem' |
167 | +install -D -m 600 /dev/null '/var/lib/juju/server\.pem' |
168 | printf '%s\\n' 'SERVER CERT\\n[^']*SERVER KEY\\n[^']*' > '/var/lib/juju/server\.pem' |
169 | mkdir -p /var/lib/juju/db/journal |
170 | chmod 0700 /var/lib/juju/db |
171 | @@ -199,6 +201,8 @@ |
172 | SyslogPort: 514, |
173 | }, |
174 | expectScripts: ` |
175 | +install -D -m 644 /dev/null '/var/lib/juju/nonce.txt' |
176 | +printf '%s\\n' 'FAKE_NONCE' > '/var/lib/juju/nonce.txt' |
177 | test -e /proc/self/fd/9 \|\| exec 9>&2 |
178 | set -xe |
179 | mkdir -p /var/lib/juju |
180 | @@ -212,7 +216,7 @@ |
181 | tar zxf \$bin/tools.tar.gz -C \$bin |
182 | rm \$bin/tools\.tar\.gz && rm \$bin/juju1\.2\.3-linux-amd64\.sha256 |
183 | printf %s '{"version":"1\.2\.3-linux-amd64","url":"http://foo\.com/tools/releases/juju1\.2\.3-linux-amd64\.tgz","sha256":"1234","size":10}' > \$bin/downloaded-tools\.txt |
184 | -install -m 600 /dev/null '/etc/rsyslog\.d/25-juju\.conf' |
185 | +install -D -m 600 /dev/null '/etc/rsyslog\.d/25-juju\.conf' |
186 | printf '%s\\n' '.*' > '/etc/rsyslog\.d/25-juju\.conf' |
187 | restart rsyslog |
188 | mkdir -p '/var/lib/juju/agents/machine-99' |
189 | |
190 | === modified file 'environs/cloudinit_test.go' |
191 | --- environs/cloudinit_test.go 2013-12-04 10:18:57 +0000 |
192 | +++ environs/cloudinit_test.go 2013-12-18 15:16:49 +0000 |
193 | @@ -214,7 +214,11 @@ |
194 | "output": map[interface{}]interface{}{ |
195 | "all": "| tee -a /var/log/cloud-init-output.log", |
196 | }, |
197 | - "runcmd": []interface{}{"script1", "script2"}, |
198 | + "runcmd": []interface{}{ |
199 | + "script1", "script2", |
200 | + "install -D -m 644 /dev/null '/var/lib/juju/nonce.txt'", |
201 | + "printf '%s\\n' '5432' > '/var/lib/juju/nonce.txt'", |
202 | + }, |
203 | "ssh_authorized_keys": []interface{}{"wheredidileavemykeys"}, |
204 | }) |
205 | } else { |
206 | |
207 | === modified file 'instance/instance.go' |
208 | --- instance/instance.go 2013-09-27 18:08:05 +0000 |
209 | +++ instance/instance.go 2013-12-18 15:16:49 +0000 |
210 | @@ -35,6 +35,9 @@ |
211 | // Status returns the provider-specific status for the instance. |
212 | Status() string |
213 | |
214 | + // Refresh refreshes local knowledge of the instance from the provider. |
215 | + Refresh() error |
216 | + |
217 | // Addresses returns a list of hostnames or ip addresses |
218 | // associated with the instance. This will supercede DNSName |
219 | // which can be implemented by selecting a preferred address. |
220 | |
221 | === modified file 'provider/azure/instance.go' |
222 | --- provider/azure/instance.go 2013-09-30 19:40:06 +0000 |
223 | +++ provider/azure/instance.go 2013-12-18 15:16:49 +0000 |
224 | @@ -36,6 +36,13 @@ |
225 | |
226 | var AZURE_DOMAIN_NAME = "cloudapp.net" |
227 | |
228 | +// Refresh is specified in the Instance interface. |
229 | +func (azInstance *azureInstance) Refresh() error { |
230 | + // TODO(axw) 2013-12-16 #1261324 |
231 | + // Cache Addresses/netInfo, refresh here. |
232 | + return nil |
233 | +} |
234 | + |
235 | // Addresses is specified in the Instance interface. |
236 | func (azInstance *azureInstance) Addresses() ([]instance.Address, error) { |
237 | addrs := []instance.Address{} |
238 | |
239 | === modified file 'provider/common/bootstrap.go' |
240 | --- provider/common/bootstrap.go 2013-12-12 12:39:20 +0000 |
241 | +++ provider/common/bootstrap.go 2013-12-18 15:16:49 +0000 |
242 | @@ -6,9 +6,10 @@ |
243 | import ( |
244 | "fmt" |
245 | "io" |
246 | - "net" |
247 | "os" |
248 | "os/signal" |
249 | + "path" |
250 | + "strings" |
251 | "sync" |
252 | "time" |
253 | |
254 | @@ -23,6 +24,9 @@ |
255 | "launchpad.net/juju-core/environs/cloudinit" |
256 | "launchpad.net/juju-core/instance" |
257 | coretools "launchpad.net/juju-core/tools" |
258 | + "launchpad.net/juju-core/utils" |
259 | + "launchpad.net/juju-core/utils/parallel" |
260 | + "launchpad.net/juju-core/utils/ssh" |
261 | ) |
262 | |
263 | var logger = loggo.GetLogger("juju.provider.common") |
264 | @@ -116,10 +120,28 @@ |
265 | var FinishBootstrap = func(ctx *BootstrapContext, inst instance.Instance, machineConfig *cloudinit.MachineConfig) error { |
266 | var t tomb.Tomb |
267 | ctx.SetInterruptHandler(func() { t.Killf("interrupted") }) |
268 | + // Each attempt to connect to an address must verify the machine is the |
269 | + // bootstrap machine by checking its nonce file exists and contains the |
270 | + // nonce in the MachineConfig. This also blocks sshinit from proceeding |
271 | + // until cloud-init has completed, which is necessary to ensure apt |
272 | + // invocations don't trample each other. |
273 | + nonceFile := utils.ShQuote(path.Join(machineConfig.DataDir, cloudinit.NonceFile)) |
274 | + checkNonceCommand := fmt.Sprintf(` |
275 | + noncefile=%s |
276 | + if [ ! -e "$noncefile" ]; then |
277 | + echo "$noncefile does not exist" >&2 |
278 | + exit 1 |
279 | + fi |
280 | + content=$(cat $noncefile) |
281 | + if [ "$content" != %s ]; then |
282 | + echo "$noncefile contents do not match machine nonce" >&2 |
283 | + exit 1 |
284 | + fi |
285 | + `, nonceFile, utils.ShQuote(machineConfig.MachineNonce)) |
286 | // TODO: jam 2013-12-04 bug #1257649 |
287 | // It would be nice if users had some controll over their bootstrap |
288 | // timeout, since it is unlikely to be a perfect match for all clouds. |
289 | - dnsName, err := waitSSH(ctx, inst, &t, DefaultBootstrapSSHTimeout()) |
290 | + addr, err := waitSSH(ctx, checkNonceCommand, inst, &t, DefaultBootstrapSSHTimeout()) |
291 | if err != nil { |
292 | return err |
293 | } |
294 | @@ -133,7 +155,7 @@ |
295 | if err := cloudinit.ConfigureJuju(machineConfig, cloudcfg); err != nil { |
296 | return err |
297 | } |
298 | - return sshinit.Configure("ubuntu@"+dnsName, cloudcfg) |
299 | + return sshinit.Configure("ubuntu@"+addr, cloudcfg) |
300 | } |
301 | |
302 | // SSHTimeoutOpts lists the amount of time we will wait for various parts of |
303 | @@ -144,81 +166,209 @@ |
304 | // a state server. |
305 | Timeout time.Duration |
306 | |
307 | - // DNSNameDelay is the amount of time between refreshing the DNS name |
308 | - DNSNameDelay time.Duration |
309 | + // ConnectDelay is the amount of time between attempts to connect to an address. |
310 | + ConnectDelay time.Duration |
311 | + |
312 | + // AddressesDelay is the amount of time between refreshing the addresses. |
313 | + AddressesDelay time.Duration |
314 | } |
315 | |
316 | // DefaultBootstrapSSHTimeout is the time we'll wait for SSH to come up on the bootstrap node |
317 | func DefaultBootstrapSSHTimeout() SSHTimeoutOpts { |
318 | return SSHTimeoutOpts{ |
319 | - Timeout: 10 * time.Minute, |
320 | - DNSNameDelay: 1 * time.Second, |
321 | - } |
322 | -} |
323 | - |
324 | -type dnsNamer interface { |
325 | - // DNSName returns the DNS name for the instance. |
326 | - // If the name is not yet allocated, it will return |
327 | - // an ErrNoDNSName error. |
328 | - DNSName() (string, error) |
329 | -} |
330 | - |
331 | -// waitSSH waits for the instance to be assigned a DNS |
332 | -// entry, then waits until we can connect to it via SSH. |
333 | -func waitSSH(ctx *BootstrapContext, inst dnsNamer, t *tomb.Tomb, timeout SSHTimeoutOpts) (dnsName string, err error) { |
334 | + Timeout: 10 * time.Minute, |
335 | + |
336 | + ConnectDelay: 5 * time.Second, |
337 | + |
338 | + // Not too frequent, as we refresh addresses from the provider each time. |
339 | + AddressesDelay: 10 * time.Second, |
340 | + } |
341 | +} |
342 | + |
343 | +type addresser interface { |
344 | + // Refresh refreshes the addresses for the instance. |
345 | + Refresh() error |
346 | + |
347 | + // Addresses returns the addresses for the instance. |
348 | + // To ensure that the results are up to date, call |
349 | + // Refresh first. |
350 | + Addresses() ([]instance.Address, error) |
351 | +} |
352 | + |
353 | +type hostChecker struct { |
354 | + addr instance.Address |
355 | + |
356 | + // checkDelay is the amount of time to wait between retries. |
357 | + checkDelay time.Duration |
358 | + |
359 | + // checkHostScript is executed on the host via SSH. |
360 | + // hostChecker.loop will return once the script |
361 | + // runs without error. |
362 | + checkHostScript string |
363 | + |
364 | + // closed is closed to indicate that the host checker should |
365 | + // return, without waiting for the result of any ongoing |
366 | + // attempts. |
367 | + closed <-chan struct{} |
368 | +} |
369 | + |
370 | +// Close implements io.Closer, as required by parallel.Try. |
371 | +func (*hostChecker) Close() error { |
372 | + return nil |
373 | +} |
374 | + |
375 | +func (hc *hostChecker) loop(dying <-chan struct{}) (io.Closer, error) { |
376 | + // The value of connectSSH is taken outside the goroutine that may outlive |
377 | + // hostChecker.loop, or we evoke the wrath of the race detector. |
378 | + connectSSH := connectSSH |
379 | + done := make(chan error, 1) |
380 | + var lastErr error |
381 | + for { |
382 | + go func() { |
383 | + done <- connectSSH(hc.addr.Value, hc.checkHostScript) |
384 | + }() |
385 | + select { |
386 | + case <-hc.closed: |
387 | + return hc, lastErr |
388 | + case <-dying: |
389 | + return hc, lastErr |
390 | + case lastErr = <-done: |
391 | + if lastErr == nil { |
392 | + return hc, nil |
393 | + } |
394 | + } |
395 | + select { |
396 | + case <-hc.closed: |
397 | + case <-dying: |
398 | + case <-time.After(hc.checkDelay): |
399 | + } |
400 | + } |
401 | +} |
402 | + |
403 | +type parallelHostChecker struct { |
404 | + *parallel.Try |
405 | + |
406 | + stderr io.Writer |
407 | + |
408 | + // active is a map of adresses to channels for addresses actively |
409 | + // being tested. The goroutine testing the address will continue |
410 | + // to attempt connecting to the address until it succeeds, the Try |
411 | + // is killed, or the corresponding channel in this map is closed. |
412 | + active map[instance.Address]chan struct{} |
413 | + |
414 | + // checkDelay is how long each hostChecker waits between attempts. |
415 | + checkDelay time.Duration |
416 | + |
417 | + // checkHostScript is the script to run on each host to check that |
418 | + // it is the host we expect. |
419 | + checkHostScript string |
420 | +} |
421 | + |
422 | +func (p *parallelHostChecker) UpdateAddresses(addrs []instance.Address) { |
423 | + for _, addr := range addrs { |
424 | + if _, ok := p.active[addr]; ok { |
425 | + continue |
426 | + } |
427 | + fmt.Fprintf(p.stderr, "Attempting to connect to %s:22\n", addr.Value) |
428 | + closed := make(chan struct{}) |
429 | + hc := &hostChecker{ |
430 | + addr: addr, |
431 | + checkDelay: p.checkDelay, |
432 | + checkHostScript: p.checkHostScript, |
433 | + closed: closed, |
434 | + } |
435 | + p.active[addr] = closed |
436 | + p.Start(hc.loop) |
437 | + } |
438 | +} |
439 | + |
440 | +// Close prevents additional functions from being added to |
441 | +// the Try, and tells each active hostChecker to exit. |
442 | +func (p *parallelHostChecker) Close() error { |
443 | + // We signal each checker to stop and wait for them |
444 | + // each to complete; this allows us to get the error, |
445 | + // as opposed to when using try.Kill which does not |
446 | + // wait for the functions to complete. |
447 | + p.Try.Close() |
448 | + for _, ch := range p.active { |
449 | + close(ch) |
450 | + } |
451 | + return nil |
452 | +} |
453 | + |
454 | +// connectSSH is called to connect to the specified host and |
455 | +// execute the "checkHostScript" bash script on it. |
456 | +var connectSSH = func(host, checkHostScript string) error { |
457 | + cmd := ssh.Command("ubuntu@"+host, []string{"/bin/bash", "-c", utils.ShQuote(checkHostScript)}) |
458 | + output, err := cmd.CombinedOutput() |
459 | + if err != nil && len(output) > 0 { |
460 | + err = fmt.Errorf("%s", strings.TrimSpace(string(output))) |
461 | + } |
462 | + return err |
463 | +} |
464 | + |
465 | +// waitSSH waits for the instance to be assigned a routable |
466 | +// address, then waits until we can connect to it via SSH. |
467 | +// |
468 | +// waitSSH attempts on all addresses returned by the instance |
469 | +// in parallel; the first succeeding one wins. We ensure that |
470 | +// private addresses are for the correct machine by checking |
471 | +// the presence of a file on the machine that contains the |
472 | +// machine's nonce. The "checkHostScript" is a bash script |
473 | +// that performs this file check. |
474 | +func waitSSH(ctx *BootstrapContext, checkHostScript string, inst addresser, t *tomb.Tomb, timeout SSHTimeoutOpts) (addr string, err error) { |
475 | defer t.Done() |
476 | globalTimeout := time.After(timeout.Timeout) |
477 | - pollDNS := time.NewTimer(0) |
478 | - fmt.Fprintln(ctx.Stderr, "Waiting for DNS name") |
479 | - var dialResultChan chan error |
480 | - var lastErr error |
481 | + pollAddresses := time.NewTimer(0) |
482 | + |
483 | + // checker checks each address in a loop, in parallel, |
484 | + // until one succeeds, the global timeout is reached, |
485 | + // or the tomb is killed. |
486 | + checker := parallelHostChecker{ |
487 | + Try: parallel.NewTry(0, nil), |
488 | + stderr: ctx.Stderr, |
489 | + active: make(map[instance.Address]chan struct{}), |
490 | + checkDelay: timeout.ConnectDelay, |
491 | + checkHostScript: checkHostScript, |
492 | + } |
493 | + defer checker.Kill() |
494 | + |
495 | + fmt.Fprintln(ctx.Stderr, "Waiting for address") |
496 | for { |
497 | - if dnsName != "" && dialResultChan == nil { |
498 | - addr := dnsName + ":22" |
499 | - fmt.Fprintf(ctx.Stderr, "Attempting to connect to %s\n", addr) |
500 | - dialResultChan = make(chan error, 1) |
501 | - go func() { |
502 | - c, err := net.Dial("tcp", addr) |
503 | - if err == nil { |
504 | - c.Close() |
505 | - } |
506 | - dialResultChan <- err |
507 | - }() |
508 | - } |
509 | select { |
510 | - case <-pollDNS.C: |
511 | - pollDNS.Reset(timeout.DNSNameDelay) |
512 | - newDNSName, err := inst.DNSName() |
513 | - if err != nil && err != instance.ErrNoDNSName { |
514 | - return "", t.Killf("getting DNS name: %v", err) |
515 | - } else if err != nil { |
516 | - lastErr = err |
517 | - } else if newDNSName != dnsName { |
518 | - dnsName = newDNSName |
519 | - dialResultChan = nil |
520 | - } |
521 | - case lastErr = <-dialResultChan: |
522 | - if lastErr == nil { |
523 | - return dnsName, nil |
524 | - } |
525 | - logger.Debugf("connection failed: %v", lastErr) |
526 | - dialResultChan = nil // retry |
527 | + case <-pollAddresses.C: |
528 | + pollAddresses.Reset(timeout.AddressesDelay) |
529 | + if err := inst.Refresh(); err != nil { |
530 | + return "", t.Killf("refreshing addresses: %v", err) |
531 | + } |
532 | + addresses, err := inst.Addresses() |
533 | + if err != nil { |
534 | + return "", t.Killf("getting addresses: %v", err) |
535 | + } |
536 | + checker.UpdateAddresses(addresses) |
537 | case <-globalTimeout: |
538 | + checker.Close() |
539 | + lastErr := checker.Wait() |
540 | format := "waited for %v " |
541 | args := []interface{}{timeout.Timeout} |
542 | - if dnsName == "" { |
543 | - format += "without getting a DNS name" |
544 | + if len(checker.active) == 0 { |
545 | + format += "without getting any addresses" |
546 | } else { |
547 | - format += "without being able to connect to %q" |
548 | - args = append(args, dnsName) |
549 | + format += "without being able to connect" |
550 | } |
551 | - if lastErr != nil { |
552 | + if lastErr != nil && lastErr != parallel.ErrStopped { |
553 | format += ": %v" |
554 | args = append(args, lastErr) |
555 | } |
556 | - return "", t.Killf(format, args...) |
557 | + return "", fmt.Errorf(format, args...) |
558 | case <-t.Dying(): |
559 | return "", t.Err() |
560 | + case <-checker.Dead(): |
561 | + result, err := checker.Result() |
562 | + if err != nil { |
563 | + return "", err |
564 | + } |
565 | + return result.(*hostChecker).addr.Value, nil |
566 | } |
567 | } |
568 | } |
569 | |
570 | === modified file 'provider/common/bootstrap_test.go' |
571 | --- provider/common/bootstrap_test.go 2013-12-13 02:19:17 +0000 |
572 | +++ provider/common/bootstrap_test.go 2013-12-18 15:16:49 +0000 |
573 | @@ -41,6 +41,9 @@ |
574 | func (s *BootstrapSuite) SetUpTest(c *gc.C) { |
575 | s.LoggingSuite.SetUpTest(c) |
576 | s.ToolsFixture.SetUpTest(c) |
577 | + s.PatchValue(common.ConnectSSH, func(host, checkHostScript string) error { |
578 | + return fmt.Errorf("mock connection failure to %s", host) |
579 | + }) |
580 | } |
581 | |
582 | func (s *BootstrapSuite) TearDownTest(c *gc.C) { |
583 | @@ -219,47 +222,57 @@ |
584 | }) |
585 | } |
586 | |
587 | -type neverDNSName struct { |
588 | -} |
589 | - |
590 | -func (neverDNSName) DNSName() (string, error) { |
591 | - return "", instance.ErrNoDNSName |
592 | +type neverRefreshes struct { |
593 | +} |
594 | + |
595 | +func (neverRefreshes) Refresh() error { |
596 | + return nil |
597 | +} |
598 | + |
599 | +type neverAddresses struct { |
600 | + neverRefreshes |
601 | +} |
602 | + |
603 | +func (neverAddresses) Addresses() ([]instance.Address, error) { |
604 | + return nil, nil |
605 | } |
606 | |
607 | var testSSHTimeout = common.SSHTimeoutOpts{ |
608 | - Timeout: 10 * time.Millisecond, |
609 | - DNSNameDelay: 1 * time.Millisecond, |
610 | + Timeout: 10 * time.Millisecond, |
611 | + ConnectDelay: 1 * time.Millisecond, |
612 | + AddressesDelay: 1 * time.Millisecond, |
613 | } |
614 | |
615 | -func (s *BootstrapSuite) TestWaitSSHTimesOutWaitingForDNSName(c *gc.C) { |
616 | +func (s *BootstrapSuite) TestWaitSSHTimesOutWaitingForAddresses(c *gc.C) { |
617 | ctx := &common.BootstrapContext{} |
618 | buf := &bytes.Buffer{} |
619 | ctx.Stderr = buf |
620 | var t tomb.Tomb |
621 | - _, err := common.WaitSSH(ctx, neverDNSName{}, &t, testSSHTimeout) |
622 | - c.Check(err, gc.ErrorMatches, "waited for 10ms without getting a DNS name: DNS name not allocated") |
623 | - c.Check(buf.String(), gc.Matches, "Waiting for DNS name\n") |
624 | + _, err := common.WaitSSH(ctx, "/bin/true", neverAddresses{}, &t, testSSHTimeout) |
625 | + c.Check(err, gc.ErrorMatches, "waited for 10ms without getting any addresses") |
626 | + c.Check(buf.String(), gc.Matches, "Waiting for address\n") |
627 | } |
628 | |
629 | -func (s *BootstrapSuite) TestWaitSSHKilledWaitingForDNSName(c *gc.C) { |
630 | +func (s *BootstrapSuite) TestWaitSSHKilledWaitingForAddresses(c *gc.C) { |
631 | ctx := &common.BootstrapContext{} |
632 | buf := &bytes.Buffer{} |
633 | ctx.Stderr = buf |
634 | var t tomb.Tomb |
635 | go func() { |
636 | <-time.After(2 * time.Millisecond) |
637 | - t.Killf("stopping WaitSSH during DNSName") |
638 | + t.Killf("stopping WaitSSH during Addresses") |
639 | }() |
640 | - _, err := common.WaitSSH(ctx, neverDNSName{}, &t, testSSHTimeout) |
641 | - c.Check(err, gc.ErrorMatches, "stopping WaitSSH during DNSName") |
642 | - c.Check(buf.String(), gc.Matches, "Waiting for DNS name\n") |
643 | -} |
644 | - |
645 | -type brokenDNSName struct { |
646 | -} |
647 | - |
648 | -func (brokenDNSName) DNSName() (string, error) { |
649 | - return "", fmt.Errorf("DNSName will never work") |
650 | + _, err := common.WaitSSH(ctx, "/bin/true", neverAddresses{}, &t, testSSHTimeout) |
651 | + c.Check(err, gc.ErrorMatches, "stopping WaitSSH during Addresses") |
652 | + c.Check(buf.String(), gc.Matches, "Waiting for address\n") |
653 | +} |
654 | + |
655 | +type brokenAddresses struct { |
656 | + neverRefreshes |
657 | +} |
658 | + |
659 | +func (brokenAddresses) Addresses() ([]instance.Address, error) { |
660 | + return nil, fmt.Errorf("Addresses will never work") |
661 | } |
662 | |
663 | func (s *BootstrapSuite) TestWaitSSHStopsOnBadError(c *gc.C) { |
664 | @@ -267,17 +280,18 @@ |
665 | buf := &bytes.Buffer{} |
666 | ctx.Stderr = buf |
667 | var t tomb.Tomb |
668 | - _, err := common.WaitSSH(ctx, brokenDNSName{}, &t, testSSHTimeout) |
669 | - c.Check(err, gc.ErrorMatches, "getting DNS name: DNSName will never work") |
670 | - c.Check(buf.String(), gc.Equals, "Waiting for DNS name\n") |
671 | + _, err := common.WaitSSH(ctx, "/bin/true", brokenAddresses{}, &t, testSSHTimeout) |
672 | + c.Check(err, gc.ErrorMatches, "getting addresses: Addresses will never work") |
673 | + c.Check(buf.String(), gc.Equals, "Waiting for address\n") |
674 | } |
675 | |
676 | type neverOpensPort struct { |
677 | - name string |
678 | + neverRefreshes |
679 | + addr string |
680 | } |
681 | |
682 | -func (n *neverOpensPort) DNSName() (string, error) { |
683 | - return n.name, nil |
684 | +func (n *neverOpensPort) Addresses() ([]instance.Address, error) { |
685 | + return []instance.Address{instance.NewAddress(n.addr)}, nil |
686 | } |
687 | |
688 | func (s *BootstrapSuite) TestWaitSSHTimesOutWaitingForDial(c *gc.C) { |
689 | @@ -286,28 +300,29 @@ |
690 | ctx.Stderr = buf |
691 | var t tomb.Tomb |
692 | // 0.x.y.z addresses are always invalid |
693 | - _, err := common.WaitSSH(ctx, &neverOpensPort{"0.1.2.3"}, &t, testSSHTimeout) |
694 | + _, err := common.WaitSSH(ctx, "/bin/true", &neverOpensPort{addr: "0.1.2.3"}, &t, testSSHTimeout) |
695 | c.Check(err, gc.ErrorMatches, |
696 | - `waited for 10ms without being able to connect to "0.1.2.3": dial tcp 0.1.2.3:22: invalid argument`) |
697 | + `waited for 10ms without being able to connect: mock connection failure to 0.1.2.3`) |
698 | c.Check(buf.String(), gc.Matches, |
699 | - "Waiting for DNS name\n"+ |
700 | + "Waiting for address\n"+ |
701 | "(Attempting to connect to 0.1.2.3:22\n)+") |
702 | } |
703 | |
704 | type killOnDial struct { |
705 | + neverRefreshes |
706 | name string |
707 | tomb *tomb.Tomb |
708 | returned bool |
709 | } |
710 | |
711 | -func (k *killOnDial) DNSName() (string, error) { |
712 | - // kill the tomb the second time DNSName is called |
713 | +func (k *killOnDial) Addresses() ([]instance.Address, error) { |
714 | + // kill the tomb the second time Addresses is called |
715 | if !k.returned { |
716 | k.returned = true |
717 | } else { |
718 | k.tomb.Killf("stopping WaitSSH during Dial") |
719 | } |
720 | - return k.name, nil |
721 | + return []instance.Address{instance.NewAddress(k.name)}, nil |
722 | } |
723 | |
724 | func (s *BootstrapSuite) TestWaitSSHKilledWaitingForDial(c *gc.C) { |
725 | @@ -317,10 +332,53 @@ |
726 | var t tomb.Tomb |
727 | timeout := testSSHTimeout |
728 | timeout.Timeout = 1 * time.Minute |
729 | - _, err := common.WaitSSH(ctx, &killOnDial{name: "0.1.2.3", tomb: &t}, &t, timeout) |
730 | + _, err := common.WaitSSH(ctx, "", &killOnDial{name: "0.1.2.3", tomb: &t}, &t, timeout) |
731 | c.Check(err, gc.ErrorMatches, "stopping WaitSSH during Dial") |
732 | // Exact timing is imprecise but it should have tried a few times before being killed |
733 | c.Check(buf.String(), gc.Matches, |
734 | - "Waiting for DNS name\n"+ |
735 | + "Waiting for address\n"+ |
736 | "(Attempting to connect to 0.1.2.3:22\n)+") |
737 | } |
738 | + |
739 | +type addressesChange struct { |
740 | + addrs [][]string |
741 | +} |
742 | + |
743 | +func (ac *addressesChange) Refresh() error { |
744 | + if len(ac.addrs) > 1 { |
745 | + ac.addrs = ac.addrs[1:] |
746 | + } |
747 | + return nil |
748 | +} |
749 | + |
750 | +func (ac *addressesChange) Addresses() ([]instance.Address, error) { |
751 | + var addrs []instance.Address |
752 | + for _, addr := range ac.addrs[0] { |
753 | + addrs = append(addrs, instance.NewAddress(addr)) |
754 | + } |
755 | + return addrs, nil |
756 | +} |
757 | + |
758 | +func (s *BootstrapSuite) TestWaitSSHRefreshAddresses(c *gc.C) { |
759 | + ctx := &common.BootstrapContext{} |
760 | + buf := &bytes.Buffer{} |
761 | + ctx.Stderr = buf |
762 | + var t tomb.Tomb |
763 | + _, err := common.WaitSSH(ctx, "", &addressesChange{addrs: [][]string{ |
764 | + nil, |
765 | + nil, |
766 | + []string{"0.1.2.3"}, |
767 | + []string{"0.1.2.3"}, |
768 | + nil, |
769 | + []string{"0.1.2.4"}, |
770 | + }}, &t, testSSHTimeout) |
771 | + // Not necessarily the last one in the list, due to scheduling. |
772 | + c.Check(err, gc.ErrorMatches, |
773 | + `waited for 10ms without being able to connect: mock connection failure to 0.1.2.[34]`) |
774 | + c.Check(buf.String(), gc.Matches, |
775 | + "Waiting for address\n"+ |
776 | + "(.|\n)*(Attempting to connect to 0.1.2.3:22\n)+(.|\n)*") |
777 | + c.Check(buf.String(), gc.Matches, |
778 | + "Waiting for address\n"+ |
779 | + "(.|\n)*(Attempting to connect to 0.1.2.4:22\n)+(.|\n)*") |
780 | +} |
781 | |
782 | === modified file 'provider/common/export_test.go' |
783 | --- provider/common/export_test.go 2013-12-04 10:28:12 +0000 |
784 | +++ provider/common/export_test.go 2013-12-18 15:16:49 +0000 |
785 | @@ -7,5 +7,6 @@ |
786 | GetDNSNames = getDNSNames |
787 | GetStateInfo = getStateInfo |
788 | ComposeAddresses = composeAddresses |
789 | + ConnectSSH = &connectSSH |
790 | WaitSSH = waitSSH |
791 | ) |
792 | |
793 | === modified file 'provider/dummy/environs.go' |
794 | --- provider/dummy/environs.go 2013-12-11 09:14:12 +0000 |
795 | +++ provider/dummy/environs.go 2013-12-18 15:16:49 +0000 |
796 | @@ -900,6 +900,10 @@ |
797 | return string(inst.id) + ".dns", nil |
798 | } |
799 | |
800 | +func (*dummyInstance) Refresh() error { |
801 | + return nil |
802 | +} |
803 | + |
804 | func (inst *dummyInstance) Addresses() ([]instance.Address, error) { |
805 | inst.mu.Lock() |
806 | defer inst.mu.Unlock() |
807 | |
808 | === modified file 'provider/ec2/ec2.go' |
809 | --- provider/ec2/ec2.go 2013-11-18 05:41:36 +0000 |
810 | +++ provider/ec2/ec2.go 2013-12-18 15:16:49 +0000 |
811 | @@ -67,64 +67,84 @@ |
812 | |
813 | type ec2Instance struct { |
814 | e *environ |
815 | + |
816 | + mu sync.Mutex |
817 | *ec2.Instance |
818 | } |
819 | |
820 | func (inst *ec2Instance) String() string { |
821 | - return inst.InstanceId |
822 | + return string(inst.Id()) |
823 | } |
824 | |
825 | var _ instance.Instance = (*ec2Instance)(nil) |
826 | |
827 | +func (inst *ec2Instance) getInstance() *ec2.Instance { |
828 | + inst.mu.Lock() |
829 | + defer inst.mu.Unlock() |
830 | + return inst.Instance |
831 | +} |
832 | + |
833 | func (inst *ec2Instance) Id() instance.Id { |
834 | - return instance.Id(inst.InstanceId) |
835 | + return instance.Id(inst.getInstance().InstanceId) |
836 | } |
837 | |
838 | func (inst *ec2Instance) Status() string { |
839 | - return inst.State.Name |
840 | -} |
841 | - |
842 | -// refreshInstance requeries the Instance details over the ec2 api |
843 | -func (inst *ec2Instance) refreshInstance() error { |
844 | - insts, err := inst.e.Instances([]instance.Id{inst.Id()}) |
845 | + return inst.getInstance().State.Name |
846 | +} |
847 | + |
848 | +// Refresh implements instance.Refresh(), requerying the |
849 | +// Instance details over the ec2 api |
850 | +func (inst *ec2Instance) Refresh() error { |
851 | + _, err := inst.refresh() |
852 | + return err |
853 | +} |
854 | + |
855 | +// refresh requeries Instance details over the ec2 api. |
856 | +func (inst *ec2Instance) refresh() (*ec2.Instance, error) { |
857 | + id := inst.Id() |
858 | + insts, err := inst.e.Instances([]instance.Id{id}) |
859 | if err != nil { |
860 | - return err |
861 | + return nil, err |
862 | } |
863 | + inst.mu.Lock() |
864 | + defer inst.mu.Unlock() |
865 | inst.Instance = insts[0].(*ec2Instance).Instance |
866 | - return nil |
867 | + return inst.Instance, nil |
868 | } |
869 | |
870 | // Addresses implements instance.Addresses() returning generic address |
871 | // details for the instance, and requerying the ec2 api if required. |
872 | func (inst *ec2Instance) Addresses() ([]instance.Address, error) { |
873 | - var addresses []instance.Address |
874 | // TODO(gz): Stop relying on this requerying logic, maybe remove error |
875 | - if inst.Instance.DNSName == "" { |
876 | + instInstance := inst.getInstance() |
877 | + if instInstance.DNSName == "" { |
878 | // Fetch the instance information again, in case |
879 | // the DNS information has become available. |
880 | - err := inst.refreshInstance() |
881 | + var err error |
882 | + instInstance, err = inst.refresh() |
883 | if err != nil { |
884 | return nil, err |
885 | } |
886 | } |
887 | + var addresses []instance.Address |
888 | possibleAddresses := []instance.Address{ |
889 | { |
890 | - Value: inst.Instance.DNSName, |
891 | + Value: instInstance.DNSName, |
892 | Type: instance.HostName, |
893 | NetworkScope: instance.NetworkPublic, |
894 | }, |
895 | { |
896 | - Value: inst.Instance.PrivateDNSName, |
897 | + Value: instInstance.PrivateDNSName, |
898 | Type: instance.HostName, |
899 | NetworkScope: instance.NetworkCloudLocal, |
900 | }, |
901 | { |
902 | - Value: inst.Instance.IPAddress, |
903 | + Value: instInstance.IPAddress, |
904 | Type: instance.Ipv4Address, |
905 | NetworkScope: instance.NetworkPublic, |
906 | }, |
907 | { |
908 | - Value: inst.Instance.PrivateIPAddress, |
909 | + Value: instInstance.PrivateIPAddress, |
910 | Type: instance.Ipv4Address, |
911 | NetworkScope: instance.NetworkCloudLocal, |
912 | }, |
913 | |
914 | === modified file 'provider/ec2/local_test.go' |
915 | --- provider/ec2/local_test.go 2013-12-03 05:20:25 +0000 |
916 | +++ provider/ec2/local_test.go 2013-12-18 15:16:49 +0000 |
917 | @@ -241,6 +241,10 @@ |
918 | "all": "| tee -a /var/log/cloud-init-output.log", |
919 | }, |
920 | "ssh_authorized_keys": []interface{}{"my-keys"}, |
921 | + "runcmd": []interface{}{ |
922 | + "install -D -m 644 /dev/null '/var/lib/juju/nonce.txt'", |
923 | + "printf '%s\\n' 'user-admin:bootstrap' > '/var/lib/juju/nonce.txt'", |
924 | + }, |
925 | }) |
926 | |
927 | // check that a new instance will be started with a machine agent |
928 | |
929 | === modified file 'provider/joyent/instance.go' |
930 | --- provider/joyent/instance.go 2013-10-07 10:57:04 +0000 |
931 | +++ provider/joyent/instance.go 2013-12-18 15:16:49 +0000 |
932 | @@ -24,6 +24,10 @@ |
933 | return "unknown (not implemented)" |
934 | } |
935 | |
936 | +func (inst *environInstance) Refresh() error { |
937 | + return nil |
938 | +} |
939 | + |
940 | func (inst *environInstance) Addresses() ([]instance.Address, error) { |
941 | _ = inst.env.getSnapshot() |
942 | return nil, errNotImplemented |
943 | |
944 | === modified file 'provider/local/instance.go' |
945 | --- provider/local/instance.go 2013-10-03 12:08:15 +0000 |
946 | +++ provider/local/instance.go 2013-12-18 15:16:49 +0000 |
947 | @@ -28,6 +28,10 @@ |
948 | return "" |
949 | } |
950 | |
951 | +func (*localInstance) Refresh() error { |
952 | + return nil |
953 | +} |
954 | + |
955 | func (inst *localInstance) Addresses() ([]instance.Address, error) { |
956 | return nil, errors.NewNotImplementedError("localInstance.Addresses") |
957 | } |
958 | |
959 | === modified file 'provider/maas/environ.go' |
960 | --- provider/maas/environ.go 2013-11-26 12:24:48 +0000 |
961 | +++ provider/maas/environ.go 2013-12-18 15:16:49 +0000 |
962 | @@ -238,7 +238,7 @@ |
963 | if node, tools, err := environ.acquireNode(cons, possibleTools); err != nil { |
964 | return nil, nil, fmt.Errorf("cannot run instances: %v", err) |
965 | } else { |
966 | - inst = &maasInstance{&node, environ} |
967 | + inst = &maasInstance{maasObject: &node, environ: environ} |
968 | machineConfig.Tools = tools |
969 | } |
970 | defer func() { |
971 | |
972 | === modified file 'provider/maas/environ_whitebox_test.go' |
973 | --- provider/maas/environ_whitebox_test.go 2013-12-10 04:25:06 +0000 |
974 | +++ provider/maas/environ_whitebox_test.go 2013-12-18 15:16:49 +0000 |
975 | @@ -285,7 +285,7 @@ |
976 | func (suite *environSuite) getInstance(systemId string) *maasInstance { |
977 | input := `{"system_id": "` + systemId + `"}` |
978 | node := suite.testMAASObject.TestServer.NewNode(input) |
979 | - return &maasInstance{&node, suite.makeEnviron()} |
980 | + return &maasInstance{maasObject: &node, environ: suite.makeEnviron()} |
981 | } |
982 | |
983 | func (suite *environSuite) TestStopInstancesReturnsIfParameterEmpty(c *gc.C) { |
984 | @@ -316,7 +316,7 @@ |
985 | hostname := "test" |
986 | input := `{"system_id": "system_id", "hostname": "` + hostname + `"}` |
987 | node := suite.testMAASObject.TestServer.NewNode(input) |
988 | - testInstance := &maasInstance{&node, suite.makeEnviron()} |
989 | + testInstance := &maasInstance{maasObject: &node, environ: suite.makeEnviron()} |
990 | err := bootstrap.SaveState( |
991 | env.Storage(), |
992 | &bootstrap.BootstrapState{StateInstances: []instance.Id{testInstance.Id()}}) |
993 | |
994 | === modified file 'provider/maas/instance.go' |
995 | --- provider/maas/instance.go 2013-10-17 18:36:49 +0000 |
996 | +++ provider/maas/instance.go 2013-12-18 15:16:49 +0000 |
997 | @@ -5,6 +5,7 @@ |
998 | |
999 | import ( |
1000 | "fmt" |
1001 | + "sync" |
1002 | |
1003 | "launchpad.net/gomaasapi" |
1004 | |
1005 | @@ -13,8 +14,10 @@ |
1006 | ) |
1007 | |
1008 | type maasInstance struct { |
1009 | + environ *maasEnviron |
1010 | + |
1011 | + mu sync.Mutex |
1012 | maasObject *gomaasapi.MAASObject |
1013 | - environ *maasEnviron |
1014 | } |
1015 | |
1016 | var _ instance.Instance = (*maasInstance)(nil) |
1017 | @@ -29,8 +32,12 @@ |
1018 | } |
1019 | |
1020 | func (mi *maasInstance) Id() instance.Id { |
1021 | + return maasObjectId(mi.getMaasObject()) |
1022 | +} |
1023 | + |
1024 | +func maasObjectId(maasObject *gomaasapi.MAASObject) instance.Id { |
1025 | // Use the node's 'resource_uri' value. |
1026 | - return instance.Id(mi.maasObject.URI().String()) |
1027 | + return instance.Id(maasObject.URI().String()) |
1028 | } |
1029 | |
1030 | func (mi *maasInstance) Status() string { |
1031 | @@ -42,10 +49,12 @@ |
1032 | return "" |
1033 | } |
1034 | |
1035 | -// refreshInstance refreshes the instance with the most up-to-date information |
1036 | +// Refresh refreshes the instance with the most up-to-date information |
1037 | // from the MAAS server. |
1038 | -func (mi *maasInstance) refreshInstance() error { |
1039 | - insts, err := mi.environ.Instances([]instance.Id{mi.Id()}) |
1040 | +func (mi *maasInstance) Refresh() error { |
1041 | + mi.mu.Lock() |
1042 | + defer mi.mu.Unlock() |
1043 | + insts, err := mi.environ.Instances([]instance.Id{maasObjectId(mi.maasObject)}) |
1044 | if err != nil { |
1045 | return err |
1046 | } |
1047 | @@ -53,6 +62,12 @@ |
1048 | return nil |
1049 | } |
1050 | |
1051 | +func (mi *maasInstance) getMaasObject() *gomaasapi.MAASObject { |
1052 | + mi.mu.Lock() |
1053 | + defer mi.mu.Unlock() |
1054 | + return mi.maasObject |
1055 | +} |
1056 | + |
1057 | func (mi *maasInstance) Addresses() ([]instance.Address, error) { |
1058 | name, err := mi.DNSName() |
1059 | if err != nil { |
1060 | @@ -76,7 +91,7 @@ |
1061 | |
1062 | func (mi *maasInstance) ipAddresses() ([]string, error) { |
1063 | // we have to do this the hard way, since maasObject doesn't have this built-in yet |
1064 | - addressArray := mi.maasObject.GetMap()["ip_addresses"] |
1065 | + addressArray := mi.getMaasObject().GetMap()["ip_addresses"] |
1066 | if addressArray.IsNil() { |
1067 | // Older MAAS versions do not return ip_addresses. |
1068 | return nil, nil |
1069 | @@ -98,7 +113,7 @@ |
1070 | |
1071 | func (mi *maasInstance) DNSName() (string, error) { |
1072 | // A MAAS instance has its DNS name immediately. |
1073 | - return mi.maasObject.GetField("hostname") |
1074 | + return mi.getMaasObject().GetField("hostname") |
1075 | } |
1076 | |
1077 | func (mi *maasInstance) WaitDNSName() (string, error) { |
1078 | |
1079 | === modified file 'provider/maas/instance_test.go' |
1080 | --- provider/maas/instance_test.go 2013-11-26 12:24:48 +0000 |
1081 | +++ provider/maas/instance_test.go 2013-12-18 15:16:49 +0000 |
1082 | @@ -21,7 +21,7 @@ |
1083 | jsonValue := `{"system_id": "system_id", "test": "test"}` |
1084 | obj := s.testMAASObject.TestServer.NewNode(jsonValue) |
1085 | resourceURI, _ := obj.GetField("resource_uri") |
1086 | - instance := maasInstance{&obj, s.makeEnviron()} |
1087 | + instance := maasInstance{maasObject: &obj, environ: s.makeEnviron()} |
1088 | |
1089 | c.Check(string(instance.Id()), gc.Equals, resourceURI) |
1090 | } |
1091 | @@ -29,7 +29,7 @@ |
1092 | func (s *instanceTest) TestString(c *gc.C) { |
1093 | jsonValue := `{"hostname": "thethingintheplace", "system_id": "system_id", "test": "test"}` |
1094 | obj := s.testMAASObject.TestServer.NewNode(jsonValue) |
1095 | - instance := &maasInstance{&obj, s.makeEnviron()} |
1096 | + instance := &maasInstance{maasObject: &obj, environ: s.makeEnviron()} |
1097 | hostname, err := instance.DNSName() |
1098 | c.Assert(err, gc.IsNil) |
1099 | expected := hostname + ":" + string(instance.Id()) |
1100 | @@ -40,7 +40,7 @@ |
1101 | // For good measure, test what happens if we don't have a hostname. |
1102 | jsonValue := `{"system_id": "system_id", "test": "test"}` |
1103 | obj := s.testMAASObject.TestServer.NewNode(jsonValue) |
1104 | - instance := &maasInstance{&obj, s.makeEnviron()} |
1105 | + instance := &maasInstance{maasObject: &obj, environ: s.makeEnviron()} |
1106 | _, err := instance.DNSName() |
1107 | c.Assert(err, gc.NotNil) |
1108 | expected := fmt.Sprintf("<DNSName failed: %q>", err) + ":" + string(instance.Id()) |
1109 | @@ -51,9 +51,9 @@ |
1110 | jsonValue := `{"system_id": "system_id", "test": "test"}` |
1111 | obj := s.testMAASObject.TestServer.NewNode(jsonValue) |
1112 | s.testMAASObject.TestServer.ChangeNode("system_id", "test2", "test2") |
1113 | - instance := maasInstance{&obj, s.makeEnviron()} |
1114 | + instance := maasInstance{maasObject: &obj, environ: s.makeEnviron()} |
1115 | |
1116 | - err := instance.refreshInstance() |
1117 | + err := instance.Refresh() |
1118 | |
1119 | c.Check(err, gc.IsNil) |
1120 | testField, err := (*instance.maasObject).GetField("test2") |
1121 | @@ -64,7 +64,7 @@ |
1122 | func (s *instanceTest) TestDNSName(c *gc.C) { |
1123 | jsonValue := `{"hostname": "DNS name", "system_id": "system_id"}` |
1124 | obj := s.testMAASObject.TestServer.NewNode(jsonValue) |
1125 | - instance := maasInstance{&obj, s.makeEnviron()} |
1126 | + instance := maasInstance{maasObject: &obj, environ: s.makeEnviron()} |
1127 | |
1128 | dnsName, err := instance.DNSName() |
1129 | |
1130 | @@ -85,7 +85,7 @@ |
1131 | "ip_addresses": [ "1.2.3.4", "fe80::d806:dbff:fe23:1199" ] |
1132 | }` |
1133 | obj := s.testMAASObject.TestServer.NewNode(jsonValue) |
1134 | - inst := maasInstance{&obj, s.makeEnviron()} |
1135 | + inst := maasInstance{maasObject: &obj, environ: s.makeEnviron()} |
1136 | |
1137 | expected := []instance.Address{ |
1138 | {Value: "testing.invalid", Type: instance.HostName, NetworkScope: instance.NetworkPublic}, |
1139 | @@ -107,7 +107,7 @@ |
1140 | "system_id": "system_id" |
1141 | }` |
1142 | obj := s.testMAASObject.TestServer.NewNode(jsonValue) |
1143 | - inst := maasInstance{&obj, s.makeEnviron()} |
1144 | + inst := maasInstance{maasObject: &obj, environ: s.makeEnviron()} |
1145 | |
1146 | addr, err := inst.Addresses() |
1147 | c.Assert(err, gc.IsNil) |
1148 | @@ -123,7 +123,7 @@ |
1149 | "ip_addresses": "incompatible" |
1150 | }` |
1151 | obj := s.testMAASObject.TestServer.NewNode(jsonValue) |
1152 | - inst := maasInstance{&obj, s.makeEnviron()} |
1153 | + inst := maasInstance{maasObject: &obj, environ: s.makeEnviron()} |
1154 | |
1155 | _, err := inst.Addresses() |
1156 | c.Assert(err, gc.NotNil) |
1157 | @@ -136,7 +136,7 @@ |
1158 | "ip_addresses": [42] |
1159 | }` |
1160 | obj := s.testMAASObject.TestServer.NewNode(jsonValue) |
1161 | - inst := maasInstance{&obj, s.makeEnviron()} |
1162 | + inst := maasInstance{maasObject: &obj, environ: s.makeEnviron()} |
1163 | |
1164 | _, err := inst.Addresses() |
1165 | c.Assert(err, gc.NotNil) |
1166 | |
1167 | === modified file 'provider/null/instance.go' |
1168 | --- provider/null/instance.go 2013-11-26 14:52:02 +0000 |
1169 | +++ provider/null/instance.go 2013-12-18 15:16:49 +0000 |
1170 | @@ -12,15 +12,19 @@ |
1171 | host string |
1172 | } |
1173 | |
1174 | -func (_ nullBootstrapInstance) Id() instance.Id { |
1175 | +func (nullBootstrapInstance) Id() instance.Id { |
1176 | // The only way to bootrap is via manual bootstrap. |
1177 | return manual.BootstrapInstanceId |
1178 | } |
1179 | |
1180 | -func (_ nullBootstrapInstance) Status() string { |
1181 | +func (nullBootstrapInstance) Status() string { |
1182 | return "" |
1183 | } |
1184 | |
1185 | +func (nullBootstrapInstance) Refresh() error { |
1186 | + return nil |
1187 | +} |
1188 | + |
1189 | func (inst nullBootstrapInstance) Addresses() (addresses []instance.Address, err error) { |
1190 | host, err := inst.DNSName() |
1191 | if err != nil { |
1192 | @@ -43,14 +47,14 @@ |
1193 | return i.DNSName() |
1194 | } |
1195 | |
1196 | -func (_ nullBootstrapInstance) OpenPorts(machineId string, ports []instance.Port) error { |
1197 | - return nil |
1198 | -} |
1199 | - |
1200 | -func (_ nullBootstrapInstance) ClosePorts(machineId string, ports []instance.Port) error { |
1201 | - return nil |
1202 | -} |
1203 | - |
1204 | -func (_ nullBootstrapInstance) Ports(machineId string) ([]instance.Port, error) { |
1205 | +func (nullBootstrapInstance) OpenPorts(machineId string, ports []instance.Port) error { |
1206 | + return nil |
1207 | +} |
1208 | + |
1209 | +func (nullBootstrapInstance) ClosePorts(machineId string, ports []instance.Port) error { |
1210 | + return nil |
1211 | +} |
1212 | + |
1213 | +func (nullBootstrapInstance) Ports(machineId string) ([]instance.Port, error) { |
1214 | return []instance.Port{}, nil |
1215 | } |
1216 | |
1217 | === modified file 'provider/openstack/provider.go' |
1218 | --- provider/openstack/provider.go 2013-12-10 04:25:06 +0000 |
1219 | +++ provider/openstack/provider.go 2013-12-18 15:16:49 +0000 |
1220 | @@ -282,24 +282,43 @@ |
1221 | var _ simplestreams.HasRegion = (*environ)(nil) |
1222 | |
1223 | type openstackInstance struct { |
1224 | - *nova.ServerDetail |
1225 | e *environ |
1226 | instType *instances.InstanceType |
1227 | arch *string |
1228 | + |
1229 | + mu sync.Mutex |
1230 | + serverDetail *nova.ServerDetail |
1231 | } |
1232 | |
1233 | func (inst *openstackInstance) String() string { |
1234 | - return inst.ServerDetail.Id |
1235 | + return string(inst.Id()) |
1236 | } |
1237 | |
1238 | var _ instance.Instance = (*openstackInstance)(nil) |
1239 | |
1240 | +func (inst *openstackInstance) Refresh() error { |
1241 | + inst.mu.Lock() |
1242 | + defer inst.mu.Unlock() |
1243 | + server, err := inst.e.nova().GetServer(inst.serverDetail.Id) |
1244 | + if err != nil { |
1245 | + return err |
1246 | + } |
1247 | + inst.serverDetail = server |
1248 | + return nil |
1249 | +} |
1250 | + |
1251 | +func (inst *openstackInstance) getServerDetail() *nova.ServerDetail { |
1252 | + inst.mu.Lock() |
1253 | + defer inst.mu.Unlock() |
1254 | + return inst.serverDetail |
1255 | +} |
1256 | + |
1257 | func (inst *openstackInstance) Id() instance.Id { |
1258 | - return instance.Id(inst.ServerDetail.Id) |
1259 | + return instance.Id(inst.getServerDetail().Id) |
1260 | } |
1261 | |
1262 | func (inst *openstackInstance) Status() string { |
1263 | - return inst.ServerDetail.Status |
1264 | + return inst.getServerDetail().Status |
1265 | } |
1266 | |
1267 | func (inst *openstackInstance) hardwareCharacteristics() *instance.HardwareCharacteristics { |
1268 | @@ -326,7 +345,7 @@ |
1269 | // getAddress returns the existing server information on addresses, |
1270 | // but fetches the details over the api again if no addresses exist. |
1271 | func (inst *openstackInstance) getAddresses() (map[string][]nova.IPAddress, error) { |
1272 | - addrs := inst.ServerDetail.Addresses |
1273 | + addrs := inst.getServerDetail().Addresses |
1274 | if len(addrs) == 0 { |
1275 | server, err := inst.e.nova().GetServer(string(inst.Id())) |
1276 | if err != nil { |
1277 | @@ -726,7 +745,7 @@ |
1278 | } |
1279 | inst := &openstackInstance{ |
1280 | e: e, |
1281 | - ServerDetail: detail, |
1282 | + serverDetail: detail, |
1283 | arch: &spec.Image.Arch, |
1284 | instType: &spec.InstanceType, |
1285 | } |
1286 | @@ -787,7 +806,7 @@ |
1287 | switch server.Status { |
1288 | case nova.StatusActive, nova.StatusBuild, nova.StatusBuildSpawning: |
1289 | // TODO(wallyworld): lookup the flavor details to fill in the instance type data |
1290 | - out[id] = &openstackInstance{e: e, ServerDetail: &server} |
1291 | + out[id] = &openstackInstance{e: e, serverDetail: &server} |
1292 | continue |
1293 | } |
1294 | } |
1295 | @@ -836,7 +855,7 @@ |
1296 | // TODO(wallyworld): lookup the flavor details to fill in the instance type data |
1297 | insts = append(insts, &openstackInstance{ |
1298 | e: e, |
1299 | - ServerDetail: &s, |
1300 | + serverDetail: &s, |
1301 | }) |
1302 | } |
1303 | } |
Reviewers: mp+198867_ code.launchpad. net,
Message:
Please take a look.
Description:
provider/common: refresh DNS name during waitSSH
We now refresh the Instance prior to calls to DNSName,
to circument address caching. This will lead to an
increase in communication with the provider API, so
I have increased the refresh interval to 10s.
Fixes #1258240
https:/ /code.launchpad .net/~axwalk/ juju-core/ lp1258240- bootstrap- refresh- dnsname- take2/+ merge/198867
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/40860048/
Affected files (+57, -3 lines): machine_ test.go common/ bootstrap. go common/ bootstrap_ test.go
A [revision details]
M cmd/jujud/
M provider/
M provider/
Index: [revision details] 20131213032430- fx3t7fjahpr3dob 2
=== added file '[revision details]'
--- [revision details] 2012-01-01 00:00:00 +0000
+++ [revision details] 2012-01-01 00:00:00 +0000
@@ -0,0 +1,2 @@
+Old revision: tarmac-
+New revision: <email address hidden>
Index: cmd/jujud/ machine_ test.go machine_ test.go' machine_ test.go 2013-12-13 02:06:53 +0000 machine_ test.go 2013-12-13 04:02:20 +0000 net/juju- core/state/ api/params" net/juju- core/state/ testing" net/juju- core/state/ watcher" net/juju- core/testing" net/juju- core/testing" net/juju- core/testing" net/juju- core/testing/ checkers" net/juju- core/testing/ testbase" net/juju- core/tools"
=== modified file 'cmd/jujud/
--- cmd/jujud/
+++ cmd/jujud/
@@ -26,8 +26,8 @@
"launchpad.
statetesting "launchpad.
"launchpad.
+ "launchpad.
coretesting "launchpad.
- "launchpad.
jc "launchpad.
"launchpad.
"launchpad.
Index: provider/ common/ bootstrap. go common/ bootstrap. go' common/ bootstrap. go 2013-12-12 12:39:20 +0000 common/ bootstrap. go 2013-12-13 04:02:20 +0000 ance{Instance: inst, env: env} (&ctx, inst, machineConfig)
=== modified file 'provider/
--- provider/
+++ provider/
@@ -78,6 +78,7 @@
if err != nil {
return fmt.Errorf("cannot save state: %v", err)
}
+ inst = &refreshingInst
return FinishBootstrap
}
@@ -109,6 +110,26 @@ SetInterruptHan dler(nil)
ctx.
}
+// refreshingInstance is one that refreshes the ance) DNSName() (string, error) { ([]instance. Id{i.Instance. Id()}) DNSName( ) pSSHTimeout is the time we'll wait for SSH to come up on pSSHTimeout( ) SSHTimeoutOpts {
+// underlying Instance before each call to DNSName.
+type refreshingInstance struct {
+ instance.Instance
+ env environs.Environ
+ refresh bool
+}
+
+func (i *refreshingInst
+ if i.refresh {
+ instances, err := i.env.Instances
+ if err != nil {
+ return "", err
+ }
+ i.Instance = instances[0]
+ }
+ i.refresh = true
+ return i.Instance.
+}
+
// FinishBootstrap completes the bootstrap process by connecting
// to the instance via SSH and carrying out the cloud-config.
//
@@ -151,8 +172,10 @@
// DefaultBootstra
the bootstrap node
func DefaultBootstra
return SSHTimeoutOpts{
- Timeout: 10 * time.Minute,
- DNSNameDelay: 1 * time.Second,
+ Timeout: 10 * time.Minute,
+
+ // Not too frequent, as we refresh addresses from the provider each time.
+ DNSNameDelay: 10 * time.Second,
}
}
Index: provider/ common/ bootstrap_ test.go
=== mod...