Merge lp:~axwalk/juju-core/manual-bootstrap 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: 1850
Proposed branch: lp:~axwalk/juju-core/manual-bootstrap
Merge into: lp:~go-bot/juju-core/trunk
Diff against target: 1104 lines (+616/-155)
16 files modified
cmd/juju/bootstrap.go (+19/-0)
cmd/jujud/machine.go (+1/-1)
environs/interface.go (+11/-0)
environs/manual/agent.go (+28/-40)
environs/manual/bootstrap.go (+135/-0)
environs/manual/bootstrap_test.go (+164/-0)
environs/manual/detection.go (+14/-3)
environs/manual/detection_test.go (+7/-63)
environs/manual/fakessh_test.go (+112/-0)
environs/manual/provisioner.go (+17/-10)
environs/manual/provisioner_test.go (+28/-23)
environs/manual/tools.go (+24/-0)
testing/testbase/patch.go (+12/-0)
testing/testbase/patch_test.go (+9/-0)
worker/localstorage/config.go (+14/-0)
worker/localstorage/worker.go (+21/-15)
To merge this branch: bzr merge lp:~axwalk/juju-core/manual-bootstrap
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+184714@code.launchpad.net

Commit message

Implement manual bootstrapping

environs/bootstrap:
  - Added BootstrapStorage interface. If an Environ
    implements this interface, its BootstrapStorage
    will be used by cmd/juju bootstrap.

cmd/juju:
  - Updated bootstrap subcommand to use
    environs/bootstrap.BootstrapStorage,
    as described above.

provider/local/storage:
  - Added LocalStorageConfig interface, which an
    Environ may implement to describe the configuration
    of a "localstorage".

environs/filestorage:
  - Added NewFileStorage (i.e. implement StorageWrite)
    for testing in environs/manual.

environs/manual:
  - Refactored existing code to support bootstrap machines.
  - Added "Bootstrap" function to manually bootstrap an
    existing machine.

To manually bootstrap, you must provide an environment that
implements the new LocalStorageConfig and BootstrapStorage
interfaces. In other words, you can (currently) only manually
bootstrap a machine which is expected to manage the storage
for its environment. This may be relaxed later if needed.

Next up is the null provider, which uses this new Bootstrap
function. I need to address one last issue with it first:
the null provider mustn't narrow the tools to default-series.

https://codereview.appspot.com/13635044/

Description of the change

Implement manual bootstrapping

environs/bootstrap:
  - Added BootstrapStorage interface. If an Environ
    implements this interface, its BootstrapStorage
    will be used by cmd/juju bootstrap.

cmd/juju:
  - Updated bootstrap subcommand to use
    environs/bootstrap.BootstrapStorage,
    as described above.

provider/local/storage:
  - Added LocalStorageConfig interface, which an
    Environ may implement to describe the configuration
    of a "localstorage".

environs/filestorage:
  - Added NewFileStorage (i.e. implement StorageWrite)
    for testing in environs/manual.

environs/manual:
  - Refactored existing code to support bootstrap machines.
  - Added "Bootstrap" function to manually bootstrap an
    existing machine.

To manually bootstrap, you must provide an environment that
implements the new LocalStorageConfig and BootstrapStorage
interfaces. In other words, you can (currently) only manually
bootstrap a machine which is expected to manage the storage
for its environment. This may be relaxed later if needed.

Next up is the null provider, which uses this new Bootstrap
function. I need to address one last issue with it first:
the null provider mustn't narrow the tools to default-series.

https://codereview.appspot.com/13635044/

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

Reviewers: mp+184714_code.launchpad.net,

Message:
Please take a look.

Description:
Implement manual bootstrapping

environs/bootstrap:
   - Added BootstrapStorage interface. If an Environ
     implements this interface, its BootstrapStorage
     will be used by cmd/juju bootstrap.

cmd/juju:
   - Updated bootstrap subcommand to use
     environs/bootstrap.BootstrapStorage,
     as described above.

provider/local/storage:
   - Added LocalStorageConfig interface, which an
     Environ may implement to describe the configuration
     of a "localstorage".

environs/filestorage:
   - Added NewFileStorage (i.e. implement StorageWrite)
     for testing in environs/manual.

environs/manual:
   - Refactored existing code to support bootstrap machines.
   - Added "Bootstrap" function to manually bootstrap an
     existing machine.

To manually bootstrap, you must provide an environment that
implements the new LocalStorageConfig and BootstrapStorage
interfaces. In other words, you can (currently) only manually
bootstrap a machine which is expected to manage the storage
for its environment. This may be relaxed later if needed.

Next up is the null provider, which uses this new Bootstrap
function. I need to address one last issue with it first:
the null provider mustn't narrow the tools to default-series.

https://code.launchpad.net/~axwalk/juju-core/manual-bootstrap/+merge/184714

(do not edit description out of merge proposal)

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

Affected files (+525, -53 lines):
   A [revision details]
   M cmd/juju/bootstrap.go
   M environs/bootstrap/bootstrap.go
   M environs/filestorage/filestorage.go
   M environs/manual/agent.go
   A environs/manual/bootstrap.go
   A environs/manual/bootstrap_test.go
   M environs/manual/provisioner.go
   M environs/manual/provisioner_test.go
   A environs/manual/tools.go
   A provider/local/storage/config.go
   M provider/local/storage/worker.go

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

Looking very nice; just a few questions, and a suggestion that you
consider moving the local storage worker into the top-level worker
package, rather than hiding it away under local.

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

https://codereview.appspot.com/13635044/diff/4001/cmd/juju/bootstrap.go#newcode71
cmd/juju/bootstrap.go:71: if bs, ok :=
environ.(bootstrap.BootstrapStorage); ok {
Would that maybe be ".(BootstrapStorager)"? Elsewhere, we're
experimenting with things like:

type HasBootstrapStorage interface {
     BootstrapStorage() (Storage, error)
}

...which IMO leads at least to more-readable casts. YMMV.

https://codereview.appspot.com/13635044/diff/4001/environs/filestorage/filestorage.go
File environs/filestorage/filestorage.go (right):

https://codereview.appspot.com/13635044/diff/4001/environs/filestorage/filestorage.go#newcode114
environs/filestorage/filestorage.go:114: _, err = io.CopyN(file, r,
length)
As suggested in the previous review, please copy the whole lot somewhere
out of the way and move atomically into place on success; in this case I
think we've definitely vulnerable to errors with longer-term
consequences.

https://codereview.appspot.com/13635044/diff/4001/environs/manual/bootstrap.go
File environs/manual/bootstrap.go (right):

https://codereview.appspot.com/13635044/diff/4001/environs/manual/bootstrap.go#newcode143
environs/manual/bootstrap.go:143: }
This is really nice; can we produce some output while we're doing so? I
guess it'll have to be straight to the log for now, but a few INFO
messages would be helpful.

https://codereview.appspot.com/13635044/diff/4001/environs/manual/bootstrap.go#newcode146
environs/manual/bootstrap.go:146: // get the *state.Machine?
Don't think so...

https://codereview.appspot.com/13635044/diff/4001/environs/manual/bootstrap_test.go
File environs/manual/bootstrap_test.go (right):

https://codereview.appspot.com/13635044/diff/4001/environs/manual/bootstrap_test.go#newcode153
environs/manual/bootstrap_test.go:153: c.Assert(Bootstrap(args),
gc.ErrorMatches, "Environ argument is nil")
FWIW, I'm kinda ok with panics for unexpected nils.

https://codereview.appspot.com/13635044/diff/4001/environs/manual/bootstrap_test.go#newcode172
environs/manual/bootstrap_test.go:172: args.MachineId = "1"
Eyebrow slightly raised here... I'm not sure that's actually valid. Do
we initialize the machine id counter in state such that all subsequent
numbers are higher?

https://codereview.appspot.com/13635044/diff/4001/environs/manual/bootstrap_test.go#newcode204
environs/manual/bootstrap_test.go:204: defer sshresponse(c,
checkProvisionedScript, "", 0)()
This code block is looking awfully familiar now. Can we tidy it up a
little?

https://codereview.appspot.com/13635044/diff/4001/environs/manual/tools.go
File environs/manual/tools.go (right):

https://codereview.appspot.com/13635044/diff/4001/environs/manual/tools.go#newcode23
environs/manual/tools.go:23: arches := possibleTools.Arches()
Why would we be getting multiple arches out of FindInstanceTools? Aren't
we passing one in?

https://codereview.appspot.com/13635044/

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

Thank you for the review. I have moved provider/local/storage to
worker/localstorage.

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

https://codereview.appspot.com/13635044/diff/4001/cmd/juju/bootstrap.go#newcode71
cmd/juju/bootstrap.go:71: if bs, ok :=
environ.(bootstrap.BootstrapStorage); ok {
On 2013/09/11 13:51:21, fwereade wrote:
> Would that maybe be ".(BootstrapStorager)"?

Yes, that's a bit better, thanks.

> Elsewhere, we're experimenting with
> things like:

> type HasBootstrapStorage interface {
> BootstrapStorage() (Storage, error)
> }

> ...which IMO leads at least to more-readable casts. YMMV.

I'm not a big fan of the "HasX" style. IMO, the name should represent
the behaviour or role, not something an implementer has.

https://codereview.appspot.com/13635044/diff/4001/environs/filestorage/filestorage.go
File environs/filestorage/filestorage.go (right):

https://codereview.appspot.com/13635044/diff/4001/environs/filestorage/filestorage.go#newcode114
environs/filestorage/filestorage.go:114: _, err = io.CopyN(file, r,
length)
On 2013/09/11 13:51:21, fwereade wrote:
> As suggested in the previous review, please copy the whole lot
somewhere out of
> the way and move atomically into place on success; in this case I
think we've
> definitely vulnerable to errors with longer-term consequences.

Done. It's a bit simpler here :)

https://codereview.appspot.com/13635044/diff/4001/environs/manual/bootstrap.go
File environs/manual/bootstrap.go (right):

https://codereview.appspot.com/13635044/diff/4001/environs/manual/bootstrap.go#newcode143
environs/manual/bootstrap.go:143: }
On 2013/09/11 13:51:21, fwereade wrote:
> This is really nice; can we produce some output while we're doing so?
I guess
> it'll have to be straight to the log for now, but a few INFO messages
would be
> helpful.

