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

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 1 день назад
Родитель
С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.identity import ClientSecretCredential
 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.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.resources.models import ResourceGroup
 from azure.mgmt.storage import StorageManagementClient
+from azure.mgmt.storage.models import Sku, StorageAccountCreateParameters
 from azure.mgmt.subscription import SubscriptionClient
 from azure.storage.blob import (BlobSasPermissions, BlobServiceClient,
                                 generate_blob_sas)
@@ -296,7 +305,7 @@ class AzureClient(object):
 
     def create_resource_group(self, name, parameters):
         return self.resource_client.resource_groups. \
-            create_or_update(name, parameters)
+            create_or_update(name, ResourceGroup(**parameters))
 
     def get_storage_account(self, storage_account):
         return self.storage_client.storage_accounts. \
@@ -304,7 +313,8 @@ class AzureClient(object):
 
     def create_storage_account(self, name, params):
         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
     # to get or create at least twice
@@ -320,9 +330,7 @@ class AzureClient(object):
                     self.get_storage_account(self.storage_account)
             except ResourceNotFoundError:
                 storage_account_params = {
-                    'sku': {
-                        'name': 'Standard_LRS'
-                    },
+                    'sku': Sku(name='Standard_LRS'),
                     'kind': 'storage',
                     'location': self.region_name,
                 }
@@ -367,17 +375,19 @@ class AzureClient(object):
 
     def create_vm_firewall(self, name, parameters):
         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):
         url_params = azure_helpers.parse_url(VM_FIREWALL_RESOURCE_ID,
                                              fw_id)
         name = url_params.get(VM_FIREWALL_NAME, "")
         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):
         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,
                                              fw_id)
         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. \
             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):
         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(
             self.resource_group,
             disk_name,
-            params
+            Disk(**params)
         ).result()
 
     def create_snapshot_disk(self, disk_name, params):
         return self.compute_client.disks.begin_create_or_update(
             self.resource_group,
             disk_name,
-            params
+            Disk(**params)
         ).result()
 
     def get_disk(self, disk_id):
@@ -514,7 +529,7 @@ class AzureClient(object):
         return self.compute_client.disks.begin_update(
             self.resource_group,
             disk_name,
-            {'tags': tags}  # type: ignore
+            DiskUpdate(tags=tags)
         ).wait()
 
     def list_snapshots(self):
@@ -532,14 +547,14 @@ class AzureClient(object):
         snapshot = self.compute_client.snapshots.begin_create_or_update(
             self.resource_group,
             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()
 
         self.update_snapshot_tags(snapshot.id, tags)
@@ -559,7 +574,7 @@ class AzureClient(object):
         return self.compute_client.snapshots.begin_update(
             self.resource_group,
             snapshot_name,
-            {'tags': tags}  # type: ignore
+            SnapshotUpdate(tags=tags)
         ).wait()
 
     def is_gallery_image(self, image_id):
@@ -570,7 +585,8 @@ class AzureClient(object):
 
     def create_image(self, name, params):
         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):
         url_params = azure_helpers.parse_url(IMAGE_RESOURCE_ID,
@@ -607,11 +623,10 @@ class AzureClient(object):
         else:
             name = url_params.get(IMAGE_NAME, "")
             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):
         return self.compute_client.virtual_machine_sizes. \
@@ -630,7 +645,9 @@ class AzureClient(object):
 
     def create_network(self, name, params):
         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):
         url_params = azure_helpers.parse_url(NETWORK_RESOURCE_ID, network_id)
@@ -673,7 +690,7 @@ class AzureClient(object):
                 self.networking_resource_group,
                 network_name,
                 subnet_name,
-                params
+                Subnet(**params)
             )
         subnet_info = result_create.result()
 
@@ -710,8 +727,10 @@ class AzureClient(object):
 
     def create_floating_ip(self, public_ip_name, public_ip_parameters):
         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):
         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,
                                              fip_id)
         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. \
-            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):
         return self.network_management_client.public_ip_addresses.list(
@@ -777,14 +801,18 @@ class AzureClient(object):
 
     def create_vm(self, vm_name, params):
         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):
         url_params = azure_helpers.parse_url(VM_RESOURCE_ID,
                                              vm_id)
         vm_name = url_params.get(VM_NAME, "")
         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):
         url_params = azure_helpers.parse_url(VM_RESOURCE_ID,
@@ -832,11 +860,15 @@ class AzureClient(object):
         nic_params = azure_helpers.\
             parse_url(NETWORK_INTERFACE_RESOURCE_ID, nic_id)
         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. \
             network_interfaces.begin_create_or_update(
                 self.resource_group,
                 nic_name,
-                params
+                nic
             )
         nic_info = async_nic_creation.result()
         return nic_info
@@ -846,7 +878,7 @@ class AzureClient(object):
             network_interfaces.begin_create_or_update(
                 self.resource_group,
                 nic_name,
-                params
+                NetworkInterface(**params)
             ).result()
 
     def create_public_key(self, entity):
@@ -891,9 +923,7 @@ class AzureClient(object):
             subnet_name
         )
         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. \
                 subnets.begin_create_or_update(
@@ -945,7 +975,7 @@ class AzureClient(object):
         return self.network_management_client. \
             route_tables.begin_create_or_update(
              self.resource_group,
-             route_table_name, params).result()
+             route_table_name, RouteTable(**params)).result()
 
     def update_route_table_tags(self, route_table_name, tags):
         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.core.exceptions import ResourceNotFoundError
