Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -2362,8 +2362,8 @@
],
"vm-repair": [
{
"downloadUrl": "https://azurecomputeaidrepair.blob.core.windows.net/prod/vm_repair-0.2.2-py2.py3-none-any.whl",
"filename": "vm_repair-0.2.2-py2.py3-none-any.whl",
"downloadUrl": "https://azurecomputeaidrepair.blob.core.windows.net/prod/vm_repair-0.2.3-py2.py3-none-any.whl",
"filename": "vm_repair-0.2.3-py2.py3-none-any.whl",
"metadata": {
"classifiers": [
"Development Status :: 4 - Beta",
Expand Down Expand Up @@ -2400,9 +2400,9 @@
"metadata_version": "2.0",
"name": "vm-repair",
"summary": "Auto repair commands to fix VMs.",
"version": "0.2.2"
"version": "0.2.3"
},
"sha256Digest": "42c6500e5587184026a398147b0a7b1a9cb7e59642de9b932dfcbe1a90f14697"
"sha256Digest": "1dadfc622001eab35a7f8e0ecd8c02b75808e1db823fdc44b1ee5c02e9bbdf10"
}
],
"vmware-cs": [
Expand Down
60 changes: 54 additions & 6 deletions src/vm-repair/azext_vm_repair/_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from datetime import datetime
from json import loads
from re import match, search
from re import match, search, findall
from knack.log import get_logger
from knack.util import CLIError

Expand Down Expand Up @@ -63,12 +63,16 @@ def validate_create(cmd, namespace):
logger.warning('The source VM\'s OS disk is encrypted.')

# Validate Auth Params
if is_linux and namespace.repair_username:
logger.warning("Setting admin username property is not allowed for Linux VMs. Ignoring the given repair-username parameter.")
if not is_linux and not namespace.repair_username:
# Prompt vm username
if not namespace.repair_username:
_prompt_repair_username(namespace)
# Validate vm username
validate_vm_username(namespace.repair_username, is_linux)
# Prompt vm password
if not namespace.repair_password:
_prompt_repair_password(namespace)
# Validate vm password
validate_vm_password(namespace.repair_password, is_linux)


def validate_restore(cmd, namespace):
Expand Down Expand Up @@ -272,10 +276,54 @@ def fetch_repair_vm(namespace):
message = 'More than one repair VM found:\n'
for vm in repair_list:
message += vm['id'] + '\n'
message += '\nPlease specify the --repair-vm-id to restore the disk-swap with.'
message += '\nPlease specify the repair VM id using the parameter --repair-vm-id'
raise CLIError(message)

# One repair VM found
namespace.repair_vm_id = repair_list[0]['id']

logger.info('Performing command on repair VM: %s\n', namespace.repair_vm_id)
logger.info('Found repair VM: %s\n', namespace.repair_vm_id)


def validate_vm_password(password, is_linux):
"""Sourced from src/azure-cli/azure/cli/command_modules/vm/_validators.py _validate_admin_password()"""
max_length = 72 if is_linux else 123
min_length = 12
if len(password) not in range(min_length, max_length + 1):
raise CLIError('Password length must be between {} and {}'.format(min_length, max_length))

contains_lower = findall('[a-z]+', password)
contains_upper = findall('[A-Z]+', password)
contains_digit = findall('[0-9]+', password)
contains_special_char = findall(r'[ `~!@#$%^&*()=+_\[\]{}\|;:.\/\'\",<>?]+', password)
count = len([x for x in [contains_lower, contains_upper, contains_digit, contains_special_char] if x])

if count < 3:
raise CLIError('Password must have the 3 of the following: 1 lower case character, 1 upper case character, 1 number and 1 special character')


def validate_vm_username(username, is_linux):
"""Sourced from src/azure-cli/azure/cli/command_modules/vm/_validators.py _validate_admin_username()"""
pattern = (r'[\\\/"\[\]:|<>+=;,?*@#()!A-Z]+' if is_linux else r'[\\\/"\[\]:|<>+=;,?*@]+')
linux_err = r'VM username cannot contain upper case character A-Z, special characters \/"[]:|<>+=;,?*@#()! or start with $ or -'
win_err = r'VM username cannot contain special characters \/"[]:|<>+=;,?*@# or ends with .'

if findall(pattern, username):
raise CLIError(linux_err if is_linux else win_err)

if is_linux and findall(r'^[$-]+', username):
raise CLIError(linux_err)

if not is_linux and username.endswith('.'):
raise CLIError(win_err)

# Sourced from vm module also
disallowed_user_names = [
"administrator", "admin", "user", "user1", "test", "user2",
"test1", "user3", "admin1", "1", "123", "a", "actuser", "adm",
"admin2", "aspnet", "backup", "console", "guest",
"owner", "root", "server", "sql", "support", "support_388945a0",
"sys", "test2", "test3", "user4", "user5"]

if username.lower() in disallowed_user_names:
raise CLIError("This username '{}' meets the general requirements, but is specifically disallowed. Please try a different value.".format(username))
55 changes: 32 additions & 23 deletions src/vm-repair/azext_vm_repair/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

# pylint: disable=line-too-long, too-many-locals, too-many-statements, broad-except, too-many-branches
import json
import logging
import os
import pkgutil
import timeit
Expand Down Expand Up @@ -45,9 +46,11 @@


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):
# begin progress reporting for long running operation
cmd.cli_ctx.get_progress_controller().begin()
cmd.cli_ctx.get_progress_controller().add(message='Running')
is_verbose = any(handler.level == logging.INFO for handler in get_logger().handlers)
# begin progress reporting for long running operation if not verbose
if not is_verbose:
cmd.cli_ctx.get_progress_controller().begin()
cmd.cli_ctx.get_progress_controller().add(message='Running')
# Function param for telemetry
func_params = _get_function_param_dict(inspect.currentframe())
# Start timer for custom telemetry
Expand Down Expand Up @@ -79,11 +82,8 @@ def create(cmd, vm_name, resource_group_name, repair_password=None, repair_usern
os_image_urn = _fetch_compatible_windows_os_urn(source_vm)

# Set up base create vm command
create_repair_vm_command = 'az vm create -g {g} -n {n} --tag {tag} --image {image} --admin-password {password}' \
.format(g=repair_group_name, n=repair_vm_name, tag=resource_tag, image=os_image_urn, password=repair_password)
# Add username field only for Windows
if not is_linux:
create_repair_vm_command += ' --admin-username {username}'.format(username=repair_username)
create_repair_vm_command = 'az vm create -g {g} -n {n} --tag {tag} --image {image} --admin-username {username} --admin-password {password}' \
.format(g=repair_group_name, n=repair_vm_name, tag=resource_tag, image=os_image_urn, username=repair_username, password=repair_password)
# fetch VM size of repair VM
sku = _fetch_compatible_sku(source_vm)
if not sku:
Expand Down Expand Up @@ -194,8 +194,9 @@ def create(cmd, vm_name, resource_group_name, repair_password=None, repair_usern
return_error_detail = str(exception)
return_message = 'An unexpected error occurred. Try running again with the --debug flag to debug.'
finally:
# end long running op for process
cmd.cli_ctx.get_progress_controller().end()
# end long running op for process if not verbose
if not is_verbose:
cmd.cli_ctx.get_progress_controller().end()

# Command failed block. Output right error message and return dict
if not command_succeeded:
Expand Down Expand Up @@ -229,9 +230,11 @@ def create(cmd, vm_name, resource_group_name, repair_password=None, repair_usern


def restore(cmd, vm_name, resource_group_name, disk_name=None, repair_vm_id=None, yes=False):
# begin progress reporting for long running operation
cmd.cli_ctx.get_progress_controller().begin()
cmd.cli_ctx.get_progress_controller().add(message='Running')
is_verbose = any(handler.level == logging.INFO for handler in get_logger().handlers)
# begin progress reporting for long running operation if not verbose
if not is_verbose:
cmd.cli_ctx.get_progress_controller().begin()
cmd.cli_ctx.get_progress_controller().add(message='Running')
# Function param for telemetry
func_params = _get_function_param_dict(inspect.currentframe())
# Start timer for custom telemetry
Expand Down Expand Up @@ -297,8 +300,9 @@ def restore(cmd, vm_name, resource_group_name, disk_name=None, repair_vm_id=None
return_error_detail = str(exception)
return_message = 'An unexpected error occurred. Try running again with the --debug flag to debug.'
finally:
# end long running op for process
cmd.cli_ctx.get_progress_controller().end()
# end long running op for process if not verbose
if not is_verbose:
cmd.cli_ctx.get_progress_controller().end()

if not command_succeeded:
return_status = STATUS_ERROR
Expand All @@ -321,9 +325,11 @@ def restore(cmd, vm_name, resource_group_name, disk_name=None, repair_vm_id=None


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):
# begin progress reporting for long running operation
cmd.cli_ctx.get_progress_controller().begin()
cmd.cli_ctx.get_progress_controller().add(message='Running')
is_verbose = any(handler.level == logging.INFO for handler in get_logger().handlers)
# begin progress reporting for long running operation if not verbose
if not is_verbose:
cmd.cli_ctx.get_progress_controller().begin()
cmd.cli_ctx.get_progress_controller().add(message='Running')
# Function param for telemetry
func_params = _get_function_param_dict(inspect.currentframe())
# Start timer and params for custom telemetry
Expand Down Expand Up @@ -424,14 +430,15 @@ def run(cmd, vm_name, resource_group_name, run_id=None, repair_vm_id=None, custo
logger.info('\nScript returned with output:\n%s\n', output)
else:
script_status = STATUS_ERROR
return_status = STATUS_ERROR
message = 'Script returned with possible errors.'
return_status = STATUS_SUCCESS
message = 'Script succesfully run but returned with possible errors.'
output = '\n'.join([log['message'] for log in logs if log['level'].lower() == 'error'])
logger.error('\nScript returned with error:\n%s\n', output)

logger.debug("stderr: %s", stderr)
return_message = message
return_dict['status'] = return_status
return_dict['script_status'] = script_status
return_dict['message'] = message
return_dict['logs'] = stdout
return_dict['log_full_path'] = log_fullpath
Expand All @@ -455,15 +462,17 @@ def run(cmd, vm_name, resource_group_name, run_id=None, repair_vm_id=None, custo
return_error_detail = str(exception)
return_message = 'An unexpected error occurred. Try running again with the --debug flag to debug.'
finally:
# end long running op for process
cmd.cli_ctx.get_progress_controller().end()
# end long running op for process if not verbose
if not is_verbose:
cmd.cli_ctx.get_progress_controller().end()

if not command_succeeded:
script_duration = ''
output = 'Script returned with possible errors.'
output = 'Repair run failed.'
script_status = STATUS_ERROR
return_status = STATUS_ERROR
return_dict = _handle_command_error(return_error_detail, return_message)
return_dict['script_status'] = script_status

# Track telemetry data
elapsed_time = timeit.default_timer() - start_time
Expand Down
4 changes: 2 additions & 2 deletions src/vm-repair/azext_vm_repair/telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def _track_command_telemetry(command_name, parameters, status, message, error_me
'parameters': json.dumps(parameters),
'command_status': status,
'message': message,
'error_messsage': error_message,
'error_message': error_message,
'subscription_id': subscription_id,
'result_json': json.dumps(result_json)
}
Expand All @@ -37,7 +37,7 @@ def _track_run_command_telemetry(command_name, parameters, status, message, erro
'parameters': json.dumps(parameters),
'command_status': status,
'message': message,
'error_messsage': error_message,
'error_message': error_message,
'subscription_id': subscription_id,
'result_json': json.dumps(result_json),
'script_run_id': script_run_id,
Expand Down
20 changes: 10 additions & 10 deletions src/vm-repair/azext_vm_repair/tests/latest/test_repair_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
class WindowsManagedDiskCreateRestoreTest(ScenarioTest):

@ResourceGroupPreparer(location='westus2')
def test_create_restore(self, resource_group):
def test_vmrepair_WinManagedCreateRestore(self, resource_group):
self.kwargs.update({
'vm': 'vm1'
})
Expand Down Expand Up @@ -42,7 +42,7 @@ def test_create_restore(self, resource_group):
class WindowsUnmanagedDiskCreateRestoreTest(ScenarioTest):

@ResourceGroupPreparer(location='westus2')
def test_create_restore(self, resource_group):
def test_vmrepair_WinUnmanagedCreateRestore(self, resource_group):
self.kwargs.update({
'vm': 'vm1'
})
Expand Down Expand Up @@ -75,19 +75,19 @@ def test_create_restore(self, resource_group):
class LinuxManagedDiskCreateRestoreTest(ScenarioTest):

@ResourceGroupPreparer(location='westus2')
def test_create_restore(self, resource_group):
def test_vmrepair_LinuxManagedCreateRestore(self, resource_group):
self.kwargs.update({
'vm': 'vm1'
})

# Create test VM
self.cmd('vm create -g {rg} -n {vm} --image UbuntuLTS --admin-password !Passw0rd2018')
self.cmd('vm create -g {rg} -n {vm} --image UbuntuLTS --admin-username azureadmin --admin-password !Passw0rd2018')
vms = self.cmd('vm list -g {rg}').get_output_in_json()
# Something wrong with vm create command if it fails here
assert len(vms) == 1

# Test create
result = self.cmd('vm repair create -g {rg} -n {vm} --repair-password !Passw0rd2018').get_output_in_json()
result = self.cmd('vm repair create -g {rg} -n {vm} --repair-username azureadmin --repair-password !Passw0rd2018').get_output_in_json()

# Check repair VM
repair_vms = self.cmd('vm list -g {}'.format(result['repair_resouce_group'])).get_output_in_json()
Expand All @@ -108,19 +108,19 @@ def test_create_restore(self, resource_group):
class LinuxUnmanagedDiskCreateRestoreTest(ScenarioTest):

@ResourceGroupPreparer(location='westus2')
def test_create_restore(self, resource_group):
def test_vmrepair_LinuxUnmanagedCreateRestore(self, resource_group):
self.kwargs.update({
'vm': 'vm1'
})

# Create test VM
self.cmd('vm create -g {rg} -n {vm} --image UbuntuLTS --admin-password !Passw0rd2018 --use-unmanaged-disk')
self.cmd('vm create -g {rg} -n {vm} --image UbuntuLTS --admin-username azureadmin --admin-password !Passw0rd2018 --use-unmanaged-disk')
vms = self.cmd('vm list -g {rg}').get_output_in_json()
# Something wrong with vm create command if it fails here
assert len(vms) == 1

# Test create
result = self.cmd('vm repair create -g {rg} -n {vm} --repair-password !Passw0rd2018').get_output_in_json()
result = self.cmd('vm repair create -g {rg} -n {vm} --repair-username azureadmin --repair-password !Passw0rd2018').get_output_in_json()

# Check repair VM
repair_vms = self.cmd('vm list -g {}'.format(result['repair_resouce_group'])).get_output_in_json()
Expand All @@ -141,7 +141,7 @@ def test_create_restore(self, resource_group):
class WindowsRunHelloWorldTest(ScenarioTest):

@ResourceGroupPreparer(location='westus2')
def test_win_hello_world(self, resource_group):
def test_vmrepair_WinRunHelloWorld(self, resource_group):
self.kwargs.update({
'vm': 'vm1'
})
Expand All @@ -162,7 +162,7 @@ def test_win_hello_world(self, resource_group):
class LinuxRunHelloWorldTest(ScenarioTest):

@ResourceGroupPreparer(location='westus2')
def test_linux_hello_world(self, resource_group):
def test_vmrepair_LinuxRunHelloWorld(self, resource_group):
self.kwargs.update({
'vm': 'vm1'
})
Expand Down
2 changes: 1 addition & 1 deletion src/vm-repair/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from codecs import open
from setuptools import setup, find_packages

VERSION = "0.2.2"
VERSION = "0.2.3"

CLASSIFIERS = [
'Development Status :: 4 - Beta',
Expand Down