Skip to content

Commit 3bb5485

Browse files
authored
[vm-repair] Adding repair-and-restore one command flow for fstab scripts (#6244)
1 parent 5c05bd2 commit 3bb5485

File tree

9 files changed

+207
-17
lines changed

9 files changed

+207
-17
lines changed

src/vm-repair/HISTORY.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
Release History
33
===============
44

5+
0.5.4
6+
++++++
7+
Adding repair-and-restore command to create a one command flow for vm-repair with fstab scripts.
8+
59
0.5.3
610
++++++
711
Removing check for EncryptionSettingsCollection.enabled is string 'false'.
@@ -18,6 +22,10 @@ Updated exsiting privateIpAddress field to privateIPAddress and privateIpAllocat
1822
++++++
1923
Support for hosting repair vm in existing resource group and fixing existing resource group logic
2024

25+
0.5.0
26+
++++++
27+
Support for hosting repair vm in existing resource group and fixing existing resource group logic
28+
2129
0.4.10
2230
++++++
2331
Support for hosting repair vm in existing resource group and fixing existing resource group logic

src/vm-repair/azext_vm_repair/_help.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,12 @@
9292
text: >
9393
az vm repair reset-nic -g MyResourceGroup -n MyVM --yes --verbose
9494
"""
95+
96+
helps['vm repair repair-and-restore'] = """
97+
type: command
98+
short-summary: Repair and restore the VM.
99+
examples:
100+
- name: Repair and restore a VM.
101+
text: >
102+
az vm repair repair-and-restore --name vmrepairtest --resource-group MyResourceGroup --verbose
103+
"""

src/vm-repair/azext_vm_repair/_params.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,11 @@ def load_arguments(self, _):
5353
with self.argument_context('vm repair reset-nic') as c:
5454
c.argument('subscriptionid', help='Subscription id to default subscription using `az account set -s NAME_OR_ID`.')
5555
c.argument('yes', help='Do not prompt for confirmation to start VM if it is not running.')
56+
57+
with self.argument_context('vm repair repair-and-restore') as c:
58+
c.argument('repair_username', help='Admin username for repair VM.')
59+
c.argument('repair_password', help='Admin password for the repair VM.')
60+
c.argument('copy_disk_name', help='Name of OS disk copy.')
61+
c.argument('repair_vm_name', help='Name of repair VM.')
62+
c.argument('copy_disk_name', help='Name of OS disk copy.')
63+
c.argument('repair_group_name', help='Name for new or existing resource group that will contain repair VM.')

src/vm-repair/azext_vm_repair/_validators.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,3 +376,57 @@ def validate_vm_username(username, is_linux):
376376

377377
if username.lower() in disallowed_user_names:
378378
raise CLIError("This username '{}' meets the general requirements, but is specifically disallowed. Please try a different value.".format(username))
379+
380+
381+
def validate_repair_and_restore(cmd, namespace):
382+
check_extension_version(EXTENSION_NAME)
383+
384+
logger.info('Validating repair and restore parameters...')
385+
386+
logger.info(namespace.vm_name + ' ' + namespace.resource_group_name)
387+
388+
# Check if VM exists and is not classic VM
389+
source_vm = _validate_and_get_vm(cmd, namespace.resource_group_name, namespace.vm_name)
390+
is_linux = _is_linux_os(source_vm)
391+
392+
# Check repair vm name
393+
namespace.repair_vm_name = ('repair-' + namespace.vm_name)[:14] + '_'
394+
logger.info('Repair VM name: %s', namespace.repair_vm_name)
395+
396+
# Check copy disk name
397+
timestamp = datetime.utcnow().strftime('%Y%m%d%H%M%S')
398+
if namespace.copy_disk_name:
399+
_validate_disk_name(namespace.copy_disk_name)
400+
else:
401+
namespace.copy_disk_name = namespace.vm_name + '-DiskCopy-' + timestamp
402+
logger.info('Copy disk name: %s', namespace.copy_disk_name)
403+
404+
# Check copy resouce group name
405+
if namespace.repair_group_name:
406+
if namespace.repair_group_name == namespace.resource_group_name:
407+
raise CLIError('The repair resource group name cannot be the same as the source VM resource group.')
408+
_validate_resource_group_name(namespace.repair_group_name)
409+
else:
410+
namespace.repair_group_name = 'repair-' + namespace.vm_name + '-' + timestamp
411+
logger.info('Repair resource group name: %s', namespace.repair_group_name)
412+
413+
# Check encrypted disk
414+
encryption_type, _, _, _ = _fetch_encryption_settings(source_vm)
415+
# Currently only supporting single pass
416+
if encryption_type in (Encryption.SINGLE_WITH_KEK, Encryption.SINGLE_WITHOUT_KEK):
417+
if not namespace.unlock_encrypted_vm:
418+
_prompt_encrypted_vm(namespace)
419+
elif encryption_type is Encryption.DUAL:
420+
logger.warning('The source VM\'s OS disk is encrypted using dual pass method.')
421+
raise CLIError('The current command does not support VMs which were encrypted using dual pass.')
422+
else:
423+
logger.debug('The source VM\'s OS disk is not encrypted')
424+
425+
validate_vm_username(namespace.repair_username, is_linux)
426+
validate_vm_password(namespace.repair_password, is_linux)
427+
# Prompt input for public ip usage
428+
namespace.associate_public_ip = False
429+
430+
# Validate repair run command
431+
source_vm = _validate_and_get_vm(cmd, namespace.resource_group_name, namespace.vm_name)
432+
is_linux = _is_linux_os(source_vm)

src/vm-repair/azext_vm_repair/commands.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ def load_command_table(self, _):
1616
g.custom_command('run', 'run', validator=validate_run)
1717
g.custom_command('list-scripts', 'list_scripts')
1818
g.custom_command('reset-nic', 'reset_nic', is_preview=True, validator=validate_reset_nic)
19+
g.custom_command('repair-and-restore', 'repair_and_restore', is_preview=True)

src/vm-repair/azext_vm_repair/custom.py

Lines changed: 92 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from azure.cli.command_modules.vm.custom import get_vm, _is_linux_os
1515
from azure.cli.command_modules.storage.storage_url_helpers import StorageResourceIdentifier
1616
from msrestazure.tools import parse_resource_id
17-
from .exceptions import SkuDoesNotSupportHyperV
17+
from .exceptions import AzCommandError, SkuNotAvailableError, UnmanagedDiskCopyError, WindowsOsNotAvailableError, RunScriptNotFoundForIdError, SkuDoesNotSupportHyperV, ScriptReturnsError, SupportingResourceNotFoundError, CommandCanceledByUserError
1818

1919
from .command_helper_class import command_helper
2020
from .repair_utils import (
@@ -45,11 +45,15 @@
4545
_check_n_start_vm,
4646
_check_existing_rg
4747
)
48-
from .exceptions import AzCommandError, SkuNotAvailableError, UnmanagedDiskCopyError, WindowsOsNotAvailableError, RunScriptNotFoundForIdError, SkuDoesNotSupportHyperV, ScriptReturnsError, SupportingResourceNotFoundError, CommandCanceledByUserError
48+
from .exceptions import AzCommandError, RunScriptNotFoundForIdError, SupportingResourceNotFoundError, CommandCanceledByUserError
4949
logger = get_logger(__name__)
5050

5151

5252
def create(cmd, vm_name, resource_group_name, repair_password=None, repair_username=None, repair_vm_name=None, copy_disk_name=None, repair_group_name=None, unlock_encrypted_vm=False, enable_nested=False, associate_public_ip=False, distro='ubuntu', yes=False):
53+
54+
# log all the parameters
55+
logger.debug('vm repair create command parameters: vm_name: %s, resource_group_name: %s, repair_password: %s, repair_username: %s, repair_vm_name: %s, copy_disk_name: %s, repair_group_name: %s, unlock_encrypted_vm: %s, enable_nested: %s, associate_public_ip: %s, distro: %s, yes: %s', vm_name, resource_group_name, repair_password, repair_username, repair_vm_name, copy_disk_name, repair_group_name, unlock_encrypted_vm, enable_nested, associate_public_ip, distro, yes)
56+
5357
# Init command helper object
5458
command = command_helper(logger, cmd, 'vm repair create')
5559
# Main command calling block
@@ -101,7 +105,8 @@ def create(cmd, vm_name, resource_group_name, repair_password=None, repair_usern
101105
create_repair_vm_command += ' --zone {zone}'.format(zone=zone)
102106

103107
# Create new resource group
104-
if not _check_existing_rg(repair_group_name):
108+
existing_rg = _check_existing_rg(repair_group_name)
109+
if not existing_rg:
105110
create_resource_group_command = 'az group create -l {loc} -n {group_name}' \
106111
.format(loc=source_vm.location, group_name=repair_group_name)
107112
logger.info('Creating resource group for repair VM and its resources...')
@@ -272,7 +277,7 @@ def create(cmd, vm_name, resource_group_name, repair_password=None, repair_usern
272277
if not command.is_status_success():
273278
command.set_status_error()
274279
return_dict = command.init_return_dict()
275-
if _check_existing_rg(repair_group_name):
280+
if existing_rg:
276281
_clean_up_resources(repair_group_name, confirm=True)
277282
else:
278283
_clean_up_resources(repair_group_name, confirm=False)
@@ -305,9 +310,11 @@ def restore(cmd, vm_name, resource_group_name, disk_name=None, repair_vm_id=None
305310
# Fetch source and repair VM data
306311
source_vm = get_vm(cmd, resource_group_name, vm_name)
307312
is_managed = _uses_managed_disk(source_vm)
308-
repair_vm_id = parse_resource_id(repair_vm_id)
309-
repair_vm_name = repair_vm_id['name']
310-
repair_resource_group = repair_vm_id['resource_group']
313+
if repair_vm_id:
314+
logger.info('Repair VM ID: %s', repair_vm_id)
315+
repair_vm_id = parse_resource_id(repair_vm_id)
316+
repair_vm_name = repair_vm_id['name']
317+
repair_resource_group = repair_vm_id['resource_group']
311318
source_disk = None
312319

313320
# MANAGED DISK
@@ -379,6 +386,10 @@ def restore(cmd, vm_name, resource_group_name, disk_name=None, repair_vm_id=None
379386

380387
def run(cmd, vm_name, resource_group_name, run_id=None, repair_vm_id=None, custom_script_file=None, parameters=None, run_on_repair=False, preview=None):
381388

389+
# log method parameters
390+
logger.debug('vm repair run parameters: vm_name: %s, resource_group_name: %s, run_id: %s, repair_vm_id: %s, custom_script_file: %s, parameters: %s, run_on_repair: %s, preview: %s',
391+
vm_name, resource_group_name, run_id, repair_vm_id, custom_script_file, parameters, run_on_repair, preview)
392+
382393
# Init command helper object
383394
command = command_helper(logger, cmd, 'vm repair run')
384395
LINUX_RUN_SCRIPT_NAME = 'linux-run-driver.sh'
@@ -397,9 +408,13 @@ def run(cmd, vm_name, resource_group_name, run_id=None, repair_vm_id=None, custo
397408
script_name = WINDOWS_RUN_SCRIPT_NAME
398409

399410
# If run_on_repair is False, then repair_vm is the source_vm (scripts run directly on source vm)
400-
repair_vm_id = parse_resource_id(repair_vm_id)
401-
repair_vm_name = repair_vm_id['name']
402-
repair_resource_group = repair_vm_id['resource_group']
411+
if run_on_repair:
412+
repair_vm_id = parse_resource_id(repair_vm_id)
413+
repair_vm_name = repair_vm_id['name']
414+
repair_resource_group = repair_vm_id['resource_group']
415+
else:
416+
repair_vm_name = vm_name
417+
repair_resource_group = resource_group_name
403418

404419
run_command_params = []
405420
additional_scripts = []
@@ -650,3 +665,70 @@ def reset_nic(cmd, vm_name, resource_group_name, yes=False):
650665
return_dict = command.init_return_dict()
651666

652667
return return_dict
668+
669+
670+
def repair_and_restore(cmd, vm_name, resource_group_name, repair_password=None, repair_username=None, repair_vm_name=None, copy_disk_name=None, repair_group_name=None):
671+
from datetime import datetime
672+
import secrets
673+
import string
674+
675+
# Init command helper object
676+
command = command_helper(logger, cmd, 'vm repair repair-and-restore')
677+
678+
password_length = 30
679+
password_characters = string.ascii_lowercase + string.digits + string.ascii_uppercase
680+
repair_password = ''.join(secrets.choice(password_characters) for i in range(password_length))
681+
682+
username_length = 20
683+
username_characters = string.ascii_lowercase + string.digits
684+
repair_username = ''.join(secrets.choice(username_characters) for i in range(username_length))
685+
686+
timestamp = datetime.utcnow().strftime('%Y%m%d%H%M%S')
687+
repair_vm_name = ('repair-' + vm_name)[:14] + '_'
688+
copy_disk_name = vm_name + '-DiskCopy-' + timestamp
689+
repair_group_name = 'repair-' + vm_name + '-' + timestamp
690+
existing_rg = _check_existing_rg(repair_group_name)
691+
692+
create_out = create(cmd, vm_name, resource_group_name, repair_password, repair_username, repair_vm_name=repair_vm_name, copy_disk_name=copy_disk_name, repair_group_name=repair_group_name, associate_public_ip=False, yes=True)
693+
694+
# log create_out
695+
logger.info('create_out: %s', create_out)
696+
697+
repair_vm_name = create_out['repair_vm_name']
698+
copy_disk_name = create_out['copied_disk_name']
699+
repair_group_name = create_out['repair_resource_group']
700+
701+
logger.info('Running fstab run command')
702+
703+
try:
704+
run_out = run(cmd, repair_vm_name, repair_group_name, run_id='linux-alar2', parameters=["fstab"])
705+
706+
except Exception:
707+
command.set_status_error()
708+
command.error_stack_trace = traceback.format_exc()
709+
command.error_message = "Command failed when running fstab script."
710+
command.message = "Command failed when running fstab script."
711+
if existing_rg:
712+
_clean_up_resources(repair_group_name, confirm=True)
713+
else:
714+
_clean_up_resources(repair_group_name, confirm=False)
715+
return
716+
717+
# log run_out
718+
logger.info('run_out: %s', run_out)
719+
720+
if run_out['script_status'] == 'ERROR':
721+
logger.error('fstab script returned an error.')
722+
if existing_rg:
723+
_clean_up_resources(repair_group_name, confirm=True)
724+
else:
725+
_clean_up_resources(repair_group_name, confirm=False)
726+
return
727+
728+
logger.info('Running restore command')
729+
show_vm_id = 'az vm show -g {g} -n {n} --query id -o tsv' \
730+
.format(g=repair_group_name, n=repair_vm_name)
731+
732+
repair_vm_id = _call_az_command(show_vm_id)
733+
734+
restore(cmd, vm_name, resource_group_name, copy_disk_name, repair_vm_id, yes=True)

src/vm-repair/azext_vm_repair/repair_utils.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ def _check_existing_rg(rg_name):
210210
logger.error(azCommandError)
211211
raise Exception('Unexpected error occured while fetching existing resource groups.')
212212

213-
logger.info('Resource group exists is \'%s\'', group_exists)
213+
logger.info('Pre-existing repair resource group with the same name is \'%s\'', group_exists)
214214
return group_exists
215215

216216

@@ -491,22 +491,17 @@ def _fetch_compatible_windows_os_urn(source_vm):
491491

492492
def _suse_image_selector(distro):
493493
fetch_urn_command = 'az vm image list --publisher SUSE --offer {offer} --sku gen1 --verbose --all --query "[].urn | reverse(sort(@))" -o json'.format(offer=distro)
494-
logger.info('Fetching compatible SUSE OS images from gallery...')
495494
urns = loads(_call_az_command(fetch_urn_command))
496495

497496
# Raise exception when not finding SUSE image
498497
if not urns:
499498
raise SuseNotAvailableError()
500499

501-
logger.debug('Fetched urns: \n%s', urns)
502-
# Returning the first URN as it is the latest image with no special use like HPC or SAP
503-
logger.debug('Return the first URN : %s', urns[0])
504500
return urns[0]
505501

506502

507503
def _suse_image_selector_gen2(distro):
508504
fetch_urn_command = 'az vm image list --publisher SUSE --offer {offer} --sku gen2 --verbose --all --query "[].urn | reverse(sort(@))" -o json'.format(offer=distro)
509-
logger.info('Fetching compatible SUSE OS images from gallery...')
510505
urns = loads(_call_az_command(fetch_urn_command))
511506

512507
# Raise exception when not finding SUSE image
@@ -711,6 +706,14 @@ def _unlock_encrypted_vm_run(repair_vm_name, repair_group_name, is_linux):
711706

712707

713708
def _create_repair_vm(copy_disk_id, create_repair_vm_command, repair_password, repair_username, fix_uuid=False):
709+
710+
# logging all parameters of the function individually
711+
logger.info('Creating repair VM with command: {}'.format(create_repair_vm_command))
712+
logger.info('copy_disk_id: {}'.format(copy_disk_id))
713+
logger.info('repair_password: {}'.format(repair_password))
714+
logger.info('repair_username: {}'.format(repair_username))
715+
logger.info('fix_uuid: {}'.format(fix_uuid))
716+
714717
if not fix_uuid:
715718
create_repair_vm_command += ' --attach-data-disks {id}'.format(id=copy_disk_id)
716719
logger.info('Validating VM template before continuing...')

src/vm-repair/azext_vm_repair/tests/latest/test_repair_commands.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,3 +717,28 @@ def test_vmrepair_ResetNicWindowsVM(self, resource_group):
717717
vm_instance_view = self.cmd('vm get-instance-view -g {rg} -n {vm} -o json').get_output_in_json()
718718
vm_power_state = vm_instance_view['instanceView']['statuses'][1]['code']
719719
assert vm_power_state == 'PowerState/running'
720+
721+
722+
@pytest.mark.repairandrestore
723+
class RepairAndRestoreLinuxVM(LiveScenarioTest):
724+
725+
@ResourceGroupPreparer(location='westus2')
726+
def test_vmrepair_RepairAndRestoreLinuxVM(self, resource_group):
727+
self.kwargs.update({
728+
'vm': 'vm1'
729+
})
730+
731+
# Create test VM
732+
self.cmd('vm create -g {rg} -n {vm} --admin-username azureadmin --image Win2016Datacenter --admin-password !Passw0rd2018')
733+
vms = self.cmd('vm list -g {rg} -o json').get_output_in_json()
734+
# Something wrong with vm create command if it fails here
735+
assert len(vms) == 1
736+
737+
# Test Repair and restore
738+
result = self.cmd('vm repair repair-and-restore -g {rg} -n {vm}')
739+
assert result['status'] == STATUS_SUCCESS, result['error_message']
740+
741+
# Check swapped OS disk
742+
vms = self.cmd('vm list -g {rg} -o json').get_output_in_json()
743+
source_vm = vms[0]
744+
assert source_vm['storageProfile']['osDisk']['name'] == result['copied_disk_name']

src/vm-repair/setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from codecs import open
99
from setuptools import setup, find_packages
1010

11-
VERSION = "0.5.3"
11+
VERSION = "0.5.4"
1212

1313
CLASSIFIERS = [
1414
'Development Status :: 4 - Beta',

0 commit comments

Comments
 (0)