Merge lp:~jtv/maas/split-boot_resources into lp:~maas-committers/maas/trunk

Proposed by Jeroen T. Vermeulen
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
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_boot_resources: being unused and untested, it was removed).

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

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) wrote :

I love these branches that are smaller than they look. Gives me a sense of achievement for not much cost :).

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :

Attempt to merge into lp:maas failed due to conflicts:

text conflict in src/provisioningserver/import_images/tests/test_boot_resources.py

Revision history for this message
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

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