1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
20 "git.arvados.org/arvados.git/lib/cloud"
21 "git.arvados.org/arvados.git/sdk/go/arvados"
22 "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-07-01/compute"
23 "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2018-06-01/network"
24 storageacct "github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2018-02-01/storage"
25 "github.com/Azure/azure-sdk-for-go/storage"
26 "github.com/Azure/go-autorest/autorest"
27 "github.com/Azure/go-autorest/autorest/azure"
28 "github.com/Azure/go-autorest/autorest/azure/auth"
29 "github.com/Azure/go-autorest/autorest/to"
30 "github.com/jmcvetta/randutil"
31 "github.com/prometheus/client_golang/prometheus"
32 "github.com/sirupsen/logrus"
33 "golang.org/x/crypto/ssh"
36 // Driver is the azure implementation of the cloud.Driver interface.
37 var Driver = cloud.DriverFunc(newAzureInstanceSet)
39 type azureInstanceSetConfig struct {
44 CloudEnvironment string
46 ImageResourceGroup string
49 NetworkResourceGroup string
53 SharedImageGalleryName string
54 SharedImageGalleryImageVersion string
55 DeleteDanglingResourcesAfter arvados.Duration
59 type containerWrapper interface {
60 GetBlobReference(name string) *storage.Blob
61 ListBlobs(params storage.ListBlobsParameters) (storage.BlobListResponse, error)
64 type virtualMachinesClientWrapper interface {
65 createOrUpdate(ctx context.Context,
66 resourceGroupName string,
68 parameters compute.VirtualMachine) (result compute.VirtualMachine, err error)
69 delete(ctx context.Context, resourceGroupName string, VMName string) (result *http.Response, err error)
70 listComplete(ctx context.Context, resourceGroupName string) (result compute.VirtualMachineListResultIterator, err error)
73 type virtualMachinesClientImpl struct {
74 inner compute.VirtualMachinesClient
77 func (cl *virtualMachinesClientImpl) createOrUpdate(ctx context.Context,
78 resourceGroupName string,
80 parameters compute.VirtualMachine) (result compute.VirtualMachine, err error) {
82 future, err := cl.inner.CreateOrUpdate(ctx, resourceGroupName, VMName, parameters)
84 return compute.VirtualMachine{}, wrapAzureError(err)
86 future.WaitForCompletionRef(ctx, cl.inner.Client)
87 r, err := future.Result(cl.inner)
88 return r, wrapAzureError(err)
91 func (cl *virtualMachinesClientImpl) delete(ctx context.Context, resourceGroupName string, VMName string) (result *http.Response, err error) {
92 future, err := cl.inner.Delete(ctx, resourceGroupName, VMName)
94 return nil, wrapAzureError(err)
96 err = future.WaitForCompletionRef(ctx, cl.inner.Client)
97 return future.Response(), wrapAzureError(err)
100 func (cl *virtualMachinesClientImpl) listComplete(ctx context.Context, resourceGroupName string) (result compute.VirtualMachineListResultIterator, err error) {
101 r, err := cl.inner.ListComplete(ctx, resourceGroupName)
102 return r, wrapAzureError(err)
105 type interfacesClientWrapper interface {
106 createOrUpdate(ctx context.Context,
107 resourceGroupName string,
108 networkInterfaceName string,
109 parameters network.Interface) (result network.Interface, err error)
110 delete(ctx context.Context, resourceGroupName string, networkInterfaceName string) (result *http.Response, err error)
111 listComplete(ctx context.Context, resourceGroupName string) (result network.InterfaceListResultIterator, err error)
114 type interfacesClientImpl struct {
115 inner network.InterfacesClient
118 func (cl *interfacesClientImpl) delete(ctx context.Context, resourceGroupName string, VMName string) (result *http.Response, err error) {
119 future, err := cl.inner.Delete(ctx, resourceGroupName, VMName)
121 return nil, wrapAzureError(err)
123 err = future.WaitForCompletionRef(ctx, cl.inner.Client)
124 return future.Response(), wrapAzureError(err)
127 func (cl *interfacesClientImpl) createOrUpdate(ctx context.Context,
128 resourceGroupName string,
129 networkInterfaceName string,
130 parameters network.Interface) (result network.Interface, err error) {
132 future, err := cl.inner.CreateOrUpdate(ctx, resourceGroupName, networkInterfaceName, parameters)
134 return network.Interface{}, wrapAzureError(err)
136 future.WaitForCompletionRef(ctx, cl.inner.Client)
137 r, err := future.Result(cl.inner)
138 return r, wrapAzureError(err)
141 func (cl *interfacesClientImpl) listComplete(ctx context.Context, resourceGroupName string) (result network.InterfaceListResultIterator, err error) {
142 r, err := cl.inner.ListComplete(ctx, resourceGroupName)
143 return r, wrapAzureError(err)
146 type disksClientWrapper interface {
147 listByResourceGroup(ctx context.Context, resourceGroupName string) (result compute.DiskListPage, err error)
148 delete(ctx context.Context, resourceGroupName string, diskName string) (result compute.DisksDeleteFuture, err error)
151 type disksClientImpl struct {
152 inner compute.DisksClient
155 func (cl *disksClientImpl) listByResourceGroup(ctx context.Context, resourceGroupName string) (result compute.DiskListPage, err error) {
156 r, err := cl.inner.ListByResourceGroup(ctx, resourceGroupName)
157 return r, wrapAzureError(err)
160 func (cl *disksClientImpl) delete(ctx context.Context, resourceGroupName string, diskName string) (result compute.DisksDeleteFuture, err error) {
161 r, err := cl.inner.Delete(ctx, resourceGroupName, diskName)
162 return r, wrapAzureError(err)
165 var quotaRe = regexp.MustCompile(`(?i:exceed|quota|limit)`)
167 type azureRateLimitError struct {
172 func (ar *azureRateLimitError) EarliestRetry() time.Time {
176 type azureQuotaError struct {
180 func (ar *azureQuotaError) IsQuotaError() bool {
184 func wrapAzureError(err error) error {
185 de, ok := err.(autorest.DetailedError)
189 rq, ok := de.Original.(*azure.RequestError)
193 if rq.Response == nil {
196 if rq.Response.StatusCode == 429 || len(rq.Response.Header["Retry-After"]) >= 1 {
198 ra := rq.Response.Header["Retry-After"][0]
199 earliestRetry, parseErr := http.ParseTime(ra)
201 // Could not parse as a timestamp, must be number of seconds
202 dur, parseErr := strconv.ParseInt(ra, 10, 64)
204 earliestRetry = time.Now().Add(time.Duration(dur) * time.Second)
206 // Couldn't make sense of retry-after,
207 // so set retry to 20 seconds
208 earliestRetry = time.Now().Add(20 * time.Second)
211 return &azureRateLimitError{*rq, earliestRetry}
213 if rq.ServiceError == nil {
216 if quotaRe.FindString(rq.ServiceError.Code) != "" || quotaRe.FindString(rq.ServiceError.Message) != "" {
217 return &azureQuotaError{*rq}
222 type azureInstanceSet struct {
223 azconfig azureInstanceSetConfig
224 vmClient virtualMachinesClientWrapper
225 netClient interfacesClientWrapper
226 disksClient disksClientWrapper
227 imageResourceGroup string
228 blobcont containerWrapper
229 azureEnv azure.Environment
230 interfaces map[string]network.Interface
234 stopFunc context.CancelFunc
235 stopWg sync.WaitGroup
236 deleteNIC chan string
237 deleteBlob chan storage.Blob
238 deleteDisk chan compute.Disk
239 logger logrus.FieldLogger
242 func newAzureInstanceSet(config json.RawMessage, dispatcherID cloud.InstanceSetID, _ cloud.SharedResourceTags, logger logrus.FieldLogger, reg *prometheus.Registry) (prv cloud.InstanceSet, err error) {
243 azcfg := azureInstanceSetConfig{}
244 err = json.Unmarshal(config, &azcfg)
249 az := azureInstanceSet{logger: logger}
250 az.ctx, az.stopFunc = context.WithCancel(context.Background())
251 err = az.setup(azcfg, string(dispatcherID))
259 func (az *azureInstanceSet) setup(azcfg azureInstanceSetConfig, dispatcherID string) (err error) {
261 vmClient := compute.NewVirtualMachinesClient(az.azconfig.SubscriptionID)
262 netClient := network.NewInterfacesClient(az.azconfig.SubscriptionID)
263 disksClient := compute.NewDisksClient(az.azconfig.SubscriptionID)
264 storageAcctClient := storageacct.NewAccountsClient(az.azconfig.SubscriptionID)
266 az.azureEnv, err = azure.EnvironmentFromName(az.azconfig.CloudEnvironment)
271 authorizer, err := auth.ClientCredentialsConfig{
272 ClientID: az.azconfig.ClientID,
273 ClientSecret: az.azconfig.ClientSecret,
274 TenantID: az.azconfig.TenantID,
275 Resource: az.azureEnv.ResourceManagerEndpoint,
276 AADEndpoint: az.azureEnv.ActiveDirectoryEndpoint,
282 vmClient.Authorizer = authorizer
283 netClient.Authorizer = authorizer
284 disksClient.Authorizer = authorizer
285 storageAcctClient.Authorizer = authorizer
287 az.vmClient = &virtualMachinesClientImpl{vmClient}
288 az.netClient = &interfacesClientImpl{netClient}
289 az.disksClient = &disksClientImpl{disksClient}
291 az.imageResourceGroup = az.azconfig.ImageResourceGroup
292 if az.imageResourceGroup == "" {
293 az.imageResourceGroup = az.azconfig.ResourceGroup
296 var client storage.Client
297 if az.azconfig.StorageAccount != "" && az.azconfig.BlobContainer != "" {
298 result, err := storageAcctClient.ListKeys(az.ctx, az.azconfig.ResourceGroup, az.azconfig.StorageAccount)
300 az.logger.WithError(err).Warn("Couldn't get account keys")
304 key1 := *(*result.Keys)[0].Value
305 client, err = storage.NewBasicClientOnSovereignCloud(az.azconfig.StorageAccount, key1, az.azureEnv)
307 az.logger.WithError(err).Warn("Couldn't make client")
311 blobsvc := client.GetBlobService()
312 az.blobcont = blobsvc.GetContainerReference(az.azconfig.BlobContainer)
313 } else if az.azconfig.StorageAccount != "" || az.azconfig.BlobContainer != "" {
314 az.logger.Error("Invalid configuration: StorageAccount and BlobContainer must both be empty or both be set")
317 az.dispatcherID = dispatcherID
318 az.namePrefix = fmt.Sprintf("compute-%s-", az.dispatcherID)
322 defer az.stopWg.Done()
324 tk := time.NewTicker(5 * time.Minute)
327 case <-az.ctx.Done():
331 if az.blobcont != nil {
339 az.deleteNIC = make(chan string)
340 az.deleteBlob = make(chan storage.Blob)
341 az.deleteDisk = make(chan compute.Disk)
343 for i := 0; i < 4; i++ {
345 for nicname := range az.deleteNIC {
346 _, delerr := az.netClient.delete(context.Background(), az.azconfig.ResourceGroup, nicname)
348 az.logger.WithError(delerr).Warnf("Error deleting %v", nicname)
350 az.logger.Printf("Deleted NIC %v", nicname)
355 for blob := range az.deleteBlob {
356 err := blob.Delete(nil)
358 az.logger.WithError(err).Warnf("Error deleting %v", blob.Name)
360 az.logger.Printf("Deleted blob %v", blob.Name)
365 for disk := range az.deleteDisk {
366 _, err := az.disksClient.delete(az.ctx, az.imageResourceGroup, *disk.Name)
368 az.logger.WithError(err).Warnf("Error deleting disk %+v", *disk.Name)
370 az.logger.Printf("Deleted disk %v", *disk.Name)
379 func (az *azureInstanceSet) cleanupNic(nic network.Interface) {
380 _, delerr := az.netClient.delete(context.Background(), az.azconfig.ResourceGroup, *nic.Name)
382 az.logger.WithError(delerr).Warnf("Error cleaning up NIC after failed create")
386 func (az *azureInstanceSet) Create(
387 instanceType arvados.InstanceType,
388 imageID cloud.ImageID,
389 newTags cloud.InstanceTags,
390 initCommand cloud.InitCommand,
391 publicKey ssh.PublicKey) (cloud.Instance, error) {
394 defer az.stopWg.Done()
396 if instanceType.AddedScratch > 0 {
397 return nil, fmt.Errorf("cannot create instance type %q: driver does not implement non-zero AddedScratch (%d)", instanceType.Name, instanceType.AddedScratch)
400 name, err := randutil.String(15, "abcdefghijklmnopqrstuvwxyz0123456789")
405 name = az.namePrefix + name
407 tags := map[string]*string{}
408 for k, v := range newTags {
409 tags[k] = to.StringPtr(v)
411 tags["created-at"] = to.StringPtr(time.Now().Format(time.RFC3339Nano))
413 networkResourceGroup := az.azconfig.NetworkResourceGroup
414 if networkResourceGroup == "" {
415 networkResourceGroup = az.azconfig.ResourceGroup
418 nicParameters := network.Interface{
419 Location: &az.azconfig.Location,
421 InterfacePropertiesFormat: &network.InterfacePropertiesFormat{
422 IPConfigurations: &[]network.InterfaceIPConfiguration{
424 Name: to.StringPtr("ip1"),
425 InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{
426 Subnet: &network.Subnet{
427 ID: to.StringPtr(fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers"+
428 "/Microsoft.Network/virtualnetworks/%s/subnets/%s",
429 az.azconfig.SubscriptionID,
430 networkResourceGroup,
432 az.azconfig.Subnet)),
434 PrivateIPAllocationMethod: network.Dynamic,
440 nic, err := az.netClient.createOrUpdate(az.ctx, az.azconfig.ResourceGroup, name+"-nic", nicParameters)
442 return nil, wrapAzureError(err)
446 customData := base64.StdEncoding.EncodeToString([]byte("#!/bin/sh\n" + initCommand + "\n"))
447 var storageProfile *compute.StorageProfile
449 re := regexp.MustCompile(`^http(s?)://`)
450 if re.MatchString(string(imageID)) {
451 if az.blobcont == nil {
453 return nil, wrapAzureError(errors.New("Invalid configuration: can't configure unmanaged image URL without StorageAccount and BlobContainer"))
455 blobname = fmt.Sprintf("%s-os.vhd", name)
456 instanceVhd := fmt.Sprintf("https://%s.blob.%s/%s/%s",
457 az.azconfig.StorageAccount,
458 az.azureEnv.StorageEndpointSuffix,
459 az.azconfig.BlobContainer,
461 az.logger.Warn("using deprecated unmanaged image, see https://doc.arvados.org/ to migrate to managed disks")
462 storageProfile = &compute.StorageProfile{
463 OsDisk: &compute.OSDisk{
464 OsType: compute.Linux,
465 Name: to.StringPtr(name + "-os"),
466 CreateOption: compute.DiskCreateOptionTypesFromImage,
467 Image: &compute.VirtualHardDisk{
468 URI: to.StringPtr(string(imageID)),
470 Vhd: &compute.VirtualHardDisk{
476 id := to.StringPtr("/subscriptions/" + az.azconfig.SubscriptionID + "/resourceGroups/" + az.imageResourceGroup + "/providers/Microsoft.Compute/images/" + string(imageID))
477 if az.azconfig.SharedImageGalleryName != "" && az.azconfig.SharedImageGalleryImageVersion != "" {
478 id = to.StringPtr("/subscriptions/" + az.azconfig.SubscriptionID + "/resourceGroups/" + az.imageResourceGroup + "/providers/Microsoft.Compute/galleries/" + az.azconfig.SharedImageGalleryName + "/images/" + string(imageID) + "/versions/" + az.azconfig.SharedImageGalleryImageVersion)
479 } else if az.azconfig.SharedImageGalleryName != "" || az.azconfig.SharedImageGalleryImageVersion != "" {
481 return nil, wrapAzureError(errors.New("Invalid configuration: SharedImageGalleryName and SharedImageGalleryImageVersion must both be set or both be empty"))
483 storageProfile = &compute.StorageProfile{
484 ImageReference: &compute.ImageReference{
487 OsDisk: &compute.OSDisk{
488 OsType: compute.Linux,
489 Name: to.StringPtr(name + "-os"),
490 CreateOption: compute.DiskCreateOptionTypesFromImage,
495 vmParameters := compute.VirtualMachine{
496 Location: &az.azconfig.Location,
498 VirtualMachineProperties: &compute.VirtualMachineProperties{
499 HardwareProfile: &compute.HardwareProfile{
500 VMSize: compute.VirtualMachineSizeTypes(instanceType.ProviderType),
502 StorageProfile: storageProfile,
503 NetworkProfile: &compute.NetworkProfile{
504 NetworkInterfaces: &[]compute.NetworkInterfaceReference{
507 NetworkInterfaceReferenceProperties: &compute.NetworkInterfaceReferenceProperties{
508 Primary: to.BoolPtr(true),
513 OsProfile: &compute.OSProfile{
515 AdminUsername: to.StringPtr(az.azconfig.AdminUsername),
516 LinuxConfiguration: &compute.LinuxConfiguration{
517 DisablePasswordAuthentication: to.BoolPtr(true),
519 CustomData: &customData,
524 if publicKey != nil {
525 vmParameters.VirtualMachineProperties.OsProfile.LinuxConfiguration.SSH = &compute.SSHConfiguration{
526 PublicKeys: &[]compute.SSHPublicKey{
528 Path: to.StringPtr("/home/" + az.azconfig.AdminUsername + "/.ssh/authorized_keys"),
529 KeyData: to.StringPtr(string(ssh.MarshalAuthorizedKey(publicKey))),
535 if instanceType.Preemptible {
536 // Setting maxPrice to -1 is the equivalent of paying spot price, up to the
537 // normal price. This means the node will not be pre-empted for price
538 // reasons. It may still be pre-empted for capacity reasons though. And
539 // Azure offers *no* SLA on spot instances.
540 var maxPrice float64 = -1
541 vmParameters.VirtualMachineProperties.Priority = compute.Spot
542 vmParameters.VirtualMachineProperties.EvictionPolicy = compute.Delete
543 vmParameters.VirtualMachineProperties.BillingProfile = &compute.BillingProfile{MaxPrice: &maxPrice}
546 vm, err := az.vmClient.createOrUpdate(az.ctx, az.azconfig.ResourceGroup, name, vmParameters)
548 // Do some cleanup. Otherwise, an unbounded number of new unused nics and
549 // blobs can pile up during times when VMs can't be created and the
550 // dispatcher keeps retrying, because the garbage collection in manageBlobs
551 // and manageNics is only triggered periodically. This is most important
552 // for nics, because those are subject to a quota.
556 _, delerr := az.blobcont.GetBlobReference(blobname).DeleteIfExists(nil)
558 az.logger.WithError(delerr).Warnf("Error cleaning up vhd blob after failed create")
562 // Leave cleaning up of managed disks to the garbage collection in manageDisks()
564 return nil, wrapAzureError(err)
567 return &azureInstance{
574 func (az *azureInstanceSet) Instances(cloud.InstanceTags) ([]cloud.Instance, error) {
576 defer az.stopWg.Done()
578 interfaces, err := az.manageNics()
583 result, err := az.vmClient.listComplete(az.ctx, az.azconfig.ResourceGroup)
585 return nil, wrapAzureError(err)
588 var instances []cloud.Instance
589 for ; result.NotDone(); err = result.Next() {
591 return nil, wrapAzureError(err)
593 instances = append(instances, &azureInstance{
596 nic: interfaces[*(*result.Value().NetworkProfile.NetworkInterfaces)[0].ID],
599 return instances, nil
602 // manageNics returns a list of Azure network interface resources.
603 // Also performs garbage collection of NICs which have "namePrefix",
604 // are not associated with a virtual machine and have a "created-at"
605 // time more than DeleteDanglingResourcesAfter (to prevent racing and
606 // deleting newly created NICs) in the past are deleted.
607 func (az *azureInstanceSet) manageNics() (map[string]network.Interface, error) {
609 defer az.stopWg.Done()
611 result, err := az.netClient.listComplete(az.ctx, az.azconfig.ResourceGroup)
613 return nil, wrapAzureError(err)
616 interfaces := make(map[string]network.Interface)
618 timestamp := time.Now()
619 for ; result.NotDone(); err = result.Next() {
621 az.logger.WithError(err).Warnf("Error listing nics")
622 return interfaces, nil
624 if strings.HasPrefix(*result.Value().Name, az.namePrefix) {
625 if result.Value().VirtualMachine != nil {
626 interfaces[*result.Value().ID] = result.Value()
628 if result.Value().Tags["created-at"] != nil {
629 createdAt, err := time.Parse(time.RFC3339Nano, *result.Value().Tags["created-at"])
631 if timestamp.Sub(createdAt) > az.azconfig.DeleteDanglingResourcesAfter.Duration() {
632 az.logger.Printf("Will delete %v because it is older than %s", *result.Value().Name, az.azconfig.DeleteDanglingResourcesAfter)
633 az.deleteNIC <- *result.Value().Name
640 return interfaces, nil
643 // manageBlobs garbage collects blobs (VM disk images) in the
644 // configured storage account container. It will delete blobs which
645 // have "namePrefix", are "available" (which means they are not
646 // leased to a VM) and haven't been modified for
647 // DeleteDanglingResourcesAfter seconds.
648 func (az *azureInstanceSet) manageBlobs() {
650 page := storage.ListBlobsParameters{Prefix: az.namePrefix}
651 timestamp := time.Now()
654 response, err := az.blobcont.ListBlobs(page)
656 az.logger.WithError(err).Warn("Error listing blobs")
659 for _, b := range response.Blobs {
660 age := timestamp.Sub(time.Time(b.Properties.LastModified))
661 if b.Properties.BlobType == storage.BlobTypePage &&
662 b.Properties.LeaseState == "available" &&
663 b.Properties.LeaseStatus == "unlocked" &&
664 age.Seconds() > az.azconfig.DeleteDanglingResourcesAfter.Duration().Seconds() {
666 az.logger.Printf("Blob %v is unlocked and not modified for %v seconds, will delete", b.Name, age.Seconds())
670 if response.NextMarker != "" {
671 page.Marker = response.NextMarker
678 // manageDisks garbage collects managed compute disks (VM disk images) in the
679 // configured resource group. It will delete disks which have "namePrefix",
680 // are "unattached" (which means they are not leased to a VM) and were created
681 // more than DeleteDanglingResourcesAfter seconds ago. (Azure provides no
682 // modification timestamp on managed disks, there is only a creation timestamp)
683 func (az *azureInstanceSet) manageDisks() {
685 re := regexp.MustCompile(`^` + regexp.QuoteMeta(az.namePrefix) + `.*-os$`)
686 threshold := time.Now().Add(-az.azconfig.DeleteDanglingResourcesAfter.Duration())
688 response, err := az.disksClient.listByResourceGroup(az.ctx, az.imageResourceGroup)
690 az.logger.WithError(err).Warn("Error listing disks")
694 for ; response.NotDone(); err = response.Next() {
696 az.logger.WithError(err).Warn("Error getting next page of disks")
699 for _, d := range response.Values() {
700 if d.DiskProperties.DiskState == compute.Unattached &&
701 d.Name != nil && re.MatchString(*d.Name) &&
702 d.DiskProperties.TimeCreated.ToTime().Before(threshold) {
704 az.logger.Printf("Disk %v is unlocked and was created at %+v, will delete", *d.Name, d.DiskProperties.TimeCreated.ToTime())
711 func (az *azureInstanceSet) Stop() {
719 type azureInstance struct {
720 provider *azureInstanceSet
721 nic network.Interface
722 vm compute.VirtualMachine
725 func (ai *azureInstance) ID() cloud.InstanceID {
726 return cloud.InstanceID(*ai.vm.ID)
729 func (ai *azureInstance) String() string {
733 func (ai *azureInstance) ProviderType() string {
734 return string(ai.vm.VirtualMachineProperties.HardwareProfile.VMSize)
737 func (ai *azureInstance) SetTags(newTags cloud.InstanceTags) error {
738 ai.provider.stopWg.Add(1)
739 defer ai.provider.stopWg.Done()
741 tags := map[string]*string{}
742 for k, v := range ai.vm.Tags {
745 for k, v := range newTags {
746 tags[k] = to.StringPtr(v)
749 vmParameters := compute.VirtualMachine{
750 Location: &ai.provider.azconfig.Location,
753 vm, err := ai.provider.vmClient.createOrUpdate(ai.provider.ctx, ai.provider.azconfig.ResourceGroup, *ai.vm.Name, vmParameters)
755 return wrapAzureError(err)
762 func (ai *azureInstance) Tags() cloud.InstanceTags {
763 tags := cloud.InstanceTags{}
764 for k, v := range ai.vm.Tags {
770 func (ai *azureInstance) Destroy() error {
771 ai.provider.stopWg.Add(1)
772 defer ai.provider.stopWg.Done()
774 _, err := ai.provider.vmClient.delete(ai.provider.ctx, ai.provider.azconfig.ResourceGroup, *ai.vm.Name)
775 return wrapAzureError(err)
778 func (ai *azureInstance) Address() string {
779 if iprops := ai.nic.InterfacePropertiesFormat; iprops == nil {
781 } else if ipconfs := iprops.IPConfigurations; ipconfs == nil || len(*ipconfs) == 0 {
783 } else if ipconfprops := (*ipconfs)[0].InterfaceIPConfigurationPropertiesFormat; ipconfprops == nil {
785 } else if addr := ipconfprops.PrivateIPAddress; addr == nil {
792 func (ai *azureInstance) PriceHistory(arvados.InstanceType) []cloud.InstancePrice {
796 func (ai *azureInstance) RemoteUser() string {
797 return ai.provider.azconfig.AdminUsername
800 func (ai *azureInstance) VerifyHostKey(ssh.PublicKey, *ssh.Client) error {
801 return cloud.ErrNotImplemented