From fc91c84a2aaaf17881ebe7b9cfb53809dd2ff8a3 Mon Sep 17 00:00:00 2001 From: Steve Kowalik Date: Thu, 4 Dec 2014 14:12:58 +1100 Subject: [PATCH] Add a glance module that allows uploading images Add a new module, glance, that allows uploading or finding kernel and ramdisk images that are in Glance. This allows us to put setup-baremetal in -incubator on a radical diet. Also adds a command-line utility, upload_kernel_ramdisk to make adoption in -incubator easier. Change-Id: I2708646090cf9435f8910ee2170a5ff2939ec27a --- .../cmd/tests/test_upload_kernel_ramdisk.py | 38 ++++++++++ os_cloud_config/cmd/upload_kernel_ramdisk.py | 56 ++++++++++++++ os_cloud_config/glance.py | 61 +++++++++++++++ os_cloud_config/tests/test_glance.py | 74 +++++++++++++++++++ setup.cfg | 1 + 5 files changed, 230 insertions(+) create mode 100644 os_cloud_config/cmd/tests/test_upload_kernel_ramdisk.py create mode 100644 os_cloud_config/cmd/upload_kernel_ramdisk.py create mode 100644 os_cloud_config/glance.py create mode 100644 os_cloud_config/tests/test_glance.py diff --git a/os_cloud_config/cmd/tests/test_upload_kernel_ramdisk.py b/os_cloud_config/cmd/tests/test_upload_kernel_ramdisk.py new file mode 100644 index 0000000..1e510df --- /dev/null +++ b/os_cloud_config/cmd/tests/test_upload_kernel_ramdisk.py @@ -0,0 +1,38 @@ +# Copyright (c) 2014 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys + +import mock + +from os_cloud_config.cmd import upload_kernel_ramdisk +from os_cloud_config.tests import base + + +class UploadKernelRamdiskTest(base.TestCase): + + @mock.patch('os_cloud_config.cmd.utils._clients.get_glance_client', + return_value='glance_client_mock') + @mock.patch('os_cloud_config.glance.create_or_find_kernel_and_ramdisk') + @mock.patch.dict('os.environ', {'OS_USERNAME': 'a', 'OS_PASSWORD': 'a', + 'OS_TENANT_NAME': 'a', 'OS_AUTH_URL': 'a'}) + @mock.patch.object(sys, 'argv', ['upload_kernel_ramdisk', '-k', + 'bm-kernel', '-r', 'bm-ramdisk', '-l', + 'kernel-file', '-s', 'ramdisk-file']) + def test_with_arguments(self, create_or_find_mock, glanceclient_mock): + upload_kernel_ramdisk.main() + create_or_find_mock.assert_called_once_with( + 'glance_client_mock', 'bm-kernel', 'bm-ramdisk', + kernel_path='kernel-file', ramdisk_path='ramdisk-file') diff --git a/os_cloud_config/cmd/upload_kernel_ramdisk.py b/os_cloud_config/cmd/upload_kernel_ramdisk.py new file mode 100644 index 0000000..98adc8b --- /dev/null +++ b/os_cloud_config/cmd/upload_kernel_ramdisk.py @@ -0,0 +1,56 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import logging +import textwrap + +from os_cloud_config.cmd.utils import _clients as clients +from os_cloud_config.cmd.utils import environment +from os_cloud_config import glance + + +def parse_args(): + description = textwrap.dedent(""" + Uploads the provided kernel and ramdisk to a Glance store. + """) + + parser = argparse.ArgumentParser( + description=description, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument('-k', '--kernel', dest='kernel', + help='Name of the kernel image', required=True) + parser.add_argument('-l' '--kernel-file', dest='kernel_file', + help='Kernel to upload', required=True) + parser.add_argument('-r', '--ramdisk', dest='ramdisk', + help='Name of the ramdisk image', required=True) + parser.add_argument('-s', '--ramdisk-file', dest='ramdisk_file', + help='Ramdisk to upload', required=True) + environment._add_logging_arguments(parser) + return parser.parse_args() + + +def main(): + args = parse_args() + environment._configure_logging(args) + try: + environment._ensure() + client = clients.get_glance_client() + glance.create_or_find_kernel_and_ramdisk( + client, args.kernel, args.ramdisk, kernel_path=args.kernel_file, + ramdisk_path=args.ramdisk_file) + except Exception: + logging.exception("Unexpected error during command execution") + return 1 + return 0 diff --git a/os_cloud_config/glance.py b/os_cloud_config/glance.py new file mode 100644 index 0000000..3e4a164 --- /dev/null +++ b/os_cloud_config/glance.py @@ -0,0 +1,61 @@ +# Copyright (c) 2014 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from glanceclient.openstack.common.apiclient import exceptions + +LOG = logging.getLogger(__name__) + + +def create_or_find_kernel_and_ramdisk(glanceclient, kernel_name, ramdisk_name, + kernel_path=None, ramdisk_path=None): + """Find or create a given kernel and ramdisk in Glance. + + If either kernel_path or ramdisk_path is None, they will not be created, + and an exception will be raised if it does not exist in Glance. + + :param glanceclient: A client for Glance. + :param kernel_name: Name to search for or create for the kernel. + :param ramdisk_name: Name to search for or create for the ramdisk. + :param kernel_path: Path to the kernel on disk. + :param ramdisk_path: Path to the ramdisk on disk. + + :returns: A dictionary mapping kernel or ramdisk to the ID in Glance. + """ + try: + kernel_image = glanceclient.images.find(name=kernel_name, + disk_format='aki') + except exceptions.NotFound: + if kernel_path: + kernel_image = glanceclient.images.create( + name=kernel_name, disk_format='aki', is_public=True, + data=open(kernel_path, 'rb')) + else: + raise ValueError("Kernel image not found in Glance, and no path " + "specified.") + try: + ramdisk_image = glanceclient.images.find(name=ramdisk_name, + disk_format='ari') + except exceptions.NotFound: + if ramdisk_path: + # public, type=ari + ramdisk_image = glanceclient.images.create( + name=ramdisk_name, disk_format='ari', is_public=True, + data=open(ramdisk_path, 'rb')) + else: + raise ValueError("Ramdisk image not found in Glance, and no path " + "specified.") + return {'kernel': kernel_image.id, 'ramdisk': ramdisk_image.id} diff --git a/os_cloud_config/tests/test_glance.py b/os_cloud_config/tests/test_glance.py new file mode 100644 index 0000000..511b398 --- /dev/null +++ b/os_cloud_config/tests/test_glance.py @@ -0,0 +1,74 @@ +# Copyright (c) 2014 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import collections +import tempfile + +from glanceclient.openstack.common.apiclient import exceptions +import mock +import testtools + +from os_cloud_config import glance +from os_cloud_config.tests import base + + +class GlanceTest(base.TestCase): + + def setUp(self): + super(GlanceTest, self).setUp() + self.image = collections.namedtuple('image', ['id']) + + def test_return_existing_kernel_and_ramdisk(self): + client = mock.MagicMock() + expected = {'kernel': 'aaa', 'ramdisk': 'zzz'} + client.images.find.side_effect = (self.image('aaa'), self.image('zzz')) + ids = glance.create_or_find_kernel_and_ramdisk(client, 'bm-kernel', + 'bm-ramdisk') + client.images.create.assert_not_called() + self.assertEqual(expected, ids) + + def test_raise_exception_kernel(self): + client = mock.MagicMock() + client.images.find.side_effect = exceptions.NotFound + message = "Kernel image not found in Glance, and no path specified." + with testtools.ExpectedException(ValueError, message): + glance.create_or_find_kernel_and_ramdisk(client, 'bm-kernel', + None) + + def test_raise_exception_ramdisk(self): + client = mock.MagicMock() + client.images.find.side_effect = (self.image('aaa'), + exceptions.NotFound) + message = "Ramdisk image not found in Glance, and no path specified." + with testtools.ExpectedException(ValueError, message): + glance.create_or_find_kernel_and_ramdisk(client, 'bm-kernel', + 'bm-ramdisk') + + def test_create_kernel_and_ramdisk(self): + client = mock.MagicMock() + client.images.find.side_effect = exceptions.NotFound + client.images.create.side_effect = (self.image('aaa'), + self.image('zzz')) + expected = {'kernel': 'aaa', 'ramdisk': 'zzz'} + with tempfile.NamedTemporaryFile() as imagefile: + ids = glance.create_or_find_kernel_and_ramdisk( + client, 'bm-kernel', 'bm-ramdisk', kernel_path=imagefile.name, + ramdisk_path=imagefile.name) + kernel_create = mock.call(name='bm-kernel', disk_format='aki', + is_public=True, data=mock.ANY) + ramdisk_create = mock.call(name='bm-ramdisk', disk_format='ari', + is_public=True, data=mock.ANY) + client.images.create.assert_has_calls([kernel_create, ramdisk_create]) + self.assertEqual(expected, ids) diff --git a/setup.cfg b/setup.cfg index f17515b..42b2915 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,6 +31,7 @@ console_scripts = setup-endpoints = os_cloud_config.cmd.setup_endpoints:main setup-flavors = os_cloud_config.cmd.setup_flavors:main setup-neutron = os_cloud_config.cmd.setup_neutron:main + upload-kernel-ramdisk = os_cloud_config.cmd.upload_kernel_ramdisk:main [build_sphinx] source-dir = doc/source