summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDmitry Tantsur <divius.inside@gmail.com>2019-01-15 15:05:13 +0100
committerDmitry Tantsur <divius.inside@gmail.com>2019-01-16 12:40:50 +0100
commit8263ca2c2e5bda54e282a8c87965f9bbb2908af8 (patch)
tree1350b2bf044758fc953e79000d3a1186af257f24
parente242d5bc3b4aa14743329155abb4beb79d59ac92 (diff)
Add sources.detect to detect various source types
Notes
Notes (review): Code-Review+2: Dmitry Tantsur <divius.inside@gmail.com> Workflow+1: Dmitry Tantsur <divius.inside@gmail.com> Verified+2: Zuul Submitted-by: Zuul Submitted-at: Wed, 16 Jan 2019 14:26:47 +0000 Reviewed-on: https://review.openstack.org/630979 Project: openstack/metalsmith Branch: refs/heads/master
-rw-r--r--metalsmith/_cmd.py49
-rw-r--r--metalsmith/sources.py85
-rw-r--r--metalsmith/test/test_cmd.py6
-rw-r--r--metalsmith/test/test_sources.py126
-rw-r--r--releasenotes/notes/source-detect-673ad8c3e98c3df1.yaml5
5 files changed, 224 insertions, 47 deletions
diff --git a/metalsmith/_cmd.py b/metalsmith/_cmd.py
index cced8ee..4d5b3a5 100644
--- a/metalsmith/_cmd.py
+++ b/metalsmith/_cmd.py
@@ -29,10 +29,6 @@ from metalsmith import sources
29LOG = logging.getLogger(__name__) 29LOG = logging.getLogger(__name__)
30 30
31 31
32def _is_http(smth):
33 return smth.startswith('http://') or smth.startswith('https://')
34
35
36class NICAction(argparse.Action): 32class NICAction(argparse.Action):
37 def __call__(self, parser, namespace, values, option_string=None): 33 def __call__(self, parser, namespace, values, option_string=None):
38 assert option_string in ('--port', '--network', '--ip') 34 assert option_string in ('--port', '--network', '--ip')
@@ -64,41 +60,10 @@ def _do_deploy(api, args, formatter):
64 if args.hostname and not _utils.is_hostname_safe(args.hostname): 60 if args.hostname and not _utils.is_hostname_safe(args.hostname):
65 raise RuntimeError("%s cannot be used as a hostname" % args.hostname) 61 raise RuntimeError("%s cannot be used as a hostname" % args.hostname)
66 62
67 if _is_http(args.image): 63 source = sources.detect(args.image,
68 kwargs = {} 64 kernel=args.image_kernel,
69 if not args.image_checksum: 65 ramdisk=args.image_ramdisk,
70 raise RuntimeError("HTTP(s) images require --image-checksum") 66 checksum=args.image_checksum)
71 elif _is_http(args.image_checksum):
72 kwargs['checksum_url'] = args.image_checksum
73 else:
74 kwargs['checksum'] = args.image_checksum
75
76 if args.image_kernel or args.image_ramdisk:
77 source = sources.HttpPartitionImage(args.image,
78 args.image_kernel,
79 args.image_ramdisk,
80 **kwargs)
81 else:
82 source = sources.HttpWholeDiskImage(args.image, **kwargs)
83 elif args.image.startswith('file://'):
84 if not args.image_checksum:
85 raise RuntimeError("File images require --image-checksum")
86
87 if args.image_kernel or args.image_ramdisk:
88 if not (args.image_kernel.startswith('file://') and
89 args.image_ramdisk.startswith('file://')):
90 raise RuntimeError('Images with the file:// schema require '
91 'kernel and ramdisk images to also use '
92 'the file:// schema')
93 source = sources.FilePartitionImage(args.image,
94 args.image_kernel,
95 args.image_ramdisk,
96 args.image_checksum)
97 else:
98 source = sources.FileWholeDiskImage(args.image,
99 args.image_checksum)
100 else:
101 source = args.image
102 67
103 config = _config.InstanceConfig(ssh_keys=ssh_keys) 68 config = _config.InstanceConfig(ssh_keys=ssh_keys)
104 if args.user_name: 69 if args.user_name:
@@ -176,10 +141,8 @@ def _parse_args(args, config):
176 required=True) 141 required=True)
177 deploy.add_argument('--image-checksum', 142 deploy.add_argument('--image-checksum',
178 help='image MD5 checksum or URL with checksums') 143 help='image MD5 checksum or URL with checksums')
179 deploy.add_argument('--image-kernel', help='URL of the image\'s kernel', 144 deploy.add_argument('--image-kernel', help='URL of the image\'s kernel')
180 default='') 145 deploy.add_argument('--image-ramdisk', help='URL of the image\'s ramdisk')
181 deploy.add_argument('--image-ramdisk', help='URL of the image\'s ramdisk',
182 default='')
183 deploy.add_argument('--network', help='network to use (name or UUID)', 146 deploy.add_argument('--network', help='network to use (name or UUID)',
184 dest='nics', action=NICAction) 147 dest='nics', action=NICAction)
185 deploy.add_argument('--port', help='port to attach (name or UUID)', 148 deploy.add_argument('--port', help='port to attach (name or UUID)',
diff --git a/metalsmith/sources.py b/metalsmith/sources.py
index 244a7b1..c33b43d 100644
--- a/metalsmith/sources.py
+++ b/metalsmith/sources.py
@@ -50,19 +50,19 @@ class GlanceImage(_Source):
50 50
51 :param image: `Image` object, ID or name. 51 :param image: `Image` object, ID or name.
52 """ 52 """
53 self._image_id = image 53 self.image = image
54 self._image_obj = None 54 self._image_obj = None
55 55
56 def _validate(self, connection): 56 def _validate(self, connection):
57 if self._image_obj is not None: 57 if self._image_obj is not None:
58 return 58 return
59 try: 59 try:
60 self._image_obj = connection.image.find_image(self._image_id, 60 self._image_obj = connection.image.find_image(self.image,
61 ignore_missing=False) 61 ignore_missing=False)
62 except openstack.exceptions.SDKException as exc: 62 except openstack.exceptions.SDKException as exc:
63 raise exceptions.InvalidImage( 63 raise exceptions.InvalidImage(
64 'Cannot find image %(image)s: %(error)s' % 64 'Cannot find image %(image)s: %(error)s' %
65 {'image': self._image_id, 'error': exc}) 65 {'image': self.image, 'error': exc})
66 66
67 def _node_updates(self, connection): 67 def _node_updates(self, connection):
68 self._validate(connection) 68 self._validate(connection)
@@ -242,3 +242,82 @@ class FilePartitionImage(FileWholeDiskImage):
242 updates['kernel'] = self.kernel_location 242 updates['kernel'] = self.kernel_location
243 updates['ramdisk'] = self.ramdisk_location 243 updates['ramdisk'] = self.ramdisk_location
244 return updates 244 return updates
245
246
247def detect(image, kernel=None, ramdisk=None, checksum=None):
248 """Try detecting the correct source type from the provided information.
249
250 .. note::
251 Images without a schema are assumed to be Glance images.
252
253 :param image: Location of the image: ``file://``, ``http://``, ``https://``
254 link or a Glance image name or UUID.
255 :param kernel: Location of the kernel (if present): ``file://``,
256 ``http://``, ``https://`` link or a Glance image name or UUID.
257 :param ramdisk: Location of the ramdisk (if present): ``file://``,
258 ``http://``, ``https://`` link or a Glance image name or UUID.
259 :param checksum: MD5 checksum of the image: ``http://`` or ``https://``
260 link or a string.
261 :return: A valid source object.
262 :raises: ValueError if the given parameters do not correspond to any
263 valid source.
264 """
265 image_type = _link_type(image)
266 checksum_type = _link_type(checksum)
267
268 if image_type == 'glance':
269 if kernel or ramdisk or checksum:
270 raise ValueError('kernel, image and checksum cannot be provided '
271 'for Glance images')
272 else:
273 return GlanceImage(image)
274
275 kernel_type = _link_type(kernel)
276 ramdisk_type = _link_type(ramdisk)
277 if not checksum:
278 raise ValueError('checksum is required for HTTP and file images')
279
280 if image_type == 'file':
281 if (kernel_type not in (None, 'file')
282 or ramdisk_type not in (None, 'file')
283 or checksum_type == 'http'):
284 raise ValueError('kernal, ramdisk and checksum can only be files '
285 'for file images')
286
287 if kernel or ramdisk:
288 return FilePartitionImage(image,
289 kernel_location=kernel,
290 ramdisk_location=ramdisk,
291 checksum=checksum)
292 else:
293 return FileWholeDiskImage(image, checksum=checksum)
294 else:
295 if (kernel_type not in (None, 'http')
296 or ramdisk_type not in (None, 'http')
297 or checksum_type == 'file'):
298 raise ValueError('kernal, ramdisk and checksum can only be HTTP '
299 'links for HTTP images')
300
301 if checksum_type == 'http':
302 kwargs = {'checksum_url': checksum}
303 else:
304 kwargs = {'checksum': checksum}
305
306 if kernel or ramdisk:
307 return HttpPartitionImage(image,
308 kernel_url=kernel,
309 ramdisk_url=ramdisk,
310 **kwargs)
311 else:
312 return HttpWholeDiskImage(image, **kwargs)
313
314
315def _link_type(link):
316 if link is None:
317 return None
318 elif link.startswith('http://') or link.startswith('https://'):
319 return 'http'
320 elif link.startswith('file://'):
321 return 'file'
322 else:
323 return 'glance'
diff --git a/metalsmith/test/test_cmd.py b/metalsmith/test/test_cmd.py
index 0753018..c081f62 100644
--- a/metalsmith/test/test_cmd.py
+++ b/metalsmith/test/test_cmd.py
@@ -49,7 +49,7 @@ class TestDeploy(testtools.TestCase):
49 candidates=None) 49 candidates=None)
50 reserve_defaults.update(reserve_args) 50 reserve_defaults.update(reserve_args)
51 51
52 provision_defaults = dict(image='myimg', 52 provision_defaults = dict(image=mock.ANY,
53 nics=[{'network': 'mynet'}], 53 nics=[{'network': 'mynet'}],
54 root_size_gb=None, 54 root_size_gb=None,
55 swap_size_mb=None, 55 swap_size_mb=None,
@@ -88,6 +88,10 @@ class TestDeploy(testtools.TestCase):
88 self.assertEqual([], config.ssh_keys) 88 self.assertEqual([], config.ssh_keys)
89 mock_log.basicConfig.assert_called_once_with(level=mock_log.WARNING, 89 mock_log.basicConfig.assert_called_once_with(level=mock_log.WARNING,
90 format=mock.ANY) 90 format=mock.ANY)
91
92 source = mock_pr.return_value.provision_node.call_args[1]['image']
93 self.assertIsInstance(source, sources.GlanceImage)
94 self.assertEqual("myimg", source.image)
91 self.assertEqual( 95 self.assertEqual(
92 mock.call('metalsmith').setLevel(mock_log.WARNING).call_list() + 96 mock.call('metalsmith').setLevel(mock_log.WARNING).call_list() +
93 mock.call(_cmd._URLLIB3_LOGGER).setLevel( 97 mock.call(_cmd._URLLIB3_LOGGER).setLevel(
diff --git a/metalsmith/test/test_sources.py b/metalsmith/test/test_sources.py
new file mode 100644
index 0000000..a358c8b
--- /dev/null
+++ b/metalsmith/test/test_sources.py
@@ -0,0 +1,126 @@
1# Copyright 2019 Red Hat, Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12# implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16import testtools
17
18from metalsmith import sources
19
20
21class TestDetect(testtools.TestCase):
22
23 def test_glance(self):
24 source = sources.detect('foobar')
25 self.assertIsInstance(source, sources.GlanceImage)
26 self.assertEqual(source.image, 'foobar')
27
28 def test_glance_invalid_arguments(self):
29 for kwargs in [{'kernel': 'foo'},
30 {'ramdisk': 'foo'},
31 {'checksum': 'foo'}]:
32 self.assertRaisesRegex(ValueError, 'cannot be provided',
33 sources.detect, 'foobar', **kwargs)
34
35 def test_checksum_required(self):
36 for tp in ('file', 'http', 'https'):
37 self.assertRaisesRegex(ValueError, 'checksum is required',
38 sources.detect, '%s://foo' % tp)
39
40 def test_file_whole_disk(self):
41 source = sources.detect('file:///image', checksum='abcd')
42 self.assertIs(source.__class__, sources.FileWholeDiskImage)
43 self.assertEqual(source.location, 'file:///image')
44 self.assertEqual(source.checksum, 'abcd')
45
46 def test_file_partition_disk(self):
47 source = sources.detect('file:///image', checksum='abcd',
48 kernel='file:///kernel',
49 ramdisk='file:///ramdisk')
50 self.assertIs(source.__class__, sources.FilePartitionImage)
51 self.assertEqual(source.location, 'file:///image')
52 self.assertEqual(source.checksum, 'abcd')
53 self.assertEqual(source.kernel_location, 'file:///kernel')
54 self.assertEqual(source.ramdisk_location, 'file:///ramdisk')
55
56 def test_file_partition_inconsistency(self):
57 for kwargs in [{'kernel': 'foo'},
58 {'ramdisk': 'foo'},
59 {'kernel': 'http://foo'},
60 {'ramdisk': 'http://foo'},
61 {'checksum': 'http://foo'}]:
62 kwargs.setdefault('checksum', 'abcd')
63 self.assertRaisesRegex(ValueError, 'can only be files',
64 sources.detect, 'file:///image', **kwargs)
65
66 def test_http_whole_disk(self):
67 source = sources.detect('http:///image', checksum='abcd')
68 self.assertIs(source.__class__, sources.HttpWholeDiskImage)
69 self.assertEqual(source.url, 'http:///image')
70 self.assertEqual(source.checksum, 'abcd')
71
72 def test_https_whole_disk(self):
73 source = sources.detect('https:///image', checksum='abcd')
74 self.assertIs(source.__class__, sources.HttpWholeDiskImage)
75 self.assertEqual(source.url, 'https:///image')
76 self.assertEqual(source.checksum, 'abcd')
77
78 def test_https_whole_disk_checksum(self):
79 source = sources.detect('https:///image',
80 checksum='https://checksum')
81 self.assertIs(source.__class__, sources.HttpWholeDiskImage)
82 self.assertEqual(source.url, 'https:///image')
83 self.assertEqual(source.checksum_url, 'https://checksum')
84
85 def test_http_partition_disk(self):
86 source = sources.detect('http:///image', checksum='abcd',
87 kernel='http:///kernel',
88 ramdisk='http:///ramdisk')
89 self.assertIs(source.__class__, sources.HttpPartitionImage)
90 self.assertEqual(source.url, 'http:///image')
91 self.assertEqual(source.checksum, 'abcd')
92 self.assertEqual(source.kernel_url, 'http:///kernel')
93 self.assertEqual(source.ramdisk_url, 'http:///ramdisk')
94
95 def test_https_partition_disk(self):
96 source = sources.detect('https:///image', checksum='abcd',
97 # Can mix HTTP and HTTPs
98 kernel='http:///kernel',
99 ramdisk='https:///ramdisk')
100 self.assertIs(source.__class__, sources.HttpPartitionImage)
101 self.assertEqual(source.url, 'https:///image')
102 self.assertEqual(source.checksum, 'abcd')
103 self.assertEqual(source.kernel_url, 'http:///kernel')
104 self.assertEqual(source.ramdisk_url, 'https:///ramdisk')
105
106 def test_https_partition_disk_checksum(self):
107 source = sources.detect('https:///image',
108 # Can mix HTTP and HTTPs
109 checksum='http://checksum',
110 kernel='http:///kernel',
111 ramdisk='https:///ramdisk')
112 self.assertIs(source.__class__, sources.HttpPartitionImage)
113 self.assertEqual(source.url, 'https:///image')
114 self.assertEqual(source.checksum_url, 'http://checksum')
115 self.assertEqual(source.kernel_url, 'http:///kernel')
116 self.assertEqual(source.ramdisk_url, 'https:///ramdisk')
117
118 def test_http_partition_inconsistency(self):
119 for kwargs in [{'kernel': 'foo'},
120 {'ramdisk': 'foo'},
121 {'kernel': 'file://foo'},
122 {'ramdisk': 'file://foo'},
123 {'checksum': 'file://foo'}]:
124 kwargs.setdefault('checksum', 'abcd')
125 self.assertRaisesRegex(ValueError, 'can only be HTTP',
126 sources.detect, 'http:///image', **kwargs)
diff --git a/releasenotes/notes/source-detect-673ad8c3e98c3df1.yaml b/releasenotes/notes/source-detect-673ad8c3e98c3df1.yaml
new file mode 100644
index 0000000..d4fd9d8
--- /dev/null
+++ b/releasenotes/notes/source-detect-673ad8c3e98c3df1.yaml
@@ -0,0 +1,5 @@
1---
2features:
3 - |
4 Adds new function ``metalsmith.sources.detect`` to automate detection of
5 various sources from their location, kernel, image and checksum.