Refactor disk partitioner code from ironic and use ironic-lib.

The disk partitioner related code from ironic/common/disk_partitioner.py
and ironic/drivers/modules/deploy_utils.py is being moved to ironic-lib.
The code in ironic needs to be removed and use ironic-lib to perform
disk related activities.

Change-Id: I7b1b1d8b45b947a7b5715f3c6ab49d84e84b6b90
This commit is contained in:
Faizan Barmawer 2015-05-20 01:16:37 -07:00
parent 7b13f3b0a6
commit 7c138f01a0
12 changed files with 421 additions and 1632 deletions

View File

@ -908,18 +908,6 @@
# Options defined in ironic.drivers.modules.deploy_utils
#
# Size of EFI system partition in MiB when configuring UEFI
# systems for local boot. (integer value)
#efi_system_partition_size=200
# Block size to use when writing to the nodes disk. (string
# value)
#dd_block_size=1M
# Maximum attempts to verify an iSCSI connection is active,
# sleeping 1 second between attempts. (integer value)
#iscsi_verify_attempts=3
# ironic-conductor node's HTTP server URL. Example:
# http://192.1.2.3:8080 (string value)
# Deprecated group/name - [pxe]/http_url
@ -956,7 +944,7 @@
[disk_partitioner]
#
# Options defined in ironic.common.disk_partitioner
# Options defined in ironic_lib.disk_partitioner
#
# After Ironic has completed creating the partition table, it
@ -972,6 +960,25 @@
#check_device_max_retries=20
[disk_utils]
#
# Options defined in ironic_lib.disk_utils
#
# Size of EFI system partition in MiB when configuring UEFI
# systems for local boot. (integer value)
#efi_system_partition_size=200
# Block size to use when writing to the nodes disk. (string
# value)
#dd_block_size=1M
# Maximum attempts to verify an iSCSI connection is active,
# sleeping 1 second between attempts. (integer value)
#iscsi_verify_attempts=3
[drac]
#
@ -1281,6 +1288,18 @@
#sensor_method=ipmitool
[ironic_lib]
#
# Options defined in ironic_lib.utils
#
# Command that is prefixed to commands that are run as root.
# If not specified, no commands are run as root. (string
# value)
#root_helper=sudo ironic-rootwrap /etc/ironic/rootwrap.conf
[keystone]
#

View File

@ -0,0 +1,19 @@
# An ironic-lib.filters to be used with rootwrap command.
# The following commands should be used in filters for disk manipulation.
# This file should be owned by (and only-writeable by) the root user.
[Filters]
# ironic_lib/disk_utils.py
blkid: CommandFilter, blkid, root
blockdev: CommandFilter, blockdev, root
hexdump: CommandFilter, hexdump, root
qemu-img: CommandFilter, qemu-img, root
# ironic_lib/utils.py
mkswap: CommandFilter, mkswap, root
mkfs: CommandFilter, mkfs, root
dd: CommandFilter, dd, root
# ironic_lib/disk_partitioner.py
fuser: CommandFilter, fuser, root
parted: CommandFilter, parted, root

View File

@ -4,9 +4,6 @@
[Filters]
# ironic/drivers/modules/deploy_utils.py
iscsiadm: CommandFilter, iscsiadm, root
blkid: CommandFilter, blkid, root
blockdev: CommandFilter, blockdev, root
hexdump: CommandFilter, hexdump, root
# ironic/common/utils.py
mkswap: CommandFilter, mkswap, root
@ -14,7 +11,3 @@ mkfs: CommandFilter, mkfs, root
mount: CommandFilter, mount, root
umount: CommandFilter, umount, root
dd: CommandFilter, dd, root
# ironic/common/disk_partitioner.py
fuser: CommandFilter, fuser, root
parted: CommandFilter, parted, root

View File

