Merge lp:~blake-rouse/maas/simplestreams-os-field into lp:~maas-committers/maas/trunk

Proposed by Blake Rouse
Status: Merged
Approved by: Blake Rouse
Approved revision: no longer in the source branch.
Merged at revision: 2798
Proposed branch: lp:~blake-rouse/maas/simplestreams-os-field
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 654 lines (+143/-61)
14 files modified
src/provisioningserver/boot/tests/test_tftppath.py (+5/-1)
src/provisioningserver/boot/tftppath.py (+1/-0)
src/provisioningserver/config.py (+1/-0)
src/provisioningserver/import_images/boot_image_mapping.py (+32/-15)
src/provisioningserver/import_images/download_descriptions.py (+17/-12)
src/provisioningserver/import_images/download_resources.py (+10/-6)
src/provisioningserver/import_images/helpers.py (+1/-0)
src/provisioningserver/import_images/testing/factory.py (+8/-0)
src/provisioningserver/import_images/tests/test_boot_image_mapping.py (+18/-7)
src/provisioningserver/import_images/tests/test_boot_resources.py (+15/-7)
src/provisioningserver/import_images/tests/test_download_descriptions.py (+25/-11)
src/provisioningserver/import_images/tests/test_download_resources.py (+1/-2)
src/provisioningserver/tests/test_config.py (+2/-0)
src/provisioningserver/utils/__init__.py (+7/-0)
To merge this branch: bzr merge lp:~blake-rouse/maas/simplestreams-os-field
Reviewer Review Type Date Requested Status
Jason Hobbs (community) Approve
Review via email: mp+231611@code.launchpad.net

Commit message

Added support for os field on product from simplestreams endpoint.

To post a comment you must log in.
Revision history for this message
Jason Hobbs (jason-hobbs) wrote :

Blake, just a few comments and questions below.

review: Needs Fixing
Revision history for this message
Blake Rouse (blake-rouse) :
Revision history for this message
Blake Rouse (blake-rouse) wrote :

Have updated the code. Could you give it another look.

Revision history for this message
Jason Hobbs (jason-hobbs) wrote :

