Merge lp:~axwalk/juju-core/lp1258240-bootstrap-refresh-dnsname-take2 into lp:~go-bot/juju-core/trunk

Proposed by Andrew Wilkins
Status: Merged
Approved by: Andrew Wilkins
Approved revision: no longer in the source branch.
Merged at revision: 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
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/juju/nonce.txt),
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

https://codereview.appspot.com/40860048/

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/juju/nonce.txt),
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

https://codereview.appspot.com/40860048/

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

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):
   A [revision details]
   M cmd/jujud/machine_test.go
   M provider/common/bootstrap.go
   M provider/common/bootstrap_test.go

Index: [revision details]
=== 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-20131213032430-fx3t7fjahpr3dob2
+New revision: <email address hidden>

Index: cmd/jujud/machine_test.go
=== modified file 'cmd/jujud/machine_test.go'
--- cmd/jujud/machine_test.go 2013-12-13 02:06:53 +0000
+++ cmd/jujud/machine_test.go 2013-12-13 04:02:20 +0000
@@ -26,8 +26,8 @@
   "launchpad.net/juju-core/state/api/params"
   statetesting "launchpad.net/juju-core/state/testing"
   "launchpad.net/juju-core/state/watcher"
+ "launchpad.net/juju-core/testing"
   coretesting "launchpad.net/juju-core/testing"
- "launchpad.net/juju-core/testing"
   jc "launchpad.net/juju-core/testing/checkers"
   "launchpad.net/juju-core/testing/testbase"
   "launchpad.net/juju-core/tools"

Index: provider/common/bootstrap.go
=== modified file 'provider/common/bootstrap.go'
--- provider/common/bootstrap.go 2013-12-12 12:39:20 +0000
+++ provider/common/bootstrap.go 2013-12-13 04:02:20 +0000
@@ -78,6 +78,7 @@
   if err != nil {
    return fmt.Errorf("cannot save state: %v", err)
   }
+ inst = &refreshingInstance{Instance: inst, env: env}
   return FinishBootstrap(&ctx, inst, machineConfig)
  }

@@ -109,6 +110,26 @@
   ctx.SetInterruptHandler(nil)
  }

+// refreshingInstance is one that refreshes the
+// underlying Instance before each call to DNSName.
+type refreshingInstance struct {
+ instance.Instance
+ env environs.Environ
+ refresh bool
+}
+
+func (i *refreshingInstance) DNSName() (string, error) {
+ if i.refresh {
+ instances, err := i.env.Instances([]instance.Id{i.Instance.Id()})
+ if err != nil {
+ return "", err
+ }
+ i.Instance = instances[0]
+ }
+ i.refresh = true
+ return i.Instance.DNSName()
+}
+
  // FinishBootstrap completes the bootstrap process by connecting
  // to the instance via SSH and carrying out the cloud-config.
  //
@@ -151,8 +172,10 @@
  // DefaultBootstrapSSHTimeout is the time we'll wait for SSH to come up on