+from azure.mgmt.compute.models import SubResource as ComputeSubResource
 from azure.mgmt.devtestlabs.models import GalleryImageReference
 from azure.mgmt.network.models import NetworkSecurityGroup
 
@@ -1225,9 +1226,7 @@ class AzureInstance(BaseInstance):
 
         create_params = {
             'location': self._provider.region_name,
-            'source_virtual_machine': {
-                'id': self.resource_id
-            },
+            'source_virtual_machine': ComputeSubResource(id=self.resource_id),
             'tags': {'Label': label}
         }
 

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

@@ -27,7 +27,17 @@ from cloudbridge.interfaces.resources import (MachineImage, Network, Snapshot,
                                               VMType, Volume)
 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,
                         AzureInstance, AzureInternetGateway, AzureKeyPair,
@@ -387,10 +397,10 @@ class AzureVolumeService(BaseVolumeService):
         if snapshot:
             params = {
                 '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
             }
 
@@ -401,9 +411,9 @@ class AzureVolumeService(BaseVolumeService):
             params = {
                 'location': zone_name,
                 'disk_size_gb': size,
-                'creation_data': {
-                    'create_option': DiskCreateOption.empty
-                },
+                'creation_data': CreationData(
+                    create_option=DiskCreateOption.empty,
+                ),
                 'tags': tags
             }
 
@@ -689,56 +699,52 @@ class AzureInstanceService(BaseInstanceService):
         if image.is_gallery_image:
             # pylint:disable=protected-access
             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:
-            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:
             data_disks, root_disk_size = self._process_block_device_mappings(
                 launch_config)
-            if data_disks:
-                storage_profile['data_disks'] = data_disks
             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):
         """
         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
         created and the relevant volume ids included in the mapping.
         """
         data_disks = []
         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):
             if device.is_volume:
@@ -749,37 +755,42 @@ class AzureInstanceService(BaseInstanceService):
                     # we are ignoring the root disk, if specified
                     if isinstance(device.source, Snapshot):
                         snapshot_vol = device.source.create_volume()
-                        disk_def = {
+                        disk_kwargs = {
                             # pylint:disable=protected-access
                             'name': snapshot_vol._volume.name,
                             '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):
-                        disk_def = {
+                        disk_kwargs = {
                             # pylint:disable=protected-access
                             'name': device.source._volume.name,
                             '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):
-                        disk_def = {
+                        disk_kwargs = {
                             # pylint:disable=protected-access
                             'name': device.source._volume.name,
                             'create_option': DiskCreateOption.from_image,
-                            'source_resource_id': device.source.id
+                            'source_resource_id': device.source.id,
                         }
                     else:
-                        disk_def = {
-                            # pylint:disable=protected-access
+                        disk_kwargs = {
                             '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)
             else:  # device is ephemeral
                 # in azure we cannot add the ephemeral disks explicitly
@@ -825,19 +836,16 @@ class AzureInstanceService(BaseInstanceService):
 
         nic_params = {
             '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:
-            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(
             instance_name + '_nic',
             nic_params
@@ -865,41 +873,41 @@ class AzureInstanceService(BaseInstanceService):
                 name=temp_kp_name)
             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 = {
             '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,
-            '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:
             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:
             params['tags'].update(Key_Pair=key_pair.id)
@@ -909,9 +917,9 @@ class AzureInstanceService(BaseInstanceService):
         except Exception as e:
             # If VM creation fails, attempt to clean up intermediary resources
             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:
                         vol = self.provider.storage.volumes.get(disk_id)
                         vol.delete()
@@ -1108,9 +1116,8 @@ class AzureNetworkService(BaseNetworkService):
         AzureNetwork.assert_valid_resource_label(label)
         params = {
             'location': self.provider.azure_client.region_name,
-            'address_space': {
-                'address_prefixes': [cidr_block]
-            },
+            'address_space': AddressSpace(
+                address_prefixes=[cidr_block]),
             'tags': {'Label': label}
         }
 

+ 14 - 11
setup.py

@@ -31,17 +31,20 @@ REQS_AWS = [
 # below are compatible with each other. List individual libraries instead
 # of using the azure umbrella package to speed up installation.
 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'
 ]
 REQS_GCP = [