Merge lp:~jtv/maas/split-boot_resources into lp:~maas-committers/maas/trunk
- split-boot_resources
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Jeroen T. Vermeulen |
Approved revision: | no longer in the source branch. |
Merged at revision: | 2245 |
Proposed branch: | lp:~jtv/maas/split-boot_resources |
Merge into: | lp:~maas-committers/maas/trunk |
Diff against target: |
1823 lines (+953/-729) 11 files modified
src/provisioningserver/import_images/boot_image_mapping.py (+61/-0) src/provisioningserver/import_images/boot_resources.py (+8/-299) src/provisioningserver/import_images/download_descriptions.py (+187/-0) src/provisioningserver/import_images/helpers.py (+73/-0) src/provisioningserver/import_images/product_mapping.py (+65/-0) src/provisioningserver/import_images/testing/factory.py (+56/-0) src/provisioningserver/import_images/tests/test_boot_image_mapping.py (+98/-0) src/provisioningserver/import_images/tests/test_boot_resources.py (+10/-430) src/provisioningserver/import_images/tests/test_download_descriptions.py (+209/-0) src/provisioningserver/import_images/tests/test_helpers.py (+61/-0) src/provisioningserver/import_images/tests/test_product_mapping.py (+125/-0) |
To merge this branch: | bzr merge lp:~jtv/maas/split-boot_resources |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Graham Binns (community) | Approve | ||
Review via email: mp+214670@code.launchpad.net |
Commit message
Break the boot_resources.py file up into smaller parts, for manageability.
Description of the change
This may look like a big branch, but it isn't, really. The code is moved, but nothing else is changed (except available_
Lots of things were going on in this file, including two Simplestreams synchronisations. Now that we have proper classes for some of the additional components, and a clearer idea of what RepoDumper does and how it fits into the big picture, we can split things up. It is particularly gratifying that RepoDumper itself is now entirely local to its module. A lot of cognitive load is relieved by simply not needing to know about this class for maintenance that doesn't involve its specific function.
Jeroen
MAAS Lander (maas-lander) wrote : | # |
Attempt to merge into lp:maas failed due to conflicts:
text conflict in src/provisionin
Jeroen T. Vermeulen (jtv) wrote : | # |
I think that may just have been a silly little conflict between an import that I removed in this branch, but also ran through the formatter in a concurrent lint branch.
Preview Diff
1 | === added file 'src/provisioningserver/import_images/boot_image_mapping.py' | |||
2 | --- src/provisioningserver/import_images/boot_image_mapping.py 1970-01-01 00:00:00 +0000 | |||
3 | +++ src/provisioningserver/import_images/boot_image_mapping.py 2014-04-08 10:22:55 +0000 | |||
4 | @@ -0,0 +1,61 @@ | |||
5 | 1 | # Copyright 2014 Canonical Ltd. This software is licensed under the | ||
6 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
7 | 3 | |||
8 | 4 | """The `BootImageMapping` class.""" | ||
9 | 5 | |||
10 | 6 | from __future__ import ( | ||
11 | 7 | absolute_import, | ||
12 | 8 | print_function, | ||
13 | 9 | unicode_literals, | ||
14 | 10 | ) | ||
15 | 11 | |||
16 | 12 | str = None | ||
17 | 13 | |||
18 | 14 | __metaclass__ = type | ||
19 | 15 | __all__ = [ | ||
20 | 16 | 'BootImageMapping', | ||
21 | 17 | ] | ||
22 | 18 | |||
23 | 19 | import json | ||
24 | 20 | |||
25 | 21 | from provisioningserver.import_images.helpers import ImageSpec | ||
26 | 22 | |||
27 | 23 | |||
28 | 24 | class BootImageMapping: | ||
29 | 25 | """Mapping of boot-image data. | ||
30 | 26 | |||
31 | 27 | Maps `ImageSpec` tuples to metadata for Simplestreams products. | ||
32 | 28 | |||
33 | 29 | This class is deliberately a bit more restrictive and less ad-hoc than a | ||
34 | 30 | dict. It helps keep a clear view of the data structures in this module. | ||
35 | 31 | """ | ||
36 | 32 | |||
37 | 33 | def __init__(self): | ||
38 | 34 | self.mapping = {} | ||
39 | 35 | |||
40 | 36 | def items(self): | ||
41 | 37 | """Iterate over `ImageSpec` keys, and their stored values.""" | ||
42 | 38 | for image_spec, item in sorted(self.mapping.items()): | ||
43 | 39 | yield image_spec, item | ||
44 | 40 | |||
45 | 41 | def setdefault(self, image_spec, item): | ||
46 | 42 | """Set metadata for `image_spec` to item, if not already set.""" | ||
47 | 43 | assert isinstance(image_spec, ImageSpec) | ||
48 | 44 | self.mapping.setdefault(image_spec, item) | ||
49 | 45 | |||
50 | 46 | def dump_json(self): | ||
51 | 47 | """Produce JSON representing the mapped boot images. | ||
52 | 48 | |||
53 | 49 | Tries to keep the output deterministic, so that identical data is | ||
54 | 50 | likely to produce identical JSON. | ||
55 | 51 | """ | ||
56 | 52 | # The meta files represent the mapping as a nested hierarchy of dicts. | ||
57 | 53 | # Keep that format. | ||
58 | 54 | data = {} | ||
59 | 55 | for image, resource in self.items(): | ||
60 | 56 | arch, subarch, release, label = image | ||
61 | 57 | data.setdefault(arch, {}) | ||
62 | 58 | data[arch].setdefault(subarch, {}) | ||
63 | 59 | data[arch][subarch].setdefault(release, {}) | ||
64 | 60 | data[arch][subarch][release][label] = resource | ||
65 | 61 | return json.dumps(data, sort_keys=True) | ||
66 | 0 | 62 | ||
67 | === modified file 'src/provisioningserver/import_images/boot_resources.py' | |||
68 | --- src/provisioningserver/import_images/boot_resources.py 2014-04-08 05:25:24 +0000 | |||
69 | +++ src/provisioningserver/import_images/boot_resources.py 2014-04-08 10:22:55 +0000 | |||
70 | @@ -12,26 +12,27 @@ | |||
71 | 12 | __metaclass__ = type | 12 | __metaclass__ = type |
72 | 13 | __all__ = [ | 13 | __all__ = [ |
73 | 14 | 'main', | 14 | 'main', |
74 | 15 | 'available_boot_resources', | ||
75 | 16 | 'make_arg_parser', | 15 | 'make_arg_parser', |
76 | 17 | ] | 16 | ] |
77 | 18 | 17 | ||
78 | 19 | from argparse import ArgumentParser | 18 | from argparse import ArgumentParser |
79 | 20 | from collections import namedtuple | ||
80 | 21 | from datetime import datetime | 19 | from datetime import datetime |
81 | 22 | import errno | 20 | import errno |
82 | 23 | import functools | ||
83 | 24 | import glob | ||
84 | 25 | from gzip import GzipFile | 21 | from gzip import GzipFile |
85 | 26 | import json | ||
86 | 27 | import logging | ||
87 | 28 | from logging import getLogger | ||
88 | 29 | import os | 22 | import os |
89 | 30 | from textwrap import dedent | 23 | from textwrap import dedent |
90 | 31 | 24 | ||
91 | 32 | from provisioningserver.boot import BootMethodRegistry | 25 | from provisioningserver.boot import BootMethodRegistry |
92 | 33 | from provisioningserver.boot.tftppath import list_boot_images | 26 | from provisioningserver.boot.tftppath import list_boot_images |
93 | 34 | from provisioningserver.config import BootConfig | 27 | from provisioningserver.config import BootConfig |
94 | 28 | from provisioningserver.import_images.download_descriptions import ( | ||
95 | 29 | download_all_image_descriptions, | ||
96 | 30 | ) | ||
97 | 31 | from provisioningserver.import_images.helpers import ( | ||
98 | 32 | get_signing_policy, | ||
99 | 33 | logger, | ||
100 | 34 | ) | ||
101 | 35 | from provisioningserver.import_images.product_mapping import ProductMapping | ||
102 | 35 | from provisioningserver.utils import ( | 36 | from provisioningserver.utils import ( |
103 | 36 | atomic_write, | 37 | atomic_write, |
104 | 37 | call_and_check, | 38 | call_and_check, |
105 | @@ -47,200 +48,14 @@ | |||
106 | 47 | from simplestreams.util import ( | 48 | from simplestreams.util import ( |
107 | 48 | item_checksums, | 49 | item_checksums, |
108 | 49 | path_from_mirror_url, | 50 | path_from_mirror_url, |
109 | 50 | policy_read_signed, | ||
110 | 51 | products_exdata, | 51 | products_exdata, |
111 | 52 | ) | 52 | ) |
112 | 53 | 53 | ||
113 | 54 | 54 | ||
114 | 55 | def init_logger(log_level=logging.INFO): | ||
115 | 56 | logger = getLogger(__name__) | ||
116 | 57 | formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') | ||
117 | 58 | handler = logging.StreamHandler() | ||
118 | 59 | handler.setFormatter(formatter) | ||
119 | 60 | logger.addHandler(handler) | ||
120 | 61 | logger.setLevel(log_level) | ||
121 | 62 | return logger | ||
122 | 63 | |||
123 | 64 | |||
124 | 65 | logger = init_logger() | ||
125 | 66 | |||
126 | 67 | |||
127 | 68 | class NoConfigFile(Exception): | 55 | class NoConfigFile(Exception): |
128 | 69 | """Raised when the config file for the script doesn't exist.""" | 56 | """Raised when the config file for the script doesn't exist.""" |
129 | 70 | 57 | ||
130 | 71 | 58 | ||
131 | 72 | # A tuple of the items that together select a boot image. | ||
132 | 73 | ImageSpec = namedtuple(b'ImageSpec', [ | ||
133 | 74 | 'arch', | ||
134 | 75 | 'subarch', | ||
135 | 76 | 'release', | ||
136 | 77 | 'label', | ||
137 | 78 | ]) | ||
138 | 79 | |||
139 | 80 | |||
140 | 81 | class BootImageMapping: | ||
141 | 82 | """Mapping of boot-image data. | ||
142 | 83 | |||
143 | 84 | Maps `ImageSpec` tuples to metadata for Simplestreams products. | ||
144 | 85 | |||
145 | 86 | This class is deliberately a bit more restrictive and less ad-hoc than a | ||
146 | 87 | dict. It helps keep a clear view of the data structures in this module. | ||
147 | 88 | """ | ||
148 | 89 | |||
149 | 90 | def __init__(self): | ||
150 | 91 | self.mapping = {} | ||
151 | 92 | |||
152 | 93 | def items(self): | ||
153 | 94 | """Iterate over `ImageSpec` keys, and their stored values.""" | ||
154 | 95 | for image_spec, item in sorted(self.mapping.items()): | ||
155 | 96 | yield image_spec, item | ||
156 | 97 | |||
157 | 98 | def setdefault(self, image_spec, item): | ||
158 | 99 | """Set metadata for `image_spec` to item, if not already set.""" | ||
159 | 100 | assert isinstance(image_spec, ImageSpec) | ||
160 | 101 | self.mapping.setdefault(image_spec, item) | ||
161 | 102 | |||
162 | 103 | def dump_json(self): | ||
163 | 104 | """Produce JSON representing the mapped boot images. | ||
164 | 105 | |||
165 | 106 | Tries to keep the output deterministic, so that identical data is | ||
166 | 107 | likely to produce identical JSON. | ||
167 | 108 | """ | ||
168 | 109 | # The meta files represent the mapping as a nested hierarchy of dicts. | ||
169 | 110 | # Keep that format. | ||
170 | 111 | data = {} | ||
171 | 112 | for image, resource in self.items(): | ||
172 | 113 | arch, subarch, release, label = image | ||
173 | 114 | data.setdefault(arch, {}) | ||
174 | 115 | data[arch].setdefault(subarch, {}) | ||
175 | 116 | data[arch][subarch].setdefault(release, {}) | ||
176 | 117 | data[arch][subarch][release][label] = resource | ||
177 | 118 | return json.dumps(data, sort_keys=True) | ||
178 | 119 | |||
179 | 120 | |||
180 | 121 | def value_passes_filter_list(filter_list, property_value): | ||
181 | 122 | """Does the given property of a boot image pass the given filter list? | ||
182 | 123 | |||
183 | 124 | The value passes if either it matches one of the entries in the list of | ||
184 | 125 | filter values, or one of the filter values is an asterisk (`*`). | ||
185 | 126 | """ | ||
186 | 127 | return '*' in filter_list or property_value in filter_list | ||
187 | 128 | |||
188 | 129 | |||
189 | 130 | def value_passes_filter(filter_value, property_value): | ||
190 | 131 | """Does the given property of a boot image pass the given filter? | ||
191 | 132 | |||
192 | 133 | The value passes the filter if either the filter value is an asterisk | ||
193 | 134 | (`*`) or the value is equal to the filter value. | ||
194 | 135 | """ | ||
195 | 136 | return filter_value in ('*', property_value) | ||
196 | 137 | |||
197 | 138 | |||
198 | 139 | def image_passes_filter(filters, arch, subarch, release, label): | ||
199 | 140 | """Filter a boot image against configured import filters. | ||
200 | 141 | |||
201 | 142 | :param filters: A list of dicts describing the filters, as in `boot_merge`. | ||
202 | 143 | If the list is empty, or `None`, any image matches. Any entry in a | ||
203 | 144 | filter may be a string containing just an asterisk (`*`) to denote that | ||
204 | 145 | the entry will match any value. | ||
205 | 146 | :param arch: The given boot image's architecture. | ||
206 | 147 | :param subarch: The given boot image's subarchitecture. | ||
207 | 148 | :param release: The given boot image's OS release. | ||
208 | 149 | :param label: The given boot image's label. | ||
209 | 150 | :return: Whether the image matches any of the dicts in `filters`. | ||
210 | 151 | """ | ||
211 | 152 | if filters is None or len(filters) == 0: | ||
212 | 153 | return True | ||
213 | 154 | for filter_dict in filters: | ||
214 | 155 | item_matches = ( | ||
215 | 156 | value_passes_filter(filter_dict['release'], release) and | ||
216 | 157 | value_passes_filter_list(filter_dict['arches'], arch) and | ||
217 | 158 | value_passes_filter_list(filter_dict['subarches'], subarch) and | ||
218 | 159 | value_passes_filter_list(filter_dict['labels'], label) | ||
219 | 160 | ) | ||
220 | 161 | if item_matches: | ||
221 | 162 | return True | ||
222 | 163 | return False | ||
223 | 164 | |||
224 | 165 | |||
225 | 166 | def boot_merge(destination, additions, filters=None): | ||
226 | 167 | """Complement one `BootImageMapping` with entries from another. | ||
227 | 168 | |||
228 | 169 | This adds entries from `additions` (that match `filters`, if given) to | ||
229 | 170 | `destination`, but only for those image specs for which `destination` does | ||
230 | 171 | not have entries yet. | ||
231 | 172 | |||
232 | 173 | :param destination: `BootImageMapping` to be updated. It will be extended | ||
233 | 174 | in-place. | ||
234 | 175 | :param additions: A second `BootImageMapping`, which will be used as a | ||
235 | 176 | source of additional entries. | ||
236 | 177 | :param filters: List of dicts, each of which contains 'arch', 'subarch', | ||
237 | 178 | and 'release' keys. If given, entries are only considered for copying | ||
238 | 179 | from `additions` to `destination` if they match at least one of the | ||
239 | 180 | filters. Entries in the filter may be the string `*` (or for entries | ||
240 | 181 | that are lists, may contain the string `*`) to make them match any | ||
241 | 182 | value. | ||
242 | 183 | """ | ||
243 | 184 | for image, resource in additions.items(): | ||
244 | 185 | arch, subarch, release, label = image | ||
245 | 186 | if image_passes_filter(filters, arch, subarch, release, label): | ||
246 | 187 | logger.debug( | ||
247 | 188 | "Merging boot resource for %s/%s/%s/%s.", | ||
248 | 189 | arch, subarch, release, label) | ||
249 | 190 | # Do not override an existing entry with the same | ||
250 | 191 | # arch/subarch/release/label: the first entry found takes | ||
251 | 192 | # precedence. | ||
252 | 193 | destination.setdefault(image, resource) | ||
253 | 194 | |||
254 | 195 | |||
255 | 196 | class ProductMapping: | ||
256 | 197 | """Mapping of product data. | ||
257 | 198 | |||
258 | 199 | Maps a combination of boot resource metadata (`content_id`, `product_name`, | ||
259 | 200 | `version_name`) to a list of subarchitectures supported by that boot | ||
260 | 201 | resource. | ||
261 | 202 | """ | ||
262 | 203 | |||
263 | 204 | def __init__(self): | ||
264 | 205 | self.mapping = {} | ||
265 | 206 | |||
266 | 207 | @staticmethod | ||
267 | 208 | def make_key(resource): | ||
268 | 209 | """Extract a key tuple from `resource`. | ||
269 | 210 | |||
270 | 211 | The key is used for indexing `mapping`. | ||
271 | 212 | |||
272 | 213 | :param resource: A dict describing a boot resource. It must contain | ||
273 | 214 | the keys `content_id`, `product_name`, and `version_name`. | ||
274 | 215 | :return: A tuple of the resource's content ID, product name, and | ||
275 | 216 | version name. | ||
276 | 217 | """ | ||
277 | 218 | return ( | ||
278 | 219 | resource['content_id'], | ||
279 | 220 | resource['product_name'], | ||
280 | 221 | resource['version_name'], | ||
281 | 222 | ) | ||
282 | 223 | |||
283 | 224 | def add(self, resource, subarch): | ||
284 | 225 | """Add `subarch` to the list of subarches supported by a boot resource. | ||
285 | 226 | |||
286 | 227 | The `resource` is a dict as returned by `products_exdata`. The method | ||
287 | 228 | will use the values identified by keys `content_id`, `product_name`, | ||
288 | 229 | and `version_name`. | ||
289 | 230 | """ | ||
290 | 231 | key = self.make_key(resource) | ||
291 | 232 | self.mapping.setdefault(key, []) | ||
292 | 233 | self.mapping[key].append(subarch) | ||
293 | 234 | |||
294 | 235 | def contains(self, resource): | ||
295 | 236 | """Does the dict contain a mapping for the given resource?""" | ||
296 | 237 | return self.make_key(resource) in self.mapping | ||
297 | 238 | |||
298 | 239 | def get(self, resource): | ||
299 | 240 | """Return the mapped subarchitectures for `resource`.""" | ||
300 | 241 | return self.mapping[self.make_key(resource)] | ||
301 | 242 | |||
302 | 243 | |||
303 | 244 | def boot_reverse(boot_images_dict): | 59 | def boot_reverse(boot_images_dict): |
304 | 245 | """Determine the subarches supported by each boot resource. | 60 | """Determine the subarches supported by each boot resource. |
305 | 246 | 61 | ||
306 | @@ -299,106 +114,6 @@ | |||
307 | 299 | return entry | 114 | return entry |
308 | 300 | 115 | ||
309 | 301 | 116 | ||
310 | 302 | def get_signing_policy(path, keyring=None): | ||
311 | 303 | """Return Simplestreams signing policy for the given path. | ||
312 | 304 | |||
313 | 305 | :param path: Path to the Simplestreams index file. | ||
314 | 306 | :param keyring: Optional keyring file for verifying signatures. | ||
315 | 307 | :return: A "signing policy" callable. It accepts a file's content, path, | ||
316 | 308 | and optional keyring as arguments, and if the signature verifies | ||
317 | 309 | correctly, returns the content. The keyring defaults to the one you | ||
318 | 310 | pass. | ||
319 | 311 | """ | ||
320 | 312 | if path.endswith('.json'): | ||
321 | 313 | # The configuration deliberately selected an un-signed index. A signed | ||
322 | 314 | # index would have a suffix of '.sjson'. Use a policy that doesn't | ||
323 | 315 | # check anything. | ||
324 | 316 | policy = lambda content, path, keyring: content | ||
325 | 317 | else: | ||
326 | 318 | # Otherwise: use default Simplestreams policy for verifying signatures. | ||
327 | 319 | policy = policy_read_signed | ||
328 | 320 | |||
329 | 321 | if keyring is not None: | ||
330 | 322 | # Pass keyring to the policy, to use if the caller inside Simplestreams | ||
331 | 323 | # does not provide one. | ||
332 | 324 | policy = functools.partial(policy, keyring=keyring) | ||
333 | 325 | |||
334 | 326 | return policy | ||
335 | 327 | |||
336 | 328 | |||
337 | 329 | def clean_up_repo_item(item): | ||
338 | 330 | """Return a subset of dict `item` for storing in a boot images dict.""" | ||
339 | 331 | keys_to_keep = ['content_id', 'product_name', 'version_name', 'path'] | ||
340 | 332 | compact_item = {key: item[key] for key in keys_to_keep} | ||
341 | 333 | return compact_item | ||
342 | 334 | |||
343 | 335 | |||
344 | 336 | class RepoDumper(BasicMirrorWriter): | ||
345 | 337 | """Gather metadata about boot images available in a Simplestreams repo. | ||
346 | 338 | |||
347 | 339 | Used inside `download_image_descriptions`. Stores basic metadata about | ||
348 | 340 | each image it finds upstream in a given `BootImageMapping`. Each stored | ||
349 | 341 | item is a dict containing the basic metadata for retrieving a boot image. | ||
350 | 342 | |||
351 | 343 | Simplestreams' `BasicMirrorWriter` in itself is stateless. It relies on | ||
352 | 344 | a subclass (such as this one) to store data. | ||
353 | 345 | |||
354 | 346 | :ivar boot_images_dict: A `BootImageMapping`. Image metadata will be | ||
355 | 347 | stored here as it is discovered. Simplestreams does not interact with | ||
356 | 348 | this variable. | ||
357 | 349 | """ | ||
358 | 350 | |||
359 | 351 | def __init__(self, boot_images_dict): | ||
360 | 352 | super(RepoDumper, self).__init__() | ||
361 | 353 | self.boot_images_dict = boot_images_dict | ||
362 | 354 | |||
363 | 355 | def load_products(self, path=None, content_id=None): | ||
364 | 356 | """Overridable from `BasicMirrorWriter`.""" | ||
365 | 357 | # It looks as if this method only makes sense for MirrorReaders, not | ||
366 | 358 | # for MirrorWriters. The default MirrorWriter implementation just | ||
367 | 359 | # raises NotImplementedError. Stop it from doing that. | ||
368 | 360 | return | ||
369 | 361 | |||
370 | 362 | def insert_item(self, data, src, target, pedigree, contentsource): | ||
371 | 363 | """Overridable from `BasicMirrorWriter`.""" | ||
372 | 364 | item = products_exdata(src, pedigree) | ||
373 | 365 | arch, subarches = item['arch'], item['subarches'] | ||
374 | 366 | release = item['release'] | ||
375 | 367 | label = item['label'] | ||
376 | 368 | base_image = ImageSpec(arch, None, release, label) | ||
377 | 369 | compact_item = clean_up_repo_item(item) | ||
378 | 370 | for subarch in subarches.split(','): | ||
379 | 371 | self.boot_images_dict.setdefault( | ||
380 | 372 | base_image._replace(subarch=subarch), compact_item) | ||
381 | 373 | |||
382 | 374 | |||
383 | 375 | def download_image_descriptions(path, keyring=None): | ||
384 | 376 | """Download image metadata from upstream Simplestreams repo. | ||
385 | 377 | |||
386 | 378 | :param path: The path to a Simplestreams repo. | ||
387 | 379 | :param keyring: Optional keyring for verifying the repo's signatures. | ||
388 | 380 | :return: A nested dict of data, indexed by image arch, subarch, release, | ||
389 | 381 | and label. | ||
390 | 382 | """ | ||
391 | 383 | mirror, rpath = path_from_mirror_url(path, None) | ||
392 | 384 | policy = get_signing_policy(rpath, keyring) | ||
393 | 385 | reader = UrlMirrorReader(mirror, policy=policy) | ||
394 | 386 | boot_images_dict = BootImageMapping() | ||
395 | 387 | dumper = RepoDumper(boot_images_dict) | ||
396 | 388 | dumper.sync(reader, rpath) | ||
397 | 389 | return boot_images_dict | ||
398 | 390 | |||
399 | 391 | |||
400 | 392 | def download_all_image_descriptions(config): | ||
401 | 393 | """Download image metadata for all sources in `config`.""" | ||
402 | 394 | boot = BootImageMapping() | ||
403 | 395 | for source in config['boot']['sources']: | ||
404 | 396 | repo_boot = download_image_descriptions( | ||
405 | 397 | source['path'], keyring=source['keyring']) | ||
406 | 398 | boot_merge(boot, repo_boot, source['selections']) | ||
407 | 399 | return boot | ||
408 | 400 | |||
409 | 401 | |||
410 | 402 | class RepoWriter(BasicMirrorWriter): | 117 | class RepoWriter(BasicMirrorWriter): |
411 | 403 | """Download boot resources from an upstream Simplestreams repo. | 118 | """Download boot resources from an upstream Simplestreams repo. |
412 | 404 | 119 | ||
413 | @@ -474,12 +189,6 @@ | |||
414 | 474 | os.link(src, link_path) | 189 | os.link(src, link_path) |
415 | 475 | 190 | ||
416 | 476 | 191 | ||
417 | 477 | def available_boot_resources(root): | ||
418 | 478 | for resource_path in glob.glob(os.path.join(root, '*/*/*/*')): | ||
419 | 479 | arch, subarch, release, label = resource_path.split('/')[-4:] | ||
420 | 480 | yield (arch, subarch, release, label) | ||
421 | 481 | |||
422 | 482 | |||
423 | 483 | def install_boot_loaders(destination): | 192 | def install_boot_loaders(destination): |
424 | 484 | """Install the all the required file from each bootloader method. | 193 | """Install the all the required file from each bootloader method. |
425 | 485 | :param destination: Directory where the loaders should be stored. | 194 | :param destination: Directory where the loaders should be stored. |
426 | 486 | 195 | ||
427 | === added file 'src/provisioningserver/import_images/download_descriptions.py' | |||
428 | --- src/provisioningserver/import_images/download_descriptions.py 1970-01-01 00:00:00 +0000 | |||
429 | +++ src/provisioningserver/import_images/download_descriptions.py 2014-04-08 10:22:55 +0000 | |||
430 | @@ -0,0 +1,187 @@ | |||
431 | 1 | # Copyright 2014 Canonical Ltd. This software is licensed under the | ||
432 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
433 | 3 | |||
434 | 4 | """Download boot resource descriptions from Simplestreams repo. | ||
435 | 5 | |||
436 | 6 | This module is responsible only for syncing the repo's metadata, not the boot | ||
437 | 7 | resources themselves. The two are handled in separate Simplestreams | ||
438 | 8 | synchronisation stages. | ||
439 | 9 | """ | ||
440 | 10 | |||
441 | 11 | from __future__ import ( | ||
442 | 12 | absolute_import, | ||
443 | 13 | print_function, | ||
444 | 14 | unicode_literals, | ||
445 | 15 | ) | ||
446 | 16 | |||
447 | 17 | str = None | ||
448 | 18 | |||
449 | 19 | __metaclass__ = type | ||
450 | 20 | __all__ = [ | ||
451 | 21 | 'download_all_image_descriptions', | ||
452 | 22 | ] | ||
453 | 23 | |||
454 | 24 | |||
455 | 25 | from provisioningserver.import_images.boot_image_mapping import ( | ||
456 | 26 | BootImageMapping, | ||
457 | 27 | ) | ||
458 | 28 | from provisioningserver.import_images.helpers import ( | ||
459 | 29 | get_signing_policy, | ||
460 | 30 | ImageSpec, | ||
461 | 31 | logger, | ||
462 | 32 | ) | ||
463 | 33 | from simplestreams.mirrors import ( | ||
464 | 34 | BasicMirrorWriter, | ||
465 | 35 | UrlMirrorReader, | ||
466 | 36 | ) | ||
467 | 37 | from simplestreams.util import ( | ||
468 | 38 | path_from_mirror_url, | ||
469 | 39 | products_exdata, | ||
470 | 40 | ) | ||
471 | 41 | |||
472 | 42 | |||
473 | 43 | def clean_up_repo_item(item): | ||
474 | 44 | """Return a subset of dict `item` for storing in a boot images dict.""" | ||
475 | 45 | keys_to_keep = ['content_id', 'product_name', 'version_name', 'path'] | ||
476 | 46 | compact_item = {key: item[key] for key in keys_to_keep} | ||
477 | 47 | return compact_item | ||
478 | 48 | |||
479 | 49 | |||
480 | 50 | class RepoDumper(BasicMirrorWriter): | ||
481 | 51 | """Gather metadata about boot images available in a Simplestreams repo. | ||
482 | 52 | |||
483 | 53 | Used inside `download_image_descriptions`. Stores basic metadata about | ||
484 | 54 | each image it finds upstream in a given `BootImageMapping`. Each stored | ||
485 | 55 | item is a dict containing the basic metadata for retrieving a boot image. | ||
486 | 56 | |||
487 | 57 | Simplestreams' `BasicMirrorWriter` in itself is stateless. It relies on | ||
488 | 58 | a subclass (such as this one) to store data. | ||
489 | 59 | |||
490 | 60 | :ivar boot_images_dict: A `BootImageMapping`. Image metadata will be | ||
491 | 61 | stored here as it is discovered. Simplestreams does not interact with | ||
492 | 62 | this variable. | ||
493 | 63 | """ | ||
494 | 64 | |||
495 | 65 | def __init__(self, boot_images_dict): | ||
496 | 66 | super(RepoDumper, self).__init__() | ||
497 | 67 | self.boot_images_dict = boot_images_dict | ||
498 | 68 | |||
499 | 69 | def load_products(self, path=None, content_id=None): | ||
500 | 70 | """Overridable from `BasicMirrorWriter`.""" | ||
501 | 71 | # It looks as if this method only makes sense for MirrorReaders, not | ||
502 | 72 | # for MirrorWriters. The default MirrorWriter implementation just | ||
503 | 73 | # raises NotImplementedError. Stop it from doing that. | ||
504 | 74 | return | ||
505 | 75 | |||
506 | 76 | def insert_item(self, data, src, target, pedigree, contentsource): | ||
507 | 77 | """Overridable from `BasicMirrorWriter`.""" | ||
508 | 78 | item = products_exdata(src, pedigree) | ||
509 | 79 | arch, subarches = item['arch'], item['subarches'] | ||
510 | 80 | release = item['release'] | ||
511 | 81 | label = item['label'] | ||
512 | 82 | base_image = ImageSpec(arch, None, release, label) | ||
513 | 83 | compact_item = clean_up_repo_item(item) | ||
514 | 84 | for subarch in subarches.split(','): | ||
515 | 85 | self.boot_images_dict.setdefault( | ||
516 | 86 | base_image._replace(subarch=subarch), compact_item) | ||
517 | 87 | |||
518 | 88 | |||
519 | 89 | def value_passes_filter_list(filter_list, property_value): | ||
520 | 90 | """Does the given property of a boot image pass the given filter list? | ||
521 | 91 | |||
522 | 92 | The value passes if either it matches one of the entries in the list of | ||
523 | 93 | filter values, or one of the filter values is an asterisk (`*`). | ||
524 | 94 | """ | ||
525 | 95 | return '*' in filter_list or property_value in filter_list | ||
526 | 96 | |||
527 | 97 | |||
528 | 98 | def value_passes_filter(filter_value, property_value): | ||
529 | 99 | """Does the given property of a boot image pass the given filter? | ||
530 | 100 | |||
531 | 101 | The value passes the filter if either the filter value is an asterisk | ||
532 | 102 | (`*`) or the value is equal to the filter value. | ||
533 | 103 | """ | ||
534 | 104 | return filter_value in ('*', property_value) | ||
535 | 105 | |||
536 | 106 | |||
537 | 107 | def image_passes_filter(filters, arch, subarch, release, label): | ||
538 | 108 | """Filter a boot image against configured import filters. | ||
539 | 109 | |||
540 | 110 | :param filters: A list of dicts describing the filters, as in `boot_merge`. | ||
541 | 111 | If the list is empty, or `None`, any image matches. Any entry in a | ||
542 | 112 | filter may be a string containing just an asterisk (`*`) to denote that | ||
543 | 113 | the entry will match any value. | ||
544 | 114 | :param arch: The given boot image's architecture. | ||
545 | 115 | :param subarch: The given boot image's subarchitecture. | ||
546 | 116 | :param release: The given boot image's OS release. | ||
547 | 117 | :param label: The given boot image's label. | ||
548 | 118 | :return: Whether the image matches any of the dicts in `filters`. | ||
549 | 119 | """ | ||
550 | 120 | if filters is None or len(filters) == 0: | ||
551 | 121 | return True | ||
552 | 122 | for filter_dict in filters: | ||
553 | 123 | item_matches = ( | ||
554 | 124 | value_passes_filter(filter_dict['release'], release) and | ||
555 | 125 | value_passes_filter_list(filter_dict['arches'], arch) and | ||
556 | 126 | value_passes_filter_list(filter_dict['subarches'], subarch) and | ||
557 | 127 | value_passes_filter_list(filter_dict['labels'], label) | ||
558 | 128 | ) | ||
559 | 129 | if item_matches: | ||
560 | 130 | return True | ||
561 | 131 | return False | ||
562 | 132 | |||
563 | 133 | |||
564 | 134 | def boot_merge(destination, additions, filters=None): | ||
565 | 135 | """Complement one `BootImageMapping` with entries from another. | ||
566 | 136 | |||
567 | 137 | This adds entries from `additions` (that match `filters`, if given) to | ||
568 | 138 | `destination`, but only for those image specs for which `destination` does | ||
569 | 139 | not have entries yet. | ||
570 | 140 | |||
571 | 141 | :param destination: `BootImageMapping` to be updated. It will be extended | ||
572 | 142 | in-place. | ||
573 | 143 | :param additions: A second `BootImageMapping`, which will be used as a | ||
574 | 144 | source of additional entries. | ||
575 | 145 | :param filters: List of dicts, each of which contains 'arch', 'subarch', | ||
576 | 146 | and 'release' keys. If given, entries are only considered for copying | ||
577 | 147 | from `additions` to `destination` if they match at least one of the | ||
578 | 148 | filters. Entries in the filter may be the string `*` (or for entries | ||
579 | 149 | that are lists, may contain the string `*`) to make them match any | ||
580 | 150 | value. | ||
581 | 151 | """ | ||
582 | 152 | for image, resource in additions.items(): | ||
583 | 153 | arch, subarch, release, label = image | ||
584 | 154 | if image_passes_filter(filters, arch, subarch, release, label): | ||
585 | 155 | logger.debug( | ||
586 | 156 | "Merging boot resource for %s/%s/%s/%s.", | ||
587 | 157 | arch, subarch, release, label) | ||
588 | 158 | # Do not override an existing entry with the same | ||
589 | 159 | # arch/subarch/release/label: the first entry found takes | ||
590 | 160 | # precedence. | ||
591 | 161 | destination.setdefault(image, resource) | ||
592 | 162 | |||
593 | 163 | |||
594 | 164 | def download_image_descriptions(path, keyring=None): | ||
595 | 165 | """Download image metadata from upstream Simplestreams repo. | ||
596 | 166 | |||
597 | 167 | :param path: The path to a Simplestreams repo. | ||
598 | 168 | :param keyring: Optional keyring for verifying the repo's signatures. | ||
599 | 169 | :return: A `BootImageMapping` describing available boot resources. | ||
600 | 170 | """ | ||
601 | 171 | mirror, rpath = path_from_mirror_url(path, None) | ||
602 | 172 | policy = get_signing_policy(rpath, keyring) | ||
603 | 173 | reader = UrlMirrorReader(mirror, policy=policy) | ||
604 | 174 | boot_images_dict = BootImageMapping() | ||
605 | 175 | dumper = RepoDumper(boot_images_dict) | ||
606 | 176 | dumper.sync(reader, rpath) | ||
607 | 177 | return boot_images_dict | ||
608 | 178 | |||
609 | 179 | |||
610 | 180 | def download_all_image_descriptions(config): | ||
611 | 181 | """Download image metadata for all sources in `config`.""" | ||
612 | 182 | boot = BootImageMapping() | ||
613 | 183 | for source in config['boot']['sources']: | ||
614 | 184 | repo_boot = download_image_descriptions( | ||
615 | 185 | source['path'], keyring=source['keyring']) | ||
616 | 186 | boot_merge(boot, repo_boot, source['selections']) | ||
617 | 187 | return boot | ||
618 | 0 | 188 | ||
619 | === added file 'src/provisioningserver/import_images/helpers.py' | |||
620 | --- src/provisioningserver/import_images/helpers.py 1970-01-01 00:00:00 +0000 | |||
621 | +++ src/provisioningserver/import_images/helpers.py 2014-04-08 10:22:55 +0000 | |||
622 | @@ -0,0 +1,73 @@ | |||
623 | 1 | # Copyright 2014 Canonical Ltd. This software is licensed under the | ||
624 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
625 | 3 | |||
626 | 4 | """Miscellaneous small definitions in support of boot-resource import.""" | ||
627 | 5 | |||
628 | 6 | from __future__ import ( | ||
629 | 7 | absolute_import, | ||
630 | 8 | print_function, | ||
631 | 9 | unicode_literals, | ||
632 | 10 | ) | ||
633 | 11 | |||
634 | 12 | str = None | ||
635 | 13 | |||
636 | 14 | __metaclass__ = type | ||
637 | 15 | __all__ = [ | ||
638 | 16 | 'get_signing_policy', | ||
639 | 17 | 'ImageSpec', | ||
640 | 18 | 'logger', | ||
641 | 19 | ] | ||
642 | 20 | |||
643 | 21 | from collections import namedtuple | ||
644 | 22 | import functools | ||
645 | 23 | import logging | ||
646 | 24 | |||
647 | 25 | from simplestreams.util import policy_read_signed | ||
648 | 26 | |||
649 | 27 | # A tuple of the items that together select a boot image. | ||
650 | 28 | ImageSpec = namedtuple(b'ImageSpec', [ | ||
651 | 29 | 'arch', | ||
652 | 30 | 'subarch', | ||
653 | 31 | 'release', | ||
654 | 32 | 'label', | ||
655 | 33 | ]) | ||
656 | 34 | |||
657 | 35 | |||
658 | 36 | def get_signing_policy(path, keyring=None): | ||
659 | 37 | """Return Simplestreams signing policy for the given path. | ||
660 | 38 | |||
661 | 39 | :param path: Path to the Simplestreams index file. | ||
662 | 40 | :param keyring: Optional keyring file for verifying signatures. | ||
663 | 41 | :return: A "signing policy" callable. It accepts a file's content, path, | ||
664 | 42 | and optional keyring as arguments, and if the signature verifies | ||
665 | 43 | correctly, returns the content. The keyring defaults to the one you | ||
666 | 44 | pass. | ||
667 | 45 | """ | ||
668 | 46 | if path.endswith('.json'): | ||
669 | 47 | # The configuration deliberately selected an un-signed index. A signed | ||
670 | 48 | # index would have a suffix of '.sjson'. Use a policy that doesn't | ||
671 | 49 | # check anything. | ||
672 | 50 | policy = lambda content, path, keyring: content | ||
673 | 51 | else: | ||
674 | 52 | # Otherwise: use default Simplestreams policy for verifying signatures. | ||
675 | 53 | policy = policy_read_signed | ||
676 | 54 | |||
677 | 55 | if keyring is not None: | ||
678 | 56 | # Pass keyring to the policy, to use if the caller inside Simplestreams | ||
679 | 57 | # does not provide one. | ||
680 | 58 | policy = functools.partial(policy, keyring=keyring) | ||
681 | 59 | |||
682 | 60 | return policy | ||
683 | 61 | |||
684 | 62 | |||
685 | 63 | def init_logger(log_level=logging.INFO): | ||
686 | 64 | logger = logging.getLogger(__name__) | ||
687 | 65 | formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') | ||
688 | 66 | handler = logging.StreamHandler() | ||
689 | 67 | handler.setFormatter(formatter) | ||
690 | 68 | logger.addHandler(handler) | ||
691 | 69 | logger.setLevel(log_level) | ||
692 | 70 | return logger | ||
693 | 71 | |||
694 | 72 | |||
695 | 73 | logger = init_logger() | ||
696 | 0 | 74 | ||
697 | === added file 'src/provisioningserver/import_images/product_mapping.py' | |||
698 | --- src/provisioningserver/import_images/product_mapping.py 1970-01-01 00:00:00 +0000 | |||
699 | +++ src/provisioningserver/import_images/product_mapping.py 2014-04-08 10:22:55 +0000 | |||
700 | @@ -0,0 +1,65 @@ | |||
701 | 1 | # Copyright 2014 Canonical Ltd. This software is licensed under the | ||
702 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
703 | 3 | |||
704 | 4 | """The `ProductMapping` class.""" | ||
705 | 5 | |||
706 | 6 | from __future__ import ( | ||
707 | 7 | absolute_import, | ||
708 | 8 | print_function, | ||
709 | 9 | unicode_literals, | ||
710 | 10 | ) | ||
711 | 11 | |||
712 | 12 | str = None | ||
713 | 13 | |||
714 | 14 | __metaclass__ = type | ||
715 | 15 | __all__ = [ | ||
716 | 16 | 'ProductMapping', | ||
717 | 17 | ] | ||
718 | 18 | |||
719 | 19 | |||
720 | 20 | class ProductMapping: | ||
721 | 21 | """Mapping of product data. | ||
722 | 22 | |||
723 | 23 | Maps a combination of boot resource metadata (`content_id`, `product_name`, | ||
724 | 24 | `version_name`) to a list of subarchitectures supported by that boot | ||
725 | 25 | resource. | ||
726 | 26 | """ | ||
727 | 27 | |||
728 | 28 | def __init__(self): | ||
729 | 29 | self.mapping = {} | ||
730 | 30 | |||
731 | 31 | @staticmethod | ||
732 | 32 | def make_key(resource): | ||
733 | 33 | """Extract a key tuple from `resource`. | ||
734 | 34 | |||
735 | 35 | The key is used for indexing `mapping`. | ||
736 | 36 | |||
737 | 37 | :param resource: A dict describing a boot resource. It must contain | ||
738 | 38 | the keys `content_id`, `product_name`, and `version_name`. | ||
739 | 39 | :return: A tuple of the resource's content ID, product name, and | ||
740 | 40 | version name. | ||
741 | 41 | """ | ||
742 | 42 | return ( | ||
743 | 43 | resource['content_id'], | ||
744 | 44 | resource['product_name'], | ||
745 | 45 | resource['version_name'], | ||
746 | 46 | ) | ||
747 | 47 | |||
748 | 48 | def add(self, resource, subarch): | ||
749 | 49 | """Add `subarch` to the list of subarches supported by a boot resource. | ||
750 | 50 | |||
751 | 51 | The `resource` is a dict as returned by `products_exdata`. The method | ||
752 | 52 | will use the values identified by keys `content_id`, `product_name`, | ||
753 | 53 | and `version_name`. | ||
754 | 54 | """ | ||
755 | 55 | key = self.make_key(resource) | ||
756 | 56 | self.mapping.setdefault(key, []) | ||
757 | 57 | self.mapping[key].append(subarch) | ||
758 | 58 | |||
759 | 59 | def contains(self, resource): | ||
760 | 60 | """Does the dict contain a mapping for the given resource?""" | ||
761 | 61 | return self.make_key(resource) in self.mapping | ||
762 | 62 | |||
763 | 63 | def get(self, resource): | ||
764 | 64 | """Return the mapped subarchitectures for `resource`.""" | ||
765 | 65 | return self.mapping[self.make_key(resource)] | ||
766 | 0 | 66 | ||
767 | === added directory 'src/provisioningserver/import_images/testing' | |||
768 | === added file 'src/provisioningserver/import_images/testing/__init__.py' | |||
769 | === added file 'src/provisioningserver/import_images/testing/factory.py' | |||
770 | --- src/provisioningserver/import_images/testing/factory.py 1970-01-01 00:00:00 +0000 | |||
771 | +++ src/provisioningserver/import_images/testing/factory.py 2014-04-08 10:22:55 +0000 | |||
772 | @@ -0,0 +1,56 @@ | |||
773 | 1 | # Copyright 2014 Canonical Ltd. This software is licensed under the | ||
774 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
775 | 3 | |||
776 | 4 | """Factory helpers for the `import_images` package.""" | ||
777 | 5 | |||
778 | 6 | from __future__ import ( | ||
779 | 7 | absolute_import, | ||
780 | 8 | print_function, | ||
781 | 9 | unicode_literals, | ||
782 | 10 | ) | ||
783 | 11 | |||
784 | 12 | str = None | ||
785 | 13 | |||
786 | 14 | __metaclass__ = type | ||
787 | 15 | __all__ = [ | ||
788 | 16 | 'make_boot_resource', | ||
789 | 17 | 'make_image_spec', | ||
790 | 18 | 'set_resource', | ||
791 | 19 | ] | ||
792 | 20 | |||
793 | 21 | from maastesting.factory import factory | ||
794 | 22 | from provisioningserver.import_images.boot_image_mapping import ( | ||
795 | 23 | BootImageMapping, | ||
796 | 24 | ) | ||
797 | 25 | from provisioningserver.import_images.helpers import ImageSpec | ||
798 | 26 | |||
799 | 27 | |||
800 | 28 | def make_boot_resource(): | ||
801 | 29 | """Create a fake resource dict.""" | ||
802 | 30 | return { | ||
803 | 31 | 'content_id': factory.make_name('content_id'), | ||
804 | 32 | 'product_name': factory.make_name('product_name'), | ||
805 | 33 | 'version_name': factory.make_name('version_name'), | ||
806 | 34 | } | ||
807 | 35 | |||
808 | 36 | |||
809 | 37 | def make_image_spec(): | ||
810 | 38 | """Return an `ImageSpec` with random values.""" | ||
811 | 39 | return ImageSpec( | ||
812 | 40 | factory.make_name('arch'), | ||
813 | 41 | factory.make_name('subarch'), | ||
814 | 42 | factory.make_name('release'), | ||
815 | 43 | factory.make_name('label'), | ||
816 | 44 | ) | ||
817 | 45 | |||
818 | 46 | |||
819 | 47 | def set_resource(boot_dict=None, image_spec=None, resource=None): | ||
820 | 48 | """Add boot resource to a `BootImageMapping`, creating it if necessary.""" | ||
821 | 49 | if boot_dict is None: | ||
822 | 50 | boot_dict = BootImageMapping() | ||
823 | 51 | if image_spec is None: | ||
824 | 52 | image_spec = make_image_spec() | ||
825 | 53 | if resource is None: | ||
826 | 54 | resource = factory.make_name('boot-resource') | ||
827 | 55 | boot_dict.mapping[image_spec] = resource | ||
828 | 56 | return boot_dict | ||
829 | 0 | 57 | ||
830 | === added file 'src/provisioningserver/import_images/tests/test_boot_image_mapping.py' | |||
831 | --- src/provisioningserver/import_images/tests/test_boot_image_mapping.py 1970-01-01 00:00:00 +0000 | |||
832 | +++ src/provisioningserver/import_images/tests/test_boot_image_mapping.py 2014-04-08 10:22:55 +0000 | |||
833 | @@ -0,0 +1,98 @@ | |||
834 | 1 | # Copyright 2014 Canonical Ltd. This software is licensed under the | ||
835 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
836 | 3 | |||
837 | 4 | """Tests for `BootImageMapping` and its module.""" | ||
838 | 5 | |||
839 | 6 | from __future__ import ( | ||
840 | 7 | absolute_import, | ||
841 | 8 | print_function, | ||
842 | 9 | unicode_literals, | ||
843 | 10 | ) | ||
844 | 11 | |||
845 | 12 | str = None | ||
846 | 13 | |||
847 | 14 | __metaclass__ = type | ||
848 | 15 | __all__ = [] | ||
849 | 16 | |||
850 | 17 | import json | ||
851 | 18 | |||
852 | 19 | from maastesting.factory import factory | ||
853 | 20 | from maastesting.testcase import MAASTestCase | ||
854 | 21 | from provisioningserver.import_images.boot_image_mapping import ( | ||
855 | 22 | BootImageMapping, | ||
856 | 23 | ) | ||
857 | 24 | from provisioningserver.import_images.testing.factory import ( | ||
858 | 25 | make_image_spec, | ||
859 | 26 | set_resource, | ||
860 | 27 | ) | ||
861 | 28 | |||
862 | 29 | |||
863 | 30 | class TestBootImageMapping(MAASTestCase): | ||
864 | 31 | """Tests for `BootImageMapping`.""" | ||
865 | 32 | |||
866 | 33 | def test_initially_empty(self): | ||
867 | 34 | self.assertItemsEqual([], BootImageMapping().items()) | ||
868 | 35 | |||
869 | 36 | def test_items_returns_items(self): | ||
870 | 37 | image = make_image_spec() | ||
871 | 38 | resource = factory.make_name('resource') | ||
872 | 39 | image_dict = set_resource(image_spec=image, resource=resource) | ||
873 | 40 | self.assertItemsEqual([(image, resource)], image_dict.items()) | ||
874 | 41 | |||
875 | 42 | def test_setdefault_sets_unset_item(self): | ||
876 | 43 | image_dict = BootImageMapping() | ||
877 | 44 | image = make_image_spec() | ||
878 | 45 | resource = factory.make_name('resource') | ||
879 | 46 | image_dict.setdefault(image, resource) | ||
880 | 47 | self.assertItemsEqual([(image, resource)], image_dict.items()) | ||
881 | 48 | |||
882 | 49 | def test_setdefault_leaves_set_item_unchanged(self): | ||
883 | 50 | image = make_image_spec() | ||
884 | 51 | old_resource = factory.make_name('resource') | ||
885 | 52 | image_dict = set_resource(image_spec=image, resource=old_resource) | ||
886 | 53 | image_dict.setdefault(image, factory.make_name('newresource')) | ||
887 | 54 | self.assertItemsEqual([(image, old_resource)], image_dict.items()) | ||
888 | 55 | |||
889 | 56 | def test_dump_json_is_consistent(self): | ||
890 | 57 | image = make_image_spec() | ||
891 | 58 | resource = factory.make_name('resource') | ||
892 | 59 | image_dict_1 = set_resource(image_spec=image, resource=resource) | ||
893 | 60 | image_dict_2 = set_resource(image_spec=image, resource=resource) | ||
894 | 61 | self.assertEqual(image_dict_1.dump_json(), image_dict_2.dump_json()) | ||
895 | 62 | |||
896 | 63 | def test_dump_json_represents_empty_dict_as_empty_object(self): | ||
897 | 64 | self.assertEqual('{}', BootImageMapping().dump_json()) | ||
898 | 65 | |||
899 | 66 | def test_dump_json_represents_entry(self): | ||
900 | 67 | image = make_image_spec() | ||
901 | 68 | resource = factory.make_name('resource') | ||
902 | 69 | image_dict = set_resource(image_spec=image, resource=resource) | ||
903 | 70 | self.assertEqual( | ||
904 | 71 | { | ||
905 | 72 | image.arch: { | ||
906 | 73 | image.subarch: { | ||
907 | 74 | image.release: {image.label: resource}, | ||
908 | 75 | }, | ||
909 | 76 | }, | ||
910 | 77 | }, | ||
911 | 78 | json.loads(image_dict.dump_json())) | ||
912 | 79 | |||
913 | 80 | def test_dump_json_combines_similar_entries(self): | ||
914 | 81 | image = make_image_spec() | ||
915 | 82 | other_release = factory.make_name('other-release') | ||
916 | 83 | resource1 = factory.make_name('resource') | ||
917 | 84 | resource2 = factory.make_name('other-resource') | ||
918 | 85 | image_dict = BootImageMapping() | ||
919 | 86 | set_resource(image_dict, image, resource1) | ||
920 | 87 | set_resource( | ||
921 | 88 | image_dict, image._replace(release=other_release), resource2) | ||
922 | 89 | self.assertEqual( | ||
923 | 90 | { | ||
924 | 91 | image.arch: { | ||
925 | 92 | image.subarch: { | ||
926 | 93 | image.release: {image.label: resource1}, | ||
927 | 94 | other_release: {image.label: resource2}, | ||
928 | 95 | }, | ||
929 | 96 | }, | ||
930 | 97 | }, | ||
931 | 98 | json.loads(image_dict.dump_json())) | ||
932 | 0 | 99 | ||
933 | === modified file 'src/provisioningserver/import_images/tests/test_boot_resources.py' | |||
934 | --- src/provisioningserver/import_images/tests/test_boot_resources.py 2014-04-08 06:00:29 +0000 | |||
935 | +++ src/provisioningserver/import_images/tests/test_boot_resources.py 2014-04-08 10:22:55 +0000 | |||
936 | @@ -25,14 +25,20 @@ | |||
937 | 25 | ) | 25 | ) |
938 | 26 | 26 | ||
939 | 27 | from maastesting.factory import factory | 27 | from maastesting.factory import factory |
940 | 28 | from maastesting.matchers import MockCalledOnceWith | ||
941 | 29 | from maastesting.testcase import MAASTestCase | 28 | from maastesting.testcase import MAASTestCase |
942 | 30 | import mock | 29 | import mock |
943 | 31 | from provisioningserver.boot.uefi import UEFIBootMethod | 30 | from provisioningserver.boot.uefi import UEFIBootMethod |
944 | 32 | from provisioningserver.config import BootConfig | 31 | from provisioningserver.config import BootConfig |
945 | 33 | from provisioningserver.import_images import boot_resources | 32 | from provisioningserver.import_images import boot_resources |
946 | 33 | from provisioningserver.import_images.boot_image_mapping import ( | ||
947 | 34 | BootImageMapping, | ||
948 | 35 | ) | ||
949 | 36 | from provisioningserver.import_images.testing.factory import ( | ||
950 | 37 | make_boot_resource, | ||
951 | 38 | make_image_spec, | ||
952 | 39 | set_resource, | ||
953 | 40 | ) | ||
954 | 34 | from provisioningserver.utils import write_text_file | 41 | from provisioningserver.utils import write_text_file |
955 | 35 | from simplestreams.util import SignatureMissingException | ||
956 | 36 | from testtools.content import Content | 42 | from testtools.content import Content |
957 | 37 | from testtools.content_type import UTF8_TEXT | 43 | from testtools.content_type import UTF8_TEXT |
958 | 38 | from testtools.matchers import ( | 44 | from testtools.matchers import ( |
959 | @@ -42,257 +48,11 @@ | |||
960 | 42 | import yaml | 48 | import yaml |
961 | 43 | 49 | ||
962 | 44 | 50 | ||
963 | 45 | def make_image_spec(): | ||
964 | 46 | """Return an `ImageSpec` with random values.""" | ||
965 | 47 | return boot_resources.ImageSpec( | ||
966 | 48 | factory.make_name('arch'), | ||
967 | 49 | factory.make_name('subarch'), | ||
968 | 50 | factory.make_name('release'), | ||
969 | 51 | factory.make_name('label'), | ||
970 | 52 | ) | ||
971 | 53 | |||
972 | 54 | |||
973 | 55 | class TestBootImageMapping(MAASTestCase): | ||
974 | 56 | """Tests for `BootImageMapping`.""" | ||
975 | 57 | |||
976 | 58 | def test_initially_empty(self): | ||
977 | 59 | self.assertItemsEqual([], boot_resources.BootImageMapping().items()) | ||
978 | 60 | |||
979 | 61 | def test_items_returns_items(self): | ||
980 | 62 | image = make_image_spec() | ||
981 | 63 | resource = factory.make_name('resource') | ||
982 | 64 | image_dict = set_resource(image_spec=image, resource=resource) | ||
983 | 65 | self.assertItemsEqual([(image, resource)], image_dict.items()) | ||
984 | 66 | |||
985 | 67 | def test_setdefault_sets_unset_item(self): | ||
986 | 68 | image_dict = boot_resources.BootImageMapping() | ||
987 | 69 | image = make_image_spec() | ||
988 | 70 | resource = factory.make_name('resource') | ||
989 | 71 | image_dict.setdefault(image, resource) | ||
990 | 72 | self.assertItemsEqual([(image, resource)], image_dict.items()) | ||
991 | 73 | |||
992 | 74 | def test_setdefault_leaves_set_item_unchanged(self): | ||
993 | 75 | image = make_image_spec() | ||
994 | 76 | old_resource = factory.make_name('resource') | ||
995 | 77 | image_dict = set_resource(image_spec=image, resource=old_resource) | ||
996 | 78 | image_dict.setdefault(image, factory.make_name('newresource')) | ||
997 | 79 | self.assertItemsEqual([(image, old_resource)], image_dict.items()) | ||
998 | 80 | |||
999 | 81 | def test_dump_json_is_consistent(self): | ||
1000 | 82 | image = make_image_spec() | ||
1001 | 83 | resource = factory.make_name('resource') | ||
1002 | 84 | image_dict_1 = set_resource(image_spec=image, resource=resource) | ||
1003 | 85 | image_dict_2 = set_resource(image_spec=image, resource=resource) | ||
1004 | 86 | self.assertEqual(image_dict_1.dump_json(), image_dict_2.dump_json()) | ||
1005 | 87 | |||
1006 | 88 | def test_dump_json_represents_empty_dict_as_empty_object(self): | ||
1007 | 89 | self.assertEqual('{}', boot_resources.BootImageMapping().dump_json()) | ||
1008 | 90 | |||
1009 | 91 | def test_dump_json_represents_entry(self): | ||
1010 | 92 | image = make_image_spec() | ||
1011 | 93 | resource = factory.make_name('resource') | ||
1012 | 94 | image_dict = set_resource(image_spec=image, resource=resource) | ||
1013 | 95 | self.assertEqual( | ||
1014 | 96 | { | ||
1015 | 97 | image.arch: { | ||
1016 | 98 | image.subarch: { | ||
1017 | 99 | image.release: {image.label: resource}, | ||
1018 | 100 | }, | ||
1019 | 101 | }, | ||
1020 | 102 | }, | ||
1021 | 103 | json.loads(image_dict.dump_json())) | ||
1022 | 104 | |||
1023 | 105 | def test_dump_json_combines_similar_entries(self): | ||
1024 | 106 | image = make_image_spec() | ||
1025 | 107 | other_release = factory.make_name('other-release') | ||
1026 | 108 | resource1 = factory.make_name('resource') | ||
1027 | 109 | resource2 = factory.make_name('other-resource') | ||
1028 | 110 | image_dict = boot_resources.BootImageMapping() | ||
1029 | 111 | set_resource(image_dict, image, resource1) | ||
1030 | 112 | set_resource( | ||
1031 | 113 | image_dict, image._replace(release=other_release), resource2) | ||
1032 | 114 | self.assertEqual( | ||
1033 | 115 | { | ||
1034 | 116 | image.arch: { | ||
1035 | 117 | image.subarch: { | ||
1036 | 118 | image.release: {image.label: resource1}, | ||
1037 | 119 | other_release: {image.label: resource2}, | ||
1038 | 120 | }, | ||
1039 | 121 | }, | ||
1040 | 122 | }, | ||
1041 | 123 | json.loads(image_dict.dump_json())) | ||
1042 | 124 | |||
1043 | 125 | |||
1044 | 126 | class TestValuePassesFilterList(MAASTestCase): | ||
1045 | 127 | """Tests for `value_passes_filter_list`.""" | ||
1046 | 128 | |||
1047 | 129 | def test_nothing_passes_empty_list(self): | ||
1048 | 130 | self.assertFalse( | ||
1049 | 131 | boot_resources.value_passes_filter_list( | ||
1050 | 132 | [], factory.make_name('value'))) | ||
1051 | 133 | |||
1052 | 134 | def test_unmatched_value_does_not_pass(self): | ||
1053 | 135 | self.assertFalse( | ||
1054 | 136 | boot_resources.value_passes_filter_list( | ||
1055 | 137 | [factory.make_name('filter')], factory.make_name('value'))) | ||
1056 | 138 | |||
1057 | 139 | def test_matched_value_passes(self): | ||
1058 | 140 | value = factory.make_name('value') | ||
1059 | 141 | self.assertTrue( | ||
1060 | 142 | boot_resources.value_passes_filter_list([value], value)) | ||
1061 | 143 | |||
1062 | 144 | def test_value_passes_if_matched_anywhere_in_filter(self): | ||
1063 | 145 | value = factory.make_name('value') | ||
1064 | 146 | self.assertTrue( | ||
1065 | 147 | boot_resources.value_passes_filter_list( | ||
1066 | 148 | [ | ||
1067 | 149 | factory.make_name('filter'), | ||
1068 | 150 | value, | ||
1069 | 151 | factory.make_name('filter'), | ||
1070 | 152 | ], | ||
1071 | 153 | value)) | ||
1072 | 154 | |||
1073 | 155 | def test_any_value_passes_asterisk(self): | ||
1074 | 156 | self.assertTrue( | ||
1075 | 157 | boot_resources.value_passes_filter_list( | ||
1076 | 158 | ['*'], factory.make_name('value'))) | ||
1077 | 159 | |||
1078 | 160 | |||
1079 | 161 | class TestValuePassesFilter(MAASTestCase): | ||
1080 | 162 | """Tests for `value_passes_filter`.""" | ||
1081 | 163 | |||
1082 | 164 | def test_unmatched_value_does_not_pass(self): | ||
1083 | 165 | self.assertFalse( | ||
1084 | 166 | boot_resources.value_passes_filter( | ||
1085 | 167 | factory.make_name('filter'), factory.make_name('value'))) | ||
1086 | 168 | |||
1087 | 169 | def test_matching_value_passes(self): | ||
1088 | 170 | value = factory.make_name('value') | ||
1089 | 171 | self.assertTrue(boot_resources.value_passes_filter(value, value)) | ||
1090 | 172 | |||
1091 | 173 | def test_any_value_matches_asterisk(self): | ||
1092 | 174 | self.assertTrue( | ||
1093 | 175 | boot_resources.value_passes_filter( | ||
1094 | 176 | '*', factory.make_name('value'))) | ||
1095 | 177 | |||
1096 | 178 | |||
1097 | 179 | def make_boot_resource(): | ||
1098 | 180 | """Create a fake resource dict.""" | ||
1099 | 181 | return { | ||
1100 | 182 | 'content_id': factory.make_name('content_id'), | ||
1101 | 183 | 'product_name': factory.make_name('product_name'), | ||
1102 | 184 | 'version_name': factory.make_name('version_name'), | ||
1103 | 185 | } | ||
1104 | 186 | |||
1105 | 187 | |||
1106 | 188 | class TestProductMapping(MAASTestCase): | ||
1107 | 189 | """Tests for `ProductMapping`.""" | ||
1108 | 190 | |||
1109 | 191 | def test_initially_empty(self): | ||
1110 | 192 | self.assertEqual({}, boot_resources.ProductMapping().mapping) | ||
1111 | 193 | |||
1112 | 194 | def test_make_key_extracts_identifying_items(self): | ||
1113 | 195 | resource = make_boot_resource() | ||
1114 | 196 | content_id = resource['content_id'] | ||
1115 | 197 | product_name = resource['product_name'] | ||
1116 | 198 | version_name = resource['version_name'] | ||
1117 | 199 | self.assertEqual( | ||
1118 | 200 | (content_id, product_name, version_name), | ||
1119 | 201 | boot_resources.ProductMapping.make_key(resource)) | ||
1120 | 202 | |||
1121 | 203 | def test_make_key_ignores_other_items(self): | ||
1122 | 204 | resource = make_boot_resource() | ||
1123 | 205 | resource['other_item'] = factory.make_name('other') | ||
1124 | 206 | self.assertEqual( | ||
1125 | 207 | ( | ||
1126 | 208 | resource['content_id'], | ||
1127 | 209 | resource['product_name'], | ||
1128 | 210 | resource['version_name'], | ||
1129 | 211 | ), | ||
1130 | 212 | boot_resources.ProductMapping.make_key(resource)) | ||
1131 | 213 | |||
1132 | 214 | def test_make_key_fails_if_key_missing(self): | ||
1133 | 215 | resource = make_boot_resource() | ||
1134 | 216 | del resource['version_name'] | ||
1135 | 217 | self.assertRaises( | ||
1136 | 218 | KeyError, | ||
1137 | 219 | boot_resources.ProductMapping.make_key, resource) | ||
1138 | 220 | |||
1139 | 221 | def test_add_creates_subarches_list_if_needed(self): | ||
1140 | 222 | product_dict = boot_resources.ProductMapping() | ||
1141 | 223 | resource = make_boot_resource() | ||
1142 | 224 | subarch = factory.make_name('subarch') | ||
1143 | 225 | product_dict.add(resource, subarch) | ||
1144 | 226 | self.assertEqual( | ||
1145 | 227 | {product_dict.make_key(resource): [subarch]}, | ||
1146 | 228 | product_dict.mapping) | ||
1147 | 229 | |||
1148 | 230 | def test_add_appends_to_existing_list(self): | ||
1149 | 231 | product_dict = boot_resources.ProductMapping() | ||
1150 | 232 | resource = make_boot_resource() | ||
1151 | 233 | subarches = [factory.make_name('subarch') for _ in range(2)] | ||
1152 | 234 | for subarch in subarches: | ||
1153 | 235 | product_dict.add(resource, subarch) | ||
1154 | 236 | self.assertEqual( | ||
1155 | 237 | {product_dict.make_key(resource): subarches}, | ||
1156 | 238 | product_dict.mapping) | ||
1157 | 239 | |||
1158 | 240 | def test_contains_returns_true_for_stored_item(self): | ||
1159 | 241 | product_dict = boot_resources.ProductMapping() | ||
1160 | 242 | resource = make_boot_resource() | ||
1161 | 243 | subarch = factory.make_name('subarch') | ||
1162 | 244 | product_dict.add(resource, subarch) | ||
1163 | 245 | self.assertTrue(product_dict.contains(resource)) | ||
1164 | 246 | |||
1165 | 247 | def test_contains_returns_false_for_unstored_item(self): | ||
1166 | 248 | self.assertFalse( | ||
1167 | 249 | boot_resources.ProductMapping().contains(make_boot_resource())) | ||
1168 | 250 | |||
1169 | 251 | def test_contains_ignores_similar_items(self): | ||
1170 | 252 | product_dict = boot_resources.ProductMapping() | ||
1171 | 253 | resource = make_boot_resource() | ||
1172 | 254 | subarch = factory.make_name('subarch') | ||
1173 | 255 | product_dict.add(resource.copy(), subarch) | ||
1174 | 256 | resource['product_name'] = factory.make_name('other') | ||
1175 | 257 | self.assertFalse(product_dict.contains(resource)) | ||
1176 | 258 | |||
1177 | 259 | def test_contains_ignores_extraneous_keys(self): | ||
1178 | 260 | product_dict = boot_resources.ProductMapping() | ||
1179 | 261 | resource = make_boot_resource() | ||
1180 | 262 | subarch = factory.make_name('subarch') | ||
1181 | 263 | product_dict.add(resource.copy(), subarch) | ||
1182 | 264 | resource['other_item'] = factory.make_name('other') | ||
1183 | 265 | self.assertTrue(product_dict.contains(resource)) | ||
1184 | 266 | |||
1185 | 267 | def test_get_returns_stored_item(self): | ||
1186 | 268 | product_dict = boot_resources.ProductMapping() | ||
1187 | 269 | resource = make_boot_resource() | ||
1188 | 270 | subarch = factory.make_name('subarch') | ||
1189 | 271 | product_dict.add(resource, subarch) | ||
1190 | 272 | self.assertEqual([subarch], product_dict.get(resource)) | ||
1191 | 273 | |||
1192 | 274 | def test_get_fails_for_unstored_item(self): | ||
1193 | 275 | product_dict = boot_resources.ProductMapping() | ||
1194 | 276 | resource = make_boot_resource() | ||
1195 | 277 | subarch = factory.make_name('subarch') | ||
1196 | 278 | product_dict.add(resource.copy(), subarch) | ||
1197 | 279 | resource['content_id'] = factory.make_name('other') | ||
1198 | 280 | self.assertRaises(KeyError, product_dict.get, resource) | ||
1199 | 281 | |||
1200 | 282 | def test_get_ignores_extraneous_keys(self): | ||
1201 | 283 | product_dict = boot_resources.ProductMapping() | ||
1202 | 284 | resource = make_boot_resource() | ||
1203 | 285 | subarch = factory.make_name('subarch') | ||
1204 | 286 | product_dict.add(resource, subarch) | ||
1205 | 287 | resource['other_item'] = factory.make_name('other') | ||
1206 | 288 | self.assertEqual([subarch], product_dict.get(resource)) | ||
1207 | 289 | |||
1208 | 290 | |||
1209 | 291 | class TestBootReverse(MAASTestCase): | 51 | class TestBootReverse(MAASTestCase): |
1210 | 292 | """Tests for `boot_reverse`.""" | 52 | """Tests for `boot_reverse`.""" |
1211 | 293 | 53 | ||
1212 | 294 | def test_maps_empty_dict_to_empty_dict(self): | 54 | def test_maps_empty_dict_to_empty_dict(self): |
1214 | 295 | empty_boot_image_dict = boot_resources.BootImageMapping() | 55 | empty_boot_image_dict = BootImageMapping() |
1215 | 296 | self.assertEqual( | 56 | self.assertEqual( |
1216 | 297 | {}, | 57 | {}, |
1217 | 298 | boot_resources.boot_reverse(empty_boot_image_dict).mapping) | 58 | boot_resources.boot_reverse(empty_boot_image_dict).mapping) |
1218 | @@ -315,7 +75,7 @@ | |||
1219 | 315 | image1 = make_image_spec() | 75 | image1 = make_image_spec() |
1220 | 316 | image2 = make_image_spec() | 76 | image2 = make_image_spec() |
1221 | 317 | resource = make_boot_resource() | 77 | resource = make_boot_resource() |
1223 | 318 | boot_dict = boot_resources.BootImageMapping() | 78 | boot_dict = BootImageMapping() |
1224 | 319 | # Create two images in boot_dict, both containing the same resource. | 79 | # Create two images in boot_dict, both containing the same resource. |
1225 | 320 | for image in [image1, image2]: | 80 | for image in [image1, image2]: |
1226 | 321 | set_resource( | 81 | set_resource( |
1227 | @@ -334,186 +94,6 @@ | |||
1228 | 334 | reverse_dict.get(resource)) | 94 | reverse_dict.get(resource)) |
1229 | 335 | 95 | ||
1230 | 336 | 96 | ||
1231 | 337 | class TestImagePassesFilter(MAASTestCase): | ||
1232 | 338 | """Tests for `image_passes_filter`.""" | ||
1233 | 339 | |||
1234 | 340 | def make_filter_from_image(self, image_spec=None): | ||
1235 | 341 | """Create a filter dict that matches the given `ImageSpec`. | ||
1236 | 342 | |||
1237 | 343 | If `image_spec` is not given, creates a random value. | ||
1238 | 344 | """ | ||
1239 | 345 | if image_spec is None: | ||
1240 | 346 | image_spec = make_image_spec() | ||
1241 | 347 | return { | ||
1242 | 348 | 'arches': [image_spec.arch], | ||
1243 | 349 | 'subarches': [image_spec.subarch], | ||
1244 | 350 | 'release': image_spec.release, | ||
1245 | 351 | 'labels': [image_spec.label], | ||
1246 | 352 | } | ||
1247 | 353 | |||
1248 | 354 | def test_any_image_passes_none_filter(self): | ||
1249 | 355 | arch, subarch, release, label = make_image_spec() | ||
1250 | 356 | self.assertTrue( | ||
1251 | 357 | boot_resources.image_passes_filter( | ||
1252 | 358 | None, arch, subarch, release, label)) | ||
1253 | 359 | |||
1254 | 360 | def test_any_image_passes_empty_filter(self): | ||
1255 | 361 | arch, subarch, release, label = make_image_spec() | ||
1256 | 362 | self.assertTrue( | ||
1257 | 363 | boot_resources.image_passes_filter( | ||
1258 | 364 | [], arch, subarch, release, label)) | ||
1259 | 365 | |||
1260 | 366 | def test_image_passes_matching_filter(self): | ||
1261 | 367 | image = make_image_spec() | ||
1262 | 368 | self.assertTrue( | ||
1263 | 369 | boot_resources.image_passes_filter( | ||
1264 | 370 | [self.make_filter_from_image(image)], | ||
1265 | 371 | image.arch, image.subarch, image.release, image.label)) | ||
1266 | 372 | |||
1267 | 373 | def test_image_does_not_pass_nonmatching_filter(self): | ||
1268 | 374 | image = make_image_spec() | ||
1269 | 375 | self.assertFalse( | ||
1270 | 376 | boot_resources.image_passes_filter( | ||
1271 | 377 | [self.make_filter_from_image()], | ||
1272 | 378 | image.arch, image.subarch, image.release, image.label)) | ||
1273 | 379 | |||
1274 | 380 | def test_image_passes_if_one_filter_matches(self): | ||
1275 | 381 | image = make_image_spec() | ||
1276 | 382 | self.assertTrue( | ||
1277 | 383 | boot_resources.image_passes_filter( | ||
1278 | 384 | [ | ||
1279 | 385 | self.make_filter_from_image(), | ||
1280 | 386 | self.make_filter_from_image(image), | ||
1281 | 387 | self.make_filter_from_image(), | ||
1282 | 388 | ], image.arch, image.subarch, image.release, image.label)) | ||
1283 | 389 | |||
1284 | 390 | def test_filter_checks_release(self): | ||
1285 | 391 | image = make_image_spec() | ||
1286 | 392 | self.assertFalse( | ||
1287 | 393 | boot_resources.image_passes_filter( | ||
1288 | 394 | [ | ||
1289 | 395 | self.make_filter_from_image(image._replace( | ||
1290 | 396 | release=factory.make_name('other-release'))) | ||
1291 | 397 | ], image.arch, image.subarch, image.release, image.label)) | ||
1292 | 398 | |||
1293 | 399 | def test_filter_checks_arches(self): | ||
1294 | 400 | image = make_image_spec() | ||
1295 | 401 | self.assertFalse( | ||
1296 | 402 | boot_resources.image_passes_filter( | ||
1297 | 403 | [ | ||
1298 | 404 | self.make_filter_from_image(image._replace( | ||
1299 | 405 | arch=factory.make_name('other-arch'))) | ||
1300 | 406 | ], image.arch, image.subarch, image.release, image.label)) | ||
1301 | 407 | |||
1302 | 408 | def test_filter_checks_subarches(self): | ||
1303 | 409 | image = make_image_spec() | ||
1304 | 410 | self.assertFalse( | ||
1305 | 411 | boot_resources.image_passes_filter( | ||
1306 | 412 | [ | ||
1307 | 413 | self.make_filter_from_image(image._replace( | ||
1308 | 414 | subarch=factory.make_name('other-subarch'))) | ||
1309 | 415 | ], image.arch, image.subarch, image.release, image.label)) | ||
1310 | 416 | |||
1311 | 417 | def test_filter_checks_labels(self): | ||
1312 | 418 | image = make_image_spec() | ||
1313 | 419 | self.assertFalse( | ||
1314 | 420 | boot_resources.image_passes_filter( | ||
1315 | 421 | [ | ||
1316 | 422 | self.make_filter_from_image(image._replace( | ||
1317 | 423 | label=factory.make_name('other-label'))) | ||
1318 | 424 | ], image.arch, image.subarch, image.release, image.label)) | ||
1319 | 425 | |||
1320 | 426 | |||
1321 | 427 | def set_resource(boot_dict=None, image_spec=None, resource=None): | ||
1322 | 428 | """Add boot resource to a `BootImageMapping`, creating it if necessary.""" | ||
1323 | 429 | if boot_dict is None: | ||
1324 | 430 | boot_dict = boot_resources.BootImageMapping() | ||
1325 | 431 | if image_spec is None: | ||
1326 | 432 | image_spec = make_image_spec() | ||
1327 | 433 | if resource is None: | ||
1328 | 434 | resource = factory.make_name('boot-resource') | ||
1329 | 435 | boot_dict.mapping[image_spec] = resource | ||
1330 | 436 | return boot_dict | ||
1331 | 437 | |||
1332 | 438 | |||
1333 | 439 | class TestBootMerge(MAASTestCase): | ||
1334 | 440 | """Tests for `boot_merge`.""" | ||
1335 | 441 | |||
1336 | 442 | def test_integrates(self): | ||
1337 | 443 | # End-to-end scenario for boot_merge: start with an empty boot | ||
1338 | 444 | # resources dict, and receive one resource from Simplestreams. | ||
1339 | 445 | total_resources = boot_resources.BootImageMapping() | ||
1340 | 446 | resources_from_repo = set_resource() | ||
1341 | 447 | boot_resources.boot_merge(total_resources, resources_from_repo) | ||
1342 | 448 | # Since we started with an empty dict, the result contains the same | ||
1343 | 449 | # item that we got from Simplestreams, and nothing else. | ||
1344 | 450 | self.assertEqual(resources_from_repo.mapping, total_resources.mapping) | ||
1345 | 451 | |||
1346 | 452 | def test_obeys_filters(self): | ||
1347 | 453 | filters = [ | ||
1348 | 454 | { | ||
1349 | 455 | 'arches': [factory.make_name('other-arch')], | ||
1350 | 456 | 'subarches': [factory.make_name('other-subarch')], | ||
1351 | 457 | 'release': factory.make_name('other-release'), | ||
1352 | 458 | 'label': [factory.make_name('other-label')], | ||
1353 | 459 | }, | ||
1354 | 460 | ] | ||
1355 | 461 | total_resources = boot_resources.BootImageMapping() | ||
1356 | 462 | resources_from_repo = set_resource() | ||
1357 | 463 | boot_resources.boot_merge( | ||
1358 | 464 | total_resources, resources_from_repo, filters=filters) | ||
1359 | 465 | self.assertEqual({}, total_resources.mapping) | ||
1360 | 466 | |||
1361 | 467 | def test_does_not_overwrite_existing_entry(self): | ||
1362 | 468 | image = make_image_spec() | ||
1363 | 469 | total_resources = set_resource( | ||
1364 | 470 | resource="Original resource", image_spec=image) | ||
1365 | 471 | original_resources = total_resources.mapping.copy() | ||
1366 | 472 | resources_from_repo = set_resource( | ||
1367 | 473 | resource="New resource", image_spec=image) | ||
1368 | 474 | boot_resources.boot_merge(total_resources, resources_from_repo) | ||
1369 | 475 | self.assertEqual(original_resources, total_resources.mapping) | ||
1370 | 476 | |||
1371 | 477 | |||
1372 | 478 | class TestGetSigningPolicy(MAASTestCase): | ||
1373 | 479 | """Tests for `get_signing_policy`.""" | ||
1374 | 480 | |||
1375 | 481 | def test_picks_nonchecking_policy_for_json_index(self): | ||
1376 | 482 | path = 'streams/v1/index.json' | ||
1377 | 483 | policy = boot_resources.get_signing_policy(path) | ||
1378 | 484 | content = factory.getRandomString() | ||
1379 | 485 | self.assertEqual( | ||
1380 | 486 | content, | ||
1381 | 487 | policy(content, path, factory.make_name('keyring'))) | ||
1382 | 488 | |||
1383 | 489 | def test_picks_checking_policy_for_sjson_index(self): | ||
1384 | 490 | path = 'streams/v1/index.sjson' | ||
1385 | 491 | content = factory.getRandomString() | ||
1386 | 492 | policy = boot_resources.get_signing_policy(path) | ||
1387 | 493 | self.assertRaises( | ||
1388 | 494 | SignatureMissingException, | ||
1389 | 495 | policy, content, path, factory.make_name('keyring')) | ||
1390 | 496 | |||
1391 | 497 | def test_picks_checking_policy_for_json_gpg_index(self): | ||
1392 | 498 | path = 'streams/v1/index.json.gpg' | ||
1393 | 499 | content = factory.getRandomString() | ||
1394 | 500 | policy = boot_resources.get_signing_policy(path) | ||
1395 | 501 | self.assertRaises( | ||
1396 | 502 | SignatureMissingException, | ||
1397 | 503 | policy, content, path, factory.make_name('keyring')) | ||
1398 | 504 | |||
1399 | 505 | def test_injects_default_keyring_if_passed(self): | ||
1400 | 506 | path = 'streams/v1/index.json.gpg' | ||
1401 | 507 | content = factory.getRandomString() | ||
1402 | 508 | keyring = factory.make_name('keyring') | ||
1403 | 509 | self.patch(boot_resources, 'policy_read_signed') | ||
1404 | 510 | policy = boot_resources.get_signing_policy(path, keyring) | ||
1405 | 511 | policy(content, path) | ||
1406 | 512 | self.assertThat( | ||
1407 | 513 | boot_resources.policy_read_signed, | ||
1408 | 514 | MockCalledOnceWith(mock.ANY, mock.ANY, keyring=keyring)) | ||
1409 | 515 | |||
1410 | 516 | |||
1411 | 517 | class TestTgtEntry(MAASTestCase): | 97 | class TestTgtEntry(MAASTestCase): |
1412 | 518 | """Tests for `tgt_entry`.""" | 98 | """Tests for `tgt_entry`.""" |
1413 | 519 | 99 | ||
1414 | 520 | 100 | ||
1415 | === added file 'src/provisioningserver/import_images/tests/test_download_descriptions.py' | |||
1416 | --- src/provisioningserver/import_images/tests/test_download_descriptions.py 1970-01-01 00:00:00 +0000 | |||
1417 | +++ src/provisioningserver/import_images/tests/test_download_descriptions.py 2014-04-08 10:22:55 +0000 | |||
1418 | @@ -0,0 +1,209 @@ | |||
1419 | 1 | # Copyright 2014 Canonical Ltd. This software is licensed under the | ||
1420 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
1421 | 3 | |||
1422 | 4 | """Tests for the `download_descriptions` module.""" | ||
1423 | 5 | |||
1424 | 6 | from __future__ import ( | ||
1425 | 7 | absolute_import, | ||
1426 | 8 | print_function, | ||
1427 | 9 | unicode_literals, | ||
1428 | 10 | ) | ||
1429 | 11 | |||
1430 | 12 | str = None | ||
1431 | 13 | |||
1432 | 14 | __metaclass__ = type | ||
1433 | 15 | __all__ = [] | ||
1434 | 16 | |||
1435 | 17 | from maastesting.factory import factory | ||
1436 | 18 | from maastesting.testcase import MAASTestCase | ||
1437 | 19 | from provisioningserver.import_images import download_descriptions | ||
1438 | 20 | from provisioningserver.import_images.boot_image_mapping import ( | ||
1439 | 21 | BootImageMapping, | ||
1440 | 22 | ) | ||
1441 | 23 | from provisioningserver.import_images.testing.factory import ( | ||
1442 | 24 | make_image_spec, | ||
1443 | 25 | set_resource, | ||
1444 | 26 | ) | ||
1445 | 27 | |||
1446 | 28 | |||
1447 | 29 | class TestValuePassesFilterList(MAASTestCase): | ||
1448 | 30 | """Tests for `value_passes_filter_list`.""" | ||
1449 | 31 | |||
1450 | 32 | def test_nothing_passes_empty_list(self): | ||
1451 | 33 | self.assertFalse( | ||
1452 | 34 | download_descriptions.value_passes_filter_list( | ||
1453 | 35 | [], factory.make_name('value'))) | ||
1454 | 36 | |||
1455 | 37 | def test_unmatched_value_does_not_pass(self): | ||
1456 | 38 | self.assertFalse( | ||
1457 | 39 | download_descriptions.value_passes_filter_list( | ||
1458 | 40 | [factory.make_name('filter')], factory.make_name('value'))) | ||
1459 | 41 | |||
1460 | 42 | def test_matched_value_passes(self): | ||
1461 | 43 | value = factory.make_name('value') | ||
1462 | 44 | self.assertTrue( | ||
1463 | 45 | download_descriptions.value_passes_filter_list([value], value)) | ||
1464 | 46 | |||
1465 | 47 | def test_value_passes_if_matched_anywhere_in_filter(self): | ||
1466 | 48 | value = factory.make_name('value') | ||
1467 | 49 | self.assertTrue( | ||
1468 | 50 | download_descriptions.value_passes_filter_list( | ||
1469 | 51 | [ | ||
1470 | 52 | factory.make_name('filter'), | ||
1471 | 53 | value, | ||
1472 | 54 | factory.make_name('filter'), | ||
1473 | 55 | ], | ||
1474 | 56 | value)) | ||
1475 | 57 | |||
1476 | 58 | def test_any_value_passes_asterisk(self): | ||
1477 | 59 | self.assertTrue( | ||
1478 | 60 | download_descriptions.value_passes_filter_list( | ||
1479 | 61 | ['*'], factory.make_name('value'))) | ||
1480 | 62 | |||
1481 | 63 | |||
1482 | 64 | class TestValuePassesFilter(MAASTestCase): | ||
1483 | 65 | """Tests for `value_passes_filter`.""" | ||
1484 | 66 | |||
1485 | 67 | def test_unmatched_value_does_not_pass(self): | ||
1486 | 68 | self.assertFalse( | ||
1487 | 69 | download_descriptions.value_passes_filter( | ||
1488 | 70 | factory.make_name('filter'), factory.make_name('value'))) | ||
1489 | 71 | |||
1490 | 72 | def test_matching_value_passes(self): | ||
1491 | 73 | value = factory.make_name('value') | ||
1492 | 74 | self.assertTrue( | ||
1493 | 75 | download_descriptions.value_passes_filter(value, value)) | ||
1494 | 76 | |||
1495 | 77 | def test_any_value_matches_asterisk(self): | ||
1496 | 78 | self.assertTrue( | ||
1497 | 79 | download_descriptions.value_passes_filter( | ||
1498 | 80 | '*', factory.make_name('value'))) | ||
1499 | 81 | |||
1500 | 82 | |||
1501 | 83 | class TestImagePassesFilter(MAASTestCase): | ||
1502 | 84 | """Tests for `image_passes_filter`.""" | ||
1503 | 85 | |||
1504 | 86 | def make_filter_from_image(self, image_spec=None): | ||
1505 | 87 | """Create a filter dict that matches the given `ImageSpec`. | ||
1506 | 88 | |||
1507 | 89 | If `image_spec` is not given, creates a random value. | ||
1508 | 90 | """ | ||
1509 | 91 | if image_spec is None: | ||
1510 | 92 | image_spec = make_image_spec() | ||
1511 | 93 | return { | ||
1512 | 94 | 'arches': [image_spec.arch], | ||
1513 | 95 | 'subarches': [image_spec.subarch], | ||
1514 | 96 | 'release': image_spec.release, | ||
1515 | 97 | 'labels': [image_spec.label], | ||
1516 | 98 | } | ||
1517 | 99 | |||
1518 | 100 | def test_any_image_passes_none_filter(self): | ||
1519 | 101 | arch, subarch, release, label = make_image_spec() | ||
1520 | 102 | self.assertTrue( | ||
1521 | 103 | download_descriptions.image_passes_filter( | ||
1522 | 104 | None, arch, subarch, release, label)) | ||
1523 | 105 | |||
1524 | 106 | def test_any_image_passes_empty_filter(self): | ||
1525 | 107 | arch, subarch, release, label = make_image_spec() | ||
1526 | 108 | self.assertTrue( | ||
1527 | 109 | download_descriptions.image_passes_filter( | ||
1528 | 110 | [], arch, subarch, release, label)) | ||
1529 | 111 | |||
1530 | 112 | def test_image_passes_matching_filter(self): | ||
1531 | 113 | image = make_image_spec() | ||
1532 | 114 | self.assertTrue( | ||
1533 | 115 | download_descriptions.image_passes_filter( | ||
1534 | 116 | [self.make_filter_from_image(image)], | ||
1535 | 117 | image.arch, image.subarch, image.release, image.label)) | ||
1536 | 118 | |||
1537 | 119 | def test_image_does_not_pass_nonmatching_filter(self): | ||
1538 | 120 | image = make_image_spec() | ||
1539 | 121 | self.assertFalse( | ||
1540 | 122 | download_descriptions.image_passes_filter( | ||
1541 | 123 | [self.make_filter_from_image()], | ||
1542 | 124 | image.arch, image.subarch, image.release, image.label)) | ||
1543 | 125 | |||
1544 | 126 | def test_image_passes_if_one_filter_matches(self): | ||
1545 | 127 | image = make_image_spec() | ||
1546 | 128 | self.assertTrue( | ||
1547 | 129 | download_descriptions.image_passes_filter( | ||
1548 | 130 | [ | ||
1549 | 131 | self.make_filter_from_image(), | ||
1550 | 132 | self.make_filter_from_image(image), | ||
1551 | 133 | self.make_filter_from_image(), | ||
1552 | 134 | ], image.arch, image.subarch, image.release, image.label)) | ||
1553 | 135 | |||
1554 | 136 | def test_filter_checks_release(self): | ||
1555 | 137 | image = make_image_spec() | ||
1556 | 138 | self.assertFalse( | ||
1557 | 139 | download_descriptions.image_passes_filter( | ||
1558 | 140 | [ | ||
1559 | 141 | self.make_filter_from_image(image._replace( | ||
1560 | 142 | release=factory.make_name('other-release'))) | ||
1561 | 143 | ], image.arch, image.subarch, image.release, image.label)) | ||
1562 | 144 | |||
1563 | 145 | def test_filter_checks_arches(self): | ||
1564 | 146 | image = make_image_spec() | ||
1565 | 147 | self.assertFalse( | ||
1566 | 148 | download_descriptions.image_passes_filter( | ||
1567 | 149 | [ | ||
1568 | 150 | self.make_filter_from_image(image._replace( | ||
1569 | 151 | arch=factory.make_name('other-arch'))) | ||
1570 | 152 | ], image.arch, image.subarch, image.release, image.label)) | ||
1571 | 153 | |||
1572 | 154 | def test_filter_checks_subarches(self): | ||
1573 | 155 | image = make_image_spec() | ||
1574 | 156 | self.assertFalse( | ||
1575 | 157 | download_descriptions.image_passes_filter( | ||
1576 | 158 | [ | ||
1577 | 159 | self.make_filter_from_image(image._replace( | ||
1578 | 160 | subarch=factory.make_name('other-subarch'))) | ||
1579 | 161 | ], image.arch, image.subarch, image.release, image.label)) | ||
1580 | 162 | |||
1581 | 163 | def test_filter_checks_labels(self): | ||
1582 | 164 | image = make_image_spec() | ||
1583 | 165 | self.assertFalse( | ||
1584 | 166 | download_descriptions.image_passes_filter( | ||
1585 | 167 | [ | ||
1586 | 168 | self.make_filter_from_image(image._replace( | ||
1587 | 169 | label=factory.make_name('other-label'))) | ||
1588 | 170 | ], image.arch, image.subarch, image.release, image.label)) | ||
1589 | 171 | |||
1590 | 172 | |||
1591 | 173 | class TestBootMerge(MAASTestCase): | ||
1592 | 174 | """Tests for `boot_merge`.""" | ||
1593 | 175 | |||
1594 | 176 | def test_integrates(self): | ||
1595 | 177 | # End-to-end scenario for boot_merge: start with an empty boot | ||
1596 | 178 | # resources dict, and receive one resource from Simplestreams. | ||
1597 | 179 | total_resources = BootImageMapping() | ||
1598 | 180 | resources_from_repo = set_resource() | ||
1599 | 181 | download_descriptions.boot_merge(total_resources, resources_from_repo) | ||
1600 | 182 | # Since we started with an empty dict, the result contains the same | ||
1601 | 183 | # item that we got from Simplestreams, and nothing else. | ||
1602 | 184 | self.assertEqual(resources_from_repo.mapping, total_resources.mapping) | ||
1603 | 185 | |||
1604 | 186 | def test_obeys_filters(self): | ||
1605 | 187 | filters = [ | ||
1606 | 188 | { | ||
1607 | 189 | 'arches': [factory.make_name('other-arch')], | ||
1608 | 190 | 'subarches': [factory.make_name('other-subarch')], | ||
1609 | 191 | 'release': factory.make_name('other-release'), | ||
1610 | 192 | 'label': [factory.make_name('other-label')], | ||
1611 | 193 | }, | ||
1612 | 194 | ] | ||
1613 | 195 | total_resources = BootImageMapping() | ||
1614 | 196 | resources_from_repo = set_resource() | ||
1615 | 197 | download_descriptions.boot_merge( | ||
1616 | 198 | total_resources, resources_from_repo, filters=filters) | ||
1617 | 199 | self.assertEqual({}, total_resources.mapping) | ||
1618 | 200 | |||
1619 | 201 | def test_does_not_overwrite_existing_entry(self): | ||
1620 | 202 | image = make_image_spec() | ||
1621 | 203 | total_resources = set_resource( | ||
1622 | 204 | resource="Original resource", image_spec=image) | ||
1623 | 205 | original_resources = total_resources.mapping.copy() | ||
1624 | 206 | resources_from_repo = set_resource( | ||
1625 | 207 | resource="New resource", image_spec=image) | ||
1626 | 208 | download_descriptions.boot_merge(total_resources, resources_from_repo) | ||
1627 | 209 | self.assertEqual(original_resources, total_resources.mapping) | ||
1628 | 0 | 210 | ||
1629 | === added file 'src/provisioningserver/import_images/tests/test_helpers.py' | |||
1630 | --- src/provisioningserver/import_images/tests/test_helpers.py 1970-01-01 00:00:00 +0000 | |||
1631 | +++ src/provisioningserver/import_images/tests/test_helpers.py 2014-04-08 10:22:55 +0000 | |||
1632 | @@ -0,0 +1,61 @@ | |||
1633 | 1 | # Copyright 2014 Canonical Ltd. This software is licensed under the | ||
1634 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
1635 | 3 | |||
1636 | 4 | """Tests for the `helpers` module.""" | ||
1637 | 5 | |||
1638 | 6 | from __future__ import ( | ||
1639 | 7 | absolute_import, | ||
1640 | 8 | print_function, | ||
1641 | 9 | unicode_literals, | ||
1642 | 10 | ) | ||
1643 | 11 | |||
1644 | 12 | str = None | ||
1645 | 13 | |||
1646 | 14 | __metaclass__ = type | ||
1647 | 15 | __all__ = [] | ||
1648 | 16 | |||
1649 | 17 | from maastesting.factory import factory | ||
1650 | 18 | from maastesting.matchers import MockCalledOnceWith | ||
1651 | 19 | from maastesting.testcase import MAASTestCase | ||
1652 | 20 | import mock | ||
1653 | 21 | from provisioningserver.import_images import helpers | ||
1654 | 22 | from simplestreams.util import SignatureMissingException | ||
1655 | 23 | |||
1656 | 24 | |||
1657 | 25 | class TestGetSigningPolicy(MAASTestCase): | ||
1658 | 26 | """Tests for `get_signing_policy`.""" | ||
1659 | 27 | |||
1660 | 28 | def test_picks_nonchecking_policy_for_json_index(self): | ||
1661 | 29 | path = 'streams/v1/index.json' | ||
1662 | 30 | policy = helpers.get_signing_policy(path) | ||
1663 | 31 | content = factory.getRandomString() | ||
1664 | 32 | self.assertEqual( | ||
1665 | 33 | content, | ||
1666 | 34 | policy(content, path, factory.make_name('keyring'))) | ||
1667 | 35 | |||
1668 | 36 | def test_picks_checking_policy_for_sjson_index(self): | ||
1669 | 37 | path = 'streams/v1/index.sjson' | ||
1670 | 38 | content = factory.getRandomString() | ||
1671 | 39 | policy = helpers.get_signing_policy(path) | ||
1672 | 40 | self.assertRaises( | ||
1673 | 41 | SignatureMissingException, | ||
1674 | 42 | policy, content, path, factory.make_name('keyring')) | ||
1675 | 43 | |||
1676 | 44 | def test_picks_checking_policy_for_json_gpg_index(self): | ||
1677 | 45 | path = 'streams/v1/index.json.gpg' | ||
1678 | 46 | content = factory.getRandomString() | ||
1679 | 47 | policy = helpers.get_signing_policy(path) | ||
1680 | 48 | self.assertRaises( | ||
1681 | 49 | SignatureMissingException, | ||
1682 | 50 | policy, content, path, factory.make_name('keyring')) | ||
1683 | 51 | |||
1684 | 52 | def test_injects_default_keyring_if_passed(self): | ||
1685 | 53 | path = 'streams/v1/index.json.gpg' | ||
1686 | 54 | content = factory.getRandomString() | ||
1687 | 55 | keyring = factory.make_name('keyring') | ||
1688 | 56 | self.patch(helpers, 'policy_read_signed') | ||
1689 | 57 | policy = helpers.get_signing_policy(path, keyring) | ||
1690 | 58 | policy(content, path) | ||
1691 | 59 | self.assertThat( | ||
1692 | 60 | helpers.policy_read_signed, | ||
1693 | 61 | MockCalledOnceWith(mock.ANY, mock.ANY, keyring=keyring)) | ||
1694 | 0 | 62 | ||
1695 | === added file 'src/provisioningserver/import_images/tests/test_product_mapping.py' | |||
1696 | --- src/provisioningserver/import_images/tests/test_product_mapping.py 1970-01-01 00:00:00 +0000 | |||
1697 | +++ src/provisioningserver/import_images/tests/test_product_mapping.py 2014-04-08 10:22:55 +0000 | |||
1698 | @@ -0,0 +1,125 @@ | |||
1699 | 1 | # Copyright 2014 Canonical Ltd. This software is licensed under the | ||
1700 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
1701 | 3 | |||
1702 | 4 | """Tests for the `ProductMapping` class.""" | ||
1703 | 5 | |||
1704 | 6 | from __future__ import ( | ||
1705 | 7 | absolute_import, | ||
1706 | 8 | print_function, | ||
1707 | 9 | unicode_literals, | ||
1708 | 10 | ) | ||
1709 | 11 | |||
1710 | 12 | str = None | ||
1711 | 13 | |||
1712 | 14 | __metaclass__ = type | ||
1713 | 15 | __all__ = [] | ||
1714 | 16 | |||
1715 | 17 | from maastesting.factory import factory | ||
1716 | 18 | from maastesting.testcase import MAASTestCase | ||
1717 | 19 | from provisioningserver.import_images.product_mapping import ProductMapping | ||
1718 | 20 | from provisioningserver.import_images.testing.factory import ( | ||
1719 | 21 | make_boot_resource, | ||
1720 | 22 | ) | ||
1721 | 23 | |||
1722 | 24 | |||
1723 | 25 | class TestProductMapping(MAASTestCase): | ||
1724 | 26 | """Tests for `ProductMapping`.""" | ||
1725 | 27 | |||
1726 | 28 | def test_initially_empty(self): | ||
1727 | 29 | self.assertEqual({}, ProductMapping().mapping) | ||
1728 | 30 | |||
1729 | 31 | def test_make_key_extracts_identifying_items(self): | ||
1730 | 32 | resource = make_boot_resource() | ||
1731 | 33 | content_id = resource['content_id'] | ||
1732 | 34 | product_name = resource['product_name'] | ||
1733 | 35 | version_name = resource['version_name'] | ||
1734 | 36 | self.assertEqual( | ||
1735 | 37 | (content_id, product_name, version_name), | ||
1736 | 38 | ProductMapping.make_key(resource)) | ||
1737 | 39 | |||
1738 | 40 | def test_make_key_ignores_other_items(self): | ||
1739 | 41 | resource = make_boot_resource() | ||
1740 | 42 | resource['other_item'] = factory.make_name('other') | ||
1741 | 43 | self.assertEqual( | ||
1742 | 44 | ( | ||
1743 | 45 | resource['content_id'], | ||
1744 | 46 | resource['product_name'], | ||
1745 | 47 | resource['version_name'], | ||
1746 | 48 | ), | ||
1747 | 49 | ProductMapping.make_key(resource)) | ||
1748 | 50 | |||
1749 | 51 | def test_make_key_fails_if_key_missing(self): | ||
1750 | 52 | resource = make_boot_resource() | ||
1751 | 53 | del resource['version_name'] | ||
1752 | 54 | self.assertRaises( | ||
1753 | 55 | KeyError, | ||
1754 | 56 | ProductMapping.make_key, resource) | ||
1755 | 57 | |||
1756 | 58 | def test_add_creates_subarches_list_if_needed(self): | ||
1757 | 59 | product_dict = ProductMapping() | ||
1758 | 60 | resource = make_boot_resource() | ||
1759 | 61 | subarch = factory.make_name('subarch') | ||
1760 | 62 | product_dict.add(resource, subarch) | ||
1761 | 63 | self.assertEqual( | ||
1762 | 64 | {product_dict.make_key(resource): [subarch]}, | ||
1763 | 65 | product_dict.mapping) | ||
1764 | 66 | |||
1765 | 67 | def test_add_appends_to_existing_list(self): | ||
1766 | 68 | product_dict = ProductMapping() | ||
1767 | 69 | resource = make_boot_resource() | ||
1768 | 70 | subarches = [factory.make_name('subarch') for _ in range(2)] | ||
1769 | 71 | for subarch in subarches: | ||
1770 | 72 | product_dict.add(resource, subarch) | ||
1771 | 73 | self.assertEqual( | ||
1772 | 74 | {product_dict.make_key(resource): subarches}, | ||
1773 | 75 | product_dict.mapping) | ||
1774 | 76 | |||
1775 | 77 | def test_contains_returns_true_for_stored_item(self): | ||
1776 | 78 | product_dict = ProductMapping() | ||
1777 | 79 | resource = make_boot_resource() | ||
1778 | 80 | subarch = factory.make_name('subarch') | ||
1779 | 81 | product_dict.add(resource, subarch) | ||
1780 | 82 | self.assertTrue(product_dict.contains(resource)) | ||
1781 | 83 | |||
1782 | 84 | def test_contains_returns_false_for_unstored_item(self): | ||
1783 | 85 | self.assertFalse( | ||
1784 | 86 | ProductMapping().contains(make_boot_resource())) | ||
1785 | 87 | |||
1786 | 88 | def test_contains_ignores_similar_items(self): | ||
1787 | 89 | product_dict = ProductMapping() | ||
1788 | 90 | resource = make_boot_resource() | ||
1789 | 91 | subarch = factory.make_name('subarch') | ||
1790 | 92 | product_dict.add(resource.copy(), subarch) | ||
1791 | 93 | resource['product_name'] = factory.make_name('other') | ||
1792 | 94 | self.assertFalse(product_dict.contains(resource)) | ||
1793 | 95 | |||
1794 | 96 | def test_contains_ignores_extraneous_keys(self): | ||
1795 | 97 | product_dict = ProductMapping() | ||
1796 | 98 | resource = make_boot_resource() | ||
1797 | 99 | subarch = factory.make_name('subarch') | ||
1798 | 100 | product_dict.add(resource.copy(), subarch) | ||
1799 | 101 | resource['other_item'] = factory.make_name('other') | ||
1800 | 102 | self.assertTrue(product_dict.contains(resource)) | ||
1801 | 103 | |||
1802 | 104 | def test_get_returns_stored_item(self): | ||
1803 | 105 | product_dict = ProductMapping() | ||
1804 | 106 | resource = make_boot_resource() | ||
1805 | 107 | subarch = factory.make_name('subarch') | ||
1806 | 108 | product_dict.add(resource, subarch) | ||
1807 | 109 | self.assertEqual([subarch], product_dict.get(resource)) | ||
1808 | 110 | |||
1809 | 111 | def test_get_fails_for_unstored_item(self): | ||
1810 | 112 | product_dict = ProductMapping() | ||
1811 | 113 | resource = make_boot_resource() | ||
1812 | 114 | subarch = factory.make_name('subarch') | ||
1813 | 115 | product_dict.add(resource.copy(), subarch) | ||
1814 | 116 | resource['content_id'] = factory.make_name('other') | ||
1815 | 117 | self.assertRaises(KeyError, product_dict.get, resource) | ||
1816 | 118 | |||
1817 | 119 | def test_get_ignores_extraneous_keys(self): | ||
1818 | 120 | product_dict = ProductMapping() | ||
1819 | 121 | resource = make_boot_resource() | ||
1820 | 122 | subarch = factory.make_name('subarch') | ||
1821 | 123 | product_dict.add(resource, subarch) | ||
1822 | 124 | resource['other_item'] = factory.make_name('other') | ||
1823 | 125 | self.assertEqual([subarch], product_dict.get(resource)) |
I love these branches that are smaller than they look. Gives me a sense of achievement for not much cost :).