Просмотр исходного кода

Wrap Azure SDK payloads in model classes for camelCase serialization

azure-mgmt-compute 38.x (corev2) no longer auto-translates snake_case
dict keys to camelCase JSON, so passing raw dicts like
{'disk_size_gb': 10, 'creation_data': {'create_option': 'Empty'}} to
disks.begin_create_or_update reaches ARM as snake_case and is rejected:
  (InvalidRequestContent) Could not find member 'disk_size_gb' on
  object of type 'ResourceDefinition'.

Wrap every begin_create_or_update / begin_create / begin_update /
create_or_update payload (and every nested dict it contains) in the
appropriate SDK model class so the SDK serializer produces the wire
shape ARM expects.

azure_client.py: payloads wrapped in Disk, Snapshot, Image,
VirtualMachine, VirtualNetwork, Subnet, PublicIPAddress, NetworkInterface,
RouteTable, NetworkSecurityGroup, SecurityRule, ResourceGroup,
StorageAccountCreateParameters, DiskUpdate, SnapshotUpdate;
storage-account Sku wrapped; subnet.route_table reassignment uses
SubResource; update_fip_tags and create_vm_firewall_rule accept either
a model or a raw dict so refresh-and-update flows still work.

services.py: nested dicts wrapped: CreationData (volume create);
ImageReference / OSDisk / StorageProfile / DataDisk /
ManagedDiskParameters (storage profile + block-device mappings);
OSProfile / LinuxConfiguration / SshConfiguration / SshPublicKey /
HardwareProfile / NetworkProfile / NetworkInterfaceReference (VM
create); NetworkInterfaceIPConfiguration / SubResource (NIC params);
AddressSpace (network create). DataDisk has no tags attribute, so the
delete_on_terminate flag and managed_disk_id (previously threaded
through disk_def['tags']) are now carried as _cb_* attributes on the
DataDisk model and read by the VM-create tag merge and failure-cleanup
path.

resources.py: Image source_virtual_machine wrapped in
ComputeSubResource.

setup.py: add lower bounds on every azure-* requirement matching the
SDK generation these fixes are tested against. Older SDKs may still
work but are not exercised by CI.
Nuwan Goonasekera 3 дней назад
Родитель
Сommit
32be202d81

+ 72 - 42
cloudbridge/providers/azure/azure_client.py

