#!/usr/bin/env python # -*- encoding: utf-8 -*- # # vim: tabstop=4 shiftwidth=4 softtabstop=4 # # Copyright (c) 2017-2018 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # """ Manage Disk partitions on this host and provide inventory updates """ import json import math import os import re import shutil import socket import subprocess import sys import time from collections import defaultdict from oslo_config import cfg from oslo_log import log from sysinv._i18n import _ from sysinv.common import constants from sysinv.common import service as sysinv_service from sysinv.common import utils from sysinv.common import disk_utils from sysinv.conductor import rpcapi as conductor_rpcapi from sysinv.openstack.common import context CONF = cfg.CONF LOG = log.getLogger(__name__) # Time between loops when waiting for partition to stabilize # from transitory states. # Lower is better. # At this moment, 0.3 seconds was found to give consistent # results in running over 100 consecutive tests. PARTITION_LOOP_WAIT_TIME = 0.3 RETURN_SUCCESS = 0 def _sectors_to_MiB(value, sector_size): """Transform sectors to MiB and return.""" return value * sector_size / (1024 ** 2) def _MiB_to_sectors(value, sector_size): """Transform MiBs to sectors and return.""" return value * (1024 ** 2) / sector_size @utils.skip_udev_partition_probe def _command(arguments, **kwargs): """Execute a command and capture stdout, stderr & return code.""" # TODO: change this to debug level log, but until proven stable # leave as info level log LOG.info("Executing command: '%s'" % " ".join(arguments)) if 'device_node' in kwargs: del kwargs['device_node'] process = subprocess.Popen( arguments, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs) out, err = process.communicate() return out, err, process.returncode @utils.skip_udev_partition_probe def _command_pipe(arguments1, arguments2=None, **kwargs): """Execute a command and capture stdout, stderr & return code.""" LOG.info("Executing pipe command: '%s'" % " ".join(arguments1)) if 'device_node' in kwargs: del kwargs['device_node'] process = subprocess.Popen( arguments1, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs) if arguments2: process2 = subprocess.Popen( arguments2, stdin=process.stdout, stdout=subprocess.PIPE, shell=False) process.stdout.close() process = process2 out, err = process.communicate() return out, err, process.returncode def _get_disk_sector_size(device_node_or_path): # Get sector size command. output, _, _ = _command(['blockdev', '--getss', device_node_or_path]) sector_size_bytes = int(output.rstrip()) return sector_size_bytes def _get_available_space(disk_device_path): """Obtain a disk's available space, in MiB.""" # Get total free space in sectors. output, _, _ = _command_pipe(['sgdisk', '-p', disk_device_path], ['grep', 'Total free space'], device_node=disk_device_path) avail_space_sectors = re.findall('\d+', output)[0].rstrip() if avail_space_sectors: avail_space_sectors = int(avail_space_sectors) else: LOG.exception( "Error trying to get the available space on disk %s" % disk_device_path) return # Get the sector size. sector_size_bytes = _get_disk_sector_size(disk_device_path) # Free space in MiB. total_available = _sectors_to_MiB(avail_space_sectors, sector_size_bytes) # Keep 2 MiB for partition table. if total_available >= 2: total_available = total_available - 2 else: total_available = 0 return total_available @utils.skip_udev_partition_probe def _gpt_table_present(device_node): """Check if a disk's partition table format is GPT or not. :param device_node: the disk's device node :returns False: the format is not GPT True: the format is GPT """ output, _, _ = _command(["udevadm", "settle", "-E", device_node]) output, _, _ = _command(["parted", "-s", device_node, "print"], device_node=device_node) if not re.search('Partition Table: gpt', output): print("Format of disk node %s is not GPT, returning" % device_node) return False return True def _get_disk_device_path(part_device_path): """Obtain the device path of a disk from a partition's device path. :param part_device_path: the partition's device path :returns the device path of the disk on which the partition resides """ return re.match('(/dev/disk/by-path/(.+))-part([0-9]+)', part_device_path).group(1) def _get_partition_number(part_device_path): """Obtain the number of a partition. :param part_device_path: the partition's device path :returns the partition's number """ return re.match('.*?([0-9]+)$', part_device_path).group(1) def _partition_exists(part_device_path): """Check if a partition exists. :param part_device_path: the partitions's device path :returns True: the partition exists False: the partition doesn't exist """ # Do not rely on the udev symlinks from /dev/disk/by-path as they may be # flushed during various partition operations. disk_device_path = _get_disk_device_path(part_device_path) part_number = _get_partition_number(part_device_path) output, err, ret = _command(['sgdisk', '-i', part_number, disk_device_path]) if "does not exist" in output: return False return True def _get_no_of_partitions(disk_device_path): """ Get the no of partitions on a device :param disk_device_path: disk's device path :return number of partitions """ output, _, _ = _command_pipe( ['sgdisk', '-p', disk_device_path], ['sed', "1,/^Number.*Start.*(sector).*End/d"], device_node=disk_device_path) rows = [row.strip() for row in output.splitlines() if row.strip()] return len(rows) @utils.skip_udev_partition_probe def _get_free_space(device_node): """Get the free spaces from a disk. :param device_node: disk's device node/path :returns array with the free spaces on disk""" free_spaces = [] output, _, _ = _command_pipe( ['parted', '-s', device_node, 'unit', 'mib', 'print', 'free'], ['grep', 'Free Space'], device_node=device_node) fields = ['start_mib', 'end_mib', 'size_mib'] output = output.replace("MiB", "").replace("Free Space", "") rows = [row.strip() for row in output.splitlines() if row.strip()] for row in rows: values = row.split() free_space = dict(zip(fields, values)) free_spaces.append(free_space) return free_spaces def _get_partition_start_end_size(disk_device_path, part_number, sector_size_bytes): """Return the start, end and size of a partitions. :param disk_device_path: disk's device path :param part_number: partition's number :returns dictionary {'start_mib': ..., 'end_mib': ..., 'size_mib': ...} """ output, err, ret = _command( ['sgdisk', '-i', str(part_number), disk_device_path], device_node=disk_device_path) partition = {} fields = {'start_mib': 'First sector', 'end_mib': 'Last sector', 'size_mib': 'Partition size'} rows = [row.strip() for row in output.splitlines() if row.strip()] for key, value in fields.items(): row = next((row for row in rows if value in row), None) if row: part_attr = re.findall('\d+', row)[0].rstrip() partition[key] = math.ceil( float(part_attr) * sector_size_bytes / (1024 ** 2)) return partition # While doing changes to partitions, there are brief moments when # the partition is in a transitory state and it is not mapped by # the udev. # This is due to the fact that "udevadm settle" command is event # based and when we call it we have no guarantee that the event # from the previous commands actually reached udev yet. # To guard against such timing issues, we must wait for a partition # to become "stable". We define the stable state as a number of # consecutive successful calls to access the partition, with a # small delay between them. def _wait_for_partition(device_path, max_retry_count=10, loop_wait_time=1, success_objective=3): success_count = 0 for step in range(1, max_retry_count): _, _, retcode = _command([ 'ls', str(device_path)]) if retcode == 0: success_count += 1 else: success_count = 0 LOG.warning("Partition/Device %s not present in the system." "Retrying" % str(device_path)) if success_count == success_objective: LOG.debug("Partition %s deemed stable" % str(device_path)) break time.sleep(loop_wait_time) else: raise IOError("Partition %s not present in OS" % str(device_path)) def _create_partition(disk_device_path, part_number, start_mib, size_mib, type_code, skip_wipe=False): """Create a partition. :param start: the start of the partition, in sectors :param size: the size of the partition, in sectors :param type_code: the type GUID of the partition :param skip_wipe: skip wiping partition start and end if True :returns dictionary containing the start, end and size of the new partition """ # Convert to sectors. sector_size_bytes = _get_disk_sector_size(disk_device_path) if not skip_wipe: # Prior to committing, we need to wipe the LVM data from this # partition so that if the LVM global filter is not set correctly # we will have stale LVM info polluting the system _wipe_partition(disk_device_path, _MiB_to_sectors(start_mib, sector_size_bytes), _MiB_to_sectors(size_mib, sector_size_bytes), sector_size_bytes) output, _, _ = _command(["udevadm", "settle", "-E", disk_device_path]) cmd = ['parted', '-s', disk_device_path, 'unit', 'mib', 'mkpart', 'primary', str(start_mib), str(start_mib + size_mib)] output, err, ret = _command(cmd, device_node=disk_device_path) if ret != RETURN_SUCCESS: raise IOError("Could not create partition %s of %sMiB on disk %s: %s" % (part_number, size_mib, disk_device_path, str(err))) output, _, _ = _command([ 'sgdisk', '--typecode={part_number}:{type_code}'.format( part_number=part_number, type_code=type_code), '--change-name={part_number}:{part_name}'.format( part_number=part_number, part_name=constants.PARTITION_NAME_PV), disk_device_path], device_node=disk_device_path) # After a partition is created we have to wait for udev to create the # corresponding device node. Otherwise if we try to open it will fail. part_device_path = '{}-part{}'.format(disk_device_path, part_number) _wait_for_partition(part_device_path, loop_wait_time=PARTITION_LOOP_WAIT_TIME) partition = _get_partition_start_end_size(disk_device_path, part_number, sector_size_bytes) return partition def _delete_partition(disk_device_path, part_number): """Delete a partition. :param disk_device_path: the device path of the disk on which the partition resides :param part_number: the partition number :returns N/A """ # Delete the partition with the specified number. cmd = ['parted', '-s', disk_device_path, 'rm', str(part_number)] output, err, ret = _command(cmd, device_node=disk_device_path) if ret != RETURN_SUCCESS: raise IOError("Could not delete partition %s from disk %s: %s" % (part_number, disk_device_path, str(err))) LOG.info("There was no %s partition on disk %s." % (part_number, disk_device_path)) def _resize_partition(disk_device_path, part_number, new_part_size_mib, start_mib, type_guid): """Modify a partition. :param disk_device_path: the device path of the disk on which the partition resides :param part_number: the partition number :param new_part_size_mib: the new size for the partition, in MiB :param type_guid: the type GUID for the partition :returns dictionary with partition's start, end, start """ try: _delete_partition(disk_device_path, part_number) except Exception as e: raise e try: partition = _create_partition( disk_device_path, part_number, start_mib, new_part_size_mib, type_guid, skip_wipe=True) except Exception as e: # An IOException usually means that the partition is in # a transitory state. We should wait for the partition # to stabilize and then try to commit the changes again LOG.error(_("IOError resizing partition %s of %s: %s") % (part_number, disk_device_path, str(e.message))) raise e return partition def _send_inventory_update(partition_update): """Send update to the sysinv conductor.""" # If this is controller-1, in an upgrade, don't send update. sw_mismatch = os.environ.get('CONTROLLER_SW_VERSIONS_MISMATCH', None) hostname = socket.gethostname() if sw_mismatch and hostname == constants.CONTROLLER_1_HOSTNAME: print("Don't send information to N-1 sysinv conductor, return.") return ctxt = context.get_admin_context() rpcapi = conductor_rpcapi.ConductorAPI( topic=conductor_rpcapi.MANAGER_TOPIC) max_tries = 2 num_of_try = 0 while num_of_try < max_tries: try: num_of_try = num_of_try + 1 rpcapi.update_partition_information(ctxt, partition_update) break except Exception as ex: print("Exception trying to contact sysinv conductor: %s: %s " % (type(ex).__name__, str(ex))) if num_of_try < max_tries and "Timeout" in type(ex).__name__: print("Could not contact sysinv conductor, try one more time..") continue else: print("Quit trying to send extra info to the conductor, " "sysinv agent will provide this info later...") def _wipe_partition(disk_node, start_in_sectors, size_in_sectors, sector_size): """Clear the locations within the partition where an LVM header may exist. """ # clear LVM and header and additional formatting data of this partition # (i.e. DRBD) # note: dd outputs to stderr, not stdout _, err_output, _ = _command( ['dd', 'bs={sector_size}'.format(sector_size=sector_size), 'if=/dev/zero', 'of={part_id}'.format(part_id=disk_node), 'oflag=direct', 'count=34', 'seek={part_end}'.format(part_end=start_in_sectors)]) # TODO: change this to debug level log, but until proven stable # leave as info level log LOG.info("Zero-out beginning of partition. Output: %s" % err_output) seek_end = start_in_sectors + size_in_sectors - 34 # format the last 1MB of the partition # note: dd outputs to stderr, not stdout _, err_output, _ = _command( ['dd', 'bs={sector_size}'.format(sector_size=sector_size), 'if=/dev/zero', 'of={part_id}'.format(part_id=disk_node), 'oflag=direct', 'count=34', 'seek={part_end}'.format(part_end=seek_end)]) # TODO: change this to debug level log, but until proven stable # leave as info level log LOG.info("Zero-out end of partition. Output: %s" % err_output) LOG.info("Partition details: %s" % {"disk_node": disk_node, "start_in_sectors": start_in_sectors, "size_in_sectors": size_in_sectors, "sector_size": sector_size, "part_end": seek_end}) def create_partitions(data, mode, pfile): """Process data for creating (a) partition(s) and send the update back to the sysinv conductor. """ if mode in ['create-only', 'send-only']: json_array = [] if mode == 'send-only': with open(pfile) as inputfile: payload = json.load(inputfile) for p in payload: _send_inventory_update(p) return print(data) json_body = json.loads(data) for p in json_body: disk_device_path = p.get('disk_device_path') part_device_path = p.get('part_device_path') if _gpt_table_present(device_node=disk_device_path): size_mib = int(p.get('req_size_mib')) type_code = p.get('req_guid') # Obtain parted device and parted disk for the given disk device # path. if _partition_exists(part_device_path): print("Partition %s already exists, returning." % part_device_path) continue # If we only allow to add and remove partition to/from the end, # then there should only be a max of two free regions (1MiB at # the beginning and the rest of the available disk, if any). free_spaces = _get_free_space(device_node=disk_device_path) if len(free_spaces) > 2: print("Disk %s is fragmented. Partition creation aborted." % disk_device_path) free_space = free_spaces[-1] number_of_partitions = _get_no_of_partitions(disk_device_path) # If this is the 1st partition, allocate an extra 1MiB. if number_of_partitions == 0: print("First partition, use an extra MiB") start_mib = 1 else: # Free space in sectors. start_mib = int(float(free_space.get('start_mib'))) response = { 'uuid': p.get('req_uuid'), 'ihost_uuid': p.get('ihost_uuid') } partition_number = number_of_partitions + 1 try: new_partition = _create_partition( disk_device_path, partition_number, start_mib, size_mib, type_code) part_device_path = '{}-part{}'.format(disk_device_path, partition_number) output, _, _ = _command(["udevadm", "settle", "-E", disk_device_path]) disk_available_mib = _get_available_space(disk_device_path) response.update({ 'start_mib': new_partition['start_mib'], 'end_mib': new_partition['end_mib'], 'size_mib': new_partition['size_mib'], 'device_path': part_device_path, 'type_guid': p.get('req_guid'), 'type_name': constants.PARTITION_NAME_PV, 'available_mib': disk_available_mib, 'status': constants.PARTITION_READY_STATUS}) except Exception as e: LOG.error("ERROR: %s" % e.message) response.update({'status': constants.PARTITION_ERROR_STATUS}) else: response = { 'uuid': p.get('req_uuid'), 'ihost_uuid': p.get('ihost_uuid'), 'status': constants.PARTITION_ERROR_STATUS_GPT } if mode == 'create-only': json_array.append(response) else: # Send results back to the conductor. _send_inventory_update(response) if mode == 'create-only': with open(pfile, 'w') as outfile: json.dump(json_array, outfile) class fix_global_filter(object): """ Some drbd metadata processing commands execute LVM commands. Therefore, our backing device has to be visible to LVM. """ def __init__(self, device_path): self.device_path = device_path self.lvm_conf_file = "/etc/lvm/lvm.conf" self.lvm_conf_backup_file = "/etc/lvm/lvm.conf.bck-manage-partitions" self.lvm_conf_temp_file = "/etc/lvm/lvm.conf.tmp-manage-partitions" def __enter__(self): # Backup existing config file shutil.copy(self.lvm_conf_file, self.lvm_conf_backup_file) # Prepare a new config file. with open(self.lvm_conf_file, "r") as lvm_conf: with open(self.lvm_conf_temp_file, "w") as lvm_new_conf: for line in lvm_conf: m = re.search('^\s*global_filter\s*=\s*(.*)', line) if m: global_filter = eval(m.group(1)) global_filter = [v for v in global_filter if v != "r|.*|"] global_filter.append("a|%s|" % self.device_path) global_filter.append("r|.*|") new_line = 'global_filter = ' + '[ "' + '", "'.join( global_filter) + '" ]\n' lvm_new_conf.write(new_line) else: lvm_new_conf.write(line) # Replace old config with new one. os.rename(self.lvm_conf_temp_file, self.lvm_conf_file) # Wait for LVM to reload its config. _wait_for_partition(self.device_path, loop_wait_time=PARTITION_LOOP_WAIT_TIME) for try_ in range(1, 10): output, _, ret_code = _command(["pvs", self.device_path]) if ret_code == 0: break else: time.sleep(1) def __exit__(self, type, value, traceback): # We are done, restore previous config. os.rename(self.lvm_conf_backup_file, self.lvm_conf_file) class DrbdFailureException(BaseException): """ Custom exception to allow DRBD config fallback""" pass def modify_partitions(data, mode, pfile): """Process data for modifying (a) partition(s) and send the update back to the sysinv conductor. """ json_body = json.loads(data) for p in json_body: # Get the partition's device path. part_device_path = p.get('part_device_path') disk_device_path = _get_disk_device_path(part_device_path) new_part_size_mib = p.get('new_size_mib') start_mib = p.get('start_mib') type_guid = p.get('req_guid') if _gpt_table_present(device_node=disk_device_path): # Separate the partition number from the disk's device path. part_number = _get_partition_number(part_device_path) response = { 'uuid': p.get('current_uuid'), 'ihost_uuid': p.get('ihost_uuid') } try: # Check if we have a DRBD partition is_drbd = False cmd_template = None metadata_dump = None _, _, _ = _command( ["udevadm", "settle", "-E", str(part_device_path)]) _wait_for_partition(part_device_path, loop_wait_time=PARTITION_LOOP_WAIT_TIME) output, _, _ = _command([ 'wipefs', '--parsable', str(part_device_path)]) for line in output.splitlines(): values = line.split(',') if len(values) and values[-1] == 'drbd': is_drbd = True LOG.info("Partition %s has drbd " "metadata!" % part_device_path) if is_drbd: # Steps based on: # https://docs.linbit.com/doc/users-guide-84/s-resizing/ # Check if drbd is configured and get a template # command to use for correctly accessing this device. # E.g. "drbdmeta 4 v08 internal dump-md output, _, _ = _command( ['drbdadm', '-d', 'dump-md', 'all']) for line in output.splitlines(): if part_device_path in line: # We found our command, remove 'dump-md' action, # we will add our own actions later. cmd_template = line.replace('dump-md', '').split() break else: # drbd meta should not be present on devices that are # not configured. Ignore it. is_drbd = False if is_drbd: # Make sure that metadata is clean - no operation are in # flight. output, err, err_code = _command( cmd_template + ['apply-al']) if err_code: raise Exception( "Failed cleaning metadata. stdout: '%s', " "stderr: '%s', return code: '%s'" % (output, err, err_code)) # Backup metadata metadata_dump, _, _ = _command(cmd_template + ['dump-md']) if err_code: raise DrbdFailureException( "Failed getting metadata. stdout: '%s', " "stderr: '%s', return code: '%s'" % (metadata_dump, err, err_code)) TMP_FILE = "/run/drbd-meta.dump" with open(TMP_FILE, "w") as f: for line in metadata_dump.splitlines(): f.write("%s\n" % line) # Resize the partition. part = _resize_partition(disk_device_path, part_number, new_part_size_mib, start_mib, type_guid) _command(["udevadm", "settle", "-E", str(part_device_path)]) if is_drbd: with fix_global_filter(part_device_path): # Initialize metadata area of resized partition # (metadata is located at the end of partition). output, err, err_code = _command( cmd_template + ['create-md', '--force']) if err_code: raise DrbdFailureException( "Failed to create metadata. stdout: '%s', " "stderr: '%s', return code: '%s'" % (output, err, err_code)) # Overwrite empty with backed-up meta new_output, err, err_code = _command( cmd_template + ['restore-md', TMP_FILE, '--force']) if err_code: raise DrbdFailureException( "Failed to restore metadata. stdout: '%s'," " stderr: '%s', return code: '%s', " "meta: %s" % (output, err, err_code, "\n".join(new_output))) if not is_drbd: # We may have a local PV, resize it. output, err, err_code = _command(['pvresize', part_device_path]) if err_code not in [0, 5]: raise Exception("Pvresize failure. stdout: '%s', " "stderr: '%s', return code: '%s', " % (output, err, err_code)) disk_available_mib = _get_available_space(disk_device_path) response.update({ 'start_mib': part['start_mib'], 'end_mib': part['end_mib'], 'size_mib': part['size_mib'], 'device_path': part_device_path, 'available_mib': disk_available_mib, 'type_name': constants.PARTITION_NAME_PV, 'status': constants.PARTITION_READY_STATUS}) except DrbdFailureException as e: if not os.path.exists('/etc/platform/simplex'): LOG.error("Partition modification failed due to DRBD cmd " "failure, recreating DRBD volume from scratch" "Details: %s", str(e)) _, _, _ = _command(['wipefs', '-a', part_device_path]) output, err, err_code = _command( cmd_template + ['create-md', '--force']) if err_code: LOG.exception( "Failed creating new metadata. stdout: '%s', " "stderr: '%s', return code: '%s', " % (output, err, err_code)) response.update( {'status': constants.PARTITION_ERROR_STATUS}) else: # We avoid wiping data if we have a single controller! LOG.exception("Partition modification failed: %s", str(e)) response.update( {'status': constants.PARTITION_ERROR_STATUS}) except Exception as e: LOG.exception("Partition modification failed: %s", str(e)) response.update({'status': constants.PARTITION_ERROR_STATUS}) # Send results back to the conductor. _send_inventory_update(response) def delete_partitions(data, mode, pfile): """Process data for deleting (a) partition(s) and send the update back to the sysinv conductor. """ json_body = json.loads(data) for p in json_body: # Get the partition's device path. part_device_path = p.get('part_device_path') disk_device_path = _get_disk_device_path(part_device_path) if _gpt_table_present(device_node=disk_device_path): # Separate the partition number from the disk's device path. part_number = _get_partition_number(part_device_path) response = { 'uuid': p.get('current_uuid'), 'ihost_uuid': p.get('ihost_uuid') } try: # Delete the partition. print("Delete partition %s from %s" % (disk_device_path, part_number)) _delete_partition(disk_device_path, part_number) disk_available_mib = _get_available_space(disk_device_path) response.update({'available_mib': disk_available_mib, 'status': constants.PARTITION_DELETED_STATUS}) except Exception as e: LOG.error("ERROR: %s" % e.message) response.update({'status': constants.PARTITION_ERROR_STATUS}) else: response = { 'uuid': p.get('req_uuid'), 'ihost_uuid': p.get('ihost_uuid'), 'status': constants.PARTITION_ERROR_STATUS_GPT } # Now that the partition is deleted, make sure that we purge it from # the LVM cache. Otherwise, if this partition is recreated and the LVM # global_filter has a view of it, it will become present from an LVM # perspective output, _, _ = _command(["pvscan", "--cache"]) # Send results back to the conductor. _send_inventory_update(response) def check_partitions(data, mode, pfile): """Check/create missing disk partitions """ json_body = json.loads(data) disks = defaultdict(list) for p in json_body: disk_device_path = p.get('disk_device_path') if not _gpt_table_present(device_node=disk_device_path): disk_utils.disk_wipe(disk_device_path) utils.execute('parted', disk_device_path, 'mklabel', 'gpt') time.sleep(1) # Wait for udev to flush partition table data disks[disk_device_path].append(p) for partitions in disks.values(): # Filter out any partitions without a start_mib. sortable_partitions = filter(lambda p: p.get('start_mib') is not None, partitions) for p in sorted(sortable_partitions, lambda p, q: p.get('start_mib') - q.get('start_mib')): disk = _get_disk_device_path(p.get('device_path')) if _partition_exists(p.get('device_path')): print('Partition {} already exists on disk {}'.format( p.get('device_path'), disk)) continue partition_number = _get_partition_number(p.get('device_path')) _create_partition(disk, partition_number, p.get('start_mib'), p.get('size_mib'), p.get('type_guid')) _, _, _ = _command( ["udevadm", "settle", "-E", p.get('disk_device_path')]) def add_action_parsers(subparsers): for action in ['delete', 'modify', 'create', 'check']: parser = subparsers.add_parser(action) parser.add_argument('-m', '--mode', choices=['create-only', 'send-only']) parser.add_argument('-f', '--pfile') parser.add_argument('data') parser.set_defaults(func=globals()[action + '_partitions']) CONF.register_cli_opt( cfg.SubCommandOpt('action', title='Action options', help='Available partition management options', handler=add_action_parsers)) @utils.synchronized(constants.PARTITION_MANAGE_LOCK) def run(action, data, mode, pfile): action(data, mode, pfile) def main(argv): sysinv_service.prepare_service(argv) global LOG LOG = log.getLogger("manage-partitions") if CONF.action.name in ['delete', 'modify', 'create', 'check']: msg = (_("Called partition '%(action)s' with '%(mode)s' '%(pfile)s' " "and '%(data)s'") % {"action": CONF.action.name, "mode": CONF.action.mode, "pfile": CONF.action.pfile, "data": CONF.action.data}) LOG.info(msg) print(msg) run(CONF.action.func, CONF.action.data, CONF.action.mode, CONF.action.pfile) else: LOG.error(_("Unknown action: %(action)") % {"action": CONF.action.name}) if __name__ == "__main__": main(sys.argv)