Add the image extension (for local boot)
Initially this extension supports installing a bootloader so the user image can boot from the local disk. Change-Id: Ia588aafc240b55119c02f1254addc0cf796f88c5
This commit is contained in:
parent
f0e21a68fb
commit
d23e0170de
@ -294,3 +294,15 @@ class ISCSIError(RESTError):
|
||||
'{1}. stdout: {2}. stderr: {3}')
|
||||
details = details.format(error_msg, exit_code, stdout, stderr)
|
||||
super(ISCSIError, self).__init__(details)
|
||||
|
||||
|
||||
class DeviceNotFound(NotFound):
|
||||
"""Error raised when the disk or partition to deploy the image onto is
|
||||
not found.
|
||||
"""
|
||||
|
||||
message = ('Error finding the disk or partition device to deploy '
|
||||
'the image onto.')
|
||||
|
||||
def __init__(self, details):
|
||||
super(DeviceNotFound, self).__init__(details)
|
||||
|
153
ironic_python_agent/extensions/image.py
Normal file
153
ironic_python_agent/extensions/image.py
Normal file
@ -0,0 +1,153 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2015 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 os
|
||||
import shlex
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
from oslo_concurrency import processutils
|
||||
|
||||
from ironic_python_agent import errors
|
||||
from ironic_python_agent.extensions import base
|
||||
from ironic_python_agent import hardware
|
||||
from ironic_python_agent.openstack.common import log
|
||||
from ironic_python_agent import utils
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
BIND_MOUNTS = ('/dev', '/sys', '/proc')
|
||||
|
||||
|
||||
def _get_root_partition(device, root_uuid):
|
||||
"""Find the root partition of a given device."""
|
||||
LOG.debug("Find the root partition %(uuid)s on device %(dev)s",
|
||||
{'dev': device, 'uuid': root_uuid})
|
||||
|
||||
try:
|
||||
# Try to tell the kernel to re-read the partition table
|
||||
try:
|
||||
utils.execute('partx', '-u', device, attempts=3,
|
||||
delay_on_retry=True)
|
||||
except processutils.ProcessExecutionError:
|
||||
LOG.warning("Couldn't re-read the partition table "
|
||||
"on device %s" % device)
|
||||
|
||||
report = utils.execute('lsblk', '-PbioKNAME,UUID,TYPE', device)[0]
|
||||
for line in report.split('\n'):
|
||||
part = {}
|
||||
# Split into KEY=VAL pairs
|
||||
vals = shlex.split(line)
|
||||
for key, val in (v.split('=', 1) for v in vals):
|
||||
part[key] = val.strip()
|
||||
# Ignore non partition
|
||||
if part.get('TYPE') != 'part':
|
||||
continue
|
||||
|
||||
if part.get('UUID') == root_uuid:
|
||||
LOG.debug("Root partition %(uuid)s found on device "
|
||||
"%(dev)s", {'uuid': root_uuid, 'dev': device})
|
||||
return '/dev/' + part.get('KNAME')
|
||||
else:
|
||||
error_msg = ("No root partition with UUID %(uuid)s found on "
|
||||
"device %(dev)s" % {'uuid': root_uuid, 'dev': device})
|
||||
LOG.error(error_msg)
|
||||
raise errors.DeviceNotFound(error_msg)
|
||||
except processutils.ProcessExecutionError as e:
|
||||
error_msg = ('Finding the root partition with UUID %(uuid)s on '
|
||||
'device %(dev)s failed with %(err)s' %
|
||||
{'uuid': root_uuid, 'dev': device, 'err': e})
|
||||
LOG.error(error_msg)
|
||||
raise errors.CommandExecutionError(error_msg)
|
||||
|
||||
|
||||
def _install_grub2(device, root_uuid):
|
||||
"""Install GRUB2 bootloader on a given device."""
|
||||
LOG.debug("Installing GRUB2 bootloader on device %s", device)
|
||||
root_partition = _get_root_partition(device, root_uuid)
|
||||
|
||||
try:
|
||||
# Mount the partition and binds
|
||||
path = tempfile.mkdtemp()
|
||||
utils.execute('mount', root_partition, path)
|
||||
for fs in BIND_MOUNTS:
|
||||
utils.execute('mount', '-o', 'bind', fs, path + fs)
|
||||
|
||||
binary_name = "grub"
|
||||
if os.path.exists(os.path.join(path, 'usr/sbin/grub2-install')):
|
||||
binary_name = "grub2"
|
||||
|
||||
# Install grub
|
||||
utils.execute('chroot %(path)s /bin/bash -c '
|
||||
'"/usr/sbin/%(bin)s-install %(dev)s"' %
|
||||
{'path': path, 'bin': binary_name, 'dev': device},
|
||||
shell=True)
|
||||
|
||||
# Generate the grub configuration file
|
||||
utils.execute('chroot %(path)s /bin/bash -c '
|
||||
'"/usr/sbin/%(bin)s-mkconfig -o '
|
||||
'/boot/%(bin)s/grub.cfg"' %
|
||||
{'path': path, 'bin': binary_name}, shell=True)
|
||||
|
||||
LOG.info("GRUB2 successfully installed on %s", device)
|
||||
|
||||
except processutils.ProcessExecutionError as e:
|
||||
error_msg = ('Installing GRUB2 boot loader to device %(dev)s '
|
||||
'failed with %(err)s. Attempted 3 times.' %
|
||||
{'dev': device, 'err': e})
|
||||
LOG.error(error_msg)
|
||||
raise errors.CommandExecutionError(error_msg)
|
||||
|
||||
finally:
|
||||
umount_warn_msg = "Unable to umount %(path)s. Error: %(error)s"
|
||||
# Umount binds and partition
|
||||
umount_binds_fail = False
|
||||
for fs in BIND_MOUNTS:
|
||||
try:
|
||||
utils.execute('umount', path + fs, attempts=3,
|
||||
delay_on_retry=True)
|
||||
except processutils.ProcessExecutionError as e:
|
||||
umount_binds_fail = True
|
||||
LOG.warning(umount_warn_msg, {'path': path + fs, 'error': e})
|
||||
|
||||
# If umounting the binds succeed then we can try to delete it
|
||||
if not umount_binds_fail:
|
||||
try:
|
||||
utils.execute('umount', path, attempts=3, delay_on_retry=True)
|
||||
except processutils.ProcessExecutionError as e:
|
||||
LOG.warning(umount_warn_msg, {'path': path, 'error': e})
|
||||
else:
|
||||
# After everything is umounted we can then remove the
|
||||
# temporary directory
|
||||
shutil.rmtree(path)
|
||||
|
||||
|
||||
class ImageExtension(base.BaseAgentExtension):
|
||||
|
||||
@base.sync_command('install_bootloader')
|
||||
def install_bootloader(self, root_uuid):
|
||||
"""Install the GRUB2 bootloader on the image.
|
||||
|
||||
:param root_uuid: The UUID of the root partition.
|
||||
:raises: CommandExecutionError if the installation of the
|
||||
bootloader fails.
|
||||
:raises: DeviceNotFound if the root partition is not found.
|
||||
|
||||
"""
|
||||
device = hardware.dispatch_to_managers('get_os_install_device')
|
||||
_install_grub2(device, root_uuid)
|
140
ironic_python_agent/tests/extensions/image.py
Normal file
140
ironic_python_agent/tests/extensions/image.py
Normal file
@ -0,0 +1,140 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2015 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 mock
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
from oslo_concurrency import processutils
|
||||
from oslotest import base as test_base
|
||||
|
||||
from ironic_python_agent import errors
|
||||
from ironic_python_agent.extensions import image
|
||||
from ironic_python_agent import hardware
|
||||
from ironic_python_agent import utils
|
||||
|
||||
|
||||
@mock.patch.object(hardware, 'dispatch_to_managers')
|
||||
@mock.patch.object(utils, 'execute')
|
||||
@mock.patch.object(tempfile, 'mkdtemp', lambda *_: '/tmp/fake-dir')
|
||||
@mock.patch.object(shutil, 'rmtree', lambda *_: None)
|
||||
class TestImageExtension(test_base.BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestImageExtension, self).setUp()
|
||||
self.agent_extension = image.ImageExtension()
|
||||
self.fake_dev = '/dev/fake'
|
||||
self.fake_root_part = '/dev/fake2'
|
||||
self.fake_root_uuid = '11111111-2222-3333-4444-555555555555'
|
||||
self.fake_dir = '/tmp/fake-dir'
|
||||
|
||||
@mock.patch.object(image, '_install_grub2')
|
||||
def test_install_bootloader(self, mock_grub2, mock_execute, mock_dispatch):
|
||||
mock_dispatch.return_value = self.fake_dev
|
||||
self.agent_extension.install_bootloader(root_uuid=self.fake_root_uuid)
|
||||
mock_dispatch.assert_called_once_with('get_os_install_device')
|
||||
mock_grub2.assert_called_once_with(self.fake_dev, self.fake_root_uuid)
|
||||
|
||||
@mock.patch.object(image, '_get_root_partition')
|
||||
def test__install_grub2(self, mock_get_root, mock_execute, mock_dispatch):
|
||||
mock_get_root.return_value = self.fake_root_part
|
||||
image._install_grub2(self.fake_dev, self.fake_root_uuid)
|
||||
|
||||
expected = [mock.call('mount', '/dev/fake2', self.fake_dir),
|
||||
mock.call('mount', '-o', 'bind', '/dev',
|
||||
self.fake_dir + '/dev'),
|
||||
mock.call('mount', '-o', 'bind', '/sys',
|
||||
self.fake_dir + '/sys'),
|
||||
mock.call('mount', '-o', 'bind', '/proc',
|
||||
self.fake_dir + '/proc'),
|
||||
mock.call(('chroot %s /bin/bash -c '
|
||||
'"/usr/sbin/grub-install %s"' %
|
||||
(self.fake_dir, self.fake_dev)), shell=True),
|
||||
mock.call(('chroot %s /bin/bash -c '
|
||||
'"/usr/sbin/grub-mkconfig -o '
|
||||
'/boot/grub/grub.cfg"' % self.fake_dir),
|
||||
shell=True),
|
||||
mock.call('umount', self.fake_dir + '/dev',
|
||||
attempts=3, delay_on_retry=True),
|
||||
mock.call('umount', self.fake_dir + '/sys',
|
||||
attempts=3, delay_on_retry=True),
|
||||
mock.call('umount', self.fake_dir + '/proc',
|
||||
attempts=3, delay_on_retry=True),
|
||||
mock.call('umount', self.fake_dir, attempts=3,
|
||||
delay_on_retry=True)]
|
||||
mock_execute.assert_has_calls(expected)
|
||||
mock_get_root.assert_called_once_with(self.fake_dev,
|
||||
self.fake_root_uuid)
|
||||
self.assertFalse(mock_dispatch.called)
|
||||
|
||||
@mock.patch.object(image, '_get_root_partition')
|
||||
def test__install_grub2_command_fail(self, mock_get_root, mock_execute,
|
||||
mock_dispatch):
|
||||
mock_get_root.return_value = self.fake_root_part
|
||||
mock_execute.side_effect = processutils.ProcessExecutionError('boom')
|
||||
|
||||
self.assertRaises(errors.CommandExecutionError, image._install_grub2,
|
||||
self.fake_dev, self.fake_root_uuid)
|
||||
|
||||
mock_get_root.assert_called_once_with(self.fake_dev,
|
||||
self.fake_root_uuid)
|
||||
self.assertFalse(mock_dispatch.called)
|
||||
|
||||
def test__get_root_partition(self, mock_execute, mock_dispatch):
|
||||
lsblk_output = ('''KNAME="test" UUID="" TYPE="disk"
|
||||
KNAME="test1" UUID="256a39e3-ca3c-4fb8-9cc2-b32eec441f47" TYPE="part"
|
||||
KNAME="test2" UUID="%s" TYPE="part"''' % self.fake_root_uuid)
|
||||
mock_execute.side_effect = (None, [lsblk_output])
|
||||
|
||||
root_part = image._get_root_partition(self.fake_dev,
|
||||
self.fake_root_uuid)
|
||||
self.assertEqual('/dev/test2', root_part)
|
||||
expected = [mock.call('partx', '-u', self.fake_dev, attempts=3,
|
||||
delay_on_retry=True),
|
||||
mock.call('lsblk', '-PbioKNAME,UUID,TYPE', self.fake_dev)]
|
||||
mock_execute.assert_has_calls(expected)
|
||||
self.assertFalse(mock_dispatch.called)
|
||||
|
||||
def test__get_root_partition_no_device_found(self, mock_execute,
|
||||
mock_dispatch):
|
||||
lsblk_output = ('''KNAME="test" UUID="" TYPE="disk"
|
||||
KNAME="test1" UUID="256a39e3-ca3c-4fb8-9cc2-b32eec441f47" TYPE="part"
|
||||
KNAME="test2" UUID="" TYPE="part"''')
|
||||
mock_execute.side_effect = (None, [lsblk_output])
|
||||
|
||||
self.assertRaises(errors.DeviceNotFound,
|
||||
image._get_root_partition, self.fake_dev,
|
||||
self.fake_root_uuid)
|
||||
expected = [mock.call('partx', '-u', self.fake_dev, attempts=3,
|
||||
delay_on_retry=True),
|
||||
mock.call('lsblk', '-PbioKNAME,UUID,TYPE', self.fake_dev)]
|
||||
mock_execute.assert_has_calls(expected)
|
||||
self.assertFalse(mock_dispatch.called)
|
||||
|
||||
def test__get_root_partition_command_fail(self, mock_execute,
|
||||
mock_dispatch):
|
||||
mock_execute.side_effect = (None,
|
||||
processutils.ProcessExecutionError('boom'))
|
||||
self.assertRaises(errors.CommandExecutionError,
|
||||
image._get_root_partition, self.fake_dev,
|
||||
self.fake_root_uuid)
|
||||
|
||||
expected = [mock.call('partx', '-u', self.fake_dev, attempts=3,
|
||||
delay_on_retry=True),
|
||||
mock.call('lsblk', '-PbioKNAME,UUID,TYPE', self.fake_dev)]
|
||||
mock_execute.assert_has_calls(expected)
|
||||
self.assertFalse(mock_dispatch.called)
|
@ -23,6 +23,7 @@ ironic_python_agent.extensions =
|
||||
decom = ironic_python_agent.extensions.decom:DecomExtension
|
||||
flow = ironic_python_agent.extensions.flow:FlowExtension
|
||||
iscsi = ironic_python_agent.extensions.iscsi:ISCSIExtension
|
||||
image = ironic_python_agent.extensions.image:ImageExtension
|
||||
|
||||
ironic_python_agent.hardware_managers =
|
||||
generic = ironic_python_agent.hardware:GenericHardwareManager
|
||||
|
Loading…
x
Reference in New Issue
Block a user