Sure. I've added some logger.Infofs to checkProvisioned,
detectSeriesAndHardwareCharacteristics and provisionMachineAgent. I've
purposely not added Errorfs, as error results will be logged upstream.

https://codereview.appspot.com/13635044/diff/4001/environs/manual/bootstrap.go#newcode146
environs/manual/bootstrap.go:146: // get the *state.Machine?
On 2013/09/11 13:51:21, fwereade wrote:
> Don't think so...

Oops! Nor do I. I had that in there from early on, and forgot to take it
out :)

https://codereview.appspot.com/13635044/diff/4001/environs/manual/bootstrap_test.go
File environs/manual/bootstrap_test.go (right):

https://codereview.appspot.com/13635044/diff/4001/environs/manual/bootstrap_test.go#newcode153
environs/manual/bootstrap_test.go:153: c.Assert(Bootstrap(args),
gc.ErrorMatches, "Environ argument is nil")
On 2013/09/11 13:51:21, fwereade wrote:
> FWIW, I'm kinda ok with panics for unexpected nils.

Noted.

https://codereview.appspot.com/13635044/diff/4001/environs/manual/bootstrap_test.go#newcode172
environs/manual/bootstrap_test.go:172: args.MachineId = "1"
On 2013/09/11 13:51:21, fwereade wrote:
> Eyebrow slightly raised here... I'm not sure that's actually valid. Do
we
> initialize the machine id counter in state such that all subsequent
numbers are
> higher?

The reason for t...

Read more...

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

This is great, thanks. Modulo the /tmp-filesystem issue, this LGTM, on
the understanding that the local storage is not yet fit for purpose and
needs some concept of authentication [0] before we can actually let
people use this feature.