@ -1,226 +0,0 @@
# Copyright 2014 Red Hat, Inc.
# All Rights Reserved.
#
# 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 re
from oslo_concurrency import processutils
from oslo_config import cfg
from oslo_log import log as logging
from oslo_service import loopingcall
from ironic.common import exception
from ironic.common.i18n import _
from ironic.common.i18n import _LW
from ironic.common import utils
opts = [
cfg.IntOpt('check_device_interval',
default=1,
help=_('After Ironic has completed creating the partition '
'table, it continues to check for activity on the '
'attached iSCSI device status at this interval prior '
'to copying the image to the node, in seconds')),
cfg.IntOpt('check_device_max_retries',
default=20,
help=_('The maximum number of times to check that the device '
'is not accessed by another process. If the device is '
'still busy after that, the disk partitioning will be '
'treated as having failed.')),
]
CONF = cfg.CONF
opt_group = cfg.OptGroup(name='disk_partitioner',
title='Options for the disk partitioner')
CONF.register_group(opt_group)
CONF.register_opts(opts, opt_group)
LOG = logging.getLogger(__name__)
class DiskPartitioner(object):
def __init__(self, device, disk_label='msdos', alignment='optimal'):
"""A convenient wrapper around the parted tool.
:param device: The device path.
:param disk_label: The type of the partition table. Valid types are:
"bsd", "dvh", "gpt", "loop", "mac", "msdos",
"pc98", or "sun".
:param alignment: Set alignment for newly created partitions.
Valid types are: none, cylinder, minimal and
optimal.
"""
self._device = device
self._disk_label = disk_label
self._alignment = alignment
self._partitions = []
self._fuser_pids_re = re.compile(r'((\d)+\s*)+')
def _exec(self, *args):
# NOTE(lucasagomes): utils.execute() is already a wrapper on top
# of processutils.execute() which raises specific
# exceptions. It also logs any failure so we don't
# need to log it again here.
utils.execute('parted', '-a', self._alignment, '-s', self._device,
'--', 'unit', 'MiB', *args, check_exit_code=[0],
use_standard_locale=True, run_as_root=True)
def add_partition(self, size, part_type='primary', fs_type='',
bootable=False):
"""Add a partition.
:param size: The size of the partition in MiB.
:param part_type: The type of the partition. Valid values are:
primary, logical, or extended.
:param fs_type: The filesystem type. Valid types are: ext2, fat32,
fat16, HFS, linux-swap, NTFS, reiserfs, ufs.
If blank (''), it will create a Linux native
partition (83).
:param bootable: Boolean value; whether the partition is bootable
or not.
:returns: The partition number.
"""
# NOTE(jlvillal): This function has been moved to ironic-lib. And is
# planned to be deleted here. If need to modify this function, please
# also do the same modification in ironic-lib
self._partitions.append({'size': size,
'type': part_type,
'fs_type': fs_type,
'bootable': bootable})
return len(self._partitions)
def get_partitions(self):
"""Get the partitioning layout.
:returns: An iterator with the partition number and the
partition layout.
"""
# NOTE(jlvillal): This function has been moved to ironic-lib. And is
# planned to be deleted here. If need to modify this function, please
# also do the same modification in ironic-lib
return enumerate(self._partitions, 1)
def _wait_for_disk_to_become_available(self, retries, max_retries, pids,
stderr):
# NOTE(jlvillal): This function has been moved to ironic-lib. And is
# planned to be deleted here. If need to modify this function, please
# also do the same modification in ironic-lib
retries[0] += 1
if retries[0] > max_retries:
raise loopingcall.LoopingCallDone()
try:
# NOTE(ifarkas): fuser returns a non-zero return code if none of
# the specified files is accessed
out, err = utils.execute('fuser', self._device,
check_exit_code=[0, 1], run_as_root=True)
if not out and not err:
raise loopingcall.LoopingCallDone()
else:
if err:
stderr[0] = err
if out:
pids_match = re.search(self._fuser_pids_re, out)
pids[0] = pids_match.group()
except processutils.ProcessExecutionError as exc:
LOG.warning(_LW('Failed to check the device %(device)s with fuser:'
' %(err)s'), {'device': self._device, 'err': exc})
def commit(self):
"""Write to the disk."""
# NOTE(jlvillal): This function has been moved to ironic-lib. And is
# planned to be deleted here. If need to modify this function, please
# also do the same modification in ironic-lib
LOG.debug("Committing partitions to disk.")
cmd_args = ['mklabel', self._disk_label]
# NOTE(lucasagomes): Lead in with 1MiB to allow room for the
# partition table itself.
start = 1
for num, part in self.get_partitions():
end = start + part['size']
cmd_args.extend(['mkpart', part['type'], part['fs_type'],
str(start), str(end)])
if part['bootable']:
cmd_args.extend(['set', str(num), 'boot', 'on'])
start = end
self._exec(*cmd_args)
retries = [0]
pids = ['']
fuser_err = ['']
interval = CONF.disk_partitioner.check_device_interval
max_retries = CONF.disk_partitioner.check_device_max_retries
timer = loopingcall.FixedIntervalLoopingCall(
self._wait_for_disk_to_become_available,
retries, max_retries, pids, fuser_err)
timer.start(interval=interval).wait()
if retries[0] > max_retries:
if pids[0]:
raise exception.InstanceDeployFailure(
_('Disk partitioning failed on device %(device)s. '
'Processes with the following PIDs are holding it: '
'%(pids)s. Time out waiting for completion.')
% {'device': self._device, 'pids': pids[0]})
else:
raise exception.InstanceDeployFailure(
_('Disk partitioning failed on device %(device)s. Fuser '
'exited with "%(fuser_err)s". Time out waiting for '
'completion.')
% {'device': self._device, 'fuser_err': fuser_err[0]})
_PARTED_PRINT_RE = re.compile(r"^(\d+):([\d\.]+)MiB:"
"([\d\.]+)MiB:([\d\.]+)MiB:(\w*)::(\w*)")
def list_partitions(device):
"""Get partitions information from given device.
:param device: The device path.
:returns: list of dictionaries (one per partition) with keys:
number, start, end, size (in MiB), filesystem, flags
"""
# NOTE(jlvillal): This function has been moved to ironic-lib. And is
# planned to be deleted here. If need to modify this function, please also
# do the same modification in ironic-lib
output = utils.execute(
'parted', '-s', '-m', device, 'unit', 'MiB', 'print',
use_standard_locale=True, run_as_root=True)[0]
if isinstance(output, bytes):
output = output.decode("utf-8")
lines = [line for line in output.split('\n') if line.strip()][2:]
# Example of line: 1:1.00MiB:501MiB:500MiB:ext4::boot
fields = ('number', 'start', 'end', 'size', 'filesystem', 'flags')
result = []
for line in lines:
match = _PARTED_PRINT_RE.match(line)
if match is None:
LOG.warning(_LW("Partition information from parted for device "
"%(device)s does not match "
"expected format: %(line)s"),
dict(device=device, line=line))
continue
# Cast int fields to ints (some are floats and we round them down)
groups = [int(float(x)) if i < 4 else x
for i, x in enumerate(match.groups())]
result.append(dict(zip(fields, groups)))
return result

View File

