VMEncryption/main/DiskUtil.py (942 lines of code) (raw):

#!/usr/bin/env python # # VMEncryption extension # # Copyright 2015 Microsoft Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import subprocess import json import os import os.path import re from subprocess import Popen import shutil import traceback import uuid import glob from datetime import datetime from EncryptionConfig import EncryptionConfig from DecryptionMarkConfig import DecryptionMarkConfig from EncryptionMarkConfig import EncryptionMarkConfig from TransactionalCopyTask import TransactionalCopyTask from CommandExecutor import CommandExecutor, ProcessCommunicator from Common import CommonVariables, CryptItem, LvmItem, DeviceItem class DiskUtil(object): os_disk_lvm = None sles_cache = {} device_id_cache = {} def __init__(self, hutil, patching, logger, encryption_environment): self.encryption_environment = encryption_environment self.hutil = hutil self.distro_patcher = patching self.logger = logger self.ide_class_id = "{32412632-86cb-44a2-9b5c-50d1417354f5}" self.vmbus_sys_path = '/sys/bus/vmbus/devices' self.command_executor = CommandExecutor(self.logger) def copy(self, ongoing_item_config, status_prefix=''): copy_task = TransactionalCopyTask(logger=self.logger, disk_util=self, hutil=self.hutil, ongoing_item_config=ongoing_item_config, patching=self.distro_patcher, encryption_environment=self.encryption_environment, status_prefix=status_prefix) try: mem_fs_result = copy_task.prepare_mem_fs() if mem_fs_result != CommonVariables.process_success: return CommonVariables.tmpfs_error else: return copy_task.begin_copy() except Exception as e: message = "Failed to perform dd copy: {0}, stack trace: {1}".format(e, traceback.format_exc()) self.logger.log(msg=message, level=CommonVariables.ErrorLevel) finally: copy_task.clear_mem_fs() def format_disk(self, dev_path, file_system): mkfs_command = "" if file_system in CommonVariables.format_supported_file_systems: mkfs_command = "mkfs." + file_system mkfs_cmd = "{0} {1}".format(mkfs_command, dev_path) return self.command_executor.Execute(mkfs_cmd) def make_sure_path_exists(self, path): mkdir_cmd = self.distro_patcher.mkdir_path + ' -p ' + path self.logger.log("make sure path exists, executing: {0}".format(mkdir_cmd)) return self.command_executor.Execute(mkdir_cmd) def touch_file(self, path): mkdir_cmd = self.distro_patcher.touch_path + ' ' + path self.logger.log("touching file, executing: {0}".format(mkdir_cmd)) return self.command_executor.Execute(mkdir_cmd) def parse_crypttab_line(self, line): crypttab_parts = line.strip().split() if len(crypttab_parts) < 3: # Line should have enough content return None if crypttab_parts[0].startswith("#"): # Line should not be a comment return None crypt_item = CryptItem() crypt_item.mapper_name = crypttab_parts[0] crypt_item.dev_path = crypttab_parts[1] keyfile_path = crypttab_parts[2] if CommonVariables.encryption_key_mount_point not in keyfile_path and self.encryption_environment.cleartext_key_base_path not in keyfile_path: return None # if the key_file path doesn't have the encryption key file name, its probably not for us to mess with if self.encryption_environment.cleartext_key_base_path in keyfile_path: crypt_item.uses_cleartext_key = True crypttab_option_string = crypttab_parts[3] crypttab_options = crypttab_option_string.split(',') for option in crypttab_options: option_pair = option.split("=") if len(option_pair) == 2: key = option_pair[0].strip() value = option_pair[1].strip() if key == "header": crypt_item.luks_header_path = value return crypt_item def parse_azure_crypt_mount_line(self, line): crypt_item = CryptItem() crypt_mount_item_properties = line.strip().split() crypt_item.mapper_name = crypt_mount_item_properties[0] crypt_item.dev_path = crypt_mount_item_properties[1] crypt_item.luks_header_path = crypt_mount_item_properties[2] if crypt_mount_item_properties[2] and crypt_mount_item_properties[2] != "None" else None crypt_item.mount_point = crypt_mount_item_properties[3] crypt_item.file_system = crypt_mount_item_properties[4] crypt_item.uses_cleartext_key = True if crypt_mount_item_properties[5] == "True" else False crypt_item.current_luks_slot = int(crypt_mount_item_properties[6]) if len(crypt_mount_item_properties) > 6 else -1 return crypt_item def get_crypt_items(self): crypt_items = [] rootfs_crypt_item_found = False if self.should_use_azure_crypt_mount(): with open(self.encryption_environment.azure_crypt_mount_config_path, 'r') as f: for line in f.readlines(): if not line.strip(): continue crypt_item = self.parse_azure_crypt_mount_line(line) if crypt_item.mount_point == "/" or crypt_item.mapper_name == CommonVariables.osmapper_name: rootfs_crypt_item_found = True crypt_items.append(crypt_item) else: self.logger.log("Using crypttab instead of azure_crypt_mount file.") crypttab_path = "/etc/crypttab" fstab_items = [] with open("/etc/fstab", "r") as f: for line in f.readlines(): fstab_device, fstab_mount_point = self.parse_fstab_line(line) if fstab_device is not None: fstab_items.append((fstab_device, fstab_mount_point)) if not os.path.exists(crypttab_path): self.logger.log("{0} does not exist".format(crypttab_path)) else: with open(crypttab_path, 'r') as f: for line in f.readlines(): if not line.strip(): continue crypt_item = self.parse_crypttab_line(line) if crypt_item is None: continue if crypt_item.mapper_name == CommonVariables.osmapper_name: rootfs_crypt_item_found = True for device_path, mount_path in fstab_items: if crypt_item.mapper_name in device_path: crypt_item.mount_point = mount_path crypt_items.append(crypt_item) encryption_status = json.loads(self.get_encryption_status()) if encryption_status["os"] == "Encrypted" and not rootfs_crypt_item_found: crypt_item = CryptItem() crypt_item.mapper_name = CommonVariables.osmapper_name proc_comm = ProcessCommunicator() grep_result = self.command_executor.ExecuteInBash("cryptsetup status {0} | grep device:".format(crypt_item.mapper_name), communicator=proc_comm) if grep_result == 0: crypt_item.dev_path = proc_comm.stdout.strip().split()[1] else: proc_comm = ProcessCommunicator() self.command_executor.Execute("dmsetup table --target crypt", communicator=proc_comm) for line in proc_comm.stdout.splitlines(): if crypt_item.mapper_name in line: majmin = filter(lambda p: re.match(r'\d+:\d+', p), line.split())[0] src_device = filter(lambda d: d.majmin == majmin, self.get_device_items(None))[0] crypt_item.dev_path = '/dev/' + src_device.name break rootfs_dev = next((m for m in self.get_mount_items() if m["dest"] == "/")) crypt_item.file_system = rootfs_dev["fs"] if not crypt_item.dev_path: raise Exception("Could not locate block device for rootfs") crypt_item.luks_header_path = "/boot/luks/osluksheader" if not os.path.exists(crypt_item.luks_header_path): crypt_item.luks_header_path = crypt_item.dev_path crypt_item.mount_point = "/" crypt_item.uses_cleartext_key = False crypt_item.current_luks_slot = -1 crypt_items.append(crypt_item) return crypt_items def should_use_azure_crypt_mount(self): if not os.path.exists(self.encryption_environment.azure_crypt_mount_config_path): return False non_os_entry_found = False with open(self.encryption_environment.azure_crypt_mount_config_path, 'r') as f: for line in f.readlines(): if not line.strip(): continue parsed_crypt_item = self.parse_azure_crypt_mount_line(line) if parsed_crypt_item.mapper_name != CommonVariables.osmapper_name: non_os_entry_found = True # if there is a non_os_entry found we should use azure_crypt_mount. Otherwise we shouldn't return non_os_entry_found def add_crypt_item(self, crypt_item, key_file_path): if self.should_use_azure_crypt_mount(): return self.add_crypt_item_to_azure_crypt_mount(crypt_item) else: return self.add_crypt_item_to_crypttab(crypt_item, key_file_path) def add_crypt_item_to_crypttab(self, crypt_item, key_file): if key_file is None and crypt_item.uses_cleartext_key: line_key_file = self.encryption_environment.cleartext_key_base_path + crypt_item.mapper_name else: line_key_file = key_file crypttab_line = "\n{0} {1} {2} luks,nofail".format(crypt_item.mapper_name, crypt_item.dev_path, line_key_file) if crypt_item.luks_header_path: crypttab_line += ",header=" + crypt_item.luks_header_path with open("/etc/crypttab", "a") as wf: wf.write(crypttab_line + "\n") return True def add_crypt_item_to_azure_crypt_mount(self, crypt_item): """ TODO we should judge that the second time. format is like this: <target name> <source device> <key file> <options> """ try: if not crypt_item.luks_header_path: crypt_item.luks_header_path = "None" mount_content_item = (crypt_item.mapper_name + " " + crypt_item.dev_path + " " + crypt_item.luks_header_path + " " + crypt_item.mount_point + " " + crypt_item.file_system + " " + str(crypt_item.uses_cleartext_key) + " " + str(crypt_item.current_luks_slot)) if os.path.exists(self.encryption_environment.azure_crypt_mount_config_path): with open(self.encryption_environment.azure_crypt_mount_config_path, 'r') as f: existing_content = f.read() if existing_content is not None and existing_content.strip() != "": new_mount_content = existing_content + "\n" + mount_content_item else: new_mount_content = mount_content_item else: new_mount_content = mount_content_item with open(self.encryption_environment.azure_crypt_mount_config_path, 'w') as wf: wf.write('\n') wf.write(new_mount_content) wf.write('\n') return True except Exception: return False def remove_crypt_item(self, crypt_item): try: if self.should_use_azure_crypt_mount(): crypt_file_path = self.encryption_environment.azure_crypt_mount_config_path crypt_line_parser = self.parse_azure_crypt_mount_line elif os.path.exists("/etc/crypttab"): crypt_file_path = "/etc/crypttab" crypt_line_parser = self.parse_crypttab_line else: return True filtered_mount_lines = [] with open(crypt_file_path, 'r') as f: self.logger.log("removing an entry from {0}".format(crypt_file_path)) for line in f: if not line.strip(): continue parsed_crypt_item = crypt_line_parser(line) if parsed_crypt_item is not None and parsed_crypt_item.mapper_name == crypt_item.mapper_name: self.logger.log("Removing crypt mount entry: {0}".format(line)) continue filtered_mount_lines.append(line) with open(crypt_file_path, 'w') as wf: wf.write(''.join(filtered_mount_lines)) return True except Exception as e: return False def update_crypt_item(self, crypt_item, key_file_path): self.logger.log("Updating entry for crypt item {0}".format(crypt_item)) self.remove_crypt_item(crypt_item) self.add_crypt_item(crypt_item, key_file_path) def migrate_crypt_items(self, passphrase_file): crypt_items = self.get_crypt_items() # Archive azure_crypt_mount file try: if os.path.exists(self.encryption_environment.azure_crypt_mount_config_path): self.logger.log(msg="archiving azure crypt mount file: {0}".format(self.encryption_environment.azure_crypt_mount_config_path)) time_stamp = datetime.now() new_name = "{0}_{1}".format(self.encryption_environment.azure_crypt_mount_config_path, time_stamp) os.rename(self.encryption_environment.azure_crypt_mount_config_path, new_name) else: self.logger.log(msg=("the azure crypt mount file not exist: {0}".format(self.encryption_environment.azure_crypt_mount_config_path)), level=CommonVariables.InfoLevel) except OSError as e: self.logger.log("Failed to archive encryption mount file with error: {0}, stack trace: {1}".format(e, traceback.format_exc())) for crypt_item in crypt_items: self.logger.log("Migrating crypt item: {0}".format(crypt_item)) if crypt_item.mount_point == "/" or CommonVariables.osmapper_name == crypt_item.mapper_name: self.logger.log("Skipping OS disk") continue if crypt_item.mount_point and crypt_item.mount_point != "None": self.logger.log(msg="restoring entry for {0} drive in fstab".format(crypt_item.mount_point), level=CommonVariables.InfoLevel) self.restore_mount_info(crypt_item.mount_point) elif crypt_item.mapper_name: self.logger.log(msg="restoring entry for {0} drive in fstab".format(crypt_item.mapper_name), level=CommonVariables.InfoLevel) self.restore_mount_info(crypt_item.mapper_name) else: self.logger.log(msg=crypt_item.dev_path + " was not in fstab when encryption was enabled, no need to restore", level=CommonVariables.InfoLevel) self.modify_fstab_entry_encrypt(crypt_item.mount_point, os.path.join(CommonVariables.dev_mapper_root, crypt_item.mapper_name)) self.add_crypt_item_to_crypttab(crypt_item, passphrase_file) def is_luks_device(self, device_path, device_header_path): """ checks if the device is set up with a luks header """ path_var = device_header_path if device_header_path else device_path cmd = 'cryptsetup isLuks ' + path_var return (int)(self.command_executor.Execute(cmd, suppress_logging=True)) == CommonVariables.process_success def create_luks_header(self, mapper_name): luks_header_file_path = self.encryption_environment.luks_header_base_path + mapper_name if not os.path.exists(luks_header_file_path): dd_command = self.distro_patcher.dd_path + ' if=/dev/zero bs=33554432 count=1 > ' + luks_header_file_path self.command_executor.ExecuteInBash(dd_command, raise_exception_on_failure=True) return luks_header_file_path def create_cleartext_key(self, mapper_name): cleartext_key_file_path = self.encryption_environment.cleartext_key_base_path + mapper_name if not os.path.exists(cleartext_key_file_path): dd_command = self.distro_patcher.dd_path + ' if=/dev/urandom bs=128 count=1 > ' + cleartext_key_file_path self.command_executor.ExecuteInBash(dd_command, raise_exception_on_failure=True) return cleartext_key_file_path def encrypt_disk(self, dev_path, passphrase_file, mapper_name, header_file): return_code = self.luks_format(passphrase_file=passphrase_file, dev_path=dev_path, header_file=header_file) if return_code != CommonVariables.process_success: self.logger.log(msg=('cryptsetup luksFormat failed, return_code is:{0}'.format(return_code)), level=CommonVariables.ErrorLevel) return return_code else: return_code = self.luks_open(passphrase_file=passphrase_file, dev_path=dev_path, mapper_name=mapper_name, header_file=header_file, uses_cleartext_key=False) if return_code != CommonVariables.process_success: self.logger.log(msg=('cryptsetup luksOpen failed, return_code is:{0}'.format(return_code)), level=CommonVariables.ErrorLevel) return return_code def check_fs(self, dev_path): self.logger.log("checking fs:" + str(dev_path)) check_fs_cmd = self.distro_patcher.e2fsck_path + " -f -y " + dev_path return self.command_executor.Execute(check_fs_cmd) def expand_fs(self, dev_path): expandfs_cmd = self.distro_patcher.resize2fs_path + " " + str(dev_path) return self.command_executor.Execute(expandfs_cmd) def shrink_fs(self, dev_path, size_shrink_to): """ size_shrink_to is in sector (512 byte) """ shrinkfs_cmd = self.distro_patcher.resize2fs_path + ' ' + str(dev_path) + ' ' + str(size_shrink_to) + 's' return self.command_executor.Execute(shrinkfs_cmd) def check_shrink_fs(self, dev_path, size_shrink_to): return_code = self.check_fs(dev_path) if return_code == CommonVariables.process_success: return_code = self.shrink_fs(dev_path=dev_path, size_shrink_to=size_shrink_to) return return_code else: return return_code def luks_format(self, passphrase_file, dev_path, header_file): """ return the return code of the process for error handling. """ self.hutil.log("dev path to cryptsetup luksFormat {0}".format(dev_path)) #walkaround for sles sp3 if self.distro_patcher.distro_info[0].lower() == 'suse' and self.distro_patcher.distro_info[1] == '11': proc_comm = ProcessCommunicator() passphrase_cmd = self.distro_patcher.cat_path + ' ' + passphrase_file self.command_executor.Execute(passphrase_cmd, communicator=proc_comm) passphrase = proc_comm.stdout cryptsetup_cmd = "{0} luksFormat {1} -q".format(self.distro_patcher.cryptsetup_path, dev_path) return self.command_executor.Execute(cryptsetup_cmd, input=passphrase) else: if header_file is not None: cryptsetup_cmd = "{0} luksFormat {1} --header {2} -d {3} -q".format(self.distro_patcher.cryptsetup_path, dev_path, header_file, passphrase_file) else: cryptsetup_cmd = "{0} luksFormat {1} -d {2} -q".format(self.distro_patcher.cryptsetup_path, dev_path, passphrase_file) return self.command_executor.Execute(cryptsetup_cmd) def luks_add_key(self, passphrase_file, dev_path, mapper_name, header_file, new_key_path): """ return the return code of the process for error handling. """ self.hutil.log("new key path: " + (new_key_path)) if not os.path.exists(new_key_path): self.hutil.error("new key does not exist") return None if header_file: cryptsetup_cmd = "{0} luksAddKey {1} {2} -d {3} -q".format(self.distro_patcher.cryptsetup_path, header_file, new_key_path, passphrase_file) else: cryptsetup_cmd = "{0} luksAddKey {1} {2} -d {3} -q".format(self.distro_patcher.cryptsetup_path, dev_path, new_key_path, passphrase_file) return self.command_executor.Execute(cryptsetup_cmd) def luks_remove_key(self, passphrase_file, dev_path, header_file): """ return the return code of the process for error handling. """ self.hutil.log("removing keyslot: {0}".format(passphrase_file)) if header_file: cryptsetup_cmd = "{0} luksRemoveKey {1} -d {2} -q".format(self.distro_patcher.cryptsetup_path, header_file, passphrase_file) else: cryptsetup_cmd = "{0} luksRemoveKey {1} -d {2} -q".format(self.distro_patcher.cryptsetup_path, dev_path, passphrase_file) return self.command_executor.Execute(cryptsetup_cmd) def luks_kill_slot(self, passphrase_file, dev_path, header_file, keyslot): """ return the return code of the process for error handling. """ self.hutil.log("killing keyslot: {0}".format(keyslot)) if header_file: cryptsetup_cmd = "{0} luksKillSlot {1} {2} -d {3} -q".format(self.distro_patcher.cryptsetup_path, header_file, keyslot, passphrase_file) else: cryptsetup_cmd = "{0} luksKillSlot {1} {2} -d {3} -q".format(self.distro_patcher.cryptsetup_path, dev_path, keyslot, passphrase_file) return self.command_executor.Execute(cryptsetup_cmd) def luks_add_cleartext_key(self, passphrase_file, dev_path, mapper_name, header_file): """ return the return code of the process for error handling. """ cleartext_key_file_path = self.encryption_environment.cleartext_key_base_path + mapper_name self.hutil.log("cleartext key path: " + (cleartext_key_file_path)) return self.luks_add_key(passphrase_file, dev_path, mapper_name, header_file, cleartext_key_file_path) def luks_dump_keyslots(self, dev_path, header_file): cryptsetup_cmd = "" if header_file: cryptsetup_cmd = "{0} luksDump {1}".format(self.distro_patcher.cryptsetup_path, header_file) else: cryptsetup_cmd = "{0} luksDump {1}".format(self.distro_patcher.cryptsetup_path, dev_path) proc_comm = ProcessCommunicator() self.command_executor.Execute(cryptsetup_cmd, communicator=proc_comm) lines = filter(lambda l: "key slot" in l.lower(), proc_comm.stdout.split("\n")) keyslots = map(lambda l: "enabled" in l.lower(), lines) return keyslots def luks_open(self, passphrase_file, dev_path, mapper_name, header_file, uses_cleartext_key): """ return the return code of the process for error handling. """ self.hutil.log("dev mapper name to cryptsetup luksOpen " + (mapper_name)) if uses_cleartext_key: passphrase_file = self.encryption_environment.cleartext_key_base_path + mapper_name self.hutil.log("keyfile: " + (passphrase_file)) if header_file: cryptsetup_cmd = "{0} luksOpen {1} {2} --header {3} -d {4} -q".format(self.distro_patcher.cryptsetup_path, dev_path, mapper_name, header_file, passphrase_file) else: cryptsetup_cmd = "{0} luksOpen {1} {2} -d {3} -q".format(self.distro_patcher.cryptsetup_path, dev_path, mapper_name, passphrase_file) return self.command_executor.Execute(cryptsetup_cmd) def luks_close(self, mapper_name): """ returns the exit code for cryptsetup process. """ self.hutil.log("dev mapper name to cryptsetup luksOpen " + (mapper_name)) cryptsetup_cmd = "{0} luksClose {1} -q".format(self.distro_patcher.cryptsetup_path, mapper_name) return self.command_executor.Execute(cryptsetup_cmd) # TODO error handling. def append_mount_info(self, dev_path, mount_point): shutil.copy2('/etc/fstab', '/etc/fstab.backup.' + str(str(uuid.uuid4()))) mount_content_item = dev_path + " " + mount_point + " auto defaults 0 0" new_mount_content = "" with open("/etc/fstab", 'r') as f: existing_content = f.read() new_mount_content = existing_content + "\n" + mount_content_item with open("/etc/fstab", 'w') as wf: wf.write(new_mount_content) def is_bek_in_fstab_file(self, lines): for line in lines: fstab_device, fstab_mount_point = self.parse_fstab_line(line) if fstab_mount_point == CommonVariables.encryption_key_mount_point: return True return False def parse_fstab_line(self, line): fstab_parts = line.strip().split() if len(fstab_parts) < 2: # Line should have enough content return None, None if fstab_parts[0].startswith("#"): # Line should not be a comment return None, None fstab_device = fstab_parts[0] fstab_mount_point = fstab_parts[1] return fstab_device, fstab_mount_point def modify_fstab_entry_encrypt(self, mount_point, mapper_path): self.logger.log("modify_fstab_entry_encrypt called with mount_point={0}, mapper_path={1}".format(mount_point, mapper_path)) if not mount_point: self.logger.log("modify_fstab_entry_encrypt: mount_point is empty") return shutil.copy2('/etc/fstab', '/etc/fstab.backup.' + str(str(uuid.uuid4()))) with open('/etc/fstab', 'r') as f: lines = f.readlines() relevant_line = None for i in range(len(lines)): line = lines[i] fstab_device, fstab_mount_point = self.parse_fstab_line(line) if fstab_mount_point != mount_point: # Not the line we are looking for continue self.logger.log("Found the relevant fstab line: " + line) relevant_line = line if self.should_use_azure_crypt_mount(): # in this case we just remove the line lines.pop(i) break else: new_line = relevant_line.replace(fstab_device, mapper_path) self.logger.log("Replacing that line with: " + new_line) lines[i] = new_line break if not self.is_bek_in_fstab_file(lines): lines.append(self.get_fstab_bek_line()) with open('/etc/fstab', 'w') as f: f.writelines(lines) if relevant_line is not None: with open('/etc/fstab.azure.backup', 'a+') as f: f.write("\n" + relevant_line) def get_fstab_bek_line(self): if self.distro_patcher.distro_info[0].lower() == 'ubuntu' and self.distro_patcher.distro_info[1].startswith('14'): return CommonVariables.bek_fstab_line_template_ubuntu_14.format(CommonVariables.encryption_key_mount_point) else: return CommonVariables.bek_fstab_line_template.format(CommonVariables.encryption_key_mount_point) def add_bek_to_default_cryptdisks(self): if os.path.exists("/etc/default/cryptdisks"): with open("/etc/default/cryptdisks", 'r') as f: lines = f.readlines() if not any(["azure_bek_disk" in line for line in lines]): with open("/etc/default/cryptdisks", 'a') as f: f.write('\n' + CommonVariables.etc_defaults_cryptdisks_line.format(CommonVariables.encryption_key_mount_point)) def remove_mount_info(self, mount_point): if not mount_point: self.logger.log("remove_mount_info: mount_point is empty") return shutil.copy2('/etc/fstab', '/etc/fstab.backup.' + str(str(uuid.uuid4()))) filtered_contents = [] removed_lines = [] with open('/etc/fstab', 'r') as f: for line in f.readlines(): line = line.strip() pattern = '\s' + re.escape(mount_point) + '\s' if re.search(pattern, line): self.logger.log("removing fstab line: {0}".format(line)) removed_lines.append(line) continue filtered_contents.append(line) with open('/etc/fstab', 'w') as f: f.write('\n') f.write('\n'.join(filtered_contents)) f.write('\n') self.logger.log("fstab updated successfully") with open('/etc/fstab.azure.backup', 'a+') as f: f.write('\n') f.write('\n'.join(removed_lines)) f.write('\n') self.logger.log("fstab.azure.backup updated successfully") def restore_mount_info(self, mount_point_or_mapper_name): if not mount_point_or_mapper_name: self.logger.log("restore_mount_info: mount_point_or_mapper_name is empty") return shutil.copy2('/etc/fstab', '/etc/fstab.backup.' + str(str(uuid.uuid4()))) lines_to_keep_in_backup_fstab = [] lines_to_put_back_to_fstab = [] with open('/etc/fstab.azure.backup', 'r') as f: for line in f.readlines(): line = line.strip() + '\n' pattern = '\s' + re.escape(mount_point_or_mapper_name) + '\s' if re.search(pattern, line): self.logger.log("removing fstab.azure.backup line: {0}".format(line)) lines_to_put_back_to_fstab.append(line) continue lines_to_keep_in_backup_fstab.append(line) with open('/etc/fstab.azure.backup', 'w') as f: f.writelines(lines_to_keep_in_backup_fstab) self.logger.log("fstab.azure.backup updated successfully") lines_that_remain_in_fstab = [] with open('/etc/fstab', 'r') as f: for line in f.readlines(): line = line.strip() + '\n' pattern = '\s' + re.escape(mount_point_or_mapper_name) + '\s' if re.search(pattern, line): # This line should not remain in the fstab. self.logger.log("removing fstab line: {0}".format(line)) continue lines_that_remain_in_fstab.append(line) with open('/etc/fstab', 'w') as f: f.writelines(lines_that_remain_in_fstab + lines_to_put_back_to_fstab) self.logger.log("fstab updated successfully") def mount_bek_volume(self, bek_label, mount_point, option_string): """ mount the BEK volume """ self.make_sure_path_exists(mount_point) mount_cmd = self.distro_patcher.mount_path + ' -L "' + bek_label + '" ' + mount_point + ' -o ' + option_string return self.command_executor.Execute(mount_cmd) def mount_auto(self, dev_path_or_mount_point): """ mount the file system via fstab entry """ mount_cmd = self.distro_patcher.mount_path + ' ' + dev_path_or_mount_point return self.command_executor.Execute(mount_cmd) def mount_filesystem(self, dev_path, mount_point, file_system=None): """ mount the file system. """ self.make_sure_path_exists(mount_point) if file_system is None: mount_cmd = self.distro_patcher.mount_path + ' ' + dev_path + ' ' + mount_point else: mount_cmd = self.distro_patcher.mount_path + ' ' + dev_path + ' ' + mount_point + ' -t ' + file_system return self.command_executor.Execute(mount_cmd) def mount_crypt_item(self, crypt_item, passphrase): self.logger.log("trying to mount the crypt item:" + str(crypt_item)) self.logger.log(msg=('First trying to auto mount for the item')) mount_filesystem_result = self.mount_auto(os.path.join(CommonVariables.dev_mapper_root, crypt_item.mapper_name)) if str(crypt_item.mount_point) != 'None' and mount_filesystem_result != CommonVariables.process_success: self.logger.log(msg=('mount_point is not None and auto mount failed. Trying manual mount.'), level=CommonVariables.WarningLevel) mount_filesystem_result = self.mount_filesystem(os.path.join(CommonVariables.dev_mapper_root, crypt_item.mapper_name), crypt_item.mount_point, crypt_item.file_system) self.logger.log("mount file system result:{0}".format(mount_filesystem_result)) def swapoff(self): return self.command_executor.Execute('swapoff -a') def umount(self, path): umount_cmd = self.distro_patcher.umount_path + ' ' + path return self.command_executor.Execute(umount_cmd) def umount_all_crypt_items(self): for crypt_item in self.get_crypt_items(): self.logger.log("Unmounting {0}".format(os.path.join(CommonVariables.dev_mapper_root, crypt_item.mapper_name))) self.umount(os.path.join(CommonVariables.dev_mapper_root, crypt_item.mapper_name)) def mount_all(self): mount_all_cmd = self.distro_patcher.mount_path + ' -a' return self.command_executor.Execute(mount_all_cmd) def get_mount_items(self): items = [] for line in file('/proc/mounts'): line = [s.decode('string_escape') for s in line.split()] item = { "src": line[0], "dest": line[1], "fs": line[2] } items.append(item) return items def get_encryption_status(self): encryption_status = { "data": "NotEncrypted", "os": "NotEncrypted" } mount_items = self.get_mount_items() device_items = self.get_device_items(None) device_items_dict = dict([(device_item.mount_point, device_item) for device_item in device_items]) os_drive_encrypted = False data_drives_found = False all_data_drives_encrypted = True osmapper_path = os.path.join(CommonVariables.dev_mapper_root, CommonVariables.osmapper_name) if self.is_os_disk_lvm(): grep_result = self.command_executor.ExecuteInBash('pvdisplay | grep {0}'.format(osmapper_path), suppress_logging=True) if grep_result == 0 and not os.path.exists('/volumes.lvm'): self.logger.log("OS PV is encrypted") os_drive_encrypted = True special_azure_devices_to_skip = self.get_azure_devices() for mount_item in mount_items: device_item = device_items_dict.get(mount_item["dest"]) if device_item is not None and \ mount_item["fs"] in CommonVariables.format_supported_file_systems and \ self.is_data_disk(device_item, special_azure_devices_to_skip): data_drives_found = True if not device_item.type == "crypt": self.logger.log("Data volume {0} is mounted from {1}".format(mount_item["dest"], mount_item["src"])) all_data_drives_encrypted = False if mount_item["dest"] == "/" and \ not self.is_os_disk_lvm() and \ CommonVariables.dev_mapper_root in mount_item["src"] or \ "/dev/dm" in mount_item["src"]: self.logger.log("OS volume {0} is mounted from {1}".format(mount_item["dest"], mount_item["src"])) os_drive_encrypted = True if not data_drives_found: encryption_status["data"] = "NotMounted" elif all_data_drives_encrypted: encryption_status["data"] = "Encrypted" if os_drive_encrypted: encryption_status["os"] = "Encrypted" encryption_marker = EncryptionMarkConfig(self.logger, self.encryption_environment) decryption_marker = DecryptionMarkConfig(self.logger, self.encryption_environment) if decryption_marker.config_file_exists(): encryption_status["data"] = "DecryptionInProgress" elif encryption_marker.config_file_exists(): encryption_config = EncryptionConfig(self.encryption_environment, self.logger) volume_type = encryption_config.get_volume_type().lower() if volume_type == CommonVariables.VolumeTypeData.lower() or \ volume_type == CommonVariables.VolumeTypeAll.lower(): encryption_status["data"] = "EncryptionInProgress" if volume_type == CommonVariables.VolumeTypeOS.lower() or \ volume_type == CommonVariables.VolumeTypeAll.lower(): if not os_drive_encrypted: encryption_status["os"] = "EncryptionInProgress" elif os.path.exists(osmapper_path) and not os_drive_encrypted: encryption_status["os"] = "VMRestartPending" return json.dumps(encryption_status) def query_dev_sdx_path_by_scsi_id(self, scsi_number): p = Popen([self.distro_patcher.lsscsi_path, scsi_number], stdout=subprocess.PIPE, stderr=subprocess.PIPE) identity, err = p.communicate() # identity sample: [5:0:0:0] disk Msft Virtual Disk 1.0 /dev/sdc self.logger.log("lsscsi output is: {0}\n".format(identity)) vals = identity.split() if vals is None or len(vals) == 0: return None sdx_path = vals[len(vals) - 1] return sdx_path def query_dev_sdx_path_by_uuid(self, uuid): """ return /dev/disk/by-id that maps to the sdx_path, otherwise return the original path """ desired_uuid_path = os.path.join(CommonVariables.disk_by_uuid_root, uuid) for disk_by_uuid in os.listdir(CommonVariables.disk_by_uuid_root): disk_by_uuid_path = os.path.join(CommonVariables.disk_by_uuid_root, disk_by_uuid) if disk_by_uuid_path == desired_uuid_path: return os.path.realpath(disk_by_uuid_path) return desired_uuid_path def query_dev_id_path_by_sdx_path(self, sdx_path): """ return /dev/disk/by-id that maps to the sdx_path, otherwise return the original path Update: now we have realised that by-id is not a good way to refer to devices (they can change on reallocations or resizes). Try not to use this- use get_stable_path_from_sdx instead """ for disk_by_id in os.listdir(CommonVariables.disk_by_id_root): disk_by_id_path = os.path.join(CommonVariables.disk_by_id_root, disk_by_id) if os.path.realpath(disk_by_id_path) == sdx_path: return disk_by_id_path return sdx_path def get_persistent_path_by_sdx_path(self, sdx_path): """ return a stable path for this /dev/sdx device """ sdx_realpath = os.path.realpath(sdx_path) # First try finding an Azure symlink azure_name_table = self.get_block_device_to_azure_udev_table() if sdx_realpath in azure_name_table: return azure_name_table[sdx_realpath] # A mapper path is also pretty good (especially for raid or lvm) for mapper_name in os.listdir(CommonVariables.dev_mapper_root): mapper_path = os.path.join(CommonVariables.dev_mapper_root, mapper_name) if os.path.realpath(mapper_path) == sdx_realpath: return mapper_path # Then try matching a uuid symlink. Those are probably the best for disk_by_uuid in os.listdir(CommonVariables.disk_by_uuid_root): disk_by_uuid_path = os.path.join(CommonVariables.disk_by_uuid_root, disk_by_uuid) if os.path.realpath(disk_by_uuid_path) == sdx_realpath: return disk_by_uuid_path # Found nothing very persistent. Just return the original sdx path. # And Log it. self.logger.log(msg="Failed to find a persistent path for [{0}].".format(sdx_path), level=CommonVariables.WarningLevel) return sdx_path def get_device_path(self, dev_name): device_path = None if os.path.exists("/dev/" + dev_name): device_path = "/dev/" + dev_name elif os.path.exists("/dev/mapper/" + dev_name): device_path = "/dev/mapper/" + dev_name return device_path def get_device_id(self, dev_path): if (dev_path) in DiskUtil.device_id_cache: return DiskUtil.device_id_cache[dev_path] udev_cmd = "udevadm info -a -p $(udevadm info -q path -n {0}) | grep device_id".format(dev_path) proc_comm = ProcessCommunicator() self.command_executor.ExecuteInBash(udev_cmd, communicator=proc_comm, suppress_logging=True) match = re.findall(r'"{(.*)}"', proc_comm.stdout.strip()) DiskUtil.device_id_cache[dev_path] = match[0] if match else "" return DiskUtil.device_id_cache[dev_path] def get_device_items_property(self, dev_name, property_name): if (dev_name, property_name) in DiskUtil.sles_cache: return DiskUtil.sles_cache[(dev_name, property_name)] self.logger.log("getting property of device {0}".format(dev_name)) device_path = self.get_device_path(dev_name) property_value = "" if property_name == "SIZE": get_property_cmd = self.distro_patcher.blockdev_path + " --getsize64 " + device_path proc_comm = ProcessCommunicator() self.command_executor.Execute(get_property_cmd, communicator=proc_comm, suppress_logging=True) property_value = proc_comm.stdout.strip() elif property_name == "DEVICE_ID": property_value = self.get_device_id(device_path) else: get_property_cmd = self.distro_patcher.lsblk_path + " " + device_path + " -b -nl -o NAME," + property_name proc_comm = ProcessCommunicator() self.command_executor.Execute(get_property_cmd, communicator=proc_comm, raise_exception_on_failure=True, suppress_logging=True) for line in proc_comm.stdout.splitlines(): if line.strip(): disk_info_item_array = line.strip().split() if dev_name == disk_info_item_array[0]: if len(disk_info_item_array) > 1: property_value = disk_info_item_array[1] DiskUtil.sles_cache[(dev_name, property_name)] = property_value return property_value def get_block_device_to_azure_udev_table(self): table = {} if not os.path.exists(CommonVariables.azure_symlinks_dir): return table for top_level_item in os.listdir(CommonVariables.azure_symlinks_dir): top_level_item_full_path = os.path.join(CommonVariables.azure_symlinks_dir, top_level_item) if os.path.isdir(top_level_item_full_path): scsi_path = os.path.join(CommonVariables.azure_symlinks_dir, top_level_item) for symlink in os.listdir(scsi_path): symlink_full_path = os.path.join(scsi_path, symlink) table[os.path.realpath(symlink_full_path)] = symlink_full_path else: table[os.path.realpath(top_level_item_full_path)] = top_level_item_full_path return table def get_azure_symlinks(self): azure_udev_links = {} if os.path.exists(CommonVariables.azure_symlinks_dir): wdbackup = os.getcwd() os.chdir(CommonVariables.azure_symlinks_dir) for symlink in os.listdir(CommonVariables.azure_symlinks_dir): azure_udev_links[os.path.basename(symlink)] = os.path.realpath(symlink) os.chdir(wdbackup) return azure_udev_links def log_lsblk_output(self): lsblk_command = 'lsblk -o NAME,TYPE,FSTYPE,LABEL,SIZE,RO,MOUNTPOINT' proc_comm = ProcessCommunicator() self.command_executor.Execute(lsblk_command, communicator=proc_comm) self.logger.log('\n' + str(proc_comm.stdout) + '\n') def get_device_items_sles(self, dev_path): if dev_path: self.logger.log(msg=("getting blk info for: {0}".format(dev_path))) device_items_to_return = [] device_items = [] #first get all the device names if dev_path is None: lsblk_command = 'lsblk -b -nl -o NAME' else: lsblk_command = 'lsblk -b -nl -o NAME ' + dev_path proc_comm = ProcessCommunicator() self.command_executor.Execute(lsblk_command, communicator=proc_comm, raise_exception_on_failure=True) for line in proc_comm.stdout.splitlines(): item_value_str = line.strip() if item_value_str: device_item = DeviceItem() device_item.name = item_value_str.split()[0] device_items.append(device_item) for device_item in device_items: device_item.file_system = self.get_device_items_property(dev_name=device_item.name, property_name='FSTYPE') device_item.mount_point = self.get_device_items_property(dev_name=device_item.name, property_name='MOUNTPOINT') device_item.label = self.get_device_items_property(dev_name=device_item.name, property_name='LABEL') device_item.uuid = self.get_device_items_property(dev_name=device_item.name, property_name='UUID') device_item.majmin = self.get_device_items_property(dev_name=device_item.name, property_name='MAJ:MIN') device_item.device_id = self.get_device_items_property(dev_name=device_item.name, property_name='DEVICE_ID') device_item.azure_name = '' for symlink, target in self.get_azure_symlinks().items(): if device_item.name in target: device_item.azure_name = symlink # get the type of device model_file_path = '/sys/block/' + device_item.name + '/device/model' if os.path.exists(model_file_path): with open(model_file_path, 'r') as f: device_item.model = f.read().strip() else: self.logger.log(msg=("no model file found for device {0}".format(device_item.name))) if device_item.model == 'Virtual Disk': self.logger.log(msg="model is virtual disk") device_item.type = 'disk' else: partition_files = glob.glob('/sys/block/*/' + device_item.name + '/partition') self.logger.log(msg="partition files exists") if partition_files is not None and len(partition_files) > 0: device_item.type = 'part' size_string = self.get_device_items_property(dev_name=device_item.name, property_name='SIZE') if size_string is not None and size_string != "": device_item.size = int(size_string) if device_item.type is None: device_item.type = '' if device_item.size is not None: device_items_to_return.append(device_item) else: self.logger.log(msg=("skip the device {0} because we could not get size of it.".format(device_item.name))) return device_items_to_return def get_device_items(self, dev_path): if self.distro_patcher.distro_info[0].lower() == 'suse' and self.distro_patcher.distro_info[1] == '11': return self.get_device_items_sles(dev_path) else: if dev_path: self.logger.log(msg=("getting blk info for: " + str(dev_path))) if dev_path is None: lsblk_command = 'lsblk -b -n -P -o NAME,TYPE,FSTYPE,MOUNTPOINT,LABEL,UUID,MODEL,SIZE,MAJ:MIN' else: lsblk_command = 'lsblk -b -n -P -o NAME,TYPE,FSTYPE,MOUNTPOINT,LABEL,UUID,MODEL,SIZE,MAJ:MIN ' + dev_path proc_comm = ProcessCommunicator() self.command_executor.Execute(lsblk_command, communicator=proc_comm, raise_exception_on_failure=True, suppress_logging=True) device_items = [] lvm_items = self.get_lvm_items() for line in proc_comm.stdout.splitlines(): if line: device_item = DeviceItem() for disk_info_property in line.split(): property_item_pair = disk_info_property.split('=') if property_item_pair[0] == 'SIZE': device_item.size = int(property_item_pair[1].strip('"')) if property_item_pair[0] == 'NAME': device_item.name = property_item_pair[1].strip('"') if property_item_pair[0] == 'TYPE': device_item.type = property_item_pair[1].strip('"') if property_item_pair[0] == 'FSTYPE': device_item.file_system = property_item_pair[1].strip('"') if property_item_pair[0] == 'MOUNTPOINT': device_item.mount_point = property_item_pair[1].strip('"') if property_item_pair[0] == 'LABEL': device_item.label = property_item_pair[1].strip('"') if property_item_pair[0] == 'UUID': device_item.uuid = property_item_pair[1].strip('"') if property_item_pair[0] == 'MODEL': device_item.model = property_item_pair[1].strip('"') if property_item_pair[0] == 'MAJ:MIN': device_item.majmin = property_item_pair[1].strip('"') device_item.device_id = self.get_device_id(self.get_device_path(device_item.name)) if device_item.type is None: device_item.type = '' if device_item.type.lower() == 'lvm': for lvm_item in lvm_items: majmin = lvm_item.lv_kernel_major + ':' + lvm_item.lv_kernel_minor if majmin == device_item.majmin: device_item.name = lvm_item.vg_name + '/' + lvm_item.lv_name device_item.azure_name = '' for symlink, target in self.get_azure_symlinks().items(): if device_item.name in target: device_item.azure_name = symlink device_items.append(device_item) return device_items def get_lvm_items(self): lvs_command = 'lvs --noheadings --nameprefixes --unquoted -o lv_name,vg_name,lv_kernel_major,lv_kernel_minor' proc_comm = ProcessCommunicator() if self.command_executor.Execute(lvs_command, communicator=proc_comm): return [] lvm_items = [] for line in proc_comm.stdout.splitlines(): if not line: continue lvm_item = LvmItem() for pair in line.strip().split(): if len(pair.split('=')) != 2: continue key, value = pair.split('=') if key == 'LVM2_LV_NAME': lvm_item.lv_name = value if key == 'LVM2_VG_NAME': lvm_item.vg_name = value if key == 'LVM2_LV_KERNEL_MAJOR': lvm_item.lv_kernel_major = value if key == 'LVM2_LV_KERNEL_MINOR': lvm_item.lv_kernel_minor = value lvm_items.append(lvm_item) return lvm_items def is_os_disk_lvm(self): if DiskUtil.os_disk_lvm is not None: return DiskUtil.os_disk_lvm device_items = self.get_device_items(None) if not any([item.type.lower() == 'lvm' for item in device_items]): DiskUtil.os_disk_lvm = False return False lvm_items = filter(lambda item: item.vg_name == "rootvg", self.get_lvm_items()) current_lv_names = set([item.lv_name for item in lvm_items]) DiskUtil.os_disk_lvm = False expected_lv_names = set(['homelv', 'optlv', 'rootlv', 'swaplv', 'tmplv', 'usrlv', 'varlv']) if expected_lv_names == current_lv_names: DiskUtil.os_disk_lvm = True expected_lv_names = set(['homelv', 'optlv', 'rootlv', 'tmplv', 'usrlv', 'varlv']) if expected_lv_names == current_lv_names: DiskUtil.os_disk_lvm = True return DiskUtil.os_disk_lvm def is_data_disk(self, device_item, azure_devices): # Root disk if device_item.device_id.startswith('00000000-0000'): self.logger.log(msg="skipping root disk", level=CommonVariables.WarningLevel) return False # Resource Disk. Not considered a "data disk" exactly (is not attached via portal and we have a separate code path for encrypting it) if device_item.device_id.startswith('00000000-0001'): self.logger.log(msg="skipping resource disk", level=CommonVariables.WarningLevel) return False for azure_blk_item in azure_devices: if azure_blk_item.name == device_item.name: self.logger.log(msg="the mountpoint is the azure disk root or resource, so skip it.") return False return True def should_skip_for_inplace_encryption(self, device_item, special_azure_devices_to_skip, encrypt_volume_type): """ TYPE="raid0" TYPE="part" TYPE="crypt" first check whether there's one file system on it. if the type is disk, then to check whether it have child-items, say the part, lvm or crypt luks. if the answer is yes, then skip it. """ if encrypt_volume_type.lower() == 'data' and not self.is_data_disk(device_item, special_azure_devices_to_skip): return True # Skip data disks if device_item.file_system is None or device_item.file_system == "": self.logger.log(msg=("there's no file system on this device: {0}, so skip it.").format(device_item)) return True else: if device_item.size < CommonVariables.min_filesystem_size_support: self.logger.log(msg="the device size is too small," + str(device_item.size) + " so skip it.", level=CommonVariables.WarningLevel) return True supported_device_type = ["disk","part","raid0","raid1","raid5","raid10","lvm"] if device_item.type not in supported_device_type: self.logger.log(msg="the device type: " + str(device_item.type) + " is not supported yet, so skip it.", level=CommonVariables.WarningLevel) return True if device_item.uuid is None or device_item.uuid == "": self.logger.log(msg="the device do not have the related uuid, so skip it.", level=CommonVariables.WarningLevel) return True sub_items = self.get_device_items("/dev/" + device_item.name) if len(sub_items) > 1: self.logger.log(msg=("there's sub items for the device:{0} , so skip it.".format(device_item.name)), level=CommonVariables.WarningLevel) return True if device_item.type == "crypt": self.logger.log(msg=("device_item.type is:{0}, so skip it.".format(device_item.type)), level=CommonVariables.WarningLevel) return True if device_item.mount_point == "/": self.logger.log(msg=("the mountpoint is root:{0}, so skip it.".format(device_item)), level=CommonVariables.WarningLevel) return True for azure_blk_item in special_azure_devices_to_skip: if azure_blk_item.name == device_item.name: self.logger.log(msg="the mountpoint is the azure disk root or resource, so skip it.") return True return False def get_azure_devices(self): ide_devices = self.get_ide_devices() blk_items = [] for ide_device in ide_devices: current_blk_items = self.get_device_items("/dev/" + ide_device) for current_blk_item in current_blk_items: blk_items.append(current_blk_item) return blk_items def get_ide_devices(self): """ this only return the device names of the ide. """ ide_devices = [] for vmbus in os.listdir(self.vmbus_sys_path): f = open('%s/%s/%s' % (self.vmbus_sys_path, vmbus, 'class_id'), 'r') class_id = f.read() f.close() if class_id.strip() == self.ide_class_id: device_sdx_path = self.find_block_sdx_path(vmbus) self.logger.log("found one ide with vmbus: {0} and the sdx path is: {1}".format(vmbus, device_sdx_path)) ide_devices.append(device_sdx_path) return ide_devices def find_block_sdx_path(self, vmbus): device = None for root, dirs, files in os.walk(os.path.join(self.vmbus_sys_path , vmbus)): if root.endswith("/block"): device = dirs[0] else : #older distros for d in dirs: if ':' in d and "block" == d.split(':')[0]: device = d.split(':')[1] break return device