the bootstrap node
  func DefaultBootstrapSSHTimeout() SSHTimeoutOpts {
   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...

Read more...

Revision history for this message
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.

https://codereview.appspot.com/40860048/

Revision history for this message
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.

https://codereview.appspot.com/40860048/

Revision history for this message
John A Meinel (jameinel) wrote :

https://codereview.appspot.com/40860048/diff/1/provider/common/bootstrap.go
File provider/common/bootstrap.go (right):

https://codereview.appspot.com/40860048/diff/1/provider/common/bootstrap.go#newcode121
provider/common/bootstrap.go:121: func (i *refreshingInstance) DNSName()
(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.

https://codereview.appspot.com/40860048/

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

Please take a look.

https://codereview.appspot.com/40860048/diff/1/provider/common/bootstrap.go
File provider/common/bootstrap.go (right):

https://codereview.appspot.com/40860048/diff/1/provider/common/bootstrap.go#newcode121
provider/common/bootstrap.go:121: func (i *refreshingInstance) DNSName()
(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.

https://codereview.appspot.com/40860048/

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

https://codereview.appspot.com/40860048/diff/20001/provider/common/bootstrap.go
File provider/common/bootstrap.go (right):

https://codereview.appspot.com/40860048/diff/20001/provider/common/bootstrap.go#newcode161
provider/common/bootstrap.go:161: `, nonceFile,
utils.ShQuote(machineConfig.MachineNonce))
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.

https://codereview.appspot.com/40860048/

Revision history for this message
Andrew Wilkins (axwalk) wrote :
Revision history for this message
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.

https://codereview.appspot.com/40860048/

Revision history for this message
Andrew Wilkins (axwalk) wrote :
Revision history for this message
Andrew Wilkins (axwalk) wrote :
Revision history for this message
Andrew Wilkins (axwalk) wrote :
Revision history for this message
Andrew Wilkins (axwalk) wrote :
Revision history for this message
Roger Peppe (rogpeppe) wrote :
Download full text (3.9 KiB)

Looks great in general, with some comments and suggestions below.

https://codereview.appspot.com/40860048/diff/60001/provider/common/bootstrap.go
File provider/common/bootstrap.go (right):

https://codereview.appspot.com/40860048/diff/60001/provider/common/bootstrap.go#newcode320
provider/common/bootstrap.go:320: // wait for the tasks to complete. If
not
s/not/no/

https://codereview.appspot.com/40860048/diff/120001/environs/cloudinit/cloudinit.go
File environs/cloudinit/cloudinit.go (right):

https://codereview.appspot.com/40860048/diff/120001/environs/cloudinit/cloudinit.go#newcode163
environs/cloudinit/cloudinit.go:163: c.AddFile(path.Join(cfg.DataDir,
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://codereview.appspot.com/40860048/diff/120001/provider/common/bootstrap.go
File provider/common/bootstrap.go (right):

https://codereview.appspot.com/40860048/diff/120001/provider/common/bootstrap.go#newcode237
provider/common/bootstrap.go:237: time.Sleep(hc.checkDelay)
This should probably allow itself to be stopped when closed.j

https://codereview.appspot.com/40860048/diff/120001/provider/common/bootstrap.go#newcode288
provider/common/bootstrap.go:288: for _, addr := range addresses {
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[instance.Address]chan struct{}
     *parallel.Try
}

func newParallelHostChecker(checkDelay, checkHostScript)
*parallelHostChecker

func (g *parallelHostChecker) SetAddresses(addrs []instance.Address)

func (g *parallelHostChecker) Close() error {
     g.Try.Close()
     for _, ch := range active {
         close(ch)
     }
     return nil
}

https://codereview.appspot.com/40860048/diff/120001/provider/common/bootstrap_test.go
File provider/common/bootstrap_test.go (right):

https://codereview.appspot.com/40860048/diff/120001/provider/common/bootstrap_test.go#newcode364
provider/common/bootstrap_test.go:364: buf := &bytes.Buffer{}
Might be worth running go test -race just to make sure that it's ok to
use this, assuming you haven't already.

https://codereview.appspot.com/40860048/diff/120001/provider/ec2/ec2.go
File provider/ec2/ec2.go (right):

https://codereview.appspot.com/40860048/diff/120001/provider/ec2/ec2.go#newcode71
provider/ec2/ec2.go:71: instanceMu sync.Mutex
just "mu" should be fine here.
conventionally it's situated above the fields that it guards.

https://codereview.appspot.com/40860048/diff/120001/provider/ec2/ec2.go#newcode75
provider/ec2/ec2.go:75: return inst.InstanceId
shouldn't this use the mutex?

https://codereview.appspot.com/40860048/diff/120001/provider/ec2/ec2.go#newcode81
provider/ec2/ec2.go:81: return instance.Id(inst.InstanceId)
ditto

https://codereview.appspot.com/40860048/diff/120001/provider/ec2/ec2.go#newcode85
provider/ec2/ec2.go:85: return inst.State.Name
ditto

https://codereview.appspot.com/40860048/diff/120001/provider/openstack/provider.go
File provider/openstack/provider.go (right):

https://codereview.appspot.com/4...

Read more...

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

Please take a look.

https://codereview.appspot.com/40860048/diff/120001/environs/cloudinit/cloudinit.go
File environs/cloudinit/cloudinit.go (right):

https://codereview.appspot.com/40860048/diff/120001/environs/cloudinit/cloudinit.go#newcode163
environs/cloudinit/cloudinit.go:163: c.AddFile(path.Join(cfg.DataDir,
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.ComposeUserData,
and so there's no nice place to do that.

I could move environs.ComposeUserData to provider/common and add the
script in that function, but I don't think we'll have gained anything.

https://codereview.appspot.com/40860048/diff/120001/provider/common/bootstrap.go
File provider/common/bootstrap.go (right):

https://codereview.appspot.com/40860048/diff/120001/provider/common/bootstrap.go#newcode237
provider/common/bootstrap.go:237: time.Sleep(hc.checkDelay)
On 2013/12/18 09:01:05, rog wrote:
> This should probably allow itself to be stopped when closed.j

Done.

https://codereview.appspot.com/40860048/diff/120001/provider/common/bootstrap.go#newcode288
provider/common/bootstrap.go:288: for _, addr := range addresses {
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[instance.Address]chan struct{}
> *parallel.Try
> }

> func newParallelHostChecker(checkDelay, checkHostScript)
*parallelHostChecker

> func (g *parallelHostChecker) SetAddresses(addrs []instance.Address)

> func (g *parallelHostChecker) Close() error {
> g.Try.Close()
> for _, ch := range active {
> close(ch)
> }
> return nil
> }

Done.

https://codereview.appspot.com/40860048/diff/120001/provider/common/bootstrap_test.go
File provider/common/bootstrap_test.go (right):

https://codereview.appspot.com/40860048/diff/120001/provider/common/bootstrap_test.go#newcode364
provider/common/bootstrap_test.go:364: buf := &bytes.Buffer{}
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://codereview.appspot.com/40860048/diff/120001/provider/ec2/ec2.go
File provider/ec2/ec2.go (right):

https://codereview.appspot.com/40860048/diff/120001/provider/ec2/ec2.go#newcode71
provider/ec2/ec2.go:71: instanceMu sync.Mutex
On 2013/12/18 09:01:05, rog wrote:
> just "mu" should be fine here.
> conventionally it's situated above the fields tha...

Read more...

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

LGTM, thanks, except I'd like to know a bit more about the AddFile vs
AddRunCmd issue.

https://codereview.appspot.com/40860048/diff/120001/environs/cloudinit/cloudinit.go
File environs/cloudinit/cloudinit.go (right):

https://codereview.appspot.com/40860048/diff/120001/environs/cloudinit/cloudinit.go#newcode163
environs/cloudinit/cloudinit.go:163: c.AddFile(path.Join(cfg.DataDir,
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.ComposeUserData, and so
> there's no nice place to do that.

I was thinking that instead of this AddFile we'd just
call c.AddRunCmd(scriptToCreateMachineNonceFile).

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://codereview.appspot.com/40860048/diff/120001/provider/ec2/ec2.go
File provider/ec2/ec2.go (right):

https://codereview.appspot.com/40860048/diff/120001/provider/ec2/ec2.go#newcode75
provider/ec2/ec2.go:75: return inst.InstanceId
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.Instance (i.e. the embedded
> *ec2.Instance). I've put the mutex above that field and separated them
from "e"
> to make it more obvious.

Ahem. Discussed online.

https://codereview.appspot.com/40860048/diff/120001/provider/ec2/ec2.go#newcode110
provider/ec2/ec2.go:110: err := inst.Refresh()
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://codereview.appspot.com/40860048/diff/120001/provider/openstack/provider.go
File provider/openstack/provider.go (right):

https://codereview.appspot.com/40860048/diff/120001/provider/openstack/provider.go#newcode321
provider/openstack/provider.go:321: func (inst *openstackInstance)
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.Id(inst.serverDetail.Id)
> > }
> > then Id can just become:
> >
> > func (inst *openstackInstance) Id() instance.Id {
> > return idFromDetail(inst.getServerDetail())
> > }

> Good point. I went a step further and inlined the use if
inst.serverDetail.Id in
> Refresh, and then:

> func (inst *openstackInstance) Id() instance.Id {
> return instance.Id(inst.getServerDetail().Id)
> }

Cool - I almost suggested that.

https://codereview.appspot.com/40860048/

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

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://codereview.appspot.com/40860048/diff/120001/environs/cloudinit/cloudinit.go
> File environs/cloudinit/cloudinit.go (right):

https://codereview.appspot.com/40860048/diff/120001/environs/cloudinit/cloudinit.go#newcode163
> environs/cloudinit/cloudinit.go:163: c.AddFile(path.Join(cfg.DataDir,
> 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.ComposeUserData, and so
> > there's no nice place to do that.

> I was thinking that instead of this AddFile we'd just
> call c.AddRunCmd(scriptToCreateMachineNonceFile).

> 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://codereview.appspot.com/40860048/diff/120001/provider/ec2/ec2.go
> File provider/ec2/ec2.go (right):

https://codereview.appspot.com/40860048/diff/120001/provider/ec2/ec2.go#newcode75
> provider/ec2/ec2.go:75: return inst.InstanceId
> 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.Instance (i.e. the embedded
> > *ec2.Instance). I've put the mutex above that field and separated
them from
> "e"
> > to make it more obvious.

> Ahem. Discussed online.

https://codereview.appspot.com/40860048/diff/120001/provider/ec2/ec2.go#newcode110
> provider/ec2/ec2.go:110: err := inst.Refresh()
> 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://codereview.appspot.com/40860048/diff/120001/provider/openstack/provider.go
> File provider/openstack/provider.go (right):

https://codereview.appspot.com/40860048/diff/120001/provider/openstack/provider.go#newcode321
> provider/openstack/provider.go:321: func (inst *openstackInstance)
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.Id(inst.serverDet...

Read more...

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

Please take a look.

https://codereview.appspot.com/40860048/diff/120001/environs/cloudinit/cloudinit.go
File environs/cloudinit/cloudinit.go (right):

https://codereview.appspot.com/40860048/diff/120001/environs/cloudinit/cloudinit.go#newcode163
environs/cloudinit/cloudinit.go:163: c.AddFile(path.Join(cfg.DataDir,
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.ComposeUserData, and so
> > there's no nice place to do that.

> I was thinking that instead of this AddFile we'd just
> call c.AddRunCmd(scriptToCreateMachineNonceFile).

> 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.

https://codereview.appspot.com/40860048/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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 }

Subscribers

People subscribed via source and target branches

to status/vote changes: