From 6f74be7a83cd1f1e8af051ff2511207b50365bf4 Mon Sep 17 00:00:00 2001 From: Tamir Kamara Date: Thu, 26 Oct 2017 22:13:39 +0100 Subject: [PATCH 1/5] image-copy: first commit --- src/image-copy/azext_imagecopy/__init__.py | 26 +++ .../azext_imagecopy/azext_metadata.json | 3 + src/image-copy/azext_imagecopy/cli_utils.py | 44 +++++ .../azext_imagecopy/create_target.py | 170 ++++++++++++++++++ src/image-copy/azext_imagecopy/custom.py | 135 ++++++++++++++ src/image-copy/setup.cfg | 2 + src/image-copy/setup.py | 42 +++++ 7 files changed, 422 insertions(+) create mode 100644 src/image-copy/azext_imagecopy/__init__.py create mode 100644 src/image-copy/azext_imagecopy/azext_metadata.json create mode 100644 src/image-copy/azext_imagecopy/cli_utils.py create mode 100644 src/image-copy/azext_imagecopy/create_target.py create mode 100644 src/image-copy/azext_imagecopy/custom.py create mode 100644 src/image-copy/setup.cfg create mode 100644 src/image-copy/setup.py diff --git a/src/image-copy/azext_imagecopy/__init__.py b/src/image-copy/azext_imagecopy/__init__.py new file mode 100644 index 00000000000..614db8f6dba --- /dev/null +++ b/src/image-copy/azext_imagecopy/__init__.py @@ -0,0 +1,26 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from azure.cli.core.help_files import helps +from azure.cli.core.sdk.util import ParametersContext + +helps['image copy'] = """ + type: command + short-summary: Allows to copy an image (or vm) to other regions. Keep in mind that it requires the source disk to be available. +""" + +def load_params(_): + with ParametersContext('image copy') as c: + c.register('source_resource_group_name', '--source-resource-group-name', help='Name of the resource gorup of the source resource') + c.register('source_object_name', '--source-object-name', help='The name of the image or vm resource') + c.register('target_location', '--target-location', help='Comma seperated location list to create the image in') + c.register('source_type', '--source-type', help='image (default) or vm') + c.register('target_resource_group_name', '--target-resource-group-name', help='Name of the resource group to create images in') + c.register('parallel_degree', '--parallel-degree', help='Number of parallel copy operations') + c.register('cleanup', '--cleanup', help='Will delete temporary resources created in process (false by default)') + +def load_commands(): + from azure.cli.core.commands import cli_command + cli_command(__name__, 'image copy', 'azext_imagecopy.custom#imagecopy') diff --git a/src/image-copy/azext_imagecopy/azext_metadata.json b/src/image-copy/azext_imagecopy/azext_metadata.json new file mode 100644 index 00000000000..b6c27f96fe3 --- /dev/null +++ b/src/image-copy/azext_imagecopy/azext_metadata.json @@ -0,0 +1,3 @@ +{ + "azext.minCliVersion": "2.0.12" +} \ No newline at end of file diff --git a/src/image-copy/azext_imagecopy/cli_utils.py b/src/image-copy/azext_imagecopy/cli_utils.py new file mode 100644 index 00000000000..a3388f9c226 --- /dev/null +++ b/src/image-copy/azext_imagecopy/cli_utils.py @@ -0,0 +1,44 @@ +import sys +import json + +from subprocess import check_output, STDOUT, CalledProcessError + +import azure.cli.core.azlogging as azlogging + +logger = azlogging.get_az_logger(__name__) + + +def run_cli_command(cmd, return_as_json=False): + try: + cmd_output = check_output(cmd, stderr=STDOUT, universal_newlines=True) + logger.debug(cmd_output) + + if return_as_json is True: + if cmd_output: + json_output = json.loads(cmd_output) + return json_output + else: + raise Exception("Command returned an unexpected empty string.") + else: + return cmd_output + except CalledProcessError as ex: + print('command failed: ', cmd) + print('output: ', ex.output) + raise ex + except: + print('command: ', cmd) + raise + +def prepare_cli_command(cmd, output_as_json=True): + full_cmd = [sys.executable, '-m', 'azure.cli'] + cmd + + if output_as_json: + full_cmd += ['--output', 'json'] + else: + full_cmd += ['--output', 'tsv'] + + # tag newly created resources + if 'create' in cmd and ('container' not in cmd): + full_cmd += ['--tags', 'created_by=image-copy-extension'] + + return full_cmd diff --git a/src/image-copy/azext_imagecopy/create_target.py b/src/image-copy/azext_imagecopy/create_target.py new file mode 100644 index 00000000000..f3c28a34226 --- /dev/null +++ b/src/image-copy/azext_imagecopy/create_target.py @@ -0,0 +1,170 @@ +import hashlib +import datetime +import time +from azext_imagecopy.cli_utils import run_cli_command, prepare_cli_command +from azure.cli.core.application import APPLICATION +import azure.cli.core.azlogging as azlogging + +logger = azlogging.get_az_logger(__name__) + + +def create_target_image(location, transient_resource_group_name, source_type, source_object_name, \ + source_os_disk_snapshot_name, source_os_disk_snapshot_url, source_os_type, \ + target_resource_group_name, azure_pool_frequency): + + #from azure.cli.core.commands.client_factory import get_subscription_id + subscription_id = get_subscription_id() + + subscription_hash = hashlib.sha1(subscription_id.encode("UTF-8")).hexdigest() + unique_subscription_string = subscription_hash[:7] + + + # create the target storage account + logger.warn("{0} - Creating target storage account (can be slow sometimes)".format(location)) + target_storage_account_name = location + unique_subscription_string + cmd = prepare_cli_command(['storage', 'account', 'create', \ + '--name', target_storage_account_name, \ + '--resource-group', transient_resource_group_name, \ + '--location', location, \ + '--sku', 'Standard_LRS']) + + json_output = run_cli_command(cmd, True) + target_blob_endpoint = json_output['primaryEndpoints']['blob'] + + + # Setup the target storage account + cmd = prepare_cli_command(['storage', 'account', 'keys', 'list', \ + '--account-name', target_storage_account_name, \ + '--resource-group', transient_resource_group_name]) + + json_output = run_cli_command(cmd, True) + + target_storage_account_key = json_output[0]['value'] + logger.debug(target_storage_account_key) + + expiry_format = "%Y-%m-%dT%H:%MZ" + expiry = datetime.datetime.utcnow() + datetime.timedelta(hours=1) + + cmd = prepare_cli_command(['storage', 'account', 'generate-sas', \ + '--account-name', target_storage_account_name, \ + '--account-key', target_storage_account_key, \ + '--expiry', expiry.strftime(expiry_format), \ + '--permissions', 'aclrpuw', '--resource-types', \ + 'sco', '--services', 'b', '--https-only'], \ + False) + + sas_token = run_cli_command(cmd) + sas_token = sas_token.rstrip("\n\r") #STRANGE + logger.debug("sas token: " + sas_token) + + + # create a container in the target blob storage account + logger.warn("{0} - Creating container in the target storage account".format(location)) + target_container_name = 'snapshots' + cmd = prepare_cli_command(['storage', 'container', 'create', \ + '--name', target_container_name, \ + '--account-name', target_storage_account_name]) + + run_cli_command(cmd) + + + # Copy the snapshot to the target region using the SAS URL + blob_name = source_os_disk_snapshot_name + '.vhd' + logger.warn("{0} - Copying blob to target storage account".format(location)) + cmd = prepare_cli_command(['storage', 'blob', 'copy', 'start', \ + '--source-uri', source_os_disk_snapshot_url, \ + '--destination-blob', blob_name, \ + '--destination-container', target_container_name, \ + '--account-name', target_storage_account_name, \ + '--sas-token', sas_token]) + + run_cli_command(cmd) + + + # Wait for the copy to complete + start_datetime = datetime.datetime.now() + wait_for_blob_copy_operation(blob_name, target_container_name, \ + target_storage_account_name, azure_pool_frequency, location) + msg = "{0} - Copy time: {1}".format(location, datetime.datetime.now()-start_datetime).ljust(40) + logger.warn(msg) + + + # Create the snapshot in the target region from the copied blob + logger.warn("{0} - Creating snapshot in target region from the copied blob".format(location)) + target_blob_path = target_blob_endpoint + target_container_name + '/' + blob_name + target_snapshot_name = source_os_disk_snapshot_name + '-' + location + cmd = prepare_cli_command(['snapshot', 'create', \ + '--resource-group', transient_resource_group_name, \ + '--name', target_snapshot_name, \ + '--location', location, \ + '--source', target_blob_path]) + + json_output = run_cli_command(cmd, True) + target_snapshot_id = json_output['id'] + + # Create the final image + logger.warn("{0} - Creating final image".format(location)) + target_image_name = source_object_name + if source_type != 'image': + target_image_name += '-image' + target_image_name += '-' + location + + cmd = prepare_cli_command(['image', 'create', \ + '--resource-group', target_resource_group_name, \ + '--name', target_image_name, \ + '--location', location, \ + '--source', target_blob_path, \ + '--os-type', source_os_type, \ + '--source', target_snapshot_id]) + + run_cli_command(cmd) + + +def wait_for_blob_copy_operation(blob_name, target_container_name, target_storage_account_name, azure_pool_frequency, location): + progress_controller = APPLICATION.get_progress_controller() + copy_status = "pending" + prev_progress = -1 + while copy_status == "pending": + cmd = prepare_cli_command(['storage', 'blob', 'show', \ + '--name', blob_name, \ + '--container-name', target_container_name, \ + '--account-name', target_storage_account_name]) + + json_output = run_cli_command(cmd, True) + copy_status = json_output["properties"]["copy"]["status"] + copy_progress_1, copy_progress_2 = json_output["properties"]["copy"]["progress"].split("/") + current_progress = round(int(copy_progress_1)/int(copy_progress_2), 1) + + if current_progress != prev_progress: + msg = "{0} - copy progress: {1}%".format(location, str(current_progress)).ljust(40) #need to justify since message overide each other + progress_controller.add(message=msg) + + prev_progress = current_progress + + try: + time.sleep(azure_pool_frequency) + except KeyboardInterrupt: + print('xxx - child') + progress_controller.stop() + return + + + if copy_status == 'success': + progress_controller.stop() + else: + logger.error("The copy operation didn't succeed. Last status: " + copy_status) + raise Exception('Blob copy failed') + + +def get_subscription_id(): + logger.warn("Get subscription id") + + cmd = prepare_cli_command(['account', 'show']) + json_output = run_cli_command(cmd, True) + subscription_id = json_output['id'] + + # from azure.cli.core._profile import Profile + # profile = Profile() + # _, subscription_id, _ = profile.get_login_credentials() + + return subscription_id diff --git a/src/image-copy/azext_imagecopy/custom.py b/src/image-copy/azext_imagecopy/custom.py new file mode 100644 index 00000000000..cad6f8e5c9b --- /dev/null +++ b/src/image-copy/azext_imagecopy/custom.py @@ -0,0 +1,135 @@ +from multiprocessing import Pool + +from azext_imagecopy.cli_utils import run_cli_command, prepare_cli_command +from azext_imagecopy.create_target import create_target_image +import azure.cli.core.azlogging as azlogging + +logger = azlogging.get_az_logger(__name__) + + +def imagecopy(source_resource_group_name, source_object_name, target_location, \ + target_resource_group_name, source_type='image', cleanup='false', parallel_degree=-1): + + # get the os disk id from source vm/image + logger.warn("Getting os disk id of the source vm/image") + cmd = prepare_cli_command([source_type, 'show', \ + '--name', source_object_name, \ + '--resource-group', source_resource_group_name]) + + json_cmd_output = run_cli_command(cmd, True) + + source_os_disk_id = json_cmd_output['storageProfile']['osDisk']['managedDisk']['id'] + source_os_type = json_cmd_output['storageProfile']['osDisk']['osType'] + logger.debug("source_os_disk_id: " + source_os_disk_id + " source_os_type: " + source_os_type) + + + # create source snapshots + logger.warn("Creating source snapshot") + source_os_disk_snapshot_name = source_object_name + '_os_disk_snapshot' + cmd = prepare_cli_command(['snapshot', 'create', \ + '--name', source_os_disk_snapshot_name, \ + '--resource-group', source_resource_group_name, \ + '--source', source_os_disk_id]) + + run_cli_command(cmd) + + + # Get SAS URL for the snapshotName + logger.warn("Getting sas url for the source snapshot") + cmd = prepare_cli_command(['snapshot', 'grant-access', \ + '--name', source_os_disk_snapshot_name, \ + '--resource-group', source_resource_group_name, \ + '--duration-in-seconds', '3600']) + + json_output = run_cli_command(cmd, True) + + source_os_disk_snapshot_url = json_output['accessSas'] + logger.debug(source_os_disk_snapshot_url) + + + # Start processing in the target locations + + transient_resource_group_name = 'image-copy-rg' + create_resource_group(transient_resource_group_name, 'eastus') + + target_locations_list = target_location.split(',') + target_locations_count = len(target_locations_list) + logger.warn("Target location count: {0}".format(target_locations_count)) + + create_resource_group(target_resource_group_name, target_locations_list[0].strip()) + + if parallel_degree == -1: + pool = Pool(target_locations_count) + elif parallel_degree == 1: + pool = Pool(1) + else: + pool = Pool(min(parallel_degree, target_locations_count)) + + # try to get a handle on arm 409s + azure_pool_frequency = 5 + if target_locations_count >= 5: + azure_pool_frequency = 15 + elif target_locations_count >= 3: + azure_pool_frequency = 10 + + tasks = [] + for location in target_location.split(','): + location = location.strip() + tasks.append((location, transient_resource_group_name, source_type, \ + source_object_name, source_os_disk_snapshot_name, source_os_disk_snapshot_url, \ + source_os_type, target_resource_group_name, azure_pool_frequency)) + + logger.warn("Starting async process for all locations") + + for task in tasks: + pool.apply_async(create_target_image, task) + + try: + pool.close() + pool.join() + except KeyboardInterrupt: + print('xxx - parent') + logger.warn('User cancelled the operation') + if 'true' in cleanup: + logger.warn('To cleanup temporary resources look for ones tagged with "image-copy-extension"') + pool.terminate() + return + + + # Cleanup + if 'true' in cleanup: + logger.warn('Deleting transient resources') + + # Delete resource group + cmd = prepare_cli_command(['group', 'delete', '--no-wait', '--yes', \ + '--name', transient_resource_group_name]) + run_cli_command(cmd) + + # Revoke sas for source snapshot + cmd = prepare_cli_command(['snapshot', 'revoke-access', \ + '--name', source_os_disk_snapshot_name, \ + '--resource-group', source_resource_group_name]) + run_cli_command(cmd) + + # Delete source snapshot + cmd = prepare_cli_command(['snapshot', 'delete', \ + '--name', source_os_disk_snapshot_name, \ + '--resource-group', source_resource_group_name]) + run_cli_command(cmd) + + +def create_resource_group(resource_group_name, location): + # check if target resource group exists + cmd = prepare_cli_command(['group', 'exists', \ + '--name', resource_group_name], False) + + cmd_output = run_cli_command(cmd) + + if 'false' in cmd_output: + # create the target resource group + logger.warn("Creating resource group") + cmd = prepare_cli_command(['group', 'create', \ + '--name', resource_group_name, \ + '--location', location]) + + run_cli_command(cmd) diff --git a/src/image-copy/setup.cfg b/src/image-copy/setup.cfg new file mode 100644 index 00000000000..3c6e79cf31d --- /dev/null +++ b/src/image-copy/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal=1 diff --git a/src/image-copy/setup.py b/src/image-copy/setup.py new file mode 100644 index 00000000000..058eb4c8bfd --- /dev/null +++ b/src/image-copy/setup.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python + +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from codecs import open +from setuptools import setup, find_packages + +VERSION = "0.0.2" + +CLASSIFIERS = [ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'Intended Audience :: System Administrators', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'License :: OSI Approved :: MIT License', +] + +DEPENDENCIES = [] + +setup( + name='imagecopyextension', + version=VERSION, + description='An Azure CLI Extension that copies images from region to region.', + long_description='An Azure CLI Extension that copies images from region to region.', + license='MIT', + author='Tamir Kamara', + author_email='tamir.kamara@microsoft.com', + url='https://github.com/ORG/REPO', + classifiers=CLASSIFIERS, + packages=find_packages(), + package_data={'azext_hello': ['azext_metadata.json']}, + install_requires=DEPENDENCIES +) From 298aae43a1a8c20ee940c68fb520b3a5a3bb9889 Mon Sep 17 00:00:00 2001 From: Tamir Kamara Date: Mon, 30 Oct 2017 21:10:10 +0200 Subject: [PATCH 2/5] fixes after @derekbekoe reviewed --- src/image-copy/azext_imagecopy/__init__.py | 15 +++++---- src/image-copy/azext_imagecopy/cli_utils.py | 12 +++---- .../azext_imagecopy/create_target.py | 27 ++++++++-------- src/image-copy/azext_imagecopy/custom.py | 32 ++++++++----------- src/image-copy/setup.py | 6 ++-- 5 files changed, 45 insertions(+), 47 deletions(-) diff --git a/src/image-copy/azext_imagecopy/__init__.py b/src/image-copy/azext_imagecopy/__init__.py index 614db8f6dba..b01594a7080 100644 --- a/src/image-copy/azext_imagecopy/__init__.py +++ b/src/image-copy/azext_imagecopy/__init__.py @@ -8,18 +8,19 @@ helps['image copy'] = """ type: command - short-summary: Allows to copy an image (or vm) to other regions. Keep in mind that it requires the source disk to be available. + short-summary: Allows to copy a managed image (or vm) to other regions. Keep in mind that it requires the source disk to be available. """ def load_params(_): with ParametersContext('image copy') as c: - c.register('source_resource_group_name', '--source-resource-group-name', help='Name of the resource gorup of the source resource') + c.register('source_resource_group_name', '--source-resource-group', help='Name of the resource group of the source resource') c.register('source_object_name', '--source-object-name', help='The name of the image or vm resource') - c.register('target_location', '--target-location', help='Comma seperated location list to create the image in') - c.register('source_type', '--source-type', help='image (default) or vm') - c.register('target_resource_group_name', '--target-resource-group-name', help='Name of the resource group to create images in') - c.register('parallel_degree', '--parallel-degree', help='Number of parallel copy operations') - c.register('cleanup', '--cleanup', help='Will delete temporary resources created in process (false by default)') + c.register('target_location', '--target-location', nargs='+', help='Space separated location list to create the image in (use location short codes like westeurope etc.)') + c.register('source_type', '--source-type', default='image', help='image or vm') + c.register('target_resource_group_name', '--target-resource-group', help='Name of the resource group to create images in') + c.register('parallel_degree', '--parallel-degree', type=int, default=-1, help='Number of parallel copy operations') + c.register('cleanup', '--cleanup', action='store_true', default=False, \ + help='Include this switch to delete temporary resources upon completion') def load_commands(): from azure.cli.core.commands import cli_command diff --git a/src/image-copy/azext_imagecopy/cli_utils.py b/src/image-copy/azext_imagecopy/cli_utils.py index a3388f9c226..9247c170edd 100644 --- a/src/image-copy/azext_imagecopy/cli_utils.py +++ b/src/image-copy/azext_imagecopy/cli_utils.py @@ -2,7 +2,7 @@ import json from subprocess import check_output, STDOUT, CalledProcessError - +from azure.cli.core.util import CLIError import azure.cli.core.azlogging as azlogging logger = azlogging.get_az_logger(__name__) @@ -11,22 +11,22 @@ def run_cli_command(cmd, return_as_json=False): try: cmd_output = check_output(cmd, stderr=STDOUT, universal_newlines=True) - logger.debug(cmd_output) + logger.debug('command: %s ended with output: %s', cmd, cmd_output) if return_as_json is True: if cmd_output: json_output = json.loads(cmd_output) return json_output else: - raise Exception("Command returned an unexpected empty string.") + raise CLIError("Command returned an unexpected empty string.") else: return cmd_output except CalledProcessError as ex: - print('command failed: ', cmd) - print('output: ', ex.output) + logger.error('command failed: %s', cmd) + logger.error('output: %s', ex.output) raise ex except: - print('command: ', cmd) + logger.error('command ended with an error: %s', cmd) raise def prepare_cli_command(cmd, output_as_json=True): diff --git a/src/image-copy/azext_imagecopy/create_target.py b/src/image-copy/azext_imagecopy/create_target.py index f3c28a34226..c65ebb0dcc8 100644 --- a/src/image-copy/azext_imagecopy/create_target.py +++ b/src/image-copy/azext_imagecopy/create_target.py @@ -3,10 +3,12 @@ import time from azext_imagecopy.cli_utils import run_cli_command, prepare_cli_command from azure.cli.core.application import APPLICATION +from azure.cli.core.util import CLIError import azure.cli.core.azlogging as azlogging logger = azlogging.get_az_logger(__name__) +PROGRESS_LINE_LENGTH = 40 def create_target_image(location, transient_resource_group_name, source_type, source_object_name, \ source_os_disk_snapshot_name, source_os_disk_snapshot_url, source_os_type, \ @@ -28,7 +30,7 @@ def create_target_image(location, transient_resource_group_name, source_type, so '--location', location, \ '--sku', 'Standard_LRS']) - json_output = run_cli_command(cmd, True) + json_output = run_cli_command(cmd, return_as_json=True) target_blob_endpoint = json_output['primaryEndpoints']['blob'] @@ -37,7 +39,7 @@ def create_target_image(location, transient_resource_group_name, source_type, so '--account-name', target_storage_account_name, \ '--resource-group', transient_resource_group_name]) - json_output = run_cli_command(cmd, True) + json_output = run_cli_command(cmd, return_as_json=True) target_storage_account_key = json_output[0]['value'] logger.debug(target_storage_account_key) @@ -51,7 +53,7 @@ def create_target_image(location, transient_resource_group_name, source_type, so '--expiry', expiry.strftime(expiry_format), \ '--permissions', 'aclrpuw', '--resource-types', \ 'sco', '--services', 'b', '--https-only'], \ - False) + output_as_json=False) sas_token = run_cli_command(cmd) sas_token = sas_token.rstrip("\n\r") #STRANGE @@ -85,7 +87,7 @@ def create_target_image(location, transient_resource_group_name, source_type, so start_datetime = datetime.datetime.now() wait_for_blob_copy_operation(blob_name, target_container_name, \ target_storage_account_name, azure_pool_frequency, location) - msg = "{0} - Copy time: {1}".format(location, datetime.datetime.now()-start_datetime).ljust(40) + msg = "{0} - Copy time: {1}".format(location, datetime.datetime.now()-start_datetime).ljust(PROGRESS_LINE_LENGTH) logger.warn(msg) @@ -99,7 +101,7 @@ def create_target_image(location, transient_resource_group_name, source_type, so '--location', location, \ '--source', target_blob_path]) - json_output = run_cli_command(cmd, True) + json_output = run_cli_command(cmd, return_as_json=True) target_snapshot_id = json_output['id'] # Create the final image @@ -130,13 +132,15 @@ def wait_for_blob_copy_operation(blob_name, target_container_name, target_storag '--container-name', target_container_name, \ '--account-name', target_storage_account_name]) - json_output = run_cli_command(cmd, True) + json_output = run_cli_command(cmd, return_as_json=True) copy_status = json_output["properties"]["copy"]["status"] copy_progress_1, copy_progress_2 = json_output["properties"]["copy"]["progress"].split("/") current_progress = round(int(copy_progress_1)/int(copy_progress_2), 1) if current_progress != prev_progress: - msg = "{0} - copy progress: {1}%".format(location, str(current_progress)).ljust(40) #need to justify since message overide each other + msg = "{0} - copy progress: {1}%"\ + .format(location, str(current_progress))\ + .ljust(PROGRESS_LINE_LENGTH) #need to justify since messages overide each other progress_controller.add(message=msg) prev_progress = current_progress @@ -144,7 +148,6 @@ def wait_for_blob_copy_operation(blob_name, target_container_name, target_storag try: time.sleep(azure_pool_frequency) except KeyboardInterrupt: - print('xxx - child') progress_controller.stop() return @@ -152,15 +155,13 @@ def wait_for_blob_copy_operation(blob_name, target_container_name, target_storag if copy_status == 'success': progress_controller.stop() else: - logger.error("The copy operation didn't succeed. Last status: " + copy_status) - raise Exception('Blob copy failed') + logger.error("The copy operation didn't succeed. Last status: %s", copy_status) + raise CLIError('Blob copy failed') def get_subscription_id(): - logger.warn("Get subscription id") - cmd = prepare_cli_command(['account', 'show']) - json_output = run_cli_command(cmd, True) + json_output = run_cli_command(cmd, return_as_json=True) subscription_id = json_output['id'] # from azure.cli.core._profile import Profile diff --git a/src/image-copy/azext_imagecopy/custom.py b/src/image-copy/azext_imagecopy/custom.py index cad6f8e5c9b..0a0171889d7 100644 --- a/src/image-copy/azext_imagecopy/custom.py +++ b/src/image-copy/azext_imagecopy/custom.py @@ -16,11 +16,11 @@ def imagecopy(source_resource_group_name, source_object_name, target_location, \ '--name', source_object_name, \ '--resource-group', source_resource_group_name]) - json_cmd_output = run_cli_command(cmd, True) + json_cmd_output = run_cli_command(cmd, return_as_json=True) source_os_disk_id = json_cmd_output['storageProfile']['osDisk']['managedDisk']['id'] source_os_type = json_cmd_output['storageProfile']['osDisk']['osType'] - logger.debug("source_os_disk_id: " + source_os_disk_id + " source_os_type: " + source_os_type) + logger.debug("source_os_disk_id: %s. source_os_type: %s", source_os_disk_id, source_os_type) # create source snapshots @@ -41,10 +41,10 @@ def imagecopy(source_resource_group_name, source_object_name, target_location, \ '--resource-group', source_resource_group_name, \ '--duration-in-seconds', '3600']) - json_output = run_cli_command(cmd, True) + json_output = run_cli_command(cmd, return_as_json=True) source_os_disk_snapshot_url = json_output['accessSas'] - logger.debug(source_os_disk_snapshot_url) + logger.debug("source os disk snapshot url: %s" , source_os_disk_snapshot_url) # Start processing in the target locations @@ -52,20 +52,17 @@ def imagecopy(source_resource_group_name, source_object_name, target_location, \ transient_resource_group_name = 'image-copy-rg' create_resource_group(transient_resource_group_name, 'eastus') - target_locations_list = target_location.split(',') - target_locations_count = len(target_locations_list) - logger.warn("Target location count: {0}".format(target_locations_count)) + target_locations_count = len(target_location) + logger.warn("Target location count: %s", target_locations_count) - create_resource_group(target_resource_group_name, target_locations_list[0].strip()) + create_resource_group(target_resource_group_name, target_location[0].strip()) if parallel_degree == -1: pool = Pool(target_locations_count) - elif parallel_degree == 1: - pool = Pool(1) else: pool = Pool(min(parallel_degree, target_locations_count)) - # try to get a handle on arm 409s + # try to get a handle on arm's 409s azure_pool_frequency = 5 if target_locations_count >= 5: azure_pool_frequency = 15 @@ -73,7 +70,7 @@ def imagecopy(source_resource_group_name, source_object_name, target_location, \ azure_pool_frequency = 10 tasks = [] - for location in target_location.split(','): + for location in target_location: location = location.strip() tasks.append((location, transient_resource_group_name, source_type, \ source_object_name, source_os_disk_snapshot_name, source_os_disk_snapshot_url, \ @@ -88,16 +85,15 @@ def imagecopy(source_resource_group_name, source_object_name, target_location, \ pool.close() pool.join() except KeyboardInterrupt: - print('xxx - parent') logger.warn('User cancelled the operation') - if 'true' in cleanup: - logger.warn('To cleanup temporary resources look for ones tagged with "image-copy-extension"') + if cleanup: + logger.warn('To cleanup temporary resources look for ones tagged with "image-copy-extension". \nYou can use the following command: az resource list --tag created_by=image-copy-extension') pool.terminate() return # Cleanup - if 'true' in cleanup: + if cleanup: logger.warn('Deleting transient resources') # Delete resource group @@ -121,13 +117,13 @@ def imagecopy(source_resource_group_name, source_object_name, target_location, \ def create_resource_group(resource_group_name, location): # check if target resource group exists cmd = prepare_cli_command(['group', 'exists', \ - '--name', resource_group_name], False) + '--name', resource_group_name], output_as_json=False) cmd_output = run_cli_command(cmd) if 'false' in cmd_output: # create the target resource group - logger.warn("Creating resource group") + logger.warn("Creating resource group: %s", ) cmd = prepare_cli_command(['group', 'create', \ '--name', resource_group_name, \ '--location', location]) diff --git a/src/image-copy/setup.py b/src/image-copy/setup.py index 058eb4c8bfd..015fd807b15 100644 --- a/src/image-copy/setup.py +++ b/src/image-copy/setup.py @@ -8,7 +8,7 @@ from codecs import open from setuptools import setup, find_packages -VERSION = "0.0.2" +VERSION = "0.0.3" CLASSIFIERS = [ 'Development Status :: 4 - Beta', @@ -34,9 +34,9 @@ license='MIT', author='Tamir Kamara', author_email='tamir.kamara@microsoft.com', - url='https://github.com/ORG/REPO', + url='https://github.com/Azure/azure-cli-extensions', classifiers=CLASSIFIERS, packages=find_packages(), - package_data={'azext_hello': ['azext_metadata.json']}, + package_data={'azext_imagecopy': ['azext_metadata.json']}, install_requires=DEPENDENCIES ) From 6a529a09693839e957340c697450f0ca9fd87eab Mon Sep 17 00:00:00 2001 From: Tamir Kamara Date: Mon, 30 Oct 2017 21:15:39 +0200 Subject: [PATCH 3/5] remove dead code --- src/image-copy/azext_imagecopy/create_target.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/image-copy/azext_imagecopy/create_target.py b/src/image-copy/azext_imagecopy/create_target.py index c65ebb0dcc8..e81e45fda90 100644 --- a/src/image-copy/azext_imagecopy/create_target.py +++ b/src/image-copy/azext_imagecopy/create_target.py @@ -14,7 +14,6 @@ def create_target_image(location, transient_resource_group_name, source_type, so source_os_disk_snapshot_name, source_os_disk_snapshot_url, source_os_type, \ target_resource_group_name, azure_pool_frequency): - #from azure.cli.core.commands.client_factory import get_subscription_id subscription_id = get_subscription_id() subscription_hash = hashlib.sha1(subscription_id.encode("UTF-8")).hexdigest() @@ -164,8 +163,4 @@ def get_subscription_id(): json_output = run_cli_command(cmd, return_as_json=True) subscription_id = json_output['id'] - # from azure.cli.core._profile import Profile - # profile = Profile() - # _, subscription_id, _ = profile.get_login_credentials() - return subscription_id From 6a551220b64e153df07a6e81c82a041574030889 Mon Sep 17 00:00:00 2001 From: Tamir Kamara Date: Tue, 31 Oct 2017 19:58:54 +0200 Subject: [PATCH 4/5] round of small fixes --- src/image-copy/azext_imagecopy/__init__.py | 2 +- src/image-copy/azext_imagecopy/azext_metadata.json | 1 - src/image-copy/azext_imagecopy/cli_utils.py | 2 +- src/image-copy/azext_imagecopy/custom.py | 2 +- src/image-copy/setup.py | 1 - src/image-copy/test.cmd | 12 ++++++++++++ 6 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 src/image-copy/test.cmd diff --git a/src/image-copy/azext_imagecopy/__init__.py b/src/image-copy/azext_imagecopy/__init__.py index b01594a7080..6b540870c11 100644 --- a/src/image-copy/azext_imagecopy/__init__.py +++ b/src/image-copy/azext_imagecopy/__init__.py @@ -16,7 +16,7 @@ def load_params(_): c.register('source_resource_group_name', '--source-resource-group', help='Name of the resource group of the source resource') c.register('source_object_name', '--source-object-name', help='The name of the image or vm resource') c.register('target_location', '--target-location', nargs='+', help='Space separated location list to create the image in (use location short codes like westeurope etc.)') - c.register('source_type', '--source-type', default='image', help='image or vm') + c.register('source_type', '--source-type', default='image', choices=['image', 'vm'], help='image or vm') c.register('target_resource_group_name', '--target-resource-group', help='Name of the resource group to create images in') c.register('parallel_degree', '--parallel-degree', type=int, default=-1, help='Number of parallel copy operations') c.register('cleanup', '--cleanup', action='store_true', default=False, \ diff --git a/src/image-copy/azext_imagecopy/azext_metadata.json b/src/image-copy/azext_imagecopy/azext_metadata.json index b6c27f96fe3..7a73a41bfdf 100644 --- a/src/image-copy/azext_imagecopy/azext_metadata.json +++ b/src/image-copy/azext_imagecopy/azext_metadata.json @@ -1,3 +1,2 @@ { - "azext.minCliVersion": "2.0.12" } \ No newline at end of file diff --git a/src/image-copy/azext_imagecopy/cli_utils.py b/src/image-copy/azext_imagecopy/cli_utils.py index 9247c170edd..32276cfa29a 100644 --- a/src/image-copy/azext_imagecopy/cli_utils.py +++ b/src/image-copy/azext_imagecopy/cli_utils.py @@ -37,7 +37,7 @@ def prepare_cli_command(cmd, output_as_json=True): else: full_cmd += ['--output', 'tsv'] - # tag newly created resources + # tag newly created resources, containers don't have tags if 'create' in cmd and ('container' not in cmd): full_cmd += ['--tags', 'created_by=image-copy-extension'] diff --git a/src/image-copy/azext_imagecopy/custom.py b/src/image-copy/azext_imagecopy/custom.py index 0a0171889d7..e8ea2952c7a 100644 --- a/src/image-copy/azext_imagecopy/custom.py +++ b/src/image-copy/azext_imagecopy/custom.py @@ -123,7 +123,7 @@ def create_resource_group(resource_group_name, location): if 'false' in cmd_output: # create the target resource group - logger.warn("Creating resource group: %s", ) + logger.warn("Creating resource group: %s", resource_group_name) cmd = prepare_cli_command(['group', 'create', \ '--name', resource_group_name, \ '--location', location]) diff --git a/src/image-copy/setup.py b/src/image-copy/setup.py index 015fd807b15..131d096e944 100644 --- a/src/image-copy/setup.py +++ b/src/image-copy/setup.py @@ -37,6 +37,5 @@ url='https://github.com/Azure/azure-cli-extensions', classifiers=CLASSIFIERS, packages=find_packages(), - package_data={'azext_imagecopy': ['azext_metadata.json']}, install_requires=DEPENDENCIES ) diff --git a/src/image-copy/test.cmd b/src/image-copy/test.cmd new file mode 100644 index 00000000000..9f31dc749d7 --- /dev/null +++ b/src/image-copy/test.cmd @@ -0,0 +1,12 @@ +SET AZURE_EXTENSION_DIR=c:\Users\takamara\.azure\devcliextensions + +C:\Python\Python36-32\python setup.py bdist_wheel +C:\Python\Python36-32\scripts\pip install --upgrade --target C:\Users\takamara\.azure\devcliextensions\imagecopyextension C:\dev\azure-cli-extensions\src\image-copy + +rem az image copy --help + +az image copy --source-resource-group "test-rg" --source-object-name "vm2" --source-type "vm" --target-location uksouth northeurope --target-resource-group "images-repo" + +rem az image copy --source-resource-group "test-img-rg" --source-object-name "vm1-image" --target-location uksouth northeurope --target-resource-group "images-repo" --cleanup + +rem az vm-image-copy --source-resource-group "test-img-rg" --source-object-name "vm1-image" --source-type "image" --target-location "uksouth, northeurope, westus, eastus, australiaeast, eastasia" --target-resource-group "images-repo" --cleanup "true" \ No newline at end of file From b90cd9674b9b5522f3c64ba208df9fd24b380860 Mon Sep 17 00:00:00 2001 From: Tamir Kamara Date: Tue, 31 Oct 2017 20:04:47 +0200 Subject: [PATCH 5/5] remove test file --- src/image-copy/test.cmd | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 src/image-copy/test.cmd diff --git a/src/image-copy/test.cmd b/src/image-copy/test.cmd deleted file mode 100644 index 9f31dc749d7..00000000000 --- a/src/image-copy/test.cmd +++ /dev/null @@ -1,12 +0,0 @@ -SET AZURE_EXTENSION_DIR=c:\Users\takamara\.azure\devcliextensions - -C:\Python\Python36-32\python setup.py bdist_wheel -C:\Python\Python36-32\scripts\pip install --upgrade --target C:\Users\takamara\.azure\devcliextensions\imagecopyextension C:\dev\azure-cli-extensions\src\image-copy - -rem az image copy --help - -az image copy --source-resource-group "test-rg" --source-object-name "vm2" --source-type "vm" --target-location uksouth northeurope --target-resource-group "images-repo" - -rem az image copy --source-resource-group "test-img-rg" --source-object-name "vm1-image" --target-location uksouth northeurope --target-resource-group "images-repo" --cleanup - -rem az vm-image-copy --source-resource-group "test-img-rg" --source-object-name "vm1-image" --source-type "image" --target-location "uksouth, northeurope, westus, eastus, australiaeast, eastasia" --target-resource-group "images-repo" --cleanup "true" \ No newline at end of file