28
from oslo.utils import excutils
29
from oslo.utils import units
30
29
from oslo_concurrency import processutils
31
30
from oslo_config import cfg
31
from oslo_serialization import jsonutils
32
from oslo_utils import excutils
33
from oslo_utils import units
36
from six.moves.urllib import parse
35
38
from ironic.common import disk_partitioner
36
39
from ironic.common import exception
37
40
from ironic.common.i18n import _
38
41
from ironic.common.i18n import _LE
42
from ironic.common.i18n import _LW
39
43
from ironic.common import images
40
44
from ironic.common import states
41
45
from ironic.common import utils
42
46
from ironic.conductor import utils as manager_utils
47
from ironic.drivers.modules import agent_client
43
48
from ironic.drivers.modules import image_cache
49
from ironic.drivers import utils as driver_utils
50
from ironic import objects
44
51
from ironic.openstack.common import log as logging
55
cfg.IntOpt('efi_system_partition_size',
57
help='Size of EFI system partition in MiB when configuring '
58
'UEFI systems for local boot.'),
48
59
cfg.StrOpt('dd_block_size',
50
61
help='Block size to use when writing to the nodes disk.'),
157
199
delay_on_retry=True)
202
def get_disk_identifier(dev):
203
"""Get the disk identifier from the disk being exposed by the ramdisk.
205
This disk identifier is appended to the pxe config which will then be
206
used by chain.c32 to detect the correct disk to chainload. This is helpful
207
in deployments to nodes with multiple disks.
209
http://www.syslinux.org/wiki/index.php/Comboot/chain.c32#mbr:
211
:param dev: Path for the already populated disk device.
212
:returns The Disk Identifier.
214
disk_identifier = utils.execute('hexdump', '-s', '440', '-n', '4',
215
'-e', '''\"0x%08x\"''',
221
return disk_identifier[0]
160
224
def make_partitions(dev, root_mb, swap_mb, ephemeral_mb,
161
configdrive_mb, commit=True):
225
configdrive_mb, commit=True, boot_option="netboot",
162
227
"""Partition the disk device.
164
229
Create partitions for root, swap, ephemeral and configdrive on a
248
338
return out.strip()
251
def switch_pxe_config(path, root_uuid, boot_mode):
252
"""Switch a pxe config from deployment mode to service mode."""
341
def _replace_lines_in_file(path, regex_pattern, replacement):
253
342
with open(path) as f:
254
343
lines = f.readlines()
345
compiled_pattern = re.compile(regex_pattern)
346
with open(path, 'w') as f:
348
line = compiled_pattern.sub(replacement, line)
352
def _replace_root_uuid(path, root_uuid):
255
353
root = 'UUID=%s' % root_uuid
256
rre = re.compile(r'\{\{ ROOT \}\}')
354
pattern = r'\{\{ ROOT \}\}'
355
_replace_lines_in_file(path, pattern, root)
358
def _replace_boot_line(path, boot_mode, is_whole_disk_image):
359
if is_whole_disk_image:
360
boot_disk_type = 'boot_whole_disk'
362
boot_disk_type = 'boot_partition'
258
364
if boot_mode == 'uefi':
259
dre = re.compile('^default=.*$')
260
boot_line = 'default=boot'
365
pattern = '^default=.*$'
366
boot_line = 'default=%s' % boot_disk_type
262
368
pxe_cmd = 'goto' if CONF.pxe.ipxe_enabled else 'default'
263
dre = re.compile('^%s .*$' % pxe_cmd)
264
boot_line = '%s boot' % pxe_cmd
266
with open(path, 'w') as f:
268
line = rre.sub(root, line)
269
line = dre.sub(boot_line, line)
369
pattern = '^%s .*$' % pxe_cmd
370
boot_line = '%s %s' % (pxe_cmd, boot_disk_type)
372
_replace_lines_in_file(path, pattern, boot_line)
375
def _replace_disk_identifier(path, disk_identifier):
376
pattern = r'\{\{ DISK_IDENTIFIER \}\}'
377
_replace_lines_in_file(path, pattern, disk_identifier)
380
def switch_pxe_config(path, root_uuid_or_disk_id, boot_mode,
381
is_whole_disk_image):
382
"""Switch a pxe config from deployment mode to service mode.
384
:param path: path to the pxe config file in tftpboot.
385
:param root_uuid_or_disk_id: root uuid in case of partition image or
386
disk_id in case of whole disk image.
387
:param boot_mode: if boot mode is uefi or bios.
388
:param is_whole_disk_image: if the image is a whole disk image or not.
390
if not is_whole_disk_image:
391
_replace_root_uuid(path, root_uuid_or_disk_id)
393
_replace_disk_identifier(path, root_uuid_or_disk_id)
395
_replace_boot_line(path, boot_mode, is_whole_disk_image)
273
398
def notify(address, port):
492
626
populate_image(image_path, root_part)
629
mkfs(dev=swap_part, fs='swap', label='swap1')
497
631
if ephemeral_part and not preserve_ephemeral:
498
mkfs_ephemeral(ephemeral_part, ephemeral_format)
632
mkfs(dev=ephemeral_part, fs=ephemeral_format, label="ephemeral0")
635
'root uuid': root_part,
636
'efi system partition uuid': part_dict.get('efi system partition')
501
root_uuid = block_uuid(root_part)
640
for part, part_dev in six.iteritems(uuids_to_return):
642
uuids_to_return[part] = block_uuid(part_dev)
502
644
except processutils.ProcessExecutionError:
503
645
with excutils.save_and_reraise_exception():
504
LOG.error(_LE("Failed to detect root device UUID."))
509
def deploy(address, port, iqn, lun, image_path,
646
LOG.error(_LE("Failed to detect %s"), part)
648
return uuids_to_return
651
def deploy_partition_image(address, port, iqn, lun, image_path,
510
652
root_mb, swap_mb, ephemeral_mb, ephemeral_format, node_uuid,
511
preserve_ephemeral=False, configdrive=None):
512
"""All-in-one function to deploy a node.
653
preserve_ephemeral=False, configdrive=None,
654
boot_option="netboot", boot_mode="bios"):
655
"""All-in-one function to deploy a partition image to a node.
514
657
:param address: The iSCSI IP address.
515
658
:param port: The iSCSI port number.
528
671
partition table has not changed).
529
672
:param configdrive: Optional. Base64 encoded Gzipped configdrive content
530
673
or configdrive HTTP URL.
531
:returns: the UUID of the root partition.
674
:param boot_option: Can be "local" or "netboot". "netboot" by default.
675
:param boot_mode: Can be "bios" or "uefi". "bios" by default.
676
:returns: a dictionary containing the UUID of root partition and efi system
677
partition (if boot mode is uefi).
679
with _iscsi_setup_and_handle_errors(address, port, iqn,
680
lun, image_path) as dev:
681
image_mb = get_image_mb(image_path)
682
if image_mb > root_mb:
685
uuid_dict_returned = work_on_disk(
686
dev, root_mb, swap_mb, ephemeral_mb, ephemeral_format, image_path,
687
node_uuid, preserve_ephemeral=preserve_ephemeral,
688
configdrive=configdrive, boot_option=boot_option,
691
return uuid_dict_returned
694
def deploy_disk_image(address, port, iqn, lun,
695
image_path, node_uuid):
696
"""All-in-one function to deploy a whole disk image to a node.
698
:param address: The iSCSI IP address.
699
:param port: The iSCSI port number.
700
:param iqn: The iSCSI qualified name.
701
:param lun: The iSCSI logical unit number.
702
:param image_path: Path for the instance's disk image.
703
:param node_uuid: node's uuid. Used for logging. Currently not in use
704
by this function but could be used in the future.
705
:returns: a dictionary containing the disk identifier for the disk.
707
with _iscsi_setup_and_handle_errors(address, port, iqn,
708
lun, image_path) as dev:
709
populate_image(image_path, dev)
710
disk_identifier = get_disk_identifier(dev)
712
return {'disk identifier': disk_identifier}
715
@contextlib.contextmanager
716
def _iscsi_setup_and_handle_errors(address, port, iqn, lun,
718
"""Function that yields an iSCSI target device to work on.
720
:param address: The iSCSI IP address.
721
:param port: The iSCSI port number.
722
:param iqn: The iSCSI qualified name.
723
:param lun: The iSCSI logical unit number.
724
:param image_path: Path for the instance's disk image.
533
726
dev = get_dev(address, port, iqn, lun)
534
image_mb = get_image_mb(image_path)
535
if image_mb > root_mb:
537
727
discovery(address, port)
538
728
login_iscsi(address, port, iqn)
729
if not is_block_device(dev):
730
raise exception.InstanceDeployFailure(_("Parent device '%s' not found")
540
root_uuid = work_on_disk(dev, root_mb, swap_mb, ephemeral_mb,
541
ephemeral_format, image_path, node_uuid,
542
preserve_ephemeral=preserve_ephemeral,
543
configdrive=configdrive)
544
734
except processutils.ProcessExecutionError as err:
545
735
with excutils.save_and_reraise_exception():
546
736
LOG.error(_LE("Deploy to address %s failed."), address)
652
840
for port in task.ports:
653
841
if port.extra.get('vif_port_id'):
654
842
return port.address
845
def parse_instance_info_capabilities(node):
846
"""Parse the instance_info capabilities.
848
One way of having these capabilities set is via Nova, where the
849
capabilities are defined in the Flavor extra_spec and passed to
850
Ironic by the Nova Ironic driver.
852
NOTE: Although our API fully supports JSON fields, to maintain the
853
backward compatibility with Juno the Nova Ironic driver is sending
856
:param node: a single Node.
857
:raises: InvalidParameterValue if the capabilities string is not a
858
dictionary or is malformed.
859
:returns: A dictionary with the capabilities if found, otherwise an
864
error_msg = (_('Error parsing capabilities from Node %s instance_info '
865
'field. A dictionary or a "jsonified" dictionary is '
866
'expected.') % node.uuid)
867
raise exception.InvalidParameterValue(error_msg)
869
capabilities = node.instance_info.get('capabilities', {})
870
if isinstance(capabilities, six.string_types):
872
capabilities = jsonutils.loads(capabilities)
873
except (ValueError, TypeError):
876
if not isinstance(capabilities, dict):
882
def agent_get_clean_steps(task):
883
"""Get the list of clean steps from the agent.
885
#TODO(JoshNang) move to BootInterface
887
:param task: a TaskManager object containing the node
888
:raises: NodeCleaningFailure if the agent returns invalid results
889
:returns: A list of clean step dictionaries
891
client = _get_agent_client()
892
ports = objects.Port.list_by_node_id(
893
task.context, task.node.id)
894
result = client.get_clean_steps(task.node, ports).get('command_result')
896
if ('clean_steps' not in result or
897
'hardware_manager_version' not in result):
898
raise exception.NodeCleaningFailure(_(
899
'get_clean_steps for node %(node)s returned invalid result:'
900
' %(result)s') % ({'node': task.node.uuid, 'result': result}))
902
driver_info = task.node.driver_internal_info
903
driver_info['hardware_manager_version'] = result[
904
'hardware_manager_version']
905
task.node.driver_internal_info = driver_info
908
# Clean steps looks like {'HardwareManager': [{step1},{steps2}..]..}
909
# Flatten clean steps into one list
910
steps_list = [step for step_list in
911
result['clean_steps'].values()
912
for step in step_list]
913
# Filter steps to only return deploy steps
914
steps = [step for step in steps_list
915
if step.get('interface') == 'deploy']
919
def agent_execute_clean_step(task, step):
920
"""Execute a clean step asynchronously on the agent.
922
#TODO(JoshNang) move to BootInterface
924
:param task: a TaskManager object containing the node
925
:param step: a clean step dictionary to execute
926
:raises: NodeCleaningFailure if the agent does not return a command status
927
:returns: states.CLEANING to signify the step will be completed async
929
client = _get_agent_client()
930
ports = objects.Port.list_by_node_id(
931
task.context, task.node.id)
932
result = client.execute_clean_step(step, task.node, ports)
933
if not result.get('command_status'):
934
raise exception.NodeCleaningFailure(_(
935
'Agent on node %(node)s returned bad command result: '
936
'%(result)s') % {'node': task.node.uuid,
937
'result': result.get('command_error')})
938
return states.CLEANING
941
def try_set_boot_device(task, device, persistent=True):
942
"""Tries to set the boot device on the node.
944
This method tries to set the boot device on the node to the given
945
boot device. Under uefi boot mode, setting of boot device may differ
946
between different machines. IPMI does not work for setting boot
947
devices in uefi mode for certain machines. This method ignores the
948
expected IPMI failure for uefi boot mode and just logs a message.
949
In error cases, it is expected the operator has to manually set the
950
node to boot from the correct device.
952
:param task: a TaskManager object containing the node
953
:param device: the boot device
954
:param persistent: Whether to set the boot device persistently
955
:raises: Any exception from set_boot_device except IPMIFailure
956
(setting of boot device using ipmi is expected to fail).
959
manager_utils.node_set_boot_device(task, device,
960
persistent=persistent)
961
except exception.IPMIFailure:
962
if driver_utils.get_node_capability(task.node,
963
'boot_mode') == 'uefi':
964
LOG.warning(_LW("ipmitool is unable to set boot device while "
965
"the node %s is in UEFI boot mode. Please set "
966
"the boot device manually.") % task.node.uuid)
971
def parse_root_device_hints(node):
972
"""Parse the root_device property of a node.
974
Parse the root_device property of a node and make it a flat string
975
to be passed via the PXE config.
977
:param node: a single Node.
978
:returns: A flat string with the following format
979
opt1=value1,opt2=value2. Or None if the
980
Node contains no hints.
981
:raises: InvalidParameterValue, if some information is invalid.
984
root_device = node.properties.get('root_device')
988
# Find invalid hints for logging
989
invalid_hints = set(root_device) - VALID_ROOT_DEVICE_HINTS
991
raise exception.InvalidParameterValue(
992
_('The hints "%(invalid_hints)s" are invalid. '
993
'Valid hints are: "%(valid_hints)s"') %
994
{'invalid_hints': ', '.join(invalid_hints),
995
'valid_hints': ', '.join(VALID_ROOT_DEVICE_HINTS)})
997
if 'size' in root_device:
999
int(root_device['size'])
1001
raise exception.InvalidParameterValue(
1002
_('Root device hint "size" is not an integer value.'))
1005
for key, value in root_device.items():
1006
# NOTE(lucasagomes): We can't have spaces in the PXE config
1007
# file, so we are going to url/percent encode the value here
1008
# and decode on the other end.
1009
if isinstance(value, six.string_types):
1010
value = value.strip()
1011
value = parse.quote(value)
1013
hints.append("%s=%s" % (key, value))
1015
return ','.join(hints)