@ -14,38 +14,28 @@
# under the License.
import base64
import contextlib
import gzip
import math
import os
import re
import shutil
import socket
import stat
import tempfile
import time
from ironic_lib import disk_utils
from oslo_concurrency import processutils
from oslo_config import cfg
from oslo_log import log as logging
from oslo_serialization import jsonutils
from oslo_utils import excutils
from oslo_utils import units
import requests
import six
from six.moves.urllib import parse
from ironic.common import dhcp_factory
from ironic.common import disk_partitioner
from ironic.common import exception
from ironic.common.glance_service import service_utils
from ironic.common.i18n import _
from ironic.common.i18n import _LE
from ironic.common.i18n import _LI
from ironic.common.i18n import _LW
from ironic.common import image_service
from ironic.common import images
from ironic.common import keystone
from ironic.common import states
from ironic.common import utils
@ -57,17 +47,6 @@ from ironic import objects
deploy_opts = [
cfg.IntOpt('efi_system_partition_size',
default=200,
help=_('Size of EFI system partition in MiB when configuring '
'UEFI systems for local boot.')),
cfg.StrOpt('dd_block_size',
default='1M',
help=_('Block size to use when writing to the nodes disk.')),
cfg.IntOpt('iscsi_verify_attempts',
default=3,
help=_('Maximum attempts to verify an iSCSI connection is '
'active, sleeping 1 second between attempts.')),
cfg.StrOpt('http_url',
help='ironic-conductor node\'s HTTP server URL. '
'Example: http://192.1.2.3:8080',
@ -95,6 +74,14 @@ deploy_opts = [
CONF = cfg.CONF
CONF.register_opts(deploy_opts, group='deploy')
# TODO(Faizan): Move this logic to common/utils.py and deprecate
# rootwrap_config.
# This is required to set the default value of ironic_lib option
# only if rootwrap_config does not contain the default value.
if CONF.rootwrap_config != '/etc/ironic/rootwrap.conf':
root_helper = 'sudo ironic-rootwrap %s' % CONF.rootwrap_config
CONF.set_default('root_helper', root_helper, 'ironic_lib')
LOG = logging.getLogger(__name__)
VALID_ROOT_DEVICE_HINTS = set(('size', 'model', 'wwn', 'serial', 'vendor',
@ -151,7 +138,7 @@ def check_file_system_for_iscsi_device(portal_address,
check_dir = "/dev/disk/by-path/ip-%s:%s-iscsi-%s-lun-1" % (portal_address,
portal_port,
target_iqn)
total_checks = CONF.deploy.iscsi_verify_attempts
total_checks = CONF.disk_utils.iscsi_verify_attempts
for attempt in range(total_checks):
if os.path.exists(check_dir):
break
@ -171,7 +158,7 @@ def verify_iscsi_connection(target_iqn):
"""Verify iscsi connection."""
LOG.debug("Checking for iSCSI target to become active.")
for attempt in range(CONF.deploy.iscsi_verify_attempts):
for attempt in range(CONF.disk_utils.iscsi_verify_attempts):
out, _err = utils.execute('iscsiadm',
'-m', 'node',
'-S',
@ -183,10 +170,10 @@ def verify_iscsi_connection(target_iqn):
LOG.debug("iSCSI connection not active. Rechecking. Attempt "
"%(attempt)d out of %(total)d",
{"attempt": attempt + 1,
"total": CONF.deploy.iscsi_verify_attempts})
"total": CONF.disk_utils.iscsi_verify_attempts})
else:
msg = _("iSCSI connection did not become active after attempting to "
"verify %d times.") % CONF.deploy.iscsi_verify_attempts
"verify %d times.") % CONF.disk_utils.iscsi_verify_attempts
LOG.error(msg)
raise exception.InstanceDeployFailure(msg)
@ -231,163 +218,6 @@ def delete_iscsi(portal_address, portal_port, target_iqn):
delay_on_retry=True)
def get_disk_identifier(dev):
"""Get the disk identifier from the disk being exposed by the ramdisk.
This disk identifier is appended to the pxe config which will then be
used by chain.c32 to detect the correct disk to chainload. This is helpful
in deployments to nodes with multiple disks.
http://www.syslinux.org/wiki/index.php/Comboot/chain.c32#mbr:
:param dev: Path for the already populated disk device.
:returns The Disk Identifier.
"""
# NOTE(jlvillal): This function has been moved to ironic-lib. And is
# planned to be deleted here. If need to modify this function, please also
# do the same modification in ironic-lib
disk_identifier = utils.execute('hexdump', '-s', '440', '-n', '4',
'-e', '''\"0x%08x\"''',
dev,
run_as_root=True,
check_exit_code=[0],
attempts=5,
delay_on_retry=True)
return disk_identifier[0]
def make_partitions(dev, root_mb, swap_mb, ephemeral_mb,
configdrive_mb, node_uuid, commit=True,
boot_option="netboot", boot_mode="bios"):
"""Partition the disk device.
Create partitions for root, swap, ephemeral and configdrive on a
disk device.
:param root_mb: Size of the root partition in mebibytes (MiB).
:param swap_mb: Size of the swap partition in mebibytes (MiB). If 0,
no partition will be created.
:param ephemeral_mb: Size of the ephemeral partition in mebibytes (MiB).
If 0, no partition will be created.
:param configdrive_mb: Size of the configdrive partition in
mebibytes (MiB). If 0, no partition will be created.
:param commit: True/False. Default for this setting is True. If False
partitions will not be written to disk.
:param boot_option: Can be "local" or "netboot". "netboot" by default.
:param boot_mode: Can be "bios" or "uefi". "bios" by default.
:param node_uuid: Node's uuid. Used for logging.
:returns: A dictionary containing the partition type as Key and partition
path as Value for the partitions created by this method.
"""
# NOTE(jlvillal): This function has been moved to ironic-lib. And is
# planned to be deleted here. If need to modify this function, please also
# do the same modification in ironic-lib
LOG.debug("Starting to partition the disk device: %(dev)s "
"for node %(node)s",
{'dev': dev, 'node': node_uuid})
part_template = dev + '-part%d'
part_dict = {}
# For uefi localboot, switch partition table to gpt and create the efi
# system partition as the first partition.
if boot_mode == "uefi" and boot_option == "local":
dp = disk_partitioner.DiskPartitioner(dev, disk_label="gpt")
part_num = dp.add_partition(CONF.deploy.efi_system_partition_size,
fs_type='fat32',
bootable=True)
part_dict['efi system partition'] = part_template % part_num
else:
dp = disk_partitioner.DiskPartitioner(dev)
if ephemeral_mb:
LOG.debug("Add ephemeral partition (%(size)d MB) to device: %(dev)s "
"for node %(node)s",
{'dev': dev, 'size': ephemeral_mb, 'node': node_uuid})
part_num = dp.add_partition(ephemeral_mb)
part_dict['ephemeral'] = part_template % part_num
if swap_mb:
LOG.debug("Add Swap partition (%(size)d MB) to device: %(dev)s "
"for node %(node)s",
{'dev': dev, 'size': swap_mb, 'node': node_uuid})
part_num = dp.add_partition(swap_mb, fs_type='linux-swap')
part_dict['swap'] = part_template % part_num
if configdrive_mb:
LOG.debug("Add config drive partition (%(size)d MB) to device: "
"%(dev)s for node %(node)s",
{'dev': dev, 'size': configdrive_mb, 'node': node_uuid})
part_num = dp.add_partition(configdrive_mb)
part_dict['configdrive'] = part_template % part_num
# NOTE(lucasagomes): Make the root partition the last partition. This
# enables tools like cloud-init's growroot utility to expand the root
# partition until the end of the disk.
LOG.debug("Add root partition (%(size)d MB) to device: %(dev)s "
"for node %(node)s",
{'dev': dev, 'size': root_mb, 'node': node_uuid})
part_num = dp.add_partition(root_mb, bootable=(boot_option == "local" and
boot_mode == "bios"))
part_dict['root'] = part_template % part_num
if commit:
# write to the disk
dp.commit()
return part_dict
def is_block_device(dev):
"""Check whether a device is block or not."""
# NOTE(jlvillal): This function has been moved to ironic-lib. And is
# planned to be deleted here. If need to modify this function, please also
# do the same modification in ironic-lib
attempts = CONF.deploy.iscsi_verify_attempts
for attempt in range(attempts):
try:
s = os.stat(dev)
except OSError as e:
LOG.debug("Unable to stat device %(dev)s. Attempt %(attempt)d "
"out of %(total)d. Error: %(err)s",
{"dev": dev, "attempt": attempt + 1,
"total": attempts, "err": e})
time.sleep(1)
else:
return stat.S_ISBLK(s.st_mode)
msg = _("Unable to stat device %(dev)s after attempting to verify "
"%(attempts)d times.") % {'dev': dev, 'attempts': attempts}
LOG.error(msg)
raise exception.InstanceDeployFailure(msg)
def dd(src, dst):
"""Execute dd from src to dst."""
# NOTE(jlvillal): This function has been moved to ironic-lib. And is
# planned to be deleted here. If need to modify this function, please also
# do the same modification in ironic-lib
utils.dd(src, dst, 'bs=%s' % CONF.deploy.dd_block_size, 'oflag=direct')
def populate_image(src, dst):
# NOTE(jlvillal): This function has been moved to ironic-lib. And is
# planned to be deleted here. If need to modify this function, please also
# do the same modification in ironic-lib
data = images.qemu_img_info(src)
if data.file_format == 'raw':
dd(src, dst)
else:
images.convert_image(src, dst, 'raw', True)
def block_uuid(dev):
"""Get UUID of a block device."""
# NOTE(jlvillal): This function has been moved to ironic-lib. And is
# planned to be deleted here. If need to modify this function, please also
# do the same modification in ironic-lib
out, _err = utils.execute('blkid', '-s', 'UUID', '-o', 'value', dev,
run_as_root=True,
check_exit_code=[0])
return out.strip()
def _replace_lines_in_file(path, regex_pattern, replacement):
with open(path) as f:
lines = f.readlines()
@ -468,278 +298,6 @@ def get_dev(address, port, iqn, lun):
return dev
def get_image_mb(image_path, virtual_size=True):
"""Get size of an image in Megabyte."""
# NOTE(jlvillal): This function has been moved to ironic-lib. And is
# planned to be deleted here. If need to modify this function, please also
# do the same modification in ironic-lib
mb = 1024 * 1024
if not virtual_size:
image_byte = os.path.getsize(image_path)
else:
image_byte = images.converted_size(image_path)
# round up size to MB
image_mb = int((image_byte + mb - 1) / mb)
return image_mb
def get_dev_block_size(dev):
"""Get the device size in 512 byte sectors."""
# NOTE(jlvillal): This function has been moved to ironic-lib. And is
# planned to be deleted here. If need to modify this function, please also
# do the same modification in ironic-lib
block_sz, cmderr = utils.execute('blockdev', '--getsz', dev,
run_as_root=True, check_exit_code=[0])
return int(block_sz)
def destroy_disk_metadata(dev, node_uuid):
"""Destroy metadata structures on node's disk.
Ensure that node's disk appears to be blank without zeroing the entire
drive. To do this we will zero:
- the first 18KiB to clear MBR / GPT data
- the last 18KiB to clear GPT and other metadata like: LVM, veritas,
MDADM, DMRAID, ...
"""
# NOTE(jlvillal): This function has been moved to ironic-lib. And is
# planned to be deleted here. If need to modify this function, please also
# do the same modification in ironic-lib
# NOTE(NobodyCam): This is needed to work around bug:
# https://bugs.launchpad.net/ironic/+bug/1317647
LOG.debug("Start destroy disk metadata for node %(node)s.",
{'node': node_uuid})
try:
utils.dd('/dev/zero', dev, 'bs=512', 'count=36')
except processutils.ProcessExecutionError as err:
with excutils.save_and_reraise_exception():
LOG.error(_LE("Failed to erase beginning of disk for node "
"%(node)s. Command: %(command)s. Error: %(error)s."),
{'node': node_uuid,
'command': err.cmd,
'error': err.stderr})
# now wipe the end of the disk.
# get end of disk seek value
try:
block_sz = get_dev_block_size(dev)
except processutils.ProcessExecutionError as err:
with excutils.save_and_reraise_exception():
LOG.error(_LE("Failed to get disk block count for node %(node)s. "
"Command: %(command)s. Error: %(error)s."),
{'node': node_uuid,
'command': err.cmd,
'error': err.stderr})
else:
seek_value = block_sz - 36
try:
utils.dd('/dev/zero', dev, 'bs=512', 'count=36',
'seek=%d' % seek_value)
except processutils.ProcessExecutionError as err:
with excutils.save_and_reraise_exception():
LOG.error(_LE("Failed to erase the end of the disk on node "
"%(node)s. Command: %(command)s. "
"Error: %(error)s."),
{'node': node_uuid,
'command': err.cmd,
'error': err.stderr})
LOG.info(_LI("Disk metadata on %(dev)s successfully destroyed for node "
"%(node)s"), {'dev': dev, 'node': node_uuid})
def _get_configdrive(configdrive, node_uuid):
"""Get the information about size and location of the configdrive.
:param configdrive: Base64 encoded Gzipped configdrive content or
configdrive HTTP URL.
:param node_uuid: Node's uuid. Used for logging.
:raises: InstanceDeployFailure if it can't download or decode the
config drive.
:returns: A tuple with the size in MiB and path to the uncompressed
configdrive file.
"""
# NOTE(jlvillal): This function has been moved to ironic-lib. And is
# planned to be deleted here. If need to modify this function, please also
# do the same modification in ironic-lib
# Check if the configdrive option is a HTTP URL or the content directly
is_url = utils.is_http_url(configdrive)
if is_url:
try:
data = requests.get(configdrive).content
except requests.exceptions.RequestException as e:
raise exception.InstanceDeployFailure(
_("Can't download the configdrive content for node %(node)s "
"from '%(url)s'. Reason: %(reason)s") %
{'node': node_uuid, 'url': configdrive, 'reason': e})
else:
data = configdrive
try:
data = six.BytesIO(base64.b64decode(data))
except TypeError:
error_msg = (_('Config drive for node %s is not base64 encoded '
'or the content is malformed.') % node_uuid)
if is_url:
error_msg += _(' Downloaded from "%s".') % configdrive
raise exception.InstanceDeployFailure(error_msg)
configdrive_file = tempfile.NamedTemporaryFile(delete=False,
prefix='configdrive',
dir=CONF.tempdir)
configdrive_mb = 0
with gzip.GzipFile('configdrive', 'rb', fileobj=data) as gunzipped:
try:
shutil.copyfileobj(gunzipped, configdrive_file)
except EnvironmentError as e:
# Delete the created file
utils.unlink_without_raise(configdrive_file.name)
raise exception.InstanceDeployFailure(
_('Encountered error while decompressing and writing '
'config drive for node %(node)s. Error: %(exc)s') %
{'node': node_uuid, 'exc': e})
else:
# Get the file size and convert to MiB
configdrive_file.seek(0, os.SEEK_END)
bytes_ = configdrive_file.tell()
configdrive_mb = int(math.ceil(float(bytes_) / units.Mi))
finally:
configdrive_file.close()
return (configdrive_mb, configdrive_file.name)
def work_on_disk(dev, root_mb, swap_mb, ephemeral_mb, ephemeral_format,
image_path, node_uuid, preserve_ephemeral=False,
configdrive=None, boot_option="netboot",
boot_mode="bios"):
"""Create partitions and copy an image to the root partition.
:param dev: Path for the device to work on.
:param root_mb: Size of the root partition in megabytes.
:param swap_mb: Size of the swap partition in megabytes.
:param ephemeral_mb: Size of the ephemeral partition in megabytes. If 0,
no ephemeral partition will be created.
:param ephemeral_format: The type of file system to format the ephemeral
partition.
:param image_path: Path for the instance's disk image.
:param node_uuid: node's uuid. Used for logging.
:param preserve_ephemeral: If True, no filesystem is written to the
ephemeral block device, preserving whatever content it had (if the
partition table has not changed).
:param configdrive: Optional. Base64 encoded Gzipped configdrive content
or configdrive HTTP URL.
:param boot_option: Can be "local" or "netboot". "netboot" by default.
:param boot_mode: Can be "bios" or "uefi". "bios" by default.
:returns: a dictionary containing the following keys:
'root uuid': UUID of root partition
'efi system partition uuid': UUID of the uefi system partition
(if boot mode is uefi).
NOTE: If key exists but value is None, it means partition doesn't
exist.
"""
# NOTE(jlvillal): This function has been moved to ironic-lib. And is
# planned to be deleted here. If need to modify this function, please also
# do the same modification in ironic-lib
# the only way for preserve_ephemeral to be set to true is if we are
# rebuilding an instance with --preserve_ephemeral.
commit = not preserve_ephemeral
# now if we are committing the changes to disk clean first.
if commit:
destroy_disk_metadata(dev, node_uuid)
try:
# If requested, get the configdrive file and determine the size
# of the configdrive partition
configdrive_mb = 0
configdrive_file = None
if configdrive:
configdrive_mb, configdrive_file = _get_configdrive(configdrive,
node_uuid)
part_dict = make_partitions(dev, root_mb, swap_mb, ephemeral_mb,
configdrive_mb, node_uuid,
commit=commit,
boot_option=boot_option,
boot_mode=boot_mode)
LOG.info(_LI("Successfully completed the disk device"
" %(dev)s partitioning for node %(node)s"),
{'dev': dev, "node": node_uuid})
ephemeral_part = part_dict.get('ephemeral')
swap_part = part_dict.get('swap')
configdrive_part = part_dict.get('configdrive')
root_part = part_dict.get('root')
if not is_block_device(root_part):
raise exception.InstanceDeployFailure(
_("Root device '%s' not found") % root_part)
for part in ('swap', 'ephemeral', 'configdrive',
'efi system partition'):
part_device = part_dict.get(part)
LOG.debug("Checking for %(part)s device (%(dev)s) on node "
"%(node)s.",
{'part': part, 'dev': part_device, 'node': node_uuid})
if part_device and not is_block_device(part_device):
raise exception.InstanceDeployFailure(
_("'%(partition)s' device '%(part_device)s' not found") %
{'partition': part, 'part_device': part_device})
# If it's a uefi localboot, then we have created the efi system
# partition. Create a fat filesystem on it.
if boot_mode == "uefi" and boot_option == "local":
efi_system_part = part_dict.get('efi system partition')
utils.mkfs('vfat', efi_system_part, 'efi-part')
if configdrive_part:
# Copy the configdrive content to the configdrive partition
dd(configdrive_file, configdrive_part)
LOG.info(_LI("Configdrive for node %(node)s successfully copied "
"onto partition %(partition)s"),
{'node': node_uuid, 'partition': configdrive_part})
finally:
# If the configdrive was requested make sure we delete the file
# after copying the content to the partition
if configdrive_file:
utils.unlink_without_raise(configdrive_file)
populate_image(image_path, root_part)
LOG.info(_LI("Image for %(node)s successfully populated"),
{'node': node_uuid})
if swap_part:
utils.mkfs('swap', swap_part, 'swap1')
LOG.info(_LI("Swap partition %(swap)s successfully formatted "
"for node %(node)s"),
{'swap': swap_part, 'node': node_uuid})
if ephemeral_part and not preserve_ephemeral:
utils.mkfs(ephemeral_format, ephemeral_part, "ephemeral0")
LOG.info(_LI("Ephemeral partition %(ephemeral)s successfully "
"formatted for node %(node)s"),
{'ephemeral': ephemeral_part, 'node': node_uuid})
uuids_to_return = {
'root uuid': root_part,
'efi system partition uuid': part_dict.get('efi system partition')
}
try:
for part, part_dev in uuids_to_return.items():
if part_dev:
uuids_to_return[part] = block_uuid(part_dev)
except processutils.ProcessExecutionError:
with excutils.save_and_reraise_exception():
LOG.error(_LE("Failed to detect %s"), part)
return uuids_to_return
def deploy_partition_image(
address, port, iqn, lun, image_path,
root_mb, swap_mb, ephemeral_mb, ephemeral_format, node_uuid,
@ -775,7 +333,7 @@ def deploy_partition_image(
NOTE: If key exists but value is None, it means partition doesn't
exist.
"""
image_mb = get_image_mb(image_path)
image_mb = disk_utils.get_image_mb(image_path)
if image_mb > root_mb:
msg = (_('Root partition is too small for requested image. Image '
'virtual size: %(image_mb)d MB, Root size: %(root_mb)d MB')
@ -783,7 +341,7 @@ def deploy_partition_image(
raise exception.InstanceDeployFailure(msg)
with _iscsi_setup_and_handle_errors(address, port, iqn, lun) as dev:
uuid_dict_returned = work_on_disk(
uuid_dict_returned = disk_utils.work_on_disk(
dev, root_mb, swap_mb, ephemeral_mb, ephemeral_format, image_path,
node_uuid, preserve_ephemeral=preserve_ephemeral,
configdrive=configdrive, boot_option=boot_option,
@ -808,8 +366,8 @@ def deploy_disk_image(address, port, iqn, lun,
"""
with _iscsi_setup_and_handle_errors(address, port, iqn,
lun) as dev:
populate_image(image_path, dev)
disk_identifier = get_disk_identifier(dev)
disk_utils.populate_image(image_path, dev)
disk_identifier = disk_utils.get_disk_identifier(dev)
return {'disk identifier': disk_identifier}
@ -826,7 +384,7 @@ def _iscsi_setup_and_handle_errors(address, port, iqn, lun):
dev = get_dev(address, port, iqn, lun)
discovery(address, port)
login_iscsi(address, port, iqn)
if not is_block_device(dev):
if not disk_utils.is_block_device(dev):
raise exception.InstanceDeployFailure(_("Parent device '%s' not found")
% dev)
try:

View File

@ -15,6 +15,7 @@
import os
from ironic_lib import disk_utils
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import fileutils
@ -241,7 +242,7 @@ def check_image_size(task):
"""
i_info = parse_instance_info(task.node)
image_path = _get_image_file_path(task.node.uuid)
image_mb = deploy_utils.get_image_mb(image_path)
image_mb = disk_utils.get_image_mb(image_path)
root_mb = 1024 * int(i_info['root_gb'])
if image_mb > root_mb:
msg = (_('Root partition is too small for requested image. Image '

View File

@ -1,198 +0,0 @@
# Copyright 2014 Red Hat, Inc.
# All Rights Reserved.
#
# 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 eventlet
import mock
from testtools.matchers import HasLength
from ironic.common import disk_partitioner
from ironic.common import exception
from ironic.common import utils
from ironic.tests import base
@mock.patch.object(eventlet.greenthread, 'sleep', lambda seconds: None)
class DiskPartitionerTestCase(base.TestCase):
def test_add_partition(self):
dp = disk_partitioner.DiskPartitioner('/dev/fake')
dp.add_partition(1024)
dp.add_partition(512, fs_type='linux-swap')
dp.add_partition(2048, bootable=True)
expected = [(1, {'bootable': False,
'fs_type': '',
'type': 'primary',
'size': 1024}),
(2, {'bootable': False,
'fs_type': 'linux-swap',
'type': 'primary',
'size': 512}),
(3, {'bootable': True,
'fs_type': '',
'type': 'primary',
'size': 2048})]
partitions = [(n, p) for n, p in dp.get_partitions()]
self.assertThat(partitions, HasLength(3))
self.assertEqual(expected, partitions)
@mock.patch.object(disk_partitioner.DiskPartitioner, '_exec',
autospec=True)
@mock.patch.object(utils, 'execute', autospec=True)
def test_commit(self, mock_utils_exc, mock_disk_partitioner_exec):
dp = disk_partitioner.DiskPartitioner('/dev/fake')
fake_parts = [(1, {'bootable': False,
'fs_type': 'fake-fs-type',
'type': 'fake-type',
'size': 1}),
(2, {'bootable': True,
'fs_type': 'fake-fs-type',
'type': 'fake-type',
'size': 1})]
with mock.patch.object(dp, 'get_partitions', autospec=True) as mock_gp:
mock_gp.return_value = fake_parts
mock_utils_exc.return_value = (None, None)
dp.commit()
mock_disk_partitioner_exec.assert_called_once_with(
mock.ANY, 'mklabel', 'msdos',
'mkpart', 'fake-type', 'fake-fs-type', '1', '2',
'mkpart', 'fake-type', 'fake-fs-type', '2', '3',
'set', '2', 'boot', 'on')
mock_utils_exc.assert_called_once_with(
'fuser', '/dev/fake', run_as_root=True, check_exit_code=[0, 1])
@mock.patch.object(disk_partitioner.DiskPartitioner, '_exec',
autospec=True)
@mock.patch.object(utils, 'execute', autospec=True)
def test_commit_with_device_is_busy_once(self, mock_utils_exc,
mock_disk_partitioner_exec):
dp = disk_partitioner.DiskPartitioner('/dev/fake')
fake_parts = [(1, {'bootable': False,
'fs_type': 'fake-fs-type',
'type': 'fake-type',
'size': 1}),
(2, {'bootable': True,
'fs_type': 'fake-fs-type',
'type': 'fake-type',
'size': 1})]
fuser_outputs = iter([("/dev/fake: 10000 10001", None), (None, None)])
with mock.patch.object(dp, 'get_partitions', autospec=True) as mock_gp:
mock_gp.return_value = fake_parts
mock_utils_exc.side_effect = fuser_outputs
dp.commit()
mock_disk_partitioner_exec.assert_called_once_with(
mock.ANY, 'mklabel', 'msdos',
'mkpart', 'fake-type', 'fake-fs-type', '1', '2',
'mkpart', 'fake-type', 'fake-fs-type', '2', '3',
'set', '2', 'boot', 'on')
mock_utils_exc.assert_called_with(
'fuser', '/dev/fake', run_as_root=True, check_exit_code=[0, 1])
self.assertEqual(2, mock_utils_exc.call_count)
@mock.patch.object(disk_partitioner.DiskPartitioner, '_exec',
autospec=True)
@mock.patch.object(utils, 'execute', autospec=True)
def test_commit_with_device_is_always_busy(self, mock_utils_exc,
mock_disk_partitioner_exec):
dp = disk_partitioner.DiskPartitioner('/dev/fake')
fake_parts = [(1, {'bootable': False,
'fs_type': 'fake-fs-type',
'type': 'fake-type',
'size': 1}),
(2, {'bootable': True,
'fs_type': 'fake-fs-type',
'type': 'fake-type',
'size': 1})]
with mock.patch.object(dp, 'get_partitions', autospec=True) as mock_gp:
mock_gp.return_value = fake_parts
mock_utils_exc.return_value = ("/dev/fake: 10000 10001", None)
self.assertRaises(exception.InstanceDeployFailure, dp.commit)
mock_disk_partitioner_exec.assert_called_once_with(
mock.ANY, 'mklabel', 'msdos',
'mkpart', 'fake-type', 'fake-fs-type', '1', '2',
'mkpart', 'fake-type', 'fake-fs-type', '2', '3',
'set', '2', 'boot', 'on')
mock_utils_exc.assert_called_with(
'fuser', '/dev/fake', run_as_root=True, check_exit_code=[0, 1])
self.assertEqual(20, mock_utils_exc.call_count)
@mock.patch.object(disk_partitioner.DiskPartitioner, '_exec',
autospec=True)
@mock.patch.object(utils, 'execute', autospec=True)
def test_commit_with_device_disconnected(self, mock_utils_exc,
mock_disk_partitioner_exec):
dp = disk_partitioner.DiskPartitioner('/dev/fake')
fake_parts = [(1, {'bootable': False,
'fs_type': 'fake-fs-type',
'type': 'fake-type',
'size': 1}),
(2, {'bootable': True,
'fs_type': 'fake-fs-type',
'type': 'fake-type',
'size': 1})]
with mock.patch.object(dp, 'get_partitions', autospec=True) as mock_gp:
mock_gp.return_value = fake_parts
mock_utils_exc.return_value = (None, "Specified filename /dev/fake"
" does not exist.")
self.assertRaises(exception.InstanceDeployFailure, dp.commit)
mock_disk_partitioner_exec.assert_called_once_with(
mock.ANY, 'mklabel', 'msdos',
'mkpart', 'fake-type', 'fake-fs-type', '1', '2',
'mkpart', 'fake-type', 'fake-fs-type', '2', '3',
'set', '2', 'boot', 'on')
mock_utils_exc.assert_called_with(
'fuser', '/dev/fake', run_as_root=True, check_exit_code=[0, 1])
self.assertEqual(20, mock_utils_exc.call_count)
@mock.patch.object(utils, 'execute', autospec=True)
class ListPartitionsTestCase(base.TestCase):
def test_correct(self, execute_mock):
output = """
BYT;
/dev/sda:500107862016B:scsi:512:4096:msdos:ATA HGST HTS725050A7:;
1:1.00MiB:501MiB:500MiB:ext4::boot;
2:501MiB:476940MiB:476439MiB:::;
"""
expected = [
{'number': 1, 'start': 1, 'end': 501, 'size': 500,
'filesystem': 'ext4', 'flags': 'boot'},
{'number': 2, 'start': 501, 'end': 476940, 'size': 476439,
'filesystem': '', 'flags': ''},
]
execute_mock.return_value = (output, '')
result = disk_partitioner.list_partitions('/dev/fake')
self.assertEqual(expected, result)
execute_mock.assert_called_once_with(
'parted', '-s', '-m', '/dev/fake', 'unit', 'MiB', 'print',
use_standard_locale=True, run_as_root=True)
@mock.patch.object(disk_partitioner.LOG, 'warning', autospec=True)
def test_incorrect(self, log_mock, execute_mock):
output = """
BYT;
/dev/sda:500107862016B:scsi:512:4096:msdos:ATA HGST HTS725050A7:;
1:XX1076MiB:---:524MiB:ext4::boot;
"""
execute_mock.return_value = (output, '')
self.assertEqual([], disk_partitioner.list_partitions('/dev/fake'))
self.assertEqual(1, log_mock.call_count)

File diff suppressed because it is too large Load Diff

View File

@ -18,6 +18,7 @@
import os
import tempfile
from ironic_lib import disk_utils
import mock
from oslo_config import cfg
from oslo_utils import fileutils
@ -344,7 +345,7 @@ class IscsiDeployMethodsTestCase(db_base.DbTestCase):
mgr_utils.mock_the_extension_manager(driver="fake_pxe")
self.node = obj_utils.create_test_node(self.context, **n)
@mock.patch.object(deploy_utils, 'get_image_mb', autospec=True)
@mock.patch.object(disk_utils, 'get_image_mb', autospec=True)
def test_check_image_size(self, get_image_mb_mock):
get_image_mb_mock.return_value = 1000
with task_manager.acquire(self.context, self.node.uuid,
@ -354,7 +355,7 @@ class IscsiDeployMethodsTestCase(db_base.DbTestCase):
get_image_mb_mock.assert_called_once_with(
iscsi_deploy._get_image_file_path(task.node.uuid))
@mock.patch.object(deploy_utils, 'get_image_mb', autospec=True)
@mock.patch.object(disk_utils, 'get_image_mb', autospec=True)
def test_check_image_size_fails(self, get_image_mb_mock):
get_image_mb_mock.return_value = 1025
with task_manager.acquire(self.context, self.node.uuid,

View File

@ -0,0 +1,20 @@
---
upgrade:
- |
Adds new configuration [ironic_lib]root_helper, to specify
the command that is prefixed to commands that are run as root.
Defaults to using the rootwrap config file at
/etc/ironic/rootwrap.conf.
- |
Moves these configuration options from [deploy] group to the
new [disk_utils] group: efi_system_partition_size, dd_block_size
and iscsi_verify_attempts.
deprecations:
- |
The following configuration options have been moved to
the [disk_utils] group; they are deprecated from the
[deploy] group: efi_system_partition_size, dd_block_size and
iscsi_verify_attempts.
other:
- Code related to disk partitioning was moved to
ironic-lib.

View File

@ -13,6 +13,7 @@ paramiko>=1.13.0
python-neutronclient>=2.6.0
python-glanceclient>=0.18.0
python-keystoneclient!=1.8.0,>=1.6.0
ironic-lib>=0.5.0
python-swiftclient>=2.2.0
pytz>=2013.6
stevedore>=1.5.0 # Apache-2.0

View File

@ -1,2 +1,2 @@
export IRONIC_CONFIG_GENERATOR_EXTRA_LIBRARIES='oslo.db oslo.messaging oslo.middleware.cors keystonemiddleware.auth_token oslo.concurrency oslo.policy oslo.log oslo.service.service oslo.service.periodic_task oslo.service.sslutils'
export IRONIC_CONFIG_GENERATOR_EXTRA_MODULES=
export IRONIC_CONFIG_GENERATOR_EXTRA_MODULES='ironic_lib.disk_utils ironic_lib.disk_partitioner ironic_lib.utils'