Merge lp:~axwalk/juju-core/azure-provider-roleisinstance 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: 2554
Proposed branch: lp:~axwalk/juju-core/azure-provider-roleisinstance
Merge into: lp:~go-bot/juju-core/trunk
Diff against target: 2709 lines (+1124/-918)
5 files modified
dependencies.tsv (+1/-1)
provider/azure/environ.go (+420/-176)
provider/azure/environ_test.go (+321/-241)
provider/azure/instance.go (+133/-146)
provider/azure/instance_test.go (+249/-354)
To merge this branch: bzr merge lp:~axwalk/juju-core/azure-provider-roleisinstance
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+210353@code.launchpad.net

Commit message

provider/azure: enable load-balancing/availability

This change is required to enable Availability Set
support for Azure. The existing implementation was
fundamentally incompatible with Availability Sets,
so the implementation has been significantly altered,
while maintaining backwards compatibility.

In the old implementation, instances are associated
one-to-one with Cloud Services. In the new
implementation, instances are associated with Roles
within a Cloud Service; there may be multiple Roles
within a single Cloud Service.

If a service is exposed, then its ports will be load-
balanced across each instance within the cloud service
which opens that port. We currently open ports 22 for
all instances, and the Mongo and API server ports for
state server instances. State server instances are
allocated to a single Cloud Service.

There are a number of incidental improvements, e.g.
using an updated version of DeleteHostedService which
destroys associated disks/blobs server side.

All instances are now allocated to an Availability Set
called "juju", which is scoped to the Cloud Service.
We do not currently group units into Cloud Services
automatically, however the ground work is laid to do
this. A follow-up will implement this work.

When we enable grouping, the same time we will have to
make changes to how we connect to machines via SSH,
either by proxying through an arbitrary machine in the
Cloud Service, or by not load balancing port 22 and
connecting to the machine's auto-allocated public port.

https://codereview.appspot.com/73910043/

Description of the change

provider/azure: enable load-balancing/availability

This change is required to enable Availability Set
support for Azure. The existing implementation was
fundamentally incompatible with Availability Sets,
so the implementation has been significantly altered,
while maintaining backwards compatibility.

In the old implementation, instances are associated
one-to-one with Cloud Services. In the new
implementation, instances are associated with Roles
within a Cloud Service; there may be multiple Roles
within a single Cloud Service.

If a service is exposed, then its ports will be load-
balanced across each instance within the cloud service
which opens that port. We currently open ports 22 for
all instances, and the Mongo and API server ports for
state server instances. State server instances are
allocated to a single Cloud Service.

There are a number of incidental improvements, e.g.
using an updated version of DeleteHostedService which
destroys associated disks/blobs server side.

All instances are now allocated to an Availability Set
called "juju", which is scoped to the Cloud Service.
We do not currently group units into Cloud Services
automatically, however the ground work is laid to do
this. A follow-up will implement this work.

When we enable grouping, the same time we will have to
make changes to how we connect to machines via SSH,
either by proxying through an arbitrary machine in the
Cloud Service, or by not load balancing port 22 and
connecting to the machine's auto-allocated public port.

https://codereview.appspot.com/73910043/

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

Reviewers: mp+210353_code.launchpad.net,

Message:
Please take a look.

Description:
provider/azure: enable load-balancing/availability

This change is required to enable Availability Set
support for Azure. The existing implementation was
fundamentally incompatible with Availability Sets,
so the implementation has been significantly altered,
while maintaining backwards compatibility.

In the old implementation, instances are associated
one-to-one with Cloud Services. In the new
implementation, instances are associated with Roles
within a Cloud Service; there may be multiple Roles
within a single Cloud Service.

If a service is exposed, then its ports will be load-
balanced across each instance within the cloud service
which opens that port. We currently open ports 22 for
all instances, and the Mongo and API server ports for
state server instances. State server instances are
allocated to a single Cloud Service.

There are a number of incidental improvements, e.g.
using an updated version of DeleteHostedService which
destroys associated disks/blobs server side.

All instances are now allocated to an Availability Set
called "juju", which is scoped to the Cloud Service.
We do not currently group units into Cloud Services
automatically, however the ground work is laid to do
this. A follow-up will implement this work.

When we enable grouping, the same time we will have to
make changes to how we connect to machines via SSH,
either by proxying through an arbitrary machine in the
Cloud Service, or by not load balancing port 22 and
connecting to the machine's auto-allocated public port.

https://code.launchpad.net/~axwalk/juju-core/azure-provider-roleisinstance/+merge/210353

(do not edit description out of merge proposal)

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

Affected files (+1090, -888 lines):
   A [revision details]
   M dependencies.tsv
   M provider/azure/environ.go
   M provider/azure/environ_test.go
   M provider/azure/instance.go
   M provider/azure/instance_test.go

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

Lots of changes here, comments below.

I'm hoping that CI for Azure would help catch any actual problems here
as it is a bit beyond a simple review :-)

https://codereview.appspot.com/73910043/diff/20001/provider/azure/environ.go
File provider/azure/environ.go (right):

https://codereview.appspot.com/73910043/diff/20001/provider/azure/environ.go#newcode232
provider/azure/environ.go:232: if err, ok := err.(*gwacl.AzureError); ok
{
oh... didn't know you could do that...

reassigning to err I mean...

https://codereview.appspot.com/73910043/diff/20001/provider/azure/environ.go#newcode443
provider/azure/environ.go:443: func (env *azureEnviron) createRole(azure
*gwacl.ManagementAPI, role *gwacl.Role, label string) (resultInst
instance.Instance, resultErr error) {
it isn't entirely clear why 'createRole' returns an instance

https://codereview.appspot.com/73910043/diff/20001/provider/azure/environ.go#newcode589
provider/azure/environ.go:589: case
deploymentNameV2(hostedService.ServiceName):
perhaps a blank before this line? wrapping lines makes it harder to pick
out.

https://codereview.appspot.com/73910043/diff/20001/provider/azure/environ.go#newcode636
provider/azure/environ.go:636: // don't load balance SSH and provie a
way of getting the
s/provie/provide/

https://codereview.appspot.com/73910043/diff/20001/provider/azure/environ.go#newcode732
provider/azure/environ.go:732: } else if len(service.Deployments) != 1 {
why skip services where we have more than one deployment?

https://codereview.appspot.com/73910043/diff/20001/provider/azure/environ_test.go
File provider/azure/environ_test.go (right):

https://codereview.appspot.com/73910043/diff/20001/provider/azure/environ_test.go#newcode978
provider/azure/environ_test.go:978: c.Check(inst1, gc.FitsTypeOf,
&azureInstance{})
Hmm... didn't know about this checker, but perhaps you should make that
one an Assert not a Check, because if it fails, the next line will
panic.

https://codereview.appspot.com/73910043/diff/20001/provider/azure/instance.go
File provider/azure/instance.go (right):

https://codereview.appspot.com/73910043/diff/20001/provider/azure/instance.go#newcode95
provider/azure/instance.go:95:
azInstance.environ.getVirtualNetworkName(),
Which parts of the *azureInstance are being protected by the mutex?

https://codereview.appspot.com/73910043/diff/20001/provider/azure/instance_test.go
File provider/azure/instance_test.go (right):

https://codereview.appspot.com/73910043/diff/20001/provider/azure/instance_test.go#newcode18
provider/azure/instance_test.go:18: type instanceSuite struct {
Normally at least want to include the testbase.LoggingSuite to capture
the logging.

https://codereview.appspot.com/73910043/

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

Please take a look.

https://codereview.appspot.com/73910043/diff/20001/provider/azure/environ.go
File provider/azure/environ.go (right):

https://codereview.appspot.com/73910043/diff/20001/provider/azure/environ.go#newcode443
provider/azure/environ.go:443: func (env *azureEnviron) createRole(azure
*gwacl.ManagementAPI, role *gwacl.Role, label string) (resultInst
instance.Instance, resultErr error) {
On 2014/03/13 04:15:05, thumper wrote:
> it isn't entirely clear why 'createRole' returns an instance

Renamed to instance. Instance and role are equivalent, but I'll keep it
to Juju terminology.

https://codereview.appspot.com/73910043/diff/20001/provider/azure/environ.go#newcode589
provider/azure/environ.go:589: case
deploymentNameV2(hostedService.ServiceName):
On 2014/03/13 04:15:05, thumper wrote:
> perhaps a blank before this line? wrapping lines makes it harder to
pick out.

Done.

https://codereview.appspot.com/73910043/diff/20001/provider/azure/environ.go#newcode636
provider/azure/environ.go:636: // don't load balance SSH and provie a
way of getting the
On 2014/03/13 04:15:05, thumper wrote:
> s/provie/provide/

Done.

https://codereview.appspot.com/73910043/diff/20001/provider/azure/environ.go#newcode732
provider/azure/environ.go:732: } else if len(service.Deployments) != 1 {
On 2014/03/13 04:15:05, thumper wrote:
> why skip services where we have more than one deployment?

Because if there's more than one, that means someone's been mucking
about with the objects. Juju only creates one deployment per Cloud
Service. You can have up to two (Prod & Dev), but we only do Production.

https://codereview.appspot.com/73910043/diff/20001/provider/azure/environ_test.go
File provider/azure/environ_test.go (right):

https://codereview.appspot.com/73910043/diff/20001/provider/azure/environ_test.go#newcode978
provider/azure/environ_test.go:978: c.Check(inst1, gc.FitsTypeOf,
&azureInstance{})
On 2014/03/13 04:15:05, thumper wrote:
> Hmm... didn't know about this checker, but perhaps you should make
that one an
> Assert not a Check, because if it fails, the next line will panic.

Done.

https://codereview.appspot.com/73910043/diff/20001/provider/azure/instance.go
File provider/azure/instance.go (right):

https://codereview.appspot.com/73910043/diff/20001/provider/azure/instance.go#newcode95
provider/azure/instance.go:95:
azInstance.environ.getVirtualNetworkName(),
On 2014/03/13 04:15:05, thumper wrote:
> Which parts of the *azureInstance are being protected by the mutex?

Only roleInstance (I've been told the convention is to put those fields
that are guarded below the mutex). All others are immutable and
thread-safe.

https://codereview.appspot.com/73910043/diff/20001/provider/azure/instance_test.go
File provider/azure/instance_test.go (right):

https://codereview.appspot.com/73910043/diff/20001/provider/azure/instance_test.go#newcode18
provider/azure/instance_test.go:18: type instanceSuite struct {
On 2014/03/13 04:15:05, thumper wrote:
> Normally at least want to include the testbase.LoggingSuite to capture
the
> logging.

Done.

https://codereview.appspot.com/73910043/

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

pre-comment for discussion

https://codereview.appspot.com/73910043/diff/40001/provider/azure/environ.go
File provider/azure/environ.go (right):

https://codereview.appspot.com/73910043/diff/40001/provider/azure/environ.go#newcode554
provider/azure/environ.go:554: if machineConfig.StateServer {
oooh, this is going to break sometime. HA state server nodes will not
necessarily be started with this field set. How much will we depend upon
this? If it's just cosmetic it's maybe not such a big deal, but really I
think it's up to juju itself to supply a sensible label to be applied if
possible.

https://codereview.appspot.com/73910043/diff/40001/provider/azure/environ.go#newcode566
provider/azure/environ.go:566: role := env.newRole(instanceType, vhd,
userData, machineConfig.StateServer)
heh, this is the same problem, but more significant. We'll have to start
distinguishing between StateServer and BootstrapMachine or something in
MachineConfig -- StateServer wouldn't affect cloudinit (because the MA
will turn itself into a stateserver when it connects) but can be handled
in this case, while Bootstrap will be the thing that controls the
special...

wait. This is more complicated than I thought. Would you ping me when
you're back from supper please?

https://codereview.appspot.com/73910043/

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

LGTM with some thoughts and suggestions below.

https://codereview.appspot.com/73910043/diff/40001/provider/azure/environ.go
File provider/azure/environ.go (right):

https://codereview.appspot.com/73910043/diff/40001/provider/azure/environ.go#newcode407
provider/azure/environ.go:407: labelBase64 :=
base64.StdEncoding.EncodeToString([]byte(label))
Shouldn't gwacl be doing this decoding, given that it's doing the
encoding itself? (it seems a bit odd that base-64 encoding is required -
the azure docs distinctly show a non-base64-encoded example)

https://codereview.appspot.com/73910043/diff/40001/provider/azure/environ.go#newcode443
provider/azure/environ.go:443: // createInstance creates all of the
necessary Azure entities in order
// createInstance creates all of the Azure entities
// necessary for a new instance. ...

?

https://codereview.appspot.com/73910043/diff/40001/provider/azure/environ.go#newcode554
provider/azure/environ.go:554: if machineConfig.StateServer {
On 2014/03/19 09:54:08, fwereade wrote:
> oooh, this is going to break sometime. HA state server nodes will not
> necessarily be started with this field set. How much will we depend
upon this?
> If it's just cosmetic it's maybe not such a big deal, but really I
think it's up
> to juju itself to supply a sensible label to be applied if possible.

I believe that it's not possible to assign a machine to an availability
set after it's been started, so unless this field *is* set at instance
start time, there's nothing we can do.

Perhaps it *might* make sense to add a "group" (or something similarly
named) field in MachineConfig. Then all the logic for deciding which
machines are grouped together can be outside of the provider logic (the
providers can remain blissfully ignorant of juju services); different
providers might decide for themselves how to interpret the group
(ignoring it would be acceptable).

https://codereview.appspot.com/73910043/diff/40001/provider/azure/instance.go
File provider/azure/instance.go (right):

https://codereview.appspot.com/73910043/diff/40001/provider/azure/instance.go#newcode29
provider/azure/instance.go:29: mu sync.RWMutex
is an RWMutex really worth it here? i'd just use a mutex - it's smaller,
and space is a more likely problem than time in this case, i'd say.

https://codereview.appspot.com/73910043/diff/40001/provider/azure/instance.go#newcode93
provider/azure/instance.go:93: ip,
could we name the fields here please?

https://codereview.appspot.com/73910043/diff/40001/provider/azure/instance.go#newcode96
provider/azure/instance.go:96: instance.NetworkCloudLocal})
trivial formatting: i think we usually put the } on a new line, so that
all the fields look the same.

https://codereview.appspot.com/73910043/diff/40001/provider/azure/instance.go#newcode98
provider/azure/instance.go:98: } else {
s/} else {/}/

(save a level of indentation)

https://codereview.appspot.com/73910043/

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

https://codereview.appspot.com/73910043/diff/40001/provider/azure/environ.go
File provider/azure/environ.go (right):

https://codereview.appspot.com/73910043/diff/40001/provider/azure/environ.go#newcode554
provider/azure/environ.go:554: if machineConfig.StateServer {
On 2014/03/19 09:54:08, fwereade wrote:
> oooh, this is going to break sometime. HA state server nodes will not
> necessarily be started with this field set. How much will we depend
upon this?
> If it's just cosmetic it's maybe not such a big deal, but really I
think it's up
> to juju itself to supply a sensible label to be applied if possible.

Based on discussion on IRC, I will change this to use
DistributionInstances from the other CL. DistributionInstances will
return state server instances if the machine being provisioned is a
state server, otherwise it will return common service instances.

The whole label thing will just disappear, since the cloud service can
be extracted from the instance ID.

https://codereview.appspot.com/73910043/

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

WIP for DistributionInstances

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

Oops, never did mail this.

https://codereview.appspot.com/73910043/diff/40001/provider/azure/environ.go
File provider/azure/environ.go (right):

https://codereview.appspot.com/73910043/diff/40001/provider/azure/environ.go#newcode407
provider/azure/environ.go:407: labelBase64 :=
base64.StdEncoding.EncodeToString([]byte(label))
On 2014/03/19 11:44:35, rog wrote:
> Shouldn't gwacl be doing this decoding, given that it's doing the
encoding
> itself? (it seems a bit odd that base-64 encoding is required - the
azure docs
> distinctly show a non-base64-encoded example)

I agree, the base64 encoding/decoding is a bit clumsy and leaky. I don't
think we'll be using the labels much longer, though.

https://codereview.appspot.com/73910043/diff/40001/provider/azure/environ.go#newcode443
provider/azure/environ.go:443: // createInstance creates all of the
necessary Azure entities in order
On 2014/03/19 11:44:35, rog wrote:
> // createInstance creates all of the Azure entities
> // necessary for a new instance. ...

> ?

Done.

https://codereview.appspot.com/73910043/diff/40001/provider/azure/instance.go
File provider/azure/instance.go (right):

https://codereview.appspot.com/73910043/diff/40001/provider/azure/instance.go#newcode29
provider/azure/instance.go:29: mu sync.RWMutex
On 2014/03/19 11:44:35, rog wrote:
> is an RWMutex really worth it here? i'd just use a mutex - it's
smaller, and
> space is a more likely problem than time in this case, i'd say.

Done.

https://codereview.appspot.com/73910043/diff/40001/provider/azure/instance.go#newcode93
provider/azure/instance.go:93: ip,
On 2014/03/19 11:44:35, rog wrote:
> could we name the fields here please?

Done.

https://codereview.appspot.com/73910043/diff/40001/provider/azure/instance.go#newcode96
provider/azure/instance.go:96: instance.NetworkCloudLocal})
On 2014/03/19 11:44:35, rog wrote:
> trivial formatting: i think we usually put the } on a new line, so
that all the
> fields look the same.

Yeah, this is existing code, I usually do that too. I've reformatted it.

https://codereview.appspot.com/73910043/diff/40001/provider/azure/instance.go#newcode98
provider/azure/instance.go:98: } else {
On 2014/03/19 11:44:35, rog wrote:
> s/} else {/}/

> (save a level of indentation)

Done.

https://codereview.appspot.com/73910043/

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

Thanks, this is great. LGTM.

https://codereview.appspot.com/73910043/diff/60001/provider/azure/instance.go
File provider/azure/instance.go (right):

https://codereview.appspot.com/73910043/diff/60001/provider/azure/instance.go#newcode56
provider/azure/instance.go:56: // TODO(axw) map instance status to
something more useful?
It's considered ok for status to use the provider's vocabulary.

https://codereview.appspot.com/73910043/diff/60001/provider/azure/instance.go#newcode188
provider/azure/instance.go:188: // concern.
This bothers me a bit, but it feels more like an azure problem than our
problem.

https://codereview.appspot.com/73910043/

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

Please take a look.

