diff --git a/metalsmith/_provisioner.py b/metalsmith/_provisioner.py index 126d8ac..8db2d76 100644 --- a/metalsmith/_provisioner.py +++ b/metalsmith/_provisioner.py @@ -26,6 +26,7 @@ from metalsmith import _os_api from metalsmith import _scheduler from metalsmith import _utils from metalsmith import exceptions +from metalsmith import sources LOG = logging.getLogger(__name__) @@ -178,7 +179,8 @@ class Provisioner(object): :param node: Node object, UUID or name. Will be reserved first, if not reserved already. Must be in the "available" state with maintenance mode off. - :param image: Image name or UUID to provision. + :param image: Image source - one of :mod:`~metalsmith.sources`, + `Image` name or UUID. :param nics: List of virtual NICs to attach to physical ports. Each item is a dict with a key describing the type of the NIC: either a port (``{"port": ""}``) or a network @@ -203,6 +205,9 @@ class Provisioner(object): """ if config is None: config = _config.InstanceConfig() + if isinstance(image, six.string_types): + image = sources.Glance(image) + node = self._check_node_for_deploy(node) created_ports = [] attached_ports = [] @@ -211,14 +216,7 @@ class Provisioner(object): hostname = self._check_hostname(node, hostname) root_disk_size = _utils.get_root_disk(root_disk_size, node) - try: - image = self._api.get_image(image) - except Exception as exc: - raise exceptions.InvalidImage( - 'Cannot find image %(image)s: %(error)s' % - {'image': image, 'error': exc}) - - LOG.debug('Image: %s', image) + image._validate(self._api) nics = self._get_nics(nics or []) @@ -235,17 +233,12 @@ class Provisioner(object): capabilities['boot_option'] = 'netboot' if netboot else 'local' - updates = {'/instance_info/image_source': image.id, - '/instance_info/root_gb': root_disk_size, + updates = {'/instance_info/root_gb': root_disk_size, '/instance_info/capabilities': capabilities, '/extra/%s' % _CREATED_PORTS: created_ports, '/extra/%s' % _ATTACHED_PORTS: attached_ports, '/instance_info/%s' % _os_api.HOSTNAME_FIELD: hostname} - - for prop in ('kernel', 'ramdisk'): - value = getattr(image, '%s_id' % prop, None) - if value: - updates['/instance_info/%s' % prop] = value + updates.update(image._node_updates(self._api)) LOG.debug('Updating node %(node)s with %(updates)s', {'node': _utils.log_node(node), 'updates': updates}) diff --git a/metalsmith/sources.py b/metalsmith/sources.py new file mode 100644 index 0000000..18a71e8 --- /dev/null +++ b/metalsmith/sources.py @@ -0,0 +1,73 @@ +# Copyright 2018 Red Hat, Inc. +# +# 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. + +"""Image sources to use when provisioning nodes.""" + +import abc +import logging + +import six + +from metalsmith import exceptions + + +LOG = logging.getLogger(__name__) + + +@six.add_metaclass(abc.ABCMeta) +class _Source(object): + + def _validate(self, api): + """Validate the source.""" + + @abc.abstractmethod + def _node_updates(self, api): + """Updates required for a node to use this source.""" + + +class Glance(_Source): + """Image from the OpenStack Image service.""" + + def __init__(self, image): + """Create a Glance source. + + :param image: `Image` object, ID or name. + """ + self._image_id = image + self._image_obj = None + + def _validate(self, api): + if self._image_obj is not None: + return + try: + self._image_obj = api.get_image(self._image_id) + except Exception as exc: + raise exceptions.InvalidImage( + 'Cannot find image %(image)s: %(error)s' % + {'image': self._image_id, 'error': exc}) + + def _node_updates(self, api): + self._validate(api) + LOG.debug('Image: %s', self._image_obj) + + updates = { + '/instance_info/image_source': self._image_obj.id + } + for prop in ('kernel', 'ramdisk'): + value = getattr(self._image_obj, '%s_id' % prop, None) + if value: + updates['/instance_info/%s' % prop] = value + + return updates diff --git a/metalsmith/test/test_provisioner.py b/metalsmith/test/test_provisioner.py index 0c38b2e..276192d 100644 --- a/metalsmith/test/test_provisioner.py +++ b/metalsmith/test/test_provisioner.py @@ -22,6 +22,7 @@ from metalsmith import _instance from metalsmith import _os_api from metalsmith import _provisioner from metalsmith import exceptions +from metalsmith import sources class Base(testtools.TestCase): @@ -206,6 +207,26 @@ class TestProvisionNode(Base): self.assertFalse(self.api.release_node.called) self.assertFalse(self.api.delete_port.called) + def test_ok_with_source(self): + inst = self.pr.provision_node(self.node, sources.Glance('image'), + [{'network': 'network'}]) + + self.assertEqual(inst.uuid, self.node.uuid) + self.assertEqual(inst.node, self.node) + + self.api.create_port.assert_called_once_with( + network_id=self.api.get_network.return_value.id) + self.api.attach_port_to_node.assert_called_once_with( + self.node.uuid, self.api.create_port.return_value.id) + self.api.update_node.assert_called_once_with(self.node, self.updates) + self.api.validate_node.assert_called_once_with(self.node, + validate_deploy=True) + self.api.node_action.assert_called_once_with(self.node, 'active', + configdrive=mock.ANY) + self.assertFalse(self.wait_mock.called) + self.assertFalse(self.api.release_node.called) + self.assertFalse(self.api.delete_port.called) + def test_with_config(self): config = mock.MagicMock(spec=_config.InstanceConfig) inst = self.pr.provision_node(self.node, 'image',