Let me know if I've completely misunderstood the overlap between
SSHStorage and LocalStorage, which is not a blocker for this CL but must
be resolved in a followup (if it's how I understood it to be).

[0] or to not be exposed, which we *might* be able to get away with if
we always used SSH storage from outside..?

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

https://codereview.appspot.com/13635044/diff/4001/cmd/juju/bootstrap.go#newcode71
cmd/juju/bootstrap.go:71: if bs, ok :=
environ.(bootstrap.BootstrapStorage); ok {
On 2013/09/12 07:59:00, axw1 wrote:
> On 2013/09/11 13:51:21, fwereade wrote:
> > Would that maybe be ".(BootstrapStorager)"?

> Yes, that's a bit better, thanks.

> > Elsewhere, we're experimenting with
> > things like:
> >
> > type HasBootstrapStorage interface {
> > BootstrapStorage() (Storage, error)
> > }
> >
> > ...which IMO leads at least to more-readable casts. YMMV.

> I'm not a big fan of the "HasX" style. IMO, the name should represent
the
> behaviour or role, not something an implementer has.

Your mileage may vary :). I'm not really sold on either way tbh.

https://codereview.appspot.com/13635044/diff/4001/environs/manual/bootstrap_test.go
File environs/manual/bootstrap_test.go (right):

https://codereview.appspot.com/13635044/diff/4001/environs/manual/bootstrap_test.go#newcode172
environs/manual/bootstrap_test.go:172: args.MachineId = "1"
On 2013/09/12 07:59:00, axw1 wrote:
> On 2013/09/11 13:51:21, fwereade wrote:
> > Eyebrow slightly raised here... I'm not sure that's actually valid.
Do we
> > initialize the machine id counter in state such that all subsequent
numbers
> are
> > higher?

> The reason for this test is that Environ.Bootstrap is provided with a
machine ID
> to use. manual.Bootstrap is invoked via the null provider's Bootstrap
method.
> Thus, the test exists so as not to make assumptions about being on
machine "0".

> Whether or not the counter is incremented... I don't think that can be
its
> concern, if it's been provided with the ID.

Yeah, this isn't your problem. I think it still is a problem that we can
allow bootstrap with arbitrary ids, but that other bits of the system
assign special meaning to "0" (it somehow seems that every time we
eliminate a special case for 0 we end up with a new one somewhere else).

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

https://codereview.appspot.com/13635044/diff/14001/cmd/jujud/machine.go#newcode30
cmd/jujud/machine.go:30: localstorage
"launchpad.net/juju-core/worker/localstorage"
drop the explicit name?

https://codereview.appspot.com/13635044/diff/14001/environs/filestorage/filestorage.go
File environs/filestorage/filestorage.go (right):

https://codereview.appspot.com/13635044/diff/14001/environs/filestorage/filestorage.go#newcode45
environs/filestorage/filestorage.go:45: func NewFileStorage(...

Read more...

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

Haven't pushed changes yet, need to sort out the storage issues. Here's
my answers anyway.

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

https://codereview.appspot.com/13635044/diff/14001/cmd/jujud/machine.go#newcode30
cmd/jujud/machine.go:30: localstorage
"launchpad.net/juju-core/worker/localstorage"
On 2013/09/13 09:24:59, fwereade wrote:
> drop the explicit name?

Done.

https://codereview.appspot.com/13635044/diff/14001/environs/filestorage/filestorage.go
File environs/filestorage/filestorage.go (right):

https://codereview.appspot.com/13635044/diff/14001/environs/filestorage/filestorage.go#newcode45
environs/filestorage/filestorage.go:45: func NewFileStorage(path string)
(environs.Storage, error) {
On 2013/09/13 09:24:59, fwereade wrote:
> High-level question while I think of it: does the SSHStorage on
bootstrap get
> reused but backed with a FileStorage? I sort of imagine so, in which
case, cool
> ...but is it possible for there to be overlap? ie should we be sharing
the
> flocking everywhere?

> or am I just fantasizing?

filestorage is only used for testing at the moment.

environs/localstorage takes over from environs/sshstorage, reusing the
storage directory. However, there is no overlap in use;
environs/sshstorage is only used to write the tools and bootstrap state
file. *If* there's a failure bootstrapping, the bootstrap storage may
also be used to remove the state file.

Since there's no overlap, there's no need to share the locking. I did
just realise that I totally broke the manual bootstrap code though, when
I changed sshstorage's directory structure. I'll have to revisit that
before merging.

https://codereview.appspot.com/13635044/diff/14001/environs/filestorage/filestorage.go#newcode110
environs/filestorage/filestorage.go:110: file, err :=
ioutil.TempFile("", "juju-filestorage-"+name)
On 2013/09/13 09:24:59, fwereade wrote:
> Same different-filesystems problem.

This is only for testing, but I will fix it anyway. I think localstorage
ought to be renamed httpstorage, and have the backend contain one of
these.

https://codereview.appspot.com/13635044/diff/14001/environs/manual/bootstrap.go
File environs/manual/bootstrap.go (right):

https://codereview.appspot.com/13635044/diff/14001/environs/manual/bootstrap.go#newcode28
environs/manual/bootstrap.go:28: bootstrap.BootstrapStorager
On 2013/09/13 09:24:59, fwereade wrote:
> OK, I think my problem here is maybe that it's not *just* about
bootstrap
> storage, right? It's the same storage we always use, but this is the
path to it
> that's accessible from a client machine. Yes?

> So, at least in the short term, we *will* need SSHStorage and
LocalStorage to
> coexist.

They're pointing to the same location, but they're never used
concurrently.

https://codereview.appspot.com/13635044/diff/14001/worker/localstorage/worker.go
File worker/localstorage/worker.go (left):

https://codereview.appspot.com/13635044/diff/14001/worker/localstorage/worker.go#oldcode1
worker/localstorage/worker.go:1: package storage
On 2013/09/13 09:24:59, fwereade wrote:
> ah, I see. Can we call this localstorage please?

Sorry, fixed.

https://co...

Read more...

Revision history for this message
Roger Peppe (rogpeppe) wrote :
Download full text (7.5 KiB)

Looks good. Some minor comments and suggestions below.

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

https://codereview.appspot.com/13635044/diff/14001/cmd/juju/bootstrap.go#newcode76
cmd/juju/bootstrap.go:76: environ = &bootstrapStorageEnviron{environ,
bootstrapStorage}
nice

https://codereview.appspot.com/13635044/diff/14001/environs/bootstrap/bootstrap.go
File environs/bootstrap/bootstrap.go (right):

https://codereview.appspot.com/13635044/diff/14001/environs/bootstrap/bootstrap.go#newcode24
environs/bootstrap/bootstrap.go:24: // BootstrapStorager is an interface
that returns a environs.Storage that may
I think I'd define this inside environs/interface.go

This package doesn't define or use it, and that file
is a useful reference for people implementing providers.

https://codereview.appspot.com/13635044/diff/14001/environs/filestorage/filestorage.go
File environs/filestorage/filestorage.go (right):

https://codereview.appspot.com/13635044/diff/14001/environs/filestorage/filestorage.go#newcode45
environs/filestorage/filestorage.go:45: func NewFileStorage(path string)
(environs.Storage, error) {
Doc comment please.

Also, wouldn't it make more sense to define NewFileStorageReader in
terms of NewFileStorage? That way you wouldn't need the dynamic type
conversion, as Storage can be assigned to StorageReader.

For future consideration, I think filestorage.New and
filestorage.NewReader would be more idiomatic names than
filestorage.NewFileStorage and filestorage.NewFileStorageReader.

https://codereview.appspot.com/13635044/diff/14001/environs/manual/agent.go
File environs/manual/agent.go (right):

https://codereview.appspot.com/13635044/diff/14001/environs/manual/agent.go#newcode41
environs/manual/agent.go:41: machineenv map[string]string
s/machineenv/machineEnv/

https://codereview.appspot.com/13635044/diff/14001/environs/manual/bootstrap.go
File environs/manual/bootstrap.go (right):

https://codereview.appspot.com/13635044/diff/14001/environs/manual/bootstrap.go#newcode42
environs/manual/bootstrap.go:42: var errHostEmpty = errors.New("Host
argument is empty")
Errors don't generally start with a capital letter.

Also, if it's only used once, I wouldn't bother defining a variable for
it.

https://codereview.appspot.com/13635044/diff/14001/environs/manual/bootstrap.go#newcode43
environs/manual/bootstrap.go:43: var errEnvironNil = errors.New("Environ
argument is nil")
ditto

https://codereview.appspot.com/13635044/diff/14001/environs/manual/bootstrap.go#newcode51
environs/manual/bootstrap.go:51: // host will manually bootstrapped.
s/will/will be/ ?

https://codereview.appspot.com/13635044/diff/14001/environs/manual/bootstrap.go#newcode65
environs/manual/bootstrap.go:65: err = fmt.Errorf("error checking if
provisioned: %v", err)
"failed to check provisioned status: %v" ?

https://codereview.appspot.com/13635044/diff/14001/environs/manual/bootstrap.go#newcode78
environs/manual/bootstrap.go:78: defer func() {
Why not defer this after the call to SaveState and thereby lose the need
for the savedState variable?

https://codereview.appspot.com/13635044/diff/14001/environs/manual/bootst...

Read more...

Revision history for this message
Tim Penhey (thumper) wrote :
Download full text (5.0 KiB)

Still some changes needed here I think

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

https://codereview.appspot.com/13635044/diff/14001/cmd/juju/bootstrap.go#newcode76
cmd/juju/bootstrap.go:76: environ = &bootstrapStorageEnviron{environ,
bootstrapStorage}
If all we are caring about is the storage, why are we changing the
environ?

Does it not make sense to change the things that depend on the storage
to just depend on the storage and not the entire environment, then you
don't need to mock out the Storage method.

Obviously here the important thing is that we want to use a particular
storage for bootstrap, so I'd expect to be assigning the
bootstrapStorage to a storage variable, not mocking out the method with
a different structure.

https://codereview.appspot.com/13635044/diff/14001/environs/bootstrap/bootstrap.go
File environs/bootstrap/bootstrap.go (right):

https://codereview.appspot.com/13635044/diff/14001/environs/bootstrap/bootstrap.go#newcode24
environs/bootstrap/bootstrap.go:24: // BootstrapStorager is an interface
that returns a environs.Storage that may
On 2013/09/14 07:28:03, rog wrote:
> I think I'd define this inside environs/interface.go

> This package doesn't define or use it, and that file
> is a useful reference for people implementing providers.

Agreed. It is something that is used by the environment, so it makes
more sense to live there.

https://codereview.appspot.com/13635044/diff/14001/environs/filestorage/filestorage.go
File environs/filestorage/filestorage.go (right):

https://codereview.appspot.com/13635044/diff/14001/environs/filestorage/filestorage.go#newcode110
environs/filestorage/filestorage.go:110: file, err :=
ioutil.TempFile("", "juju-filestorage-"+name)
On 2013/09/13 09:41:26, axw1 wrote:
> On 2013/09/13 09:24:59, fwereade wrote:
> > Same different-filesystems problem.

> This is only for testing, but I will fix it anyway. I think
localstorage ought
> to be renamed httpstorage, and have the backend contain one of these.

That sounds like a good idea IMO

https://codereview.appspot.com/13635044/diff/14001/environs/manual/agent.go
File environs/manual/agent.go (right):

https://codereview.appspot.com/13635044/diff/14001/environs/manual/agent.go#newcode82
environs/manual/agent.go:82: err := environs.FinishMachineConfig(mcfg,
args.envcfg, constraints.Value{})
I fear that using names like "envcfg" means that we have more and more
assumed knowledge encoded into our variable names. Here I like some
parts of the Zen of Python "Explicit is better than implicit", yes it is
python, but I think the principle holds.

https://codereview.appspot.com/13635044/diff/14001/environs/manual/bootstrap.go
File environs/manual/bootstrap.go (right):

https://codereview.appspot.com/13635044/diff/14001/environs/manual/bootstrap.go#newcode28
environs/manual/bootstrap.go:28: bootstrap.BootstrapStorager
Arg, here I know that I'm going to clash on names with Rog, but IMO
   BootstrapStorager
is a terrible name.
   HasBootstrapStorage
makes a lot more sense to me. Firstly you can look at the name and know
what is going on, and we aren't inventing words to go by the weird...

Read more...

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

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

https://codereview.appspot.com/13635044/diff/14001/cmd/juju/bootstrap.go#newcode76
cmd/juju/bootstrap.go:76: environ = &bootstrapStorageEnviron{environ,
bootstrapStorage}
On 2013/09/16 03:02:08, thumper wrote:
> If all we are caring about is the storage, why are we changing the
environ?

> Does it not make sense to change the things that depend on the storage
to just
> depend on the storage and not the entire environment, then you don't
need to
> mock out the Storage method.

The storage must be passed down to environs/bootstrap Bootstrap, and
then to environs/tools FindTools. Possibly more places. Those things
also want an Environ (in the case of FindTools, it's hidden in an
interface conversion for the legacy fallback codepath).

Bootstrap storage must be used for *everything* until the environment is
bootstrapped. Leaving a method for acquiring broken/unusable storage
seems like a mistake to me.

> Obviously here the important thing is that we want to use a particular
storage
> for bootstrap, so I'd expect to be assigning the bootstrapStorage to a
storage
> variable, not mocking out the method with a different structure.

https://codereview.appspot.com/13635044/diff/14001/environs/bootstrap/bootstrap.go
File environs/bootstrap/bootstrap.go (right):

https://codereview.appspot.com/13635044/diff/14001/environs/bootstrap/bootstrap.go#newcode24
environs/bootstrap/bootstrap.go:24: // BootstrapStorager is an interface
that returns a environs.Storage that may
On 2013/09/14 07:28:03, rog wrote:
> I think I'd define this inside environs/interface.go

> This package doesn't define or use it, and that file
> is a useful reference for people implementing providers.

Fair point, done.

https://codereview.appspot.com/13635044/diff/14001/environs/filestorage/filestorage.go
File environs/filestorage/filestorage.go (right):

https://codereview.appspot.com/13635044/diff/14001/environs/filestorage/filestorage.go#newcode45
environs/filestorage/filestorage.go:45: func NewFileStorage(path string)
(environs.Storage, error) {
On 2013/09/14 07:28:03, rog wrote:
> Doc comment please.

Done. Seems someone else came along and wrote a writer constructor in
the mean time. It lacks a doc comment, so I'll add to that :)

> Also, wouldn't it make more sense to define NewFileStorageReader in
terms of
> NewFileStorage? That way you wouldn't need the dynamic type
conversion, as
> Storage can be assigned to StorageReader.

That would simplify the code here. My rationale for doing it this way is
that it limits exposure.

IMO, you shouldn't be able to convert a StorageReader to a Storage,
unless you originally created it with NewStorage and converted it to
StorageReader.

> For future consideration, I think filestorage.New and
filestorage.NewReader
> would be more idiomatic names than filestorage.NewFileStorage and
> filestorage.NewFileStorageReader.

I do agree, but sadly it seems there's no consensus.

https://codereview.appspot.com/13635044/diff/14001/environs/filestorage/filestorage.go#newcode110
environs/filestorage/filestorage.go:110: file, err :=
ioutil.TempFile...

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

https://codereview.appspot.com/13635044/diff/14001/environs/manual/bootstrap.go
File environs/manual/bootstrap.go (right):

https://codereview.appspot.com/13635044/diff/14001/environs/manual/bootstrap.go#newcode28
environs/manual/bootstrap.go:28: bootstrap.BootstrapStorager
On 2013/09/16 04:56:48, axw1 wrote:
> On 2013/09/16 03:02:08, thumper wrote:
> > Arg, here I know that I'm going to clash on names with Rog, but IMO
> > BootstrapStorager
> > is a terrible name.
> > HasBootstrapStorage
> > makes a lot more sense to me. Firstly you can look at the name and
know what
> is
> > going on, and we aren't inventing words to go by the weird idea that
> interfaces
> > should end with "er".
> >
> > bootstrapStorage, ok := sometype.(HasBootstrapStorage)
> >
> > reads ok to me (FSVO ok given the syntax of the type assertion).

> Copy and paste of my reply to William: I'm not a big fan of the "HasX"
style.
> IMO, the name should represent the behaviour or role, not something an
> implementer has.

+lots.

I'm not keen on "BootstrapStorager" as a name either,
but I think it's important that interface names are nouns
not predicates - an interface name should represent what the
object *is*, not what it *can be*.

https://codereview.appspot.com/13635044/

Revision history for this message
Tim Penhey (thumper) wrote :

On 2013/09/16 16:51:34, rog wrote:

https://codereview.appspot.com/13635044/diff/14001/environs/manual/bootstrap.go
> File environs/manual/bootstrap.go (right):

https://codereview.appspot.com/13635044/diff/14001/environs/manual/bootstrap.go#newcode28
> environs/manual/bootstrap.go:28: bootstrap.BootstrapStorager
> On 2013/09/16 04:56:48, axw1 wrote:
> > On 2013/09/16 03:02:08, thumper wrote:
> > > Arg, here I know that I'm going to clash on names with Rog, but
IMO
> > > BootstrapStorager
> > > is a terrible name.
> > > HasBootstrapStorage
> > > makes a lot more sense to me. Firstly you can look at the name
and know
> what
> > is
> > > going on, and we aren't inventing words to go by the weird idea
that
> > interfaces
> > > should end with "er".
> > >
> > > bootstrapStorage, ok := sometype.(HasBootstrapStorage)
> > >
> > > reads ok to me (FSVO ok given the syntax of the type assertion).
> >
> > Copy and paste of my reply to William: I'm not a big fan of the
"HasX" style.
> > IMO, the name should represent the behaviour or role, not something
an
> > implementer has.

> +lots.

> I'm not keen on "BootstrapStorager" as a name either,
> but I think it's important that interface names are nouns
> not predicates - an interface name should represent what the
> object *is*, not what it *can be*.

Why not then just call the interface "BootstrapStorage".

https://codereview.appspot.com/13635044/

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

On 16 September 2013 22:01, <email address hidden> wrote:
> Why not then just call the interface "BootstrapStorage".

That would be appropriate for the storage itself, but this
interface represents an object that can be asked *for* the storage,
which doesn't seem quite the same thing to me.

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

If the only remaining contentious point is (Has)BootstrapStorage(r),
then I call dealer's choice and LGTM whatever he picks; I don't think
any of the names are great, but I also don't think any of them are so
bad as to unacceptably reduce the quality of the codebase ;).

If there are other problems, carry on as usual, ofc, but let's not block
this on naming arguments.

https://codereview.appspot.com/13635044/

Revision history for this message
Go Bot (go-bot) wrote :

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

environs/manual/bootstrap_test.go

# launchpad.net/juju-core/cmd/juju
cmd/juju/bootstrap.go:237: undefined: environs.Storage
cmd/juju/bootstrap.go:240: undefined: environs.Storage

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cmd/juju/bootstrap.go'
2--- cmd/juju/bootstrap.go 2013-09-20 00:52:13 +0000
3+++ cmd/juju/bootstrap.go 2013-09-20 05:05:14 +0000
4@@ -17,6 +17,7 @@
5 "launchpad.net/juju-core/environs"
6 "launchpad.net/juju-core/environs/bootstrap"
7 "launchpad.net/juju-core/environs/config"
8+ "launchpad.net/juju-core/environs/storage"
9 "launchpad.net/juju-core/environs/sync"
10 envtools "launchpad.net/juju-core/environs/tools"
11 "launchpad.net/juju-core/errors"
12@@ -68,6 +69,15 @@
13 if err != nil {
14 return err
15 }
16+ // If the environment has a special bootstrap Storage, use it wherever
17+ // we'd otherwise use environ.Storage.
18+ if bs, ok := environ.(environs.BootstrapStorager); ok {
19+ bootstrapStorage, err := bs.BootstrapStorage()
20+ if err != nil {
21+ return fmt.Errorf("failed to acquire bootstrap storage: %v", err)
22+ }
23+ environ = &bootstrapStorageEnviron{environ, bootstrapStorage}
24+ }
25 // TODO: if in verbose mode, write out to Stdout if a new cert was created.
26 _, err = environs.EnsureCertificate(environ, environs.WriteCertAndKey)
27 if err != nil {
28@@ -222,3 +232,12 @@
29 }
30 return unique.Values()
31 }
32+
33+type bootstrapStorageEnviron struct {
34+ environs.Environ
35+ bootstrapStorage storage.Storage
36+}
37+
38+func (b *bootstrapStorageEnviron) Storage() storage.Storage {
39+ return b.bootstrapStorage
40+}
41
42=== modified file 'cmd/jujud/machine.go'
43--- cmd/jujud/machine.go 2013-09-18 06:27:52 +0000
44+++ cmd/jujud/machine.go 2013-09-20 05:05:14 +0000
45@@ -20,7 +20,6 @@
46 "launchpad.net/juju-core/log"
47 "launchpad.net/juju-core/names"
48 "launchpad.net/juju-core/provider"
49- localstorage "launchpad.net/juju-core/provider/local/storage"
50 "launchpad.net/juju-core/state"
51 "launchpad.net/juju-core/state/api/params"
52 "launchpad.net/juju-core/state/apiserver"
53@@ -29,6 +28,7 @@
54 "launchpad.net/juju-core/worker/cleaner"
55 "launchpad.net/juju-core/worker/deployer"
56 "launchpad.net/juju-core/worker/firewaller"
57+ "launchpad.net/juju-core/worker/localstorage"
58 "launchpad.net/juju-core/worker/logger"
59 "launchpad.net/juju-core/worker/machiner"
60 "launchpad.net/juju-core/worker/minunitsworker"
61
62=== modified file 'environs/interface.go'
63--- environs/interface.go 2013-09-18 07:13:09 +0000
64+++ environs/interface.go 2013-09-20 05:05:14 +0000
65@@ -68,6 +68,17 @@
66 PublicStorage() storage.StorageReader
67 }
68
69+// BootstrapStorager is an interface that returns a environs.Storage that may
70+// be used before the bootstrap machine agent has been provisioned.
71+//
72+// This is useful for environments where the storage is managed by the machine
73+// agent once bootstrapped.
74+type BootstrapStorager interface {
75+ // BootstrapStorager returns an environs.Storage that may be used while
76+ // bootstrapping a machine.
77+ BootstrapStorage() (storage.Storage, error)
78+}
79+
80 // ConfigGetter implements access to an environments configuration.
81 type ConfigGetter interface {
82 // Config returns the configuration data with which the Environ was created.
83
84=== modified file 'environs/manual/agent.go'
85--- environs/manual/agent.go 2013-09-13 08:39:36 +0000
86+++ environs/manual/agent.go 2013-09-20 05:05:14 +0000
87@@ -14,7 +14,7 @@
88 "launchpad.net/juju-core/constraints"
89 "launchpad.net/juju-core/environs"
90 "launchpad.net/juju-core/environs/cloudinit"
91- envtools "launchpad.net/juju-core/environs/tools"
92+ "launchpad.net/juju-core/environs/config"
93 "launchpad.net/juju-core/provider"
94 "launchpad.net/juju-core/state"
95 "launchpad.net/juju-core/state/api"
96@@ -23,21 +23,27 @@
97 )
98
99 type provisionMachineAgentArgs struct {
100- host string
101- dataDir string
102- env environs.Environ
103- machine *state.Machine
104- nonce string
105- stateInfo *state.Info
106- apiInfo *api.Info
107- series string
108- arch string
109- tools *tools.Tools
110+ host string
111+ dataDir string
112+ bootstrap bool
113+ environConfig *config.Config
114+ machineId string
115+ nonce string
116+ stateFileURL string
117+ stateInfo *state.Info
118+ apiInfo *api.Info
119+ tools *tools.Tools
120+
121+ // agentEnv is an optional map of
122+ // arbitrary key/value pairs to pass
123+ // into the machine agent.
124+ agentEnv map[string]string
125 }
126
127 // provisionMachineAgent connects to a machine over SSH,
128 // copies across the tools, and installs a machine agent.
129 func provisionMachineAgent(args provisionMachineAgentArgs) error {
130+ logger.Infof("Provisioning machine agent on %s", args.host)
131 script, err := provisionMachineAgentScript(args)
132 if err != nil {
133 return err
134@@ -58,28 +64,27 @@
135 // provisionMachineAgentScript generates the script necessary
136 // to install a machine agent on the specified host.
137 func provisionMachineAgentScript(args provisionMachineAgentArgs) (string, error) {
138- tools := args.tools
139- if tools == nil {
140- var err error
141- tools, err = findMachineAgentTools(args.env, args.series, args.arch)
142- if err != nil {
143- return "", err
144- }
145- }
146-
147 // We generate a cloud-init config, which we'll then pull the runcmds
148 // and prerequisite packages out of. Rather than generating a cloud-config,
149 // we'll just generate a shell script.
150- mcfg := environs.NewMachineConfig(args.machine.Id(), args.nonce, args.stateInfo, args.apiInfo)
151+ var mcfg *cloudinit.MachineConfig
152+ if args.bootstrap {
153+ mcfg = environs.NewBootstrapMachineConfig(args.machineId, args.nonce)
154+ } else {
155+ mcfg = environs.NewMachineConfig(args.machineId, args.nonce, args.stateInfo, args.apiInfo)
156+ }
157 if args.dataDir != "" {
158 mcfg.DataDir = args.dataDir
159 }
160- mcfg.Tools = tools
161- err := environs.FinishMachineConfig(mcfg, args.env.Config(), constraints.Value{})
162+ mcfg.Tools = args.tools
163+ err := environs.FinishMachineConfig(mcfg, args.environConfig, constraints.Value{})
164 if err != nil {
165 return "", err
166 }
167 mcfg.AgentEnvironment[agent.ProviderType] = provider.Null
168+ for k, v := range args.agentEnv {
169+ mcfg.AgentEnvironment[k] = v
170+ }
171 cloudcfg := corecloudinit.New()
172 if cloudcfg, err = cloudinit.Configure(mcfg, cloudcfg); err != nil {
173 return "", err
174@@ -116,20 +121,3 @@
175 script = append(head, tail...)
176 return strings.Join(script, "\n"), nil
177 }
178-
179-func findMachineAgentTools(env environs.Environ, series, arch string) (*tools.Tools, error) {
180- agentVersion, ok := env.Config().AgentVersion()
181- if !ok {
182- return nil, fmt.Errorf("no agent version set in environment configuration")
183- }
184- possibleTools, err := envtools.FindInstanceTools(env, agentVersion, series, &arch)
185- if err != nil {
186- return nil, err
187- }
188- arches := possibleTools.Arches()
189- possibleTools, err = possibleTools.Match(tools.Filter{Arch: arch})
190- if err != nil {
191- return nil, fmt.Errorf("chosen architecture %v not present in %v", arch, arches)
192- }
193- return possibleTools[0], nil
194-}
195
196=== added file 'environs/manual/bootstrap.go'
197--- environs/manual/bootstrap.go 1970-01-01 00:00:00 +0000
198+++ environs/manual/bootstrap.go 2013-09-20 05:05:14 +0000
199@@ -0,0 +1,135 @@
200+// Copyright 2013 Canonical Ltd.
201+// Licensed under the AGPLv3, see LICENCE file for details.
202+
203+package manual
204+
205+import (
206+ "errors"
207+ "fmt"
208+
209+ "launchpad.net/juju-core/agent"
210+ "launchpad.net/juju-core/environs"
211+ envtools "launchpad.net/juju-core/environs/tools"
212+ "launchpad.net/juju-core/instance"
213+ "launchpad.net/juju-core/names"
214+ "launchpad.net/juju-core/provider"
215+ "launchpad.net/juju-core/state"
216+ "launchpad.net/juju-core/tools"
217+ "launchpad.net/juju-core/worker/localstorage"
218+)
219+
220+const BootstrapInstanceId = instance.Id(manualInstancePrefix)
221+
222+// LocalStorageEnviron is an Environ where the bootstrap node
223+// manages its own local storage.
224+type LocalStorageEnviron interface {
225+ environs.Environ
226+ environs.BootstrapStorager
227+ localstorage.LocalStorageConfig
228+}
229+
230+type BootstrapArgs struct {
231+ Host string
232+ Environ LocalStorageEnviron
233+ MachineId string
234+ PossibleTools tools.List
235+}
236+
237+// TODO(axw) make this configurable?
238+const dataDir = "/var/lib/juju"
239+
240+func errMachineIdInvalid(machineId string) error {
241+ return fmt.Errorf("%q is not a valid machine ID", machineId)
242+}
243+
244+// NewManualBootstrapEnviron wraps a LocalStorageEnviron with another which
245+// overrides the Bootstrap method; when Bootstrap is invoked, the specified
246+// host will be manually bootstrapped.
247+func Bootstrap(args BootstrapArgs) (err error) {
248+ if args.Host == "" {
249+ return errors.New("host argument is empty")
250+ }
251+ if args.Environ == nil {
252+ return errors.New("environ argument is nil")
253+ }
254+ if !names.IsMachine(args.MachineId) {
255+ return errMachineIdInvalid(args.MachineId)
256+ }
257+
258+ provisioned, err := checkProvisioned(args.Host)
259+ if err != nil {
260+ return fmt.Errorf("failed to check provisioned status: %v", err)
261+ }
262+ if provisioned {
263+ return ErrProvisioned
264+ }
265+
266+ bootstrapStorage, err := args.Environ.BootstrapStorage()
267+ if err != nil {
268+ return err
269+ }
270+
271+ hc, series, err := detectSeriesAndHardwareCharacteristics(args.Host)
272+ if err != nil {
273+ return fmt.Errorf("error detecting hardware characteristics: %v", err)
274+ }
275+
276+ // Filter tools based on detected series/arch.
277+ logger.Infof("Filtering possible tools: %v", args.PossibleTools)
278+ possibleTools, err := args.PossibleTools.Match(tools.Filter{
279+ Arch: *hc.Arch,
280+ Series: series,
281+ })
282+ if err != nil {
283+ return err
284+ }
285+
286+ // Store the state file. If provisioning fails, we'll remove the file.
287+ logger.Infof("Saving bootstrap state file to bootstrap storage")
288+ err = provider.SaveState(
289+ bootstrapStorage,
290+ &provider.BootstrapState{
291+ StateInstances: []instance.Id{BootstrapInstanceId},
292+ Characteristics: []instance.HardwareCharacteristics{hc},
293+ },
294+ )
295+ if err != nil {
296+ return err
297+ }
298+ defer func() {
299+ if err != nil {
300+ logger.Errorf("bootstrapping failed, removing state file: %v", err)
301+ bootstrapStorage.Remove(provider.StateFile)
302+ }
303+ }()
304+
305+ // Get a file:// scheme tools URL for the tools, which will have been
306+ // copied to the remote machine's storage directory.
307+ tools := *possibleTools[0]
308+ storageDir := args.Environ.StorageDir()
309+ toolsStorageName := envtools.StorageName(tools.Version)
310+ tools.URL = fmt.Sprintf("file://%s/%s", storageDir, toolsStorageName)
311+
312+ // Add the local storage configuration.
313+ agentEnv := map[string]string{
314+ agent.StorageAddr: args.Environ.StorageAddr(),
315+ agent.StorageDir: storageDir,
316+ agent.SharedStorageAddr: args.Environ.SharedStorageAddr(),
317+ agent.SharedStorageDir: args.Environ.SharedStorageDir(),
318+ }
319+
320+ // Finally, provision the machine agent.
321+ stateFileURL := fmt.Sprintf("file://%s/%s", storageDir, provider.StateFile)
322+ err = provisionMachineAgent(provisionMachineAgentArgs{
323+ host: args.Host,
324+ dataDir: dataDir,
325+ environConfig: args.Environ.Config(),
326+ stateFileURL: stateFileURL,
327+ machineId: args.MachineId,
328+ bootstrap: true,
329+ nonce: state.BootstrapNonce,
330+ tools: &tools,
331+ agentEnv: agentEnv,
332+ })
333+ return err
334+}
335
336=== added file 'environs/manual/bootstrap_test.go'
337--- environs/manual/bootstrap_test.go 1970-01-01 00:00:00 +0000
338+++ environs/manual/bootstrap_test.go 2013-09-20 05:05:14 +0000
339@@ -0,0 +1,164 @@
340+// Copyright 2013 Canonical Ltd.
341+// Licensed under the AGPLv3, see LICENCE file for details.
342+
343+package manual
344+
345+import (
346+ "os"
347+
348+ gc "launchpad.net/gocheck"
349+
350+ "launchpad.net/juju-core/environs"
351+ "launchpad.net/juju-core/environs/filestorage"
352+ "launchpad.net/juju-core/environs/storage"
353+ "launchpad.net/juju-core/environs/tools"
354+ coreerrors "launchpad.net/juju-core/errors"
355+ "launchpad.net/juju-core/instance"
356+ "launchpad.net/juju-core/juju/testing"
357+ "launchpad.net/juju-core/provider"
358+ jc "launchpad.net/juju-core/testing/checkers"
359+)
360+
361+type bootstrapSuite struct {
362+ testing.JujuConnSuite
363+ env *localStorageEnviron
364+}
365+
366+var _ = gc.Suite(&bootstrapSuite{})
367+
368+type localStorageEnviron struct {
369+ environs.Environ
370+ storage storage.Storage
371+ storageAddr string
372+ storageDir string
373+ sharedStorageAddr string
374+ sharedStorageDir string
375+}
376+
377+func (e *localStorageEnviron) BootstrapStorage() (storage.Storage, error) {
378+ return e.storage, nil
379+}
380+
381+func (e *localStorageEnviron) StorageAddr() string {
382+ return e.storageAddr
383+}
384+
385+func (e *localStorageEnviron) StorageDir() string {
386+ return e.storageDir
387+}
388+
389+func (e *localStorageEnviron) SharedStorageAddr() string {
390+ return e.sharedStorageAddr
391+}
392+
393+func (e *localStorageEnviron) SharedStorageDir() string {
394+ return e.sharedStorageDir
395+}
396+
397+func (s *bootstrapSuite) SetUpTest(c *gc.C) {
398+ s.JujuConnSuite.SetUpTest(c)
399+ s.env = &localStorageEnviron{
400+ Environ: s.Conn.Environ,
401+ storageDir: c.MkDir(),
402+ }
403+ storage, err := filestorage.NewFileStorageWriter(s.env.storageDir, filestorage.UseDefaultTmpDir)
404+ c.Assert(err, gc.IsNil)
405+ s.env.storage = storage
406+}
407+
408+func (s *bootstrapSuite) getArgs(c *gc.C) BootstrapArgs {
409+ hostname, err := os.Hostname()
410+ c.Assert(err, gc.IsNil)
411+ toolsList, err := tools.FindBootstrapTools(s.Conn.Environ, tools.BootstrapToolsParams{})
412+ c.Assert(err, gc.IsNil)
413+ return BootstrapArgs{
414+ Host: hostname,
415+ Environ: s.env,
416+ MachineId: "0",
417+ PossibleTools: toolsList,
418+ }
419+}
420+
421+func (s *bootstrapSuite) TestBootstrap(c *gc.C) {
422+ args := s.getArgs(c)
423+ args.Host = "ubuntu@" + args.Host
424+
425+ defer fakeSSH{series: s.Conn.Environ.Config().DefaultSeries()}.install(c).Restore()
426+ err := Bootstrap(args)
427+ c.Assert(err, gc.IsNil)
428+
429+ bootstrapState, err := provider.LoadState(s.env.storage)
430+ c.Assert(err, gc.IsNil)
431+ c.Assert(
432+ bootstrapState.StateInstances,
433+ gc.DeepEquals,
434+ []instance.Id{BootstrapInstanceId},
435+ )
436+
437+ // Do it all again; this should work, despite the fact that
438+ // there's a bootstrap state file. Existence for that is
439+ // checked in general bootstrap code (environs/bootstrap).
440+ defer fakeSSH{series: s.Conn.Environ.Config().DefaultSeries()}.install(c).Restore()
441+ err = Bootstrap(args)
442+ c.Assert(err, gc.IsNil)
443+
444+ // We *do* check that the machine has no juju* upstart jobs, though.
445+ defer installFakeSSH(c, "", "/etc/init/jujud-machine-0.conf", 0).Restore()
446+ err = Bootstrap(args)
447+ c.Assert(err, gc.Equals, ErrProvisioned)
448+}
449+
450+func (s *bootstrapSuite) TestBootstrapScriptFailure(c *gc.C) {
451+ args := s.getArgs(c)
452+ args.Host = "ubuntu@" + args.Host
453+ series := s.Conn.Environ.Config().DefaultSeries()
454+ defer fakeSSH{series: series, provisionAgentExitCode: 1}.install(c).Restore()
455+ err := Bootstrap(args)
456+ c.Assert(err, gc.NotNil)
457+
458+ // Since the script failed, the state file should have been
459+ // removed from storage.
460+ _, err = provider.LoadState(s.env.storage)
461+ c.Assert(err, jc.Satisfies, coreerrors.IsNotBootstrapped)
462+}
463+
464+func (s *bootstrapSuite) TestBootstrapEmptyHost(c *gc.C) {
465+ args := s.getArgs(c)
466+ args.Host = ""
467+ c.Assert(Bootstrap(args), gc.ErrorMatches, "host argument is empty")
468+}
469+
470+func (s *bootstrapSuite) TestBootstrapNilEnviron(c *gc.C) {
471+ args := s.getArgs(c)
472+ args.Environ = nil
473+ c.Assert(Bootstrap(args), gc.ErrorMatches, "environ argument is nil")
474+}
475+
476+func (s *bootstrapSuite) TestBootstrapInvalidMachineId(c *gc.C) {
477+ args := s.getArgs(c)
478+ args.MachineId = ""
479+ c.Assert(Bootstrap(args), gc.ErrorMatches, `"" is not a valid machine ID`)
480+ args.MachineId = "bahhumbug"
481+ c.Assert(Bootstrap(args), gc.ErrorMatches, `"bahhumbug" is not a valid machine ID`)
482+}
483+
484+func (s *bootstrapSuite) TestBootstrapAlternativeMachineId(c *gc.C) {
485+ args := s.getArgs(c)
486+ args.MachineId = "1"
487+ defer fakeSSH{series: s.Conn.Environ.Config().DefaultSeries()}.install(c).Restore()
488+ c.Assert(Bootstrap(args), gc.IsNil)
489+}
490+
491+func (s *bootstrapSuite) TestBootstrapNoMatchingTools(c *gc.C) {
492+ // Empty tools list.
493+ args := s.getArgs(c)
494+ args.PossibleTools = nil
495+ series := s.Conn.Environ.Config().DefaultSeries()
496+ defer fakeSSH{series: series, skipProvisionAgent: true}.install(c).Restore()
497+ c.Assert(Bootstrap(args), gc.ErrorMatches, "no matching tools available")
498+
499+ // Non-empty list, but none that match the series/arch.
500+ defer fakeSSH{series: "edgy", skipProvisionAgent: true}.install(c).Restore()
501+ args = s.getArgs(c)
502+ c.Assert(Bootstrap(args), gc.ErrorMatches, "no matching tools available")
503+}
504
505=== modified file 'environs/manual/detection.go'
506--- environs/manual/detection.go 2013-09-12 02:04:05 +0000
507+++ environs/manual/detection.go 2013-09-20 05:05:14 +0000
508@@ -11,6 +11,7 @@
509 "strings"
510
511 "launchpad.net/juju-core/instance"
512+ "launchpad.net/juju-core/utils"
513 )
514
515 // checkProvisionedScript is the script to run on the remote machine
516@@ -23,9 +24,9 @@
517 // checkProvisioned checks if any juju upstart jobs already
518 // exist on the host machine.
519 func checkProvisioned(sshHost string) (bool, error) {
520- cmd := sshCommand(sshHost, "bash")
521+ logger.Infof("Checking if %s is already provisioned", sshHost)
522+ cmd := sshCommand(sshHost, fmt.Sprintf("bash -c %s", utils.ShQuote(checkProvisionedScript)))
523 var stdout, stderr bytes.Buffer
524- cmd.Stdin = bytes.NewBufferString(checkProvisionedScript)
525 cmd.Stdout = &stdout
526 cmd.Stderr = &stderr
527 if err := cmd.Run(); err != nil {
528@@ -34,13 +35,21 @@
529 }
530 return false, err
531 }
532- return len(strings.TrimSpace(stdout.String())) > 0, nil
533+ output := strings.TrimSpace(stdout.String())
534+ provisioned := len(output) > 0
535+ if provisioned {
536+ logger.Infof("%s is already provisioned [%q]", sshHost, output)
537+ } else {
538+ logger.Infof("%s is not provisioned", sshHost)
539+ }
540+ return provisioned, nil
541 }
542
543 // detectSeriesHardwareCharacteristics detects the OS series
544 // and hardware characteristics of the remote machine by
545 // connecting to the machine and executing a bash script.
546 func detectSeriesAndHardwareCharacteristics(sshHost string) (hc instance.HardwareCharacteristics, series string, err error) {
547+ logger.Infof("Detecting series and characteristics on %s", sshHost)
548 cmd := sshCommand(sshHost, "bash")
549 cmd.Stdin = bytes.NewBufferString(detectionScript)
550 out, err := cmd.CombinedOutput()
551@@ -101,6 +110,7 @@
552 }
553
554 // TODO(axw) calculate CpuPower. What algorithm do we use?
555+ logger.Infof("series: %s, characteristics: %s", series, hc)
556 return hc, series, nil
557 }
558
559@@ -116,6 +126,7 @@
560 }
561
562 const detectionScript = `#!/bin/bash
563+set -e
564 lsb_release -cs
565 uname -m
566 grep MemTotal /proc/meminfo
567
568=== modified file 'environs/manual/detection_test.go'
569--- environs/manual/detection_test.go 2013-09-20 02:53:59 +0000
570+++ environs/manual/detection_test.go 2013-09-20 05:05:14 +0000
571@@ -4,10 +4,6 @@
572 package manual
573
574 import (
575- "fmt"
576- "io/ioutil"
577- "os"
578- "path/filepath"
579 "strings"
580
581 gc "launchpad.net/gocheck"
582@@ -22,58 +18,6 @@
583
584 var _ = gc.Suite(&detectionSuite{})
585
586-// sshscript should only print the result on the first execution,
587-// to handle the case where it's called multiple times. On
588-// subsequent executions, it should find the next 'ssh' in $PATH
589-// and exec that.
590-var sshscript = `#!/bin/bash
591-if [ ! -e "$0.run" ]; then
592- touch "$0.run"
593- diff "$0.expected-input" -
594- exitcode=$?
595- if [ $exitcode -ne 0 ]; then
596- echo "ERROR: did not match expected input" >&2
597- exit $exitcode
598- fi
599- # stdout
600- %s
601- # stderr
602- %s
603- exit %d
604-else
605- export PATH=${PATH#*:}
606- exec ssh $*
607-fi`
608-
609-// sshresponse creates a fake "ssh" command in a new $PATH,
610-// updates $PATH, and returns a function to reset $PATH to
611-// its original value when called.
612-//
613-// output may be:
614-// - nil (no output)
615-// - a string (stdout)
616-// - a slice of strings, of length two (stdout, stderr)
617-func sshresponse(c *gc.C, input string, output interface{}, rc int) func() {
618- fakebin := c.MkDir()
619- ssh := filepath.Join(fakebin, "ssh")
620- sshexpectedinput := ssh + ".expected-input"
621- var stdout, stderr string
622- switch output := output.(type) {
623- case nil:
624- case string:
625- stdout = fmt.Sprintf("cat<<EOF\n%s\nEOF", output)
626- case []string:
627- stdout = fmt.Sprintf("cat<<EOF\n%s\nEOF", output[0])
628- stderr = fmt.Sprintf("cat>&2<<EOF\n%s\nEOF", output[1])
629- }
630- script := fmt.Sprintf(sshscript, stdout, stderr, rc)
631- err := ioutil.WriteFile(ssh, []byte(script), 0777)
632- c.Assert(err, gc.IsNil)
633- err = ioutil.WriteFile(sshexpectedinput, []byte(input), 0644)
634- c.Assert(err, gc.IsNil)
635- return testbase.PatchEnvironment("PATH", fakebin+":"+os.Getenv("PATH"))
636-}
637-
638 func (s *detectionSuite) TestDetectSeries(c *gc.C) {
639 response := strings.Join([]string{
640 "edgy",
641@@ -81,14 +25,14 @@
642 "MemTotal: 4096 kB",
643 "processor: 0",
644 }, "\n")
645- defer sshresponse(c, detectionScript, response, 0)()
646+ defer installFakeSSH(c, detectionScript, response, 0)()
647 _, series, err := detectSeriesAndHardwareCharacteristics("whatever")
648 c.Assert(err, gc.IsNil)
649 c.Assert(series, gc.Equals, "edgy")
650 }
651
652 func (s *detectionSuite) TestDetectionError(c *gc.C) {
653- defer sshresponse(c, detectionScript, "oh noes", 33)()
654+ defer installFakeSSH(c, detectionScript, "oh noes", 33)()
655 _, _, err := detectSeriesAndHardwareCharacteristics("whatever")
656 c.Assert(err, gc.ErrorMatches, "exit status 33 \\(oh noes\\)")
657 }
658@@ -148,7 +92,7 @@
659 for i, test := range tests {
660 c.Logf("test %d: %s", i, test.summary)
661 scriptResponse := strings.Join(test.scriptResponse, "\n")
662- defer sshresponse(c, detectionScript, scriptResponse, 0)()
663+ defer installFakeSSH(c, detectionScript, scriptResponse, 0)()
664 hc, _, err := detectSeriesAndHardwareCharacteristics("hostname")
665 c.Assert(err, gc.IsNil)
666 c.Assert(hc.String(), gc.Equals, test.expectedHc)
667@@ -156,25 +100,25 @@
668 }
669
670 func (s *detectionSuite) TestCheckProvisioned(c *gc.C) {
671- defer sshresponse(c, checkProvisionedScript, "", 0)()
672+ defer installFakeSSH(c, "", "", 0)()
673 provisioned, err := checkProvisioned("example.com")
674 c.Assert(err, gc.IsNil)
675 c.Assert(provisioned, jc.IsFalse)
676
677- defer sshresponse(c, checkProvisionedScript, "non-empty", 0)()
678+ defer installFakeSSH(c, "", "non-empty", 0)()
679 provisioned, err = checkProvisioned("example.com")
680 c.Assert(err, gc.IsNil)
681 c.Assert(provisioned, jc.IsTrue)
682
683 // stderr should not affect result.
684- defer sshresponse(c, checkProvisionedScript, []string{"", "non-empty-stderr"}, 0)()
685+ defer installFakeSSH(c, "", []string{"", "non-empty-stderr"}, 0)()
686 provisioned, err = checkProvisioned("example.com")
687 c.Assert(err, gc.IsNil)
688 c.Assert(provisioned, jc.IsFalse)
689
690 // if the script fails for whatever reason, then checkProvisioned
691 // will return an error. stderr will be included in the error message.
692- defer sshresponse(c, checkProvisionedScript, []string{"non-empty-stdout", "non-empty-stderr"}, 255)()
693+ defer installFakeSSH(c, "", []string{"non-empty-stdout", "non-empty-stderr"}, 255)()
694 _, err = checkProvisioned("example.com")
695 c.Assert(err, gc.ErrorMatches, "exit status 255 \\(non-empty-stderr\\)")
696 }
697
698=== added file 'environs/manual/fakessh_test.go'
699--- environs/manual/fakessh_test.go 1970-01-01 00:00:00 +0000
700+++ environs/manual/fakessh_test.go 2013-09-20 05:05:14 +0000
701@@ -0,0 +1,112 @@
702+// Copyright 2013 Canonical Ltd.
703+// Licensed under the AGPLv3, see LICENCE file for details.
704+
705+package manual
706+
707+import (
708+ "fmt"
709+ "io/ioutil"
710+ "os"
711+ "path/filepath"
712+ "strings"
713+
714+ gc "launchpad.net/gocheck"
715+
716+ "launchpad.net/juju-core/testing/testbase"
717+)
718+
719+// sshscript should only print the result on the first execution,
720+// to handle the case where it's called multiple times. On
721+// subsequent executions, it should find the next 'ssh' in $PATH
722+// and exec that.
723+var sshscript = `#!/bin/bash
724+if [ ! -e "$0.run" ]; then
725+ touch "$0.run"
726+ diff "$0.expected-input" -
727+ exitcode=$?
728+ if [ $exitcode -ne 0 ]; then
729+ echo "ERROR: did not match expected input" >&2
730+ exit $exitcode
731+ fi
732+ # stdout
733+ %s
734+ # stderr
735+ %s
736+ exit %d
737+else
738+ export PATH=${PATH#*:}
739+ exec ssh $*
740+fi`
741+
742+// installFakeSSH creates a fake "ssh" command in a new $PATH,
743+// updates $PATH, and returns a function to reset $PATH to
744+// its original value when called.
745+//
746+// output may be:
747+// - nil (no output)
748+// - a string (stdout)
749+// - a slice of strings, of length two (stdout, stderr)
750+func installFakeSSH(c *gc.C, input string, output interface{}, rc int) testbase.Restorer {
751+ fakebin := c.MkDir()
752+ ssh := filepath.Join(fakebin, "ssh")
753+ sshexpectedinput := ssh + ".expected-input"
754+ var stdout, stderr string
755+ switch output := output.(type) {
756+ case nil:
757+ case string:
758+ stdout = fmt.Sprintf("cat<<EOF\n%s\nEOF", output)
759+ case []string:
760+ stdout = fmt.Sprintf("cat<<EOF\n%s\nEOF", output[0])
761+ stderr = fmt.Sprintf("cat>&2<<EOF\n%s\nEOF", output[1])
762+ }
763+ script := fmt.Sprintf(sshscript, stdout, stderr, rc)
764+ err := ioutil.WriteFile(ssh, []byte(script), 0777)
765+ c.Assert(err, gc.IsNil)
766+ err = ioutil.WriteFile(sshexpectedinput, []byte(input), 0644)
767+ c.Assert(err, gc.IsNil)
768+ return testbase.PatchEnvironment("PATH", fakebin+":"+os.Getenv("PATH"))
769+}
770+
771+// fakeSSH wraps the invocation of installFakeSSH based on the parameters.
772+type fakeSSH struct {
773+ series string
774+ arch string
775+
776+ // exit code for the machine agent provisioning script.
777+ provisionAgentExitCode int
778+
779+ // there are conditions other than error in the above
780+ // that might cause provisioning to not go ahead, such
781+ // as tools being missing.
782+ skipProvisionAgent bool
783+}
784+
785+// install installs fake SSH commands, which will respond to
786+// manual provisioning/bootstrapping commands with the specified
787+// output and exit codes.
788+func (r fakeSSH) install(c *gc.C) testbase.Restorer {
789+ series := r.series
790+ if series == "" {
791+ series = "precise"
792+ }
793+ arch := r.arch
794+ if arch == "" {
795+ arch = "amd64"
796+ }
797+ detectionoutput := strings.Join([]string{
798+ series,
799+ arch,
800+ "MemTotal: 4096 kB",
801+ "processor: 0",
802+ }, "\n")
803+ var restore testbase.Restorer
804+ add := func(input string, output interface{}, rc int) {
805+ restore = restore.Add(installFakeSSH(c, input, output, rc))
806+ }
807+ if !r.skipProvisionAgent {
808+ add("", nil, r.provisionAgentExitCode)
809+ }
810+ add(detectionScript, detectionoutput, 0)
811+ add("", nil, 0) // checkProvisioned
812+ return restore
813+}
814
815=== modified file 'environs/manual/provisioner.go'
816--- environs/manual/provisioner.go 2013-09-18 04:43:04 +0000
817+++ environs/manual/provisioner.go 2013-09-20 05:05:14 +0000
818@@ -104,6 +104,14 @@
819 return nil, err
820 }
821
822+ tools := args.Tools
823+ if tools == nil {
824+ tools, err = findInstanceTools(env, series, *hc.Arch)
825+ if err != nil {
826+ return nil, err
827+ }
828+ }
829+
830 // Generate a unique nonce for the machine.
831 uuid, err := utils.NewUUID()
832 if err != nil {
833@@ -137,16 +145,15 @@
834
835 // Finally, provision the machine agent.
836 err = provisionMachineAgent(provisionMachineAgentArgs{
837- host: args.Host,
838- dataDir: args.DataDir,
839- env: env,
840- machine: m,
841- nonce: nonce,
842- stateInfo: stateInfo,
843- apiInfo: apiInfo,
844- series: series,
845- arch: *hc.Arch,
846- tools: args.Tools,
847+ host: args.Host,
848+ dataDir: args.DataDir,
849+ environConfig: env.Config(),
850+ machineId: m.Id(),
851+ bootstrap: false,
852+ nonce: nonce,
853+ stateInfo: stateInfo,
854+ apiInfo: apiInfo,
855+ tools: tools,
856 })
857 if err != nil {
858 return m, err
859
860=== modified file 'environs/manual/provisioner_test.go'
861--- environs/manual/provisioner_test.go 2013-09-20 00:33:38 +0000
862+++ environs/manual/provisioner_test.go 2013-09-20 05:05:14 +0000
863@@ -6,13 +6,15 @@
864 import (
865 "fmt"
866 "os"
867- "strings"
868
869 gc "launchpad.net/gocheck"
870
871- "launchpad.net/juju-core/environs/tools"
872+ "launchpad.net/juju-core/environs/storage"
873+ envtesting "launchpad.net/juju-core/environs/testing"
874 "launchpad.net/juju-core/instance"
875 "launchpad.net/juju-core/juju/testing"
876+ jc "launchpad.net/juju-core/testing/checkers"
877+ "launchpad.net/juju-core/version"
878 )
879
880 type provisionerSuite struct {
881@@ -31,32 +33,35 @@
882 }
883
884 func (s *provisionerSuite) TestProvisionMachine(c *gc.C) {
885- // Prepare a mock ssh response for the detection phase.
886- detectionoutput := strings.Join([]string{
887- "edgy",
888- "armv4",
889- "MemTotal: 4096 kB",
890- "processor: 0",
891- }, "\n")
892+ const series = "precise"
893+ const arch = "amd64"
894
895 args := s.getArgs(c)
896 hostname := args.Host
897 args.Host = "ubuntu@" + args.Host
898
899- defer sshresponse(c, detectionScript, detectionoutput, 0)()
900- defer sshresponse(c, checkProvisionedScript, "", 0)()
901+ envtesting.RemoveTools(c, s.Conn.Environ.Storage())
902+ envtesting.RemoveTools(c, s.Conn.Environ.PublicStorage().(storage.Storage))
903+ defer fakeSSH{
904+ series: series, arch: arch, skipProvisionAgent: true,
905+ }.install(c).Restore()
906 m, err := ProvisionMachine(args)
907- c.Assert(err, gc.ErrorMatches, "no matching tools available")
908+ c.Assert(err, gc.ErrorMatches, "no tools available")
909 c.Assert(m, gc.IsNil)
910
911- toolsList, err := tools.FindBootstrapTools(s.Conn.Environ, tools.BootstrapToolsParams{})
912- c.Assert(err, gc.IsNil)
913- args.Tools = toolsList[0]
914+ cfg := s.Conn.Environ.Config()
915+ number, ok := cfg.AgentVersion()
916+ c.Assert(ok, jc.IsTrue)
917+ binVersion := version.Binary{number, series, arch}
918+ envtesting.UploadFakeToolsVersion(c, s.Conn.Environ.Storage(), binVersion)
919
920- for _, errorCode := range []int{255, 0} {
921- defer sshresponse(c, "", "", errorCode)() // executing script
922- defer sshresponse(c, detectionScript, detectionoutput, 0)()
923- defer sshresponse(c, checkProvisionedScript, "", 0)()
924+ for i, errorCode := range []int{255, 0} {
925+ c.Logf("test %d: code %d", i, errorCode)
926+ defer fakeSSH{
927+ series: series,
928+ arch: arch,
929+ provisionAgentExitCode: errorCode,
930+ }.install(c).Restore()
931 m, err = ProvisionMachine(args)
932 if errorCode != 0 {
933 c.Assert(err, gc.ErrorMatches, fmt.Sprintf("exit status %d", errorCode))
934@@ -64,9 +69,9 @@
935 } else {
936 c.Assert(err, gc.IsNil)
937 c.Assert(m, gc.NotNil)
938- // machine ID will be 2, not 1. Even though we failed and the
939+ // machine ID will be incremented. Even though we failed and the
940 // machine is removed, the ID is not reused.
941- c.Assert(m.Id(), gc.Equals, "2")
942+ c.Assert(m.Id(), gc.Equals, fmt.Sprint(i))
943 instanceId, err := m.InstanceId()
944 c.Assert(err, gc.IsNil)
945 c.Assert(instanceId, gc.Equals, instance.Id("manual:"+hostname))
946@@ -75,10 +80,10 @@
947
948 // Attempting to provision a machine twice should fail. We effect
949 // this by checking for existing juju upstart configurations.
950- defer sshresponse(c, checkProvisionedScript, "/etc/init/jujud-machine-0.conf", 0)()
951+ defer installFakeSSH(c, "", "/etc/init/jujud-machine-0.conf", 0)()
952 _, err = ProvisionMachine(args)
953 c.Assert(err, gc.Equals, ErrProvisioned)
954- defer sshresponse(c, checkProvisionedScript, "/etc/init/jujud-machine-0.conf", 255)()
955+ defer installFakeSSH(c, "", "/etc/init/jujud-machine-0.conf", 255)()
956 _, err = ProvisionMachine(args)
957 c.Assert(err, gc.ErrorMatches, "error checking if provisioned: exit status 255")
958 }
959
960=== added file 'environs/manual/tools.go'
961--- environs/manual/tools.go 1970-01-01 00:00:00 +0000
962+++ environs/manual/tools.go 2013-09-20 05:05:14 +0000
963@@ -0,0 +1,24 @@
964+// Copyright 2013 Canonical Ltd.
965+// Licensed under the AGPLv3, see LICENCE file for details.
966+
967+package manual
968+
969+import (
970+ "fmt"
971+
972+ "launchpad.net/juju-core/environs"
973+ envtools "launchpad.net/juju-core/environs/tools"
974+ "launchpad.net/juju-core/tools"
975+)
976+
977+func findInstanceTools(env environs.Environ, series, arch string) (*tools.Tools, error) {
978+ agentVersion, ok := env.Config().AgentVersion()
979+ if !ok {
980+ return nil, fmt.Errorf("no agent version set in environment configuration")
981+ }
982+ possibleTools, err := envtools.FindInstanceTools(env, agentVersion, series, &arch)
983+ if err != nil {
984+ return nil, err
985+ }
986+ return possibleTools[0], nil
987+}
988
989=== modified file 'testing/testbase/patch.go'
990--- testing/testbase/patch.go 2013-09-20 02:10:46 +0000
991+++ testing/testbase/patch.go 2013-09-20 05:05:14 +0000
992@@ -12,6 +12,18 @@
993 // to restore some previous state.
994 type Restorer func()
995
996+// Add returns a Restorer that restores first f1
997+// and then f. It is valid to call this on a nil
998+// Restorer.
999+func (f Restorer) Add(f1 Restorer) Restorer {
1000+ return func() {
1001+ f1.Restore()
1002+ if f != nil {
1003+ f.Restore()
1004+ }
1005+ }
1006+}
1007+
1008 // Restore restores some previous state.
1009 func (r Restorer) Restore() {
1010 r()
1011
1012=== modified file 'testing/testbase/patch_test.go'
1013--- testing/testbase/patch_test.go 2013-09-20 02:10:46 +0000
1014+++ testing/testbase/patch_test.go 2013-09-20 05:05:14 +0000
1015@@ -74,3 +74,12 @@
1016 c.Check(os.Getenv(envName), gc.Equals, "initial")
1017 os.Setenv(envName, oldValue)
1018 }
1019+
1020+func (*PatchEnvironmentSuite) TestRestorerAdd(c *gc.C) {
1021+ var order []string
1022+ first := testbase.Restorer(func() { order = append(order, "first") })
1023+ second := testbase.Restorer(func() { order = append(order, "second") })
1024+ restore := first.Add(second)
1025+ restore()
1026+ c.Assert(order, gc.DeepEquals, []string{"second", "first"})
1027+}
1028
1029=== renamed directory 'provider/local/storage' => 'worker/localstorage'
1030=== added file 'worker/localstorage/config.go'
1031--- worker/localstorage/config.go 1970-01-01 00:00:00 +0000
1032+++ worker/localstorage/config.go 2013-09-20 05:05:14 +0000
1033@@ -0,0 +1,14 @@
1034+// Copyright 2013 Canonical Ltd.
1035+// Licensed under the AGPLv3, see LICENCE file for details.
1036+
1037+package localstorage
1038+
1039+// LocalStorageConfig is an interface that, if implemented, may be used
1040+// to configure a machine agent for use with the localstorage worker in
1041+// this package.
1042+type LocalStorageConfig interface {
1043+ StorageDir() string
1044+ StorageAddr() string
1045+ SharedStorageDir() string
1046+ SharedStorageAddr() string
1047+}
1048
1049=== modified file 'worker/localstorage/worker.go'
1050--- provider/local/storage/worker.go 2013-09-19 03:14:56 +0000
1051+++ worker/localstorage/worker.go 2013-09-20 05:05:14 +0000
1052@@ -1,4 +1,7 @@
1053-package storage
1054+// Copyright 2013 Canonical Ltd.
1055+// Licensed under the AGPLv3, see LICENCE file for details.
1056+
1057+package localstorage
1058
1059 import (
1060 "launchpad.net/loggo"
1061@@ -10,7 +13,7 @@
1062 "launchpad.net/juju-core/worker"
1063 )
1064
1065-var logger = loggo.GetLogger("juju.local.storage")
1066+var logger = loggo.GetLogger("juju.worker.localstorage")
1067
1068 type storageWorker struct {
1069 config agent.Config
1070@@ -55,19 +58,22 @@
1071
1072 sharedStorageDir := s.config.Value(agent.SharedStorageDir)
1073 sharedStorageAddr := s.config.Value(agent.SharedStorageAddr)
1074- logger.Infof("serving %s on %s", sharedStorageDir, sharedStorageAddr)
1075-
1076- sharedStorage, err := filestorage.NewFileStorageWriter(sharedStorageDir, filestorage.UseDefaultTmpDir)
1077- if err != nil {
1078- logger.Errorf("error with local storage: %v", err)
1079- return err
1080- }
1081- sharedStorageListener, err := httpstorage.Serve(sharedStorageAddr, sharedStorage)
1082- if err != nil {
1083- logger.Errorf("error with local storage: %v", err)
1084- return err
1085- }
1086- defer sharedStorageListener.Close()
1087+ if sharedStorageAddr != "" && sharedStorageDir != "" {
1088+ logger.Infof("serving %s on %s", sharedStorageDir, sharedStorageAddr)
1089+ sharedStorage, err := filestorage.NewFileStorageWriter(sharedStorageDir, filestorage.UseDefaultTmpDir)
1090+ if err != nil {
1091+ logger.Errorf("error with local storage: %v", err)
1092+ return err
1093+ }
1094+ sharedStorageListener, err := httpstorage.Serve(sharedStorageAddr, sharedStorage)
1095+ if err != nil {
1096+ logger.Errorf("error with local storage: %v", err)
1097+ return err
1098+ }
1099+ defer sharedStorageListener.Close()
1100+ } else {
1101+ logger.Infof("no shared storage: dir=%q addr=%q", sharedStorageDir, sharedStorageAddr)
1102+ }
1103
1104 logger.Infof("storage routines started, awaiting death")
1105

Subscribers

People subscribed via source and target branches

to status/vote changes: