From 6bdd479773445f806ed331084501dd1488965d9e Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Tue, 4 Sep 2018 18:37:16 +0200 Subject: [PATCH] Support for HTTP image location Story: #2002048 Task: #19695 Change-Id: I75f33ebca3ea65274dcfcd8f4ddbd193f34706a9 --- .zuul.yaml | 16 ++ metalsmith/_cmd.py | 23 ++- metalsmith/_provisioner.py | 2 +- metalsmith/_utils.py | 13 ++ metalsmith/sources.py | 107 +++++++++- metalsmith/test/test_cmd.py | 67 +++++++ metalsmith/test/test_provisioner.py | 187 +++++++++++++++++- playbooks/integration/cirros-image.yaml | 35 +++- playbooks/integration/exercise.yaml | 1 + playbooks/integration/run.yaml | 16 +- requirements.txt | 1 + roles/metalsmith_deployment/README.rst | 6 +- roles/metalsmith_deployment/defaults/main.yml | 1 + roles/metalsmith_deployment/tasks/main.yml | 4 + 14 files changed, 460 insertions(+), 19 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index cb9a1c2..9693d0d 100644 --- a/.zuul.yaml +++ b/.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 diff --git a/metalsmith/_cmd.py b/metalsmith/_cmd.py index 7ddfa16..d319253 100644 --- a/metalsmith/_cmd.py +++ b/metalsmith/_cmd.py @@ -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)', diff --git a/metalsmith/_provisioner.py b/metalsmith/_provisioner.py index d9668e8..a8a2bcf 100644 --- a/metalsmith/_provisioner.py +++ b/metalsmith/_provisioner.py @@ -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 = [] diff --git a/metalsmith/_utils.py b/metalsmith/_utils.py index 98c8861..03aa47a 100644 --- a/metalsmith/_utils.py +++ b/metalsmith/_utils.py @@ -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 diff --git a/metalsmith/sources.py b/metalsmith/sources.py index 1cc39bf..44c361f 100644 --- a/metalsmith/sources.py +++ b/metalsmith/sources.py @@ -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 diff --git a/metalsmith/test/test_cmd.py b/metalsmith/test/test_cmd.py index 8ada457..a8160fc 100644 --- a/metalsmith/test/test_cmd.py +++ b/metalsmith/test/test_cmd.py @@ -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'] diff --git a/metalsmith/test/test_provisioner.py b/metalsmith/test/test_provisioner.py index 4b17900..fe21f41 100644 --- a/metalsmith/test/test_provisioner.py +++ b/metalsmith/test/test_provisioner.py @@ -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 = """ + +

I am not a checksum file!

+""" + + 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): diff --git a/playbooks/integration/cirros-image.yaml b/playbooks/integration/cirros-image.yaml index afc2d86..8f64b12 100644 --- a/playbooks/integration/cirros-image.yaml +++ b/playbooks/integration/cirros-image.yaml @@ -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) diff --git a/playbooks/integration/exercise.yaml b/playbooks/integration/exercise.yaml index 93ba3d1..b303664 100644 --- a/playbooks/integration/exercise.yaml +++ b/playbooks/integration/exercise.yaml @@ -23,6 +23,7 @@ metalsmith_instances: - hostname: test image: "{{ image }}" + image_checksum: "{{ image_checksum | default('') }}" nics: - "{{ nic }}" ssh_public_keys: diff --git a/playbooks/integration/run.yaml b/playbooks/integration/run.yaml index 00f78e3..5fa696a 100644 --- a/playbooks/integration/run.yaml +++ b/playbooks/integration/run.yaml @@ -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)) diff --git a/requirements.txt b/requirements.txt index 0128870..1ba9a9a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/roles/metalsmith_deployment/README.rst b/roles/metalsmith_deployment/README.rst index d62a7fc..8c28b21 100644 --- a/roles/metalsmith_deployment/README.rst +++ b/roles/metalsmith_deployment/README.rst @@ -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). diff --git a/roles/metalsmith_deployment/defaults/main.yml b/roles/metalsmith_deployment/defaults/main.yml index 9e2bf34..9d0e784 100644 --- a/roles/metalsmith_deployment/defaults/main.yml +++ b/roles/metalsmith_deployment/defaults/main.yml @@ -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: diff --git a/roles/metalsmith_deployment/tasks/main.yml b/roles/metalsmith_deployment/tasks/main.yml index 6273b9b..705f8fb 100644 --- a/roles/metalsmith_deployment/tasks/main.yml +++ b/roles/metalsmith_deployment/tasks/main.yml @@ -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) }}"