https://codereview.appspot.com/73910043/diff/60001/provider/azure/instance.go
File provider/azure/instance.go (right):

https://codereview.appspot.com/73910043/diff/60001/provider/azure/instance.go#newcode56
provider/azure/instance.go:56: // TODO(axw) map instance status to
something more useful?
On 2014/04/03 07:31:15, fwereade wrote:
> It's considered ok for status to use the provider's vocabulary.

Removed.

https://codereview.appspot.com/73910043/

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

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

"/home/tarmac/trees/src/launchpad.net/gwacl" now at tarmac-20140312041035-ac7gw7kcqjx7db63
ok launchpad.net/juju-core 0.014s
ok launchpad.net/juju-core/agent 1.177s
ok launchpad.net/juju-core/agent/mongo 0.507s
ok launchpad.net/juju-core/agent/tools 0.194s
ok launchpad.net/juju-core/bzr 4.868s
ok launchpad.net/juju-core/cert 2.236s
ok launchpad.net/juju-core/charm 0.378s
? launchpad.net/juju-core/charm/hooks [no test files]
? launchpad.net/juju-core/charm/testing [no test files]
ok launchpad.net/juju-core/cloudinit 0.028s
ok launchpad.net/juju-core/cloudinit/sshinit 0.876s
ok launchpad.net/juju-core/cmd 0.168s
ok launchpad.net/juju-core/cmd/charm-admin 0.750s
? launchpad.net/juju-core/cmd/charmd [no test files]
? launchpad.net/juju-core/cmd/charmload [no test files]
ok launchpad.net/juju-core/cmd/envcmd 0.173s
ok launchpad.net/juju-core/cmd/juju 211.216s
ok launchpad.net/juju-core/cmd/jujud 68.049s
ok launchpad.net/juju-core/cmd/plugins/juju-metadata 8.855s
? launchpad.net/juju-core/cmd/plugins/juju-restore [no test files]
ok launchpad.net/juju-core/cmd/plugins/local 0.218s
? launchpad.net/juju-core/cmd/plugins/local/juju-local [no test files]
ok launchpad.net/juju-core/constraints 0.022s
ok launchpad.net/juju-core/container 0.042s
ok launchpad.net/juju-core/container/factory 0.035s
ok launchpad.net/juju-core/container/kvm 0.200s
ok launchpad.net/juju-core/container/kvm/mock 0.038s
? launchpad.net/juju-core/container/kvm/testing [no test files]
ok launchpad.net/juju-core/container/lxc 4.282s
? launchpad.net/juju-core/container/lxc/mock [no test files]
? launchpad.net/juju-core/container/lxc/testing [no test files]
? launchpad.net/juju-core/container/testing [no test files]
ok launchpad.net/juju-core/downloader 5.257s
ok launchpad.net/juju-core/environs 2.523s
ok launchpad.net/juju-core/environs/bootstrap 10.527s
ok launchpad.net/juju-core/environs/cloudinit 0.517s
ok launchpad.net/juju-core/environs/config 1.844s
ok launchpad.net/juju-core/environs/configstore 0.041s
ok launchpad.net/juju-core/environs/filestorage 0.031s
ok launchpad.net/juju-core/environs/httpstorage 0.601s
ok launchpad.net/juju-core/environs/imagemetadata 0.445s
? launchpad.net/juju-core/environs/imagemetadata/testing [no test files]
ok launchpad.net/juju-core/environs/instances 0.037s
ok launchpad.net/juju-core/environs/jujutest 0.173s
ok launchpad.net/juju-core/environs/manual 8.851s
ok launchpad.net/juju-core/environs/simplestreams 0.280s
? launchpad.net/juju-core/environs/simplestreams/testing [no test files]
ok launchpad.net/juju-core/environs/sshstorage 0.769s
ok launchpad.net/juju-core/environs/storage 0.766s
ok launchpad.net/juju-core/environs/sync 43.665s
ok launchpad.net/juju-core/environs/testing 0.141s
ok launchpad.net/juju-core/environs/tools 4.729s
? launchpad.net/juju-core/environs/tools/testing [no test files]
ok launchpad.net/juju-core/errors 0.011s
ok launchpad.net/juju-core/instance 0.019s
? ...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'dependencies.tsv'
2--- dependencies.tsv 2014-04-02 11:35:49 +0000
3+++ dependencies.tsv 2014-04-03 07:35:44 +0000
4@@ -16,6 +16,6 @@
5 launchpad.net/gomaasapi bzr martin.packman@canonical.com-20140401145548-bcmsw2gic5e576ns 48
6 launchpad.net/goose bzr tarmac-20140124165235-h9rloooc531udms5 116
7 launchpad.net/goyaml bzr gustavo@niemeyer.net-20131114120802-abe042syx64z2m7s 50
8-launchpad.net/gwacl bzr tarmac-20140205045433-81h182mhz24fzp5e 231
9+launchpad.net/gwacl bzr tarmac-20140312041035-ac7gw7kcqjx7db63 234
10 launchpad.net/lpad bzr gustavo@niemeyer.net-20120626194701-536yx0g9jdq2ik3h 64
11 launchpad.net/tomb bzr gustavo@niemeyer.net-20130531003818-70ikdgklbxopn8x4 17
12
13=== modified file 'provider/azure/environ.go'
14--- provider/azure/environ.go 2014-04-03 03:37:45 +0000
15+++ provider/azure/environ.go 2014-04-03 07:35:44 +0000
16@@ -4,8 +4,11 @@
17 package azure
18
19 import (
20+ "encoding/base64"
21 "fmt"
22 "net/http"
23+ "regexp"
24+ "strings"
25 "sync"
26 "time"
27
28@@ -23,16 +26,12 @@
29 "launchpad.net/juju-core/provider/common"
30 "launchpad.net/juju-core/state"
31 "launchpad.net/juju-core/state/api"
32+ "launchpad.net/juju-core/state/api/params"
33 "launchpad.net/juju-core/utils"
34- "launchpad.net/juju-core/utils/parallel"
35+ "launchpad.net/juju-core/utils/set"
36 )
37
38 const (
39- // In our initial implementation, each instance gets its own hosted
40- // service, deployment and role in Azure. The role always gets this
41- // hostname (instance==service).
42- roleHostname = "default"
43-
44 // deploymentSlot says in which slot to deploy instances. Azure
45 // supports 'Production' or 'Staging'.
46 // This provider always deploys to Production. Think twice about
47@@ -48,6 +47,10 @@
48 // environement, in CIDR notation. This is the network used for
49 // machine-to-machine communication.
50 networkDefinition = "10.0.0.0/8"
51+
52+ // stateServerLabel is the label applied to the cloud service created
53+ // for state servers.
54+ stateServerLabel = "juju-state-server"
55 )
56
57 type azureEnviron struct {
58@@ -210,14 +213,40 @@
59 return azure.AddVirtualNetworkSite(&virtualNetwork)
60 }
61
62+// deleteVnetAttempt is an AttemptyStrategy for use
63+// when attempting delete a virtual network. This is
64+// necessary as Azure apparently does not release all
65+// references to the vnet even when all cloud services
66+// are deleted.
67+var deleteVnetAttempt = utils.AttemptStrategy{
68+ Total: 30 * time.Second,
69+ Delay: 1 * time.Second,
70+}
71+
72+var networkInUse = regexp.MustCompile(".*The virtual network .* is currently in use.*")
73+
74 func (env *azureEnviron) deleteVirtualNetwork() error {
75 azure, err := env.getManagementAPI()
76 if err != nil {
77 return err
78 }
79 defer env.releaseManagementAPI(azure)
80- vnetName := env.getVirtualNetworkName()
81- return azure.RemoveVirtualNetworkSite(vnetName)
82+ for a := deleteVnetAttempt.Start(); a.Next(); {
83+ vnetName := env.getVirtualNetworkName()
84+ err = azure.RemoveVirtualNetworkSite(vnetName)
85+ if err == nil {
86+ return nil
87+ }
88+ if err, ok := err.(*gwacl.AzureError); ok {
89+ if err.StatusCode() == 400 && networkInUse.MatchString(err.Message) {
90+ // Retry on "virtual network XYZ is currently in use".
91+ continue
92+ }
93+ }
94+ // Any other error should be returned.
95+ break
96+ }
97+ return err
98 }
99
100 // getContainerName returns the name of the private storage account container
101@@ -294,7 +323,7 @@
102 // name it chooses (based on the given prefix), but recognizes that the name
103 // may not be available. If the name is not available, it does not treat that
104 // as an error but just returns nil.
105-func attemptCreateService(azure *gwacl.ManagementAPI, prefix string, affinityGroupName string, location string) (*gwacl.CreateHostedService, error) {
106+func attemptCreateService(azure *gwacl.ManagementAPI, prefix, affinityGroupName, label string) (*gwacl.CreateHostedService, error) {
107 var err error
108 name := gwacl.MakeRandomHostedServiceName(prefix)
109 err = azure.CheckHostedServiceNameAvailability(name)
110@@ -302,7 +331,10 @@
111 // The calling function should retry.
112 return nil, nil
113 }
114- req := gwacl.NewCreateHostedServiceWithLocation(name, name, location)
115+ if label == "" {
116+ label = name
117+ }
118+ req := gwacl.NewCreateHostedServiceWithLocation(name, label, "")
119 req.AffinityGroup = affinityGroupName
120 err = azure.AddHostedService(req)
121 if err != nil {
122@@ -313,19 +345,19 @@
123
124 // newHostedService creates a hosted service. It will make up a unique name,
125 // starting with the given prefix.
126-func newHostedService(azure *gwacl.ManagementAPI, prefix string, affinityGroupName string, location string) (*gwacl.CreateHostedService, error) {
127+func newHostedService(azure *gwacl.ManagementAPI, prefix, affinityGroupName, label string) (*gwacl.HostedService, error) {
128 var err error
129- var svc *gwacl.CreateHostedService
130- for tries := 10; tries > 0 && err == nil && svc == nil; tries-- {
131- svc, err = attemptCreateService(azure, prefix, affinityGroupName, location)
132+ var createdService *gwacl.CreateHostedService
133+ for tries := 10; tries > 0 && err == nil && createdService == nil; tries-- {
134+ createdService, err = attemptCreateService(azure, prefix, affinityGroupName, label)
135 }
136 if err != nil {
137 return nil, fmt.Errorf("could not create hosted service: %v", err)
138 }
139- if svc == nil {
140+ if createdService == nil {
141 return nil, fmt.Errorf("could not come up with a unique hosted service name - is your randomizer initialized?")
142 }
143- return svc, nil
144+ return azure.GetHostedServiceProperties(createdService.ServiceName, true)
145 }
146
147 // SupportedArchitectures is specified on the EnvironCapability interface.
148@@ -385,6 +417,90 @@
149 return spec.InstanceType.Id, spec.Image.Id, nil
150 }
151
152+// createInstance creates all of the Azure entities necessary for a
153+// new instance. This includes Cloud Service, Deployment and Role.
154+//
155+// If serviceName is non-empty, then createInstance will assign to
156+// the Cloud Service with that name. Otherwise, a new Cloud Service
157+// will be created.
158+func (env *azureEnviron) createInstance(azure *gwacl.ManagementAPI, role *gwacl.Role, serviceName string, stateServer bool) (resultInst instance.Instance, resultErr error) {
159+ var inst instance.Instance
160+ defer func() {
161+ if inst != nil && resultErr != nil {
162+ if err := env.StopInstances([]instance.Instance{inst}); err != nil {
163+ // Failure upon failure. Log it, but return the original error.
164+ logger.Errorf("error releasing failed instance: %v", err)
165+ }
166+ }
167+ }()
168+ var err error
169+ var service *gwacl.HostedService
170+ if serviceName != "" {
171+ service, err = azure.GetHostedServiceProperties(serviceName, true)
172+ } else {
173+ // If we're creating a cloud service for state servers,
174+ // we will want to open additional ports. We need to
175+ // record this against the cloud service, so we use a
176+ // special label for the purpose.
177+ var label string
178+ if stateServer {
179+ label = stateServerLabel
180+ }
181+ service, err = newHostedService(azure, env.getEnvPrefix(), env.getAffinityGroupName(), label)
182+ }
183+ if err != nil {
184+ return nil, err
185+ }
186+ if len(service.Deployments) == 0 {
187+ // This is a newly created cloud service, so we
188+ // should destroy it if anything below fails.
189+ defer func() {
190+ if resultErr != nil {
191+ azure.DeleteHostedService(service.ServiceName)
192+ // Destroying the hosted service destroys the instance,
193+ // so ensure StopInstances isn't called.
194+ inst = nil
195+ }
196+ }()
197+ // Create an initial deployment.
198+ deployment := gwacl.NewDeploymentForCreateVMDeployment(
199+ deploymentNameV2(service.ServiceName),
200+ deploymentSlot,
201+ deploymentNameV2(service.ServiceName),
202+ []gwacl.Role{*role},
203+ env.getVirtualNetworkName(),
204+ )
205+ if err := azure.AddDeployment(deployment, service.ServiceName); err != nil {
206+ return nil, err
207+ }
208+ service.Deployments = append(service.Deployments, *deployment)
209+ } else {
210+ // Update the deployment.
211+ deployment := &service.Deployments[0]
212+ if err := azure.AddRole(&gwacl.AddRoleRequest{
213+ ServiceName: service.ServiceName,
214+ DeploymentName: deployment.Name,
215+ PersistentVMRole: (*gwacl.PersistentVMRole)(role),
216+ }); err != nil {
217+ return nil, err
218+ }
219+ deployment.RoleList = append(deployment.RoleList, *role)
220+ }
221+ return env.getInstance(service, role.RoleName)
222+}
223+
224+// deploymentNameV1 returns the deployment name used
225+// in the original implementation of the Azure provider.
226+func deploymentNameV1(serviceName string) string {
227+ return serviceName
228+}
229+
230+// deploymentNameV2 returns the deployment name used
231+// in the current implementation of the Azure provider.
232+func deploymentNameV2(serviceName string) string {
233+ return serviceName + "-v2"
234+}
235+
236 // StartInstance is specified in the InstanceBroker interface.
237 func (env *azureEnviron) StartInstance(args environs.StartInstanceParams) (_ instance.Instance, _ *instance.HardwareCharacteristics, err error) {
238
239@@ -417,24 +533,7 @@
240 }
241 defer env.releaseManagementAPI(azure)
242
243- snap := env.getSnapshot()
244- location := snap.ecfg.location()
245- service, err := newHostedService(azure.ManagementAPI, env.getEnvPrefix(), env.getAffinityGroupName(), location)
246- if err != nil {
247- return nil, nil, err
248- }
249- serviceName := service.ServiceName
250-
251- // If we fail after this point, clean up the hosted service.
252- defer func() {
253- if err != nil {
254- azure.DestroyHostedService(
255- &gwacl.DestroyHostedServiceRequest{
256- ServiceName: serviceName,
257- })
258- }
259- }()
260-
261+ location := env.getSnapshot().ecfg.location()
262 instanceType, sourceImageName, err := env.selectInstanceTypeAndImage(&instances.InstanceConstraint{
263 Region: location,
264 Series: args.Tools.OneSeries(),
265@@ -445,42 +544,38 @@
266 return nil, nil, err
267 }
268
269- // virtualNetworkName is the virtual network to which all the
270- // deployments in this environment belong.
271- virtualNetworkName := env.getVirtualNetworkName()
272+ // We use the cloud service label as a way to group instances with
273+ // the same affinity, so that machines can be be allocated to the
274+ // same availability set.
275+ var cloudServiceName string
276+ // TODO(axw) replace "false &&" with mode check once
277+ // availability-sets-enabled change is landed.
278+ if false && args.DistributionGroup != nil {
279+ instanceIds, err := args.DistributionGroup()
280+ if err != nil {
281+ return nil, nil, err
282+ }
283+ for _, id := range instanceIds {
284+ cloudServiceName, _ = env.splitInstanceId(id)
285+ if cloudServiceName != "" {
286+ break
287+ }
288+ }
289+ logger.Debugf("using existing cloud service: %q", cloudServiceName)
290+ }
291
292- // 1. Create an OS Disk.
293 vhd := env.newOSDisk(sourceImageName)
294-
295- // 2. Create a Role for a Linux machine.
296- role := env.newRole(instanceType, vhd, userData, roleHostname)
297-
298- // 3. Create the Deployment object.
299- deployment := env.newDeployment(role, serviceName, serviceName, virtualNetworkName)
300-
301- err = azure.AddDeployment(deployment, serviceName)
302- if err != nil {
303- return nil, nil, err
304- }
305-
306- var inst instance.Instance
307-
308- // From here on, remember to shut down the instance before returning
309- // any error.
310- defer func() {
311- if err != nil && inst != nil {
312- err2 := env.StopInstances([]instance.Instance{inst})
313- if err2 != nil {
314- // Failure upon failure. Log it, but return
315- // the original error.
316- logger.Errorf("error releasing failed instance: %v", err)
317- }
318+ // If we're creating machine-0, we'll want to expose port 22.
319+ // All other machines get an auto-generated public port for SSH.
320+ stateServer := false
321+ for _, job := range args.MachineConfig.Jobs {
322+ if job == params.JobManageEnviron {
323+ stateServer = true
324+ break
325 }
326- }()
327-
328- // Assign the returned instance to 'inst' so that the deferred method
329- // above can perform its check.
330- inst, err = env.getInstance(serviceName)
331+ }
332+ role := env.newRole(instanceType, vhd, userData, stateServer)
333+ inst, err := env.createInstance(azure.ManagementAPI, role, cloudServiceName, stateServer)
334 if err != nil {
335 return nil, nil, err
336 }
337@@ -490,17 +585,52 @@
338
339 // getInstance returns an up-to-date version of the instance with the given
340 // name.
341-func (env *azureEnviron) getInstance(instanceName string) (instance.Instance, error) {
342- context, err := env.getManagementAPI()
343- if err != nil {
344- return nil, err
345- }
346- defer env.releaseManagementAPI(context)
347- service, err := context.GetHostedServiceProperties(instanceName, false)
348- if err != nil {
349- return nil, fmt.Errorf("could not get instance %q: %v", instanceName, err)
350- }
351- instance := &azureInstance{service.HostedServiceDescriptor, env}
352+func (env *azureEnviron) getInstance(hostedService *gwacl.HostedService, roleName string) (instance.Instance, error) {
353+ if n := len(hostedService.Deployments); n != 1 {
354+ return nil, fmt.Errorf("expected one deployment for %q, got %d", hostedService.ServiceName, n)
355+ }
356+ deployment := &hostedService.Deployments[0]
357+
358+ var maskStateServerPorts bool
359+ var instanceId instance.Id
360+ switch deployment.Name {
361+ case deploymentNameV1(hostedService.ServiceName):
362+ // Old style instance.
363+ instanceId = instance.Id(hostedService.ServiceName)
364+ if n := len(deployment.RoleList); n != 1 {
365+ return nil, fmt.Errorf("expected one role for %q, got %d", deployment.Name, n)
366+ }
367+ roleName = deployment.RoleList[0].RoleName
368+ // In the old implementation of the Azure provider,
369+ // all machines opened the state and API server ports.
370+ maskStateServerPorts = true
371+
372+ case deploymentNameV2(hostedService.ServiceName):
373+ instanceId = instance.Id(fmt.Sprintf("%s-%s", hostedService.ServiceName, roleName))
374+ // Newly created state server machines are put into
375+ // the cloud service with the stateServerLabel label.
376+ if decoded, err := base64.StdEncoding.DecodeString(hostedService.Label); err == nil {
377+ maskStateServerPorts = string(decoded) == stateServerLabel
378+ }
379+ }
380+
381+ var roleInstance *gwacl.RoleInstance
382+ for _, role := range deployment.RoleInstanceList {
383+ if role.RoleName == roleName {
384+ roleInstance = &role
385+ break
386+ }
387+ }
388+
389+ instance := &azureInstance{
390+ environ: env,
391+ hostedService: &hostedService.HostedServiceDescriptor,
392+ instanceId: instanceId,
393+ deploymentName: deployment.Name,
394+ roleName: roleName,
395+ roleInstance: roleInstance,
396+ maskStateServerPorts: maskStateServerPorts,
397+ }
398 return instance, nil
399 }
400
401@@ -519,29 +649,40 @@
402
403 // getInitialEndpoints returns a slice of the endpoints every instance should have open
404 // (ssh port, etc).
405-func (env *azureEnviron) getInitialEndpoints() []gwacl.InputEndpoint {
406+func (env *azureEnviron) getInitialEndpoints(stateServer bool) []gwacl.InputEndpoint {
407+ // TODO(axw) either proxy ssh traffic through one of the
408+ // randomly chosen VMs to the internal address, or otherwise
409+ // don't load balance SSH and provide a way of getting the
410+ // local port.
411 cfg := env.Config()
412- return []gwacl.InputEndpoint{
413- {
414- LocalPort: 22,
415- Name: "sshport",
416- Port: 22,
417- Protocol: "tcp",
418- },
419- // TODO: Ought to have this only for state servers.
420- {
421+ endpoints := []gwacl.InputEndpoint{{
422+ LocalPort: 22,
423+ Name: "sshport",
424+ Port: 22,
425+ Protocol: "tcp",
426+ }}
427+ if stateServer {
428+ endpoints = append(endpoints, []gwacl.InputEndpoint{{
429 LocalPort: cfg.StatePort(),
430+ Port: cfg.StatePort(),
431+ Protocol: "tcp",
432 Name: "stateport",
433- Port: cfg.StatePort(),
434- Protocol: "tcp",
435- },
436- // TODO: Ought to have this only for API servers.
437- {
438+ }, {
439 LocalPort: cfg.APIPort(),
440+ Port: cfg.APIPort(),
441+ Protocol: "tcp",
442 Name: "apiport",
443- Port: cfg.APIPort(),
444- Protocol: "tcp",
445- }}
446+ }}...)
447+ }
448+ for i, endpoint := range endpoints {
449+ endpoint.LoadBalancedEndpointSetName = endpoint.Name
450+ endpoint.LoadBalancerProbe = &gwacl.LoadBalancerProbe{
451+ Port: endpoint.Port,
452+ Protocol: "TCP",
453+ }
454+ endpoints[i] = endpoint
455+ }
456+ return endpoints
457 }
458
459 // newRole creates a gwacl.Role object (an Azure Virtual Machine) which uses
460@@ -550,101 +691,204 @@
461 // The VM will have:
462 // - an 'ubuntu' user defined with an unguessable (randomly generated) password
463 // - its ssh port (TCP 22) open
464+// (if a state server)
465 // - its state port (TCP mongoDB) port open
466 // - its API port (TCP) open
467 //
468 // roleSize is the name of one of Azure's machine types, e.g. ExtraSmall,
469 // Large, A6 etc.
470-func (env *azureEnviron) newRole(roleSize string, vhd *gwacl.OSVirtualHardDisk, userData string, roleHostname string) *gwacl.Role {
471+func (env *azureEnviron) newRole(roleSize string, vhd *gwacl.OSVirtualHardDisk, userData string, stateServer bool) *gwacl.Role {
472+ roleName := gwacl.MakeRandomRoleName("juju")
473 // Create a Linux Configuration with the username and the password
474 // empty and disable SSH with password authentication.
475- hostname := roleHostname
476+ hostname := roleName
477 username := "ubuntu"
478 password := gwacl.MakeRandomPassword()
479 linuxConfigurationSet := gwacl.NewLinuxProvisioningConfigurationSet(hostname, username, password, userData, "true")
480- // Generate a Network Configuration with the initially required ports
481- // open.
482- networkConfigurationSet := gwacl.NewNetworkConfigurationSet(env.getInitialEndpoints(), nil)
483- roleName := gwacl.MakeRandomRoleName("juju")
484- // The ordering of these configuration sets is significant for the tests.
485- return gwacl.NewRole(
486- roleSize, roleName,
487+ // Generate a Network Configuration with the initially required ports open.
488+ networkConfigurationSet := gwacl.NewNetworkConfigurationSet(env.getInitialEndpoints(stateServer), nil)
489+ role := gwacl.NewRole(
490+ roleSize, roleName, vhd,
491 []gwacl.ConfigurationSet{*linuxConfigurationSet, *networkConfigurationSet},
492- []gwacl.OSVirtualHardDisk{*vhd})
493-}
494-
495-// newDeployment creates and returns a gwacl Deployment object.
496-func (env *azureEnviron) newDeployment(role *gwacl.Role, deploymentName string, deploymentLabel string, virtualNetworkName string) *gwacl.Deployment {
497- // Use the service name as the label for the deployment.
498- return gwacl.NewDeploymentForCreateVMDeployment(deploymentName, deploymentSlot, deploymentLabel, []gwacl.Role{*role}, virtualNetworkName)
499-}
500-
501-// Spawn this many goroutines to issue requests for destroying services.
502-// TODO: this is currently set to 1 because of a problem in Azure:
503-// removing Services in the same affinity group concurrently causes a conflict.
504-// This conflict is wrongly reported by Azure as a BadRequest (400).
505-// This has been reported to Windows Azure.
506-var maxConcurrentDeletes = 1
507+ )
508+ role.AvailabilitySetName = "juju"
509+ return role
510+}
511
512 // StartInstance is specified in the InstanceBroker interface.
513 func (env *azureEnviron) StopInstances(instances []instance.Instance) error {
514- // Each Juju instance is an Azure Service (instance==service), destroy
515- // all the Azure services.
516- // Acquire management API object.
517 context, err := env.getManagementAPI()
518 if err != nil {
519 return err
520 }
521 defer env.releaseManagementAPI(context)
522
523- // Destroy all the services in parallel.
524- run := parallel.NewRun(maxConcurrentDeletes)
525+ // Map services to role names we want to delete.
526+ serviceInstances := make(map[string]map[string]bool)
527 for _, instance := range instances {
528- serviceName := string(instance.Id())
529- run.Do(func() error {
530- request := &gwacl.DestroyHostedServiceRequest{ServiceName: serviceName}
531- return context.DestroyHostedService(request)
532- })
533- }
534- return run.Wait()
535+ instance, ok := instance.(*azureInstance)
536+ if !ok {
537+ continue
538+ }
539+ serviceName := instance.hostedService.ServiceName
540+ deleteRoleNames, ok := serviceInstances[serviceName]
541+ if !ok {
542+ deleteRoleNames = make(map[string]bool)
543+ serviceInstances[serviceName] = deleteRoleNames
544+ }
545+ deleteRoleNames[instance.roleName] = true
546+ }
547+
548+ // Load the properties of each service, so we know whether to
549+ // delete the entire service.
550+ //
551+ // Note: concurrent operations on Affinity Groups have been
552+ // found to cause conflict responses, so we do everything serially.
553+ for serviceName, deleteRoleNames := range serviceInstances {
554+ service, err := context.GetHostedServiceProperties(serviceName, true)
555+ if err != nil {
556+ return err
557+ } else if len(service.Deployments) != 1 {
558+ continue
559+ }
560+ // Filter the instances that have no corresponding role.
561+ var roleNames set.Strings
562+ for _, role := range service.Deployments[0].RoleList {
563+ roleNames.Add(role.RoleName)
564+ }
565+ for roleName := range deleteRoleNames {
566+ if !roleNames.Contains(roleName) {
567+ delete(deleteRoleNames, roleName)
568+ }
569+ }
570+ // If we're deleting all the roles, we need to delete the
571+ // entire cloud service or we'll get an error.
572+ if len(deleteRoleNames) == roleNames.Size() {
573+ if err := context.DeleteHostedService(serviceName); err != nil {
574+ return err
575+ }
576+ } else {
577+ for roleName := range deleteRoleNames {
578+ if err := context.DeleteRole(&gwacl.DeleteRoleRequest{
579+ ServiceName: serviceName,
580+ DeploymentName: service.Deployments[0].Name,
581+ RoleName: roleName,
582+ DeleteMedia: true,
583+ }); err != nil {
584+ return err
585+ }
586+ }
587+ }
588+ }
589+ return nil
590+}
591+
592+// destroyAllServices destroys all Cloud Services and deployments contained.
593+// This is needed to clean up broken environments, in which there are cloud
594+// services with no deployments.
595+func (env *azureEnviron) destroyAllServices() error {
596+ context, err := env.getManagementAPI()
597+ if err != nil {
598+ return err
599+ }
600+ defer env.releaseManagementAPI(context)
601+
602+ request := &gwacl.ListPrefixedHostedServicesRequest{ServiceNamePrefix: env.getEnvPrefix()}
603+ services, err := context.ListPrefixedHostedServices(request)
604+ if err != nil {
605+ return err
606+ }
607+ for _, service := range services {
608+ if err := context.DeleteHostedService(service.ServiceName); err != nil {
609+ return err
610+ }
611+ }
612+ return nil
613+}
614+
615+// splitInstanceId splits the specified instance.Id into its
616+// cloud-service and role parts. Both values will be empty
617+// if the instance-id is non-matching, and role will be empty
618+// for legacy instance-ids.
619+func (env *azureEnviron) splitInstanceId(id instance.Id) (service, role string) {
620+ prefix := env.getEnvPrefix()
621+ if !strings.HasPrefix(string(id), prefix) {
622+ return "", ""
623+ }
624+ fields := strings.Split(string(id)[len(prefix):], "-")
625+ service = prefix + fields[0]
626+ if len(fields) > 1 {
627+ role = fields[1]
628+ }
629+ return service, role
630 }
631
632 // Instances is specified in the Environ interface.
633 func (env *azureEnviron) Instances(ids []instance.Id) ([]instance.Instance, error) {
634- // The instance list is built using the list of all the relevant
635- // Azure Services (instance==service).
636- // Acquire management API object.
637 context, err := env.getManagementAPI()
638 if err != nil {
639 return nil, err
640 }
641 defer env.releaseManagementAPI(context)
642
643- // Prepare gwacl request object.
644- serviceNames := make([]string, len(ids))
645+ type instanceId struct {
646+ serviceName, roleName string
647+ }
648+
649+ instancesIds := make([]instanceId, len(ids))
650+ var serviceNames set.Strings
651 for i, id := range ids {
652- serviceNames[i] = string(id)
653+ serviceName, roleName := env.splitInstanceId(id)
654+ if serviceName == "" {
655+ continue
656+ }
657+ instancesIds[i] = instanceId{
658+ serviceName: serviceName,
659+ roleName: roleName,
660+ }
661+ serviceNames.Add(serviceName)
662 }
663- request := &gwacl.ListSpecificHostedServicesRequest{ServiceNames: serviceNames}
664
665- // Issue 'ListSpecificHostedServices' request with gwacl.
666- services, err := context.ListSpecificHostedServices(request)
667+ // Map service names to gwacl.HostedServices.
668+ services, err := context.ListSpecificHostedServices(&gwacl.ListSpecificHostedServicesRequest{
669+ ServiceNames: serviceNames.Values(),
670+ })
671 if err != nil {
672 return nil, err
673 }
674-
675- // If no instances were found, return ErrNoInstances.
676 if len(services) == 0 {
677 return nil, environs.ErrNoInstances
678 }
679-
680- instances := convertToInstances(services, env)
681-
682- // Check if we got a partial result.
683- if len(ids) != len(instances) {
684- return instances, environs.ErrPartialInstances
685- }
686- return instances, nil
687+ hostedServices := make(map[string]*gwacl.HostedService)
688+ for _, s := range services {
689+ hostedService, err := context.GetHostedServiceProperties(s.ServiceName, true)
690+ if err != nil {
691+ return nil, err
692+ }
693+ hostedServices[s.ServiceName] = hostedService
694+ }
695+
696+ err = nil
697+ instances := make([]instance.Instance, len(ids))
698+ for i, id := range instancesIds {
699+ if id.serviceName == "" {
700+ // Previously determined to be an invalid instance ID.
701+ continue
702+ }
703+ hostedService := hostedServices[id.serviceName]
704+ instance, err := env.getInstance(hostedService, id.roleName)
705+ if err == nil {
706+ instances[i] = instance
707+ } else {
708+ logger.Debugf("failed to get instance for role %q in service %q: %v", id.roleName, hostedService.ServiceName, err)
709+ }
710+ }
711+ for _, instance := range instances {
712+ if instance == nil {
713+ err = environs.ErrPartialInstances
714+ }
715+ }
716+ return instances, err
717 }
718
719 // AllInstances is specified in the InstanceBroker interface.
720@@ -659,11 +903,29 @@
721 defer env.releaseManagementAPI(context)
722
723 request := &gwacl.ListPrefixedHostedServicesRequest{ServiceNamePrefix: env.getEnvPrefix()}
724- services, err := context.ListPrefixedHostedServices(request)
725+ serviceDescriptors, err := context.ListPrefixedHostedServices(request)
726 if err != nil {
727 return nil, err
728 }
729- return convertToInstances(services, env), nil
730+
731+ var instances []instance.Instance
732+ for _, sd := range serviceDescriptors {
733+ hostedService, err := context.GetHostedServiceProperties(sd.ServiceName, true)
734+ if err != nil {
735+ return nil, err
736+ } else if len(hostedService.Deployments) != 1 {
737+ continue
738+ }
739+ deployment := &hostedService.Deployments[0]
740+ for _, role := range deployment.RoleList {
741+ instance, err := env.getInstance(hostedService, role.RoleName)
742+ if err != nil {
743+ return nil, err
744+ }
745+ instances = append(instances, instance)
746+ }
747+ }
748+ return instances, nil
749 }
750
751 // getEnvPrefix returns the prefix used to name the objects specific to this
752@@ -672,16 +934,6 @@
753 return fmt.Sprintf("juju-%s-", env.Name())
754 }
755
756-// convertToInstances converts a slice of gwacl.HostedServiceDescriptor objects
757-// into a slice of instance.Instance objects.
758-func convertToInstances(services []gwacl.HostedServiceDescriptor, env *azureEnviron) []instance.Instance {
759- instances := make([]instance.Instance, len(services))
760- for i, service := range services {
761- instances[i] = &azureInstance{service, env}
762- }
763- return instances
764-}
765-
766 // Storage is specified in the Environ interface.
767 func (env *azureEnviron) Storage() storage.Storage {
768 return env.getSnapshot().storage
769@@ -692,22 +944,15 @@
770 logger.Debugf("destroying environment %q", env.name)
771
772 // Stop all instances.
773- insts, err := env.AllInstances()
774- if err != nil {
775- return fmt.Errorf("cannot get instances: %v", err)
776- }
777- err = env.StopInstances(insts)
778- if err != nil {
779- return fmt.Errorf("cannot stop instances: %v", err)
780+ if err := env.destroyAllServices(); err != nil {
781+ return fmt.Errorf("cannot destroy instances: %v", err)
782 }
783
784 // Delete vnet and affinity group.
785- err = env.deleteVirtualNetwork()
786- if err != nil {
787+ if err := env.deleteVirtualNetwork(); err != nil {
788 return fmt.Errorf("cannot delete the environment's virtual network: %v", err)
789 }
790- err = env.deleteAffinityGroup()
791- if err != nil {
792+ if err := env.deleteAffinityGroup(); err != nil {
793 return fmt.Errorf("cannot delete the environment's affinity group: %v", err)
794 }
795
796@@ -716,8 +961,7 @@
797 // half way through the Destroy() method, the storage won't be cleaned
798 // up and thus an attempt to re-boostrap the environment will lead to
799 // a "error: environment is already bootstrapped" error.
800- err = env.Storage().RemoveAll()
801- if err != nil {
802+ if err := env.Storage().RemoveAll(); err != nil {
803 return fmt.Errorf("cannot clean up storage: %v", err)
804 }
805 return nil
806
807=== modified file 'provider/azure/environ_test.go'
808--- provider/azure/environ_test.go 2014-03-28 13:27:45 +0000
809+++ provider/azure/environ_test.go 2014-04-03 07:35:44 +0000
810@@ -144,7 +144,7 @@
811 // The real test is that this does not panic.
812 }
813
814-func getAzureServiceListResponse(c *gc.C, services []gwacl.HostedServiceDescriptor) []gwacl.DispatcherResponse {
815+func getAzureServiceListResponse(c *gc.C, services ...gwacl.HostedServiceDescriptor) []gwacl.DispatcherResponse {
816 list := gwacl.HostedServiceDescriptorList{HostedServices: services}
817 listXML, err := list.Serialize()
818 c.Assert(err, gc.IsNil)
819@@ -156,22 +156,34 @@
820 return responses
821 }
822
823-// getAzureServiceResponses returns the slice of responses
824-// (gwacl.DispatcherResponse) which correspond to the API requests used to
825-// get the properties of a Service.
826-func getAzureServiceResponses(c *gc.C, service gwacl.HostedService) []gwacl.DispatcherResponse {
827+// getAzureServiceResponses returns a gwacl.DispatcherResponse corresponding
828+// to the API request used to get the properties of a Service.
829+func getAzureServiceResponse(c *gc.C, service gwacl.HostedService) gwacl.DispatcherResponse {
830 serviceXML, err := service.Serialize()
831 c.Assert(err, gc.IsNil)
832- responses := []gwacl.DispatcherResponse{gwacl.NewDispatcherResponse(
833- []byte(serviceXML),
834- http.StatusOK,
835- nil,
836- )}
837- return responses
838+ return gwacl.NewDispatcherResponse([]byte(serviceXML), http.StatusOK, nil)
839 }
840
841 func patchWithServiceListResponse(c *gc.C, services []gwacl.HostedServiceDescriptor) *[]*gwacl.X509Request {
842- responses := getAzureServiceListResponse(c, services)
843+ responses := getAzureServiceListResponse(c, services...)
844+ return gwacl.PatchManagementAPIResponses(responses)
845+}
846+
847+func patchInstancesResponses(c *gc.C, prefix string, services ...*gwacl.HostedService) *[]*gwacl.X509Request {
848+ descriptors := make([]gwacl.HostedServiceDescriptor, len(services))
849+ for i, service := range services {
850+ descriptors[i] = service.HostedServiceDescriptor
851+ }
852+ responses := getAzureServiceListResponse(c, descriptors...)
853+ for _, service := range services {
854+ if !strings.HasPrefix(service.ServiceName, prefix) {
855+ continue
856+ }
857+ serviceXML, err := service.Serialize()
858+ c.Assert(err, gc.IsNil)
859+ serviceGetResponse := gwacl.NewDispatcherResponse([]byte(serviceXML), http.StatusOK, nil)
860+ responses = append(responses, serviceGetResponse)
861+ }
862 return gwacl.PatchManagementAPIResponses(responses)
863 }
864
865@@ -201,25 +213,34 @@
866 func (suite *environSuite) TestAllInstances(c *gc.C) {
867 env := makeEnviron(c)
868 prefix := env.getEnvPrefix()
869- services := []gwacl.HostedServiceDescriptor{{ServiceName: "deployment-in-another-env"}, {ServiceName: prefix + "deployment-1"}, {ServiceName: prefix + "deployment-2"}}
870- requests := patchWithServiceListResponse(c, services)
871+ service1 := makeLegacyDeployment(env, prefix+"service1")
872+ service2 := makeDeployment(env, prefix+"service2")
873+ service3 := makeDeployment(env, "not"+prefix+"service3")
874+
875+ requests := patchInstancesResponses(c, prefix, service1, service2, service3)
876 instances, err := env.AllInstances()
877 c.Assert(err, gc.IsNil)
878- c.Check(len(instances), gc.Equals, 2)
879- c.Check(instances[0].Id(), gc.Equals, instance.Id(prefix+"deployment-1"))
880- c.Check(instances[1].Id(), gc.Equals, instance.Id(prefix+"deployment-2"))
881- c.Check(len(*requests), gc.Equals, 1)
882+ c.Check(len(instances), gc.Equals, 3)
883+ c.Check(instances[0].Id(), gc.Equals, instance.Id(prefix+"service1"))
884+ service2Role1Name := service2.Deployments[0].RoleList[0].RoleName
885+ service2Role2Name := service2.Deployments[0].RoleList[1].RoleName
886+ c.Check(instances[1].Id(), gc.Equals, instance.Id(prefix+"service2-"+service2Role1Name))
887+ c.Check(instances[2].Id(), gc.Equals, instance.Id(prefix+"service2-"+service2Role2Name))
888+ c.Check(len(*requests), gc.Equals, 3)
889 }
890
891 func (suite *environSuite) TestInstancesReturnsFilteredList(c *gc.C) {
892- services := []gwacl.HostedServiceDescriptor{{ServiceName: "deployment-1"}, {ServiceName: "deployment-2"}}
893- requests := patchWithServiceListResponse(c, services)
894 env := makeEnviron(c)
895- instances, err := env.Instances([]instance.Id{"deployment-1"})
896+ prefix := env.getEnvPrefix()
897+ service := makeDeployment(env, prefix+"service")
898+ requests := patchInstancesResponses(c, prefix, service)
899+ role1Name := service.Deployments[0].RoleList[0].RoleName
900+ instId := instance.Id(prefix + "service-" + role1Name)
901+ instances, err := env.Instances([]instance.Id{instId})
902 c.Assert(err, gc.IsNil)
903 c.Check(len(instances), gc.Equals, 1)
904- c.Check(instances[0].Id(), gc.Equals, instance.Id("deployment-1"))
905- c.Check(len(*requests), gc.Equals, 1)
906+ c.Check(instances[0].Id(), gc.Equals, instId)
907+ c.Check(len(*requests), gc.Equals, 2)
908 }
909
910 func (suite *environSuite) TestInstancesReturnsErrNoInstancesIfNoInstancesRequested(c *gc.C) {
911@@ -241,14 +262,22 @@
912 }
913
914 func (suite *environSuite) TestInstancesReturnsPartialInstancesIfSomeInstancesAreNotFound(c *gc.C) {
915- services := []gwacl.HostedServiceDescriptor{{ServiceName: "deployment-1"}, {ServiceName: "deployment-2"}}
916- requests := patchWithServiceListResponse(c, services)
917 env := makeEnviron(c)
918- instances, err := env.Instances([]instance.Id{"deployment-1", "unknown-deployment"})
919+ prefix := env.getEnvPrefix()
920+ service := makeDeployment(env, prefix+"service")
921+
922+ role1Name := service.Deployments[0].RoleList[0].RoleName
923+ role2Name := service.Deployments[0].RoleList[1].RoleName
924+ inst1Id := instance.Id(prefix + "service-" + role1Name)
925+ inst2Id := instance.Id(prefix + "service-" + role2Name)
926+ patchInstancesResponses(c, prefix, service)
927+
928+ instances, err := env.Instances([]instance.Id{inst1Id, "unknown", inst2Id})
929 c.Assert(err, gc.Equals, environs.ErrPartialInstances)
930- c.Check(len(instances), gc.Equals, 1)
931- c.Check(instances[0].Id(), gc.Equals, instance.Id("deployment-1"))
932- c.Check(len(*requests), gc.Equals, 1)
933+ c.Check(len(instances), gc.Equals, 3)
934+ c.Check(instances[0].Id(), gc.Equals, inst1Id)
935+ c.Check(instances[1], gc.IsNil)
936+ c.Check(instances[2].Id(), gc.Equals, inst2Id)
937 }
938
939 func (*environSuite) TestStorage(c *gc.C) {
940@@ -470,22 +499,23 @@
941 }
942
943 func (s *environSuite) TestStateInfo(c *gc.C) {
944- instanceID := "my-instance"
945- patchWithServiceListResponse(c, []gwacl.HostedServiceDescriptor{{
946- ServiceName: instanceID,
947- }})
948 env := makeEnviron(c)
949 s.setDummyStorage(c, env)
950+ prefix := env.getEnvPrefix()
951+
952+ service := makeDeployment(env, prefix+"myservice")
953+ instId := instance.Id(service.ServiceName + "-" + service.Deployments[0].RoleList[0].RoleName)
954+ patchInstancesResponses(c, prefix, service)
955 err := bootstrap.SaveState(
956 env.Storage(),
957- &bootstrap.BootstrapState{StateInstances: []instance.Id{instance.Id(instanceID)}})
958+ &bootstrap.BootstrapState{StateInstances: []instance.Id{instId}},
959+ )
960 c.Assert(err, gc.IsNil)
961
962 stateInfo, apiInfo, err := env.StateInfo()
963 c.Assert(err, gc.IsNil)
964-
965 config := env.Config()
966- dnsName := "my-instance." + AZURE_DOMAIN_NAME
967+ dnsName := prefix + "myservice." + AZURE_DOMAIN_NAME
968 stateServerAddr := fmt.Sprintf("%s:%d", dnsName, config.StatePort())
969 apiServerAddr := fmt.Sprintf("%s:%d", dnsName, config.APIPort())
970 c.Check(stateInfo.Addrs, gc.DeepEquals, []string{stateServerAddr})
971@@ -502,6 +532,14 @@
972 return &body
973 }
974
975+// getHostedServicePropertiesServiceName extracts the service name parameter
976+// from the GetHostedServiceProperties request URL.
977+func getHostedServicePropertiesServiceName(c *gc.C, request *gwacl.X509Request) string {
978+ url, err := url.Parse(request.URL)
979+ c.Assert(err, gc.IsNil)
980+ return path.Base(url.Path)
981+}
982+
983 // makeNonAvailabilityResponse simulates a reply to the
984 // CheckHostedServiceNameAvailability call saying that a name is not available.
985 func makeNonAvailabilityResponse(c *gc.C) []byte {
986@@ -524,7 +562,7 @@
987 func (*environSuite) TestAttemptCreateServiceCreatesService(c *gc.C) {
988 prefix := "myservice"
989 affinityGroup := "affinity-group"
990- location := "location"
991+
992 responses := []gwacl.DispatcherResponse{
993 gwacl.NewDispatcherResponse(makeAvailabilityResponse(c), http.StatusOK, nil),
994 gwacl.NewDispatcherResponse(nil, http.StatusOK, nil),
995@@ -533,7 +571,7 @@
996 azure, err := gwacl.NewManagementAPI("subscription", "", "West US")
997 c.Assert(err, gc.IsNil)
998
999- service, err := attemptCreateService(azure, prefix, affinityGroup, location)
1000+ service, err := attemptCreateService(azure, prefix, affinityGroup, "")
1001 c.Assert(err, gc.IsNil)
1002
1003 c.Assert(*requests, gc.HasLen, 2)
1004@@ -541,11 +579,8 @@
1005 c.Check(body.ServiceName, gc.Equals, service.ServiceName)
1006 c.Check(body.AffinityGroup, gc.Equals, affinityGroup)
1007 c.Check(service.ServiceName, gc.Matches, prefix+".*")
1008- c.Check(service.Location, gc.Equals, location)
1009-
1010- label, err := base64.StdEncoding.DecodeString(service.Label)
1011- c.Assert(err, gc.IsNil)
1012- c.Check(string(label), gc.Equals, service.ServiceName)
1013+ // We specify AffinityGroup, so Location should be empty.
1014+ c.Check(service.Location, gc.Equals, "")
1015 }
1016
1017 func (*environSuite) TestAttemptCreateServiceReturnsNilIfNameNotUnique(c *gc.C) {
1018@@ -556,7 +591,7 @@
1019 azure, err := gwacl.NewManagementAPI("subscription", "", "West US")
1020 c.Assert(err, gc.IsNil)
1021
1022- service, err := attemptCreateService(azure, "service", "affinity-group", "location")
1023+ service, err := attemptCreateService(azure, "service", "affinity-group", "")
1024 c.Check(err, gc.IsNil)
1025 c.Check(service, gc.IsNil)
1026 }
1027@@ -570,7 +605,7 @@
1028 azure, err := gwacl.NewManagementAPI("subscription", "", "West US")
1029 c.Assert(err, gc.IsNil)
1030
1031- _, err = attemptCreateService(azure, "service", "affinity-group", "location")
1032+ _, err = attemptCreateService(azure, "service", "affinity-group", "")
1033 c.Assert(err, gc.NotNil)
1034 c.Check(err, gc.ErrorMatches, ".*Not Found.*")
1035 }
1036@@ -578,24 +613,30 @@
1037 func (*environSuite) TestNewHostedServiceCreatesService(c *gc.C) {
1038 prefix := "myservice"
1039 affinityGroup := "affinity-group"
1040- location := "location"
1041 responses := []gwacl.DispatcherResponse{
1042 gwacl.NewDispatcherResponse(makeAvailabilityResponse(c), http.StatusOK, nil),
1043 gwacl.NewDispatcherResponse(nil, http.StatusOK, nil),
1044+ getAzureServiceResponse(c, gwacl.HostedService{
1045+ HostedServiceDescriptor: gwacl.HostedServiceDescriptor{
1046+ ServiceName: "anything",
1047+ },
1048+ }),
1049 }
1050 requests := gwacl.PatchManagementAPIResponses(responses)
1051 azure, err := gwacl.NewManagementAPI("subscription", "", "West US")
1052 c.Assert(err, gc.IsNil)
1053
1054- service, err := newHostedService(azure, prefix, affinityGroup, location)
1055+ service, err := newHostedService(azure, prefix, affinityGroup, "")
1056 c.Assert(err, gc.IsNil)
1057
1058- c.Assert(*requests, gc.HasLen, 2)
1059+ c.Assert(*requests, gc.HasLen, 3)
1060 body := parseCreateServiceRequest(c, (*requests)[1])
1061- c.Check(body.ServiceName, gc.Equals, service.ServiceName)
1062+ requestedServiceName := getHostedServicePropertiesServiceName(c, (*requests)[2])
1063+ c.Check(body.ServiceName, gc.Matches, prefix+".*")
1064+ c.Check(body.ServiceName, gc.Equals, requestedServiceName)
1065 c.Check(body.AffinityGroup, gc.Equals, affinityGroup)
1066- c.Check(service.ServiceName, gc.Matches, prefix+".*")
1067- c.Check(service.Location, gc.Equals, location)
1068+ c.Check(service.ServiceName, gc.Equals, "anything")
1069+ c.Check(service.Location, gc.Equals, "")
1070 }
1071
1072 func (*environSuite) TestNewHostedServiceRetriesIfNotUnique(c *gc.C) {
1073@@ -606,17 +647,22 @@
1074 responses := []gwacl.DispatcherResponse{
1075 gwacl.NewDispatcherResponse(errorBody, http.StatusOK, nil),
1076 gwacl.NewDispatcherResponse(errorBody, http.StatusOK, nil),
1077- gwacl.NewDispatcherResponse(okBody, http.StatusOK, nil),
1078- gwacl.NewDispatcherResponse(nil, http.StatusOK, nil),
1079+ gwacl.NewDispatcherResponse(okBody, http.StatusOK, nil), // name is unique
1080+ gwacl.NewDispatcherResponse(nil, http.StatusOK, nil), // create service
1081+ getAzureServiceResponse(c, gwacl.HostedService{
1082+ HostedServiceDescriptor: gwacl.HostedServiceDescriptor{
1083+ ServiceName: "anything",
1084+ },
1085+ }),
1086 }
1087 requests := gwacl.PatchManagementAPIResponses(responses)
1088 azure, err := gwacl.NewManagementAPI("subscription", "", "West US")
1089 c.Assert(err, gc.IsNil)
1090
1091- service, err := newHostedService(azure, "service", "affinity-group", "location")
1092+ service, err := newHostedService(azure, "service", "affinity-group", "")
1093 c.Check(err, gc.IsNil)
1094
1095- c.Assert(*requests, gc.HasLen, 4)
1096+ c.Assert(*requests, gc.HasLen, 5)
1097 // How many names have been attempted, and how often?
1098 // There is a minute chance that this tries the same name twice, and
1099 // then this test will fail. If that happens, try seeding the
1100@@ -637,11 +683,8 @@
1101 c.Check(attemptedNames, gc.HasLen, 3)
1102
1103 // Once newHostedService succeeds, we get a hosted service with the
1104- // last requested name.
1105- c.Check(
1106- service.ServiceName,
1107- gc.Equals,
1108- parseCreateServiceRequest(c, (*requests)[3]).ServiceName)
1109+ // name returned from GetHostedServiceProperties.
1110+ c.Check(service.ServiceName, gc.Equals, "anything")
1111 }
1112
1113 func (*environSuite) TestNewHostedServiceFailsIfUnableToFindUniqueName(c *gc.C) {
1114@@ -654,128 +697,142 @@
1115 azure, err := gwacl.NewManagementAPI("subscription", "", "West US")
1116 c.Assert(err, gc.IsNil)
1117
1118- _, err = newHostedService(azure, "service", "affinity-group", "location")
1119+ _, err = newHostedService(azure, "service", "affinity-group", "")
1120 c.Assert(err, gc.NotNil)
1121 c.Check(err, gc.ErrorMatches, "could not come up with a unique hosted service name.*")
1122 }
1123
1124-// buildDestroyAzureServiceResponses returns a slice containing the responses that a fake Azure server
1125-// can use to simulate the deletion of the given list of services.
1126-func buildDestroyAzureServiceResponses(c *gc.C, services []*gwacl.HostedService) []gwacl.DispatcherResponse {
1127- responses := []gwacl.DispatcherResponse{}
1128- for _, service := range services {
1129- // When destroying a hosted service, gwacl first issues a Get request
1130- // to fetch the properties of the services. Then it destroys all the
1131- // deployments found in this service (none in this case, we make sure
1132- // the service does not contain deployments to keep the testing simple)
1133- // And it finally deletes the service itself.
1134- if len(service.Deployments) != 0 {
1135- panic("buildDestroyAzureServiceResponses does not support services with deployments!")
1136- }
1137+func buildGetServicePropertiesResponses(c *gc.C, services ...*gwacl.HostedService) []gwacl.DispatcherResponse {
1138+ responses := make([]gwacl.DispatcherResponse, len(services))
1139+ for i, service := range services {
1140 serviceXML, err := service.Serialize()
1141 c.Assert(err, gc.IsNil)
1142- serviceGetResponse := gwacl.NewDispatcherResponse(
1143- []byte(serviceXML),
1144- http.StatusOK,
1145- nil,
1146- )
1147- responses = append(responses, serviceGetResponse)
1148- serviceDeleteResponse := gwacl.NewDispatcherResponse(
1149- nil,
1150- http.StatusOK,
1151- nil,
1152- )
1153- responses = append(responses, serviceDeleteResponse)
1154- }
1155- return responses
1156-}
1157-
1158-func makeAzureService(name string) (*gwacl.HostedService, *gwacl.HostedServiceDescriptor) {
1159- service1Desc := &gwacl.HostedServiceDescriptor{ServiceName: name}
1160- service1 := &gwacl.HostedService{HostedServiceDescriptor: *service1Desc}
1161- return service1, service1Desc
1162-}
1163-
1164-func (s *environSuite) setServiceDeletionConcurrency(nbGoroutines int) {
1165- s.PatchValue(&maxConcurrentDeletes, nbGoroutines)
1166+ responses[i] = gwacl.NewDispatcherResponse([]byte(serviceXML), http.StatusOK, nil)
1167+ }
1168+ return responses
1169+}
1170+
1171+func buildStatusOKResponses(c *gc.C, n int) []gwacl.DispatcherResponse {
1172+ responses := make([]gwacl.DispatcherResponse, n)
1173+ for i := range responses {
1174+ responses[i] = gwacl.NewDispatcherResponse(nil, http.StatusOK, nil)
1175+ }
1176+ return responses
1177+}
1178+
1179+func makeAzureService(name string) *gwacl.HostedService {
1180+ return &gwacl.HostedService{
1181+ HostedServiceDescriptor: gwacl.HostedServiceDescriptor{ServiceName: name},
1182+ }
1183+}
1184+
1185+func makeRole(env *azureEnviron) *gwacl.Role {
1186+ size := "Large"
1187+ vhd := env.newOSDisk("source-image-name")
1188+ userData := "example-user-data"
1189+ return env.newRole(size, vhd, userData, false)
1190+}
1191+
1192+func makeLegacyDeployment(env *azureEnviron, serviceName string) *gwacl.HostedService {
1193+ service := makeAzureService(serviceName)
1194+ service.Deployments = []gwacl.Deployment{{
1195+ Name: serviceName,
1196+ RoleList: []gwacl.Role{*makeRole(env)},
1197+ }}
1198+ return service
1199+}
1200+
1201+func makeDeployment(env *azureEnviron, serviceName string) *gwacl.HostedService {
1202+ service := makeAzureService(serviceName)
1203+ service.Deployments = []gwacl.Deployment{{
1204+ Name: serviceName + "-v2",
1205+ RoleList: []gwacl.Role{*makeRole(env), *makeRole(env)},
1206+ }}
1207+ return service
1208 }
1209
1210 func (s *environSuite) TestStopInstancesDestroysMachines(c *gc.C) {
1211- s.setServiceDeletionConcurrency(3)
1212+ env := makeEnviron(c)
1213 service1Name := "service1"
1214- service1, service1Desc := makeAzureService(service1Name)
1215+ service1 := makeLegacyDeployment(env, service1Name)
1216 service2Name := "service2"
1217- service2, service2Desc := makeAzureService(service2Name)
1218- services := []*gwacl.HostedService{service1, service2}
1219- responses := buildDestroyAzureServiceResponses(c, services)
1220+ service2 := makeDeployment(env, service2Name)
1221+
1222+ inst1, err := env.getInstance(service1, "")
1223+ c.Assert(err, gc.IsNil)
1224+ role2Name := service2.Deployments[0].RoleList[0].RoleName
1225+ inst2, err := env.getInstance(service2, role2Name)
1226+ c.Assert(err, gc.IsNil)
1227+ role3Name := service2.Deployments[0].RoleList[1].RoleName
1228+ inst3, err := env.getInstance(service2, role3Name)
1229+ c.Assert(err, gc.IsNil)
1230+
1231+ responses := buildGetServicePropertiesResponses(c, service1)
1232+ responses = append(responses, buildStatusOKResponses(c, 1)...) // DeleteHostedService
1233+ responses = append(responses, buildGetServicePropertiesResponses(c, service2)...)
1234+ responses = append(responses, buildStatusOKResponses(c, 1)...) // DeleteHostedService
1235 requests := gwacl.PatchManagementAPIResponses(responses)
1236- env := makeEnviron(c)
1237- instances := convertToInstances(
1238- []gwacl.HostedServiceDescriptor{*service1Desc, *service2Desc},
1239- env)
1240-
1241- err := env.StopInstances(instances)
1242+ err = env.StopInstances([]instance.Instance{inst1, inst2, inst3})
1243 c.Check(err, gc.IsNil)
1244
1245- // It takes 2 API calls to delete each service:
1246- // - one GET request to fetch the service's properties;
1247- // - one DELETE request to delete the service.
1248- c.Check(len(*requests), gc.Equals, len(services)*2)
1249- assertOneRequestMatches(c, *requests, "GET", ".*"+service1Name+".*")
1250+ // One GET and DELETE per service
1251+ // (GetHostedServiceProperties and DeleteHostedService).
1252+ c.Check(len(*requests), gc.Equals, len(responses))
1253+ assertOneRequestMatches(c, *requests, "GET", ".*"+service1Name+".")
1254+ assertOneRequestMatches(c, *requests, "GET", ".*"+service2Name+".*")
1255 assertOneRequestMatches(c, *requests, "DELETE", ".*"+service1Name+".*")
1256- assertOneRequestMatches(c, *requests, "GET", ".*"+service2Name+".")
1257 assertOneRequestMatches(c, *requests, "DELETE", ".*"+service2Name+".*")
1258 }
1259
1260+func (s *environSuite) TestStopInstancesServiceSubset(c *gc.C) {
1261+ env := makeEnviron(c)
1262+ service := makeDeployment(env, "service")
1263+
1264+ role1Name := service.Deployments[0].RoleList[0].RoleName
1265+ inst1, err := env.getInstance(service, role1Name)
1266+ c.Assert(err, gc.IsNil)
1267+
1268+ responses := buildGetServicePropertiesResponses(c, service)
1269+ responses = append(responses, buildStatusOKResponses(c, 1)...) // DeleteRole
1270+ requests := gwacl.PatchManagementAPIResponses(responses)
1271+ err = env.StopInstances([]instance.Instance{inst1})
1272+ c.Check(err, gc.IsNil)
1273+
1274+ // One GET for the service, and one DELETE for the role.
1275+ // The service isn't deleted because it has two roles,
1276+ // and only one is being deleted.
1277+ c.Check(len(*requests), gc.Equals, len(responses))
1278+ assertOneRequestMatches(c, *requests, "GET", ".*"+service.ServiceName+".")
1279+ assertOneRequestMatches(c, *requests, "DELETE", ".*"+role1Name+".*")
1280+}
1281+
1282 func (s *environSuite) TestStopInstancesWhenStoppingMachinesFails(c *gc.C) {
1283- s.setServiceDeletionConcurrency(3)
1284- responses := []gwacl.DispatcherResponse{
1285- gwacl.NewDispatcherResponse(nil, http.StatusConflict, nil),
1286- }
1287- service1Name := "service1"
1288- _, service1Desc := makeAzureService(service1Name)
1289- service2Name := "service2"
1290- service2, service2Desc := makeAzureService(service2Name)
1291- services := []*gwacl.HostedService{service2}
1292- destroyResponses := buildDestroyAzureServiceResponses(c, services)
1293- responses = append(responses, destroyResponses...)
1294+ env := makeEnviron(c)
1295+ service1 := makeDeployment(env, "service1")
1296+ service2 := makeDeployment(env, "service2")
1297+ service1Role1Name := service1.Deployments[0].RoleList[0].RoleName
1298+ inst1, err := env.getInstance(service1, service1Role1Name)
1299+ c.Assert(err, gc.IsNil)
1300+ service2Role1Name := service2.Deployments[0].RoleList[0].RoleName
1301+ inst2, err := env.getInstance(service2, service2Role1Name)
1302+ c.Assert(err, gc.IsNil)
1303+
1304+ responses := buildGetServicePropertiesResponses(c, service1, service2)
1305+ // Failed to delete one of the services.
1306+ responses = append(responses, gwacl.NewDispatcherResponse(nil, http.StatusConflict, nil))
1307 requests := gwacl.PatchManagementAPIResponses(responses)
1308- env := makeEnviron(c)
1309- instances := convertToInstances(
1310- []gwacl.HostedServiceDescriptor{*service1Desc, *service2Desc}, env)
1311
1312- err := env.StopInstances(instances)
1313+ instances := []instance.Instance{inst1, inst2}
1314+ err = env.StopInstances(instances)
1315 c.Check(err, gc.ErrorMatches, ".*Conflict.*")
1316
1317- c.Check(len(*requests), gc.Equals, 3)
1318- assertOneRequestMatches(c, *requests, "GET", ".*"+service1Name+".")
1319- assertOneRequestMatches(c, *requests, "GET", ".*"+service2Name+".")
1320- // Only one of the services was deleted.
1321- assertOneRequestMatches(c, *requests, "DELETE", ".*")
1322-}
1323-
1324-func (s *environSuite) TestStopInstancesWithLimitedConcurrency(c *gc.C) {
1325- s.setServiceDeletionConcurrency(3)
1326- services := []*gwacl.HostedService{}
1327- serviceDescs := []gwacl.HostedServiceDescriptor{}
1328- for i := 0; i < 10; i++ {
1329- serviceName := fmt.Sprintf("service%d", i)
1330- service, serviceDesc := makeAzureService(serviceName)
1331- services = append(services, service)
1332- serviceDescs = append(serviceDescs, *serviceDesc)
1333- }
1334- responses := buildDestroyAzureServiceResponses(c, services)
1335- requests := gwacl.PatchManagementAPIResponses(responses)
1336- env := makeEnviron(c)
1337- instances := convertToInstances(serviceDescs, env)
1338-
1339- err := env.StopInstances(instances)
1340- c.Check(err, gc.IsNil)
1341- c.Check(len(*requests), gc.Equals, len(services)*2)
1342+ c.Check(len(*requests), gc.Equals, len(responses))
1343+ assertOneRequestMatches(c, *requests, "GET", ".*"+service1.ServiceName+".*")
1344+ assertOneRequestMatches(c, *requests, "GET", ".*"+service2.ServiceName+".*")
1345+ assertOneRequestMatches(c, *requests, "DELETE", ".*("+service1.ServiceName+"|"+service2.ServiceName+").")
1346 }
1347
1348 func (s *environSuite) TestStopInstancesWithZeroInstance(c *gc.C) {
1349- s.setServiceDeletionConcurrency(3)
1350 env := makeEnviron(c)
1351 instances := []instance.Instance{}
1352
1353@@ -832,8 +889,7 @@
1354 env.Storage(),
1355 &bootstrap.BootstrapState{StateInstances: []instance.Id{instance.Id("test-id")}})
1356 c.Assert(err, gc.IsNil)
1357- services := []gwacl.HostedServiceDescriptor{}
1358- responses := getAzureServiceListResponse(c, services)
1359+ responses := getAzureServiceListResponse(c)
1360 cleanupResponses := getVnetAndAffinityGroupCleanupResponses(c)
1361 responses = append(responses, cleanupResponses...)
1362 gwacl.PatchManagementAPIResponses(responses)
1363@@ -849,8 +905,7 @@
1364 func (s *environSuite) TestDestroyDeletesVirtualNetworkAndAffinityGroup(c *gc.C) {
1365 env := makeEnviron(c)
1366 s.setDummyStorage(c, env)
1367- services := []gwacl.HostedServiceDescriptor{}
1368- responses := getAzureServiceListResponse(c, services)
1369+ responses := getAzureServiceListResponse(c)
1370 // Prepare a configuration with a single virtual network.
1371 existingConfig := &gwacl.NetworkConfiguration{
1372 XMLNS: gwacl.XMLNS_NC,
1373@@ -914,52 +969,98 @@
1374 }
1375
1376 func (s *environSuite) TestDestroyStopsAllInstances(c *gc.C) {
1377- s.setServiceDeletionConcurrency(3)
1378 env := makeEnviron(c)
1379 s.setDummyStorage(c, env)
1380+ prefix := env.getEnvPrefix()
1381+ service1 := makeDeployment(env, prefix+"service1")
1382+ service2 := makeDeployment(env, prefix+"service2")
1383
1384- // Simulate 2 instances corresponding to two Azure services.
1385- prefix := env.getEnvPrefix()
1386- service1Name := prefix + "service1"
1387- service1, service1Desc := makeAzureService(service1Name)
1388- services := []*gwacl.HostedService{service1}
1389 // The call to AllInstances() will return only one service (service1).
1390- listInstancesResponses := getAzureServiceListResponse(c, []gwacl.HostedServiceDescriptor{*service1Desc})
1391- destroyResponses := buildDestroyAzureServiceResponses(c, services)
1392- responses := append(listInstancesResponses, destroyResponses...)
1393- cleanupResponses := getVnetAndAffinityGroupCleanupResponses(c)
1394- responses = append(responses, cleanupResponses...)
1395+ responses := getAzureServiceListResponse(c, service1.HostedServiceDescriptor, service2.HostedServiceDescriptor)
1396+ responses = append(responses, buildStatusOKResponses(c, 2)...) // DeleteHostedService
1397+ responses = append(responses, getVnetAndAffinityGroupCleanupResponses(c)...)
1398 requests := gwacl.PatchManagementAPIResponses(responses)
1399
1400 err := env.Destroy()
1401 c.Check(err, gc.IsNil)
1402
1403 // One request to get the list of all the environment's instances.
1404- // Then two requests per destroyed machine (one to fetch the
1405- // service's information, one to delete it) and two requests to delete
1406- // the Virtual Network and the Affinity Group.
1407- c.Check((*requests), gc.HasLen, 1+len(services)*2+2)
1408+ // One delete request per destroyed service, and two additional
1409+ // requests to delete the Virtual Network and the Affinity Group.
1410+ c.Check((*requests), gc.HasLen, 5)
1411 c.Check((*requests)[0].Method, gc.Equals, "GET")
1412- assertOneRequestMatches(c, *requests, "GET", ".*"+service1Name+".*")
1413- assertOneRequestMatches(c, *requests, "DELETE", ".*"+service1Name+".*")
1414-}
1415-
1416-func (*environSuite) TestGetInstance(c *gc.C) {
1417- env := makeEnviron(c)
1418- prefix := env.getEnvPrefix()
1419- serviceName := prefix + "instance-name"
1420- serviceDesc := gwacl.HostedServiceDescriptor{ServiceName: serviceName}
1421- service := gwacl.HostedService{HostedServiceDescriptor: serviceDesc}
1422- responses := getAzureServiceResponses(c, service)
1423- gwacl.PatchManagementAPIResponses(responses)
1424-
1425- instance, err := env.getInstance("serviceName")
1426- c.Check(err, gc.IsNil)
1427-
1428- c.Check(string(instance.Id()), gc.Equals, serviceName)
1429- c.Check(instance, gc.FitsTypeOf, &azureInstance{})
1430- azInstance := instance.(*azureInstance)
1431- c.Check(azInstance.environ, gc.Equals, env)
1432+ assertOneRequestMatches(c, *requests, "DELETE", ".*"+service1.ServiceName+".*")
1433+ assertOneRequestMatches(c, *requests, "DELETE", ".*"+service2.ServiceName+".*")
1434+}
1435+
1436+func (s *environSuite) TestGetInstance(c *gc.C) {
1437+ env := makeEnviron(c)
1438+ service1 := makeLegacyDeployment(env, "service1")
1439+ service2 := makeDeployment(env, "service1")
1440+
1441+ // azureEnviron.Instances will call getInstance with roleName==""
1442+ // for legacy instances. This will cause getInstance to get the
1443+ // one and only role (or error if there is more than one).
1444+ inst1, err := env.getInstance(service1, "")
1445+ c.Assert(err, gc.IsNil)
1446+ c.Check(inst1.Id(), gc.Equals, instance.Id("service1"))
1447+ c.Assert(inst1, gc.FitsTypeOf, &azureInstance{})
1448+ c.Check(inst1.(*azureInstance).environ, gc.Equals, env)
1449+ c.Check(inst1.(*azureInstance).roleName, gc.Equals, service1.Deployments[0].RoleList[0].RoleName)
1450+ service1.Deployments[0].RoleList = service2.Deployments[0].RoleList
1451+ inst1, err = env.getInstance(service1, "")
1452+ c.Check(err, gc.ErrorMatches, `expected one role for "service1", got 2`)
1453+
1454+ inst2, err := env.getInstance(service2, service2.Deployments[0].RoleList[0].RoleName)
1455+ c.Assert(err, gc.IsNil)
1456+ c.Check(inst2.Id(), gc.Equals, instance.Id("service1-"+service2.Deployments[0].RoleList[0].RoleName))
1457+}
1458+
1459+func (s *environSuite) TestInitialPorts(c *gc.C) {
1460+ env := makeEnviron(c)
1461+ service1 := makeLegacyDeployment(env, "service1")
1462+ service2 := makeDeployment(env, "service2")
1463+ service3 := makeDeployment(env, "service3")
1464+ service3.Label = base64.StdEncoding.EncodeToString([]byte(stateServerLabel))
1465+
1466+ role1 := &service1.Deployments[0].RoleList[0]
1467+ inst1, err := env.getInstance(service1, role1.RoleName)
1468+ c.Assert(err, gc.IsNil)
1469+ c.Assert(inst1.(*azureInstance).maskStateServerPorts, jc.IsTrue)
1470+ role2 := &service2.Deployments[0].RoleList[0]
1471+ inst2, err := env.getInstance(service2, role2.RoleName)
1472+ c.Assert(err, gc.IsNil)
1473+ role3 := &service3.Deployments[0].RoleList[0]
1474+ inst3, err := env.getInstance(service3, role3.RoleName)
1475+ c.Assert(err, gc.IsNil)
1476+
1477+ // Only role2 should report opened state server ports via the Ports method.
1478+ dummyRole := *role1
1479+ configSetNetwork(&dummyRole).InputEndpoints = &[]gwacl.InputEndpoint{{
1480+ LocalPort: env.Config().StatePort(),
1481+ Protocol: "tcp",
1482+ Name: "stateserver",
1483+ Port: env.Config().StatePort(),
1484+ }, {
1485+ LocalPort: env.Config().APIPort(),
1486+ Protocol: "tcp",
1487+ Name: "apiserver",
1488+ Port: env.Config().APIPort(),
1489+ }}
1490+ reportsStateServerPorts := func(inst instance.Instance) bool {
1491+ responses := preparePortChangeConversation(c, &dummyRole)
1492+ gwacl.PatchManagementAPIResponses(responses)
1493+ ports, err := inst.Ports("")
1494+ c.Assert(err, gc.IsNil)
1495+ portmap := make(map[int]bool)
1496+ for _, port := range ports {
1497+ portmap[port.Number] = true
1498+ }
1499+ return portmap[env.Config().StatePort()] && portmap[env.Config().APIPort()]
1500+ }
1501+ c.Check(inst1, gc.Not(jc.Satisfies), reportsStateServerPorts)
1502+ c.Check(inst2, jc.Satisfies, reportsStateServerPorts)
1503+ c.Check(inst3, gc.Not(jc.Satisfies), reportsStateServerPorts)
1504 }
1505
1506 func (*environSuite) TestNewOSVirtualDisk(c *gc.C) {
1507@@ -989,25 +1090,32 @@
1508 return mapping
1509 }
1510
1511-func (*environSuite) TestNewRole(c *gc.C) {
1512+func (s *environSuite) TestNewRole(c *gc.C) {
1513+ s.testNewRole(c, false)
1514+}
1515+
1516+func (s *environSuite) TestNewRoleStateServer(c *gc.C) {
1517+ s.testNewRole(c, true)
1518+}
1519+
1520+func (*environSuite) testNewRole(c *gc.C, stateServer bool) {
1521 env := makeEnviron(c)
1522 size := "Large"
1523 vhd := env.newOSDisk("source-image-name")
1524 userData := "example-user-data"
1525- hostname := "hostname"
1526
1527- role := env.newRole(size, vhd, userData, hostname)
1528+ role := env.newRole(size, vhd, userData, stateServer)
1529
1530 configs := role.ConfigurationSets
1531 linuxConfig := configs[0]
1532 networkConfig := configs[1]
1533 c.Check(linuxConfig.CustomData, gc.Equals, userData)
1534- c.Check(linuxConfig.Hostname, gc.Equals, hostname)
1535+ c.Check(linuxConfig.Hostname, gc.Equals, role.RoleName)
1536 c.Check(linuxConfig.Username, gc.Not(gc.Equals), "")
1537 c.Check(linuxConfig.Password, gc.Not(gc.Equals), "")
1538 c.Check(linuxConfig.DisableSSHPasswordAuthentication, gc.Equals, "true")
1539 c.Check(role.RoleSize, gc.Equals, size)
1540- c.Check(role.OSVirtualHardDisk[0], gc.Equals, *vhd)
1541+ c.Check(role.OSVirtualHardDisk, gc.DeepEquals, vhd)
1542
1543 endpoints := mapInputEndpointsByPort(c, *networkConfig.InputEndpoints)
1544
1545@@ -1017,35 +1125,19 @@
1546 c.Check(sshEndpoint.LocalPort, gc.Equals, 22)
1547 c.Check(sshEndpoint.Protocol, gc.Equals, "tcp")
1548
1549- // There's also an endpoint for the state (mongodb) port.
1550- // TODO: Ought to have this only for state servers.
1551- stateEndpoint, ok := endpoints[env.Config().StatePort()]
1552- c.Assert(ok, gc.Equals, true)
1553- c.Check(stateEndpoint.LocalPort, gc.Equals, env.Config().StatePort())
1554- c.Check(stateEndpoint.Protocol, gc.Equals, "tcp")
1555-
1556- // And one for the API port.
1557- // TODO: Ought to have this only for API servers.
1558- apiEndpoint, ok := endpoints[env.Config().APIPort()]
1559- c.Assert(ok, gc.Equals, true)
1560- c.Check(apiEndpoint.LocalPort, gc.Equals, env.Config().APIPort())
1561- c.Check(apiEndpoint.Protocol, gc.Equals, "tcp")
1562-}
1563-
1564-func (*environSuite) TestNewDeployment(c *gc.C) {
1565- env := makeEnviron(c)
1566- deploymentName := "deployment-name"
1567- deploymentLabel := "deployment-label"
1568- virtualNetworkName := "virtual-network-name"
1569- vhd := env.newOSDisk("source-image-name")
1570- role := env.newRole("Small", vhd, "user-data", "hostname")
1571-
1572- deployment := env.newDeployment(role, deploymentName, deploymentLabel, virtualNetworkName)
1573-
1574- base64Label := base64.StdEncoding.EncodeToString([]byte(deploymentLabel))
1575- c.Check(deployment.Label, gc.Equals, base64Label)
1576- c.Check(deployment.Name, gc.Equals, deploymentName)
1577- c.Check(deployment.RoleList, gc.HasLen, 1)
1578+ if stateServer {
1579+ // There's also an endpoint for the state (mongodb) port.
1580+ stateEndpoint, ok := endpoints[env.Config().StatePort()]
1581+ c.Assert(ok, gc.Equals, true)
1582+ c.Check(stateEndpoint.LocalPort, gc.Equals, env.Config().StatePort())
1583+ c.Check(stateEndpoint.Protocol, gc.Equals, "tcp")
1584+
1585+ // And one for the API port.
1586+ apiEndpoint, ok := endpoints[env.Config().APIPort()]
1587+ c.Assert(ok, gc.Equals, true)
1588+ c.Check(apiEndpoint.LocalPort, gc.Equals, env.Config().APIPort())
1589+ c.Check(apiEndpoint.Protocol, gc.Equals, "tcp")
1590+ }
1591 }
1592
1593 func (*environSuite) TestProviderReturnsAzureEnvironProvider(c *gc.C) {
1594@@ -1235,18 +1327,6 @@
1595 c.Assert(image, gc.Equals, "image-id")
1596 }
1597
1598-func (*environSuite) TestConvertToInstances(c *gc.C) {
1599- services := []gwacl.HostedServiceDescriptor{
1600- {ServiceName: "foo"}, {ServiceName: "bar"},
1601- }
1602- env := makeEnviron(c)
1603- instances := convertToInstances(services, env)
1604- c.Check(instances, gc.DeepEquals, []instance.Instance{
1605- &azureInstance{services[0], env},
1606- &azureInstance{services[1], env},
1607- })
1608-}
1609-
1610 func (*environSuite) TestExtractStorageKeyPicksPrimaryKeyIfSet(c *gc.C) {
1611 keys := gwacl.StorageAccountKeys{
1612 Primary: "mainkey",
1613
1614=== modified file 'provider/azure/instance.go'
1615--- provider/azure/instance.go 2014-01-20 17:18:13 +0000
1616+++ provider/azure/instance.go 2014-04-03 07:35:44 +0000
1617@@ -6,18 +6,28 @@
1618 import (
1619 "fmt"
1620 "strings"
1621+ "sync"
1622
1623 "launchpad.net/gwacl"
1624
1625+ "launchpad.net/juju-core/errors"
1626 "launchpad.net/juju-core/instance"
1627 "launchpad.net/juju-core/provider/common"
1628 "launchpad.net/juju-core/worker/firewaller"
1629 )
1630
1631+const AZURE_DOMAIN_NAME = "cloudapp.net"
1632+
1633 type azureInstance struct {
1634- // An instance contains an Azure Service (instance==service).
1635- gwacl.HostedServiceDescriptor
1636- environ *azureEnviron
1637+ environ *azureEnviron
1638+ hostedService *gwacl.HostedServiceDescriptor
1639+ instanceId instance.Id
1640+ deploymentName string
1641+ roleName string
1642+ maskStateServerPorts bool
1643+
1644+ mu sync.Mutex
1645+ roleInstance *gwacl.RoleInstance
1646 }
1647
1648 // azureInstance implements Instance.
1649@@ -25,38 +35,71 @@
1650
1651 // Id is specified in the Instance interface.
1652 func (azInstance *azureInstance) Id() instance.Id {
1653- return instance.Id(azInstance.ServiceName)
1654+ return azInstance.instanceId
1655+}
1656+
1657+// supportsLoadBalancing returns true iff the instance is
1658+// not a legacy instance where endpoints may have been
1659+// created without load balancing set names associated.
1660+func (azInstance *azureInstance) supportsLoadBalancing() bool {
1661+ v1Name := deploymentNameV1(azInstance.hostedService.ServiceName)
1662+ return azInstance.deploymentName != v1Name
1663 }
1664
1665 // Status is specified in the Instance interface.
1666 func (azInstance *azureInstance) Status() string {
1667- return azInstance.HostedServiceDescriptor.Status
1668+ azInstance.mu.Lock()
1669+ defer azInstance.mu.Unlock()
1670+ if azInstance.roleInstance == nil {
1671+ return ""
1672+ }
1673+ return azInstance.roleInstance.InstanceStatus
1674 }
1675
1676-var AZURE_DOMAIN_NAME = "cloudapp.net"
1677+func (azInstance *azureInstance) serviceName() string {
1678+ return azInstance.hostedService.ServiceName
1679+}
1680
1681 // Refresh is specified in the Instance interface.
1682 func (azInstance *azureInstance) Refresh() error {
1683- // TODO(axw) 2013-12-16 #1261324
1684- // Cache Addresses/netInfo, refresh here.
1685- return nil
1686+ return azInstance.apiCall(false, func(c *azureManagementContext) error {
1687+ d, err := c.GetDeployment(&gwacl.GetDeploymentRequest{
1688+ ServiceName: azInstance.serviceName(),
1689+ DeploymentName: azInstance.deploymentName,
1690+ })
1691+ if err != nil {
1692+ return err
1693+ }
1694+ // Look for the role instance.
1695+ for _, role := range d.RoleInstanceList {
1696+ if role.RoleName == azInstance.roleName {
1697+ azInstance.mu.Lock()
1698+ azInstance.roleInstance = &role
1699+ azInstance.mu.Unlock()
1700+ return nil
1701+ }
1702+ }
1703+ return errors.NotFoundf("role instance %q", azInstance.roleName)
1704+ })
1705 }
1706
1707 // Addresses is specified in the Instance interface.
1708 func (azInstance *azureInstance) Addresses() ([]instance.Address, error) {
1709- addrs := []instance.Address{}
1710- ip, netname, err := azInstance.netInfo()
1711- if err != nil {
1712- return nil, err
1713- }
1714- if ip != "" {
1715- addrs = append(addrs, instance.Address{
1716- ip,
1717- instance.Ipv4Address,
1718- netname,
1719- instance.NetworkCloudLocal})
1720- }
1721-
1722+ var addrs []instance.Address
1723+ for i := 0; i < 2; i++ {
1724+ if ip := azInstance.ipAddress(); ip != "" {
1725+ addrs = append(addrs, instance.Address{
1726+ Value: ip,
1727+ Type: instance.Ipv4Address,
1728+ NetworkName: azInstance.environ.getVirtualNetworkName(),
1729+ NetworkScope: instance.NetworkCloudLocal,
1730+ })
1731+ break
1732+ }
1733+ if err := azInstance.Refresh(); err != nil {
1734+ return nil, err
1735+ }
1736+ }
1737 name, err := azInstance.DNSName()
1738 if err != nil {
1739 return nil, err
1740@@ -66,32 +109,14 @@
1741 return addrs, nil
1742 }
1743
1744-func (azInstance *azureInstance) netInfo() (ip, netname string, err error) {
1745- err = azInstance.apiCall(false, func(c *azureManagementContext) error {
1746- d, err := c.GetDeployment(&gwacl.GetDeploymentRequest{
1747- ServiceName: azInstance.ServiceName,
1748- DeploymentName: azInstance.ServiceName,
1749- })
1750- if err != nil {
1751- return err
1752- }
1753- switch len(d.RoleInstanceList) {
1754- case 0:
1755- // nothing to do, this can happen if the instances aren't finished deploying
1756- return nil
1757- case 1:
1758- // success
1759- ip = d.RoleInstanceList[0].IPAddress
1760- netname = d.VirtualNetworkName
1761- return nil
1762- default:
1763- return fmt.Errorf("Too many instances, expected one, got %d", len(d.RoleInstanceList))
1764- }
1765- })
1766- if err != nil {
1767- return "", "", err
1768+func (azInstance *azureInstance) ipAddress() string {
1769+ azInstance.mu.Lock()
1770+ defer azInstance.mu.Unlock()
1771+ if azInstance.roleInstance == nil {
1772+ // RoleInstance hasn't finished deploying.
1773+ return ""
1774 }
1775- return ip, netname, nil
1776+ return azInstance.roleInstance.IPAddress
1777 }
1778
1779 // DNSName is specified in the Instance interface.
1780@@ -101,7 +126,7 @@
1781 // (For Staging deployments it's all much weirder: they get random
1782 // names assigned, which somehow don't seem to resolve from the
1783 // outside.)
1784- name := fmt.Sprintf("%s.%s", azInstance.ServiceName, AZURE_DOMAIN_NAME)
1785+ name := fmt.Sprintf("%s.%s", azInstance.serviceName(), AZURE_DOMAIN_NAME)
1786 return name, nil
1787 }
1788
1789@@ -121,13 +146,11 @@
1790 // the environment
1791 func (azInstance *azureInstance) apiCall(lock bool, f func(*azureManagementContext) error) error {
1792 env := azInstance.environ
1793-
1794 context, err := env.getManagementAPI()
1795 if err != nil {
1796 return err
1797 }
1798 defer env.releaseManagementAPI(context)
1799-
1800 if lock {
1801 env.Lock()
1802 defer env.Unlock()
1803@@ -139,37 +162,40 @@
1804 // responsible for locking and unlocking the environ and releasing the
1805 // management context.
1806 func (azInstance *azureInstance) openEndpoints(context *azureManagementContext, ports []instance.Port) error {
1807- deployments, err := context.ListAllDeployments(&gwacl.ListAllDeploymentsRequest{
1808- ServiceName: azInstance.ServiceName,
1809- })
1810- if err != nil {
1811- return err
1812- }
1813-
1814- for _, deployment := range deployments {
1815- for _, role := range deployment.RoleList {
1816- request := &gwacl.AddRoleEndpointsRequest{
1817- ServiceName: azInstance.ServiceName,
1818- DeploymentName: deployment.Name,
1819- RoleName: role.RoleName,
1820- }
1821- for _, port := range ports {
1822- request.InputEndpoints = append(
1823- request.InputEndpoints, gwacl.InputEndpoint{
1824- LocalPort: port.Number,
1825- Name: fmt.Sprintf("%s%d", port.Protocol, port.Number),
1826- Port: port.Number,
1827- Protocol: port.Protocol,
1828- })
1829- }
1830- err := context.AddRoleEndpoints(request)
1831- if err != nil {
1832- return err
1833- }
1834- }
1835- }
1836-
1837- return nil
1838+ request := &gwacl.AddRoleEndpointsRequest{
1839+ ServiceName: azInstance.serviceName(),
1840+ DeploymentName: azInstance.deploymentName,
1841+ RoleName: azInstance.roleName,
1842+ }
1843+ for _, port := range ports {
1844+ name := fmt.Sprintf("%s%d", port.Protocol, port.Number)
1845+ endpoint := gwacl.InputEndpoint{
1846+ LocalPort: port.Number,
1847+ Name: name,
1848+ Port: port.Number,
1849+ Protocol: port.Protocol,
1850+ }
1851+ if azInstance.supportsLoadBalancing() {
1852+ probePort := port.Number
1853+ if strings.ToUpper(endpoint.Protocol) == "UDP" {
1854+ // Load balancing needs a TCP port to probe, or an HTTP
1855+ // server port & path to query. For UDP, we just use the
1856+ // machine's SSH agent port to test machine liveness.
1857+ //
1858+ // It probably doesn't make sense to load balance most UDP
1859+ // protocols transparently, but that's an application level
1860+ // concern.
1861+ probePort = 22
1862+ }
1863+ endpoint.LoadBalancedEndpointSetName = name
1864+ endpoint.LoadBalancerProbe = &gwacl.LoadBalancerProbe{
1865+ Port: probePort,
1866+ Protocol: "TCP",
1867+ }
1868+ }
1869+ request.InputEndpoints = append(request.InputEndpoints, endpoint)
1870+ }
1871+ return context.AddRoleEndpoints(request)
1872 }
1873
1874 // ClosePorts is specified in the Instance interface.
1875@@ -183,40 +209,25 @@
1876 // responsible for locking and unlocking the environ and releasing the
1877 // management context.
1878 func (azInstance *azureInstance) closeEndpoints(context *azureManagementContext, ports []instance.Port) error {
1879- deployments, err := context.ListAllDeployments(&gwacl.ListAllDeploymentsRequest{
1880- ServiceName: azInstance.ServiceName,
1881- })
1882- if err != nil {
1883- return err
1884- }
1885-
1886- for _, deployment := range deployments {
1887- for _, role := range deployment.RoleList {
1888- request := &gwacl.RemoveRoleEndpointsRequest{
1889- ServiceName: azInstance.ServiceName,
1890- DeploymentName: deployment.Name,
1891- RoleName: role.RoleName,
1892- }
1893- for _, port := range ports {
1894- request.InputEndpoints = append(
1895- request.InputEndpoints, gwacl.InputEndpoint{
1896- LocalPort: port.Number,
1897- Name: fmt.Sprintf("%s%d", port.Protocol, port.Number),
1898- Port: port.Number,
1899- Protocol: port.Protocol,
1900- })
1901- }
1902- err := context.RemoveRoleEndpoints(request)
1903- if err != nil {
1904- return err
1905- }
1906- }
1907- }
1908-
1909- return nil
1910+ request := &gwacl.RemoveRoleEndpointsRequest{
1911+ ServiceName: azInstance.serviceName(),
1912+ DeploymentName: azInstance.deploymentName,
1913+ RoleName: azInstance.roleName,
1914+ }
1915+ for _, port := range ports {
1916+ name := fmt.Sprintf("%s%d", port.Protocol, port.Number)
1917+ request.InputEndpoints = append(request.InputEndpoints, gwacl.InputEndpoint{
1918+ LocalPort: port.Number,
1919+ Name: name,
1920+ Port: port.Number,
1921+ Protocol: port.Protocol,
1922+ LoadBalancedEndpointSetName: name,
1923+ })
1924+ }
1925+ return context.RemoveRoleEndpoints(request)
1926 }
1927
1928-// convertAndFilterEndpoints converts a slice of gwacl.InputEndpoint into a slice of instance.Port.
1929+// convertEndpointsToPorts converts a slice of gwacl.InputEndpoint into a slice of instance.Port.
1930 func convertEndpointsToPorts(endpoints []gwacl.InputEndpoint) []instance.Port {
1931 ports := []instance.Port{}
1932 for _, endpoint := range endpoints {
1933@@ -230,10 +241,11 @@
1934
1935 // convertAndFilterEndpoints converts a slice of gwacl.InputEndpoint into a slice of instance.Port
1936 // and filters out the initial endpoints that every instance should have opened (ssh port, etc.).
1937-func convertAndFilterEndpoints(endpoints []gwacl.InputEndpoint, env *azureEnviron) []instance.Port {
1938+func convertAndFilterEndpoints(endpoints []gwacl.InputEndpoint, env *azureEnviron, stateServer bool) []instance.Port {
1939 return firewaller.Diff(
1940 convertEndpointsToPorts(endpoints),
1941- convertEndpointsToPorts(env.getInitialEndpoints()))
1942+ convertEndpointsToPorts(env.getInitialEndpoints(stateServer)),
1943+ )
1944 }
1945
1946 // Ports is specified in the Instance interface.
1947@@ -254,39 +266,14 @@
1948 // responsible for locking and unlocking the environ and releasing the
1949 // management context.
1950 func (azInstance *azureInstance) listPorts(context *azureManagementContext) ([]instance.Port, error) {
1951- deployments, err := context.ListAllDeployments(&gwacl.ListAllDeploymentsRequest{
1952- ServiceName: azInstance.ServiceName,
1953+ endpoints, err := context.ListRoleEndpoints(&gwacl.ListRoleEndpointsRequest{
1954+ ServiceName: azInstance.serviceName(),
1955+ DeploymentName: azInstance.deploymentName,
1956+ RoleName: azInstance.roleName,
1957 })
1958 if err != nil {
1959 return nil, err
1960 }
1961-
1962- env := azInstance.environ
1963- switch {
1964- // Only zero or one deployment is a valid state (instance==service).
1965- case len(deployments) > 1:
1966- return nil, fmt.Errorf("more than one Azure deployment inside the service named %q", azInstance.ServiceName)
1967- case len(deployments) == 1:
1968- deployment := deployments[0]
1969- switch {
1970- // Only zero or one role is a valid state (instance==service).
1971- case len(deployment.RoleList) > 1:
1972- return nil, fmt.Errorf("more than one Azure role inside the deployment named %q", deployment.Name)
1973- case len(deployment.RoleList) == 1:
1974- role := deployment.RoleList[0]
1975-
1976- endpoints, err := context.ListRoleEndpoints(&gwacl.ListRoleEndpointsRequest{
1977- ServiceName: azInstance.ServiceName,
1978- DeploymentName: deployment.Name,
1979- RoleName: role.RoleName,
1980- })
1981- if err != nil {
1982- return nil, err
1983- }
1984- ports := convertAndFilterEndpoints(endpoints, env)
1985- return ports, nil
1986- }
1987- return nil, nil
1988- }
1989- return nil, nil
1990+ ports := convertAndFilterEndpoints(endpoints, azInstance.environ, azInstance.maskStateServerPorts)
1991+ return ports, nil
1992 }
1993
1994=== modified file 'provider/azure/instance_test.go'
1995--- provider/azure/instance_test.go 2014-03-13 07:54:56 +0000
1996+++ provider/azure/instance_test.go 2014-04-03 07:35:44 +0000
1997@@ -13,12 +13,42 @@
1998 "launchpad.net/gwacl"
1999
2000 "launchpad.net/juju-core/instance"
2001+ "launchpad.net/juju-core/testing/testbase"
2002 )
2003
2004-type instanceSuite struct{}
2005+type instanceSuite struct {
2006+ testbase.LoggingSuite
2007+ env *azureEnviron
2008+ service *gwacl.HostedService
2009+ deployment *gwacl.Deployment
2010+ role *gwacl.Role
2011+ instance *azureInstance
2012+}
2013
2014 var _ = gc.Suite(&instanceSuite{})
2015
2016+func (s *instanceSuite) SetUpTest(c *gc.C) {
2017+ s.env = makeEnviron(c)
2018+ s.service = makeDeployment(s.env, "service-name")
2019+ s.deployment = &s.service.Deployments[0]
2020+ s.deployment.Name = "deployment-one"
2021+ s.role = &s.deployment.RoleList[0]
2022+ s.role.RoleName = "role-one"
2023+ inst, err := s.env.getInstance(s.service, s.role.RoleName)
2024+ c.Assert(err, gc.IsNil)
2025+ c.Assert(inst, gc.FitsTypeOf, &azureInstance{})
2026+ s.instance = inst.(*azureInstance)
2027+}
2028+
2029+func configSetNetwork(role *gwacl.Role) *gwacl.ConfigurationSet {
2030+ for i, configSet := range role.ConfigurationSets {
2031+ if configSet.ConfigurationSetType == gwacl.CONFIG_SET_NETWORK {
2032+ return &role.ConfigurationSets[i]
2033+ }
2034+ }
2035+ return nil
2036+}
2037+
2038 // makeHostedServiceDescriptor creates a HostedServiceDescriptor with the
2039 // given service name.
2040 func makeHostedServiceDescriptor(name string) *gwacl.HostedServiceDescriptor {
2041@@ -27,66 +57,50 @@
2042 }
2043
2044 func (*instanceSuite) TestId(c *gc.C) {
2045- serviceName := "test-name"
2046- testService := makeHostedServiceDescriptor(serviceName)
2047- azInstance := azureInstance{*testService, nil}
2048- c.Check(azInstance.Id(), gc.Equals, instance.Id(serviceName))
2049+ azInstance := azureInstance{instanceId: "whatever"}
2050+ c.Check(azInstance.Id(), gc.Equals, instance.Id("whatever"))
2051 }
2052
2053 func (*instanceSuite) TestStatus(c *gc.C) {
2054- serviceName := "test-name"
2055- testService := makeHostedServiceDescriptor(serviceName)
2056- testService.Status = "something"
2057- azInstance := azureInstance{*testService, nil}
2058- c.Check(azInstance.Status(), gc.Equals, testService.Status)
2059+ var inst azureInstance
2060+ c.Check(inst.Status(), gc.Equals, "")
2061+ inst.roleInstance = &gwacl.RoleInstance{InstanceStatus: "anyoldthing"}
2062+ c.Check(inst.Status(), gc.Equals, "anyoldthing")
2063 }
2064
2065 func (*instanceSuite) TestDNSName(c *gc.C) {
2066- // An instance's DNS name is computed from its hosted-service name.
2067- host := "hostname"
2068- testService := makeHostedServiceDescriptor(host)
2069- azInstance := azureInstance{*testService, nil}
2070+ testService := makeHostedServiceDescriptor("cloud-service-name")
2071+ azInstance := azureInstance{hostedService: testService}
2072 dnsName, err := azInstance.DNSName()
2073 c.Assert(err, gc.IsNil)
2074- c.Check(dnsName, gc.Equals, host+"."+AZURE_DOMAIN_NAME)
2075+ c.Check(dnsName, gc.Equals, "cloud-service-name.cloudapp.net")
2076 }
2077
2078 func (*instanceSuite) TestWaitDNSName(c *gc.C) {
2079 // An Azure instance gets its DNS name immediately, so there's no
2080 // waiting involved.
2081- host := "hostname"
2082- testService := makeHostedServiceDescriptor(host)
2083- azInstance := azureInstance{*testService, nil}
2084+ testService := makeHostedServiceDescriptor("cloud-service-name")
2085+ azInstance := azureInstance{hostedService: testService}
2086 dnsName, err := azInstance.WaitDNSName()
2087 c.Assert(err, gc.IsNil)
2088- c.Check(dnsName, gc.Equals, host+"."+AZURE_DOMAIN_NAME)
2089-}
2090-
2091-func makeRole(name string, endpoints ...gwacl.InputEndpoint) gwacl.Role {
2092- return gwacl.Role{
2093- RoleName: name,
2094- ConfigurationSets: []gwacl.ConfigurationSet{
2095- {
2096- ConfigurationSetType: gwacl.CONFIG_SET_NETWORK,
2097- InputEndpoints: &endpoints,
2098- },
2099- },
2100- }
2101-}
2102-
2103-func makeDeployment(name string, roles ...gwacl.Role) gwacl.Deployment {
2104- return gwacl.Deployment{
2105- Name: name,
2106- RoleList: roles,
2107- }
2108+ c.Check(dnsName, gc.Equals, "cloud-service-name.cloudapp.net")
2109 }
2110
2111 func makeInputEndpoint(port int, protocol string) gwacl.InputEndpoint {
2112+ name := fmt.Sprintf("%s%d", protocol, port)
2113+ probe := &gwacl.LoadBalancerProbe{Port: port, Protocol: "TCP"}
2114+ if protocol == "udp" {
2115+ // We just use port 22 (SSH) for the
2116+ // probe when a UDP port is exposed.
2117+ probe.Port = 22
2118+ }
2119 return gwacl.InputEndpoint{
2120 LocalPort: port,
2121- Name: fmt.Sprintf("%s%d", protocol, port),
2122- Port: port,
2123- Protocol: protocol,
2124+ Name: name,
2125+ LoadBalancedEndpointSetName: name,
2126+ LoadBalancerProbe: probe,
2127+ Port: port,
2128+ Protocol: protocol,
2129 }
2130 }
2131
2132@@ -104,36 +118,18 @@
2133 }
2134 }
2135
2136-func preparePortChangeConversation(
2137- c *gc.C, service *gwacl.HostedServiceDescriptor,
2138- deployments ...gwacl.Deployment) []gwacl.DispatcherResponse {
2139- // Construct the series of responses to expected requests.
2140- responses := []gwacl.DispatcherResponse{
2141- // First, GetHostedServiceProperties
2142- gwacl.NewDispatcherResponse(
2143- serialize(c, &gwacl.HostedService{
2144- Deployments: deployments,
2145- HostedServiceDescriptor: *service,
2146- XMLNS: gwacl.XMLNS,
2147- }),
2148- http.StatusOK, nil),
2149- }
2150- for _, deployment := range deployments {
2151- for _, role := range deployment.RoleList {
2152- // GetRole returns a PersistentVMRole.
2153- persistentRole := &gwacl.PersistentVMRole{
2154- XMLNS: gwacl.XMLNS,
2155- RoleName: role.RoleName,
2156- ConfigurationSets: role.ConfigurationSets,
2157- }
2158- responses = append(responses, gwacl.NewDispatcherResponse(
2159- serialize(c, persistentRole), http.StatusOK, nil))
2160- // UpdateRole expects a 200 response, that's all.
2161- responses = append(responses,
2162- gwacl.NewDispatcherResponse(nil, http.StatusOK, nil))
2163- }
2164- }
2165- return responses
2166+func preparePortChangeConversation(c *gc.C, role *gwacl.Role) []gwacl.DispatcherResponse {
2167+ persistentRole := &gwacl.PersistentVMRole{
2168+ XMLNS: gwacl.XMLNS,
2169+ RoleName: role.RoleName,
2170+ ConfigurationSets: role.ConfigurationSets,
2171+ }
2172+ return []gwacl.DispatcherResponse{
2173+ // GetRole returns a PersistentVMRole.
2174+ gwacl.NewDispatcherResponse(serialize(c, persistentRole), http.StatusOK, nil),
2175+ // UpdateRole expects a 200 response, that's all.
2176+ gwacl.NewDispatcherResponse(nil, http.StatusOK, nil),
2177+ }
2178 }
2179
2180 // point is 1-indexed; it represents which request should fail.
2181@@ -155,20 +151,16 @@
2182 }
2183 }
2184
2185-func (*instanceSuite) TestAddresses(c *gc.C) {
2186- name := "service-name"
2187- vnn := "Virt Net Name"
2188- service := makeHostedServiceDescriptor(name)
2189- responses := prepareDeploymentInfoResponse(c,
2190- gwacl.Deployment{
2191- RoleInstanceList: []gwacl.RoleInstance{
2192- gwacl.RoleInstance{IPAddress: "1.2.3.4"},
2193- },
2194- VirtualNetworkName: vnn,
2195- })
2196-
2197+func (s *instanceSuite) TestAddresses(c *gc.C) {
2198+ vnn := s.env.getVirtualNetworkName()
2199+ responses := prepareDeploymentInfoResponse(c, gwacl.Deployment{
2200+ RoleInstanceList: []gwacl.RoleInstance{{
2201+ RoleName: s.role.RoleName,
2202+ IPAddress: "1.2.3.4",
2203+ }},
2204+ VirtualNetworkName: vnn,
2205+ })
2206 gwacl.PatchManagementAPIResponses(responses)
2207- inst := azureInstance{*service, makeEnviron(c)}
2208
2209 expected := []instance.Address{
2210 instance.Address{
2211@@ -178,217 +170,153 @@
2212 instance.NetworkCloudLocal,
2213 },
2214 instance.Address{
2215- name + "." + AZURE_DOMAIN_NAME,
2216+ s.service.ServiceName + "." + AZURE_DOMAIN_NAME,
2217 instance.HostName,
2218 "",
2219 instance.NetworkPublic,
2220 },
2221 }
2222
2223- addrs, err := inst.Addresses()
2224+ addrs, err := s.instance.Addresses()
2225 c.Check(err, gc.IsNil)
2226-
2227 c.Check(addrs, jc.SameContents, expected)
2228 }
2229
2230-func (*instanceSuite) TestOpenPorts(c *gc.C) {
2231- service := makeHostedServiceDescriptor("service-name")
2232- responses := preparePortChangeConversation(c, service,
2233- makeDeployment("deployment-one",
2234- makeRole("role-one"), makeRole("role-two")),
2235- makeDeployment("deployment-two",
2236- makeRole("role-three")))
2237+func (s *instanceSuite) TestOpenPorts(c *gc.C) {
2238+ // Close the default ports.
2239+ configSetNetwork((*gwacl.Role)(s.role)).InputEndpoints = nil
2240+
2241+ responses := preparePortChangeConversation(c, s.role)
2242 record := gwacl.PatchManagementAPIResponses(responses)
2243- azInstance := azureInstance{*service, makeEnviron(c)}
2244-
2245- err := azInstance.OpenPorts("machine-id", []instance.Port{
2246+ err := s.instance.OpenPorts("machine-id", []instance.Port{
2247 {"tcp", 79}, {"tcp", 587}, {"udp", 9},
2248 })
2249-
2250 c.Assert(err, gc.IsNil)
2251+
2252 assertPortChangeConversation(c, *record, []expectedRequest{
2253- {"GET", ".*/services/hostedservices/service-name[?].*"}, // GetHostedServiceProperties
2254- {"GET", ".*/deployments/deployment-one/roles/role-one"}, // GetRole
2255- {"PUT", ".*/deployments/deployment-one/roles/role-one"}, // UpdateRole
2256- {"GET", ".*/deployments/deployment-one/roles/role-two"}, // GetRole
2257- {"PUT", ".*/deployments/deployment-one/roles/role-two"}, // UpdateRole
2258- {"GET", ".*/deployments/deployment-two/roles/role-three"}, // GetRole
2259- {"PUT", ".*/deployments/deployment-two/roles/role-three"}, // UpdateRole
2260+ {"GET", ".*/deployments/deployment-one/roles/role-one"}, // GetRole
2261+ {"PUT", ".*/deployments/deployment-one/roles/role-one"}, // UpdateRole
2262 })
2263
2264 // A representative UpdateRole payload includes configuration for the
2265 // ports requested.
2266 role := &gwacl.PersistentVMRole{}
2267- err = role.Deserialize((*record)[2].Payload)
2268+ err = role.Deserialize((*record)[1].Payload)
2269 c.Assert(err, gc.IsNil)
2270 c.Check(
2271- *(role.ConfigurationSets[0].InputEndpoints),
2272- gc.DeepEquals, []gwacl.InputEndpoint{
2273+ *configSetNetwork((*gwacl.Role)(role)).InputEndpoints,
2274+ gc.DeepEquals,
2275+ []gwacl.InputEndpoint{
2276 makeInputEndpoint(79, "tcp"),
2277 makeInputEndpoint(587, "tcp"),
2278 makeInputEndpoint(9, "udp"),
2279+ },
2280+ )
2281+}
2282+
2283+func (s *instanceSuite) TestOpenPortsFailsWhenUnableToGetRole(c *gc.C) {
2284+ responses := preparePortChangeConversation(c, s.role)
2285+ failPortChangeConversationAt(1, responses) // 1st request, GetRole
2286+ record := gwacl.PatchManagementAPIResponses(responses)
2287+ err := s.instance.OpenPorts("machine-id", []instance.Port{
2288+ {"tcp", 79}, {"tcp", 587}, {"udp", 9},
2289+ })
2290+ c.Check(err, gc.ErrorMatches, "GET request failed [(]500: Internal Server Error[)]")
2291+ c.Check(*record, gc.HasLen, 1)
2292+}
2293+
2294+func (s *instanceSuite) TestOpenPortsFailsWhenUnableToUpdateRole(c *gc.C) {
2295+ responses := preparePortChangeConversation(c, s.role)
2296+ failPortChangeConversationAt(2, responses) // 2nd request, UpdateRole
2297+ record := gwacl.PatchManagementAPIResponses(responses)
2298+ err := s.instance.OpenPorts("machine-id", []instance.Port{
2299+ {"tcp", 79}, {"tcp", 587}, {"udp", 9},
2300+ })
2301+ c.Check(err, gc.ErrorMatches, "PUT request failed [(]500: Internal Server Error[)]")
2302+ c.Check(*record, gc.HasLen, 2)
2303+}
2304+
2305+func (s *instanceSuite) TestClosePorts(c *gc.C) {
2306+ type test struct {
2307+ inputPorts []instance.Port
2308+ removePorts []instance.Port
2309+ outputPorts []instance.Port
2310+ }
2311+
2312+ tests := []test{{
2313+ inputPorts: []instance.Port{{"tcp", 1}, {"tcp", 2}, {"udp", 3}},
2314+ removePorts: nil,
2315+ outputPorts: []instance.Port{{"tcp", 1}, {"tcp", 2}, {"udp", 3}},
2316+ }, {
2317+ inputPorts: []instance.Port{{"tcp", 1}},
2318+ removePorts: []instance.Port{{"udp", 1}},
2319+ outputPorts: []instance.Port{{"tcp", 1}},
2320+ }, {
2321+ inputPorts: []instance.Port{{"tcp", 1}, {"tcp", 2}, {"udp", 3}},
2322+ removePorts: []instance.Port{{"tcp", 1}, {"tcp", 2}, {"udp", 3}},
2323+ outputPorts: []instance.Port{},
2324+ }, {
2325+ inputPorts: []instance.Port{{"tcp", 1}, {"tcp", 2}, {"udp", 3}},
2326+ removePorts: []instance.Port{{"tcp", 99}},
2327+ outputPorts: []instance.Port{{"tcp", 1}, {"tcp", 2}, {"udp", 3}},
2328+ }}
2329+
2330+ for i, test := range tests {
2331+ c.Logf("test %d: %#v", i, test)
2332+
2333+ inputEndpoints := make([]gwacl.InputEndpoint, len(test.inputPorts))
2334+ for i, port := range test.inputPorts {
2335+ inputEndpoints[i] = makeInputEndpoint(port.Number, port.Protocol)
2336+ }
2337+ configSetNetwork(s.role).InputEndpoints = &inputEndpoints
2338+ responses := preparePortChangeConversation(c, s.role)
2339+ record := gwacl.PatchManagementAPIResponses(responses)
2340+
2341+ err := s.instance.ClosePorts("machine-id", test.removePorts)
2342+ c.Assert(err, gc.IsNil)
2343+ assertPortChangeConversation(c, *record, []expectedRequest{
2344+ {"GET", ".*/deployments/deployment-one/roles/role-one"}, // GetRole
2345+ {"PUT", ".*/deployments/deployment-one/roles/role-one"}, // UpdateRole
2346 })
2347-}
2348-
2349-func (*instanceSuite) TestOpenPortsFailsWhenUnableToGetServiceProperties(c *gc.C) {
2350- service := makeHostedServiceDescriptor("service-name")
2351- responses := []gwacl.DispatcherResponse{
2352- // GetHostedServiceProperties breaks.
2353- gwacl.NewDispatcherResponse(nil, http.StatusInternalServerError, nil),
2354- }
2355- record := gwacl.PatchManagementAPIResponses(responses)
2356- azInstance := azureInstance{*service, makeEnviron(c)}
2357-
2358- err := azInstance.OpenPorts("machine-id", []instance.Port{
2359- {"tcp", 79}, {"tcp", 587}, {"udp", 9},
2360- })
2361-
2362- c.Check(err, gc.ErrorMatches, "GET request failed [(]500: Internal Server Error[)]")
2363- c.Check(*record, gc.HasLen, 1)
2364-}
2365-
2366-func (*instanceSuite) TestOpenPortsFailsWhenUnableToGetRole(c *gc.C) {
2367- service := makeHostedServiceDescriptor("service-name")
2368- responses := preparePortChangeConversation(c, service,
2369- makeDeployment("deployment-one", makeRole("role-one")))
2370- failPortChangeConversationAt(2, responses) // 2nd request, GetRole
2371- record := gwacl.PatchManagementAPIResponses(responses)
2372- azInstance := azureInstance{*service, makeEnviron(c)}
2373-
2374- err := azInstance.OpenPorts("machine-id", []instance.Port{
2375- {"tcp", 79}, {"tcp", 587}, {"udp", 9},
2376- })
2377-
2378- c.Check(err, gc.ErrorMatches, "GET request failed [(]500: Internal Server Error[)]")
2379- c.Check(*record, gc.HasLen, 2)
2380-}
2381-
2382-func (*instanceSuite) TestOpenPortsFailsWhenUnableToUpdateRole(c *gc.C) {
2383- service := makeHostedServiceDescriptor("service-name")
2384- responses := preparePortChangeConversation(c, service,
2385- makeDeployment("deployment-one", makeRole("role-one")))
2386- failPortChangeConversationAt(3, responses) // 3rd request, UpdateRole
2387- record := gwacl.PatchManagementAPIResponses(responses)
2388- azInstance := azureInstance{*service, makeEnviron(c)}
2389-
2390- err := azInstance.OpenPorts("machine-id", []instance.Port{
2391- {"tcp", 79}, {"tcp", 587}, {"udp", 9},
2392- })
2393-
2394- c.Check(err, gc.ErrorMatches, "PUT request failed [(]500: Internal Server Error[)]")
2395- c.Check(*record, gc.HasLen, 3)
2396-}
2397-
2398-func (*instanceSuite) TestClosePorts(c *gc.C) {
2399- service := makeHostedServiceDescriptor("service-name")
2400- responses := preparePortChangeConversation(c, service,
2401- makeDeployment("deployment-one",
2402- makeRole("role-one",
2403- makeInputEndpoint(587, "tcp"),
2404- ),
2405- makeRole("role-two",
2406- makeInputEndpoint(79, "tcp"),
2407- makeInputEndpoint(9, "udp"),
2408- )),
2409- makeDeployment("deployment-two",
2410- makeRole("role-three",
2411- makeInputEndpoint(9, "tcp"),
2412- makeInputEndpoint(9, "udp"),
2413- )))
2414- record := gwacl.PatchManagementAPIResponses(responses)
2415- azInstance := azureInstance{*service, makeEnviron(c)}
2416-
2417- err := azInstance.ClosePorts("machine-id",
2418- []instance.Port{{"tcp", 587}, {"udp", 9}})
2419-
2420- c.Assert(err, gc.IsNil)
2421- assertPortChangeConversation(c, *record, []expectedRequest{
2422- {"GET", ".*/services/hostedservices/service-name[?].*"}, // GetHostedServiceProperties
2423- {"GET", ".*/deployments/deployment-one/roles/role-one"}, // GetRole
2424- {"PUT", ".*/deployments/deployment-one/roles/role-one"}, // UpdateRole
2425- {"GET", ".*/deployments/deployment-one/roles/role-two"}, // GetRole
2426- {"PUT", ".*/deployments/deployment-one/roles/role-two"}, // UpdateRole
2427- {"GET", ".*/deployments/deployment-two/roles/role-three"}, // GetRole
2428- {"PUT", ".*/deployments/deployment-two/roles/role-three"}, // UpdateRole
2429- })
2430-
2431- // The first UpdateRole removes all endpoints from the role's
2432- // configuration.
2433- roleOne := &gwacl.PersistentVMRole{}
2434- err = roleOne.Deserialize((*record)[2].Payload)
2435- c.Assert(err, gc.IsNil)
2436- c.Check(roleOne.ConfigurationSets[0].InputEndpoints, gc.IsNil)
2437-
2438- // The second UpdateRole removes all but 79/TCP.
2439- roleTwo := &gwacl.PersistentVMRole{}
2440- err = roleTwo.Deserialize((*record)[4].Payload)
2441- c.Assert(err, gc.IsNil)
2442- c.Check(
2443- roleTwo.ConfigurationSets[0].InputEndpoints,
2444- gc.DeepEquals,
2445- &[]gwacl.InputEndpoint{makeInputEndpoint(79, "tcp")})
2446-
2447- // The third UpdateRole removes all but 9/TCP.
2448- roleThree := &gwacl.PersistentVMRole{}
2449- err = roleThree.Deserialize((*record)[6].Payload)
2450- c.Assert(err, gc.IsNil)
2451- c.Check(
2452- roleThree.ConfigurationSets[0].InputEndpoints,
2453- gc.DeepEquals,
2454- &[]gwacl.InputEndpoint{makeInputEndpoint(9, "tcp")})
2455-}
2456-
2457-func (*instanceSuite) TestClosePortsFailsWhenUnableToGetServiceProperties(c *gc.C) {
2458- service := makeHostedServiceDescriptor("service-name")
2459- responses := []gwacl.DispatcherResponse{
2460- // GetHostedServiceProperties breaks.
2461- gwacl.NewDispatcherResponse(nil, http.StatusInternalServerError, nil),
2462- }
2463- record := gwacl.PatchManagementAPIResponses(responses)
2464- azInstance := azureInstance{*service, makeEnviron(c)}
2465-
2466- err := azInstance.ClosePorts("machine-id", []instance.Port{
2467- {"tcp", 79}, {"tcp", 587}, {"udp", 9},
2468- })
2469-
2470- c.Check(err, gc.ErrorMatches, "GET request failed [(]500: Internal Server Error[)]")
2471- c.Check(*record, gc.HasLen, 1)
2472-}
2473-
2474-func (*instanceSuite) TestClosePortsFailsWhenUnableToGetRole(c *gc.C) {
2475- service := makeHostedServiceDescriptor("service-name")
2476- responses := preparePortChangeConversation(c, service,
2477- makeDeployment("deployment-one", makeRole("role-one")))
2478- failPortChangeConversationAt(2, responses) // 2nd request, GetRole
2479- record := gwacl.PatchManagementAPIResponses(responses)
2480- azInstance := azureInstance{*service, makeEnviron(c)}
2481-
2482- err := azInstance.ClosePorts("machine-id", []instance.Port{
2483- {"tcp", 79}, {"tcp", 587}, {"udp", 9},
2484- })
2485-
2486- c.Check(err, gc.ErrorMatches, "GET request failed [(]500: Internal Server Error[)]")
2487- c.Check(*record, gc.HasLen, 2)
2488-}
2489-
2490-func (*instanceSuite) TestClosePortsFailsWhenUnableToUpdateRole(c *gc.C) {
2491- service := makeHostedServiceDescriptor("service-name")
2492- responses := preparePortChangeConversation(c, service,
2493- makeDeployment("deployment-one", makeRole("role-one")))
2494- failPortChangeConversationAt(3, responses) // 3rd request, UpdateRole
2495- record := gwacl.PatchManagementAPIResponses(responses)
2496- azInstance := azureInstance{*service, makeEnviron(c)}
2497-
2498- err := azInstance.ClosePorts("machine-id", []instance.Port{
2499- {"tcp", 79}, {"tcp", 587}, {"udp", 9},
2500- })
2501-
2502- c.Check(err, gc.ErrorMatches, "PUT request failed [(]500: Internal Server Error[)]")
2503- c.Check(*record, gc.HasLen, 3)
2504-}
2505-
2506-func (*instanceSuite) TestConvertAndFilterEndpoints(c *gc.C) {
2507- env := makeEnviron(c)
2508+
2509+ // The first UpdateRole removes all endpoints from the role's
2510+ // configuration.
2511+ roleOne := &gwacl.PersistentVMRole{}
2512+ err = roleOne.Deserialize((*record)[1].Payload)
2513+ c.Assert(err, gc.IsNil)
2514+ endpoints := configSetNetwork((*gwacl.Role)(roleOne)).InputEndpoints
2515+ if len(test.outputPorts) == 0 {
2516+ c.Check(endpoints, gc.IsNil)
2517+ } else {
2518+ c.Check(endpoints, gc.NotNil)
2519+ c.Check(convertAndFilterEndpoints(*endpoints, s.env, false), gc.DeepEquals, test.outputPorts)
2520+ }
2521+ }
2522+}
2523+
2524+func (s *instanceSuite) TestClosePortsFailsWhenUnableToGetRole(c *gc.C) {
2525+ responses := preparePortChangeConversation(c, s.role)
2526+ failPortChangeConversationAt(1, responses) // 1st request, GetRole
2527+ record := gwacl.PatchManagementAPIResponses(responses)
2528+ err := s.instance.ClosePorts("machine-id", []instance.Port{
2529+ {"tcp", 79}, {"tcp", 587}, {"udp", 9},
2530+ })
2531+ c.Check(err, gc.ErrorMatches, "GET request failed [(]500: Internal Server Error[)]")
2532+ c.Check(*record, gc.HasLen, 1)
2533+}
2534+
2535+func (s *instanceSuite) TestClosePortsFailsWhenUnableToUpdateRole(c *gc.C) {
2536+ responses := preparePortChangeConversation(c, s.role)
2537+ failPortChangeConversationAt(2, responses) // 2nd request, UpdateRole
2538+ record := gwacl.PatchManagementAPIResponses(responses)
2539+ err := s.instance.ClosePorts("machine-id", []instance.Port{
2540+ {"tcp", 79}, {"tcp", 587}, {"udp", 9},
2541+ })
2542+ c.Check(err, gc.ErrorMatches, "PUT request failed [(]500: Internal Server Error[)]")
2543+ c.Check(*record, gc.HasLen, 2)
2544+}
2545+
2546+func (s *instanceSuite) TestConvertAndFilterEndpoints(c *gc.C) {
2547 endpoints := []gwacl.InputEndpoint{
2548 {
2549 LocalPort: 123,
2550@@ -402,7 +330,7 @@
2551 Name: "test456",
2552 Port: 44,
2553 }}
2554- endpoints = append(endpoints, env.getInitialEndpoints()...)
2555+ endpoints = append(endpoints, s.env.getInitialEndpoints(true)...)
2556 expectedPorts := []instance.Port{
2557 {
2558 Number: 1123,
2559@@ -412,99 +340,66 @@
2560 Number: 44,
2561 Protocol: "tcp",
2562 }}
2563- c.Check(convertAndFilterEndpoints(endpoints, env), gc.DeepEquals, expectedPorts)
2564+ c.Check(convertAndFilterEndpoints(endpoints, s.env, true), gc.DeepEquals, expectedPorts)
2565 }
2566
2567-func (*instanceSuite) TestConvertAndFilterEndpointsEmptySlice(c *gc.C) {
2568- env := makeEnviron(c)
2569- ports := convertAndFilterEndpoints([]gwacl.InputEndpoint{}, env)
2570+func (s *instanceSuite) TestConvertAndFilterEndpointsEmptySlice(c *gc.C) {
2571+ ports := convertAndFilterEndpoints([]gwacl.InputEndpoint{}, s.env, true)
2572 c.Check(ports, gc.HasLen, 0)
2573 }
2574
2575-func (*instanceSuite) TestPorts(c *gc.C) {
2576- service := makeHostedServiceDescriptor("service-name")
2577- endpoints := []gwacl.InputEndpoint{
2578- {
2579- LocalPort: 223,
2580- Protocol: "udp",
2581- Name: "test223",
2582- Port: 2123,
2583- },
2584- {
2585- LocalPort: 123,
2586- Protocol: "udp",
2587- Name: "test123",
2588- Port: 1123,
2589- },
2590- {
2591- LocalPort: 456,
2592- Protocol: "tcp",
2593- Name: "test456",
2594- Port: 4456,
2595- }}
2596-
2597- responses := preparePortChangeConversation(c, service,
2598- makeDeployment("deployment-one",
2599- makeRole("role-one", endpoints...)))
2600+func (s *instanceSuite) TestPorts(c *gc.C) {
2601+ s.testPorts(c, false)
2602+ s.testPorts(c, true)
2603+}
2604+
2605+func (s *instanceSuite) testPorts(c *gc.C, maskStateServerPorts bool) {
2606+ // Update the role's endpoints by hand.
2607+ configSetNetwork(s.role).InputEndpoints = &[]gwacl.InputEndpoint{{
2608+ LocalPort: 223,
2609+ Protocol: "udp",
2610+ Name: "test223",
2611+ Port: 2123,
2612+ }, {
2613+ LocalPort: 123,
2614+ Protocol: "udp",
2615+ Name: "test123",
2616+ Port: 1123,
2617+ }, {
2618+ LocalPort: 456,
2619+ Protocol: "tcp",
2620+ Name: "test456",
2621+ Port: 4456,
2622+ }, {
2623+ LocalPort: s.env.Config().StatePort(),
2624+ Protocol: "tcp",
2625+ Name: "stateserver",
2626+ Port: s.env.Config().StatePort(),
2627+ }, {
2628+ LocalPort: s.env.Config().APIPort(),
2629+ Protocol: "tcp",
2630+ Name: "apiserver",
2631+ Port: s.env.Config().APIPort(),
2632+ }}
2633+
2634+ responses := preparePortChangeConversation(c, s.role)
2635 record := gwacl.PatchManagementAPIResponses(responses)
2636- azInstance := azureInstance{*service, makeEnviron(c)}
2637-
2638- ports, err := azInstance.Ports("machine-id")
2639-
2640+ s.instance.maskStateServerPorts = maskStateServerPorts
2641+ ports, err := s.instance.Ports("machine-id")
2642 c.Assert(err, gc.IsNil)
2643 assertPortChangeConversation(c, *record, []expectedRequest{
2644- {"GET", ".*/services/hostedservices/service-name[?].*"}, // GetHostedServiceProperties
2645 {"GET", ".*/deployments/deployment-one/roles/role-one"}, // GetRole
2646 })
2647
2648- c.Check(
2649- ports,
2650- gc.DeepEquals,
2651- // The result is sorted using instance.SortPorts() (i.e. first by protocol,
2652- // then by number).
2653- []instance.Port{
2654- {Number: 4456, Protocol: "tcp"},
2655- {Number: 1123, Protocol: "udp"},
2656- {Number: 2123, Protocol: "udp"},
2657- })
2658-}
2659-
2660-func (*instanceSuite) TestPortsErrorsIfMoreThanOneRole(c *gc.C) {
2661- service := makeHostedServiceDescriptor("service-name")
2662- responses := preparePortChangeConversation(c, service,
2663- makeDeployment("deployment-one",
2664- makeRole("role-one"), makeRole("role-two")))
2665- gwacl.PatchManagementAPIResponses(responses)
2666- azInstance := azureInstance{*service, makeEnviron(c)}
2667-
2668- _, err := azInstance.Ports("machine-id")
2669-
2670- c.Check(err, gc.ErrorMatches, ".*more than one Azure role inside the deployment.*")
2671-}
2672-
2673-func (*instanceSuite) TestPortsErrorsIfMoreThanOneDeployment(c *gc.C) {
2674- service := makeHostedServiceDescriptor("service-name")
2675- responses := preparePortChangeConversation(c, service,
2676- makeDeployment("deployment-one",
2677- makeRole("role-one")),
2678- makeDeployment("deployment-two",
2679- makeRole("role-two")))
2680- gwacl.PatchManagementAPIResponses(responses)
2681- azInstance := azureInstance{*service, makeEnviron(c)}
2682-
2683- _, err := azInstance.Ports("machine-id")
2684-
2685- c.Check(err, gc.ErrorMatches, ".*more than one Azure deployment inside the service.*")
2686-}
2687-
2688-func (*instanceSuite) TestPortsReturnsEmptySliceIfNoDeployment(c *gc.C) {
2689- service := makeHostedServiceDescriptor("service-name")
2690- responses := preparePortChangeConversation(c, service)
2691- gwacl.PatchManagementAPIResponses(responses)
2692- azInstance := azureInstance{*service, makeEnviron(c)}
2693-
2694- ports, err := azInstance.Ports("machine-id")
2695-
2696- c.Assert(err, gc.IsNil)
2697- c.Check(ports, gc.HasLen, 0)
2698+ expected := []instance.Port{
2699+ {Number: 4456, Protocol: "tcp"},
2700+ {Number: 1123, Protocol: "udp"},
2701+ {Number: 2123, Protocol: "udp"},
2702+ }
2703+ if !maskStateServerPorts {
2704+ expected = append(expected, instance.Port{Number: s.env.Config().StatePort(), Protocol: "tcp"})
2705+ expected = append(expected, instance.Port{Number: s.env.Config().APIPort(), Protocol: "tcp"})
2706+ instance.SortPorts(expected)
2707+ }
2708+ c.Check(ports, gc.DeepEquals, expected)
2709 }

Subscribers

People subscribed via source and target branches

to status/vote changes: