Foundation for different deployment sources

Change-Id: I7c9538e37476d9d3ea5b9cc403419dda95cf77cc
Story: #2002048
Task: #26064
This commit is contained in:
Dmitry Tantsur 2018-09-04 17:19:35 +02:00
parent 1f92d9a5fb
commit a34d0e0951
3 changed files with 103 additions and 16 deletions

View File

@ -26,6 +26,7 @@ from metalsmith import _os_api
from metalsmith import _scheduler from metalsmith import _scheduler
from metalsmith import _utils from metalsmith import _utils
from metalsmith import exceptions from metalsmith import exceptions
from metalsmith import sources
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -178,7 +179,8 @@ class Provisioner(object):
:param node: Node object, UUID or name. Will be reserved first, if :param node: Node object, UUID or name. Will be reserved first, if
not reserved already. Must be in the "available" state with not reserved already. Must be in the "available" state with
maintenance mode off. 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. :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: Each item is a dict with a key describing the type of the NIC:
either a port (``{"port": "<port name or ID>"}``) or a network either a port (``{"port": "<port name or ID>"}``) or a network
@ -203,6 +205,9 @@ class Provisioner(object):
""" """
if config is None: if config is None:
config = _config.InstanceConfig() config = _config.InstanceConfig()
if isinstance(image, six.string_types):
image = sources.Glance(image)
node = self._check_node_for_deploy(node) node = self._check_node_for_deploy(node)
created_ports = [] created_ports = []
attached_ports = [] attached_ports = []
@ -211,14 +216,7 @@ class Provisioner(object):
hostname = self._check_hostname(node, hostname) hostname = self._check_hostname(node, hostname)
root_disk_size = _utils.get_root_disk(root_disk_size, node) root_disk_size = _utils.get_root_disk(root_disk_size, node)
try: image._validate(self._api)
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)
nics = self._get_nics(nics or []) nics = self._get_nics(nics or [])
@ -235,17 +233,12 @@ class Provisioner(object):
capabilities['boot_option'] = 'netboot' if netboot else 'local' capabilities['boot_option'] = 'netboot' if netboot else 'local'
updates = {'/instance_info/image_source': image.id, updates = {'/instance_info/root_gb': root_disk_size,
'/instance_info/root_gb': root_disk_size,
'/instance_info/capabilities': capabilities, '/instance_info/capabilities': capabilities,
'/extra/%s' % _CREATED_PORTS: created_ports, '/extra/%s' % _CREATED_PORTS: created_ports,
'/extra/%s' % _ATTACHED_PORTS: attached_ports, '/extra/%s' % _ATTACHED_PORTS: attached_ports,
'/instance_info/%s' % _os_api.HOSTNAME_FIELD: hostname} '/instance_info/%s' % _os_api.HOSTNAME_FIELD: hostname}
updates.update(image._node_updates(self._api))
for prop in ('kernel', 'ramdisk'):
value = getattr(image, '%s_id' % prop, None)
if value:
updates['/instance_info/%s' % prop] = value
LOG.debug('Updating node %(node)s with %(updates)s', LOG.debug('Updating node %(node)s with %(updates)s',
{'node': _utils.log_node(node), 'updates': updates}) {'node': _utils.log_node(node), 'updates': updates})

73
metalsmith/sources.py Normal file
View File

@ -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

View File

@ -22,6 +22,7 @@ from metalsmith import _instance
from metalsmith import _os_api from metalsmith import _os_api
from metalsmith import _provisioner from metalsmith import _provisioner
from metalsmith import exceptions from metalsmith import exceptions
from metalsmith import sources
class Base(testtools.TestCase): class Base(testtools.TestCase):
@ -206,6 +207,26 @@ class TestProvisionNode(Base):
self.assertFalse(self.api.release_node.called) self.assertFalse(self.api.release_node.called)
self.assertFalse(self.api.delete_port.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): def test_with_config(self):
config = mock.MagicMock(spec=_config.InstanceConfig) config = mock.MagicMock(spec=_config.InstanceConfig)
inst = self.pr.provision_node(self.node, 'image', inst = self.pr.provision_node(self.node, 'image',