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:
|
devstack_localrc:
|
||||||
IRONIC_DEFAULT_DEPLOY_INTERFACE: direct
|
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:
|
- project:
|
||||||
templates:
|
templates:
|
||||||
- check-requirements
|
- check-requirements
|
||||||
|
@ -161,9 +175,11 @@
|
||||||
- metalsmith-integration-glance-localboot-centos7
|
- metalsmith-integration-glance-localboot-centos7
|
||||||
- metalsmith-integration-glance-netboot-cirros-iscsi-py3
|
- metalsmith-integration-glance-netboot-cirros-iscsi-py3
|
||||||
- metalsmith-integration-glance-netboot-cirros-direct
|
- metalsmith-integration-glance-netboot-cirros-direct
|
||||||
|
- metalsmith-integration-http-netboot-cirros-direct-py3
|
||||||
gate:
|
gate:
|
||||||
jobs:
|
jobs:
|
||||||
- openstack-tox-lower-constraints
|
- openstack-tox-lower-constraints
|
||||||
- metalsmith-integration-glance-localboot-centos7
|
- metalsmith-integration-glance-localboot-centos7
|
||||||
- metalsmith-integration-glance-netboot-cirros-iscsi-py3
|
- metalsmith-integration-glance-netboot-cirros-iscsi-py3
|
||||||
- metalsmith-integration-glance-netboot-cirros-direct
|
- 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 _format
|
||||||
from metalsmith import _provisioner
|
from metalsmith import _provisioner
|
||||||
from metalsmith import _utils
|
from metalsmith import _utils
|
||||||
|
from metalsmith import sources
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_http(smth):
|
||||||
|
return smth.startswith('http://') or smth.startswith('https://')
|
||||||
|
|
||||||
|
|
||||||
class NICAction(argparse.Action):
|
class NICAction(argparse.Action):
|
||||||
def __call__(self, parser, namespace, values, option_string=None):
|
def __call__(self, parser, namespace, values, option_string=None):
|
||||||
assert option_string in ('--port', '--network')
|
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):
|
if args.hostname and not _utils.is_hostname_safe(args.hostname):
|
||||||
raise RuntimeError("%s cannot be used as a hostname" % 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)
|
config = _config.InstanceConfig(ssh_keys=ssh_keys)
|
||||||
if args.user_name:
|
if args.user_name:
|
||||||
config.add_user(args.user_name, sudo=args.passwordless_sudo)
|
config.add_user(args.user_name, sudo=args.passwordless_sudo)
|
||||||
|
@ -61,7 +78,7 @@ def _do_deploy(api, args, formatter):
|
||||||
capabilities=capabilities,
|
capabilities=capabilities,
|
||||||
candidates=args.candidate)
|
candidates=args.candidate)
|
||||||
instance = api.provision_node(node,
|
instance = api.provision_node(node,
|
||||||
image=args.image,
|
image=source,
|
||||||
nics=args.nics,
|
nics=args.nics,
|
||||||
root_disk_size=args.root_disk_size,
|
root_disk_size=args.root_disk_size,
|
||||||
config=config,
|
config=config,
|
||||||
|
@ -122,8 +139,10 @@ def _parse_args(args, config):
|
||||||
'active')
|
'active')
|
||||||
wait_grp.add_argument('--no-wait', action='store_true',
|
wait_grp.add_argument('--no-wait', action='store_true',
|
||||||
help='disable waiting for deploy to finish')
|
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)
|
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)',
|
deploy.add_argument('--network', help='network to use (name or UUID)',
|
||||||
dest='nics', action=NICAction)
|
dest='nics', action=NICAction)
|
||||||
deploy.add_argument('--port', help='port to attach (name or UUID)',
|
deploy.add_argument('--port', help='port to attach (name or UUID)',
|
||||||
|
|
|
@ -234,7 +234,7 @@ class Provisioner(object):
|
||||||
if config is None:
|
if config is None:
|
||||||
config = _config.InstanceConfig()
|
config = _config.InstanceConfig()
|
||||||
if isinstance(image, six.string_types):
|
if isinstance(image, six.string_types):
|
||||||
image = sources.Glance(image)
|
image = sources.GlanceImage(image)
|
||||||
|
|
||||||
node = self._check_node_for_deploy(node)
|
node = self._check_node_for_deploy(node)
|
||||||
created_ports = []
|
created_ports = []
|
||||||
|
|
|
@ -112,3 +112,16 @@ def validate_nics(nics):
|
||||||
if unknown_nic_types:
|
if unknown_nic_types:
|
||||||
raise ValueError("Unexpected NIC type(s) %s, supported values are "
|
raise ValueError("Unexpected NIC type(s) %s, supported values are "
|
||||||
"'port' and 'network'" % ', '.join(unknown_nic_types))
|
"'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 abc
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
import openstack.exceptions
|
import openstack.exceptions
|
||||||
|
import requests
|
||||||
import six
|
import six
|
||||||
|
from six.moves.urllib import parse as urlparse
|
||||||
|
|
||||||
|
from metalsmith import _utils
|
||||||
from metalsmith import exceptions
|
from metalsmith import exceptions
|
||||||
|
|
||||||
|
|
||||||
|
@ -38,7 +42,7 @@ class _Source(object):
|
||||||
"""Updates required for a node to use this source."""
|
"""Updates required for a node to use this source."""
|
||||||
|
|
||||||
|
|
||||||
class Glance(_Source):
|
class GlanceImage(_Source):
|
||||||
"""Image from the OpenStack Image service."""
|
"""Image from the OpenStack Image service."""
|
||||||
|
|
||||||
def __init__(self, image):
|
def __init__(self, image):
|
||||||
|
@ -73,3 +77,104 @@ class Glance(_Source):
|
||||||
updates['/instance_info/%s' % prop] = value
|
updates['/instance_info/%s' % prop] = value
|
||||||
|
|
||||||
return updates
|
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 _config
|
||||||
from metalsmith import _instance
|
from metalsmith import _instance
|
||||||
from metalsmith import _provisioner
|
from metalsmith import _provisioner
|
||||||
|
from metalsmith import sources
|
||||||
|
|
||||||
|
|
||||||
@mock.patch.object(_provisioner, 'Provisioner', autospec=True)
|
@mock.patch.object(_provisioner, 'Provisioner', autospec=True)
|
||||||
|
@ -625,6 +626,72 @@ class TestDeploy(testtools.TestCase):
|
||||||
netboot=False,
|
netboot=False,
|
||||||
wait=1800)
|
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):
|
def test_args_custom_wait(self, mock_os_conf, mock_pr):
|
||||||
args = ['deploy', '--network', 'mynet', '--image', 'myimg',
|
args = ['deploy', '--network', 'mynet', '--image', 'myimg',
|
||||||
'--wait', '3600', '--resource-class', 'compute']
|
'--wait', '3600', '--resource-class', 'compute']
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
import fixtures
|
import fixtures
|
||||||
import mock
|
import mock
|
||||||
from openstack import exceptions as os_exc
|
from openstack import exceptions as os_exc
|
||||||
|
import requests
|
||||||
import testtools
|
import testtools
|
||||||
|
|
||||||
from metalsmith import _config
|
from metalsmith import _config
|
||||||
|
@ -280,7 +281,7 @@ class TestProvisionNode(Base):
|
||||||
self.assertFalse(self.conn.network.delete_port.called)
|
self.assertFalse(self.conn.network.delete_port.called)
|
||||||
|
|
||||||
def test_ok_with_source(self):
|
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'}])
|
[{'network': 'network'}])
|
||||||
|
|
||||||
self.assertEqual(inst.uuid, self.node.uuid)
|
self.assertEqual(inst.uuid, self.node.uuid)
|
||||||
|
@ -434,6 +435,100 @@ class TestProvisionNode(Base):
|
||||||
self.assertFalse(self.api.release_node.called)
|
self.assertFalse(self.api.release_node.called)
|
||||||
self.assertFalse(self.conn.network.delete_port.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):
|
def test_with_root_disk_size(self):
|
||||||
self.updates['/instance_info/root_gb'] = 50
|
self.updates['/instance_info/root_gb'] = 50
|
||||||
|
|
||||||
|
@ -700,6 +795,82 @@ class TestProvisionNode(Base):
|
||||||
self.assertFalse(self.api.node_action.called)
|
self.assertFalse(self.api.node_action.called)
|
||||||
self.api.release_node.assert_called_once_with(self.node)
|
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):
|
def test_invalid_network(self):
|
||||||
self.conn.network.find_network.side_effect = RuntimeError('Not found')
|
self.conn.network.find_network.side_effect = RuntimeError('Not found')
|
||||||
self.assertRaisesRegex(exceptions.InvalidNIC, '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.node_action.called)
|
||||||
self.assertFalse(self.api.release_node.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):
|
class TestUnprovisionNode(Base):
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,39 @@
|
||||||
- name: Find Cirros UEC image
|
- name: Find Cirros UEC image
|
||||||
shell: |
|
shell: openstack image list -f value -c Name | grep 'cirros-.*-uec$'
|
||||||
openstack image list -f value -c ID -c Name \
|
|
||||||
| awk '/cirros.*uec/ { print $1; exit 0; }'
|
|
||||||
register: cirros_uec_image_result
|
register: cirros_uec_image_result
|
||||||
failed_when: cirros_uec_image_result.stdout == ""
|
failed_when: cirros_uec_image_result.stdout == ""
|
||||||
|
|
||||||
- name: Find Cirros disk image
|
- name: Find Cirros disk image
|
||||||
shell: |
|
shell: openstack image list -f value -c Name | grep 'cirros-.*-disk$'
|
||||||
openstack image list -f value -c ID -c Name \
|
|
||||||
| awk '/cirros.*disk/ { print $1; exit 0; }'
|
|
||||||
register: cirros_disk_image_result
|
register: cirros_disk_image_result
|
||||||
failed_when: cirros_disk_image_result.stdout == ""
|
failed_when: cirros_disk_image_result.stdout == ""
|
||||||
|
|
||||||
- name: Set image facts
|
- name: Set image facts for Glance image
|
||||||
set_fact:
|
set_fact:
|
||||||
metalsmith_whole_disk_image: "{{ cirros_disk_image_result.stdout }}"
|
metalsmith_whole_disk_image: "{{ cirros_disk_image_result.stdout }}"
|
||||||
metalsmith_partition_image: "{{ cirros_uec_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:
|
metalsmith_instances:
|
||||||
- hostname: test
|
- hostname: test
|
||||||
image: "{{ image }}"
|
image: "{{ image }}"
|
||||||
|
image_checksum: "{{ image_checksum | default('') }}"
|
||||||
nics:
|
nics:
|
||||||
- "{{ nic }}"
|
- "{{ nic }}"
|
||||||
ssh_public_keys:
|
ssh_public_keys:
|
||||||
|
|
|
@ -8,14 +8,18 @@
|
||||||
- include: cirros-image.yaml
|
- include: cirros-image.yaml
|
||||||
when: metalsmith_whole_disk_image is not defined
|
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
|
- name: Test a whole-disk image
|
||||||
include: exercise.yaml
|
include: exercise.yaml
|
||||||
vars:
|
vars:
|
||||||
image: "{{ metalsmith_whole_disk_image }}"
|
image: "{{ metalsmith_whole_disk_image }}"
|
||||||
|
image_checksum: "{{ metalsmith_whole_disk_checksum | default('') }}"
|
||||||
precreate_port: false
|
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
|
pbr!=2.1.0,>=2.0.0 # Apache-2.0
|
||||||
openstacksdk>=0.11.0 # Apache-2.0
|
openstacksdk>=0.11.0 # Apache-2.0
|
||||||
python-ironicclient>=1.14.0 # Apache-2.0
|
python-ironicclient>=1.14.0 # Apache-2.0
|
||||||
|
requests>=2.18.4 # Apache-2.0
|
||||||
six>=1.10.0 # MIT
|
six>=1.10.0 # MIT
|
||||||
|
|
|
@ -23,6 +23,8 @@ The following optional variables provide the defaults for Instance_ attributes:
|
||||||
the default for ``extra_args``.
|
the default for ``extra_args``.
|
||||||
``metalsmith_image``
|
``metalsmith_image``
|
||||||
the default for ``image``.
|
the default for ``image``.
|
||||||
|
``metalsmith_image_checksum``
|
||||||
|
the default for ``image_checksum``.
|
||||||
``metalsmith_netboot``
|
``metalsmith_netboot``
|
||||||
the default for ``netboot``
|
the default for ``netboot``
|
||||||
``metalsmith_nics``
|
``metalsmith_nics``
|
||||||
|
@ -53,7 +55,9 @@ Each instances has the following attributes:
|
||||||
``extra_args`` (defaults to ``metalsmith_extra_args``)
|
``extra_args`` (defaults to ``metalsmith_extra_args``)
|
||||||
additional arguments to pass to the ``metalsmith`` CLI on all calls.
|
additional arguments to pass to the ``metalsmith`` CLI on all calls.
|
||||||
``image`` (defaults to ``metalsmith_image``)
|
``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``
|
``netboot``
|
||||||
whether to boot the deployed instance from network (PXE, iPXE, etc).
|
whether to boot the deployed instance from network (PXE, iPXE, etc).
|
||||||
The default is to use local boot (requires a bootloader on the image).
|
The default is to use local boot (requires a bootloader on the image).
|
||||||
|
|
|
@ -3,6 +3,7 @@ metalsmith_candidates: []
|
||||||
metalsmith_capabilities: {}
|
metalsmith_capabilities: {}
|
||||||
metalsmith_conductor_group:
|
metalsmith_conductor_group:
|
||||||
metalsmith_extra_args:
|
metalsmith_extra_args:
|
||||||
|
metalsmith_image_checksum:
|
||||||
metalsmith_netboot: false
|
metalsmith_netboot: false
|
||||||
metalsmith_nics: []
|
metalsmith_nics: []
|
||||||
metalsmith_resource_class:
|
metalsmith_resource_class:
|
||||||
|
|
|
@ -34,6 +34,9 @@
|
||||||
{% for node in candidates %}
|
{% for node in candidates %}
|
||||||
--candidate {{ node }}
|
--candidate {{ node }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% if image_checksum %}
|
||||||
|
--image-checksum {{ image_checksum }}
|
||||||
|
{% endif %}
|
||||||
when: state == 'present'
|
when: state == 'present'
|
||||||
vars:
|
vars:
|
||||||
candidates: "{{ instance.candidates | default(metalsmith_candidates) }}"
|
candidates: "{{ instance.candidates | default(metalsmith_candidates) }}"
|
||||||
|
@ -41,6 +44,7 @@
|
||||||
conductor_group: "{{ instance.conductor_group | default(metalsmith_conductor_group) }}"
|
conductor_group: "{{ instance.conductor_group | default(metalsmith_conductor_group) }}"
|
||||||
extra_args: "{{ instance.extra_args | default(metalsmith_extra_args) }}"
|
extra_args: "{{ instance.extra_args | default(metalsmith_extra_args) }}"
|
||||||
image: "{{ instance.image | default(metalsmith_image) }}"
|
image: "{{ instance.image | default(metalsmith_image) }}"
|
||||||
|
image: "{{ instance.image_checksum | default(metalsmith_image_checksum) }}"
|
||||||
netboot: "{{ instance.netboot | default(metalsmith_netboot) }}"
|
netboot: "{{ instance.netboot | default(metalsmith_netboot) }}"
|
||||||
nics: "{{ instance.nics | default(metalsmith_nics) }}"
|
nics: "{{ instance.nics | default(metalsmith_nics) }}"
|
||||||
resource_class: "{{ instance.resource_class | default(metalsmith_resource_class) }}"
|
resource_class: "{{ instance.resource_class | default(metalsmith_resource_class) }}"
|
||||||
|
|
Loading…
Reference in New Issue