Skip to content

Commit c906263

Browse files
swbae31Ubuntujofranceroot
authored
vm-repair: Fix Encrypted Ubuntu Bug, add LVM support (Azure#2339)
* Adding code for unlock and added encryptformatall * removing temp files * Changes to logging and logic * Fixing code for data disk detection on RedHat * Create function for _invoke_run_command * add NF for finding the data disk with awk * Adding error traps to allow stderr to be printed * Added function for validating tag if secret is changed * Tidy code Co-authored-by: Ubuntu <ceschi@ubuntudevrepair.mygpwaslj2re3c1zv23vyhla2c.bx.internal.cloudapp.net> Co-authored-by: Francisco Franceschi <[email protected]> Co-authored-by: root <root@ubuntu18dev1.vdtdwq5rkvyuhaj2iq0ntqetub.syx.internal.cloudapp.net>
1 parent db1a4b3 commit c906263

File tree

9 files changed

+296
-123
lines changed

9 files changed

+296
-123
lines changed

azure-cli-extensions.pyproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4756,7 +4756,7 @@
47564756
<Content Include="src\virtual-wan\azext_vwan\azext_metadata.json" />
47574757
<Content Include="src\virtual-wan\readme.md" />
47584758
<Content Include="src\vm-repair\azext_vm_repair\scripts\linux-run-driver.sh" />
4759-
<Content Include="src\vm-repair\azext_vm_repair\scripts\mount_encrypted_disk.sh" />
4759+
<Content Include="src\vm-repair\azext_vm_repair\scripts\mount-encrypted-disk.sh" />
47604760
<Content Include="src\vm-repair\azext_vm_repair\scripts\win-run-driver.ps1" />
47614761
<Content Include="src\vm-repair\README.md" />
47624762
<Content Include="src\vm-repair\setup.cfg" />

src/vm-repair/azext_vm_repair/_validators.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def validate_create(cmd, namespace):
5959
namespace.repair_group_name = 'repair-' + namespace.vm_name + '-' + timestamp
6060

6161
# Check encrypted disk
62-
encryption_type, _, _ = _fetch_encryption_settings(source_vm)
62+
encryption_type, _, _, _ = _fetch_encryption_settings(source_vm)
6363
# Currently only supporting single pass
6464
if encryption_type in (Encryption.SINGLE_WITH_KEK, Encryption.SINGLE_WITHOUT_KEK):
6565
if not namespace.unlock_encrypted_vm:

src/vm-repair/azext_vm_repair/custom.py

Lines changed: 12 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,7 @@
44
# --------------------------------------------------------------------------------------------
55

66
# pylint: disable=line-too-long, too-many-locals, too-many-statements, broad-except, too-many-branches
7-
import json
87
import timeit
9-
import os
10-
import pkgutil
118
import traceback
129
import requests
1310
from knack.log import get_logger
@@ -33,6 +30,7 @@
3330
_check_script_succeeded,
3431
_fetch_disk_info,
3532
_unlock_singlepass_encrypted_disk,
33+
_invoke_run_command
3634
)
3735
from .exceptions import AzCommandError, SkuNotAvailableError, UnmanagedDiskCopyError, WindowsOsNotAvailableError, RunScriptNotFoundForIdError
3836
logger = get_logger(__name__)
@@ -106,7 +104,7 @@ def create(cmd, vm_name, resource_group_name, repair_password=None, repair_usern
106104

107105
# Handle encrypted VM cases
108106
if unlock_encrypted_vm:
109-
_unlock_singlepass_encrypted_disk(source_vm, is_linux, repair_group_name, repair_vm_name)
107+
_unlock_singlepass_encrypted_disk(source_vm, resource_group_name, repair_vm_name, repair_group_name, copy_disk_name, is_linux)
110108

111109
# UNMANAGED DISK
112110
else:
@@ -301,57 +299,44 @@ def run(cmd, vm_name, resource_group_name, run_id=None, repair_vm_id=None, custo
301299
# Init command helper object
302300
command = command_helper(logger, cmd, 'vm repair run')
303301

304-
REPAIR_DIR_NAME = 'azext_vm_repair'
305-
SCRIPTS_DIR_NAME = 'scripts'
306302
LINUX_RUN_SCRIPT_NAME = 'linux-run-driver.sh'
307303
WINDOWS_RUN_SCRIPT_NAME = 'win-run-driver.ps1'
308-
RUN_COMMAND_RUN_SHELL_ID = 'RunShellScript'
309-
RUN_COMMAND_RUN_PS_ID = 'RunPowerShellScript'
310304

311305
try:
312306
# Fetch VM data
313307
source_vm = get_vm(cmd, resource_group_name, vm_name)
314-
315-
# Build absoulte path of driver script
316-
loader = pkgutil.get_loader(REPAIR_DIR_NAME)
317-
mod = loader.load_module(REPAIR_DIR_NAME)
318-
rootpath = os.path.dirname(mod.__file__)
319308
is_linux = _is_linux_os(source_vm)
309+
320310
if is_linux:
321-
run_script = os.path.join(rootpath, SCRIPTS_DIR_NAME, LINUX_RUN_SCRIPT_NAME)
322-
command_id = RUN_COMMAND_RUN_SHELL_ID
311+
script_name = LINUX_RUN_SCRIPT_NAME
323312
else:
324-
run_script = os.path.join(rootpath, SCRIPTS_DIR_NAME, WINDOWS_RUN_SCRIPT_NAME)
325-
command_id = RUN_COMMAND_RUN_PS_ID
313+
script_name = WINDOWS_RUN_SCRIPT_NAME
326314

327315
# If run_on_repair is False, then repair_vm is the source_vm (scripts run directly on source vm)
328316
repair_vm_id = parse_resource_id(repair_vm_id)
329317
repair_vm_name = repair_vm_id['name']
330318
repair_resource_group = repair_vm_id['resource_group']
331319

332-
repair_run_command = 'az vm run-command invoke -g {rg} -n {vm} --command-id {command_id} ' \
333-
'--scripts "@{run_script}" -o json' \
334-
.format(rg=repair_resource_group, vm=repair_vm_name, command_id=command_id, run_script=run_script)
320+
run_command_params = []
321+
additional_scripts = []
335322

336323
# Normal scenario with run id
337324
if not custom_script_file:
338325
# Fetch run path from GitHub
339326
repair_script_path = _fetch_run_script_path(run_id)
340-
repair_run_command += ' --parameters script_path="./{repair_script}"'.format(repair_script=repair_script_path)
327+
run_command_params.append('script_path="./{}"'.format(repair_script_path))
341328
# Custom script scenario for script testers
342329
else:
343-
# no-op run id
344-
repair_run_command += ' "@{custom_file}" --parameters script_path=no-op'.format(custom_file=custom_script_file)
330+
run_command_params.append('script_path=no-op')
331+
additional_scripts.append(custom_script_file)
345332

346333
# Append Parameters
347334
if parameters:
348335
if is_linux:
349336
param_string = _process_bash_parameters(parameters)
350337
else:
351338
param_string = _process_ps_parameters(parameters)
352-
# Work around for run-command bug, unexpected behavior with space characters
353-
param_string = param_string.replace(' ', '%20')
354-
repair_run_command += ' params="{}"'.format(param_string)
339+
run_command_params.append('params="{}"'.format(param_string))
355340
if run_on_repair:
356341
vm_string = 'repair VM'
357342
else:
@@ -360,18 +345,8 @@ def run(cmd, vm_name, resource_group_name, run_id=None, repair_vm_id=None, custo
360345

361346
# Run script and measure script run-time
362347
script_start_time = timeit.default_timer()
363-
return_str = _call_az_command(repair_run_command)
348+
stdout, stderr = _invoke_run_command(script_name, repair_vm_name, repair_resource_group, is_linux, run_command_params, additional_scripts)
364349
command.script.run_time = timeit.default_timer() - script_start_time
365-
366-
# Extract stdout and stderr, if stderr exists then possible error
367-
run_command_return = json.loads(return_str)
368-
if is_linux:
369-
run_command_message = run_command_return['value'][0]['message'].split('[stdout]')[1].split('[stderr]')
370-
stdout = run_command_message[0].strip('\n')
371-
stderr = run_command_message[1].strip('\n')
372-
else:
373-
stdout = run_command_return['value'][0]['message']
374-
stderr = run_command_return['value'][1]['message']
375350
logger.debug("stderr: %s", stderr)
376351

377352
# Parse through stdout to populate log properties: 'level', 'message'

src/vm-repair/azext_vm_repair/repair_utils.py

Lines changed: 89 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,54 @@ def _call_az_command(command_string, run_async=False, secure_params=None):
6767
return None
6868

6969

70+
def _invoke_run_command(script_name, vm_name, rg_name, is_linux, parameters=None, additional_custom_scripts=None):
71+
"""
72+
Use azure run command to run the scripts within the vm-repair/scripts file and return stdout, stderr.
73+
"""
74+
75+
REPAIR_DIR_NAME = 'azext_vm_repair'
76+
SCRIPTS_DIR_NAME = 'scripts'
77+
RUN_COMMAND_RUN_SHELL_ID = 'RunShellScript'
78+
RUN_COMMAND_RUN_PS_ID = 'RunPowerShellScript'
79+
80+
# Build absoulte path of driver script
81+
loader = pkgutil.get_loader(REPAIR_DIR_NAME)
82+
mod = loader.load_module(REPAIR_DIR_NAME)
83+
rootpath = os.path.dirname(mod.__file__)
84+
run_script = os.path.join(rootpath, SCRIPTS_DIR_NAME, script_name)
85+
86+
if is_linux:
87+
command_id = RUN_COMMAND_RUN_SHELL_ID
88+
else:
89+
command_id = RUN_COMMAND_RUN_PS_ID
90+
91+
# Process script list to scripts string
92+
additional_scripts_string = ''
93+
if additional_custom_scripts:
94+
for script in additional_custom_scripts:
95+
additional_scripts_string += ' "@{script_name}"'.format(script_name=script)
96+
97+
run_command = 'az vm run-command invoke -g {rg} -n {vm} --command-id {command_id} ' \
98+
'--scripts @"{run_script}"{additional_scripts} -o json' \
99+
.format(rg=rg_name, vm=vm_name, command_id=command_id, run_script=run_script, additional_scripts=additional_scripts_string)
100+
if parameters:
101+
run_command += " --parameters {params}".format(params=' '.join(parameters))
102+
return_str = _call_az_command(run_command)
103+
104+
# Extract stdout and stderr, if stderr exists then possible error
105+
run_command_return = loads(return_str)
106+
107+
if is_linux:
108+
run_command_message = run_command_return['value'][0]['message'].split('[stdout]')[1].split('[stderr]')
109+
stdout = run_command_message[0].strip('\n')
110+
stderr = run_command_message[1].strip('\n')
111+
else:
112+
stdout = run_command_return['value'][0]['message']
113+
stderr = run_command_return['value'][1]['message']
114+
115+
return stdout, stderr
116+
117+
70118
def _get_current_vmrepair_version():
71119
from azure.cli.core.extension.operations import list_extensions
72120
version = [ext['version'] for ext in list_extensions() if ext['name'] == 'vm-repair']
@@ -186,27 +234,46 @@ def _list_resource_ids_in_rg(resource_group_name):
186234
def _fetch_encryption_settings(source_vm):
187235
key_vault = None
188236
kekurl = None
237+
secreturl = None
189238
if source_vm.storage_profile.os_disk.encryption_settings is not None:
190-
return Encryption.DUAL, key_vault, kekurl
239+
return Encryption.DUAL, key_vault, kekurl, secreturl
191240
# Unmanaged disk only support dual
192241
if not _uses_managed_disk(source_vm):
193-
return Encryption.NONE, key_vault, kekurl
242+
return Encryption.NONE, key_vault, kekurl, secreturl
194243

195244
disk_id = source_vm.storage_profile.os_disk.managed_disk.id
196-
show_disk_command = 'az disk show --id {i} --query [encryptionSettingsCollection,encryptionSettingsCollection.encryptionSettings[].diskEncryptionKey.sourceVault.id,encryptionSettingsCollection.encryptionSettings[].keyEncryptionKey.keyUrl] -o json'.format(i=disk_id)
197-
encryption_type, key_vault, kekurl = loads(_call_az_command(show_disk_command))
245+
show_disk_command = 'az disk show --id {i} --query [encryptionSettingsCollection,encryptionSettingsCollection.encryptionSettings[].diskEncryptionKey.sourceVault.id,encryptionSettingsCollection.encryptionSettings[].keyEncryptionKey.keyUrl,encryptionSettingsCollection.encryptionSettings[].diskEncryptionKey.secretUrl] -o json' \
246+
.format(i=disk_id)
247+
encryption_type, key_vault, kekurl, secreturl = loads(_call_az_command(show_disk_command))
198248
if [encryption_type, key_vault, kekurl] == [None, None, None]:
199-
return Encryption.NONE, key_vault, kekurl
249+
return Encryption.NONE, key_vault, kekurl, secreturl
200250
if kekurl == []:
201-
key_vault = key_vault[0]
202-
return Encryption.SINGLE_WITHOUT_KEK, key_vault, kekurl
203-
key_vault, kekurl = key_vault[0], kekurl[0]
204-
return Encryption.SINGLE_WITH_KEK, key_vault, kekurl
251+
key_vault, secreturl = key_vault[0], secreturl[0]
252+
return Encryption.SINGLE_WITHOUT_KEK, key_vault, kekurl, secreturl
253+
key_vault, kekurl, secreturl = key_vault[0], kekurl[0], secreturl[0]
254+
return Encryption.SINGLE_WITH_KEK, key_vault, kekurl, secreturl
255+
256+
257+
def _secret_tag_check(resource_group_name, copy_disk_name, secreturl):
258+
DEFAULT_LINUXPASSPHRASE_FILENAME = 'LinuxPassPhraseFileName'
259+
show_disk_command = 'az disk show -g {g} -n {n} --query encryptionSettingsCollection.encryptionSettings[].diskEncryptionKey.secretUrl -o json' \
260+
.format(n=copy_disk_name, g=resource_group_name)
261+
secreturl_new = loads(_call_az_command(show_disk_command))[0]
262+
if secreturl == secreturl_new:
263+
logger.debug('Secret urls are same. Skipping the tag check...')
264+
else:
265+
logger.debug('Secret urls are not same. Changing the tag...')
266+
show_tag_command = 'az keyvault secret show --id {securl} --query [tags.DiskEncryptionKeyEncryptionAlgorithm,tags.DiskEncryptionKeyEncryptionKeyURL] -o json' \
267+
.format(securl=secreturl_new)
268+
algorithm, keyurl = loads(_call_az_command(show_tag_command))
269+
set_tag_command = 'az keyvault secret set-attributes --tags DiskEncryptionKeyFileName={keyfile} DiskEncryptionKeyEncryptionAlgorithm={alg} DiskEncryptionKeyEncryptionKeyURL={kekurl} --id {securl}' \
270+
.format(keyfile=DEFAULT_LINUXPASSPHRASE_FILENAME, alg=algorithm, kekurl=keyurl, securl=secreturl_new)
271+
_call_az_command(set_tag_command)
205272

206273

207-
def _unlock_singlepass_encrypted_disk(source_vm, is_linux, repair_group_name, repair_vm_name):
274+
def _unlock_singlepass_encrypted_disk(source_vm, resource_group_name, repair_vm_name, repair_group_name, copy_disk_name, is_linux):
208275
# Installs the extension on repair VM and mounts the disk after unlocking.
209-
encryption_type, key_vault, kekurl = _fetch_encryption_settings(source_vm)
276+
encryption_type, key_vault, kekurl, secreturl = _fetch_encryption_settings(source_vm)
210277
if is_linux:
211278
volume_type = 'DATA'
212279
else:
@@ -219,36 +286,33 @@ def _unlock_singlepass_encrypted_disk(source_vm, is_linux, repair_group_name, re
219286
elif encryption_type is Encryption.SINGLE_WITHOUT_KEK:
220287
install_ade_extension_command = 'az vm encryption enable --disk-encryption-keyvault {vault} --name {repair} --resource-group {g} --volume-type {volume}' \
221288
.format(g=repair_group_name, repair=repair_vm_name, vault=key_vault, volume=volume_type)
289+
# Add format-all flag for linux vms
290+
if is_linux:
291+
install_ade_extension_command += " --encrypt-format-all"
222292
logger.info('Unlocking attached copied disk...')
223293
_call_az_command(install_ade_extension_command)
224294
# Linux VM encryption extension has a bug and we need to manually unlock and mount its disk
225295
if is_linux:
296+
# Validating secret tag and setting original tag if it got changed
297+
_secret_tag_check(resource_group_name, copy_disk_name, secreturl)
226298
logger.debug("Manually unlocking and mounting disk for Linux VMs.")
227-
_manually_unlock_mount_encrypted_disk(repair_group_name, repair_vm_name)
299+
_manually_unlock_mount_encrypted_disk(repair_vm_name, repair_group_name)
228300
except AzCommandError as azCommandError:
229301
error_message = str(azCommandError)
230302
# Linux VM encryption extension bug where it fails and then continue to mount disk manually
231303
if is_linux and "Failed to encrypt data volumes with error" in error_message:
232304
logger.debug("Expected bug for linux VMs. Ignoring error.")
233-
_manually_unlock_mount_encrypted_disk(repair_group_name, repair_vm_name)
305+
# Validating secret tag and setting original tag if it got changed
306+
_secret_tag_check(resource_group_name, copy_disk_name, secreturl)
307+
_manually_unlock_mount_encrypted_disk(repair_vm_name, repair_group_name)
234308
else:
235309
raise
236310

237311

238-
def _manually_unlock_mount_encrypted_disk(repair_group_name, repair_vm_name):
312+
def _manually_unlock_mount_encrypted_disk(repair_vm_name, repair_group_name):
239313
# Unlocks the disk using the phasephrase and mounts it on the repair VM.
240-
REPAIR_DIR_NAME = 'azext_vm_repair'
241-
SCRIPTS_DIR_NAME = 'scripts'
242314
LINUX_RUN_SCRIPT_NAME = 'mount-encrypted-disk.sh'
243-
command_id = 'RunShellScript'
244-
loader = pkgutil.get_loader(REPAIR_DIR_NAME)
245-
mod = loader.load_module(REPAIR_DIR_NAME)
246-
rootpath = os.path.dirname(mod.__file__)
247-
run_script = os.path.join(rootpath, SCRIPTS_DIR_NAME, LINUX_RUN_SCRIPT_NAME)
248-
mount_disk_command = 'az vm run-command invoke -g {rg} -n {vm} --command-id {command_id} ' \
249-
'--scripts "@{run_script}" -o json' \
250-
.format(rg=repair_group_name, vm=repair_vm_name, command_id=command_id, run_script=run_script)
251-
_call_az_command(mount_disk_command)
315+
return _invoke_run_command(LINUX_RUN_SCRIPT_NAME, repair_vm_name, repair_group_name, True)
252316

253317

254318
def _fetch_compatible_windows_os_urn(source_vm):

src/vm-repair/azext_vm_repair/scripts/linux-run-driver.sh

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@ if [ $(ls | wc -l) -eq 3 ]; then
1919
# Normal GitHub script scenario
2020
if [ "$1" != "no-op" ]; then
2121
chmod u+x $1 &&
22-
# Work around for passing space characters through run-command
23-
params=$(echo "$2" | sed "s/%20/ /") &&
24-
command_string="$1 $params" &&
22+
command_string="$*" &&
2523
bash -e $command_string >> $logFile
2624
else # Custom script scenario
2725
# Call the same script but it will only run the appended custom scripts

0 commit comments

Comments
 (0)