diff --git a/ironic/common/boot_devices.py b/ironic/common/boot_devices.py new file mode 100644 index 0000000000..c225164cbe --- /dev/null +++ b/ironic/common/boot_devices.py @@ -0,0 +1,42 @@ +# Copyright 2014 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. + + +""" +Mapping of boot devices used when requesting the system to boot +from an alternate device. + +The options presented were based on the IPMItool chassis +bootdev command. You can find the documentation at: +http://linux.die.net/man/1/ipmitool + +NOTE: This module does not include all the options from ipmitool because +they don't make sense in the limited context of Ironic right now. +""" + +PXE = 'pxe' +"Boot from PXE boot" + +DISK = 'disk' +"Boot from default Hard-drive" + +CDROM = 'cdrom' +"Boot from CD/DVD" + +BIOS = 'bios' +"Boot into BIOS setup" + +SAFE = 'safe' +"Boot from default Hard-drive, request Safe Mode" diff --git a/ironic/drivers/base.py b/ironic/drivers/base.py index 01916c8ef8..9cb4788b06 100644 --- a/ironic/drivers/base.py +++ b/ironic/drivers/base.py @@ -69,6 +69,14 @@ class BaseDriver(object): May be None, if unsupported by a driver. """ + management = None + """`Standard` attribute for management related features. + + A reference to an instance of :class:ManagementInterface. + May be None, if unsupported by a driver. + """ + standard_interfaces.append('management') + vendor = None """Attribute for accessing any vendor-specific extensions. @@ -348,3 +356,55 @@ class VendorInterface(object): raise exception.UnsupportedDriverExtension( _('Vendor interface does not support driver vendor_passthru ' 'method: %s') % method) + + +@six.add_metaclass(abc.ABCMeta) +class ManagementInterface(object): + """Interface for management related actions.""" + + # TODO(lucasagomes): The 'node' parameter + # needs to be passed to validate() because of the + # ConductorManager.validate_driver_interfaces(). Remove it as part of + # https://bugs.launchpad.net/ironic/+bug/1312632. + @abc.abstractmethod + def validate(self, task, node): + """Validate the driver-specific management information. + + :param task: a task from TaskManager. + :param node: a single Node to validate. + :raises: InvalidParameterValue + """ + + @abc.abstractmethod + def get_supported_boot_devices(self): + """Get a list of the supported boot devices. + + :returns: A list with the supported boot devices defined + in :mod:`ironic.common.boot_devices`. + """ + + @abc.abstractmethod + def set_boot_device(self, task, device, **kwargs): + """Set the boot device for a node. + + Set the boot device to use on next reboot of the node. + + :param task: a task from TaskManager. + :param device: the boot device, one of + :mod:`ironic.common.boot_devices`. + :param kwargs: extra driver-specific parameters. + :raises: InvalidParameterValue if an invalid boot device is + specified. + """ + + @abc.abstractmethod + def get_boot_device(self, task): + """Get the current boot device for a node. + + Provides the current boot device of the node. Be aware that not + all drivers support this. + + :param task: a task from TaskManager. + :returns: the boot device, one of :mod:`ironic.common.boot_devices` + or None if it is unknown. + """ diff --git a/ironic/drivers/fake.py b/ironic/drivers/fake.py index 36b0cce85f..3855478d9c 100644 --- a/ironic/drivers/fake.py +++ b/ironic/drivers/fake.py @@ -42,6 +42,7 @@ class FakeDriver(base.BaseDriver): 'second_method': self.b} self.vendor = utils.MixinVendorInterface(self.mapping) self.console = fake.FakeConsole() + self.management = fake.FakeManagement() class FakeIPMIToolDriver(base.BaseDriver): diff --git a/ironic/drivers/modules/fake.py b/ironic/drivers/modules/fake.py index d4a13f901b..6c5a1473c2 100644 --- a/ironic/drivers/modules/fake.py +++ b/ironic/drivers/modules/fake.py @@ -24,6 +24,7 @@ functionality between a power interface and a deploy interface, when both rely on seprate vendor_passthru methods. """ +from ironic.common import boot_devices from ironic.common import exception from ironic.common import states from ironic.drivers import base @@ -143,3 +144,21 @@ class FakeConsole(base.ConsoleInterface): def get_console(self, task, node): return {} + + +class FakeManagement(base.ManagementInterface): + """Example implementation of a simple management interface.""" + + def validate(self, task, node): + return True + + def get_supported_boot_devices(self): + return [boot_devices.PXE] + + def set_boot_device(self, task, device, **kwargs): + if device not in self.get_supported_boot_devices(): + raise exception.InvalidParameterValue(_( + "Invalid boot device %s specified.") % device) + + def get_boot_device(self, task): + return boot_devices.PXE diff --git a/ironic/tests/conductor/test_manager.py b/ironic/tests/conductor/test_manager.py index 6e28d0437d..b29398c54b 100644 --- a/ironic/tests/conductor/test_manager.py +++ b/ironic/tests/conductor/test_manager.py @@ -752,6 +752,7 @@ class ManagerTestCase(tests_db_base.DbTestCase): node['uuid']) expected = {'console': {'result': True}, 'power': {'result': True}, + 'management': {'result': True}, 'deploy': {'result': True}} self.assertEqual(expected, ret) diff --git a/ironic/tests/drivers/test_fake.py b/ironic/tests/drivers/test_fake.py index eb4a01c4ff..11ab2d7dd9 100644 --- a/ironic/tests/drivers/test_fake.py +++ b/ironic/tests/drivers/test_fake.py @@ -17,16 +17,18 @@ """Test class for Fake driver.""" +import mock + +from ironic.common import boot_devices from ironic.common import driver_factory from ironic.common import exception from ironic.common import states from ironic.conductor import task_manager -from ironic.db import api as db_api from ironic.drivers import base as driver_base from ironic.openstack.common import context from ironic.tests import base from ironic.tests.conductor import utils as mgr_utils -from ironic.tests.db import utils as db_utils +from ironic.tests.objects import utils as obj_utils class FakeDriverTestCase(base.TestCase): @@ -34,11 +36,13 @@ class FakeDriverTestCase(base.TestCase): def setUp(self): super(FakeDriverTestCase, self).setUp() self.context = context.get_admin_context() - self.dbapi = db_api.get_instance() mgr_utils.mock_the_extension_manager() self.driver = driver_factory.get_driver("fake") - db_node = db_utils.get_test_node() - self.node = self.dbapi.create_node(db_node) + self.node = obj_utils.get_test_node(self.context) + self.task = mock.Mock(spec=task_manager.TaskManager) + self.task.shared = False + self.task.node = self.node + self.task.driver = self.driver def test_driver_interfaces(self): # fake driver implements only 3 out of 5 interfaces @@ -50,15 +54,14 @@ class FakeDriverTestCase(base.TestCase): self.assertIsNone(self.driver.rescue) def test_power_interface(self): - with task_manager.acquire(self.context, - [self.node.uuid]) as task: - self.driver.power.validate(task, self.node) - self.driver.power.get_power_state(task, self.node) - self.assertRaises(exception.InvalidParameterValue, - self.driver.power.set_power_state, - task, self.node, states.NOSTATE) - self.driver.power.set_power_state(task, self.node, states.POWER_ON) - self.driver.power.reboot(task, self.node) + self.driver.power.validate(self.task, self.node) + self.driver.power.get_power_state(self.task, self.node) + self.assertRaises(exception.InvalidParameterValue, + self.driver.power.set_power_state, + self.task, self.node, states.NOSTATE) + self.driver.power.set_power_state(self.task, self.node, + states.POWER_ON) + self.driver.power.reboot(self.task, self.node) def test_deploy_interface(self): self.driver.deploy.validate(None, self.node) @@ -70,3 +73,23 @@ class FakeDriverTestCase(base.TestCase): self.driver.deploy.clean_up(None, None) self.driver.deploy.tear_down(None, None) + + def test_management_interface_validate(self): + self.driver.management.validate(self.task, self.node) + + def test_management_interface_set_boot_device_good(self): + self.driver.management.set_boot_device(self.task, boot_devices.PXE) + + def test_management_interface_set_boot_device_fail(self): + self.assertRaises(exception.InvalidParameterValue, + self.driver.management.set_boot_device, self.task, + 'not-supported') + + def test_management_interface_get_supported_boot_devices(self): + expected = [boot_devices.PXE] + self.assertEqual(expected, + self.driver.management.get_supported_boot_devices()) + + def test_management_interface_get_boot_device(self): + self.assertEqual(boot_devices.PXE, + self.driver.management.get_boot_device(self.task))