Support for HTTP image location

Story: #2002048
Task: #19695
Change-Id: I75f33ebca3ea65274dcfcd8f4ddbd193f34706a9
This commit is contained in:
Dmitry Tantsur 2018-09-04 18:37:16 +02:00
parent f57e7547af
commit 6bdd479773
14 changed files with 460 additions and 19 deletions

View File

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

View File

@ -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)',

View File

@ -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 = []

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) }}"