Support for HTTP image location
Story: #2002048 Task: #19695 Change-Id: I75f33ebca3ea65274dcfcd8f4ddbd193f34706a9
This commit is contained in:
parent
f57e7547af
commit
6bdd479773
16
.zuul.yaml
16
.zuul.yaml
@ -146,6 +146,20 @@
|
||||
devstack_localrc:
|
||||
IRONIC_DEFAULT_DEPLOY_INTERFACE: direct
|
||||
|
||||
- job:
|
||||
name: metalsmith-integration-http-netboot-cirros-direct-py3
|
||||
description: |
|
||||
Integration job using HTTP as image source and direct deploy.
|
||||
parent: metalsmith-integration-base
|
||||
run: playbooks/integration/run.yaml
|
||||
vars:
|
||||
metalsmith_netboot: true
|
||||
metalsmith_python: python3
|
||||
metalsmith_use_http: true
|
||||
devstack_localrc:
|
||||
IRONIC_DEFAULT_DEPLOY_INTERFACE: direct
|
||||
USE_PYTHON3: true
|
||||
|
||||
- project:
|
||||
templates:
|
||||
- check-requirements
|
||||
@ -161,9 +175,11 @@
|
||||
- metalsmith-integration-glance-localboot-centos7
|
||||
- metalsmith-integration-glance-netboot-cirros-iscsi-py3
|
||||
- metalsmith-integration-glance-netboot-cirros-direct
|
||||
- metalsmith-integration-http-netboot-cirros-direct-py3
|
||||
gate:
|
||||
jobs:
|
||||
- openstack-tox-lower-constraints
|
||||
- metalsmith-integration-glance-localboot-centos7
|
||||
- metalsmith-integration-glance-netboot-cirros-iscsi-py3
|
||||
- metalsmith-integration-glance-netboot-cirros-direct
|
||||
- metalsmith-integration-http-netboot-cirros-direct-py3
|
||||
|
@ -23,11 +23,16 @@ from metalsmith import _config
|
||||
from metalsmith import _format
|
||||
from metalsmith import _provisioner
|
||||
from metalsmith import _utils
|
||||
from metalsmith import sources
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _is_http(smth):
|
||||
return smth.startswith('http://') or smth.startswith('https://')
|
||||
|
||||
|
||||
class NICAction(argparse.Action):
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
assert option_string in ('--port', '--network')
|
||||
@ -52,6 +57,18 @@ def _do_deploy(api, args, formatter):
|
||||
if args.hostname and not _utils.is_hostname_safe(args.hostname):
|
||||
raise RuntimeError("%s cannot be used as a hostname" % args.hostname)
|
||||
|
||||
if _is_http(args.image):
|
||||
if not args.image_checksum:
|
||||
raise RuntimeError("HTTP(s) images require --image-checksum")
|
||||
elif _is_http(args.image_checksum):
|
||||
source = sources.HttpWholeDiskImage(
|
||||
args.image, checksum_url=args.image_checksum)
|
||||
else:
|
||||
source = sources.HttpWholeDiskImage(
|
||||
args.image, checksum=args.image_checksum)
|
||||
else:
|
||||
source = args.image
|
||||
|
||||
config = _config.InstanceConfig(ssh_keys=ssh_keys)
|
||||
if args.user_name:
|
||||
config.add_user(args.user_name, sudo=args.passwordless_sudo)
|
||||
@ -61,7 +78,7 @@ def _do_deploy(api, args, formatter):
|
||||
capabilities=capabilities,
|
||||
candidates=args.candidate)
|
||||
instance = api.provision_node(node,
|
||||
image=args.image,
|
||||
image=source,
|
||||
nics=args.nics,
|
||||
root_disk_size=args.root_disk_size,
|
||||
config=config,
|
||||
@ -122,8 +139,10 @@ def _parse_args(args, config):
|
||||
'active')
|
||||
wait_grp.add_argument('--no-wait', action='store_true',
|
||||
help='disable waiting for deploy to finish')
|
||||
deploy.add_argument('--image', help='image to use (name or UUID)',
|
||||
deploy.add_argument('--image', help='image to use (name, UUID or URL)',
|
||||
required=True)
|
||||
deploy.add_argument('--image-checksum',
|
||||
help='image MD5 checksum or URL with checksums')
|
||||
deploy.add_argument('--network', help='network to use (name or UUID)',
|
||||
dest='nics', action=NICAction)
|
||||
deploy.add_argument('--port', help='port to attach (name or UUID)',
|
||||
|
@ -234,7 +234,7 @@ class Provisioner(object):
|
||||
if config is None:
|
||||
config = _config.InstanceConfig()
|
||||
if isinstance(image, six.string_types):
|
||||
image = sources.Glance(image)
|
||||
image = sources.GlanceImage(image)
|
||||
|
||||
node = self._check_node_for_deploy(node)
|
||||
created_ports = []
|
||||
|
@ -112,3 +112,16 @@ def validate_nics(nics):
|
||||
if unknown_nic_types:
|
||||
raise ValueError("Unexpected NIC type(s) %s, supported values are "
|
||||
"'port' and 'network'" % ', '.join(unknown_nic_types))
|
||||
|
||||
|
||||
def parse_checksums(checksums):
|
||||
"""Parse standard checksums file."""
|
||||
result = {}
|
||||
for line in checksums.split('\n'):
|
||||
if not line.strip():
|
||||
continue
|
||||
|
||||
checksum, fname = line.strip().split(None, 1)
|
||||
result[fname.strip().lstrip('*')] = checksum.strip()
|
||||
|
||||
return result
|
||||
|
@ -17,10 +17,14 @@
|
||||
|
||||
import abc
|
||||
import logging
|
||||
import os
|
||||
|
||||
import openstack.exceptions
|
||||
import requests
|
||||
import six
|
||||
from six.moves.urllib import parse as urlparse
|
||||
|
||||
from metalsmith import _utils
|
||||
from metalsmith import exceptions
|
||||
|
||||
|
||||
@ -38,7 +42,7 @@ class _Source(object):
|
||||
"""Updates required for a node to use this source."""
|
||||
|
||||
|
||||
class Glance(_Source):
|
||||
class GlanceImage(_Source):
|
||||
"""Image from the OpenStack Image service."""
|
||||
|
||||
def __init__(self, image):
|
||||
@ -73,3 +77,104 @@ class Glance(_Source):
|
||||
updates['/instance_info/%s' % prop] = value
|
||||
|
||||
return updates
|
||||
|
||||
|
||||
class HttpWholeDiskImage(_Source):
|
||||
"""A whole-disk image from HTTP(s) location.
|
||||
|
||||
Some deployment methods require a checksum of the image. It has to be
|
||||
provided via ``checksum`` or ``checksum_url``.
|
||||
|
||||
Only ``checksum_url`` (if provided) has to be accessible from the current
|
||||
machine. Other URLs have to be accessible by the Bare Metal service (more
|
||||
specifically, by **ironic-conductor** processes).
|
||||
"""
|
||||
|
||||
def __init__(self, url, checksum=None, checksum_url=None,
|
||||
kernel_url=None, ramdisk_url=None):
|
||||
"""Create an HTTP source.
|
||||
|
||||
:param url: URL of the image.
|
||||
:param checksum: MD5 checksum of the image. Mutually exclusive with
|
||||
``checksum_url``.
|
||||
:param checksum_url: URL of the checksum file for the image. Has to
|
||||
be in the standard format of the ``md5sum`` tool. Mutually
|
||||
exclusive with ``checksum``.
|
||||
"""
|
||||
if (checksum and checksum_url) or (not checksum and not checksum_url):
|
||||
raise TypeError('Exactly one of checksum and checksum_url has '
|
||||
'to be specified')
|
||||
|
||||
self.url = url
|
||||
self.checksum = checksum
|
||||
self.checksum_url = checksum_url
|
||||
self.kernel_url = kernel_url
|
||||
self.ramdisk_url = ramdisk_url
|
||||
|
||||
def _validate(self, connection):
|
||||
# TODO(dtantsur): should we validate image URLs here? Ironic will do it
|
||||
# as well, and images do not have to be accessible from where
|
||||
# metalsmith is running.
|
||||
if self.checksum:
|
||||
return
|
||||
|
||||
try:
|
||||
response = requests.get(self.checksum_url)
|
||||
response.raise_for_status()
|
||||
checksums = response.text
|
||||
except requests.RequestException as exc:
|
||||
raise exceptions.InvalidImage(
|
||||
'Cannot download checksum file %(url)s: %(err)s' %
|
||||
{'url': self.checksum_url, 'err': exc})
|
||||
|
||||
try:
|
||||
checksums = _utils.parse_checksums(checksums)
|
||||
except (ValueError, TypeError) as exc:
|
||||
raise exceptions.InvalidImage(
|
||||
'Invalid checksum file %(url)s: %(err)s' %
|
||||
{'url': self.checksum_url, 'err': exc})
|
||||
|
||||
fname = os.path.basename(urlparse.urlparse(self.url).path)
|
||||
try:
|
||||
self.checksum = checksums[fname]
|
||||
except KeyError:
|
||||
raise exceptions.InvalidImage(
|
||||
'There is no image checksum for %(fname)s in %(url)s' %
|
||||
{'fname': fname, 'url': self.checksum_url})
|
||||
|
||||
def _node_updates(self, connection):
|
||||
self._validate(connection)
|
||||
LOG.debug('Image: %(image)s, checksum %(checksum)s',
|
||||
{'image': self.url, 'checksum': self.checksum})
|
||||
return {
|
||||
'/instance_info/image_source': self.url,
|
||||
'/instance_info/image_checksum': self.checksum,
|
||||
}
|
||||
|
||||
|
||||
class HttpPartitionImage(HttpWholeDiskImage):
|
||||
"""A partition image from an HTTP(s) location."""
|
||||
|
||||
def __init__(self, url, kernel_url, ramdisk_url, checksum=None,
|
||||
checksum_url=None):
|
||||
"""Create an HTTP source.
|
||||
|
||||
:param url: URL of the root disk image.
|
||||
:param kernel_url: URL of the kernel image.
|
||||
:param ramdisk_url: URL of the initramfs image.
|
||||
:param checksum: MD5 checksum of the root disk image. Mutually
|
||||
exclusive with ``checksum_url``.
|
||||
:param checksum_url: URL of the checksum file for the root disk image.
|
||||
Has to be in the standard format of the ``md5sum`` tool. Mutually
|
||||
exclusive with ``checksum``.
|
||||
"""
|
||||
super(HttpPartitionImage, self).__init__(url, checksum=checksum,
|
||||
checksum_url=checksum_url)
|
||||
self.kernel_url = kernel_url
|
||||
self.ramdisk_url = ramdisk_url
|
||||
|
||||
def _node_updates(self, connection):
|
||||
updates = super(HttpPartitionImage, self)._node_updates(connection)
|
||||
updates['/instance_info/kernel'] = self.kernel_url
|
||||
updates['/instance_info/ramdisk'] = self.ramdisk_url
|
||||
return updates
|
||||
|
@ -25,6 +25,7 @@ from metalsmith import _cmd
|
||||
from metalsmith import _config
|
||||
from metalsmith import _instance
|
||||
from metalsmith import _provisioner
|
||||
from metalsmith import sources
|
||||
|
||||
|
||||
@mock.patch.object(_provisioner, 'Provisioner', autospec=True)
|
||||
@ -625,6 +626,72 @@ class TestDeploy(testtools.TestCase):
|
||||
netboot=False,
|
||||
wait=1800)
|
||||
|
||||
def test_args_http_image_with_checksum(self, mock_os_conf, mock_pr):
|
||||
args = ['deploy', '--image', 'https://example.com/image.img',
|
||||
'--image-checksum', '95e750180c7921ea0d545c7165db66b8',
|
||||
'--resource-class', 'compute']
|
||||
_cmd.main(args)
|
||||
mock_pr.assert_called_once_with(
|
||||
cloud_region=mock_os_conf.return_value.get_one.return_value,
|
||||
dry_run=False)
|
||||
mock_pr.return_value.reserve_node.assert_called_once_with(
|
||||
resource_class='compute',
|
||||
conductor_group=None,
|
||||
capabilities={},
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
mock_pr.return_value.reserve_node.return_value,
|
||||
image=mock.ANY,
|
||||
nics=None,
|
||||
root_disk_size=None,
|
||||
config=mock.ANY,
|
||||
hostname=None,
|
||||
netboot=False,
|
||||
wait=1800)
|
||||
source = mock_pr.return_value.provision_node.call_args[1]['image']
|
||||
self.assertIsInstance(source, sources.HttpWholeDiskImage)
|
||||
self.assertEqual('https://example.com/image.img', source.url)
|
||||
self.assertEqual('95e750180c7921ea0d545c7165db66b8', source.checksum)
|
||||
|
||||
def test_args_http_image_with_checksum_url(self, mock_os_conf, mock_pr):
|
||||
args = ['deploy', '--image', 'http://example.com/image.img',
|
||||
'--image-checksum', 'http://example.com/CHECKSUMS',
|
||||
'--resource-class', 'compute']
|
||||
_cmd.main(args)
|
||||
mock_pr.assert_called_once_with(
|
||||
cloud_region=mock_os_conf.return_value.get_one.return_value,
|
||||
dry_run=False)
|
||||
mock_pr.return_value.reserve_node.assert_called_once_with(
|
||||
resource_class='compute',
|
||||
conductor_group=None,
|
||||
capabilities={},
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
mock_pr.return_value.reserve_node.return_value,
|
||||
image=mock.ANY,
|
||||
nics=None,
|
||||
root_disk_size=None,
|
||||
config=mock.ANY,
|
||||
hostname=None,
|
||||
netboot=False,
|
||||
wait=1800)
|
||||
source = mock_pr.return_value.provision_node.call_args[1]['image']
|
||||
self.assertIsInstance(source, sources.HttpWholeDiskImage)
|
||||
self.assertEqual('http://example.com/image.img', source.url)
|
||||
self.assertEqual('http://example.com/CHECKSUMS', source.checksum_url)
|
||||
|
||||
@mock.patch.object(_cmd.LOG, 'critical', autospec=True)
|
||||
def test_args_http_image_without_checksum(self, mock_log, mock_os_conf,
|
||||
mock_pr):
|
||||
args = ['deploy', '--image', 'http://example.com/image.img',
|
||||
'--resource-class', 'compute']
|
||||
self.assertRaises(SystemExit, _cmd.main, args)
|
||||
self.assertTrue(mock_log.called)
|
||||
self.assertFalse(mock_pr.return_value.reserve_node.called)
|
||||
self.assertFalse(mock_pr.return_value.provision_node.called)
|
||||
|
||||
def test_args_custom_wait(self, mock_os_conf, mock_pr):
|
||||
args = ['deploy', '--network', 'mynet', '--image', 'myimg',
|
||||
'--wait', '3600', '--resource-class', 'compute']
|
||||
|
@ -16,6 +16,7 @@
|
||||
import fixtures
|
||||
import mock
|
||||
from openstack import exceptions as os_exc
|
||||
import requests
|
||||
import testtools
|
||||
|
||||
from metalsmith import _config
|
||||
@ -280,7 +281,7 @@ class TestProvisionNode(Base):
|
||||
self.assertFalse(self.conn.network.delete_port.called)
|
||||
|
||||
def test_ok_with_source(self):
|
||||
inst = self.pr.provision_node(self.node, sources.Glance('image'),
|
||||
inst = self.pr.provision_node(self.node, sources.GlanceImage('image'),
|
||||
[{'network': 'network'}])
|
||||
|
||||
self.assertEqual(inst.uuid, self.node.uuid)
|
||||
@ -434,6 +435,100 @@ class TestProvisionNode(Base):
|
||||
self.assertFalse(self.api.release_node.called)
|
||||
self.assertFalse(self.conn.network.delete_port.called)
|
||||
|
||||
def test_with_http_and_checksum_whole_disk(self):
|
||||
self.updates['/instance_info/image_source'] = 'https://host/image'
|
||||
self.updates['/instance_info/image_checksum'] = 'abcd'
|
||||
del self.updates['/instance_info/kernel']
|
||||
del self.updates['/instance_info/ramdisk']
|
||||
|
||||
inst = self.pr.provision_node(
|
||||
self.node,
|
||||
sources.HttpWholeDiskImage('https://host/image', checksum='abcd'),
|
||||
[{'network': 'network'}])
|
||||
|
||||
self.assertEqual(inst.uuid, self.node.uuid)
|
||||
self.assertEqual(inst.node, self.node)
|
||||
|
||||
self.assertFalse(self.conn.image.find_image.called)
|
||||
self.conn.network.create_port.assert_called_once_with(
|
||||
network_id=self.conn.network.find_network.return_value.id)
|
||||
self.api.attach_port_to_node.assert_called_once_with(
|
||||
self.node.uuid, self.conn.network.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.conn.network.delete_port.called)
|
||||
|
||||
@mock.patch.object(requests, 'get', autospec=True)
|
||||
def test_with_http_and_checksum_url(self, mock_get):
|
||||
self.updates['/instance_info/image_source'] = 'https://host/image'
|
||||
self.updates['/instance_info/image_checksum'] = 'abcd'
|
||||
del self.updates['/instance_info/kernel']
|
||||
del self.updates['/instance_info/ramdisk']
|
||||
mock_get.return_value.text = """
|
||||
defg *something else
|
||||
abcd image
|
||||
"""
|
||||
|
||||
inst = self.pr.provision_node(
|
||||
self.node,
|
||||
sources.HttpWholeDiskImage('https://host/image',
|
||||
checksum_url='https://host/checksums'),
|
||||
[{'network': 'network'}])
|
||||
|
||||
self.assertEqual(inst.uuid, self.node.uuid)
|
||||
self.assertEqual(inst.node, self.node)
|
||||
|
||||
self.assertFalse(self.conn.image.find_image.called)
|
||||
mock_get.assert_called_once_with('https://host/checksums')
|
||||
self.conn.network.create_port.assert_called_once_with(
|
||||
network_id=self.conn.network.find_network.return_value.id)
|
||||
self.api.attach_port_to_node.assert_called_once_with(
|
||||
self.node.uuid, self.conn.network.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.conn.network.delete_port.called)
|
||||
|
||||
def test_with_http_and_checksum_partition(self):
|
||||
self.updates['/instance_info/image_source'] = 'https://host/image'
|
||||
self.updates['/instance_info/image_checksum'] = 'abcd'
|
||||
self.updates['/instance_info/kernel'] = 'https://host/kernel'
|
||||
self.updates['/instance_info/ramdisk'] = 'https://host/ramdisk'
|
||||
|
||||
inst = self.pr.provision_node(
|
||||
self.node,
|
||||
sources.HttpPartitionImage('https://host/image',
|
||||
checksum='abcd',
|
||||
kernel_url='https://host/kernel',
|
||||
ramdisk_url='https://host/ramdisk'),
|
||||
[{'network': 'network'}])
|
||||
|
||||
self.assertEqual(inst.uuid, self.node.uuid)
|
||||
self.assertEqual(inst.node, self.node)
|
||||
|
||||
self.assertFalse(self.conn.image.find_image.called)
|
||||
self.conn.network.create_port.assert_called_once_with(
|
||||
network_id=self.conn.network.find_network.return_value.id)
|
||||
self.api.attach_port_to_node.assert_called_once_with(
|
||||
self.node.uuid, self.conn.network.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.conn.network.delete_port.called)
|
||||
|
||||
def test_with_root_disk_size(self):
|
||||
self.updates['/instance_info/root_gb'] = 50
|
||||
|
||||
@ -700,6 +795,82 @@ class TestProvisionNode(Base):
|
||||
self.assertFalse(self.api.node_action.called)
|
||||
self.api.release_node.assert_called_once_with(self.node)
|
||||
|
||||
@mock.patch.object(requests, 'get', autospec=True)
|
||||
def test_no_checksum_with_http_image(self, mock_get):
|
||||
self.updates['/instance_info/image_source'] = 'https://host/image'
|
||||
self.updates['/instance_info/image_checksum'] = 'abcd'
|
||||
del self.updates['/instance_info/kernel']
|
||||
del self.updates['/instance_info/ramdisk']
|
||||
mock_get.return_value.text = """
|
||||
defg *something else
|
||||
abcd and-not-image-again
|
||||
"""
|
||||
|
||||
self.assertRaisesRegex(exceptions.InvalidImage,
|
||||
'no image checksum',
|
||||
self.pr.provision_node,
|
||||
self.node,
|
||||
sources.HttpWholeDiskImage(
|
||||
'https://host/image',
|
||||
checksum_url='https://host/checksums'),
|
||||
[{'network': 'network'}])
|
||||
|
||||
self.assertFalse(self.conn.image.find_image.called)
|
||||
mock_get.assert_called_once_with('https://host/checksums')
|
||||
self.api.update_node.assert_called_once_with(self.node, CLEAN_UP)
|
||||
self.assertFalse(self.api.node_action.called)
|
||||
self.api.release_node.assert_called_once_with(self.node)
|
||||
|
||||
@mock.patch.object(requests, 'get', autospec=True)
|
||||
def test_malformed_checksum_with_http_image(self, mock_get):
|
||||
self.updates['/instance_info/image_source'] = 'https://host/image'
|
||||
self.updates['/instance_info/image_checksum'] = 'abcd'
|
||||
del self.updates['/instance_info/kernel']
|
||||
del self.updates['/instance_info/ramdisk']
|
||||
mock_get.return_value.text = """
|
||||
<html>
|
||||
<p>I am not a checksum file!</p>
|
||||
</html>"""
|
||||
|
||||
self.assertRaisesRegex(exceptions.InvalidImage,
|
||||
'Invalid checksum file',
|
||||
self.pr.provision_node,
|
||||
self.node,
|
||||
sources.HttpWholeDiskImage(
|
||||
'https://host/image',
|
||||
checksum_url='https://host/checksums'),
|
||||
[{'network': 'network'}])
|
||||
|
||||
self.assertFalse(self.conn.image.find_image.called)
|
||||
mock_get.assert_called_once_with('https://host/checksums')
|
||||
self.api.update_node.assert_called_once_with(self.node, CLEAN_UP)
|
||||
self.assertFalse(self.api.node_action.called)
|
||||
self.api.release_node.assert_called_once_with(self.node)
|
||||
|
||||
@mock.patch.object(requests, 'get', autospec=True)
|
||||
def test_cannot_download_checksum_with_http_image(self, mock_get):
|
||||
self.updates['/instance_info/image_source'] = 'https://host/image'
|
||||
self.updates['/instance_info/image_checksum'] = 'abcd'
|
||||
del self.updates['/instance_info/kernel']
|
||||
del self.updates['/instance_info/ramdisk']
|
||||
mock_get.return_value.raise_for_status.side_effect = (
|
||||
requests.RequestException("boom"))
|
||||
|
||||
self.assertRaisesRegex(exceptions.InvalidImage,
|
||||
'Cannot download checksum file',
|
||||
self.pr.provision_node,
|
||||
self.node,
|
||||
sources.HttpWholeDiskImage(
|
||||
'https://host/image',
|
||||
checksum_url='https://host/checksums'),
|
||||
[{'network': 'network'}])
|
||||
|
||||
self.assertFalse(self.conn.image.find_image.called)
|
||||
mock_get.assert_called_once_with('https://host/checksums')
|
||||
self.api.update_node.assert_called_once_with(self.node, CLEAN_UP)
|
||||
self.assertFalse(self.api.node_action.called)
|
||||
self.api.release_node.assert_called_once_with(self.node)
|
||||
|
||||
def test_invalid_network(self):
|
||||
self.conn.network.find_network.side_effect = RuntimeError('Not found')
|
||||
self.assertRaisesRegex(exceptions.InvalidNIC, 'Not found',
|
||||
@ -835,6 +1006,20 @@ class TestProvisionNode(Base):
|
||||
self.assertFalse(self.api.node_action.called)
|
||||
self.assertFalse(self.api.release_node.called)
|
||||
|
||||
def test_invalid_http_source(self):
|
||||
self.assertRaises(TypeError, sources.HttpWholeDiskImage,
|
||||
'http://host/image')
|
||||
self.assertRaises(TypeError, sources.HttpWholeDiskImage,
|
||||
'http://host/image', checksum='abcd',
|
||||
checksum_url='http://host/checksum')
|
||||
self.assertRaises(TypeError, sources.HttpPartitionImage,
|
||||
'http://host/image', 'http://host/kernel',
|
||||
'http://host/ramdisk')
|
||||
self.assertRaises(TypeError, sources.HttpPartitionImage,
|
||||
'http://host/image', 'http://host/kernel',
|
||||
'http://host/ramdisk', checksum='abcd',
|
||||
checksum_url='http://host/checksum')
|
||||
|
||||
|
||||
class TestUnprovisionNode(Base):
|
||||
|
||||
|
@ -1,18 +1,39 @@
|
||||
- name: Find Cirros UEC image
|
||||
shell: |
|
||||
openstack image list -f value -c ID -c Name \
|
||||
| awk '/cirros.*uec/ { print $1; exit 0; }'
|
||||
shell: openstack image list -f value -c Name | grep 'cirros-.*-uec$'
|
||||
register: cirros_uec_image_result
|
||||
failed_when: cirros_uec_image_result.stdout == ""
|
||||
|
||||
- name: Find Cirros disk image
|
||||
shell: |
|
||||
openstack image list -f value -c ID -c Name \
|
||||
| awk '/cirros.*disk/ { print $1; exit 0; }'
|
||||
shell: openstack image list -f value -c Name | grep 'cirros-.*-disk$'
|
||||
register: cirros_disk_image_result
|
||||
failed_when: cirros_disk_image_result.stdout == ""
|
||||
|
||||
- name: Set image facts
|
||||
- name: Set image facts for Glance image
|
||||
set_fact:
|
||||
metalsmith_whole_disk_image: "{{ cirros_disk_image_result.stdout }}"
|
||||
metalsmith_partition_image: "{{ cirros_uec_image_result.stdout }}"
|
||||
when: not (metalsmith_use_http | default(false))
|
||||
|
||||
- block:
|
||||
- name: Get baremetal HTTP endpoint
|
||||
shell: |
|
||||
source /opt/stack/devstack/openrc admin admin > /dev/null
|
||||
iniget /etc/ironic/ironic.conf deploy http_url
|
||||
args:
|
||||
executable: /bin/bash
|
||||
register: baremetal_endpoint_result
|
||||
failed_when: baremetal_endpoint_result.stdout == ""
|
||||
|
||||
- name: Calculate MD5 checksum for HTTP disk image
|
||||
shell: |
|
||||
md5sum /opt/stack/devstack/files/{{ cirros_disk_image_result.stdout }}.img \
|
||||
| awk '{ print $1; }'
|
||||
register: cirros_disk_image_checksum_result
|
||||
failed_when: cirros_disk_image_checksum_result.stdout == ""
|
||||
|
||||
- name: Set facts for HTTP image
|
||||
set_fact:
|
||||
metalsmith_whole_disk_image: "{{ baremetal_endpoint_result.stdout}}/{{ cirros_disk_image_result.stdout }}.img"
|
||||
metalsmith_whole_disk_checksum: "{{ cirros_disk_image_checksum_result.stdout }}"
|
||||
|
||||
when: metalsmith_use_http | default(false)
|
||||
|
@ -23,6 +23,7 @@
|
||||
metalsmith_instances:
|
||||
- hostname: test
|
||||
image: "{{ image }}"
|
||||
image_checksum: "{{ image_checksum | default('') }}"
|
||||
nics:
|
||||
- "{{ nic }}"
|
||||
ssh_public_keys:
|
||||
|
@ -8,14 +8,18 @@
|
||||
- include: cirros-image.yaml
|
||||
when: metalsmith_whole_disk_image is not defined
|
||||
|
||||
- name: Test a partition image
|
||||
include: exercise.yaml
|
||||
vars:
|
||||
image: "{{ metalsmith_partition_image }}"
|
||||
precreate_port: false
|
||||
|
||||
- name: Test a whole-disk image
|
||||
include: exercise.yaml
|
||||
vars:
|
||||
image: "{{ metalsmith_whole_disk_image }}"
|
||||
image_checksum: "{{ metalsmith_whole_disk_checksum | default('') }}"
|
||||
precreate_port: false
|
||||
|
||||
- name: Test a partition image
|
||||
include: exercise.yaml
|
||||
vars:
|
||||
image: "{{ metalsmith_partition_image }}"
|
||||
image_checksum: "{{ metalsmith_partition_checksum | default('') }}"
|
||||
precreate_port: false
|
||||
# FIXME(dtantsur): cover partition images
|
||||
when: not (metalsmith_use_http | default(false))
|
||||
|
@ -4,4 +4,5 @@
|
||||
pbr!=2.1.0,>=2.0.0 # Apache-2.0
|
||||
openstacksdk>=0.11.0 # Apache-2.0
|
||||
python-ironicclient>=1.14.0 # Apache-2.0
|
||||
requests>=2.18.4 # Apache-2.0
|
||||
six>=1.10.0 # MIT
|
||||
|
@ -23,6 +23,8 @@ The following optional variables provide the defaults for Instance_ attributes:
|
||||
the default for ``extra_args``.
|
||||
``metalsmith_image``
|
||||
the default for ``image``.
|
||||
``metalsmith_image_checksum``
|
||||
the default for ``image_checksum``.
|
||||
``metalsmith_netboot``
|
||||
the default for ``netboot``
|
||||
``metalsmith_nics``
|
||||
@ -53,7 +55,9 @@ Each instances has the following attributes:
|
||||
``extra_args`` (defaults to ``metalsmith_extra_args``)
|
||||
additional arguments to pass to the ``metalsmith`` CLI on all calls.
|
||||
``image`` (defaults to ``metalsmith_image``)
|
||||
UUID or name of the image to use for deployment. Mandatory.
|
||||
UUID, name or HTTP(s) URL of the image to use for deployment. Mandatory.
|
||||
``image_checksum`` (defaults to ``metalsmith_image_checksum``)
|
||||
MD5 checksum or checksum file URL for an HTTP(s) image.
|
||||
``netboot``
|
||||
whether to boot the deployed instance from network (PXE, iPXE, etc).
|
||||
The default is to use local boot (requires a bootloader on the image).
|
||||
|
@ -3,6 +3,7 @@ metalsmith_candidates: []
|
||||
metalsmith_capabilities: {}
|
||||
metalsmith_conductor_group:
|
||||
metalsmith_extra_args:
|
||||
metalsmith_image_checksum:
|
||||
metalsmith_netboot: false
|
||||
metalsmith_nics: []
|
||||
metalsmith_resource_class:
|
||||
|
@ -34,6 +34,9 @@
|
||||
{% for node in candidates %}
|
||||
--candidate {{ node }}
|
||||
{% endfor %}
|
||||
{% if image_checksum %}
|
||||
--image-checksum {{ image_checksum }}
|
||||
{% endif %}
|
||||
when: state == 'present'
|
||||
vars:
|
||||
candidates: "{{ instance.candidates | default(metalsmith_candidates) }}"
|
||||
@ -41,6 +44,7 @@
|
||||
conductor_group: "{{ instance.conductor_group | default(metalsmith_conductor_group) }}"
|
||||
extra_args: "{{ instance.extra_args | default(metalsmith_extra_args) }}"
|
||||
image: "{{ instance.image | default(metalsmith_image) }}"
|
||||
image: "{{ instance.image_checksum | default(metalsmith_image_checksum) }}"
|
||||
netboot: "{{ instance.netboot | default(metalsmith_netboot) }}"
|
||||
nics: "{{ instance.nics | default(metalsmith_nics) }}"
|
||||
resource_class: "{{ instance.resource_class | default(metalsmith_resource_class) }}"
|
||||
|
Loading…
x
Reference in New Issue
Block a user