bareon/bareon/drivers/deploy/generic.py

657 lines
23 KiB
Python

#
# Copyright 2016 Cray 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 abc
import itertools
import json
import os
import re
from oslo_config import cfg
from oslo_log import log as logging
import six
from bareon.actions import configdrive
from bareon.actions import partitioning
from bareon.drivers.data import generic as generic_data_driver
from bareon.drivers.deploy.base import BaseDeployDriver
from bareon.drivers.deploy import mixins
from bareon import errors
from bareon import objects
from bareon.utils import block_device
from bareon.utils import fs as fu
from bareon.utils import grub as gu
from bareon.utils import lvm as lu
from bareon.utils import md as mu
from bareon.utils import partition as pu
from bareon.utils import utils
opts = [
cfg.StrOpt(
'udev_rules_dir',
default='/etc/udev/rules.d',
help='Path where to store actual rules for udev daemon',
),
cfg.StrOpt(
'udev_rules_lib_dir',
default='/lib/udev/rules.d',
help='Path where to store default rules for udev daemon',
),
cfg.StrOpt(
'udev_rename_substr',
default='.renamedrule',
help='Substring to which file extension .rules be renamed',
),
cfg.StrOpt(
'udev_empty_rule',
default='empty_rule',
help='Correct empty rule for udev daemon',
),
cfg.IntOpt(
'grub_timeout',
default=5,
help='Timeout in secs for GRUB'
),
]
CONF = cfg.CONF
CONF.register_opts(opts)
LOG = logging.getLogger(__name__)
# TODO(lobur): This driver mostly copies nailgun driver. Need to merge them.
class GenericDeployDriver(BaseDeployDriver, mixins.MountableMixin):
def do_reboot(self):
LOG.debug('--- Rebooting node (do_reboot) ---')
utils.execute('reboot')
def do_provisioning(self):
LOG.debug('--- Provisioning (do_provisioning) ---')
self.do_partitioning()
self.do_configdrive()
avoid_storage_modification = False
if isinstance(self.driver, generic_data_driver.GenericDataDriver):
if self.driver.partitions_policy == 'verify':
avoid_storage_modification = True
if not avoid_storage_modification:
map(self.do_install_os, self.driver.get_os_ids())
if self.driver.is_multiboot:
self.do_multiboot_bootloader()
LOG.debug('--- Provisioning END (do_provisioning) ---')
def do_partitioning(self):
LOG.debug('--- Partitioning disks (do_partitioning) ---')
try:
storage_claim = self.driver.storage_claim
except AttributeError:
# TODO(dbogun): completely replace deprecated partitioning code
PolicyPartitioner(self.driver).partition()
else:
handlers_map = {
'clean': PartitionPolicyClean,
'verify': PartitionPolicyVerify,
'nailgun_legacy': PartitionPolicyNailgun}
handler = handlers_map[self.driver.partitions_policy]
handler = handler(self, storage_claim)
handler()
LOG.debug('--- Partitioning disks END (do_partitioning) ---')
def do_configdrive(self):
configdrive.ConfigDriveAction(self.driver).execute()
def do_copyimage(self):
raise NotImplementedError
def do_install_os(self, os_id):
self.do_copyimage(os_id)
os_dir = '/tmp/target'
with self.mount_target(os_dir, os_id, treat_mtab=False):
if not self.driver.is_multiboot:
self.do_singleboot_bootloader(os_dir, os_id)
self.do_generate_fstab(os_dir, os_id)
def do_generate_fstab(self, os_path, os_id):
mount2uuid = self._mount2uuid(os_id, check_root=False)
if not os.path.exists(os.path.join(os_path, 'etc')):
LOG.info('Can\'t create fstab for {} image'.format(os_id))
return
with open(os.path.join(os_path, 'etc/fstab'), 'wb') as f:
for fs in self.driver.partition_scheme.fs_by_os_id(os_id):
f.write('{enabled}UUID={uuid} {mount} {fs} {options} '
'0 0\n'.format(enabled='' if fs.fstab_enabled else '#',
uuid=mount2uuid[fs.mount],
mount=fs.mount,
fs=fs.type,
options=fs.fstab_options))
def do_multiboot_bootloader(self):
install_devices = [d.name for d in self.driver.partition_scheme.parteds
if d.install_bootloader]
mount_dir = '/tmp/target'
with self._mount_bootloader(mount_dir) as uuid:
gu.grub2_install(install_devices, boot_root=mount_dir)
self._generate_boot_info(mount_dir, uuid)
def _mount2uuid(self, os_id, check_root=True):
mount2uuid = {}
for fs in self.driver.partition_scheme.fs_by_os_id(os_id):
mount2uuid[fs.mount] = pu.get_uuid(fs.device)
if check_root and '/' not in mount2uuid:
raise errors.WrongPartitionSchemeError(
'Error: device with / mountpoint has not been found')
return mount2uuid
def _uuid2osid(self, check_root=True):
uuid2image = {}
for os_id in self.driver.get_os_ids():
mount2uuid = self._mount2uuid(os_id, check_root=check_root)
uuid = mount2uuid.get('/', '')
if uuid:
uuid2image[uuid] = os_id
return uuid2image
def _get_multiboot_boot_image(self):
return next((image for image in self.driver.image_scheme.images if
image.os_boot), None)
def do_singleboot_bootloader(self, chroot, os_id):
grub = self.driver.grub
try:
guessed_version = gu.guess_grub_version(chroot=chroot)
except errors.GrubUtilsError as ex:
LOG.warning('Grub detection failed. Error: {}'.format(ex))
guessed_version = -1
if guessed_version != grub.version:
grub.version = guessed_version
LOG.warning('Grub version differs from which the operating system '
'should have by default. Found version in image: '
'{0}'.format(guessed_version))
if grub.version == 1 and self.driver.is_multiboot:
LOG.warning('Grub1 is being used in a multiboot deployment, '
'thus it is not guaranteed that image name "{}" will '
'be discovered by os-prober and appear in the common '
'grub.cfg'.format(os_id))
install_devices = [d.name for d in self.driver.partition_scheme.parteds
if d.install_bootloader]
if grub.version == 1:
mount2uuid = self._mount2uuid(os_id)
grub.append_kernel_params('root=UUID=%s ' % mount2uuid['/'])
GRUB_INSTALLERS = {1: self._do_bootloader_grub1,
2: self._do_bootloader_grub2,
-1: self._do_bootloader_grub2_bundled}
GRUB_INSTALLERS[grub.version](grub, chroot, install_devices,
self.driver.boot_on_lvm)
def _do_bootloader_grub1(self, grub, chroot, install_devices,
lvm_boot=False):
if lvm_boot:
raise NotImplementedError("Grub 1 does not support boot from LVM.")
# TODO(kozhukalov): implement which kernel to use by default
# Currently only grub1_cfg accepts kernel and initrd parameters.
boot_device = self.driver.partition_scheme.boot_device(grub.version)
kernel = grub.kernel_name or gu.guess_kernel(chroot=chroot,
regexp=grub.kernel_regexp)
initrd = grub.initrd_name or gu.guess_initrd(chroot=chroot,
regexp=grub.initrd_regexp)
gu.grub1_cfg(kernel=kernel, initrd=initrd,
kernel_params=grub.kernel_params, chroot=chroot,
grub_timeout=CONF.grub_timeout)
gu.grub1_install(install_devices, boot_device, chroot=chroot)
def _do_bootloader_grub2(self, grub, chroot, install_devices,
lvm_boot=False):
try:
gu.grub2_cfg(kernel_params=grub.kernel_params, chroot=chroot,
grub_timeout=CONF.grub_timeout, lvm_boot=lvm_boot)
gu.grub2_install(install_devices, chroot=chroot, lvm_boot=lvm_boot)
except errors.ProcessExecutionError as ex:
LOG.warning('Tenant grub2 install failed. Error: {}'.format(ex))
LOG.warning('Trying to install using bundled grub2')
self._do_bootloader_grub2_bundled(grub, chroot, install_devices,
lvm_boot=lvm_boot)
def _do_bootloader_grub2_bundled(self, grub, chroot, install_devices,
lvm_boot=False):
gu.grub2_install(install_devices, boot_root=chroot, lvm_boot=lvm_boot)
gu.grub2_cfg_bundled(kernel_params=grub.kernel_params,
chroot=chroot, grub_timeout=CONF.grub_timeout,
lvm_boot=lvm_boot)
def _generate_boot_info(self, chroot, uuid=None):
def list_of_seq_unique_by_key(seq, key):
seen = set()
seen_add = seen.add
return [x for x in seq if
x[key] not in seen and not seen_add(x[key])]
regex = re.compile('menuentry \'(?P<name>[^\']+)\'.*?search '
'.*?(?P<uuid>[0-9a-f\-]{36}).*?linux(?:16)? '
'(?P<kernel>.*?) .*?initrd(?:16)? '
'(?P<initrd>[^\n]*)', re.M | re.DOTALL)
entries = '''
set timeout=1
insmod part_gpt
insmod ext2
'''
boot_entry = '''
menuentry '{name}'{{
search --no-floppy --fs-uuid --set=root {uuid}
linux {kernel} root=UUID={uuid} ro {kernel_params}
initrd {initrd}
}}
'''
boot_elements = []
os.environ['GRUB_DISABLE_SUBMENU'] = 'y'
os_prober_entries = utils.execute('/etc/grub.d/30_os-prober')[0]
uuid2osid = self._uuid2osid(check_root=False)
for index, element in enumerate(re.finditer(regex, os_prober_entries)):
os_id = uuid2osid.get(element.group('uuid'), '')
if not os_id:
continue
image = self.driver.image_scheme.get_os_root(os_id)
entries += boot_entry.format(**{
'name': element.group('name'),
'uuid': element.group('uuid'),
'kernel': element.group('kernel'),
'initrd': element.group('initrd'),
'kernel_params': self.driver.data.get('deploy_data', {}).get(
'kernel_params', '')
})
boot_elements.append({
'boot_name': element.group('name'),
'root_uuid': element.group('uuid'),
'os_id': os_id,
'image_name': image.image_name,
'image_uuid': image.image_uuid,
'grub_id': index,
})
boot_elements = list_of_seq_unique_by_key(boot_elements, 'root_uuid')
boot_image = self._get_multiboot_boot_image()
if boot_image:
root_uuid = self._mount2uuid(boot_image.os_id)['/']
boot_id = next((element['grub_id'] for element in boot_elements if
element['root_uuid'] == root_uuid), 0)
entries += 'set default={}'.format(boot_id)
with open(os.path.join(chroot, 'boot', 'grub2', 'grub.cfg'),
'w') as conf:
conf.write(entries)
result = {'elements': boot_elements,
'multiboot_partition': uuid,
'current_element': boot_id}
with open('/tmp/boot_entries.json', 'w') as boot_entries_file:
json.dump(result, boot_entries_file)
# FIXME(dbogun): deprecated due to NEWTCORE-360 fix
class PolicyPartitioner(object):
def __init__(self, driver):
self.driver = driver
self.partitioning = partitioning.PartitioningAction(self.driver)
def partition(self):
policy = self.driver.partitions_policy
LOG.debug("Using partitioning policy '%s'."
% self.driver.partitions_policy)
policy_handlers = {
"nailgun_legacy": self._handle_nailgun_legacy,
}
if policy not in policy_handlers:
raise errors.WrongPartitionPolicyError(
"'%s' policy is not one of known ones: %s"
% (policy, ', '.join(policy_handlers)))
policy_handlers[policy]()
def _handle_nailgun_legacy(self):
# Corresponds to nailgun behavior.
self.partitioning.execute()
@six.add_metaclass(abc.ABCMeta)
class AbstractPartitionPolicy(object):
space_allocation_accuracy = block_device.SizeUnit(1, 'MiB')
def __init__(self, deploy, storage):
self.deploy_driver = deploy
self.storage_claim = storage
self.dev_finder = block_device.DeviceFinder()
self.partition = self._make_partition_plan()
@abc.abstractmethod
def __call__(self):
pass
def dev_by_guid(self, guid):
needle = 'disk/by-partuuid/{}'.format(guid.lower())
dev_info = self.dev_finder('path', needle)
return dev_info['device']
def _make_partition_plan(self):
disks = {}
for claim in self.storage_claim.items_by_kind(
objects.block_device.Disk):
LOG.info('Make partition plan for "%s"', claim.dev)
disks[claim.dev] = self._disk_partition(claim)
return disks
def _disk_partition(self, claim):
disk = block_device.Disk.new_by_scan(claim.dev, partitions=False)
disk.allocate_accuracy = self.space_allocation_accuracy
remaining = None
from_tail = []
for idx, claim in enumerate(claim.items):
if claim.size.kind == claim.size.KIND_BIGGEST:
remaining = claim
from_tail = claim.items[idx + 1:]
break
self._apply_claim(disk, claim)
from_tail.reverse()
for claim in from_tail:
self._apply_claim(disk, claim, from_tail=True)
if remaining is not None:
self._apply_claim(disk, remaining)
return disk
def _lvm_vg_partition(self, claim):
vg = block_device.LVM.new_by_scan(claim.idnr, lv=False)
vg.allocate_accuracy = self.space_allocation_accuracy
remaining = None
for lv in claim.items_by_kind(objects.block_device.LVMlv):
if lv.size.kind == lv.size.KIND_BIGGEST:
remaining = lv
continue
self._apply_claim(vg, lv)
if remaining is not None:
self._apply_claim(vg, remaining)
return vg
def _handle_filesystems(self):
for claim in self.storage_claim.items_by_kind(
objects.block_device.Disk):
self._resolv_disk_partitions(claim)
for claim in self.storage_claim.items_by_kind(
objects.block_device.FileSystemMixin, recursion=True):
self._make_filesystem(claim)
def _resolv_disk_partitions(self, claim_disk):
partition_disk = self.partition[claim_disk.dev]
actual_disk = block_device.Disk.new_by_scan(claim_disk.dev)
fuzzy_factor = actual_disk.sizeunit_to_blocks(
self.space_allocation_accuracy)
claim_segments = []
actual_segments = []
for storage, target in (
(partition_disk, claim_segments),
(actual_disk, actual_segments)):
for segment in storage.segments:
if segment.kind != segment.KIND_BUSY:
continue
segment.set_fuzzy_cmp_factor(fuzzy_factor)
target.append(segment)
idx_iter = itertools.count()
for claim, actual in itertools.izip_longest(
claim_segments, actual_segments):
idx = next(idx_iter)
if claim == actual:
if isinstance(
claim.payload, objects.block_device.Partition):
claim.payload.guid = actual.payload.guid
continue
raise errors.PartitionSchemeMismatchError(
'Unable to resolv claim devices into physical devices. '
'Claim and physical devices partitions are different. '
'(dev={}, {}: {!r} != {!r})'.format(
claim_disk.dev, idx, claim, actual))
def _make_filesystem(self, claim):
if not claim.file_system:
return
if isinstance(claim, objects.block_device.Partition):
dev = self.dev_by_guid(claim.guid)
elif isinstance(claim, objects.block_device.MDRaid):
dev = claim.name
elif isinstance(claim, objects.block_device.LVMlv):
dev = claim.dev
else:
raise errors.InternalError(exc_info=False)
# FIXME(dbogun): label
fu.make_fs(claim.file_system, '', '', dev)
def _apply_claim(self, storage, claim, from_tail=False):
segment = claim.size(storage, from_tail=from_tail)
if isinstance(claim, objects.block_device.BlockDevice):
if claim.is_service:
segment.set_is_service()
segment.payload = claim
class PartitionPolicyClean(AbstractPartitionPolicy):
def __call__(self):
LOG.info('Apply "clean" partitions policy')
self._remove_all_compound_devices()
for dev in sorted(self.partition):
self._handle_disk(self.partition[dev])
# update dev finder after all changes to disks
self.dev_finder = block_device.DeviceFinder()
for md in self.storage_claim.items_by_kind(
objects.block_device.MDRaid):
self._handle_mdraid(md)
for vg in self.storage_claim.items_by_kind(objects.block_device.LVMvg):
self._handle_lvm(vg)
self._handle_filesystems()
def _remove_all_compound_devices(self):
mu.mdclean_all()
lu.lvremove_all()
lu.vgremove_all()
lu.pvremove_all()
def _handle_disk(self, disk):
gdisk = block_device.GDisk(disk.dev)
gdisk.zap()
try:
idx = itertools.count(1)
for segment in disk.segments:
if segment.is_free():
continue
partition = block_device.Partition.new_by_disk_segment(
segment, next(idx), segment.payload.guid_code)
partition.guid = segment.payload.guid
segment.payload.guid = gdisk.new(partition)
finally:
pu.reread_partitions(disk.dev)
def _handle_mdraid(self, md):
components = set()
for item in md.items:
components.add(self.dev_by_guid(item.guid))
mu.mdcreate(md.name, md.level, sorted(components))
def _handle_lvm(self, vg):
components = set()
for pv in vg.items_by_kind(objects.block_device.LVMpv):
dev = self.dev_by_guid(pv.guid)
components.add(dev)
args = {}
if pv.meta_size is not None:
args['metadatasize'] = pv.meta_size.size.in_unit(
'MiB').value_int
lu.pvcreate(dev, **args)
lu.vgcreate(vg.idnr, *sorted(components))
partition = self._lvm_vg_partition(vg)
for segment in partition.segments:
if segment.kind != segment.KIND_BUSY:
continue
lu.lvcreate(vg.idnr, segment.payload.name, segment.size)
def _resolv_disk_partitions(self, claim_dist):
"""Dummy to avoid already done operation
Actual disk partition resolv have happened in __call__ method, during
disks partitioning.
"""
class PartitionPolicyNailgun(PartitionPolicyClean):
_respect_keep_data = True
def __call__(self):
self._respect_keep_data = self._check_keep_data_claim()
if self._respect_keep_data:
LOG.debug('Some of fs has keep_data (preserve) flag, '
'skipping partitioning')
self._handle_filesystems()
return
super(PartitionPolicyNailgun, self).__call__()
def _check_keep_data_claim(self):
for item in itertools.chain(
self.storage_claim.items_by_kind(
objects.block_device.FileSystemMixin, recursion=True),
self.storage_claim.items_by_kind(objects.block_device.LVMvg)):
if not item.keep_data_flag:
continue
break
else:
return False
return True
def _make_filesystem(self, claim):
if self._respect_keep_data and claim.keep_data_flag:
return
super(PartitionPolicyNailgun, self)._make_filesystem(claim)
class PartitionPolicyVerify(AbstractPartitionPolicy):
_lvm_fuzzy_cmp_factor = 2
def __call__(self):
LOG.info('Apply "verify" partitions policy')
for dev in self.partition:
self._handle_disk(self.partition[dev])
for vg in self.storage_claim.items_by_kind(objects.block_device.LVMvg):
self._handle_lvm(vg)
self._handle_filesystems()
def _handle_disk(self, disk):
actual_disk = block_device.Disk.new_by_scan(disk.dev)
actual_partition = self._grab_storage_segments(actual_disk)
desired_partition = self._grab_storage_segments(disk)
if actual_partition == desired_partition:
return
self._report_mismatch(disk.dev, desired_partition, actual_partition)
def _handle_lvm(self, vg_claim):
try:
vg = block_device.LVM.new_by_scan(vg_claim.idnr)
actual_partition = self._grab_storage_segments(
vg, self._lvm_fuzzy_cmp_factor)
vg = self._lvm_vg_partition(vg_claim)
desired_partition = self._grab_storage_segments(
vg, self._lvm_fuzzy_cmp_factor)
except errors.VGNotFoundError:
raise errors.PartitionSchemeMismatchError(
'There is no LVMvg {}'.format(vg_claim.idnr))
if actual_partition == desired_partition:
return
self._report_mismatch(
vg_claim.idnr, desired_partition, actual_partition)
def _grab_storage_segments(self, storage, factor=None):
if factor is None:
factor = storage.sizeunit_to_blocks(self.space_allocation_accuracy)
result = []
for segment in storage.segments:
if segment.kind != segment.KIND_BUSY:
continue
segment.set_fuzzy_cmp_factor(factor)
result.append(segment)
return result
# TODO(dbogun): increase verbosity
def _report_mismatch(self, dev, desired, actual):
raise errors.PartitionSchemeMismatchError(
'Partition mismatch on {}'.format(dev))
def _make_filesystem(self, claim):
if claim.keep_data_flag:
return
super(PartitionPolicyVerify, self)._make_filesystem(claim)