def _check_partition(device, number, part_type, start, end): ''' Check if the proposed partition match the current one. Returns a tri-state value: - `True`: the proposed partition match - `False`: the proposed partition do not match - `None`: the proposed partition is a new partition ''' # The `start` and `end` fields are expressed with units (the same # kind of units that `parted` allows). To make a fair comparison # we need to normalize each field to the same units that we can # use to read the current partitions. A good candidate is sector # ('s'). The problem is that we need to reimplement the same # conversion logic from `parted` here [1], as we need the same # round logic when we convert from 'MiB' to 's', for example. # # To avoid this duplicity of code we can do a trick: for each # field in the proposed partition we request a `partition.list` # with the same unit. We make `parted` to make the conversion for # us, in exchange for an slower algorithm. # # We can change it once we decide to take care of alignment. # # [1] Check libparted/unit.c number = str(number) partitions = _get_cached_partitions(device) if number not in partitions: return None if part_type != partitions[number]['type']: return False for value, name in ((start, 'start'), (end, 'end')): value, unit = disk.units(value) p_value = _get_cached_partitions(device, unit)[number][name] p_value = disk.units(p_value)[0] min_value = value - OVERLAPPING_ERROR max_value = value + OVERLAPPING_ERROR if not min_value <= p_value <= max_value: return False return True
def _get_first_overlapping_partition(device, start): ''' Return the first partition that contains the start point. ''' # Check if there is a partition in the system that start at # specified point. value, unit = disk.units(start) value += OVERLAPPING_ERROR partitions = _get_cached_partitions(device, unit) partition_number = None partition_start = 0 for number, partition in partitions.items(): p_start = disk.units(partition['start'])[0] p_end = disk.units(partition['end'])[0] if p_start <= value <= p_end: if partition_number is None or partition_start < p_start: partition_number = number partition_start = p_start return partition_number
def prepare_partition_data(partitions): '''Helper function to prepare the patition data from the pillar.''' # Validate and normalize the `partitions` pillar. The state will # expect a dictionary with this schema: # # partitions_normalized = { # '/dev/sda': { # 'label': 'gpt', # 'pmbr_boot': False, # 'partitions': [ # { # 'part_id': '/dev/sda1', # 'part_type': 'primary' # 'fs_type': 'ext2', # 'flags': ['esp'], # 'start': '0MB', # 'end': '100%', # }, # ], # }, # } is_uefi = __grains__['efi'] # Get the fallback values for label and initial_gap config = partitions.get('config', {}) global_label = config.get('label', LABEL) global_initial_gap = config.get('initial_gap', INITIAL_GAP) partitions_normalized = {} for device, device_info in partitions['devices'].items(): label = device_info.get('label', global_label) initial_gap = device_info.get('initial_gap', global_initial_gap) if initial_gap: initial_gap_num, units = disk.units(initial_gap, default=None) else: initial_gap_num, units = 0, None device_normalized = { 'label': label, 'pmbr_boot': label == 'gpt' and not is_uefi, 'partitions': [] } partitions_normalized[device] = device_normalized # Control the start of the next partition start_size = initial_gap_num # Flag to detect if `rest` size was used before rest = False for index, partition in enumerate(device_info.get('partitions', [])): # Detect if there is another partition after we create one # that complete the free space if rest: raise SaltInvocationError( 'Partition defined after one filled all the rest free ' 'space. Use `rest` only on the last partition.') # Validate the partition type part_type = partition.get('type') if part_type not in VALID_PART_TYPE: raise SaltInvocationError( 'Partition type {} not recognized'.format(part_type)) # If part_id is not given, we can create a partition name # based on the position of the partition and the name of # the device # # TODO(aplanas) The partition number will be deduced, so # the require section in mkfs_partition will fail part_id = '{}{}{}'.format( device, 'p' if __salt__['filters.is_raid'](device) else '', partitions.get('number', index + 1)) part_id = partition.get('id', part_id) # For parted we usually need to set a ext2 filesystem # type, except for SWAP or UEFI fs_type = { 'swap': 'linux-swap', 'efi': 'fat16' }.get(part_type, 'ext2') # Check if we are changing units inside the device if partition['size'] == 'rest': rest = True # If units is not set, we default to '%' units = units or '%' start = '{}{}'.format(start_size, units) end = '100%' else: size, size_units = disk.units(partition['size']) if units and size_units and units != size_units: raise SaltInvocationError( 'Units needs to be the same for the partitions inside ' 'a device. Found {} but expected {}. Note that ' '`initial_gap` is also considered.'.format( size_units, units)) # If units and size_units is not set, we default to UNITS units = units or size_units or UNITS start = '{}{}'.format(start_size, units) end = '{}{}'.format(start_size + size, units) start_size += size flags = None if part_type in ('raid', 'lvm'): flags = [part_type] elif part_type == 'boot' and label == 'gpt' and not is_uefi: flags = ['bios_grub'] elif part_type == 'efi' and label == 'gpt' and is_uefi: flags = ['esp'] device_normalized['partitions'].append({ 'part_id': part_id, # TODO(aplanas) If msdos we need to create extended # and logical 'part_type': 'primary', 'fs_type': fs_type, 'start': start, 'end': end, 'flags': flags, }) return partitions_normalized
def _get_partition_number(device, part_type, start, end): ''' Return a partition number for a [start, end] range and a partition type. If the range is allocated and the partition type match, return the partition number. If the type do not match but is a logical partition inside an extended one, return the next partition number. If the range is not allocated, return the next partition number. ''' unit = disk.units(start)[1] partitions = _get_cached_partitions(device, unit) # Check if there is a partition in the system that start or # containst the start point number = _get_first_overlapping_partition(device, start) if number: if partitions[number]['type'] == part_type: return number elif not (partitions[number]['type'] == 'extended' and part_type == 'logical'): raise EnumerateException('Do not overlap partitions') def __primary_partition_free_slot(partitions, label): if label == 'msdos': max_primary = 4 else: max_primary = 1024 for i in range(1, max_primary + 1): i = str(i) if i not in partitions: return i # The partition is not already there, we guess the next number label = _get_cached_info(device)['partition table'] if part_type == 'primary': candidate = __primary_partition_free_slot(partitions, label) if not candidate: raise EnumerateException('No free slot for primary partition') return candidate elif part_type == 'extended': if label == 'gpt': raise EnumerateException('Extended partitions not allowed in gpt') if 'extended' in (info['type'] for info in partitions.values()): raise EnumerateException('Already found a extended partition') candidate = __primary_partition_free_slot(partitions, label) if not candidate: raise EnumerateException('No free slot for extended partition') return candidate elif part_type == 'logical': if label == 'gpt': raise EnumerateException('Extended partitions not allowed in gpt') if 'extended' not in (part['type'] for part in partitions.values()): raise EnumerateException('Missing extended partition') candidate = max( (int(part['number']) for part in partitions.values() if part['type'] == 'logical'), default=4) return str(candidate + 1)
def _get_partition_number(device, part_type, start, end): """ Return a partition number for a [start, end] range and a partition type. If the range is allocated and the partition type match, return the partition number. If the type do not match but is a logical partition inside an extended one, return the next partition number. If the range is not allocated, return the next partition number. """ unit = disk.units(start)[1] partitions = _get_cached_partitions(device, unit) # Check if there is a partition in the system that start or # containst the start point number = _get_first_overlapping_partition(device, start) if number: if partitions[number]["type"] == part_type: return number elif not (partitions[number]["type"] == "extended" and part_type == "logical"): raise EnumerateException("Do not overlap partitions") def __primary_partition_free_slot(partitions, label): if label == "msdos": max_primary = 4 else: max_primary = 1024 for i in range(1, max_primary + 1): i = str(i) if i not in partitions: return i # The partition is not already there, we guess the next number label = _get_cached_info(device)["partition table"] if part_type == "primary": candidate = __primary_partition_free_slot(partitions, label) if not candidate: raise EnumerateException("No free slot for primary partition") return candidate elif part_type == "extended": if label == "gpt": raise EnumerateException("Extended partitions not allowed in gpt") if "extended" in (info["type"] for info in partitions.values()): raise EnumerateException("Already found a extended partition") candidate = __primary_partition_free_slot(partitions, label) if not candidate: raise EnumerateException("No free slot for extended partition") return candidate elif part_type == "logical": if label == "gpt": raise EnumerateException("Extended partitions not allowed in gpt") if "extended" not in (part["type"] for part in partitions.values()): raise EnumerateException("Missing extended partition") candidate = max( (int(part["number"]) for part in partitions.values() if part["type"] == "logical"), default=4, ) return str(candidate + 1)
def prepare_partition_data(partitions): """Helper function to prepare the patition data from the pillar.""" # Validate and normalize the `partitions` pillar. The state will # expect a dictionary with this schema: # # partitions_normalized = { # '/dev/sda': { # 'label': 'gpt', # 'pmbr_boot': False, # 'partitions': [ # { # 'part_id': '/dev/sda1', # 'part_type': 'primary' # 'fs_type': 'ext2', # 'flags': ['esp'], # 'start': '0MB', # 'end': '100%', # }, # ], # }, # } is_uefi = __grains__["efi"] # Get the fallback values for label and initial_gap config = partitions.get("config", {}) global_label = config.get("label", LABEL) global_initial_gap = config.get("initial_gap", INITIAL_GAP) partitions_normalized = {} for device, device_info in partitions["devices"].items(): label = device_info.get("label", global_label) initial_gap = device_info.get("initial_gap", global_initial_gap) if initial_gap: initial_gap_num, units = disk.units(initial_gap, default=None) else: initial_gap_num, units = 0, None device_normalized = { "label": label, "pmbr_boot": label == "gpt" and not is_uefi, "partitions": [], } partitions_normalized[device] = device_normalized # Control the start of the next partition start_size = initial_gap_num # Flag to detect if `rest` size was used before rest = False for index, partition in enumerate(device_info.get("partitions", [])): # Detect if there is another partition after we create one # that complete the free space if rest: raise SaltInvocationError( "Partition defined after one filled all the rest free " "space. Use `rest` only on the last partition." ) # Validate the partition type part_type = partition.get("type") if part_type not in VALID_PART_TYPE: raise SaltInvocationError( "Partition type {} not recognized".format(part_type) ) # If part_id is not given, we can create a partition name # based on the position of the partition and the name of # the device # # TODO(aplanas) The partition number will be deduced, so # the require section in mkfs_partition will fail part_id = "{}{}{}".format( device, "p" if __salt__["filters.is_raid"](device) else "", partitions.get("number", index + 1), ) part_id = partition.get("id", part_id) # For parted we usually need to set a ext2 filesystem # type, except for SWAP or UEFI fs_type = {"swap": "linux-swap", "efi": "fat16"}.get(part_type, "ext2") # Check if we are changing units inside the device if partition["size"] == "rest": rest = True # If units is not set, we default to '%' units = units or "%" start = "{}{}".format(start_size, units) end = "100%" else: size, size_units = disk.units(partition["size"]) if units and size_units and units != size_units: raise SaltInvocationError( "Units needs to be the same for the partitions inside " "a device. Found {} but expected {}. Note that " "`initial_gap` is also considered.".format(size_units, units) ) # If units and size_units is not set, we default to UNITS units = units or size_units or UNITS start = "{}{}".format(start_size, units) end = "{}{}".format(start_size + size, units) start_size += size flags = None if part_type in ("raid", "lvm"): flags = [part_type] elif part_type == "boot" and label == "gpt" and not is_uefi: flags = ["bios_grub"] elif part_type == "efi" and label == "gpt" and is_uefi: flags = ["esp"] device_normalized["partitions"].append( { "part_id": part_id, # TODO(aplanas) If msdos we need to create extended # and logical "part_type": "primary", "fs_type": fs_type, "start": start, "end": end, "flags": flags, } ) return partitions_normalized