github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/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  	"errors"
     9  	"fmt"
    10  	"io/ioutil"
    11  	"net/http"
    12  	"path"
    13  	"reflect"
    14  	"time"
    15  
    16  	"github.com/Azure/azure-sdk-for-go/arm/compute"
    17  	"github.com/Azure/azure-sdk-for-go/arm/network"
    18  	"github.com/Azure/azure-sdk-for-go/arm/resources/resources"
    19  	"github.com/Azure/azure-sdk-for-go/arm/storage"
    20  	autorestazure "github.com/Azure/go-autorest/autorest/azure"
    21  	"github.com/Azure/go-autorest/autorest/mocks"
    22  	"github.com/Azure/go-autorest/autorest/to"
    23  	gitjujutesting "github.com/juju/testing"
    24  	jc "github.com/juju/testing/checkers"
    25  	"github.com/juju/utils"
    26  	"github.com/juju/utils/arch"
    27  	gc "gopkg.in/check.v1"
    28  	"gopkg.in/juju/names.v2"
    29  
    30  	"github.com/juju/juju/api"
    31  	"github.com/juju/juju/cloudconfig/instancecfg"
    32  	"github.com/juju/juju/constraints"
    33  	"github.com/juju/juju/environs"
    34  	"github.com/juju/juju/environs/imagemetadata"
    35  	"github.com/juju/juju/environs/simplestreams"
    36  	"github.com/juju/juju/environs/tags"
    37  	envtesting "github.com/juju/juju/environs/testing"
    38  	envtools "github.com/juju/juju/environs/tools"
    39  	"github.com/juju/juju/instance"
    40  	"github.com/juju/juju/provider/azure"
    41  	"github.com/juju/juju/provider/azure/internal/armtemplates"
    42  	"github.com/juju/juju/provider/azure/internal/azureauth"
    43  	"github.com/juju/juju/provider/azure/internal/azuretesting"
    44  	"github.com/juju/juju/testing"
    45  	"github.com/juju/juju/tools"
    46  	"github.com/juju/version"
    47  )
    48  
    49  const storageAccountName = "juju400d80004b1d0d06f00d"
    50  
    51  var (
    52  	quantalImageReference = compute.ImageReference{
    53  		Publisher: to.StringPtr("Canonical"),
    54  		Offer:     to.StringPtr("UbuntuServer"),
    55  		Sku:       to.StringPtr("12.10"),
    56  		Version:   to.StringPtr("latest"),
    57  	}
    58  	win2012ImageReference = compute.ImageReference{
    59  		Publisher: to.StringPtr("MicrosoftWindowsServer"),
    60  		Offer:     to.StringPtr("WindowsServer"),
    61  		Sku:       to.StringPtr("2012-Datacenter"),
    62  		Version:   to.StringPtr("latest"),
    63  	}
    64  	centos7ImageReference = compute.ImageReference{
    65  		Publisher: to.StringPtr("OpenLogic"),
    66  		Offer:     to.StringPtr("CentOS"),
    67  		Sku:       to.StringPtr("7.1"),
    68  		Version:   to.StringPtr("latest"),
    69  	}
    70  
    71  	sshPublicKeys = []compute.SSHPublicKey{{
    72  		Path:    to.StringPtr("/home/ubuntu/.ssh/authorized_keys"),
    73  		KeyData: to.StringPtr(testing.FakeAuthKeys),
    74  	}}
    75  	linuxOsProfile = compute.OSProfile{
    76  		ComputerName:  to.StringPtr("machine-0"),
    77  		CustomData:    to.StringPtr("<juju-goes-here>"),
    78  		AdminUsername: to.StringPtr("ubuntu"),
    79  		LinuxConfiguration: &compute.LinuxConfiguration{
    80  			DisablePasswordAuthentication: to.BoolPtr(true),
    81  			SSH: &compute.SSHConfiguration{
    82  				PublicKeys: &sshPublicKeys,
    83  			},
    84  		},
    85  	}
    86  	windowsOsProfile = compute.OSProfile{
    87  		ComputerName:  to.StringPtr("machine-0"),
    88  		CustomData:    to.StringPtr("<juju-goes-here>"),
    89  		AdminUsername: to.StringPtr("JujuAdministrator"),
    90  		AdminPassword: to.StringPtr("sorandom"),
    91  		WindowsConfiguration: &compute.WindowsConfiguration{
    92  			ProvisionVMAgent:       to.BoolPtr(true),
    93  			EnableAutomaticUpdates: to.BoolPtr(true),
    94  		},
    95  	}
    96  )
    97  
    98  type environSuite struct {
    99  	testing.BaseSuite
   100  
   101  	provider      environs.EnvironProvider
   102  	requests      []*http.Request
   103  	storageClient azuretesting.MockStorageClient
   104  	sender        azuretesting.Senders
   105  	retryClock    mockClock
   106  
   107  	controllerUUID     string
   108  	envTags            map[string]*string
   109  	vmTags             map[string]*string
   110  	group              *resources.ResourceGroup
   111  	vmSizes            *compute.VirtualMachineSizeListResult
   112  	storageAccounts    []storage.Account
   113  	storageAccount     *storage.Account
   114  	storageAccountKeys *storage.AccountListKeysResult
   115  	ubuntuServerSKUs   []compute.VirtualMachineImageResource
   116  	deployment         *resources.Deployment
   117  }
   118  
   119  var _ = gc.Suite(&environSuite{})
   120  
   121  func (s *environSuite) SetUpTest(c *gc.C) {
   122  	s.BaseSuite.SetUpTest(c)
   123  	s.storageClient = azuretesting.MockStorageClient{}
   124  	s.sender = nil
   125  	s.requests = nil
   126  	s.retryClock = mockClock{Clock: gitjujutesting.NewClock(time.Time{})}
   127  
   128  	s.provider = newProvider(c, azure.ProviderConfig{
   129  		Sender:           azuretesting.NewSerialSender(&s.sender),
   130  		RequestInspector: azuretesting.RequestRecorder(&s.requests),
   131  		NewStorageClient: s.storageClient.NewClient,
   132  		RetryClock: &gitjujutesting.AutoAdvancingClock{
   133  			&s.retryClock, s.retryClock.Advance,
   134  		},
   135  		RandomWindowsAdminPassword:        func() string { return "sorandom" },
   136  		InteractiveCreateServicePrincipal: azureauth.InteractiveCreateServicePrincipal,
   137  	})
   138  
   139  	s.controllerUUID = testing.ControllerTag.Id()
   140  	s.envTags = map[string]*string{
   141  		"juju-model-uuid":      to.StringPtr(testing.ModelTag.Id()),
   142  		"juju-controller-uuid": to.StringPtr(s.controllerUUID),
   143  	}
   144  	s.vmTags = map[string]*string{
   145  		"juju-model-uuid":      to.StringPtr(testing.ModelTag.Id()),
   146  		"juju-controller-uuid": to.StringPtr(s.controllerUUID),
   147  		"juju-machine-name":    to.StringPtr("machine-0"),
   148  	}
   149  
   150  	s.group = &resources.ResourceGroup{
   151  		Location: to.StringPtr("westus"),
   152  		Tags:     &s.envTags,
   153  		Properties: &resources.ResourceGroupProperties{
   154  			ProvisioningState: to.StringPtr("Succeeded"),
   155  		},
   156  	}
   157  
   158  	vmSizes := []compute.VirtualMachineSize{{
   159  		Name:                 to.StringPtr("Standard_D1"),
   160  		NumberOfCores:        to.Int32Ptr(1),
   161  		OsDiskSizeInMB:       to.Int32Ptr(1047552),
   162  		ResourceDiskSizeInMB: to.Int32Ptr(51200),
   163  		MemoryInMB:           to.Int32Ptr(3584),
   164  		MaxDataDiskCount:     to.Int32Ptr(2),
   165  	}}
   166  	s.vmSizes = &compute.VirtualMachineSizeListResult{Value: &vmSizes}
   167  
   168  	s.storageAccount = &storage.Account{
   169  		Name: to.StringPtr("my-storage-account"),
   170  		Type: to.StringPtr("Standard_LRS"),
   171  		Tags: &s.envTags,
   172  		Properties: &storage.AccountProperties{
   173  			PrimaryEndpoints: &storage.Endpoints{
   174  				Blob: to.StringPtr(fmt.Sprintf("https://%s.blob.storage.azurestack.local/", storageAccountName)),
   175  			},
   176  			ProvisioningState: "Succeeded",
   177  		},
   178  	}
   179  
   180  	keys := []storage.AccountKey{{
   181  		KeyName:     to.StringPtr("key-1-name"),
   182  		Value:       to.StringPtr("key-1"),
   183  		Permissions: storage.FULL,
   184  	}}
   185  	s.storageAccountKeys = &storage.AccountListKeysResult{
   186  		Keys: &keys,
   187  	}
   188  
   189  	s.ubuntuServerSKUs = []compute.VirtualMachineImageResource{
   190  		{Name: to.StringPtr("12.04-LTS")},
   191  		{Name: to.StringPtr("12.10")},
   192  		{Name: to.StringPtr("14.04-LTS")},
   193  		{Name: to.StringPtr("15.04")},
   194  		{Name: to.StringPtr("15.10")},
   195  		{Name: to.StringPtr("16.04-LTS")},
   196  	}
   197  
   198  	s.deployment = nil
   199  }
   200  
   201  func (s *environSuite) openEnviron(c *gc.C, attrs ...testing.Attrs) environs.Environ {
   202  	return openEnviron(c, s.provider, &s.sender, attrs...)
   203  }
   204  
   205  func openEnviron(
   206  	c *gc.C,
   207  	provider environs.EnvironProvider,
   208  	sender *azuretesting.Senders,
   209  	attrs ...testing.Attrs,
   210  ) environs.Environ {
   211  	// Opening the environment should not incur network communication,
   212  	// so we don't set s.sender until after opening.
   213  	cfg := makeTestModelConfig(c, attrs...)
   214  	env, err := provider.Open(environs.OpenParams{
   215  		Cloud:  fakeCloudSpec(),
   216  		Config: cfg,
   217  	})
   218  	c.Assert(err, jc.ErrorIsNil)
   219  
   220  	// Force an explicit refresh of the access token, so it isn't done
   221  	// implicitly during the tests.
   222  	*sender = azuretesting.Senders{
   223  		discoverAuthSender(),
   224  		tokenRefreshSender(),
   225  	}
   226  	err = azure.ForceTokenRefresh(env)
   227  	c.Assert(err, jc.ErrorIsNil)
   228  	return env
   229  }
   230  
   231  func prepareForBootstrap(
   232  	c *gc.C,
   233  	ctx environs.BootstrapContext,
   234  	provider environs.EnvironProvider,
   235  	sender *azuretesting.Senders,
   236  	attrs ...testing.Attrs,
   237  ) environs.Environ {
   238  	// Opening the environment should not incur network communication,
   239  	// so we don't set s.sender until after opening.
   240  	cfg, err := provider.PrepareConfig(environs.PrepareConfigParams{
   241  		Config: makeTestModelConfig(c, attrs...),
   242  		Cloud:  fakeCloudSpec(),
   243  	})
   244  	c.Assert(err, jc.ErrorIsNil)
   245  
   246  	env, err := provider.Open(environs.OpenParams{
   247  		Cloud:  fakeCloudSpec(),
   248  		Config: cfg,
   249  	})
   250  	c.Assert(err, jc.ErrorIsNil)
   251  
   252  	*sender = azuretesting.Senders{
   253  		discoverAuthSender(),
   254  		tokenRefreshSender(),
   255  	}
   256  	err = env.PrepareForBootstrap(ctx)
   257  	c.Assert(err, jc.ErrorIsNil)
   258  	return env
   259  }
   260  
   261  func fakeCloudSpec() environs.CloudSpec {
   262  	return environs.CloudSpec{
   263  		Type:             "azure",
   264  		Name:             "azure",
   265  		Region:           "westus",
   266  		Endpoint:         "https://api.azurestack.local",
   267  		IdentityEndpoint: "https://login.microsoftonline.com",
   268  		StorageEndpoint:  "https://storage.azurestack.local",
   269  		Credential:       fakeServicePrincipalCredential(),
   270  	}
   271  }
   272  
   273  func tokenRefreshSender() *azuretesting.MockSender {
   274  	tokenRefreshSender := azuretesting.NewSenderWithValue(&autorestazure.Token{
   275  		AccessToken: "access-token",
   276  		ExpiresOn:   fmt.Sprint(time.Now().Add(time.Hour).Unix()),
   277  		Type:        "Bearer",
   278  	})
   279  	tokenRefreshSender.PathPattern = ".*/oauth2/token"
   280  	return tokenRefreshSender
   281  }
   282  
   283  func discoverAuthSender() *azuretesting.MockSender {
   284  	const fakeTenantId = "11111111-1111-1111-1111-111111111111"
   285  	sender := mocks.NewSender()
   286  	resp := mocks.NewResponseWithStatus("", http.StatusUnauthorized)
   287  	mocks.SetResponseHeaderValues(resp, "WWW-Authenticate", []string{
   288  		fmt.Sprintf(
   289  			`authorization_uri="https://testing.invalid/%s"`,
   290  			fakeTenantId,
   291  		),
   292  	})
   293  	sender.AppendResponse(resp)
   294  	return &azuretesting.MockSender{
   295  		Sender:      sender,
   296  		PathPattern: ".*/subscriptions/" + fakeSubscriptionId,
   297  	}
   298  }
   299  
   300  func (s *environSuite) initResourceGroupSenders() azuretesting.Senders {
   301  	resourceGroupName := "juju-testenv-model-deadbeef-0bad-400d-8000-4b1d0d06f00d"
   302  	senders := azuretesting.Senders{s.makeSender(".*/resourcegroups/"+resourceGroupName, s.group)}
   303  	return senders
   304  }
   305  
   306  func (s *environSuite) startInstanceSenders(controller bool) azuretesting.Senders {
   307  	senders := azuretesting.Senders{s.vmSizesSender()}
   308  	if s.ubuntuServerSKUs != nil {
   309  		senders = append(senders, s.makeSender(".*/Canonical/.*/UbuntuServer/skus", s.ubuntuServerSKUs))
   310  	}
   311  	senders = append(senders, s.makeSender("/deployments/machine-0", s.deployment))
   312  	return senders
   313  }
   314  
   315  func (s *environSuite) networkInterfacesSender(nics ...network.Interface) *azuretesting.MockSender {
   316  	return s.makeSender(".*/networkInterfaces", network.InterfaceListResult{Value: &nics})
   317  }
   318  
   319  func (s *environSuite) publicIPAddressesSender(pips ...network.PublicIPAddress) *azuretesting.MockSender {
   320  	return s.makeSender(".*/publicIPAddresses", network.PublicIPAddressListResult{Value: &pips})
   321  }
   322  
   323  func (s *environSuite) virtualMachinesSender(vms ...compute.VirtualMachine) *azuretesting.MockSender {
   324  	return s.makeSender(".*/virtualMachines", compute.VirtualMachineListResult{Value: &vms})
   325  }
   326  
   327  func (s *environSuite) vmSizesSender() *azuretesting.MockSender {
   328  	return s.makeSender(".*/vmSizes", s.vmSizes)
   329  }
   330  
   331  func (s *environSuite) storageAccountSender() *azuretesting.MockSender {
   332  	return s.makeSender(".*/storageAccounts/"+storageAccountName, s.storageAccount)
   333  }
   334  
   335  func (s *environSuite) storageAccountKeysSender() *azuretesting.MockSender {
   336  	return s.makeSender(".*/storageAccounts/.*/listKeys", s.storageAccountKeys)
   337  }
   338  
   339  func (s *environSuite) makeSender(pattern string, v interface{}) *azuretesting.MockSender {
   340  	sender := azuretesting.NewSenderWithValue(v)
   341  	sender.PathPattern = pattern
   342  	return sender
   343  }
   344  
   345  func makeStartInstanceParams(c *gc.C, controllerUUID, series string) environs.StartInstanceParams {
   346  	machineTag := names.NewMachineTag("0")
   347  	apiInfo := &api.Info{
   348  		Addrs:    []string{"localhost:17777"},
   349  		CACert:   testing.CACert,
   350  		Password: "admin",
   351  		Tag:      machineTag,
   352  		ModelTag: testing.ModelTag,
   353  	}
   354  
   355  	icfg, err := instancecfg.NewInstanceConfig(
   356  		names.NewControllerTag(controllerUUID),
   357  		machineTag.Id(), "yanonce", imagemetadata.ReleasedStream,
   358  		series, apiInfo,
   359  	)
   360  	c.Assert(err, jc.ErrorIsNil)
   361  	icfg.Tags = map[string]string{
   362  		tags.JujuModel:      testing.ModelTag.Id(),
   363  		tags.JujuController: controllerUUID,
   364  	}
   365  
   366  	return environs.StartInstanceParams{
   367  		ControllerUUID: controllerUUID,
   368  		Tools:          makeToolsList(series),
   369  		InstanceConfig: icfg,
   370  	}
   371  }
   372  
   373  func makeToolsList(series string) tools.List {
   374  	var toolsVersion version.Binary
   375  	toolsVersion.Number = version.MustParse("1.26.0")
   376  	toolsVersion.Arch = arch.AMD64
   377  	toolsVersion.Series = series
   378  	return tools.List{{
   379  		Version: toolsVersion,
   380  		URL:     fmt.Sprintf("http://example.com/tools/juju-%s.tgz", toolsVersion),
   381  		SHA256:  "1234567890abcdef",
   382  		Size:    1024,
   383  	}}
   384  }
   385  
   386  func unmarshalRequestBody(c *gc.C, req *http.Request, out interface{}) {
   387  	bytes, err := ioutil.ReadAll(req.Body)
   388  	c.Assert(err, jc.ErrorIsNil)
   389  	err = json.Unmarshal(bytes, out)
   390  	c.Assert(err, jc.ErrorIsNil)
   391  }
   392  
   393  func assertRequestBody(c *gc.C, req *http.Request, expect interface{}) {
   394  	unmarshalled := reflect.New(reflect.TypeOf(expect).Elem()).Interface()
   395  	unmarshalRequestBody(c, req, unmarshalled)
   396  	c.Assert(unmarshalled, jc.DeepEquals, expect)
   397  }
   398  
   399  type mockClock struct {
   400  	gitjujutesting.Stub
   401  	*gitjujutesting.Clock
   402  }
   403  
   404  func (c *mockClock) After(d time.Duration) <-chan time.Time {
   405  	c.MethodCall(c, "After", d)
   406  	c.PopNoErr()
   407  	return c.Clock.After(d)
   408  }
   409  
   410  func (s *environSuite) TestOpen(c *gc.C) {
   411  	env := s.openEnviron(c)
   412  	c.Assert(env, gc.NotNil)
   413  }
   414  
   415  func (s *environSuite) TestCloudEndpointManagementURI(c *gc.C) {
   416  	env := s.openEnviron(c)
   417  
   418  	sender := mocks.NewSender()
   419  	sender.AppendResponse(mocks.NewResponseWithContent("{}"))
   420  	s.sender = azuretesting.Senders{sender}
   421  	s.requests = nil
   422  	env.AllInstances() // trigger a query
   423  
   424  	c.Assert(s.requests, gc.HasLen, 1)
   425  	c.Assert(s.requests[0].URL.Host, gc.Equals, "api.azurestack.local")
   426  }
   427  
   428  func (s *environSuite) TestStartInstance(c *gc.C) {
   429  	env := s.openEnviron(c)
   430  	s.sender = s.startInstanceSenders(false)
   431  	s.requests = nil
   432  	result, err := env.StartInstance(makeStartInstanceParams(c, s.controllerUUID, "quantal"))
   433  	c.Assert(err, jc.ErrorIsNil)
   434  	c.Assert(result, gc.NotNil)
   435  	c.Assert(result.Instance, gc.NotNil)
   436  	c.Assert(result.NetworkInfo, gc.HasLen, 0)
   437  	c.Assert(result.Volumes, gc.HasLen, 0)
   438  	c.Assert(result.VolumeAttachments, gc.HasLen, 0)
   439  
   440  	arch := "amd64"
   441  	mem := uint64(3584)
   442  	rootDisk := uint64(30 * 1024) // 30 GiB
   443  	cpuCores := uint64(1)
   444  	c.Assert(result.Hardware, jc.DeepEquals, &instance.HardwareCharacteristics{
   445  		Arch:     &arch,
   446  		Mem:      &mem,
   447  		RootDisk: &rootDisk,
   448  		CpuCores: &cpuCores,
   449  	})
   450  	s.assertStartInstanceRequests(c, s.requests, assertStartInstanceRequestsParams{
   451  		imageReference: &quantalImageReference,
   452  		diskSizeGB:     32,
   453  		osProfile:      &linuxOsProfile,
   454  	})
   455  }
   456  
   457  func (s *environSuite) TestStartInstanceWindowsMinRootDisk(c *gc.C) {
   458  	// The minimum OS disk size for Windows machines is 127GiB.
   459  	cons := constraints.MustParse("root-disk=44G")
   460  	s.testStartInstanceWindows(c, cons, 127*1024, 136)
   461  }
   462  
   463  func (s *environSuite) TestStartInstanceWindowsGrowableRootDisk(c *gc.C) {
   464  	// The OS disk size may be grown larger than 127GiB.
   465  	cons := constraints.MustParse("root-disk=200G")
   466  	s.testStartInstanceWindows(c, cons, 200*1024, 214)
   467  }
   468  
   469  func (s *environSuite) testStartInstanceWindows(
   470  	c *gc.C, cons constraints.Value,
   471  	expect uint64, requestValue int,
   472  ) {
   473  	// Starting a Windows VM, we should not expect an image query.
   474  	s.PatchValue(&s.ubuntuServerSKUs, nil)
   475  
   476  	env := s.openEnviron(c)
   477  	s.sender = s.startInstanceSenders(false)
   478  	s.requests = nil
   479  	args := makeStartInstanceParams(c, s.controllerUUID, "win2012")
   480  	args.Constraints = cons
   481  	result, err := env.StartInstance(args)
   482  	c.Assert(err, jc.ErrorIsNil)
   483  	c.Assert(result, gc.NotNil)
   484  	c.Assert(result.Hardware.RootDisk, jc.DeepEquals, &expect)
   485  
   486  	vmExtensionSettings := map[string]interface{}{
   487  		"commandToExecute": `` +
   488  			`move C:\AzureData\CustomData.bin C:\AzureData\CustomData.ps1 && ` +
   489  			`powershell.exe -ExecutionPolicy Unrestricted -File C:\AzureData\CustomData.ps1 && ` +
   490  			`del /q C:\AzureData\CustomData.ps1`,
   491  	}
   492  	s.assertStartInstanceRequests(c, s.requests, assertStartInstanceRequestsParams{
   493  		imageReference: &win2012ImageReference,
   494  		diskSizeGB:     requestValue,
   495  		vmExtension: &compute.VirtualMachineExtensionProperties{
   496  			Publisher:               to.StringPtr("Microsoft.Compute"),
   497  			Type:                    to.StringPtr("CustomScriptExtension"),
   498  			TypeHandlerVersion:      to.StringPtr("1.4"),
   499  			AutoUpgradeMinorVersion: to.BoolPtr(true),
   500  			Settings:                &vmExtensionSettings,
   501  		},
   502  		osProfile: &windowsOsProfile,
   503  	})
   504  }
   505  
   506  func (s *environSuite) TestStartInstanceCentOS(c *gc.C) {
   507  	// Starting a CentOS VM, we should not expect an image query.
   508  	s.PatchValue(&s.ubuntuServerSKUs, nil)
   509  
   510  	env := s.openEnviron(c)
   511  	s.sender = s.startInstanceSenders(false)
   512  	s.requests = nil
   513  	args := makeStartInstanceParams(c, s.controllerUUID, "centos7")
   514  	_, err := env.StartInstance(args)
   515  	c.Assert(err, jc.ErrorIsNil)
   516  
   517  	vmExtensionSettings := map[string]interface{}{
   518  		"commandToExecute": `bash -c 'base64 -d /var/lib/waagent/CustomData | bash'`,
   519  	}
   520  	s.assertStartInstanceRequests(c, s.requests, assertStartInstanceRequestsParams{
   521  		imageReference: &centos7ImageReference,
   522  		diskSizeGB:     32,
   523  		vmExtension: &compute.VirtualMachineExtensionProperties{
   524  			Publisher:               to.StringPtr("Microsoft.OSTCExtensions"),
   525  			Type:                    to.StringPtr("CustomScriptForLinux"),
   526  			TypeHandlerVersion:      to.StringPtr("1.4"),
   527  			AutoUpgradeMinorVersion: to.BoolPtr(true),
   528  			Settings:                &vmExtensionSettings,
   529  		},
   530  		osProfile: &linuxOsProfile,
   531  	})
   532  }
   533  
   534  func (s *environSuite) TestStartInstanceTooManyRequests(c *gc.C) {
   535  	env := s.openEnviron(c)
   536  	senders := s.startInstanceSenders(false)
   537  	s.requests = nil
   538  
   539  	// 6 failures to get to 1 minute, and show that we cap it there.
   540  	const failures = 6
   541  
   542  	// Make the VirtualMachines.CreateOrUpdate call respond with
   543  	// 429 (StatusTooManyRequests) failures, and then with success.
   544  	rateLimitedSender := mocks.NewSender()
   545  	rateLimitedSender.AppendAndRepeatResponse(mocks.NewResponseWithBodyAndStatus(
   546  		mocks.NewBody("{}"), // empty JSON response to appease go-autorest
   547  		http.StatusTooManyRequests,
   548  		"(」゜ロ゜)」",
   549  	), failures)
   550  	successSender := senders[len(senders)-1]
   551  	senders = senders[:len(senders)-1]
   552  	for i := 0; i < failures; i++ {
   553  		senders = append(senders, rateLimitedSender)
   554  	}
   555  	senders = append(senders, successSender)
   556  	s.sender = senders
   557  
   558  	_, err := env.StartInstance(makeStartInstanceParams(c, s.controllerUUID, "quantal"))
   559  	c.Assert(err, jc.ErrorIsNil)
   560  
   561  	c.Assert(s.requests, gc.HasLen, numExpectedStartInstanceRequests+failures)
   562  	s.assertStartInstanceRequests(c, s.requests[:numExpectedStartInstanceRequests], assertStartInstanceRequestsParams{
   563  		imageReference: &quantalImageReference,
   564  		diskSizeGB:     32,
   565  		osProfile:      &linuxOsProfile,
   566  	})
   567  
   568  	// The final requests should all be identical.
   569  	for i := numExpectedStartInstanceRequests; i < numExpectedStartInstanceRequests+failures; i++ {
   570  		c.Assert(s.requests[i].Method, gc.Equals, "PUT")
   571  		c.Assert(s.requests[i].URL.Path, gc.Equals, s.requests[numExpectedStartInstanceRequests-1].URL.Path)
   572  	}
   573  
   574  	s.retryClock.CheckCalls(c, []gitjujutesting.StubCall{
   575  		{"After", []interface{}{5 * time.Second}},
   576  		{"After", []interface{}{10 * time.Second}},
   577  		{"After", []interface{}{20 * time.Second}},
   578  		{"After", []interface{}{40 * time.Second}},
   579  		{"After", []interface{}{1 * time.Minute}},
   580  		{"After", []interface{}{1 * time.Minute}},
   581  	})
   582  }
   583  
   584  func (s *environSuite) TestStartInstanceTooManyRequestsTimeout(c *gc.C) {
   585  	env := s.openEnviron(c)
   586  	senders := s.startInstanceSenders(false)
   587  	s.requests = nil
   588  
   589  	// 8 failures to get to 5 minutes, which is as long as we'll keep
   590  	// retrying before giving up.
   591  	const failures = 8
   592  
   593  	// Make the VirtualMachines.Get call respond with enough 429
   594  	// (StatusTooManyRequests) failures to cause the method to give
   595  	// up retrying.
   596  	rateLimitedSender := mocks.NewSender()
   597  	rateLimitedSender.AppendAndRepeatResponse(mocks.NewResponseWithBodyAndStatus(
   598  		mocks.NewBody("{}"), // empty JSON response to appease go-autorest
   599  		http.StatusTooManyRequests,
   600  		"(」゜ロ゜)」",
   601  	), failures)
   602  	senders = senders[:len(senders)-1]
   603  	for i := 0; i < failures; i++ {
   604  		senders = append(senders, rateLimitedSender)
   605  	}
   606  	s.sender = senders
   607  
   608  	_, err := env.StartInstance(makeStartInstanceParams(c, s.controllerUUID, "quantal"))
   609  	c.Assert(err, gc.ErrorMatches, `creating virtual machine "machine-0": creating deployment "machine-0": max duration exceeded: .*`)
   610  
   611  	s.retryClock.CheckCalls(c, []gitjujutesting.StubCall{
   612  		{"After", []interface{}{5 * time.Second}},  // t0 + 5s
   613  		{"After", []interface{}{10 * time.Second}}, // t0 + 15s
   614  		{"After", []interface{}{20 * time.Second}}, // t0 + 35s
   615  		{"After", []interface{}{40 * time.Second}}, // t0 + 1m15s
   616  		{"After", []interface{}{1 * time.Minute}},  // t0 + 2m15s
   617  		{"After", []interface{}{1 * time.Minute}},  // t0 + 3m15s
   618  		{"After", []interface{}{1 * time.Minute}},  // t0 + 4m15s
   619  		// There would be another call here, but since the time
   620  		// exceeds the give minute limit, retrying is aborted.
   621  	})
   622  }
   623  
   624  func (s *environSuite) TestStartInstanceDistributionGroup(c *gc.C) {
   625  	c.Skip("TODO: test StartInstance's DistributionGroup behaviour")
   626  }
   627  
   628  func (s *environSuite) TestStartInstanceServiceAvailabilitySet(c *gc.C) {
   629  	env := s.openEnviron(c)
   630  	unitsDeployed := "mysql/0 wordpress/0"
   631  	s.vmTags[tags.JujuUnitsDeployed] = &unitsDeployed
   632  	s.sender = s.startInstanceSenders(false)
   633  	s.requests = nil
   634  	params := makeStartInstanceParams(c, s.controllerUUID, "quantal")
   635  	params.InstanceConfig.Tags[tags.JujuUnitsDeployed] = unitsDeployed
   636  
   637  	_, err := env.StartInstance(params)
   638  	c.Assert(err, jc.ErrorIsNil)
   639  	s.assertStartInstanceRequests(c, s.requests, assertStartInstanceRequestsParams{
   640  		availabilitySetName: "mysql",
   641  		imageReference:      &quantalImageReference,
   642  		diskSizeGB:          32,
   643  		osProfile:           &linuxOsProfile,
   644  	})
   645  }
   646  
   647  const numExpectedStartInstanceRequests = 3
   648  
   649  type assertStartInstanceRequestsParams struct {
   650  	availabilitySetName string
   651  	imageReference      *compute.ImageReference
   652  	vmExtension         *compute.VirtualMachineExtensionProperties
   653  	diskSizeGB          int
   654  	osProfile           *compute.OSProfile
   655  }
   656  
   657  func (s *environSuite) assertStartInstanceRequests(
   658  	c *gc.C,
   659  	requests []*http.Request,
   660  	args assertStartInstanceRequestsParams,
   661  ) startInstanceRequests {
   662  	nsgId := `[resourceId('Microsoft.Network/networkSecurityGroups', 'juju-internal-nsg')]`
   663  	securityRules := []network.SecurityRule{{
   664  		Name: to.StringPtr("SSHInbound"),
   665  		Properties: &network.SecurityRulePropertiesFormat{
   666  			Description:              to.StringPtr("Allow SSH access to all machines"),
   667  			Protocol:                 network.TCP,
   668  			SourceAddressPrefix:      to.StringPtr("*"),
   669  			SourcePortRange:          to.StringPtr("*"),
   670  			DestinationAddressPrefix: to.StringPtr("*"),
   671  			DestinationPortRange:     to.StringPtr("22"),
   672  			Access:                   network.Allow,
   673  			Priority:                 to.Int32Ptr(100),
   674  			Direction:                network.Inbound,
   675  		},
   676  	}, {
   677  		Name: to.StringPtr("JujuAPIInbound"),
   678  		Properties: &network.SecurityRulePropertiesFormat{
   679  			Description:              to.StringPtr("Allow API connections to controller machines"),
   680  			Protocol:                 network.TCP,
   681  			SourceAddressPrefix:      to.StringPtr("*"),
   682  			SourcePortRange:          to.StringPtr("*"),
   683  			DestinationAddressPrefix: to.StringPtr("192.168.16.0/20"),
   684  			DestinationPortRange:     to.StringPtr("17777"),
   685  			Access:                   network.Allow,
   686  			Priority:                 to.Int32Ptr(101),
   687  			Direction:                network.Inbound,
   688  		},
   689  	}}
   690  	subnets := []network.Subnet{{
   691  		Name: to.StringPtr("juju-internal-subnet"),
   692  		Properties: &network.SubnetPropertiesFormat{
   693  			AddressPrefix: to.StringPtr("192.168.0.0/20"),
   694  			NetworkSecurityGroup: &network.SecurityGroup{
   695  				ID: to.StringPtr(nsgId),
   696  			},
   697  		},
   698  	}, {
   699  		Name: to.StringPtr("juju-controller-subnet"),
   700  		Properties: &network.SubnetPropertiesFormat{
   701  			AddressPrefix: to.StringPtr("192.168.16.0/20"),
   702  			NetworkSecurityGroup: &network.SecurityGroup{
   703  				ID: to.StringPtr(nsgId),
   704  			},
   705  		},
   706  	}}
   707  
   708  	subnetName := "juju-internal-subnet"
   709  	privateIPAddress := "192.168.0.4"
   710  	if args.availabilitySetName == "juju-controller" {
   711  		subnetName = "juju-controller-subnet"
   712  		privateIPAddress = "192.168.16.4"
   713  	}
   714  	subnetId := fmt.Sprintf(
   715  		`[concat(resourceId('Microsoft.Network/virtualNetworks', 'juju-internal-network'), '/subnets/%s')]`,
   716  		subnetName,
   717  	)
   718  
   719  	publicIPAddressId := `[resourceId('Microsoft.Network/publicIPAddresses', 'machine-0-public-ip')]`
   720  
   721  	ipConfigurations := []network.InterfaceIPConfiguration{{
   722  		Name: to.StringPtr("primary"),
   723  		Properties: &network.InterfaceIPConfigurationPropertiesFormat{
   724  			Primary:                   to.BoolPtr(true),
   725  			PrivateIPAddress:          to.StringPtr(privateIPAddress),
   726  			PrivateIPAllocationMethod: network.Static,
   727  			Subnet: &network.Subnet{ID: to.StringPtr(subnetId)},
   728  			PublicIPAddress: &network.PublicIPAddress{
   729  				ID: to.StringPtr(publicIPAddressId),
   730  			},
   731  		},
   732  	}}
   733  
   734  	nicId := `[resourceId('Microsoft.Network/networkInterfaces', 'machine-0-primary')]`
   735  	nics := []compute.NetworkInterfaceReference{{
   736  		ID: to.StringPtr(nicId),
   737  		Properties: &compute.NetworkInterfaceReferenceProperties{
   738  			Primary: to.BoolPtr(true),
   739  		},
   740  	}}
   741  	vmDependsOn := []string{
   742  		nicId,
   743  		`[resourceId('Microsoft.Storage/storageAccounts', '` + storageAccountName + `')]`,
   744  	}
   745  
   746  	addressPrefixes := []string{"192.168.0.0/20", "192.168.16.0/20"}
   747  	templateResources := []armtemplates.Resource{{
   748  		APIVersion: network.APIVersion,
   749  		Type:       "Microsoft.Network/networkSecurityGroups",
   750  		Name:       "juju-internal-nsg",
   751  		Location:   "westus",
   752  		Tags:       to.StringMap(s.envTags),
   753  		Properties: &network.SecurityGroupPropertiesFormat{
   754  			SecurityRules: &securityRules,
   755  		},
   756  	}, {
   757  		APIVersion: network.APIVersion,
   758  		Type:       "Microsoft.Network/virtualNetworks",
   759  		Name:       "juju-internal-network",
   760  		Location:   "westus",
   761  		Tags:       to.StringMap(s.envTags),
   762  		Properties: &network.VirtualNetworkPropertiesFormat{
   763  			AddressSpace: &network.AddressSpace{&addressPrefixes},
   764  			Subnets:      &subnets,
   765  		},
   766  		DependsOn: []string{nsgId},
   767  	}, {
   768  		APIVersion: storage.APIVersion,
   769  		Type:       "Microsoft.Storage/storageAccounts",
   770  		Name:       storageAccountName,
   771  		Location:   "westus",
   772  		Tags:       to.StringMap(s.envTags),
   773  		StorageSku: &storage.Sku{
   774  			Name: storage.SkuName("Standard_LRS"),
   775  		},
   776  	}}
   777  
   778  	var availabilitySetSubResource *compute.SubResource
   779  	if args.availabilitySetName != "" {
   780  		availabilitySetId := fmt.Sprintf(
   781  			`[resourceId('Microsoft.Compute/availabilitySets','%s')]`,
   782  			args.availabilitySetName,
   783  		)
   784  		templateResources = append(templateResources, armtemplates.Resource{
   785  			APIVersion: compute.APIVersion,
   786  			Type:       "Microsoft.Compute/availabilitySets",
   787  			Name:       args.availabilitySetName,
   788  			Location:   "westus",
   789  			Tags:       to.StringMap(s.envTags),
   790  		})
   791  		availabilitySetSubResource = &compute.SubResource{
   792  			ID: to.StringPtr(availabilitySetId),
   793  		}
   794  		vmDependsOn = append([]string{availabilitySetId}, vmDependsOn...)
   795  	}
   796  
   797  	templateResources = append(templateResources, []armtemplates.Resource{{
   798  		APIVersion: network.APIVersion,
   799  		Type:       "Microsoft.Network/publicIPAddresses",
   800  		Name:       "machine-0-public-ip",
   801  		Location:   "westus",
   802  		Tags:       to.StringMap(s.vmTags),
   803  		Properties: &network.PublicIPAddressPropertiesFormat{
   804  			PublicIPAllocationMethod: network.Dynamic,
   805  		},
   806  	}, {
   807  		APIVersion: network.APIVersion,
   808  		Type:       "Microsoft.Network/networkInterfaces",
   809  		Name:       "machine-0-primary",
   810  		Location:   "westus",
   811  		Tags:       to.StringMap(s.vmTags),
   812  		Properties: &network.InterfacePropertiesFormat{
   813  			IPConfigurations: &ipConfigurations,
   814  		},
   815  		DependsOn: []string{
   816  			publicIPAddressId,
   817  			`[resourceId('Microsoft.Network/virtualNetworks', 'juju-internal-network')]`,
   818  		},
   819  	}, {
   820  		APIVersion: compute.APIVersion,
   821  		Type:       "Microsoft.Compute/virtualMachines",
   822  		Name:       "machine-0",
   823  		Location:   "westus",
   824  		Tags:       to.StringMap(s.vmTags),
   825  		Properties: &compute.VirtualMachineProperties{
   826  			HardwareProfile: &compute.HardwareProfile{
   827  				VMSize: "Standard_D1",
   828  			},
   829  			StorageProfile: &compute.StorageProfile{
   830  				ImageReference: args.imageReference,
   831  				OsDisk: &compute.OSDisk{
   832  					Name:         to.StringPtr("machine-0"),
   833  					CreateOption: compute.FromImage,
   834  					Caching:      compute.ReadWrite,
   835  					Vhd: &compute.VirtualHardDisk{
   836  						URI: to.StringPtr(fmt.Sprintf(
   837  							`[concat(reference(resourceId('Microsoft.Storage/storageAccounts', '%s'), '%s').primaryEndpoints.blob, 'osvhds/machine-0.vhd')]`,
   838  							storageAccountName, storage.APIVersion,
   839  						)),
   840  					},
   841  					DiskSizeGB: to.Int32Ptr(int32(args.diskSizeGB)),
   842  				},
   843  			},
   844  			OsProfile:       args.osProfile,
   845  			NetworkProfile:  &compute.NetworkProfile{&nics},
   846  			AvailabilitySet: availabilitySetSubResource,
   847  		},
   848  		DependsOn: vmDependsOn,
   849  	}}...)
   850  	if args.vmExtension != nil {
   851  		templateResources = append(templateResources, armtemplates.Resource{
   852  			APIVersion: compute.APIVersion,
   853  			Type:       "Microsoft.Compute/virtualMachines/extensions",
   854  			Name:       "machine-0/JujuCustomScriptExtension",
   855  			Location:   "westus",
   856  			Tags:       to.StringMap(s.vmTags),
   857  			Properties: args.vmExtension,
   858  			DependsOn:  []string{"Microsoft.Compute/virtualMachines/machine-0"},
   859  		})
   860  	}
   861  	templateMap := map[string]interface{}{
   862  		"$schema":        "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
   863  		"contentVersion": "1.0.0.0",
   864  		"resources":      templateResources,
   865  	}
   866  	deployment := &resources.Deployment{
   867  		&resources.DeploymentProperties{
   868  			Template: &templateMap,
   869  			Mode:     resources.Incremental,
   870  		},
   871  	}
   872  
   873  	// Validate HTTP request bodies.
   874  	var startInstanceRequests startInstanceRequests
   875  	if args.vmExtension != nil {
   876  		// It must be Windows or CentOS, so
   877  		// there should be no image query.
   878  		c.Assert(requests, gc.HasLen, numExpectedStartInstanceRequests-1)
   879  		c.Assert(requests[0].Method, gc.Equals, "GET") // vmSizes
   880  		c.Assert(requests[1].Method, gc.Equals, "PUT") // create deployment
   881  		startInstanceRequests.vmSizes = requests[0]
   882  		startInstanceRequests.deployment = requests[1]
   883  	} else {
   884  		c.Assert(requests, gc.HasLen, numExpectedStartInstanceRequests)
   885  		c.Assert(requests[0].Method, gc.Equals, "GET") // vmSizes
   886  		c.Assert(requests[1].Method, gc.Equals, "GET") // skus
   887  		c.Assert(requests[2].Method, gc.Equals, "PUT") // create deployment
   888  		startInstanceRequests.vmSizes = requests[0]
   889  		startInstanceRequests.skus = requests[1]
   890  		startInstanceRequests.deployment = requests[2]
   891  	}
   892  
   893  	// Marshal/unmarshal the deployment we expect, so it's in map form.
   894  	var expected resources.Deployment
   895  	data, err := json.Marshal(&deployment)
   896  	c.Assert(err, jc.ErrorIsNil)
   897  	err = json.Unmarshal(data, &expected)
   898  	c.Assert(err, jc.ErrorIsNil)
   899  
   900  	// Check that we send what we expect. CustomData is non-deterministic,
   901  	// so don't compare it.
   902  	// TODO(axw) shouldn't CustomData be deterministic? Look into this.
   903  	var actual resources.Deployment
   904  	unmarshalRequestBody(c, startInstanceRequests.deployment, &actual)
   905  	c.Assert(actual.Properties, gc.NotNil)
   906  	c.Assert(actual.Properties.Template, gc.NotNil)
   907  	resources := (*actual.Properties.Template)["resources"].([]interface{})
   908  	c.Assert(resources, gc.HasLen, len(templateResources))
   909  
   910  	vmResourceIndex := len(resources) - 1
   911  	if args.vmExtension != nil {
   912  		vmResourceIndex--
   913  	}
   914  	vmResource := resources[vmResourceIndex].(map[string]interface{})
   915  	vmResourceProperties := vmResource["properties"].(map[string]interface{})
   916  	osProfile := vmResourceProperties["osProfile"].(map[string]interface{})
   917  	osProfile["customData"] = "<juju-goes-here>"
   918  	c.Assert(actual, jc.DeepEquals, expected)
   919  
   920  	return startInstanceRequests
   921  }
   922  
   923  type startInstanceRequests struct {
   924  	vmSizes    *http.Request
   925  	skus       *http.Request
   926  	deployment *http.Request
   927  }
   928  
   929  func (s *environSuite) TestBootstrap(c *gc.C) {
   930  	defer envtesting.DisableFinishBootstrap()()
   931  
   932  	ctx := envtesting.BootstrapContext(c)
   933  	env := prepareForBootstrap(c, ctx, s.provider, &s.sender)
   934  
   935  	s.sender = s.initResourceGroupSenders()
   936  	s.sender = append(s.sender, s.startInstanceSenders(true)...)
   937  	s.requests = nil
   938  	result, err := env.Bootstrap(
   939  		ctx, environs.BootstrapParams{
   940  			ControllerConfig: testing.FakeControllerConfig(),
   941  			AvailableTools:   makeToolsList("quantal"),
   942  			BootstrapSeries:  "quantal",
   943  		},
   944  	)
   945  	c.Assert(err, jc.ErrorIsNil)
   946  	c.Assert(result.Arch, gc.Equals, "amd64")
   947  	c.Assert(result.Series, gc.Equals, "quantal")
   948  
   949  	c.Assert(len(s.requests), gc.Equals, numExpectedStartInstanceRequests+1)
   950  	s.vmTags[tags.JujuIsController] = to.StringPtr("true")
   951  	s.assertStartInstanceRequests(c, s.requests[1:], assertStartInstanceRequestsParams{
   952  		availabilitySetName: "juju-controller",
   953  		imageReference:      &quantalImageReference,
   954  		diskSizeGB:          32,
   955  		osProfile:           &linuxOsProfile,
   956  	})
   957  }
   958  
   959  func (s *environSuite) TestAllInstancesResourceGroupNotFound(c *gc.C) {
   960  	env := s.openEnviron(c)
   961  	sender := mocks.NewSender()
   962  	sender.AppendResponse(mocks.NewResponseWithStatus(
   963  		"resource group not found", http.StatusNotFound,
   964  	))
   965  	s.sender = azuretesting.Senders{sender}
   966  	_, err := env.AllInstances()
   967  	c.Assert(err, jc.ErrorIsNil)
   968  }
   969  
   970  func (s *environSuite) TestStopInstancesNotFound(c *gc.C) {
   971  	env := s.openEnviron(c)
   972  	sender0 := mocks.NewSender()
   973  	sender0.AppendResponse(mocks.NewResponseWithStatus(
   974  		"vm not found", http.StatusNotFound,
   975  	))
   976  	sender1 := mocks.NewSender()
   977  	sender1.AppendResponse(mocks.NewResponseWithStatus(
   978  		"vm not found", http.StatusNotFound,
   979  	))
   980  	s.sender = azuretesting.Senders{sender0, sender1}
   981  	err := env.StopInstances("a", "b")
   982  	c.Assert(err, jc.ErrorIsNil)
   983  }
   984  
   985  func (s *environSuite) TestStopInstances(c *gc.C) {
   986  	env := s.openEnviron(c)
   987  
   988  	// Security group has rules for machine-0, as well as a rule that doesn't match.
   989  	nsg := makeSecurityGroup(
   990  		makeSecurityRule("machine-0-80", "192.168.0.4", "80"),
   991  		makeSecurityRule("machine-0-1000-2000", "192.168.0.4", "1000-2000"),
   992  		makeSecurityRule("machine-42", "192.168.0.5", "*"),
   993  	)
   994  
   995  	// Create an IP configuration with a public IP reference. This will
   996  	// cause an update to the NIC to detach public IPs.
   997  	nic0IPConfiguration := makeIPConfiguration("192.168.0.4")
   998  	nic0IPConfiguration.Properties.PublicIPAddress = &network.PublicIPAddress{}
   999  	nic0 := makeNetworkInterface("nic-0", "machine-0", nic0IPConfiguration)
  1000  
  1001  	s.sender = azuretesting.Senders{
  1002  		s.makeSender(".*/deployments/machine-0/cancel", nil), // POST
  1003  		s.storageAccountSender(),
  1004  		s.storageAccountKeysSender(),
  1005  		s.networkInterfacesSender(nic0),
  1006  		s.publicIPAddressesSender(makePublicIPAddress("pip-0", "machine-0", "1.2.3.4")),
  1007  		s.makeSender(".*/virtualMachines/machine-0", nil),                                                 // DELETE
  1008  		s.makeSender(".*/networkSecurityGroups/juju-internal-nsg", nsg),                                   // GET
  1009  		s.makeSender(".*/networkSecurityGroups/juju-internal-nsg/securityRules/machine-0-80", nil),        // DELETE
  1010  		s.makeSender(".*/networkSecurityGroups/juju-internal-nsg/securityRules/machine-0-1000-2000", nil), // DELETE
  1011  		s.makeSender(".*/networkInterfaces/nic-0", nil),                                                   // DELETE
  1012  		s.makeSender(".*/publicIPAddresses/pip-0", nil),                                                   // DELETE
  1013  		s.makeSender(".*/deployments/machine-0", nil),                                                     // DELETE
  1014  	}
  1015  	err := env.StopInstances("machine-0")
  1016  	c.Assert(err, jc.ErrorIsNil)
  1017  
  1018  	s.storageClient.CheckCallNames(c,
  1019  		"NewClient", "DeleteBlobIfExists",
  1020  	)
  1021  	s.storageClient.CheckCall(c, 1, "DeleteBlobIfExists", "osvhds", "machine-0")
  1022  }
  1023  
  1024  func (s *environSuite) TestStopInstancesMultiple(c *gc.C) {
  1025  	env := s.openEnviron(c)
  1026  
  1027  	vmDeleteSender0 := s.makeSender(".*/virtualMachines/machine-[01]", nil)
  1028  	vmDeleteSender1 := s.makeSender(".*/virtualMachines/machine-[01]", nil)
  1029  	vmDeleteSender0.SetError(errors.New("blargh"))
  1030  	vmDeleteSender1.SetError(errors.New("blargh"))
  1031  
  1032  	s.sender = azuretesting.Senders{
  1033  		s.makeSender(".*/deployments/machine-[01]/cancel", nil), // POST
  1034  		s.makeSender(".*/deployments/machine-[01]/cancel", nil), // POST
  1035  
  1036  		// We should only query the NICs, public IPs, and storage
  1037  		// account/keys, regardless of how many instances are deleted.
  1038  		s.storageAccountSender(),
  1039  		s.storageAccountKeysSender(),
  1040  		s.networkInterfacesSender(),
  1041  		s.publicIPAddressesSender(),
  1042  
  1043  		vmDeleteSender0,
  1044  		vmDeleteSender1,
  1045  	}
  1046  	err := env.StopInstances("machine-0", "machine-1")
  1047  	c.Assert(err, gc.ErrorMatches, `deleting instance "machine-[01]":.*blargh`)
  1048  }
  1049  
  1050  func (s *environSuite) TestStopInstancesDeploymentNotFound(c *gc.C) {
  1051  	env := s.openEnviron(c)
  1052  
  1053  	cancelSender := mocks.NewSender()
  1054  	cancelSender.AppendResponse(mocks.NewResponseWithStatus(
  1055  		"deployment not found", http.StatusNotFound,
  1056  	))
  1057  	s.sender = azuretesting.Senders{cancelSender}
  1058  	err := env.StopInstances("machine-0")
  1059  	c.Assert(err, jc.ErrorIsNil)
  1060  }
  1061  
  1062  func (s *environSuite) TestStopInstancesStorageAccountNoKeys(c *gc.C) {
  1063  	s.PatchValue(&s.storageAccountKeys.Keys, nil)
  1064  	s.testStopInstancesStorageAccountNotFound(c)
  1065  }
  1066  
  1067  func (s *environSuite) TestStopInstancesStorageAccountNoFullKey(c *gc.C) {
  1068  	keys := *s.storageAccountKeys.Keys
  1069  	s.PatchValue(&keys[0].Permissions, storage.READ)
  1070  	s.testStopInstancesStorageAccountNotFound(c)
  1071  }
  1072  
  1073  func (s *environSuite) testStopInstancesStorageAccountNotFound(c *gc.C) {
  1074  	env := s.openEnviron(c)
  1075  	s.sender = azuretesting.Senders{
  1076  		s.makeSender("/deployments/machine-0", s.deployment), // Cancel
  1077  		s.storageAccountSender(),
  1078  		s.storageAccountKeysSender(),
  1079  		s.networkInterfacesSender(),                                                     // GET: no NICs
  1080  		s.publicIPAddressesSender(),                                                     // GET: no public IPs
  1081  		s.makeSender(".*/virtualMachines/machine-0", nil),                               // DELETE
  1082  		s.makeSender(".*/networkSecurityGroups/juju-internal-nsg", makeSecurityGroup()), // GET: no rules
  1083  		s.makeSender(".*/deployments/machine-0", nil),                                   // DELETE
  1084  	}
  1085  	err := env.StopInstances("machine-0")
  1086  	c.Assert(err, jc.ErrorIsNil)
  1087  }
  1088  
  1089  func (s *environSuite) TestStopInstancesStorageAccountError(c *gc.C) {
  1090  	env := s.openEnviron(c)
  1091  	errorSender := s.storageAccountSender()
  1092  	errorSender.SetError(errors.New("blargh"))
  1093  	s.sender = azuretesting.Senders{
  1094  		s.makeSender("/deployments/machine-0", s.deployment), // Cancel
  1095  		errorSender,
  1096  	}
  1097  	err := env.StopInstances("machine-0")
  1098  	c.Assert(err, gc.ErrorMatches, "getting storage account:.*blargh")
  1099  }
  1100  
  1101  func (s *environSuite) TestStopInstancesStorageAccountKeysError(c *gc.C) {
  1102  	env := s.openEnviron(c)
  1103  	errorSender := s.storageAccountKeysSender()
  1104  	errorSender.SetError(errors.New("blargh"))
  1105  	s.sender = azuretesting.Senders{
  1106  		s.makeSender("/deployments/machine-0", s.deployment), // Cancel
  1107  		s.storageAccountSender(),
  1108  		errorSender,
  1109  	}
  1110  	err := env.StopInstances("machine-0")
  1111  	c.Assert(err, gc.ErrorMatches, "getting storage account key:.*blargh")
  1112  }
  1113  
  1114  func (s *environSuite) TestConstraintsValidatorUnsupported(c *gc.C) {
  1115  	validator := s.constraintsValidator(c)
  1116  	unsupported, err := validator.Validate(constraints.MustParse(
  1117  		"arch=amd64 tags=foo cpu-power=100 virt-type=kvm",
  1118  	))
  1119  	c.Assert(err, jc.ErrorIsNil)
  1120  	c.Assert(unsupported, jc.SameContents, []string{"tags", "cpu-power", "virt-type"})
  1121  }
  1122  
  1123  func (s *environSuite) TestConstraintsValidatorVocabulary(c *gc.C) {
  1124  	validator := s.constraintsValidator(c)
  1125  	_, err := validator.Validate(constraints.MustParse("arch=armhf"))
  1126  	c.Assert(err, gc.ErrorMatches,
  1127  		"invalid constraint value: arch=armhf\nvalid values are: \\[amd64\\]",
  1128  	)
  1129  	_, err = validator.Validate(constraints.MustParse("instance-type=t1.micro"))
  1130  	c.Assert(err, gc.ErrorMatches,
  1131  		"invalid constraint value: instance-type=t1.micro\nvalid values are: \\[D1 Standard_D1\\]",
  1132  	)
  1133  }
  1134  
  1135  func (s *environSuite) TestConstraintsValidatorMerge(c *gc.C) {
  1136  	validator := s.constraintsValidator(c)
  1137  	cons, err := validator.Merge(
  1138  		constraints.MustParse("mem=3G arch=amd64"),
  1139  		constraints.MustParse("instance-type=D1"),
  1140  	)
  1141  	c.Assert(err, jc.ErrorIsNil)
  1142  	c.Assert(cons.String(), gc.Equals, "instance-type=D1")
  1143  }
  1144  
  1145  func (s *environSuite) constraintsValidator(c *gc.C) constraints.Validator {
  1146  	env := s.openEnviron(c)
  1147  	s.sender = azuretesting.Senders{s.vmSizesSender()}
  1148  	validator, err := env.ConstraintsValidator()
  1149  	c.Assert(err, jc.ErrorIsNil)
  1150  	return validator
  1151  }
  1152  
  1153  func (s *environSuite) TestAgentMirror(c *gc.C) {
  1154  	env := s.openEnviron(c)
  1155  	c.Assert(env, gc.Implements, new(envtools.HasAgentMirror))
  1156  	cloudSpec, err := env.(envtools.HasAgentMirror).AgentMirror()
  1157  	c.Assert(err, jc.ErrorIsNil)
  1158  	c.Assert(cloudSpec, gc.Equals, simplestreams.CloudSpec{
  1159  		Region:   "westus",
  1160  		Endpoint: "https://storage.azurestack.local/",
  1161  	})
  1162  }
  1163  
  1164  func (s *environSuite) TestDestroyHostedModel(c *gc.C) {
  1165  	env := s.openEnviron(c, testing.Attrs{"controller-uuid": utils.MustNewUUID().String()})
  1166  	s.sender = azuretesting.Senders{
  1167  		s.makeSender(".*/resourcegroups/juju-testenv-model-"+testing.ModelTag.Id(), nil), // DELETE
  1168  	}
  1169  	err := env.Destroy()
  1170  	c.Assert(err, jc.ErrorIsNil)
  1171  	c.Assert(s.requests, gc.HasLen, 1)
  1172  	c.Assert(s.requests[0].Method, gc.Equals, "DELETE")
  1173  }
  1174  
  1175  func (s *environSuite) TestDestroyController(c *gc.C) {
  1176  	groups := []resources.ResourceGroup{{
  1177  		Name: to.StringPtr("group1"),
  1178  	}, {
  1179  		Name: to.StringPtr("group2"),
  1180  	}}
  1181  	result := resources.ResourceGroupListResult{Value: &groups}
  1182  
  1183  	env := s.openEnviron(c)
  1184  	s.sender = azuretesting.Senders{
  1185  		s.makeSender(".*/resourcegroups", result),        // GET
  1186  		s.makeSender(".*/resourcegroups/group[12]", nil), // DELETE
  1187  		s.makeSender(".*/resourcegroups/group[12]", nil), // DELETE
  1188  	}
  1189  	err := env.DestroyController(s.controllerUUID)
  1190  	c.Assert(err, jc.ErrorIsNil)
  1191  
  1192  	c.Assert(s.requests, gc.HasLen, 3)
  1193  	c.Assert(s.requests[0].Method, gc.Equals, "GET")
  1194  	c.Assert(s.requests[0].URL.Query().Get("$filter"), gc.Equals, fmt.Sprintf(
  1195  		"tagname eq 'juju-controller-uuid' and tagvalue eq '%s'",
  1196  		testing.ControllerTag.Id(),
  1197  	))
  1198  	c.Assert(s.requests[1].Method, gc.Equals, "DELETE")
  1199  	c.Assert(s.requests[2].Method, gc.Equals, "DELETE")
  1200  
  1201  	// Groups are deleted concurrently, so there's no known order.
  1202  	groupsDeleted := []string{
  1203  		path.Base(s.requests[1].URL.Path),
  1204  		path.Base(s.requests[2].URL.Path),
  1205  	}
  1206  	c.Assert(groupsDeleted, jc.SameContents, []string{"group1", "group2"})
  1207  }
  1208  
  1209  func (s *environSuite) TestDestroyControllerErrors(c *gc.C) {
  1210  	groups := []resources.ResourceGroup{
  1211  		{Name: to.StringPtr("group1")},
  1212  		{Name: to.StringPtr("group2")},
  1213  	}
  1214  	result := resources.ResourceGroupListResult{Value: &groups}
  1215  
  1216  	makeErrorSender := func(err string) *azuretesting.MockSender {
  1217  		errorSender := &azuretesting.MockSender{
  1218  			Sender:      mocks.NewSender(),
  1219  			PathPattern: ".*/resourcegroups/group[12].*",
  1220  		}
  1221  		errorSender.SetError(errors.New(err))
  1222  		return errorSender
  1223  	}
  1224  
  1225  	env := s.openEnviron(c)
  1226  	s.requests = nil
  1227  	s.sender = azuretesting.Senders{
  1228  		s.makeSender(".*/resourcegroups", result), // GET
  1229  		makeErrorSender("foo"),                    // DELETE
  1230  		makeErrorSender("bar"),                    // DELETE
  1231  	}
  1232  	destroyErr := env.DestroyController(s.controllerUUID)
  1233  	// checked below, once we know the order of deletions.
  1234  
  1235  	c.Assert(s.requests, gc.HasLen, 3)
  1236  	c.Assert(s.requests[0].Method, gc.Equals, "GET")
  1237  	c.Assert(s.requests[1].Method, gc.Equals, "DELETE")
  1238  	c.Assert(s.requests[2].Method, gc.Equals, "DELETE")
  1239  
  1240  	// Groups are deleted concurrently, so there's no known order.
  1241  	groupsDeleted := []string{
  1242  		path.Base(s.requests[1].URL.Path),
  1243  		path.Base(s.requests[2].URL.Path),
  1244  	}
  1245  	c.Assert(groupsDeleted, jc.SameContents, []string{"group1", "group2"})
  1246  
  1247  	c.Check(destroyErr, gc.ErrorMatches,
  1248  		`deleting resource group "group1":.*; `+
  1249  			`deleting resource group "group2":.*`)
  1250  	c.Check(destroyErr, gc.ErrorMatches, ".*foo.*")
  1251  	c.Check(destroyErr, gc.ErrorMatches, ".*bar.*")
  1252  }