Browse Source

Add sources.detect to detect various source types

Change-Id: Ic1e325538f0975b04750e10233e877ffcfbf4263
tags/0.9.0
Dmitry Tantsur 5 months ago
parent
commit
8263ca2c2e

+ 6
- 43
metalsmith/_cmd.py View File

@@ -29,10 +29,6 @@ from metalsmith import sources
29 29
 LOG = logging.getLogger(__name__)
30 30
 
31 31
 
32
-def _is_http(smth):
33
-    return smth.startswith('http://') or smth.startswith('https://')
34
-
35
-
36 32
 class NICAction(argparse.Action):
37 33
     def __call__(self, parser, namespace, values, option_string=None):
38 34
         assert option_string in ('--port', '--network', '--ip')
@@ -64,41 +60,10 @@ def _do_deploy(api, args, formatter):
64 60
     if args.hostname and not _utils.is_hostname_safe(args.hostname):
65 61
         raise RuntimeError("%s cannot be used as a hostname" % args.hostname)
66 62
 
67
-    if _is_http(args.image):
68
-        kwargs = {}
69
-        if not args.image_checksum:
70
-            raise RuntimeError("HTTP(s) images require --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
63
+    source = sources.detect(args.image,
64
+                            kernel=args.image_kernel,
65
+                            ramdisk=args.image_ramdisk,
66
+                            checksum=args.image_checksum)
102 67
 
103 68
     config = _config.InstanceConfig(ssh_keys=ssh_keys)
104 69
     if args.user_name:
@@ -176,10 +141,8 @@ def _parse_args(args, config):
176 141
                         required=True)
177 142
     deploy.add_argument('--image-checksum',
178 143
                         help='image MD5 checksum or URL with checksums')
179
-    deploy.add_argument('--image-kernel', help='URL of the image\'s kernel',
180
-                        default='')
181
-    deploy.add_argument('--image-ramdisk', help='URL of the image\'s ramdisk',
182
-                        default='')
144
+    deploy.add_argument('--image-kernel', help='URL of the image\'s kernel')
145
+    deploy.add_argument('--image-ramdisk', help='URL of the image\'s ramdisk')
183 146
     deploy.add_argument('--network', help='network to use (name or UUID)',
184 147
                         dest='nics', action=NICAction)
185 148
     deploy.add_argument('--port', help='port to attach (name or UUID)',

+ 82
- 3
metalsmith/sources.py View File

@@ -50,19 +50,19 @@ class GlanceImage(_Source):
50 50
 
51 51
         :param image: `Image` object, ID or name.
52 52
         """
53
-        self._image_id = image
53
+        self.image = image
54 54
         self._image_obj = None
55 55
 
56 56
     def _validate(self, connection):
57 57
         if self._image_obj is not None:
58 58
             return
59 59
         try:
60
-            self._image_obj = connection.image.find_image(self._image_id,
60
+            self._image_obj = connection.image.find_image(self.image,
61 61
                                                           ignore_missing=False)
62 62
         except openstack.exceptions.SDKException as exc:
63 63
             raise exceptions.InvalidImage(
64 64
                 'Cannot find image %(image)s: %(error)s' %
65
-                {'image': self._image_id, 'error': exc})
65
+                {'image': self.image, 'error': exc})
66 66
 
67 67
     def _node_updates(self, connection):
68 68
         self._validate(connection)
@@ -242,3 +242,82 @@ class FilePartitionImage(FileWholeDiskImage):
242 242
         updates['kernel'] = self.kernel_location
243 243
         updates['ramdisk'] = self.ramdisk_location
244 244
         return updates
245
+
246
+
247
+def 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
+
315
+def _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'

+ 5
- 1
metalsmith/test/test_cmd.py View File

@@ -49,7 +49,7 @@ class TestDeploy(testtools.TestCase):
49 49
                                 candidates=None)
50 50
         reserve_defaults.update(reserve_args)
51 51
 
52
-        provision_defaults = dict(image='myimg',
52
+        provision_defaults = dict(image=mock.ANY,
53 53
                                   nics=[{'network': 'mynet'}],
54 54
                                   root_size_gb=None,
55 55
                                   swap_size_mb=None,
@@ -88,6 +88,10 @@ class TestDeploy(testtools.TestCase):
88 88
         self.assertEqual([], config.ssh_keys)
89 89
         mock_log.basicConfig.assert_called_once_with(level=mock_log.WARNING,
90 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 95
         self.assertEqual(
92 96
             mock.call('metalsmith').setLevel(mock_log.WARNING).call_list() +
93 97
             mock.call(_cmd._URLLIB3_LOGGER).setLevel(

+ 126
- 0
metalsmith/test/test_sources.py View File

@@ -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
+
16
+import testtools
17
+
18
+from metalsmith import sources
19
+
20
+
21
+class 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)

+ 5
- 0
releasenotes/notes/source-detect-673ad8c3e98c3df1.yaml View File

@@ -0,0 +1,5 @@
1
+---
2
+features:
3
+  - |
4
+    Adds new function ``metalsmith.sources.detect`` to automate detection of
5
+    various sources from their location, kernel, image and checksum.

Loading…
Cancel
Save