github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/provider/azure/environ_test.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package azure_test
     5  
     6  import (
     7  	"encoding/json"
     8  	"fmt"
     9  	"io/ioutil"
    10  	"net/http"
    11  	"path"
    12  	"reflect"
    13  	"time"
    14  
    15  	autorestazure "github.com/Azure/azure-sdk-for-go/Godeps/_workspace/src/github.com/Azure/go-autorest/autorest/azure"
    16  	"github.com/Azure/azure-sdk-for-go/Godeps/_workspace/src/github.com/Azure/go-autorest/autorest/mocks"
    17  	"github.com/Azure/azure-sdk-for-go/Godeps/_workspace/src/github.com/Azure/go-autorest/autorest/to"
    18  	"github.com/Azure/azure-sdk-for-go/arm/compute"
    19  	"github.com/Azure/azure-sdk-for-go/arm/network"
    20  	"github.com/Azure/azure-sdk-for-go/arm/resources"
    21  	"github.com/Azure/azure-sdk-for-go/arm/storage"
    22  	"github.com/juju/names"
    23  	gitjujutesting "github.com/juju/testing"
    24  	jc "github.com/juju/testing/checkers"
    25  	"github.com/juju/utils/arch"
    26  	"github.com/juju/utils/series"
    27  	gc "gopkg.in/check.v1"
    28  
    29  	"github.com/juju/juju/api"
    30  	"github.com/juju/juju/cloudconfig/instancecfg"
    31  	"github.com/juju/juju/constraints"
    32  	"github.com/juju/juju/environs"
    33  	"github.com/juju/juju/environs/imagemetadata"
    34  	"github.com/juju/juju/environs/simplestreams"
    35  	"github.com/juju/juju/environs/tags"
    36  	envtesting "github.com/juju/juju/environs/testing"
    37  	envtools "github.com/juju/juju/environs/tools"
    38  	"github.com/juju/juju/instance"
    39  	"github.com/juju/juju/mongo"
    40  	"github.com/juju/juju/provider/azure"
    41  	"github.com/juju/juju/provider/azure/internal/azuretesting"
    42  	"github.com/juju/juju/testing"
    43  	"github.com/juju/juju/tools"
    44  	"github.com/juju/version"
    45  )
    46  
    47  type environSuite struct {
    48  	testing.BaseSuite
    49  
    50  	provider      environs.EnvironProvider
    51  	requests      []*http.Request
    52  	storageClient azuretesting.MockStorageClient
    53  	sender        azuretesting.Senders
    54  	retryClock    mockClock
    55  
    56  	tags                          map[string]*string
    57  	group                         *resources.ResourceGroup
    58  	vmSizes                       *compute.VirtualMachineSizeListResult
    59  	storageNameAvailabilityResult *storage.CheckNameAvailabilityResult
    60  	storageAccount                *storage.Account
    61  	storageAccountKeys            *storage.AccountKeys
    62  	vnet                          *network.VirtualNetwork
    63  	nsg                           *network.SecurityGroup
    64  	subnet                        *network.Subnet
    65  	ubuntuServerSKUs              []compute.VirtualMachineImageResource
    66  	publicIPAddress               *network.PublicIPAddress
    67  	oldNetworkInterfaces          *network.InterfaceListResult
    68  	newNetworkInterface           *network.Interface
    69  	jujuAvailabilitySet           *compute.AvailabilitySet
    70  	virtualMachine                *compute.VirtualMachine
    71  }
    72  
    73  var _ = gc.Suite(&environSuite{})
    74  
    75  func (s *environSuite) SetUpTest(c *gc.C) {
    76  	s.BaseSuite.SetUpTest(c)
    77  	s.storageClient = azuretesting.MockStorageClient{}
    78  	s.sender = nil
    79  	s.retryClock = mockClock{Clock: testing.NewClock(time.Time{})}
    80  
    81  	s.provider, _ = newProviders(c, azure.ProviderConfig{
    82  		Sender:           &s.sender,
    83  		RequestInspector: requestRecorder(&s.requests),
    84  		NewStorageClient: s.storageClient.NewClient,
    85  		RetryClock: &testing.AutoAdvancingClock{
    86  			&s.retryClock, s.retryClock.Advance,
    87  		},
    88  	})
    89  
    90  	envTags := map[string]*string{
    91  		"juju-model-uuid":      to.StringPtr(testing.ModelTag.Id()),
    92  		"juju-controller-uuid": to.StringPtr(testing.ModelTag.Id()),
    93  	}
    94  	s.tags = map[string]*string{
    95  		"juju-machine-name": to.StringPtr("machine-0"),
    96  	}
    97  
    98  	s.group = &resources.ResourceGroup{
    99  		Location: to.StringPtr("westus"),
   100  		Tags:     &envTags,
   101  	}
   102  
   103  	vmSizes := []compute.VirtualMachineSize{{
   104  		Name:                 to.StringPtr("Standard_D1"),
   105  		NumberOfCores:        to.IntPtr(1),
   106  		OsDiskSizeInMB:       to.IntPtr(1047552),
   107  		ResourceDiskSizeInMB: to.IntPtr(51200),
   108  		MemoryInMB:           to.IntPtr(3584),
   109  		MaxDataDiskCount:     to.IntPtr(2),
   110  	}}
   111  	s.vmSizes = &compute.VirtualMachineSizeListResult{Value: &vmSizes}
   112  
   113  	s.storageNameAvailabilityResult = &storage.CheckNameAvailabilityResult{
   114  		NameAvailable: to.BoolPtr(true),
   115  	}
   116  
   117  	s.storageAccount = &storage.Account{
   118  		Name: to.StringPtr("my-storage-account"),
   119  		Type: to.StringPtr("Standard_LRS"),
   120  		Tags: &envTags,
   121  		Properties: &storage.AccountProperties{
   122  			PrimaryEndpoints: &storage.Endpoints{
   123  				Blob: to.StringPtr(fmt.Sprintf("https://%s.blob.storage.azurestack.local/", fakeStorageAccount)),
   124  			},
   125  		},
   126  	}
   127  
   128  	s.storageAccountKeys = &storage.AccountKeys{
   129  		Key1: to.StringPtr("key-1"),
   130  	}
   131  
   132  	addressPrefixes := []string{"10.0.0.0/16"}
   133  	s.vnet = &network.VirtualNetwork{
   134  		ID:       to.StringPtr("juju-internal-network"),
   135  		Name:     to.StringPtr("juju-internal-network"),
   136  		Location: to.StringPtr("westus"),
   137  		Tags:     &envTags,
   138  		Properties: &network.VirtualNetworkPropertiesFormat{
   139  			AddressSpace: &network.AddressSpace{&addressPrefixes},
   140  		},
   141  	}
   142  
   143  	s.nsg = &network.SecurityGroup{
   144  		ID: to.StringPtr(path.Join(
   145  			"/subscriptions", fakeSubscriptionId,
   146  			"resourceGroups", "juju-testenv-model-"+testing.ModelTag.Id(),
   147  			"providers/Microsoft.Network/networkSecurityGroups/juju-internal-nsg",
   148  		)),
   149  		Tags: &envTags,
   150  	}
   151  
   152  	s.subnet = &network.Subnet{
   153  		ID:   to.StringPtr("subnet-id"),
   154  		Name: to.StringPtr("juju-internal-subnet"),
   155  		Properties: &network.SubnetPropertiesFormat{
   156  			AddressPrefix:        to.StringPtr("10.0.0.0/16"),
   157  			NetworkSecurityGroup: &network.SubResource{s.nsg.ID},
   158  		},
   159  	}
   160  
   161  	s.ubuntuServerSKUs = []compute.VirtualMachineImageResource{
   162  		{Name: to.StringPtr("12.04-LTS")},
   163  		{Name: to.StringPtr("12.10")},
   164  		{Name: to.StringPtr("14.04-LTS")},
   165  		{Name: to.StringPtr("15.04")},
   166  		{Name: to.StringPtr("15.10")},
   167  		{Name: to.StringPtr("16.04-LTS")},
   168  	}
   169  
   170  	s.publicIPAddress = &network.PublicIPAddress{
   171  		ID:       to.StringPtr("public-ip-id"),
   172  		Name:     to.StringPtr("machine-0-public-ip"),
   173  		Location: to.StringPtr("westus"),
   174  		Tags:     &s.tags,
   175  		Properties: &network.PublicIPAddressPropertiesFormat{
   176  			PublicIPAllocationMethod: network.Dynamic,
   177  			IPAddress:                to.StringPtr("1.2.3.4"),
   178  		},
   179  	}
   180  
   181  	// Existing IPs/NICs. These are the results of querying NICs so we
   182  	// can tell which IP to allocate.
   183  	oldIPConfigurations := []network.InterfaceIPConfiguration{{
   184  		ID:   to.StringPtr("ip-configuration-0-id"),
   185  		Name: to.StringPtr("ip-configuration-0"),
   186  		Properties: &network.InterfaceIPConfigurationPropertiesFormat{
   187  			PrivateIPAddress:          to.StringPtr("10.0.0.4"),
   188  			PrivateIPAllocationMethod: network.Static,
   189  			Subnet: &network.SubResource{ID: s.subnet.ID},
   190  		},
   191  	}}
   192  	oldNetworkInterfaces := []network.Interface{{
   193  		ID:   to.StringPtr("network-interface-0-id"),
   194  		Name: to.StringPtr("network-interface-0"),
   195  		Properties: &network.InterfacePropertiesFormat{
   196  			IPConfigurations: &oldIPConfigurations,
   197  			Primary:          to.BoolPtr(true),
   198  		},
   199  	}}
   200  	s.oldNetworkInterfaces = &network.InterfaceListResult{
   201  		Value: &oldNetworkInterfaces,
   202  	}
   203  
   204  	// The newly created IP/NIC.
   205  	newIPConfigurations := []network.InterfaceIPConfiguration{{
   206  		ID:   to.StringPtr("ip-configuration-1-id"),
   207  		Name: to.StringPtr("primary"),
   208  		Properties: &network.InterfaceIPConfigurationPropertiesFormat{
   209  			PrivateIPAddress:          to.StringPtr("10.0.0.5"),
   210  			PrivateIPAllocationMethod: network.Static,
   211  			Subnet:          &network.SubResource{ID: s.subnet.ID},
   212  			PublicIPAddress: &network.SubResource{ID: s.publicIPAddress.ID},
   213  		},
   214  	}}
   215  	s.newNetworkInterface = &network.Interface{
   216  		ID:       to.StringPtr("network-interface-1-id"),
   217  		Name:     to.StringPtr("network-interface-1"),
   218  		Location: to.StringPtr("westus"),
   219  		Tags:     &s.tags,
   220  		Properties: &network.InterfacePropertiesFormat{
   221  			IPConfigurations: &newIPConfigurations,
   222  		},
   223  	}
   224  
   225  	s.jujuAvailabilitySet = &compute.AvailabilitySet{
   226  		ID:       to.StringPtr("juju-availability-set-id"),
   227  		Name:     to.StringPtr("juju"),
   228  		Location: to.StringPtr("westus"),
   229  		Tags:     &envTags,
   230  	}
   231  
   232  	sshPublicKeys := []compute.SSHPublicKey{{
   233  		Path:    to.StringPtr("/home/ubuntu/.ssh/authorized_keys"),
   234  		KeyData: to.StringPtr(testing.FakeAuthKeys),
   235  	}}
   236  	networkInterfaceReferences := []compute.NetworkInterfaceReference{{
   237  		ID: s.newNetworkInterface.ID,
   238  		Properties: &compute.NetworkInterfaceReferenceProperties{
   239  			Primary: to.BoolPtr(true),
   240  		},
   241  	}}
   242  	s.virtualMachine = &compute.VirtualMachine{
   243  		ID:       to.StringPtr("machine-0-id"),
   244  		Name:     to.StringPtr("machine-0"),
   245  		Location: to.StringPtr("westus"),
   246  		Tags:     &s.tags,
   247  		Properties: &compute.VirtualMachineProperties{
   248  			HardwareProfile: &compute.HardwareProfile{
   249  				VMSize: "Standard_D1",
   250  			},
   251  			StorageProfile: &compute.StorageProfile{
   252  				ImageReference: &compute.ImageReference{
   253  					Publisher: to.StringPtr("Canonical"),
   254  					Offer:     to.StringPtr("UbuntuServer"),
   255  					Sku:       to.StringPtr("12.10"),
   256  					Version:   to.StringPtr("latest"),
   257  				},
   258  				OsDisk: &compute.OSDisk{
   259  					Name:         to.StringPtr("machine-0"),
   260  					CreateOption: compute.FromImage,
   261  					Caching:      compute.ReadWrite,
   262  					Vhd: &compute.VirtualHardDisk{
   263  						URI: to.StringPtr(fmt.Sprintf(
   264  							"https://%s.blob.storage.azurestack.local/osvhds/machine-0.vhd",
   265  							fakeStorageAccount,
   266  						)),
   267  					},
   268  				},
   269  			},
   270  			OsProfile: &compute.OSProfile{
   271  				ComputerName:  to.StringPtr("machine-0"),
   272  				CustomData:    to.StringPtr("<juju-goes-here>"),
   273  				AdminUsername: to.StringPtr("ubuntu"),
   274  				LinuxConfiguration: &compute.LinuxConfiguration{
   275  					DisablePasswordAuthentication: to.BoolPtr(true),
   276  					SSH: &compute.SSHConfiguration{
   277  						PublicKeys: &sshPublicKeys,
   278  					},
   279  				},
   280  			},
   281  			NetworkProfile: &compute.NetworkProfile{
   282  				NetworkInterfaces: &networkInterfaceReferences,
   283  			},
   284  			AvailabilitySet:   &compute.SubResource{ID: s.jujuAvailabilitySet.ID},
   285  			ProvisioningState: to.StringPtr("Successful"),
   286  		},
   287  	}
   288  }
   289  
   290  func (s *environSuite) openEnviron(c *gc.C, attrs ...testing.Attrs) environs.Environ {
   291  	attrs = append([]testing.Attrs{{"storage-account": fakeStorageAccount}}, attrs...)
   292  	return openEnviron(c, s.provider, &s.sender, attrs...)
   293  }
   294  
   295  func openEnviron(
   296  	c *gc.C,
   297  	provider environs.EnvironProvider,
   298  	sender *azuretesting.Senders,
   299  	attrs ...testing.Attrs,
   300  ) environs.Environ {
   301  	// Opening the environment should not incur network communication,
   302  	// so we don't set s.sender until after opening.
   303  	cfg := makeTestModelConfig(c, attrs...)
   304  	env, err := provider.Open(cfg)
   305  	c.Assert(err, jc.ErrorIsNil)
   306  
   307  	// Force an explicit refresh of the access token, so it isn't done
   308  	// implicitly during the tests.
   309  	*sender = azuretesting.Senders{tokenRefreshSender()}
   310  	err = azure.ForceTokenRefresh(env)
   311  	c.Assert(err, jc.ErrorIsNil)
   312  	return env
   313  }
   314  
   315  func prepareForBootstrap(
   316  	c *gc.C,
   317  	ctx environs.BootstrapContext,
   318  	provider environs.EnvironProvider,
   319  	sender *azuretesting.Senders,
   320  	attrs ...testing.Attrs,
   321  ) environs.Environ {
   322  	// Opening the environment should not incur network communication,
   323  	// so we don't set s.sender until after opening.
   324  	cfg := makeTestModelConfig(c, attrs...)
   325  	*sender = azuretesting.Senders{tokenRefreshSender()}
   326  	cfg, err := provider.BootstrapConfig(environs.BootstrapConfigParams{
   327  		Config:               cfg,
   328  		CloudRegion:          "westus",
   329  		CloudEndpoint:        "https://management.azure.com",
   330  		CloudStorageEndpoint: "https://core.windows.net",
   331  		Credentials:          fakeUserPassCredential(),
   332  	})
   333  	c.Assert(err, jc.ErrorIsNil)
   334  	env, err := provider.PrepareForBootstrap(ctx, cfg)
   335  	c.Assert(err, jc.ErrorIsNil)
   336  	return env
   337  }
   338  
   339  func tokenRefreshSender() *azuretesting.MockSender {
   340  	tokenRefreshSender := azuretesting.NewSenderWithValue(&autorestazure.Token{
   341  		AccessToken: "access-token",
   342  		ExpiresOn:   fmt.Sprint(time.Now().Add(time.Hour).Unix()),
   343  		Type:        "Bearer",
   344  	})
   345  	tokenRefreshSender.PathPattern = ".*/oauth2/token"
   346  	return tokenRefreshSender
   347  }
   348  
   349  func (s *environSuite) initResourceGroupSenders() azuretesting.Senders {
   350  	resourceGroupName := "juju-testenv-model-deadbeef-0bad-400d-8000-4b1d0d06f00d"
   351  	return azuretesting.Senders{
   352  		s.makeSender(".*/resourcegroups/"+resourceGroupName, s.group),
   353  		s.makeSender(".*/virtualnetworks/juju-internal-network", s.vnet),
   354  		s.makeSender(".*/networkSecurityGroups/juju-internal-nsg", s.nsg),
   355  		s.makeSender(".*/virtualnetworks/juju-internal-network/subnets/juju-internal-subnet", s.subnet),
   356  		s.makeSender(".*/checkNameAvailability", s.storageNameAvailabilityResult),
   357  		s.makeSender(".*/storageAccounts/.*", s.storageAccount),
   358  		s.makeSender(".*/storageAccounts/.*/listKeys", s.storageAccountKeys),
   359  	}
   360  }
   361  
   362  func (s *environSuite) startInstanceSenders(controller bool) azuretesting.Senders {
   363  	senders := azuretesting.Senders{
   364  		s.vmSizesSender(),
   365  		s.makeSender(".*/subnets/juju-internal-subnet", s.subnet),
   366  		s.makeSender(".*/Canonical/.*/UbuntuServer/skus", s.ubuntuServerSKUs),
   367  		s.makeSender(".*/publicIPAddresses/machine-0-public-ip", s.publicIPAddress),
   368  		s.makeSender(".*/networkInterfaces", s.oldNetworkInterfaces),
   369  		s.makeSender(".*/networkInterfaces/machine-0-primary", s.newNetworkInterface),
   370  	}
   371  	if controller {
   372  		senders = append(senders,
   373  			s.makeSender(".*/networkSecurityGroups/juju-internal-nsg", &network.SecurityGroup{
   374  				Properties: &network.SecurityGroupPropertiesFormat{},
   375  			}),
   376  			s.makeSender(".*/networkSecurityGroups/juju-internal-nsg", &network.SecurityGroup{}),
   377  		)
   378  	}
   379  	senders = append(senders,
   380  		s.makeSender(".*/availabilitySets/.*", s.jujuAvailabilitySet),
   381  		s.makeSender(".*/virtualMachines/machine-0", s.virtualMachine),
   382  	)
   383  	return senders
   384  }
   385  
   386  func (s *environSuite) networkInterfacesSender(nics ...network.Interface) *azuretesting.MockSender {
   387  	return s.makeSender(".*/networkInterfaces", network.InterfaceListResult{Value: &nics})
   388  }
   389  
   390  func (s *environSuite) publicIPAddressesSender(pips ...network.PublicIPAddress) *azuretesting.MockSender {
   391  	return s.makeSender(".*/publicIPAddresses", network.PublicIPAddressListResult{Value: &pips})
   392  }
   393  
   394  func (s *environSuite) virtualMachinesSender(vms ...compute.VirtualMachine) *azuretesting.MockSender {
   395  	return s.makeSender(".*/virtualMachines", compute.VirtualMachineListResult{Value: &vms})
   396  }
   397  
   398  func (s *environSuite) vmSizesSender() *azuretesting.MockSender {
   399  	return s.makeSender(".*/vmSizes", s.vmSizes)
   400  }
   401  
   402  func (s *environSuite) makeSender(pattern string, v interface{}) *azuretesting.MockSender {
   403  	sender := azuretesting.NewSenderWithValue(v)
   404  	sender.PathPattern = pattern
   405  	return sender
   406  }
   407  
   408  func makeStartInstanceParams(c *gc.C, series string) environs.StartInstanceParams {
   409  	machineTag := names.NewMachineTag("0")
   410  	stateInfo := &mongo.MongoInfo{
   411  		Info: mongo.Info{
   412  			CACert: testing.CACert,
   413  			Addrs:  []string{"localhost:123"},
   414  		},
   415  		Password: "password",
   416  		Tag:      machineTag,
   417  	}
   418  	apiInfo := &api.Info{
   419  		Addrs:    []string{"localhost:246"},
   420  		CACert:   testing.CACert,
   421  		Password: "admin",
   422  		Tag:      machineTag,
   423  		ModelTag: testing.ModelTag,
   424  	}
   425  
   426  	const secureServerConnections = true
   427  	icfg, err := instancecfg.NewInstanceConfig(
   428  		machineTag.Id(), "yanonce", imagemetadata.ReleasedStream,
   429  		series, "", secureServerConnections, stateInfo, apiInfo,
   430  	)
   431  	c.Assert(err, jc.ErrorIsNil)
   432  
   433  	return environs.StartInstanceParams{
   434  		Tools:          makeToolsList(series),
   435  		InstanceConfig: icfg,
   436  	}
   437  }
   438  
   439  func makeToolsList(series string) tools.List {
   440  	var toolsVersion version.Binary
   441  	toolsVersion.Number = version.MustParse("1.26.0")
   442  	toolsVersion.Arch = arch.AMD64
   443  	toolsVersion.Series = series
   444  	return tools.List{{
   445  		Version: toolsVersion,
   446  		URL:     fmt.Sprintf("http://example.com/tools/juju-%s.tgz", toolsVersion),
   447  		SHA256:  "1234567890abcdef",
   448  		Size:    1024,
   449  	}}
   450  }
   451  
   452  func unmarshalRequestBody(c *gc.C, req *http.Request, out interface{}) {
   453  	bytes, err := ioutil.ReadAll(req.Body)
   454  	c.Assert(err, jc.ErrorIsNil)
   455  	err = json.Unmarshal(bytes, out)
   456  	c.Assert(err, jc.ErrorIsNil)
   457  }
   458  
   459  func assertRequestBody(c *gc.C, req *http.Request, expect interface{}) {
   460  	unmarshalled := reflect.New(reflect.TypeOf(expect).Elem()).Interface()
   461  	unmarshalRequestBody(c, req, unmarshalled)
   462  	c.Assert(unmarshalled, jc.DeepEquals, expect)
   463  }
   464  
   465  type mockClock struct {
   466  	gitjujutesting.Stub
   467  	*testing.Clock
   468  }
   469  
   470  func (c *mockClock) After(d time.Duration) <-chan time.Time {
   471  	c.MethodCall(c, "After", d)
   472  	c.PopNoErr()
   473  	return c.Clock.After(d)
   474  }
   475  
   476  func (s *environSuite) TestOpen(c *gc.C) {
   477  	cfg := makeTestModelConfig(c)
   478  	env, err := s.provider.Open(cfg)
   479  	c.Assert(err, jc.ErrorIsNil)
   480  	c.Assert(env, gc.NotNil)
   481  }
   482  
   483  func (s *environSuite) TestCloudEndpointManagementURI(c *gc.C) {
   484  	env := s.openEnviron(c)
   485  
   486  	sender := mocks.NewSender()
   487  	sender.EmitContent("{}")
   488  	s.sender = azuretesting.Senders{sender}
   489  	s.requests = nil
   490  	env.AllInstances() // trigger a query
   491  
   492  	c.Assert(s.requests, gc.HasLen, 1)
   493  	c.Assert(s.requests[0].URL.Host, gc.Equals, "api.azurestack.local")
   494  }
   495  
   496  func (s *environSuite) TestStartInstance(c *gc.C) {
   497  	env := s.openEnviron(c)
   498  	s.sender = s.startInstanceSenders(false)
   499  	s.requests = nil
   500  	result, err := env.StartInstance(makeStartInstanceParams(c, "quantal"))
   501  	c.Assert(err, jc.ErrorIsNil)
   502  	c.Assert(result, gc.NotNil)
   503  	c.Assert(result.Instance, gc.NotNil)
   504  	c.Assert(result.NetworkInfo, gc.HasLen, 0)
   505  	c.Assert(result.Volumes, gc.HasLen, 0)
   506  	c.Assert(result.VolumeAttachments, gc.HasLen, 0)
   507  
   508  	arch := "amd64"
   509  	mem := uint64(3584)
   510  	rootDisk := uint64(29495) // ~30 GB
   511  	cpuCores := uint64(1)
   512  	c.Assert(result.Hardware, jc.DeepEquals, &instance.HardwareCharacteristics{
   513  		Arch:     &arch,
   514  		Mem:      &mem,
   515  		RootDisk: &rootDisk,
   516  		CpuCores: &cpuCores,
   517  	})
   518  	requests := s.assertStartInstanceRequests(c, s.requests)
   519  	availabilitySetName := path.Base(requests.availabilitySet.URL.Path)
   520  	c.Assert(availabilitySetName, gc.Equals, "juju")
   521  }
   522  
   523  func (s *environSuite) TestStartInstanceTooManyRequests(c *gc.C) {
   524  	env := s.openEnviron(c)
   525  	senders := s.startInstanceSenders(false)
   526  	s.requests = nil
   527  
   528  	// 6 failures to get to 1 minute, and show that we cap it there.
   529  	const failures = 6
   530  
   531  	// Make the VirtualMachines.CreateOrUpdate call respond with
   532  	// 429 (StatusTooManyRequests) failures, and then with success.
   533  	rateLimitedSender := mocks.NewSender()
   534  	rateLimitedSender.EmitStatus("(」゜ロ゜)」", http.StatusTooManyRequests)
   535  	successSender := senders[len(senders)-1]
   536  	senders = senders[:len(senders)-1]
   537  	for i := 0; i < failures; i++ {
   538  		senders = append(senders, rateLimitedSender)
   539  	}
   540  	senders = append(senders, successSender)
   541  	s.sender = senders
   542  
   543  	_, err := env.StartInstance(makeStartInstanceParams(c, "quantal"))
   544  	c.Assert(err, jc.ErrorIsNil)
   545  
   546  	c.Assert(s.requests, gc.HasLen, 8+failures)
   547  	s.assertStartInstanceRequests(c, s.requests[:8])
   548  
   549  	// The last two requests should match the third-to-last, which
   550  	// is checked by assertStartInstanceRequests.
   551  	for i := 8; i < 8+failures; i++ {
   552  		c.Assert(s.requests[i].Method, gc.Equals, "PUT")
   553  		assertCreateVirtualMachineRequestBody(c, s.requests[i], s.virtualMachine)
   554  	}
   555  
   556  	s.retryClock.CheckCalls(c, []gitjujutesting.StubCall{
   557  		{"After", []interface{}{5 * time.Second}},
   558  		{"After", []interface{}{10 * time.Second}},
   559  		{"After", []interface{}{20 * time.Second}},
   560  		{"After", []interface{}{40 * time.Second}},
   561  		{"After", []interface{}{1 * time.Minute}},
   562  		{"After", []interface{}{1 * time.Minute}},
   563  	})
   564  }
   565  
   566  func (s *environSuite) TestStartInstanceTooManyRequestsTimeout(c *gc.C) {
   567  	env := s.openEnviron(c)
   568  	senders := s.startInstanceSenders(false)
   569  	s.requests = nil
   570  
   571  	// 8 failures to get to 5 minutes, which is as long as we'll keep
   572  	// retrying before giving up.
   573  	const failures = 8
   574  
   575  	// Make the VirtualMachines.CreateOrUpdate call respond with
   576  	// enough 429 (StatusTooManyRequests) failures to cause the
   577  	// method to give up retrying.
   578  	rateLimitedSender := mocks.NewSender()
   579  	rateLimitedSender.EmitStatus("(」゜ロ゜)」", http.StatusTooManyRequests)
   580  	senders = senders[:len(senders)-1]
   581  	for i := 0; i < failures; i++ {
   582  		senders = append(senders, rateLimitedSender)
   583  	}
   584  	s.sender = senders
   585  
   586  	_, err := env.StartInstance(makeStartInstanceParams(c, "quantal"))
   587  	c.Assert(err, gc.ErrorMatches, "creating virtual machine.*: max duration exceeded: .*failed with.*")
   588  
   589  	s.retryClock.CheckCalls(c, []gitjujutesting.StubCall{
   590  		{"After", []interface{}{5 * time.Second}},  // t0 + 5s
   591  		{"After", []interface{}{10 * time.Second}}, // t0 + 15s
   592  		{"After", []interface{}{20 * time.Second}}, // t0 + 35s
   593  		{"After", []interface{}{40 * time.Second}}, // t0 + 1m15s
   594  		{"After", []interface{}{1 * time.Minute}},  // t0 + 2m15s
   595  		{"After", []interface{}{1 * time.Minute}},  // t0 + 3m15s
   596  		{"After", []interface{}{1 * time.Minute}},  // t0 + 4m15s
   597  		// There would be another call here, but since the time
   598  		// exceeds the give minute limit, retrying is aborted.
   599  	})
   600  }
   601  
   602  func (s *environSuite) TestStartInstanceDistributionGroup(c *gc.C) {
   603  	c.Skip("TODO: test StartInstance's DistributionGroup behaviour")
   604  }
   605  
   606  func (s *environSuite) TestStartInstanceServiceAvailabilitySet(c *gc.C) {
   607  	env := s.openEnviron(c)
   608  	s.sender = s.startInstanceSenders(false)
   609  	s.requests = nil
   610  	unitsDeployed := "mysql/0 wordpress/0"
   611  	params := makeStartInstanceParams(c, "quantal")
   612  	params.InstanceConfig.Tags[tags.JujuUnitsDeployed] = unitsDeployed
   613  	_, err := env.StartInstance(params)
   614  	c.Assert(err, jc.ErrorIsNil)
   615  	s.tags[tags.JujuUnitsDeployed] = &unitsDeployed
   616  	requests := s.assertStartInstanceRequests(c, s.requests)
   617  	availabilitySetName := path.Base(requests.availabilitySet.URL.Path)
   618  	c.Assert(availabilitySetName, gc.Equals, "mysql")
   619  }
   620  
   621  func (s *environSuite) assertStartInstanceRequests(c *gc.C, requests []*http.Request) startInstanceRequests {
   622  	// Clear the fields that don't get sent in the request.
   623  	s.publicIPAddress.ID = nil
   624  	s.publicIPAddress.Name = nil
   625  	s.publicIPAddress.Properties.IPAddress = nil
   626  	s.newNetworkInterface.ID = nil
   627  	s.newNetworkInterface.Name = nil
   628  	(*s.newNetworkInterface.Properties.IPConfigurations)[0].ID = nil
   629  	s.jujuAvailabilitySet.ID = nil
   630  	s.jujuAvailabilitySet.Name = nil
   631  	s.virtualMachine.ID = nil
   632  	s.virtualMachine.Name = nil
   633  	s.virtualMachine.Properties.ProvisioningState = nil
   634  
   635  	// Validate HTTP request bodies.
   636  	c.Assert(requests, gc.HasLen, 8)
   637  	c.Assert(requests[0].Method, gc.Equals, "GET") // vmSizes
   638  	c.Assert(requests[1].Method, gc.Equals, "GET") // juju-testenv-model-deadbeef-0bad-400d-8000-4b1d0d06f00d
   639  	c.Assert(requests[2].Method, gc.Equals, "GET") // skus
   640  	c.Assert(requests[3].Method, gc.Equals, "PUT")
   641  	assertRequestBody(c, requests[3], s.publicIPAddress)
   642  	c.Assert(requests[4].Method, gc.Equals, "GET") // NICs
   643  	c.Assert(requests[5].Method, gc.Equals, "PUT")
   644  	assertRequestBody(c, requests[5], s.newNetworkInterface)
   645  	c.Assert(requests[6].Method, gc.Equals, "PUT")
   646  	assertRequestBody(c, requests[6], s.jujuAvailabilitySet)
   647  	c.Assert(requests[7].Method, gc.Equals, "PUT")
   648  	assertCreateVirtualMachineRequestBody(c, requests[7], s.virtualMachine)
   649  
   650  	return startInstanceRequests{
   651  		vmSizes:          requests[0],
   652  		subnet:           requests[1],
   653  		skus:             requests[2],
   654  		publicIPAddress:  requests[3],
   655  		nics:             requests[4],
   656  		networkInterface: requests[5],
   657  		availabilitySet:  requests[6],
   658  		virtualMachine:   requests[7],
   659  	}
   660  }
   661  
   662  func assertCreateVirtualMachineRequestBody(c *gc.C, req *http.Request, expect *compute.VirtualMachine) {
   663  	// CustomData is non-deterministic, so don't compare it.
   664  	// TODO(axw) shouldn't CustomData be deterministic? Look into this.
   665  	var virtualMachine compute.VirtualMachine
   666  	unmarshalRequestBody(c, req, &virtualMachine)
   667  	c.Assert(to.String(virtualMachine.Properties.OsProfile.CustomData), gc.Not(gc.HasLen), 0)
   668  	virtualMachine.Properties.OsProfile.CustomData = to.StringPtr("<juju-goes-here>")
   669  	c.Assert(&virtualMachine, jc.DeepEquals, expect)
   670  }
   671  
   672  type startInstanceRequests struct {
   673  	vmSizes          *http.Request
   674  	subnet           *http.Request
   675  	skus             *http.Request
   676  	publicIPAddress  *http.Request
   677  	nics             *http.Request
   678  	networkInterface *http.Request
   679  	availabilitySet  *http.Request
   680  	virtualMachine   *http.Request
   681  }
   682  
   683  func (s *environSuite) TestBootstrap(c *gc.C) {
   684  	defer envtesting.DisableFinishBootstrap()()
   685  
   686  	ctx := envtesting.BootstrapContext(c)
   687  	env := prepareForBootstrap(c, ctx, s.provider, &s.sender)
   688  
   689  	s.sender = s.initResourceGroupSenders()
   690  	s.sender = append(s.sender, s.startInstanceSenders(true)...)
   691  	s.requests = nil
   692  	result, err := env.Bootstrap(
   693  		ctx, environs.BootstrapParams{
   694  			AvailableTools: makeToolsList(series.LatestLts()),
   695  		},
   696  	)
   697  	c.Assert(err, jc.ErrorIsNil)
   698  	c.Assert(result.Arch, gc.Equals, "amd64")
   699  	c.Assert(result.Series, gc.Equals, series.LatestLts())
   700  
   701  	c.Assert(len(s.requests), gc.Equals, 17)
   702  
   703  	c.Assert(s.requests[0].Method, gc.Equals, "PUT")  // resource group
   704  	c.Assert(s.requests[1].Method, gc.Equals, "PUT")  // vnet
   705  	c.Assert(s.requests[2].Method, gc.Equals, "PUT")  // network security group
   706  	c.Assert(s.requests[3].Method, gc.Equals, "PUT")  // subnet
   707  	c.Assert(s.requests[4].Method, gc.Equals, "POST") // check storage account name
   708  	c.Assert(s.requests[5].Method, gc.Equals, "PUT")  // create storage account
   709  	c.Assert(s.requests[6].Method, gc.Equals, "POST") // get storage account keys
   710  
   711  	assertRequestBody(c, s.requests[0], &s.group)
   712  
   713  	s.vnet.ID = nil
   714  	s.vnet.Name = nil
   715  	assertRequestBody(c, s.requests[1], s.vnet)
   716  
   717  	securityRules := []network.SecurityRule{{
   718  		Name: to.StringPtr("SSHInbound"),
   719  		Properties: &network.SecurityRulePropertiesFormat{
   720  			Description:              to.StringPtr("Allow SSH access to all machines"),
   721  			Protocol:                 network.TCP,
   722  			SourceAddressPrefix:      to.StringPtr("*"),
   723  			SourcePortRange:          to.StringPtr("*"),
   724  			DestinationAddressPrefix: to.StringPtr("*"),
   725  			DestinationPortRange:     to.StringPtr("22"),
   726  			Access:                   network.Allow,
   727  			Priority:                 to.IntPtr(100),
   728  			Direction:                network.Inbound,
   729  		},
   730  	}}
   731  	assertRequestBody(c, s.requests[2], &network.SecurityGroup{
   732  		Location: to.StringPtr("westus"),
   733  		Tags:     s.nsg.Tags,
   734  		Properties: &network.SecurityGroupPropertiesFormat{
   735  			SecurityRules: &securityRules,
   736  		},
   737  	})
   738  
   739  	s.subnet.ID = nil
   740  	s.subnet.Name = nil
   741  	assertRequestBody(c, s.requests[3], s.subnet)
   742  
   743  	assertRequestBody(c, s.requests[4], &storage.AccountCheckNameAvailabilityParameters{
   744  		Name: to.StringPtr(fakeStorageAccount),
   745  		Type: to.StringPtr("Microsoft.Storage/storageAccounts"),
   746  	})
   747  
   748  	assertRequestBody(c, s.requests[5], &storage.AccountCreateParameters{
   749  		Location: to.StringPtr("westus"),
   750  		Tags:     s.storageAccount.Tags,
   751  		Properties: &storage.AccountPropertiesCreateParameters{
   752  			AccountType: "Standard_LRS",
   753  		},
   754  	})
   755  }
   756  
   757  func (s *environSuite) TestAllInstancesResourceGroupNotFound(c *gc.C) {
   758  	env := s.openEnviron(c)
   759  	sender := mocks.NewSender()
   760  	sender.EmitStatus("resource group not found", http.StatusNotFound)
   761  	s.sender = azuretesting.Senders{sender}
   762  	_, err := env.AllInstances()
   763  	c.Assert(err, jc.ErrorIsNil)
   764  }
   765  
   766  func (s *environSuite) TestStopInstancesNotFound(c *gc.C) {
   767  	env := s.openEnviron(c)
   768  	sender := mocks.NewSender()
   769  	sender.EmitStatus("vm not found", http.StatusNotFound)
   770  	s.sender = azuretesting.Senders{sender, sender, sender}
   771  	err := env.StopInstances("a", "b")
   772  	c.Assert(err, jc.ErrorIsNil)
   773  }
   774  
   775  func (s *environSuite) TestStopInstances(c *gc.C) {
   776  	env := s.openEnviron(c)
   777  
   778  	// Security group has rules for machine-0 but not machine-1, and
   779  	// has a rule that doesn't match either.
   780  	nsg := makeSecurityGroup(
   781  		makeSecurityRule("machine-0-80", "10.0.0.4", "80"),
   782  		makeSecurityRule("machine-0-1000-2000", "10.0.0.4", "1000-2000"),
   783  		makeSecurityRule("machine-42", "10.0.0.5", "*"),
   784  	)
   785  
   786  	// Create an IP configuration with a public IP reference. This will
   787  	// cause an update to the NIC to detach public IPs.
   788  	nic0IPConfiguration := makeIPConfiguration("10.0.0.4")
   789  	nic0IPConfiguration.Properties.PublicIPAddress = &network.SubResource{}
   790  	nic0 := makeNetworkInterface("nic-0", "machine-0", nic0IPConfiguration)
   791  
   792  	s.sender = azuretesting.Senders{
   793  		s.networkInterfacesSender(
   794  			nic0,
   795  			makeNetworkInterface("nic-1", "machine-1"),
   796  			makeNetworkInterface("nic-2", "machine-1"),
   797  		),
   798  		s.virtualMachinesSender(makeVirtualMachine("machine-0")),
   799  		s.publicIPAddressesSender(
   800  			makePublicIPAddress("pip-0", "machine-0", "1.2.3.4"),
   801  		),
   802  		s.makeSender(".*/virtualMachines/machine-0", nil),                                                 // DELETE
   803  		s.makeSender(".*/networkSecurityGroups/juju-internal-nsg", nsg),                                   // GET
   804  		s.makeSender(".*/networkSecurityGroups/juju-internal-nsg/securityRules/machine-0-80", nil),        // DELETE
   805  		s.makeSender(".*/networkSecurityGroups/juju-internal-nsg/securityRules/machine-0-1000-2000", nil), // DELETE
   806  		s.makeSender(".*/networkInterfaces/nic-0", nic0),                                                  // PUT
   807  		s.makeSender(".*/publicIPAddresses/pip-0", nil),                                                   // DELETE
   808  		s.makeSender(".*/networkInterfaces/nic-0", nil),                                                   // DELETE
   809  		s.makeSender(".*/virtualMachines/machine-1", nil),                                                 // DELETE
   810  		s.makeSender(".*/networkSecurityGroups/juju-internal-nsg", nsg),                                   // GET
   811  		s.makeSender(".*/networkInterfaces/nic-1", nil),                                                   // DELETE
   812  		s.makeSender(".*/networkInterfaces/nic-2", nil),                                                   // DELETE
   813  	}
   814  	err := env.StopInstances("machine-0", "machine-1", "machine-2")
   815  	c.Assert(err, jc.ErrorIsNil)
   816  
   817  	s.storageClient.CheckCallNames(c,
   818  		"NewClient", "DeleteBlobIfExists", "DeleteBlobIfExists",
   819  	)
   820  	s.storageClient.CheckCall(c, 1, "DeleteBlobIfExists", "osvhds", "machine-0")
   821  	s.storageClient.CheckCall(c, 2, "DeleteBlobIfExists", "osvhds", "machine-1")
   822  }
   823  
   824  func (s *environSuite) TestConstraintsValidatorUnsupported(c *gc.C) {
   825  	validator := s.constraintsValidator(c)
   826  	unsupported, err := validator.Validate(constraints.MustParse(
   827  		"arch=amd64 tags=foo cpu-power=100 virt-type=kvm",
   828  	))
   829  	c.Assert(err, jc.ErrorIsNil)
   830  	c.Assert(unsupported, jc.SameContents, []string{"tags", "cpu-power", "virt-type"})
   831  }
   832  
   833  func (s *environSuite) TestConstraintsValidatorVocabulary(c *gc.C) {
   834  	validator := s.constraintsValidator(c)
   835  	_, err := validator.Validate(constraints.MustParse("arch=armhf"))
   836  	c.Assert(err, gc.ErrorMatches,
   837  		"invalid constraint value: arch=armhf\nvalid values are: \\[amd64\\]",
   838  	)
   839  	_, err = validator.Validate(constraints.MustParse("instance-type=t1.micro"))
   840  	c.Assert(err, gc.ErrorMatches,
   841  		"invalid constraint value: instance-type=t1.micro\nvalid values are: \\[D1 Standard_D1\\]",
   842  	)
   843  }
   844  
   845  func (s *environSuite) TestConstraintsValidatorMerge(c *gc.C) {
   846  	validator := s.constraintsValidator(c)
   847  	cons, err := validator.Merge(
   848  		constraints.MustParse("mem=3G arch=amd64"),
   849  		constraints.MustParse("instance-type=D1"),
   850  	)
   851  	c.Assert(err, jc.ErrorIsNil)
   852  	c.Assert(cons.String(), gc.Equals, "instance-type=D1")
   853  }
   854  
   855  func (s *environSuite) constraintsValidator(c *gc.C) constraints.Validator {
   856  	env := s.openEnviron(c)
   857  	s.sender = azuretesting.Senders{s.vmSizesSender()}
   858  	validator, err := env.ConstraintsValidator()
   859  	c.Assert(err, jc.ErrorIsNil)
   860  	return validator
   861  }
   862  
   863  func (s *environSuite) TestAgentMirror(c *gc.C) {
   864  	env := s.openEnviron(c)
   865  	c.Assert(env, gc.Implements, new(envtools.HasAgentMirror))
   866  	cloudSpec, err := env.(envtools.HasAgentMirror).AgentMirror()
   867  	c.Assert(err, jc.ErrorIsNil)
   868  	c.Assert(cloudSpec, gc.Equals, simplestreams.CloudSpec{
   869  		Region:   "westus",
   870  		Endpoint: "https://storage.azurestack.local/",
   871  	})
   872  }