@@ -14,10 +14,19 @@ from azure.core.exceptions import (ClientAuthenticationError,
 from azure.data.tables import TableServiceClient
 from azure.data.tables import TableServiceClient
 from azure.identity import ClientSecretCredential
 from azure.identity import ClientSecretCredential
 from azure.mgmt.compute import ComputeManagementClient
 from azure.mgmt.compute import ComputeManagementClient
+from azure.mgmt.compute.models import (CreationData, Disk, DiskUpdate, Image,
+                                       Snapshot, SnapshotUpdate,
+                                       VirtualMachine)
 from azure.mgmt.devtestlabs.models import GalleryImageReference
 from azure.mgmt.devtestlabs.models import GalleryImageReference
 from azure.mgmt.network import NetworkManagementClient
 from azure.mgmt.network import NetworkManagementClient
+from azure.mgmt.network.models import (NetworkInterface,
+                                       NetworkSecurityGroup, PublicIPAddress,
+                                       RouteTable, SecurityRule, SubResource,
+                                       Subnet, VirtualNetwork)
 from azure.mgmt.resource import ResourceManagementClient
 from azure.mgmt.resource import ResourceManagementClient
+from azure.mgmt.resource.resources.models import ResourceGroup
 from azure.mgmt.storage import StorageManagementClient
 from azure.mgmt.storage import StorageManagementClient
+from azure.mgmt.storage.models import Sku, StorageAccountCreateParameters
 from azure.mgmt.subscription import SubscriptionClient
 from azure.mgmt.subscription import SubscriptionClient
 from azure.storage.blob import (BlobSasPermissions, BlobServiceClient,
 from azure.storage.blob import (BlobSasPermissions, BlobServiceClient,
                                 generate_blob_sas)
                                 generate_blob_sas)
@@ -296,7 +305,7 @@ class AzureClient(object):
 
 
     def create_resource_group(self, name, parameters):
     def create_resource_group(self, name, parameters):
         return self.resource_client.resource_groups. \
         return self.resource_client.resource_groups. \
-            create_or_update(name, parameters)
+            create_or_update(name, ResourceGroup(**parameters))
 
 
     def get_storage_account(self, storage_account):
     def get_storage_account(self, storage_account):
         return self.storage_client.storage_accounts. \
         return self.storage_client.storage_accounts. \
@@ -304,7 +313,8 @@ class AzureClient(object):
 
 
     def create_storage_account(self, name, params):
     def create_storage_account(self, name, params):
         return self.storage_client.storage_accounts. \
         return self.storage_client.storage_accounts. \
-            begin_create(self.resource_group, name.lower(), params).result()
+            begin_create(self.resource_group, name.lower(),
+                         StorageAccountCreateParameters(**params)).result()
 
 
     # Create a storage account. To prevent a race condition, try
     # Create a storage account. To prevent a race condition, try
     # to get or create at least twice
     # to get or create at least twice
@@ -320,9 +330,7 @@ class AzureClient(object):
                     self.get_storage_account(self.storage_account)
                     self.get_storage_account(self.storage_account)
             except ResourceNotFoundError:
             except ResourceNotFoundError:
                 storage_account_params = {
                 storage_account_params = {
-                    'sku': {
-                        'name': 'Standard_LRS'
-                    },
+                    'sku': Sku(name='Standard_LRS'),
                     'kind': 'storage',
                     'kind': 'storage',
                     'location': self.region_name,
                     'location': self.region_name,
                 }
                 }
@@ -367,17 +375,19 @@ class AzureClient(object):
 
 
     def create_vm_firewall(self, name, parameters):
     def create_vm_firewall(self, name, parameters):
         return self.network_management_client.network_security_groups. \
         return self.network_management_client.network_security_groups. \
-            begin_create_or_update(self.resource_group, name,
-                                   parameters).result()
+            begin_create_or_update(
+                self.resource_group, name,
+                NetworkSecurityGroup(**parameters)).result()
 
 
     def update_vm_firewall_tags(self, fw_id, tags):
     def update_vm_firewall_tags(self, fw_id, tags):
         url_params = azure_helpers.parse_url(VM_FIREWALL_RESOURCE_ID,
         url_params = azure_helpers.parse_url(VM_FIREWALL_RESOURCE_ID,
                                              fw_id)
                                              fw_id)
         name = url_params.get(VM_FIREWALL_NAME, "")
         name = url_params.get(VM_FIREWALL_NAME, "")
         return self.network_management_client.network_security_groups. \
         return self.network_management_client.network_security_groups. \
-            begin_create_or_update(self.resource_group, name,
-                                   {'tags': tags,
-                                    'location': self.region_name}).result()
+            begin_create_or_update(
+                self.resource_group, name,
+                NetworkSecurityGroup(
+                    tags=tags, location=self.region_name)).result()
 
 
     def get_vm_firewall(self, fw_id):
     def get_vm_firewall(self, fw_id):
         url_params = azure_helpers.parse_url(VM_FIREWALL_RESOURCE_ID,
         url_params = azure_helpers.parse_url(VM_FIREWALL_RESOURCE_ID,
@@ -398,9 +408,14 @@ class AzureClient(object):
         url_params = azure_helpers.parse_url(VM_FIREWALL_RESOURCE_ID,
         url_params = azure_helpers.parse_url(VM_FIREWALL_RESOURCE_ID,
                                              fw_id)
                                              fw_id)
         vm_firewall_name = url_params.get(VM_FIREWALL_NAME, "")
         vm_firewall_name = url_params.get(VM_FIREWALL_NAME, "")
+        # parameters may be either a raw dict (from VMFirewallRuleService) or
+        # an existing SecurityRule model (when overriding default rules from
+        # the firewall.default_security_rules list).
+        rule = (parameters if isinstance(parameters, SecurityRule)
+                else SecurityRule(**parameters))
         return self.network_management_client.security_rules. \
         return self.network_management_client.security_rules. \
             begin_create_or_update(self.resource_group, vm_firewall_name,
             begin_create_or_update(self.resource_group, vm_firewall_name,
-                                   rule_name, parameters).result()
+                                   rule_name, rule).result()
 
 
     def delete_vm_firewall_rule(self, fw_rule_id, vm_firewall):
     def delete_vm_firewall_rule(self, fw_rule_id, vm_firewall):
         url_params = azure_helpers.parse_url(VM_FIREWALL_RULE_RESOURCE_ID,
         url_params = azure_helpers.parse_url(VM_FIREWALL_RULE_RESOURCE_ID,
@@ -481,14 +496,14 @@ class AzureClient(object):
         return self.compute_client.disks.begin_create_or_update(
         return self.compute_client.disks.begin_create_or_update(
             self.resource_group,
             self.resource_group,
             disk_name,
             disk_name,
-            params
+            Disk(**params)
         ).result()
         ).result()
 
 
     def create_snapshot_disk(self, disk_name, params):
     def create_snapshot_disk(self, disk_name, params):
         return self.compute_client.disks.begin_create_or_update(
         return self.compute_client.disks.begin_create_or_update(
             self.resource_group,
             self.resource_group,
             disk_name,
             disk_name,
-            params
+            Disk(**params)
         ).result()
         ).result()
 
 
     def get_disk(self, disk_id):
     def get_disk(self, disk_id):
@@ -514,7 +529,7 @@ class AzureClient(object):
         return self.compute_client.disks.begin_update(
         return self.compute_client.disks.begin_update(
             self.resource_group,
             self.resource_group,
             disk_name,
             disk_name,
-            {'tags': tags}  # type: ignore
+            DiskUpdate(tags=tags)
         ).wait()
         ).wait()
 
 
     def list_snapshots(self):
     def list_snapshots(self):
@@ -532,14 +547,14 @@ class AzureClient(object):
         snapshot = self.compute_client.snapshots.begin_create_or_update(
         snapshot = self.compute_client.snapshots.begin_create_or_update(
             self.resource_group,
             self.resource_group,
             snapshot_name,
             snapshot_name,
-            {
-                'location': volume.location,
-                'creation_data': {
-                    'create_option': 'Copy',
-                    'source_uri': volume.id
-                },
-                'tags': tags
-            }
+            Snapshot(
+                location=volume.location,
+                creation_data=CreationData(
+                    create_option='Copy',
+                    source_uri=volume.id
+                ),
+                tags=tags
+            )
         ).result()
         ).result()
 
 
         self.update_snapshot_tags(snapshot.id, tags)
         self.update_snapshot_tags(snapshot.id, tags)
@@ -559,7 +574,7 @@ class AzureClient(object):
         return self.compute_client.snapshots.begin_update(
         return self.compute_client.snapshots.begin_update(
             self.resource_group,
             self.resource_group,
             snapshot_name,
             snapshot_name,
-            {'tags': tags}  # type: ignore
+            SnapshotUpdate(tags=tags)
         ).wait()
         ).wait()
 
 
     def is_gallery_image(self, image_id):
     def is_gallery_image(self, image_id):
@@ -570,7 +585,8 @@ class AzureClient(object):
 
 
     def create_image(self, name, params):
     def create_image(self, name, params):
         return self.compute_client.images. \
         return self.compute_client.images. \
-            begin_create_or_update(self.resource_group, name, params).result()
+            begin_create_or_update(
+                self.resource_group, name, Image(**params)).result()
 
 
     def delete_image(self, image_id):
     def delete_image(self, image_id):
         url_params = azure_helpers.parse_url(IMAGE_RESOURCE_ID,
         url_params = azure_helpers.parse_url(IMAGE_RESOURCE_ID,
@@ -607,11 +623,10 @@ class AzureClient(object):
         else:
         else:
             name = url_params.get(IMAGE_NAME, "")
             name = url_params.get(IMAGE_NAME, "")
             return self.compute_client.images. \
             return self.compute_client.images. \
-                begin_create_or_update(self.resource_group, name,
-                                       {
-                                           'tags': tags,
-                                           'location': self.region_name
-                                       }).result()
+                begin_create_or_update(
+                    self.resource_group, name,
+                    Image(tags=tags,
+                          location=self.region_name)).result()
 
 
     def list_vm_types(self):
     def list_vm_types(self):
         return self.compute_client.virtual_machine_sizes. \
         return self.compute_client.virtual_machine_sizes. \
@@ -630,7 +645,9 @@ class AzureClient(object):
 
 
     def create_network(self, name, params):
     def create_network(self, name, params):
         return self.network_management_client.virtual_networks. \
         return self.network_management_client.virtual_networks. \
-            begin_create_or_update(self.networking_resource_group, name, parameters=params).result()
+            begin_create_or_update(
+                self.networking_resource_group, name,
+                parameters=VirtualNetwork(**params)).result()
 
 
     def delete_network(self, network_id):
     def delete_network(self, network_id):
         url_params = azure_helpers.parse_url(NETWORK_RESOURCE_ID, network_id)
         url_params = azure_helpers.parse_url(NETWORK_RESOURCE_ID, network_id)
@@ -673,7 +690,7 @@ class AzureClient(object):
                 self.networking_resource_group,
                 self.networking_resource_group,
                 network_name,
                 network_name,
                 subnet_name,
                 subnet_name,
-                params
+                Subnet(**params)
             )
             )
         subnet_info = result_create.result()
         subnet_info = result_create.result()
 
 
@@ -710,8 +727,10 @@ class AzureClient(object):
 
 
     def create_floating_ip(self, public_ip_name, public_ip_parameters):
     def create_floating_ip(self, public_ip_name, public_ip_parameters):
         return self.network_management_client.public_ip_addresses. \
         return self.network_management_client.public_ip_addresses. \
-            begin_create_or_update(self.networking_resource_group,
-                                   public_ip_name, public_ip_parameters).result()
+            begin_create_or_update(
+                self.networking_resource_group,
+                public_ip_name,
+                PublicIPAddress(**public_ip_parameters)).result()
 
 
     def get_floating_ip(self, public_ip_id):
     def get_floating_ip(self, public_ip_id):
         url_params = azure_helpers.parse_url(PUBLIC_IP_RESOURCE_ID,
         url_params = azure_helpers.parse_url(PUBLIC_IP_RESOURCE_ID,
@@ -732,8 +751,13 @@ class AzureClient(object):
         url_params = azure_helpers.parse_url(PUBLIC_IP_RESOURCE_ID,
         url_params = azure_helpers.parse_url(PUBLIC_IP_RESOURCE_ID,
                                              fip_id)
                                              fip_id)
         fip_name = url_params.get(PUBLIC_IP_NAME, "")
         fip_name = url_params.get(PUBLIC_IP_NAME, "")
+        # Accept either a full PublicIPAddress model (refresh-and-update flow)
+        # or a raw tags dict.
+        fip = (tags if isinstance(tags, PublicIPAddress)
+               else PublicIPAddress(tags=tags, location=self.region_name))
         self.network_management_client.public_ip_addresses. \
         self.network_management_client.public_ip_addresses. \
-            begin_create_or_update(self.networking_resource_group, fip_name, tags).result()
+            begin_create_or_update(
+                self.networking_resource_group, fip_name, fip).result()
 
 
     def list_floating_ips(self):
     def list_floating_ips(self):
         return self.network_management_client.public_ip_addresses.list(
         return self.network_management_client.public_ip_addresses.list(
@@ -777,14 +801,18 @@ class AzureClient(object):
 
 
     def create_vm(self, vm_name, params):
     def create_vm(self, vm_name, params):
         return self.compute_client.virtual_machines. \
         return self.compute_client.virtual_machines. \
-            begin_create_or_update(self.resource_group, vm_name, params).result()
+            begin_create_or_update(
+                self.resource_group, vm_name,
+                VirtualMachine(**params)).result()
 
 
     def update_vm(self, vm_id, params):
     def update_vm(self, vm_id, params):
         url_params = azure_helpers.parse_url(VM_RESOURCE_ID,
         url_params = azure_helpers.parse_url(VM_RESOURCE_ID,
                                              vm_id)
                                              vm_id)
         vm_name = url_params.get(VM_NAME, "")
         vm_name = url_params.get(VM_NAME, "")
         return self.compute_client.virtual_machines. \
         return self.compute_client.virtual_machines. \
-            begin_create_or_update(self.resource_group, vm_name, params).wait()
+            begin_create_or_update(
+                self.resource_group, vm_name,
+                VirtualMachine(**params)).wait()
 
 
     def deallocate_vm(self, vm_id):
     def deallocate_vm(self, vm_id):
         url_params = azure_helpers.parse_url(VM_RESOURCE_ID,
         url_params = azure_helpers.parse_url(VM_RESOURCE_ID,
@@ -832,11 +860,15 @@ class AzureClient(object):
         nic_params = azure_helpers.\
         nic_params = azure_helpers.\
             parse_url(NETWORK_INTERFACE_RESOURCE_ID, nic_id)
             parse_url(NETWORK_INTERFACE_RESOURCE_ID, nic_id)
         nic_name = nic_params.get(NETWORK_INTERFACE_NAME, "")
         nic_name = nic_params.get(NETWORK_INTERFACE_NAME, "")
+        # update_nic is called with the existing NIC model (from get_nic());
+        # create_nic is called with a raw dict from services.py. Accept both.
+        nic = (params if isinstance(params, NetworkInterface)
+               else NetworkInterface(**params))
         async_nic_creation = self.network_management_client. \
         async_nic_creation = self.network_management_client. \
             network_interfaces.begin_create_or_update(
             network_interfaces.begin_create_or_update(
                 self.resource_group,
                 self.resource_group,
                 nic_name,
                 nic_name,
-                params
+                nic
             )
             )
         nic_info = async_nic_creation.result()
         nic_info = async_nic_creation.result()
         return nic_info
         return nic_info
@@ -846,7 +878,7 @@ class AzureClient(object):
             network_interfaces.begin_create_or_update(
             network_interfaces.begin_create_or_update(
                 self.resource_group,
                 self.resource_group,
                 nic_name,
                 nic_name,
-                params
+                NetworkInterface(**params)
             ).result()
             ).result()
 
 
     def create_public_key(self, entity):
     def create_public_key(self, entity):
@@ -891,9 +923,7 @@ class AzureClient(object):
             subnet_name
             subnet_name
         )
         )
         if subnet_info:
         if subnet_info:
-            subnet_info.route_table = {
-                'id': route_table_id
-            }
+            subnet_info.route_table = SubResource(id=route_table_id)
 
 
             result_create = self.network_management_client. \
             result_create = self.network_management_client. \
                 subnets.begin_create_or_update(
                 subnets.begin_create_or_update(
@@ -945,7 +975,7 @@ class AzureClient(object):
         return self.network_management_client. \
         return self.network_management_client. \
             route_tables.begin_create_or_update(
             route_tables.begin_create_or_update(
              self.resource_group,
              self.resource_group,
-             route_table_name, params).result()
+             route_table_name, RouteTable(**params)).result()
 
 
     def update_route_table_tags(self, route_table_name, tags):
     def update_route_table_tags(self, route_table_name, tags):
         self.network_management_client.route_tables. \
         self.network_management_client.route_tables. \

+ 2 - 3
cloudbridge/providers/azure/resources.py

@@ -23,6 +23,7 @@ from cloudbridge.interfaces.resources import (Instance, MachineImageState,
 
 
 from azure.common import AzureException
 from azure.common import AzureException
 from azure.core.exceptions import ResourceNotFoundError
 from azure.core.exceptions import ResourceNotFoundError
+from azure.mgmt.compute.models import SubResource as ComputeSubResource
 from azure.mgmt.devtestlabs.models import GalleryImageReference
 from azure.mgmt.devtestlabs.models import GalleryImageReference
 from azure.mgmt.network.models import NetworkSecurityGroup
 from azure.mgmt.network.models import NetworkSecurityGroup
 
 
@@ -1225,9 +1226,7 @@ class AzureInstance(BaseInstance):
 
 
         create_params = {
         create_params = {
             'location': self._provider.region_name,
             'location': self._provider.region_name,
-            'source_virtual_machine': {
-                'id': self.resource_id
-            },
+            'source_virtual_machine': ComputeSubResource(id=self.resource_id),
             'tags': {'Label': label}
             'tags': {'Label': label}
         }
         }
 
 

+ 105 - 98
cloudbridge/providers/azure/services.py

@@ -27,7 +27,17 @@ from cloudbridge.interfaces.resources import (MachineImage, Network, Snapshot,
                                               VMType, Volume)
                                               VMType, Volume)
 from azure.core.exceptions import ResourceNotFoundError
 from azure.core.exceptions import ResourceNotFoundError
 
 
-from azure.mgmt.compute.models import DiskCreateOption
+from azure.mgmt.compute.models import (CreationData, DataDisk,
+                                       DiskCreateOption, HardwareProfile,
+                                       ImageReference, LinuxConfiguration,
+                                       ManagedDiskParameters,
+                                       NetworkInterfaceReference,
+                                       NetworkProfile, OSDisk, OSProfile,
+                                       SshConfiguration, SshPublicKey,
+                                       StorageProfile)
+from azure.mgmt.network.models import (AddressSpace,
+                                       NetworkInterfaceIPConfiguration,
+                                       SubResource)
 
 
 from .resources import (AzureBucket, AzureBucketObject, AzureFloatingIP,
 from .resources import (AzureBucket, AzureBucketObject, AzureFloatingIP,
                         AzureInstance, AzureInternetGateway, AzureKeyPair,
                         AzureInstance, AzureInternetGateway, AzureKeyPair,
@@ -387,10 +397,10 @@ class AzureVolumeService(BaseVolumeService):
         if snapshot:
         if snapshot:
             params = {
             params = {
                 'location': zone_name,
                 'location': zone_name,
-                'creation_data': {
-                    'create_option': DiskCreateOption.copy,
-                    'source_uri': snapshot.resource_id
-                },
+                'creation_data': CreationData(
+                    create_option=DiskCreateOption.copy,
+                    source_uri=snapshot.resource_id,
+                ),
                 'tags': tags
                 'tags': tags
             }
             }
 
 
@@ -401,9 +411,9 @@ class AzureVolumeService(BaseVolumeService):
             params = {
             params = {
                 'location': zone_name,
                 'location': zone_name,
                 'disk_size_gb': size,
                 'disk_size_gb': size,
-                'creation_data': {
-                    'create_option': DiskCreateOption.empty
-                },
+                'creation_data': CreationData(
+                    create_option=DiskCreateOption.empty,
+                ),
                 'tags': tags
                 'tags': tags
             }
             }
 
 
@@ -689,56 +699,52 @@ class AzureInstanceService(BaseInstanceService):
         if image.is_gallery_image:
         if image.is_gallery_image:
             # pylint:disable=protected-access
             # pylint:disable=protected-access
             reference = image._image.as_dict()
             reference = image._image.as_dict()
-            image_ref = {
-                'publisher': reference['publisher'],
-                'offer': reference['offer'],
-                'sku': reference['sku'],
-                'version': reference['version']
-            }
+            image_ref = ImageReference(
+                publisher=reference['publisher'],
+                offer=reference['offer'],
+                sku=reference['sku'],
+                version=reference['version'],
+            )
         else:
         else:
-            image_ref = {
-                'id': image.resource_id
-            }
+            image_ref = ImageReference(id=image.resource_id)
 
 
-        storage_profile = {
-            'image_reference': image_ref,
-            "os_disk": {
-                "name": instance_name + '_os_disk',
-                "create_option": DiskCreateOption.from_image
-            },
-        }
+        os_disk = OSDisk(
+            name=instance_name + '_os_disk',
+            create_option=DiskCreateOption.from_image,
+        )
 
 
+        data_disks = None
         if launch_config:
         if launch_config:
             data_disks, root_disk_size = self._process_block_device_mappings(
             data_disks, root_disk_size = self._process_block_device_mappings(
                 launch_config)
                 launch_config)
-            if data_disks:
-                storage_profile['data_disks'] = data_disks
             if root_disk_size:
             if root_disk_size:
-                storage_profile['os_disk']['disk_size_gb'] = root_disk_size
+                os_disk.disk_size_gb = root_disk_size
 
 
-        return storage_profile
+        return StorageProfile(
+            image_reference=image_ref,
+            os_disk=os_disk,
+            data_disks=data_disks or None,
+        )
 
 
     def _process_block_device_mappings(self, launch_config):
     def _process_block_device_mappings(self, launch_config):
         """
         """
         Processes block device mapping information
         Processes block device mapping information
-        and returns a Data disk dictionary list. If new volumes
+        and returns a DataDisk model list. If new volumes
         are requested (source is None and destination is VOLUME), they will be
         are requested (source is None and destination is VOLUME), they will be
         created and the relevant volume ids included in the mapping.
         created and the relevant volume ids included in the mapping.
         """
         """
         data_disks = []
         data_disks = []
         root_disk_size = None
         root_disk_size = None
 
 
-        def append_disk(disk_def, device_no, delete_on_terminate):
-            # In azure, there is no option to specify terminate disks
-            # (similar to AWS delete_on_terminate) on VM delete.
-            # This method uses the azure tags functionality to store
-            # the  delete_on_terminate option when the virtual machine
-            # is deleted, we parse the tags and delete accordingly
-            disk_def['lun'] = device_no
-            disk_def['tags'] = {
-                'delete_on_terminate': delete_on_terminate
-            }
-            data_disks.append(disk_def)
+        def append_disk(disk_kwargs, device_no, delete_on_terminate,
+                        managed_disk_id=None):
+            # Azure has no direct equivalent of AWS' delete_on_terminate, so
+            # the cleanup tag is recorded on the parent VM later; we just
+            # carry the flag (and the disk id) alongside the DataDisk model.
+            disk = DataDisk(lun=device_no, **disk_kwargs)
+            disk._cb_delete_on_terminate = delete_on_terminate
+            disk._cb_managed_disk_id = managed_disk_id
+            data_disks.append(disk)
 
 
         for device_no, device in enumerate(launch_config.block_devices):
         for device_no, device in enumerate(launch_config.block_devices):
             if device.is_volume:
             if device.is_volume:
@@ -749,37 +755,42 @@ class AzureInstanceService(BaseInstanceService):
                     # we are ignoring the root disk, if specified
                     # we are ignoring the root disk, if specified
                     if isinstance(device.source, Snapshot):
                     if isinstance(device.source, Snapshot):
                         snapshot_vol = device.source.create_volume()
                         snapshot_vol = device.source.create_volume()
-                        disk_def = {
+                        disk_kwargs = {
                             # pylint:disable=protected-access
                             # pylint:disable=protected-access
                             'name': snapshot_vol._volume.name,
                             'name': snapshot_vol._volume.name,
                             'create_option': DiskCreateOption.attach,
                             'create_option': DiskCreateOption.attach,
-                            'managed_disk': {
-                                'id': snapshot_vol.id
-                            }
+                            'managed_disk': ManagedDiskParameters(
+                                id=snapshot_vol.id),
                         }
                         }
+                        append_disk(disk_kwargs, device_no,
+                                    device.delete_on_terminate,
+                                    managed_disk_id=snapshot_vol.id)
+                        continue
                     elif isinstance(device.source, Volume):
                     elif isinstance(device.source, Volume):
-                        disk_def = {
+                        disk_kwargs = {
                             # pylint:disable=protected-access
                             # pylint:disable=protected-access
                             'name': device.source._volume.name,
                             'name': device.source._volume.name,
                             'create_option': DiskCreateOption.attach,
                             'create_option': DiskCreateOption.attach,
-                            'managed_disk': {
-                                'id': device.source.id
-                            }
+                            'managed_disk': ManagedDiskParameters(
+                                id=device.source.id),
                         }
                         }
+                        append_disk(disk_kwargs, device_no,
+                                    device.delete_on_terminate,
+                                    managed_disk_id=device.source.id)
+                        continue
                     elif isinstance(device.source, MachineImage):
                     elif isinstance(device.source, MachineImage):
-                        disk_def = {
+                        disk_kwargs = {
                             # pylint:disable=protected-access
                             # pylint:disable=protected-access
                             'name': device.source._volume.name,
                             'name': device.source._volume.name,
                             'create_option': DiskCreateOption.from_image,
                             'create_option': DiskCreateOption.from_image,
-                            'source_resource_id': device.source.id
+                            'source_resource_id': device.source.id,
                         }
                         }
                     else:
                     else:
-                        disk_def = {
-                            # pylint:disable=protected-access
+                        disk_kwargs = {
                             'create_option': DiskCreateOption.empty,
                             'create_option': DiskCreateOption.empty,
-                            'disk_size_gb': device.size
+                            'disk_size_gb': device.size,
                         }
                         }
-                    append_disk(disk_def, device_no,
+                    append_disk(disk_kwargs, device_no,
                                 device.delete_on_terminate)
                                 device.delete_on_terminate)
             else:  # device is ephemeral
             else:  # device is ephemeral
                 # in azure we cannot add the ephemeral disks explicitly
                 # in azure we cannot add the ephemeral disks explicitly
@@ -825,19 +836,16 @@ class AzureInstanceService(BaseInstanceService):
 
 
         nic_params = {
         nic_params = {
             'location': self.provider.region_name,
             'location': self.provider.region_name,
-            'ip_configurations': [{
-                'name': instance_name + '_ip_config',
-                'private_ip_allocation_method': 'Dynamic',
-                'subnet': {
-                    'id': subnet_id
-                }
-            }]
+            'ip_configurations': [NetworkInterfaceIPConfiguration(
+                name=instance_name + '_ip_config',
+                private_ip_allocation_method='Dynamic',
+                subnet=SubResource(id=subnet_id),
+            )],
         }
         }
 
 
         if vm_firewall_id:
         if vm_firewall_id:
-            nic_params['network_security_group'] = {
-                'id': vm_firewall_id
-            }
+            nic_params['network_security_group'] = SubResource(
+                id=vm_firewall_id)
         nic_info = self.provider.azure_client.create_nic(
         nic_info = self.provider.azure_client.create_nic(
             instance_name + '_nic',
             instance_name + '_nic',
             nic_params
             nic_params
@@ -865,41 +873,41 @@ class AzureInstanceService(BaseInstanceService):
                 name=temp_kp_name)
                 name=temp_kp_name)
             temp_key_pair = key_pair
             temp_key_pair = key_pair
 
 
+        os_profile = OSProfile(
+            admin_username=self.provider.vm_default_user_name,
+            computer_name=instance_name,
+            linux_configuration=LinuxConfiguration(
+                disable_password_authentication=True,
+                ssh=SshConfiguration(public_keys=[SshPublicKey(
+                    path="/home/{}/.ssh/authorized_keys".format(
+                        self.provider.vm_default_user_name),
+                    key_data=key_pair._key_pair['Key'],
+                )]),
+            ),
+        )
+
+        tags = {'Label': label}
+        # Surface each data disk's delete-on-terminate flag onto the parent
+        # VM tags so the VM-delete path can later clean them up.
+        for disk in (storage_profile.data_disks or []):
+            tags['delete_on_terminate'] = getattr(
+                disk, '_cb_delete_on_terminate', False)
+
         params = {
         params = {
             'location': zone_id,
             'location': zone_id,
-            'os_profile': {
-                'admin_username': self.provider.vm_default_user_name,
-                'computer_name': instance_name,
-                'linux_configuration': {
-                    "disable_password_authentication": True,
-                    "ssh": {
-                        "public_keys": [{
-                            "path":
-                                "/home/{}/.ssh/authorized_keys".format(
-                                        self.provider.vm_default_user_name),
-                                "key_data": key_pair._key_pair['Key']
-                        }]
-                    }
-                }
-            },
-            'hardware_profile': {
-                'vm_size': instance_size
-            },
-            'network_profile': {
-                'network_interfaces': [{
-                    'id': nic_info.id
-                }]
-            },
+            'os_profile': os_profile,
+            'hardware_profile': HardwareProfile(vm_size=instance_size),
+            'network_profile': NetworkProfile(
+                network_interfaces=[NetworkInterfaceReference(
+                    id=nic_info.id)],
+            ),
             'storage_profile': storage_profile,
             'storage_profile': storage_profile,
-            'tags': {'Label': label}
+            'tags': tags,
         }
         }
 
 
-        for disk_def in storage_profile.get('data_disks', []):
-            params['tags'] = dict(disk_def.get('tags', {}), **params['tags'])
-
         if user_data:
         if user_data:
             custom_data = base64.b64encode(bytes(ud, 'utf-8'))
             custom_data = base64.b64encode(bytes(ud, 'utf-8'))
-            params['os_profile']['custom_data'] = str(custom_data, 'utf-8')
+            params['os_profile'].custom_data = str(custom_data, 'utf-8')
 
 
         if not temp_key_pair:
         if not temp_key_pair:
             params['tags'].update(Key_Pair=key_pair.id)
             params['tags'].update(Key_Pair=key_pair.id)
@@ -909,9 +917,9 @@ class AzureInstanceService(BaseInstanceService):
         except Exception as e:
         except Exception as e:
             # If VM creation fails, attempt to clean up intermediary resources
             # If VM creation fails, attempt to clean up intermediary resources
             self.provider.azure_client.delete_nic(nic_info.id)
             self.provider.azure_client.delete_nic(nic_info.id)
-            for disk_def in storage_profile.get('data_disks', []):
-                if disk_def.get('tags', {}).get('delete_on_terminate'):
-                    disk_id = disk_def.get('managed_disk', {}).get('id')
+            for disk in (storage_profile.data_disks or []):
+                if getattr(disk, '_cb_delete_on_terminate', False):
+                    disk_id = getattr(disk, '_cb_managed_disk_id', None)
                     if disk_id:
                     if disk_id:
                         vol = self.provider.storage.volumes.get(disk_id)
                         vol = self.provider.storage.volumes.get(disk_id)
                         vol.delete()
                         vol.delete()
@@ -1108,9 +1116,8 @@ class AzureNetworkService(BaseNetworkService):
         AzureNetwork.assert_valid_resource_label(label)
         AzureNetwork.assert_valid_resource_label(label)
         params = {
         params = {
             'location': self.provider.azure_client.region_name,
             'location': self.provider.azure_client.region_name,
-            'address_space': {
-                'address_prefixes': [cidr_block]
-            },
+            'address_space': AddressSpace(
+                address_prefixes=[cidr_block]),
             'tags': {'Label': label}
             'tags': {'Label': label}
         }
         }
 
 

+ 14 - 11
setup.py

@@ -31,17 +31,20 @@ REQS_AWS = [
 # below are compatible with each other. List individual libraries instead
 # below are compatible with each other. List individual libraries instead
 # of using the azure umbrella package to speed up installation.
 # of using the azure umbrella package to speed up installation.
 REQS_AZURE = [
 REQS_AZURE = [
-    'azure-identity<2.0.0',
-    'azure-common<2.0.0',
-    'azure-core<2.0.0',
-    'azure-mgmt-devtestlabs<10.0.0',
-    'azure-mgmt-resource<26.0.0',
-    'azure-mgmt-subscription<4.0.0',
-    'azure-mgmt-compute>=27.2.0,<39.0.0',
-    'azure-mgmt-network<31.0.0',
-    'azure-mgmt-storage<25.0.0',
-    'azure-storage-blob<13.0.0',
-    'azure-data-tables<13.0.0',
+    # Minimums match SDK generation tested against the model-class
+    # serialization fixes in cloudbridge/providers/azure/. Older SDKs may
+    # work but are not covered by integration tests.
+    'azure-identity>=1.20.0,<2.0.0',
+    'azure-common>=1.1.28,<2.0.0',
+    'azure-core>=1.30.0,<2.0.0',
+    'azure-mgmt-devtestlabs>=9.0.0,<10.0.0',
+    'azure-mgmt-resource>=23.0.0,<26.0.0',
+    'azure-mgmt-subscription>=3.0.0,<4.0.0',
+    'azure-mgmt-compute>=34.0.0,<39.0.0',
+    'azure-mgmt-network>=28.0.0,<31.0.0',
+    'azure-mgmt-storage>=22.0.0,<25.0.0',
+    'azure-storage-blob>=12.20.0,<13.0.0',
+    'azure-data-tables>=12.4.0,<13.0.0',
     'paramiko<6.0.0'
     'paramiko<6.0.0'
 ]
 ]
 REQS_GCP = [
 REQS_GCP = [