bareon/bareon/drivers/deploy/generic.py

607 lines
25 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 json
import os
import re
from contextlib import contextmanager
from oslo_config import cfg
from bareon.drivers.deploy.base import BaseDeployDriver
from bareon import errors
from bareon.openstack.common import log as logging
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):
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()
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) ---')
PolicyPartitioner(self.driver).partition()
LOG.debug('--- Partitioning disks END (do_partitioning) ---')
def do_configdrive(self):
self.driver.create_configdrive()
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)
def _mount_target(self, mount_dir, os_id, pseudo=True, treat_mtab=True):
LOG.debug('Mounting target file systems: %s', mount_dir)
# Here we are going to mount all file systems in partition schema.
for fs in self.driver.partition_scheme.fs_sorted_by_depth(os_id):
if fs.mount == 'swap':
continue
mount = os.path.join(mount_dir, fs.mount.strip(os.sep))
utils.makedirs_if_not_exists(mount)
fu.mount_fs(fs.type, str(fs.device), mount)
if pseudo:
for path in ('/sys', '/dev', '/proc'):
utils.makedirs_if_not_exists(
os.path.join(mount_dir, path.strip(os.sep)))
fu.mount_bind(mount_dir, path)
if treat_mtab:
mtab = utils.execute('chroot', mount_dir, 'grep', '-v', 'rootfs',
'/proc/mounts')[0]
mtab_path = os.path.join(mount_dir, 'etc/mtab')
if os.path.islink(mtab_path):
os.remove(mtab_path)
with open(mtab_path, 'wb') as f:
f.write(mtab)
def _umount_target(self, mount_dir, os_id, pseudo=True):
LOG.debug('Umounting target file systems: %s', mount_dir)
if pseudo:
for path in ('/proc', '/dev', '/sys'):
fu.umount_fs(os.path.join(mount_dir, path.strip(os.sep)),
try_lazy_umount=True)
for fs in self.driver.partition_scheme.fs_sorted_by_depth(os_id,
True):
if fs.mount == 'swap':
continue
fu.umount_fs(os.path.join(mount_dir, fs.mount.strip(os.sep)))
@contextmanager
def mount_target(self, mount_dir, os_id, pseudo=True, treat_mtab=True):
self._mount_target(mount_dir, os_id, pseudo=pseudo,
treat_mtab=treat_mtab)
try:
yield
finally:
self._umount_target(mount_dir, os_id, pseudo)
@contextmanager
def _mount_bootloader(self, mount_dir):
fs = filter(lambda fss: fss.mount == 'multiboot',
self.driver.partition_scheme.fss)
if len(fs) > 1:
raise errors.WrongPartitionSchemeError(
'Multiple multiboot partitions found')
utils.makedirs_if_not_exists(mount_dir)
fu.mount_fs(fs[0].type, str(fs[0].device), mount_dir)
yield pu.get_uuid(fs[0].device)
fu.umount_fs(mount_dir)
class PolicyPartitioner(object):
def __init__(self, driver):
self.driver = driver
def partition(self):
policy = self.driver.partitions_policy
LOG.debug("Using partitioning policy '%s'."
% self.driver.partitions_policy)
policy_handlers = {
"verify": self._handle_verify,
"clean": self._handle_clean,
"nailgun_legacy": self._handle_nailgun_legacy,
}
known_policies = policy_handlers.keys()
if policy not in known_policies:
raise errors.WrongPartitionPolicyError(
"'%s' policy is not one of known ones: %s"
% (policy, known_policies))
policy_handlers[policy]()
def _handle_verify(self):
provision_schema = self.driver.partition_scheme.to_dict()
hw_schema = self.driver.hw_partition_scheme.to_dict()
PartitionSchemaCompareTool().assert_no_diff(provision_schema,
hw_schema)
self._do_clean_filesystems()
@staticmethod
def _verify_disk_size(parteds, hu_disks):
for parted in parteds:
disks = [d for d in hu_disks if d.get('name') == parted.name]
if not disks:
raise errors.DiskNotFoundError(
'No physical disks found matching: %s' % parted.name)
disk_size_bytes = disks[0].get('bspec', {}).get('size64')
if not disk_size_bytes:
raise ValueError('Cannot read size of the disk: %s'
% disks[0].get('name'))
# It's safer to understate the physical disk size
disk_size_mib = utils.B2MiB(disk_size_bytes, ceil=False)
if parted.size > disk_size_mib:
raise errors.NotEnoughSpaceError(
'Partition scheme for: %(disk)s exceeds the size of the '
'disk. Scheme size is %(scheme_size)s MiB, and disk size '
'is %(disk_size)s MiB.' % {
'disk': parted.name, 'scheme_size': parted.size,
'disk_size': disk_size_mib})
def _handle_clean(self):
self._verify_disk_size(self.driver.partition_scheme.parteds,
self.driver.hu_disks)
self._do_partitioning()
def _handle_nailgun_legacy(self):
# Corresponds to nailgun behavior.
if self.driver.partition_scheme.skip_partitioning:
LOG.debug('Some of fs has keep_data (preserve) flag, '
'skipping partitioning')
self._do_clean_filesystems()
else:
LOG.debug('No keep_data (preserve) flag passed, wiping out all'
'disks and re-partitioning')
self._do_partitioning()
def _do_clean_filesystems(self):
# NOTE(agordeev): it turns out that only mkfs.xfs needs '-f' flag in
# order to force recreation of filesystem.
# This option will be added to mkfs.xfs call explicitly in fs utils.
# TODO(asvechnikov): need to refactor processing keep_flag logic when
# data model will become flat
for fs in self.driver.partition_scheme.fss:
if not fs.keep_data:
fu.make_fs(fs.type, fs.options, fs.label, fs.device)
def _do_partitioning(self):
# If disks are not wiped out at all, it is likely they contain lvm
# and md metadata which will prevent re-creating a partition table
# with 'device is busy' error.
mu.mdclean_all()
lu.lvremove_all()
lu.vgremove_all()
lu.pvremove_all()
LOG.debug("Enabling udev's rules blacklisting")
utils.blacklist_udev_rules(udev_rules_dir=CONF.udev_rules_dir,
udev_rules_lib_dir=CONF.udev_rules_lib_dir,
udev_rename_substr=CONF.udev_rename_substr,
udev_empty_rule=CONF.udev_empty_rule)
for parted in self.driver.partition_scheme.parteds:
for prt in parted.partitions:
# We wipe out the beginning of every new partition
# right after creating it. It allows us to avoid possible
# interactive dialog if some data (metadata or file system)
# present on this new partition and it also allows udev not
# hanging trying to parse this data.
utils.execute('dd', 'if=/dev/zero', 'bs=1M',
'seek=%s' % max(prt.begin - 3, 0), 'count=5',
'of=%s' % prt.device, check_exit_code=[0])
# Also wipe out the ending of every new partition.
# Different versions of md stores metadata in different places.
# Adding exit code 1 to be accepted as for handling situation
# when 'no space left on device' occurs.
utils.execute('dd', 'if=/dev/zero', 'bs=1M',
'seek=%s' % max(prt.end - 3, 0), 'count=5',
'of=%s' % prt.device, check_exit_code=[0, 1])
for parted in self.driver.partition_scheme.parteds:
pu.make_label(parted.name, parted.label)
for prt in parted.partitions:
pu.make_partition(prt.device, prt.begin, prt.end, prt.type)
for flag in prt.flags:
pu.set_partition_flag(prt.device, prt.count, flag)
if prt.guid:
pu.set_gpt_type(prt.device, prt.count, prt.guid)
# If any partition to be created doesn't exist it's an error.
# Probably it's again 'device or resource busy' issue.
if not os.path.exists(prt.name):
raise errors.PartitionNotFoundError(
'Partition %s not found after creation' % prt.name)
LOG.debug("Disabling udev's rules blacklisting")
utils.unblacklist_udev_rules(
udev_rules_dir=CONF.udev_rules_dir,
udev_rename_substr=CONF.udev_rename_substr)
# If one creates partitions with the same boundaries as last time,
# there might be md and lvm metadata on those partitions. To prevent
# failing of creating md and lvm devices we need to make sure
# unused metadata are wiped out.
mu.mdclean_all()
lu.lvremove_all()
lu.vgremove_all()
lu.pvremove_all()
# creating meta disks
for md in self.driver.partition_scheme.mds:
mu.mdcreate(md.name, md.level, md.devices, md.metadata)
# creating physical volumes
for pv in self.driver.partition_scheme.pvs:
lu.pvcreate(pv.name, metadatasize=pv.metadatasize,
metadatacopies=pv.metadatacopies)
# creating volume groups
for vg in self.driver.partition_scheme.vgs:
lu.vgcreate(vg.name, *vg.pvnames)
# creating logical volumes
for lv in self.driver.partition_scheme.lvs:
lu.lvcreate(lv.vgname, lv.name, lv.size)
# making file systems
for fs in self.driver.partition_scheme.fss:
found_images = [img for img in self.driver.image_scheme.images
if img.target_device == fs.device]
if not found_images:
fu.make_fs(fs.type, fs.options, fs.label, fs.device)
class PartitionSchemaCompareTool(object):
def assert_no_diff(self, user_schema, hw_schema):
usr_sch = self._prepare_user_schema(user_schema, hw_schema)
hw_sch = self._prepare_hw_schema(user_schema, hw_schema)
# NOTE(lobur): this may not work on bm hardware: because of the
# partition alignments sizes may not match precisely, so need to
# write own diff tool
if not usr_sch == hw_sch:
diff_str = utils.dict_diff(usr_sch, hw_sch,
"user_schema", "hw_schema")
raise errors.PartitionSchemeMismatchError(diff_str)
LOG.debug("hw_schema and user_schema matched")
def _prepare_user_schema(self, user_schema, hw_schema):
LOG.debug('Preparing user_schema for verification:\n%s' %
user_schema)
# Set all keep_data (preserve) flags to false.
# They are just instructions to deploy driver and do not stored on
# resulting partitions, so we have no means to read them from
# hw_schema
for fs in user_schema['fss']:
fs['keep_data'] = False
fs['os_id'] = []
for parted in user_schema['parteds']:
for part in parted['partitions']:
part['keep_data'] = False
self._drop_schema_size(user_schema)
LOG.debug('Prepared user_schema is:\n%s' % user_schema)
return user_schema
@staticmethod
def _drop_schema_size(schema):
# If it exists, drop the schema size attribute. This should
# be valid because doing a full layout comparison implicitly
# involves verification of the disk sizes, and we don't need
# to check those separately.
for parted in schema['parteds']:
parted.pop('size', None)
def _prepare_hw_schema(self, user_schema, hw_schema):
LOG.debug('Preparing hw_schema to verification:\n%s' %
hw_schema)
user_disks = [p['name'] for p in user_schema['parteds']]
# Ignore disks which are not mentioned in user_schema
filtered_disks = []
for disk in hw_schema['parteds']:
if disk['name'] in user_disks:
filtered_disks.append(disk)
else:
LOG.info("Node disk '%s' is not mentioned in deploy_config"
" thus it will be skipped." % disk['name'])
hw_schema['parteds'] = filtered_disks
# Ignore filesystems that belong to disk not mentioned in user_schema
filtered_fss = []
for fs in hw_schema['fss']:
if fs['device'].rstrip("0123456789") in user_disks:
filtered_fss.append(fs)
else:
LOG.info("Node filesystem '%s' belongs to disk not mentioned"
" in deploy_config thus it will be skipped."
% fs['device'])
hw_schema['fss'] = filtered_fss
# Transform filesystem types
for fs in hw_schema['fss']:
fs['fs_type'] = self._transform_fs_type(fs['fs_type'])
fs['os_id'] = []
self._drop_schema_size(hw_schema)
LOG.debug('Prepared hw_schema is:\n%s' % hw_schema)
return hw_schema
def _transform_fs_type(self, hw_fs_type):
# hw fstype name pattern -> fstype name in user schema
hw_fs_to_user_fs_map = {
'linux-swap': 'swap'
}
for hw_fs_pattern, usr_schema_val in hw_fs_to_user_fs_map.iteritems():
if hw_fs_pattern in hw_fs_type:
LOG.info("Node fs type '%s' is transformed to the user "
"schema type as '%s'."
% (hw_fs_type, usr_schema_val))
return usr_schema_val
return hw_fs_type