summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLucas Alvares Gomes <lucasagomes@gmail.com>2015-02-19 13:42:39 +0000
committerLucas Alvares Gomes <lucasagomes@gmail.com>2015-03-04 16:34:17 +0000
commitd23e0170de23cfb22eecb728105ec6e102185a1b (patch)
tree6809aee81e7e6dc541787694c6a8d92c29948ba8
parentf0e21a68fb3c2813634c6b15501050f2e9fde5c1 (diff)
downloadironic-python-agent-d23e0170de23cfb22eecb728105ec6e102185a1b.tar.gz
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
-rw-r--r--ironic_python_agent/errors.py12
-rw-r--r--ironic_python_agent/extensions/image.py153
-rw-r--r--ironic_python_agent/tests/extensions/image.py140
-rw-r--r--setup.cfg1
4 files changed, 306 insertions, 0 deletions
diff --git a/ironic_python_agent/errors.py b/ironic_python_agent/errors.py
index 26dac154..ee2df169 100644
--- a/ironic_python_agent/errors.py
+++ b/ironic_python_agent/errors.py
@@ -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)
diff --git a/ironic_python_agent/extensions/image.py b/ironic_python_agent/extensions/image.py
new file mode 100644
index 00000000..1158eae4
--- /dev/null
+++ b/ironic_python_agent/extensions/image.py
@@ -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)
diff --git a/ironic_python_agent/tests/extensions/image.py b/ironic_python_agent/tests/extensions/image.py
new file mode 100644
index 00000000..fc23e71a
--- /dev/null
+++ b/ironic_python_agent/tests/extensions/image.py
@@ -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)
diff --git a/setup.cfg b/setup.cfg
index 74469df0..0384fe76 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -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