Looks good, thanks.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/provisioningserver/boot/tests/test_tftppath.py'
2--- src/provisioningserver/boot/tests/test_tftppath.py 2014-08-13 21:49:35 +0000
3+++ src/provisioningserver/boot/tests/test_tftppath.py 2014-08-22 21:52:07 +0000
4@@ -122,6 +122,7 @@
5
6 def make_meta_file(self, image_params, image_resource, tftproot):
7 image = ImageSpec(
8+ os=image_params["osystem"],
9 arch=image_params["architecture"],
10 subarch=image_params["subarchitecture"],
11 release=image_params["release"], label=image_params["label"])
12@@ -349,6 +350,7 @@
13 # and subarch vs subarchitecture means I can't just do a simple
14 # dict parameter expansion here.
15 params = {
16+ "osystem": image.os,
17 "architecture": image.arch,
18 "subarchitecture": image.subarch,
19 "release": image.release,
20@@ -372,6 +374,7 @@
21 # and subarch vs subarchitecture means I can't just do a simple
22 # dict parameter expansion here.
23 params = {
24+ "osystem": image.os,
25 "architecture": image.arch,
26 "subarchitecture": image.subarch,
27 "release": image.release,
28@@ -459,7 +462,8 @@
29
30 # Create some maas.meta content.
31 image = ImageSpec(
32- arch=arch, subarch=subarch, release=release, label=label)
33+ os=osystem, arch=arch, subarch=subarch, release=release,
34+ label=label)
35 image_resource = dict(subarches=factory.make_name("subarches"))
36 mapping = BootImageMapping()
37 mapping.setdefault(image, image_resource)
38
39=== modified file 'src/provisioningserver/boot/tftppath.py'
40--- src/provisioningserver/boot/tftppath.py 2014-08-13 21:49:35 +0000
41+++ src/provisioningserver/boot/tftppath.py 2014-08-22 21:52:07 +0000
42@@ -132,6 +132,7 @@
43 mapping = BootImageMapping.load_json(metadata)
44
45 image = ImageSpec(
46+ os=params["osystem"],
47 arch=params["architecture"],
48 subarch=params["subarchitecture"],
49 release=params["release"],
50
51=== modified file 'src/provisioningserver/config.py'
52--- src/provisioningserver/config.py 2014-08-13 21:49:35 +0000
53+++ src/provisioningserver/config.py 2014-08-22 21:52:07 +0000
54@@ -174,6 +174,7 @@
55
56 if_key_missing = None
57
58+ os = String(if_missing="*")
59 release = String(if_missing="*")
60 arches = Set(if_missing=["*"])
61 subarches = Set(if_missing=['*'])
62
63=== modified file 'src/provisioningserver/import_images/boot_image_mapping.py'
64--- src/provisioningserver/import_images/boot_image_mapping.py 2014-05-19 05:27:29 +0000
65+++ src/provisioningserver/import_images/boot_image_mapping.py 2014-08-22 21:52:07 +0000
66@@ -19,6 +19,20 @@
67 import json
68
69 from provisioningserver.import_images.helpers import ImageSpec
70+from provisioningserver.utils import dict_depth
71+
72+
73+def gen_image_spec_with_resource(os, data):
74+ """Generate image and resource for given operating system and data."""
75+ for arch in data:
76+ for subarch in data[arch]:
77+ for release in data[arch][subarch]:
78+ for label in data[arch][subarch][release]:
79+ image = ImageSpec(
80+ os=os, arch=arch, subarch=subarch,
81+ release=release, label=label)
82+ resource = data[arch][subarch][release][label]
83+ yield image, resource
84
85
86 class BootImageMapping:
87@@ -57,11 +71,12 @@
88 # Keep that format.
89 data = {}
90 for image, resource in self.items():
91- arch, subarch, release, label = image
92- data.setdefault(arch, {})
93- data[arch].setdefault(subarch, {})
94- data[arch][subarch].setdefault(release, {})
95- data[arch][subarch][release][label] = resource
96+ os, arch, subarch, release, label = image
97+ data.setdefault(os, {})
98+ data[os].setdefault(arch, {})
99+ data[os][arch].setdefault(subarch, {})
100+ data[os][arch][subarch].setdefault(release, {})
101+ data[os][arch][subarch][release][label] = resource
102 return json.dumps(data, sort_keys=True)
103
104 @staticmethod
105@@ -79,14 +94,16 @@
106 except ValueError:
107 return mapping
108
109- for arch in data:
110- for subarch in data[arch]:
111- for release in data[arch][subarch]:
112- for label in data[arch][subarch][release]:
113- image = ImageSpec(
114- arch=arch, subarch=subarch, release=release,
115- label=label)
116- resource = data[arch][subarch][release][label]
117- mapping.setdefault(image, resource)
118-
119+ depth = dict_depth(data)
120+ if depth == 5:
121+ # Support for older data. This has no operating system, then
122+ # it is ubuntu.
123+ for image, resource in gen_image_spec_with_resource(
124+ "ubuntu", data):
125+ mapping.setdefault(image, resource)
126+ elif depth == 6:
127+ for os in data:
128+ for image, resource in gen_image_spec_with_resource(
129+ os, data[os]):
130+ mapping.setdefault(image, resource)
131 return mapping
132
133=== modified file 'src/provisioningserver/import_images/download_descriptions.py'
134--- src/provisioningserver/import_images/download_descriptions.py 2014-08-13 21:49:35 +0000
135+++ src/provisioningserver/import_images/download_descriptions.py 2014-08-22 21:52:07 +0000
136@@ -26,6 +26,7 @@
137 BootImageMapping,
138 )
139 from provisioningserver.import_images.helpers import (
140+ get_os_from_product,
141 get_signing_policy,
142 ImageSpec,
143 maaslog,
144@@ -77,10 +78,11 @@
145 def insert_item(self, data, src, target, pedigree, contentsource):
146 """Overridable from `BasicMirrorWriter`."""
147 item = products_exdata(src, pedigree)
148+ os = get_os_from_product(item)
149 arch, subarches = item['arch'], item['subarches']
150 release = item['release']
151 label = item['label']
152- base_image = ImageSpec(arch, None, release, label)
153+ base_image = ImageSpec(os, arch, None, release, label)
154 compact_item = clean_up_repo_item(item)
155 for subarch in subarches.split(','):
156 self.boot_images_dict.setdefault(
157@@ -105,13 +107,14 @@
158 return filter_value in ('*', property_value)
159
160
161-def image_passes_filter(filters, arch, subarch, release, label):
162+def image_passes_filter(filters, os, arch, subarch, release, label):
163 """Filter a boot image against configured import filters.
164
165 :param filters: A list of dicts describing the filters, as in `boot_merge`.
166 If the list is empty, or `None`, any image matches. Any entry in a
167 filter may be a string containing just an asterisk (`*`) to denote that
168 the entry will match any value.
169+ :param os: The given boot image's operating system.
170 :param arch: The given boot image's architecture.
171 :param subarch: The given boot image's subarchitecture.
172 :param release: The given boot image's OS release.
173@@ -122,6 +125,7 @@
174 return True
175 for filter_dict in filters:
176 item_matches = (
177+ value_passes_filter(filter_dict['os'], os) and
178 value_passes_filter(filter_dict['release'], release) and
179 value_passes_filter_list(filter_dict['arches'], arch) and
180 value_passes_filter_list(filter_dict['subarches'], subarch) and
181@@ -143,21 +147,22 @@
182 in-place.
183 :param additions: A second `BootImageMapping`, which will be used as a
184 source of additional entries.
185- :param filters: List of dicts, each of which contains 'arch', 'subarch',
186- and 'release' keys. If given, entries are only considered for copying
187- from `additions` to `destination` if they match at least one of the
188- filters. Entries in the filter may be the string `*` (or for entries
189- that are lists, may contain the string `*`) to make them match any
190- value.
191+ :param filters: List of dicts, each of which contains 'os', arch',
192+ 'subarch', 'release', and 'label' keys. If given, entries are only
193+ considered for copying from `additions` to `destination` if they match
194+ at least one of the filters. Entries in the filter may be the string
195+ `*` (or for entries that are lists, may contain the string `*`) to make
196+ them match any value.
197 """
198 for image, resource in additions.items():
199- arch, subarch, release, label = image
200- if image_passes_filter(filters, arch, subarch, release, label):
201+ os, arch, subarch, release, label = image
202+ if image_passes_filter(
203+ filters, os, arch, subarch, release, label):
204 maaslog.debug(
205 "Merging boot resource for %s/%s/%s/%s.",
206 arch, subarch, release, label)
207 # Do not override an existing entry with the same
208- # arch/subarch/release/label: the first entry found takes
209+ # os/arch/subarch/release/label: the first entry found takes
210 # precedence.
211 destination.setdefault(image, resource)
212
213@@ -188,6 +193,6 @@
214 boot = BootImageMapping()
215 for source in sources:
216 repo_boot = download_image_descriptions(
217- source['url'], keyring=source['keyring'])
218+ source['url'], keyring=source.get('keyring', None))
219 boot_merge(boot, repo_boot, source['selections'])
220 return boot
221
222=== modified file 'src/provisioningserver/import_images/download_resources.py'
223--- src/provisioningserver/import_images/download_resources.py 2014-08-13 21:49:35 +0000
224+++ src/provisioningserver/import_images/download_resources.py 2014-08-22 21:52:07 +0000
225@@ -21,6 +21,7 @@
226 import os.path
227
228 from provisioningserver.import_images.helpers import (
229+ get_os_from_product,
230 get_signing_policy,
231 maaslog,
232 )
233@@ -121,7 +122,8 @@
234 return [(root_image_path, 'root-image'), (root_tgz_path, 'root-tgz')]
235
236
237-def link_resources(snapshot_path, links, arch, release, label, subarches):
238+def link_resources(snapshot_path, links, osystem, arch, release, label,
239+ subarches):
240 """Hardlink entries in the snapshot directory to resources in the cache.
241
242 This creates file entries in the snapshot directory for boot resources
243@@ -132,6 +134,7 @@
244 the cache. Each link is described as a tuple of (path, logical
245 name). The path points to a file in the cache directory. The logical
246 name will be link's filename, without path.
247+ :param osystem: Operating system with this boot image supports.
248 :param arch: Architecture which this boot image supports.
249 :param release: OS release of which this boot image is a part.
250 :param label: OS release label of which this boot image is a part, e.g.
251@@ -143,7 +146,8 @@
252 for older Ubuntu releases.
253 """
254 for subarch in subarches:
255- directory = os.path.join(snapshot_path, arch, subarch, release, label)
256+ directory = os.path.join(
257+ snapshot_path, osystem, arch, subarch, release, label)
258 if not os.path.exists(directory):
259 os.makedirs(directory)
260 for cached_file, logical_name in links:
261@@ -194,11 +198,12 @@
262 links = insert_file(
263 self.store, ftype, tag, checksums, size, contentsource)
264
265+ os = get_os_from_product(item)
266 subarches = self.product_mapping.get(item)
267 link_resources(
268 snapshot_path=self.root_path, links=links,
269- arch=item['arch'], release=item['release'], label=item['label'],
270- subarches=subarches)
271+ osystem=os, arch=item['arch'], release=item['release'],
272+ label=item['label'], subarches=subarches)
273
274
275 def download_boot_resources(path, store, snapshot_path, product_mapping,
276@@ -262,7 +267,6 @@
277 """
278 storage_path = os.path.abspath(storage_path)
279 snapshot_path = compose_snapshot_path(storage_path)
280- ubuntu_path = os.path.join(snapshot_path, 'ubuntu')
281 # Use a FileStore as our ObjectStore implementation. It will write to the
282 # cache directory.
283 if store is None:
284@@ -273,7 +277,7 @@
285
286 for source in sources:
287 download_boot_resources(
288- source['url'], store, ubuntu_path, product_mapping,
289+ source['url'], store, snapshot_path, product_mapping,
290 keyring_file=source.get('keyring')),
291
292 return snapshot_path
293
294=== modified file 'src/provisioningserver/import_images/helpers.py'
295--- src/provisioningserver/import_images/helpers.py 2014-08-15 04:03:34 +0000
296+++ src/provisioningserver/import_images/helpers.py 2014-08-22 21:52:07 +0000
297@@ -27,6 +27,7 @@
298
299 # A tuple of the items that together select a boot image.
300 ImageSpec = namedtuple(b'ImageSpec', [
301+ 'os',
302 'arch',
303 'subarch',
304 'release',
305
306=== modified file 'src/provisioningserver/import_images/testing/factory.py'
307--- src/provisioningserver/import_images/testing/factory.py 2014-05-19 02:45:04 +0000
308+++ src/provisioningserver/import_images/testing/factory.py 2014-08-22 21:52:07 +0000
309@@ -16,6 +16,7 @@
310 'make_boot_resource',
311 'make_image_spec',
312 'make_maas_meta',
313+ 'make_maas_meta_without_os',
314 'set_resource',
315 ]
316
317@@ -31,6 +32,12 @@
318 def make_maas_meta():
319 """Return fake maas.meta data."""
320 return dedent("""\
321+ {"ubuntu": {"amd64": {"generic": {"precise": {"release": {"content_id": "com.ubuntu.maas:v2:download", "path": "precise/amd64/20140410/raring/generic/boot-kernel", "product_name": "com.ubuntu.maas:v2:boot:12.04:amd64:hwe-r", "subarches": "generic,hwe-p,hwe-q,hwe-r", "version_name": "20140410"}}, "trusty": {"release": {"content_id": "com.ubuntu.maas:v2:download", "path": "trusty/amd64/20140416.1/root-image.gz", "product_name": "com.ubuntu.maas:v2:boot:14.04:amd64:hwe-t", "subarches": "generic,hwe-p,hwe-q,hwe-r,hwe-s,hwe-t", "version_name": "20140416.1"}}}, "hwe-s": {"precise": {"release": {"content_id": "com.ubuntu.maas:v2:download", "path": "precise/amd64/20140410/saucy/generic/boot-kernel", "product_name": "com.ubuntu.maas:v2:boot:12.04:amd64:hwe-s", "subarches": "generic,hwe-p,hwe-q,hwe-r,hwe-s", "version_name": "20140410"}}}}}}""") # NOQA
322+
323+
324+def make_maas_meta_without_os():
325+ """Return fake maas.meta data, without the os field."""
326+ return dedent("""\
327 {"amd64": {"generic": {"precise": {"release": {"content_id": "com.ubuntu.maas:v2:download", "path": "precise/amd64/20140410/raring/generic/boot-kernel", "product_name": "com.ubuntu.maas:v2:boot:12.04:amd64:hwe-r", "subarches": "generic,hwe-p,hwe-q,hwe-r", "version_name": "20140410"}}, "trusty": {"release": {"content_id": "com.ubuntu.maas:v2:download", "path": "trusty/amd64/20140416.1/root-image.gz", "product_name": "com.ubuntu.maas:v2:boot:14.04:amd64:hwe-t", "subarches": "generic,hwe-p,hwe-q,hwe-r,hwe-s,hwe-t", "version_name": "20140416.1"}}}, "hwe-s": {"precise": {"release": {"content_id": "com.ubuntu.maas:v2:download", "path": "precise/amd64/20140410/saucy/generic/boot-kernel", "product_name": "com.ubuntu.maas:v2:boot:12.04:amd64:hwe-s", "subarches": "generic,hwe-p,hwe-q,hwe-r,hwe-s", "version_name": "20140410"}}}}}""") # NOQA
328
329
330@@ -46,6 +53,7 @@
331 def make_image_spec():
332 """Return an `ImageSpec` with random values."""
333 return ImageSpec(
334+ factory.make_name('os'),
335 factory.make_name('arch'),
336 factory.make_name('subarch'),
337 factory.make_name('release'),
338
339=== modified file 'src/provisioningserver/import_images/tests/test_boot_image_mapping.py'
340--- src/provisioningserver/import_images/tests/test_boot_image_mapping.py 2014-05-19 05:27:29 +0000
341+++ src/provisioningserver/import_images/tests/test_boot_image_mapping.py 2014-08-22 21:52:07 +0000
342@@ -24,6 +24,7 @@
343 from provisioningserver.import_images.testing.factory import (
344 make_image_spec,
345 make_maas_meta,
346+ make_maas_meta_without_os,
347 set_resource,
348 )
349
350@@ -78,9 +79,11 @@
351 image_dict = set_resource(image_spec=image, resource=resource)
352 self.assertEqual(
353 {
354- image.arch: {
355- image.subarch: {
356- image.release: {image.label: resource},
357+ image.os: {
358+ image.arch: {
359+ image.subarch: {
360+ image.release: {image.label: resource},
361+ },
362 },
363 },
364 },
365@@ -97,10 +100,12 @@
366 image_dict, image._replace(release=other_release), resource2)
367 self.assertEqual(
368 {
369- image.arch: {
370- image.subarch: {
371- image.release: {image.label: resource1},
372- other_release: {image.label: resource2},
373+ image.os: {
374+ image.arch: {
375+ image.subarch: {
376+ image.release: {image.label: resource1},
377+ other_release: {image.label: resource2},
378+ },
379 },
380 },
381 },
382@@ -114,6 +119,12 @@
383 dumped = mapping.dump_json()
384 self.assertEqual(test_meta_file_content, dumped)
385
386+ def test_load_json_result_of_old_data_uses_ubuntu_as_os(self):
387+ test_meta_file_content = make_maas_meta_without_os()
388+ mapping = BootImageMapping.load_json(test_meta_file_content)
389+ os = {image.os for image, _ in mapping.items()}.pop()
390+ self.assertEqual('ubuntu', os)
391+
392 def test_load_json_returns_empty_mapping_for_invalid_json(self):
393 bad_json = ""
394 mapping = BootImageMapping.load_json(bad_json)
395
396=== modified file 'src/provisioningserver/import_images/tests/test_boot_resources.py'
397--- src/provisioningserver/import_images/tests/test_boot_resources.py 2014-08-13 21:49:35 +0000
398+++ src/provisioningserver/import_images/tests/test_boot_resources.py 2014-08-22 21:52:07 +0000
399@@ -114,7 +114,8 @@
400 self.patch(
401 provisioningserver.config, 'BOOT_RESOURCES_STORAGE', self.storage)
402 self.image = make_image_spec()
403- self.arch, self.subarch, self.release, self.label = self.image
404+ self.os, self.arch, self.subarch, \
405+ self.release, self.label = self.image
406 self.repo = self.make_simplestreams_repo(self.image)
407
408 def patch_maaslog(self):
409@@ -212,6 +213,7 @@
410 'subarches': [image_spec.subarch],
411 'release': image_spec.release,
412 'arch': image_spec.arch,
413+ 'os': image_spec.os,
414 },
415 },
416 }
417@@ -255,6 +257,7 @@
418 'url': self.repo,
419 'selections': [
420 {
421+ 'os': self.os,
422 'release': self.release,
423 'arches': [self.arch],
424 'subarches': [self.subarch],
425@@ -287,6 +290,7 @@
426 self.patch(boot_method, 'install_bootloader')
427
428 args = self.make_working_args()
429+ osystem = self.os
430 arch = self.arch
431 subarch = self.subarch
432 release = self.release
433@@ -305,16 +309,18 @@
434 self.assertThat(os.path.join(current, 'maas.tgt'), FileExists())
435 self.assertThat(
436 os.path.join(
437- current, 'ubuntu', arch, subarch, self.release, self.label),
438+ current, osystem, arch, subarch, self.release, self.label),
439 DirExists())
440
441 # Verify the contents of the "meta" file.
442 with open(os.path.join(current, 'maas.meta'), 'rb') as meta_file:
443 meta_data = json.load(meta_file)
444- self.assertEqual([arch], meta_data.keys())
445- self.assertEqual([subarch], meta_data[arch].keys())
446- self.assertEqual([release], meta_data[arch][subarch].keys())
447- self.assertEqual([label], meta_data[arch][subarch][release].keys())
448+ self.assertEqual([osystem], meta_data.keys())
449+ self.assertEqual([arch], meta_data[osystem].keys())
450+ self.assertEqual([subarch], meta_data[osystem][arch].keys())
451+ self.assertEqual([release], meta_data[osystem][arch][subarch].keys())
452+ self.assertEqual(
453+ [label], meta_data[osystem][arch][subarch][release].keys())
454 self.assertItemsEqual(
455 [
456 'content_id',
457@@ -323,7 +329,7 @@
458 'version_name',
459 'subarches',
460 ],
461- meta_data[arch][subarch][release][label].keys())
462+ meta_data[osystem][arch][subarch][release][label].keys())
463
464 def test_warns_if_no_sources_selected(self):
465 self.patch_maaslog()
466@@ -442,6 +448,7 @@
467 'url': factory.make_name("something"),
468 'selections': [
469 {
470+ 'os': factory.make_name("os"),
471 'release': factory.make_name("release"),
472 'arches': [factory.make_name("arch")],
473 'subarches': [factory.make_name("subarch")],
474@@ -477,6 +484,7 @@
475 'url': factory.make_name("something"),
476 'selections': [
477 {
478+ 'os': factory.make_name("os"),
479 'release': factory.make_name("release"),
480 'arches': [factory.make_name("arch")],
481 'subarches': [factory.make_name("subarch")],
482
483=== modified file 'src/provisioningserver/import_images/tests/test_download_descriptions.py'
484--- src/provisioningserver/import_images/tests/test_download_descriptions.py 2014-08-13 21:49:35 +0000
485+++ src/provisioningserver/import_images/tests/test_download_descriptions.py 2014-08-22 21:52:07 +0000
486@@ -92,6 +92,7 @@
487 if image_spec is None:
488 image_spec = make_image_spec()
489 return {
490+ 'os': image_spec.os,
491 'arches': [image_spec.arch],
492 'subarches': [image_spec.subarch],
493 'release': image_spec.release,
494@@ -99,30 +100,32 @@
495 }
496
497 def test_any_image_passes_none_filter(self):
498- arch, subarch, release, label = make_image_spec()
499+ os, arch, subarch, release, label = make_image_spec()
500 self.assertTrue(
501 download_descriptions.image_passes_filter(
502- None, arch, subarch, release, label))
503+ None, os, arch, subarch, release, label))
504
505 def test_any_image_passes_empty_filter(self):
506- arch, subarch, release, label = make_image_spec()
507+ os, arch, subarch, release, label = make_image_spec()
508 self.assertTrue(
509 download_descriptions.image_passes_filter(
510- [], arch, subarch, release, label))
511+ [], os, arch, subarch, release, label))
512
513 def test_image_passes_matching_filter(self):
514 image = make_image_spec()
515 self.assertTrue(
516 download_descriptions.image_passes_filter(
517 [self.make_filter_from_image(image)],
518- image.arch, image.subarch, image.release, image.label))
519+ image.os, image.arch, image.subarch,
520+ image.release, image.label))
521
522 def test_image_does_not_pass_nonmatching_filter(self):
523 image = make_image_spec()
524 self.assertFalse(
525 download_descriptions.image_passes_filter(
526 [self.make_filter_from_image()],
527- image.arch, image.subarch, image.release, image.label))
528+ image.os, image.arch, image.subarch,
529+ image.release, image.label))
530
531 def test_image_passes_if_one_filter_matches(self):
532 image = make_image_spec()
533@@ -132,7 +135,9 @@
534 self.make_filter_from_image(),
535 self.make_filter_from_image(image),
536 self.make_filter_from_image(),
537- ], image.arch, image.subarch, image.release, image.label))
538+ ],
539+ image.os, image.arch, image.subarch,
540+ image.release, image.label))
541
542 def test_filter_checks_release(self):
543 image = make_image_spec()
544@@ -141,7 +146,9 @@
545 [
546 self.make_filter_from_image(image._replace(
547 release=factory.make_name('other-release')))
548- ], image.arch, image.subarch, image.release, image.label))
549+ ],
550+ image.os, image.arch, image.subarch,
551+ image.release, image.label))
552
553 def test_filter_checks_arches(self):
554 image = make_image_spec()
555@@ -150,7 +157,9 @@
556 [
557 self.make_filter_from_image(image._replace(
558 arch=factory.make_name('other-arch')))
559- ], image.arch, image.subarch, image.release, image.label))
560+ ],
561+ image.os, image.arch, image.subarch,
562+ image.release, image.label))
563
564 def test_filter_checks_subarches(self):
565 image = make_image_spec()
566@@ -159,7 +168,9 @@
567 [
568 self.make_filter_from_image(image._replace(
569 subarch=factory.make_name('other-subarch')))
570- ], image.arch, image.subarch, image.release, image.label))
571+ ],
572+ image.os, image.arch, image.subarch,
573+ image.release, image.label))
574
575 def test_filter_checks_labels(self):
576 image = make_image_spec()
577@@ -168,7 +179,9 @@
578 [
579 self.make_filter_from_image(image._replace(
580 label=factory.make_name('other-label')))
581- ], image.arch, image.subarch, image.release, image.label))
582+ ],
583+ image.os, image.arch, image.subarch,
584+ image.release, image.label))
585
586
587 class TestBootMerge(MAASTestCase):
588@@ -187,6 +200,7 @@
589 def test_obeys_filters(self):
590 filters = [
591 {
592+ 'os': factory.make_name('os'),
593 'arches': [factory.make_name('other-arch')],
594 'subarches': [factory.make_name('other-subarch')],
595 'release': factory.make_name('other-release'),
596
597=== modified file 'src/provisioningserver/import_images/tests/test_download_resources.py'
598--- src/provisioningserver/import_images/tests/test_download_resources.py 2014-06-10 14:45:14 +0000
599+++ src/provisioningserver/import_images/tests/test_download_resources.py 2014-08-22 21:52:07 +0000
600@@ -55,7 +55,6 @@
601 storage_path = self.make_dir()
602 snapshot_path = download_resources.compose_snapshot_path(
603 storage_path)
604- ubuntu_path = os.path.join(snapshot_path, 'ubuntu')
605 cache_path = os.path.join(storage_path, 'cache')
606 file_store = FileStore(cache_path)
607 source = {
608@@ -70,7 +69,7 @@
609 self.assertThat(
610 fake,
611 MockCalledWith(
612- source['url'], file_store, ubuntu_path, product_mapping,
613+ source['url'], file_store, snapshot_path, product_mapping,
614 keyring_file=source['keyring']))
615
616
617
618=== modified file 'src/provisioningserver/tests/test_config.py'
619--- src/provisioningserver/tests/test_config.py 2014-07-18 17:05:57 +0000
620+++ src/provisioningserver/tests/test_config.py 2014-08-22 21:52:07 +0000
621@@ -437,6 +437,7 @@
622 'keyring_data': None,
623 'selections': [
624 {
625+ 'os': '*',
626 'release': '*',
627 'labels': ['*'],
628 'arches': ['*'],
629@@ -452,6 +453,7 @@
630 'keyring': factory.make_name('keyring'),
631 'keyring_data': factory.make_string(),
632 'selections': [{
633+ 'os': factory.make_name('os'),
634 'release': factory.make_name('release'),
635 'labels': [factory.make_name('label')],
636 'arches': [factory.make_name('arch')],
637
638=== modified file 'src/provisioningserver/utils/__init__.py'
639--- src/provisioningserver/utils/__init__.py 2014-08-13 21:49:35 +0000
640+++ src/provisioningserver/utils/__init__.py 2014-08-22 21:52:07 +0000
641@@ -143,6 +143,13 @@
642 }
643
644
645+def dict_depth(d, depth=0):
646+ """Returns the max depth of a dictionary."""
647+ if not isinstance(d, dict) or not d:
648+ return depth
649+ return max(dict_depth(v, depth + 1) for _, v in d.iteritems())
650+
651+
652 def split_lines(input, separator):
653 """Split each item from `input` into a key/value pair."""
654 return (line.split(separator, 1) for line in input if line.strip() != '')