Merge lp:~blake-rouse/maas/osystem-registry into lp:~maas-committers/maas/trunk
- osystem-registry
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Blake Rouse | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 2314 | ||||
Proposed branch: | lp:~blake-rouse/maas/osystem-registry | ||||
Merge into: | lp:~maas-committers/maas/trunk | ||||
Prerequisite: | lp:~blake-rouse/maas/add-osystem-to-bootimage | ||||
Diff against target: |
783 lines (+472/-20) 14 files modified
src/maasserver/testing/factory.py (+19/-0) src/maasserver/testing/osystems.py (+90/-0) src/maasserver/views/tests/test_boot_image_list.py (+18/-4) src/maasserver/views/tests/test_clusters.py (+10/-2) src/provisioningserver/boot/__init__.py (+1/-1) src/provisioningserver/boot/tests/test_pxe.py (+5/-3) src/provisioningserver/boot/tests/test_tftppath.py (+71/-1) src/provisioningserver/boot/tests/test_uefi.py (+1/-1) src/provisioningserver/boot/tftppath.py (+9/-8) src/provisioningserver/driver/__init__.py (+73/-0) src/provisioningserver/driver/os_ubuntu.py (+89/-0) src/provisioningserver/driver/tests/test_os_ubuntu.py (+76/-0) src/provisioningserver/driver/tests/test_registries.py (+8/-0) src/provisioningserver/rpc/tests/test_clusterservice.py (+2/-0) |
||||
To merge this branch: | bzr merge lp:~blake-rouse/maas/osystem-registry | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Julian Edwards (community) | Approve | ||
Review via email: mp+217057@code.launchpad.net |
Commit message
Added OperatingSystem, OperatingSystem
Description of the change
This is the second change in the series of changes to all MAAS to deploy other operating systems. As we have Windows and CentOS support coming soon this is needed to easily add new operating systems.
Adds an OperatingSystem class that each supported operating system extends, to provide information about that operating system. Each OperatingSystem class gets registered in the OperatingSystem
Added the UbuntuOS class, that provides the information for Ubuntu. Currently this class hard codes the releases that it supports and its purposes for each, but it could pull this data from simplestreams allowing new releases without modifying the code base.
Note: osystem was used throughout the code instead of os, as the python os module would conflict throughout the code base.
Changes were coordinated with allenap.
Jeroen T. Vermeulen (jtv) wrote : | # |
One question, which came up with other registries as well: does OperatingSystem really need a class hierarchy? If a simple class with plain objects will do, prefer simplicity.
The RPC API is not meant to be public or backwards-
Jeroen T. Vermeulen (jtv) wrote : | # |
By the way, dromedary-case method names like getRandomOS are sort of a legacy thing in MAAS. We would now call that make_OS.
Blake Rouse (blake-rouse) wrote : | # |
Fixed 1,2,3,6,8,10,13,14.
[4] Spoke with Gavin and Andres. Keeping it as is for now as this will be backported into 1.5 for SRU. Will do another branch for trunk that will use RPC, and remove importing from pserv.
[5] Removed title, going back the previous way. Looks just as good, no need to import from pserv.
[7] Leaving as is for now, will move in later branch.
[11,12] Moved the OperatingSystem and OperatingSystem
Julian Edwards (julian-edwards) wrote : | # |
Thanks for all the fixes, it's looking much better. I'll mark it approved with some conditions as below as I know you're trying to get all this done for ODS.
I am not comfortable with leaving [4] only in trunk because there is always the risk that future SRUs/MREs will get rejected and then an LTS is stuck with code that won't work as required. However I think the risk is minimal here. Having said that, please do the RPC stuff in trunk as the simplest change that you can do and then it should backport cleanly to 1.5 as and when required.
For every fix that is being deferred, please file a bug with the description, tag it "tech-debt" and then refer to it with an XXX in the code like this:
# XXX 2014-05-05 blake-rouse bug=NNNNNNN
# Doing >blah< later.
So hopefully that means things won't get forgotten.
You didn't mention [9] in your response, are you willing to do that now or defer to a later branch? I think it's worth doing, the code will be much cleaner.
Blake Rouse (blake-rouse) wrote : | # |
[4] Add bug comment to fix later.
[9] I will also fix in a later branch.
Preview Diff
1 | === modified file 'src/maasserver/testing/factory.py' |
2 | --- src/maasserver/testing/factory.py 2014-05-13 18:43:40 +0000 |
3 | +++ src/maasserver/testing/factory.py 2014-05-13 18:46:09 +0000 |
4 | @@ -60,6 +60,10 @@ |
5 | ) |
6 | from netaddr import IPAddress |
7 | |
8 | +# XXX 2014-05-13 blake-rouse bug=1319143 |
9 | +# Need to not import directly, use RPC to info from cluster. |
10 | +from provisioningserver.driver import OperatingSystemRegistry |
11 | + |
12 | # We have a limited number of public keys: |
13 | # src/maasserver/tests/data/test_rsa{0, 1, 2, 3, 4}.pub |
14 | MAX_PUBLIC_KEYS = 5 |
15 | @@ -140,6 +144,21 @@ |
16 | [choice for choice in list(get_power_types().keys()) |
17 | if choice not in but_not]) |
18 | |
19 | + def getRandomOS(self): |
20 | + """Pick a random operating system from the registry.""" |
21 | + osystems = [obj for _, obj in OperatingSystemRegistry] |
22 | + return random.choice(osystems) |
23 | + |
24 | + def getRandomRelease(self, osystem): |
25 | + """Pick a random release from operating system.""" |
26 | + releases = osystem.get_supported_releases() |
27 | + return random.choice(releases) |
28 | + |
29 | + def getRandomCommissioningRelease(self, osystem): |
30 | + """Pick a random commissioning release from operating system.""" |
31 | + releases = osystem.get_supported_commissioning_releases() |
32 | + return random.choice(releases) |
33 | + |
34 | def _save_node_unchecked(self, node): |
35 | """Save a :class:`Node`, but circumvent status transition checks.""" |
36 | valid_initial_states = NODE_TRANSITIONS[None] |
37 | |
38 | === added file 'src/maasserver/testing/osystems.py' |
39 | --- src/maasserver/testing/osystems.py 1970-01-01 00:00:00 +0000 |
40 | +++ src/maasserver/testing/osystems.py 2014-05-13 18:46:09 +0000 |
41 | @@ -0,0 +1,90 @@ |
42 | +# Copyright 2014 Canonical Ltd. This software is licensed under the |
43 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
44 | + |
45 | +"""Helpers for operating systems in testing.""" |
46 | + |
47 | +from __future__ import ( |
48 | + absolute_import, |
49 | + print_function, |
50 | + unicode_literals, |
51 | + ) |
52 | + |
53 | +str = None |
54 | + |
55 | +__metaclass__ = type |
56 | +__all__ = [ |
57 | + 'make_usable_osystem', |
58 | + 'patch_usable_osystems', |
59 | + ] |
60 | + |
61 | +from random import randint |
62 | + |
63 | +from maasserver import forms |
64 | +from maasserver.testing.factory import factory |
65 | +from provisioningserver.driver import BOOT_IMAGE_PURPOSE |
66 | +from provisioningserver.boot.tests.test_tftppath import make_osystem |
67 | + |
68 | + |
69 | +def make_osystem_with_releases(testcase, osystem_name=None, releases=None): |
70 | + """Generate an arbitrary operating system. |
71 | + |
72 | + :param osystem_name: The operating system name. Useful in cases where |
73 | + we need to test that not supplying an os works correctly. |
74 | + :param releases: The list of releases name. Useful in cases where |
75 | + we need to test that not supplying a release works correctly. |
76 | + """ |
77 | + if osystem_name is None: |
78 | + osystem_name = factory.make_name('os') |
79 | + if releases is None: |
80 | + releases = [factory.make_name('release') for _ in range(3)] |
81 | + |
82 | + osystem = make_osystem( |
83 | + testcase, |
84 | + osystem_name, |
85 | + [BOOT_IMAGE_PURPOSE.INSTALL, BOOT_IMAGE_PURPOSE.XINSTALL]) |
86 | + if releases is not None and releases != []: |
87 | + osystem.fake_list = releases |
88 | + return osystem |
89 | + |
90 | + |
91 | +def patch_usable_osystems(testcase, osystems=None, allow_empty=True): |
92 | + """Set a fixed list of usable operating systems. |
93 | + |
94 | + A usable operating system is one for which boot images are available. |
95 | + |
96 | + :param testcase: A `TestCase` whose `patch` this function can use. |
97 | + :param osystems: Optional list of operating systems. If omitted, |
98 | + defaults to a list (which may be empty) of random operating systems. |
99 | + """ |
100 | + start = 0 |
101 | + if allow_empty is False: |
102 | + start = 1 |
103 | + if osystems is None: |
104 | + osystems = [ |
105 | + make_osystem_with_releases(testcase) |
106 | + for _ in range(randint(start, 2)) |
107 | + ] |
108 | + distro_series = {} |
109 | + for osystem in osystems: |
110 | + distro_series[osystem.name] = osystem.get_supported_releases() |
111 | + testcase.patch(forms, 'list_all_usable_osystems').return_value = osystems |
112 | + testcase.patch( |
113 | + forms, 'list_all_usable_releases').return_value = distro_series |
114 | + |
115 | + |
116 | +def make_usable_osystem(testcase, osystem_name=None, releases=None): |
117 | + """Return arbitrary operating system, and make it "usable." |
118 | + |
119 | + A usable operating system is one for which boot images are available. |
120 | + |
121 | + :param testcase: A `TestCase` whose `patch` this function can pass to |
122 | + `patch_usable_osystems`. |
123 | + :param osystem_name: The operating system name. Useful in cases where |
124 | + we need to test that not supplying an os works correctly. |
125 | + :param releases: The list of releases name. Useful in cases where |
126 | + we need to test that not supplying a release works correctly. |
127 | + """ |
128 | + osystem = make_osystem_with_releases( |
129 | + testcase, osystem_name=osystem_name, releases=releases) |
130 | + patch_usable_osystems(testcase, [osystem]) |
131 | + return osystem |
132 | |
133 | === modified file 'src/maasserver/views/tests/test_boot_image_list.py' |
134 | --- src/maasserver/views/tests/test_boot_image_list.py 2014-04-24 13:52:17 +0000 |
135 | +++ src/maasserver/views/tests/test_boot_image_list.py 2014-05-13 18:46:09 +0000 |
136 | @@ -22,16 +22,22 @@ |
137 | from maasserver.testing.factory import factory |
138 | from maasserver.testing.testcase import MAASServerTestCase |
139 | from maasserver.views.clusters import BootImagesListView |
140 | +from provisioningserver.boot.tests.test_tftppath import make_osystem |
141 | from testtools.matchers import ContainsAll |
142 | |
143 | |
144 | class BootImageListTest(MAASServerTestCase): |
145 | |
146 | + def setUp(self): |
147 | + super(BootImageListTest, self).setUp() |
148 | + |
149 | def test_contains_boot_image_list(self): |
150 | self.client_log_in(as_admin=True) |
151 | nodegroup = factory.make_node_group() |
152 | - images = [ |
153 | + boot_images = [ |
154 | factory.make_boot_image(nodegroup=nodegroup) for _ in range(3)] |
155 | + for bi in boot_images: |
156 | + make_osystem(self, bi.osystem, ['install']) |
157 | response = self.client.get( |
158 | reverse('cluster-bootimages-list', args=[nodegroup.uuid])) |
159 | self.assertEqual( |
160 | @@ -44,8 +50,9 @@ |
161 | image.release, |
162 | image.subarchitecture, |
163 | image.architecture, |
164 | + image.osystem, |
165 | '%s' % image.updated.year, |
166 | - ] for image in images] |
167 | + ] for image in boot_images] |
168 | self.assertThat( |
169 | response.content, ContainsAll(itertools.chain(*items_in_page))) |
170 | |
171 | @@ -54,7 +61,12 @@ |
172 | self.client_log_in(as_admin=True) |
173 | nodegroup = factory.make_node_group() |
174 | # Create 4 images. |
175 | - [factory.make_boot_image(nodegroup=nodegroup) for _ in range(4)] |
176 | + boot_images = [ |
177 | + factory.make_boot_image(nodegroup=nodegroup) |
178 | + for _ in range(4) |
179 | + ] |
180 | + for bi in boot_images: |
181 | + make_osystem(self, bi.osystem, ['install']) |
182 | response = self.client.get( |
183 | reverse('cluster-bootimages-list', args=[nodegroup.uuid])) |
184 | self.assertEqual(httplib.OK, response.status_code) |
185 | @@ -65,7 +77,9 @@ |
186 | |
187 | def test_displays_warning_if_boot_image_list_is_empty(self): |
188 | # Create boot images in another nodegroup. |
189 | - [factory.make_boot_image() for _ in range(3)] |
190 | + boot_images = [factory.make_boot_image() for _ in range(3)] |
191 | + for bi in boot_images: |
192 | + make_osystem(self, bi.osystem, ['install']) |
193 | self.client_log_in(as_admin=True) |
194 | nodegroup = factory.make_node_group() |
195 | response = self.client.get( |
196 | |
197 | === modified file 'src/maasserver/views/tests/test_clusters.py' |
198 | --- src/maasserver/views/tests/test_clusters.py 2014-04-24 13:52:17 +0000 |
199 | +++ src/maasserver/views/tests/test_clusters.py 2014-05-13 18:46:09 +0000 |
200 | @@ -41,6 +41,7 @@ |
201 | ANY, |
202 | call, |
203 | ) |
204 | +from provisioningserver.boot.tests.test_tftppath import make_osystem |
205 | from testtools.matchers import ( |
206 | AllMatch, |
207 | Contains, |
208 | @@ -309,7 +310,12 @@ |
209 | def test_contains_link_to_boot_image_list(self): |
210 | self.client_log_in(as_admin=True) |
211 | nodegroup = factory.make_node_group() |
212 | - [factory.make_boot_image(nodegroup=nodegroup) for _ in range(3)] |
213 | + boot_images = [ |
214 | + factory.make_boot_image(nodegroup=nodegroup) |
215 | + for _ in range(3) |
216 | + ] |
217 | + for bi in boot_images: |
218 | + make_osystem(self, bi.osystem, ['install']) |
219 | response = self.client.get( |
220 | reverse('cluster-edit', args=[nodegroup.uuid])) |
221 | self.assertEqual( |
222 | @@ -320,7 +326,9 @@ |
223 | |
224 | def test_displays_warning_if_boot_image_list_is_empty(self): |
225 | # Create boot images in another nodegroup. |
226 | - [factory.make_boot_image() for _ in range(3)] |
227 | + boot_images = [factory.make_boot_image() for _ in range(3)] |
228 | + for bi in boot_images: |
229 | + make_osystem(self, bi.osystem, ['install']) |
230 | self.client_log_in(as_admin=True) |
231 | nodegroup = factory.make_node_group() |
232 | response = self.client.get( |
233 | |
234 | === modified file 'src/provisioningserver/boot/__init__.py' |
235 | --- src/provisioningserver/boot/__init__.py 2014-04-23 16:09:39 +0000 |
236 | +++ src/provisioningserver/boot/__init__.py 2014-05-13 18:46:09 +0000 |
237 | @@ -168,7 +168,7 @@ |
238 | """ |
239 | def image_dir(params): |
240 | return compose_image_path( |
241 | - 'ubuntu', params.arch, params.subarch, |
242 | + params.osystem, params.arch, params.subarch, |
243 | params.release, params.label) |
244 | |
245 | def initrd_path(params): |
246 | |
247 | === modified file 'src/provisioningserver/boot/tests/test_pxe.py' |
248 | --- src/provisioningserver/boot/tests/test_pxe.py 2014-04-23 16:09:39 +0000 |
249 | +++ src/provisioningserver/boot/tests/test_pxe.py 2014-05-13 18:46:09 +0000 |
250 | @@ -163,7 +163,7 @@ |
251 | self.assertThat(output, StartsWith("DEFAULT ")) |
252 | # The PXE parameters are all set according to the options. |
253 | image_dir = compose_image_path( |
254 | - osystem='ubuntu', arch=params.arch, subarch=params.subarch, |
255 | + osystem=params.osystem, arch=params.arch, subarch=params.subarch, |
256 | release=params.release, label=params.label) |
257 | self.assertThat( |
258 | output, MatchesAll( |
259 | @@ -243,9 +243,11 @@ |
260 | method = PXEBootMethod() |
261 | get_ephemeral_name = self.patch(kernel_opts, "get_ephemeral_name") |
262 | get_ephemeral_name.return_value = factory.make_name("ephemeral") |
263 | + osystem = factory.make_name('osystem') |
264 | options = { |
265 | "kernel_params": make_kernel_parameters( |
266 | - testcase=self, subarch="generic", purpose=self.purpose), |
267 | + testcase=self, osystem=osystem, subarch="generic", |
268 | + purpose=self.purpose), |
269 | } |
270 | output = method.render_config(**options) |
271 | config = parse_pxe_config(output) |
272 | @@ -269,7 +271,7 @@ |
273 | self.assertThat( |
274 | section, ContainsAll(("KERNEL", "INITRD", "APPEND"))) |
275 | contains_arch_path = StartsWith( |
276 | - "ubuntu/%s/" % section_label) |
277 | + "%s/%s/" % (osystem, section_label)) |
278 | self.assertThat(section["KERNEL"], contains_arch_path) |
279 | self.assertThat(section["INITRD"], contains_arch_path) |
280 | self.assertIn("APPEND", section) |
281 | |
282 | === modified file 'src/provisioningserver/boot/tests/test_tftppath.py' |
283 | --- src/provisioningserver/boot/tests/test_tftppath.py 2014-04-23 16:09:39 +0000 |
284 | +++ src/provisioningserver/boot/tests/test_tftppath.py 2014-05-13 18:46:09 +0000 |
285 | @@ -30,6 +30,10 @@ |
286 | list_subdirs, |
287 | locate_tftp_path, |
288 | ) |
289 | +from provisioningserver.driver import ( |
290 | + OperatingSystem, |
291 | + OperatingSystemRegistry, |
292 | + ) |
293 | from provisioningserver.testing.boot_images import ( |
294 | make_boot_image_storage_params, |
295 | ) |
296 | @@ -41,6 +45,40 @@ |
297 | from testtools.testcase import ExpectedException |
298 | |
299 | |
300 | +class FakeOS(OperatingSystem): |
301 | + |
302 | + name = "" |
303 | + title = "" |
304 | + |
305 | + def __init__(self, name, purpose, releases=None): |
306 | + self.name = name |
307 | + self.title = name |
308 | + self.purpose = purpose |
309 | + if releases is None: |
310 | + self.fake_list = [ |
311 | + factory.getRandomString() |
312 | + for _ in range(3) |
313 | + ] |
314 | + else: |
315 | + self.fake_list = releases |
316 | + |
317 | + def get_boot_image_purposes(self, *args): |
318 | + return self.purpose |
319 | + |
320 | + def get_supported_releases(self): |
321 | + return self.fake_list |
322 | + |
323 | + def get_default_release(self): |
324 | + return self.fake_list[0] |
325 | + |
326 | + def format_release_choices(self, releases): |
327 | + return [ |
328 | + (release, release) |
329 | + for release in releases |
330 | + if release in self.fake_list |
331 | + ] |
332 | + |
333 | + |
334 | def make_image(params, purpose): |
335 | """Describe an image as a dict similar to what `list_boot_images` returns. |
336 | |
337 | @@ -51,6 +89,29 @@ |
338 | return image |
339 | |
340 | |
341 | +def make_osystem(testcase, osystem, purpose): |
342 | + """Makes the operating system class and registers it.""" |
343 | + if osystem not in OperatingSystemRegistry: |
344 | + fake = FakeOS(osystem, purpose) |
345 | + OperatingSystemRegistry.register_item(fake.name, fake) |
346 | + testcase.addCleanup( |
347 | + OperatingSystemRegistry.unregister_item, osystem) |
348 | + return fake |
349 | + |
350 | + else: |
351 | + |
352 | + obj = OperatingSystemRegistry[osystem] |
353 | + old_func = obj.get_boot_image_purposes |
354 | + testcase.patch(obj, 'get_boot_image_purposes').return_value = purpose |
355 | + |
356 | + def reset_func(obj, old_func): |
357 | + obj.get_boot_image_purposes = old_func |
358 | + |
359 | + testcase.addCleanup(reset_func, obj, old_func) |
360 | + |
361 | + return obj |
362 | + |
363 | + |
364 | class TestTFTPPath(MAASTestCase): |
365 | |
366 | def setUp(self): |
367 | @@ -123,22 +184,31 @@ |
368 | params = make_boot_image_storage_params() |
369 | self.make_image_dir(params, self.tftproot) |
370 | purposes = ['install', 'commissioning', 'xinstall'] |
371 | + make_osystem(self, params['osystem'], purposes) |
372 | self.assertItemsEqual( |
373 | [make_image(params, purpose) for purpose in purposes], |
374 | list_boot_images(self.tftproot)) |
375 | |
376 | def test_list_boot_images_enumerates_boot_images(self): |
377 | + purposes = ['install', 'commissioning', 'xinstall'] |
378 | params = [make_boot_image_storage_params() for counter in range(3)] |
379 | for param in params: |
380 | self.make_image_dir(param, self.tftproot) |
381 | + make_osystem(self, param['osystem'], purposes) |
382 | self.assertItemsEqual( |
383 | [ |
384 | make_image(param, purpose) |
385 | for param in params |
386 | - for purpose in ['install', 'commissioning', 'xinstall'] |
387 | + for purpose in purposes |
388 | ], |
389 | list_boot_images(self.tftproot)) |
390 | |
391 | + def test_list_boot_images_empty_on_missing_osystems(self): |
392 | + params = [make_boot_image_storage_params() for counter in range(3)] |
393 | + for param in params: |
394 | + self.make_image_dir(param, self.tftproot) |
395 | + self.assertItemsEqual([], list_boot_images(self.tftproot)) |
396 | + |
397 | def test_is_visible_subdir_ignores_regular_files(self): |
398 | plain_file = self.make_file() |
399 | self.assertFalse( |
400 | |
401 | === modified file 'src/provisioningserver/boot/tests/test_uefi.py' |
402 | --- src/provisioningserver/boot/tests/test_uefi.py 2014-04-23 16:09:39 +0000 |
403 | +++ src/provisioningserver/boot/tests/test_uefi.py 2014-05-13 18:46:09 +0000 |
404 | @@ -73,7 +73,7 @@ |
405 | self.assertThat(output, StartsWith("set default=\"0\"")) |
406 | # The UEFI parameters are all set according to the options. |
407 | image_dir = compose_image_path( |
408 | - osystem='ubuntu', arch=params.arch, subarch=params.subarch, |
409 | + osystem=params.osystem, arch=params.arch, subarch=params.subarch, |
410 | release=params.release, label=params.label) |
411 | |
412 | self.assertThat( |
413 | |
414 | === modified file 'src/provisioningserver/boot/tftppath.py' |
415 | --- src/provisioningserver/boot/tftppath.py 2014-04-23 16:09:39 +0000 |
416 | +++ src/provisioningserver/boot/tftppath.py 2014-05-13 18:46:09 +0000 |
417 | @@ -25,6 +25,7 @@ |
418 | from logging import getLogger |
419 | import os.path |
420 | |
421 | +from provisioningserver.driver import OperatingSystemRegistry |
422 | |
423 | logger = getLogger(__name__) |
424 | |
425 | @@ -114,16 +115,16 @@ |
426 | def extract_image_params(path): |
427 | """Represent a list of TFTP path elements as a list of boot-image dicts. |
428 | |
429 | - The path must consist of a full [architecture, subarchitecture, release] |
430 | - that identify a kind of boot that we may need an image for. |
431 | + The path must consist of a full [osystem, architecture, subarchitecture, |
432 | + release] that identify a kind of boot that we may need an image for. |
433 | """ |
434 | osystem, arch, subarch, release, label = path |
435 | - # XXX: rvb 2014-03-24: The images import script currently imports all the |
436 | - # images for the configured selections (where a selection is an |
437 | - # arch/subarch/series/label combination). When the import script grows the |
438 | - # ability to import the images for a particular purpose, we need to change |
439 | - # this code to report what is actually present. |
440 | - purposes = ['commissioning', 'install', 'xinstall'] |
441 | + osystem_obj = OperatingSystemRegistry.get_item(osystem, default=None) |
442 | + if osystem_obj is None: |
443 | + return [] |
444 | + |
445 | + purposes = osystem_obj.get_boot_image_purposes( |
446 | + arch, subarch, release, label) |
447 | return [ |
448 | dict( |
449 | osystem=osystem, architecture=arch, subarchitecture=subarch, |
450 | |
451 | === modified file 'src/provisioningserver/driver/__init__.py' |
452 | --- src/provisioningserver/driver/__init__.py 2014-03-28 16:46:55 +0000 |
453 | +++ src/provisioningserver/driver/__init__.py 2014-05-13 18:46:09 +0000 |
454 | @@ -16,17 +16,30 @@ |
455 | "Architecture", |
456 | "ArchitectureRegistry", |
457 | "BootResource", |
458 | + "OperatingSystem", |
459 | + "OperatingSystemRegistry", |
460 | ] |
461 | |
462 | from abc import ( |
463 | ABCMeta, |
464 | abstractmethod, |
465 | + abstractproperty, |
466 | ) |
467 | |
468 | from provisioningserver.power_schema import JSON_POWER_TYPE_PARAMETERS |
469 | from provisioningserver.utils.registry import Registry |
470 | |
471 | |
472 | +class BOOT_IMAGE_PURPOSE: |
473 | + """The vocabulary of a `BootImage`'s purpose.""" |
474 | + #: Usable for commissioning |
475 | + COMMISSIONING = 'commissioning' |
476 | + #: Usable for install |
477 | + INSTALL = 'install' |
478 | + #: Usable for fast-path install |
479 | + XINSTALL = 'xinstall' |
480 | + |
481 | + |
482 | class Architecture: |
483 | |
484 | def __init__(self, name, description, pxealiases=None, |
485 | @@ -98,6 +111,54 @@ |
486 | """ |
487 | |
488 | |
489 | +class OperatingSystem: |
490 | + """Skeleton for an operating system.""" |
491 | + |
492 | + __metaclass__ = ABCMeta |
493 | + |
494 | + @abstractproperty |
495 | + def name(self): |
496 | + """Name of the operating system.""" |
497 | + |
498 | + @abstractproperty |
499 | + def title(self): |
500 | + """Title of the operating system.""" |
501 | + |
502 | + @abstractmethod |
503 | + def get_supported_releases(self): |
504 | + """Gets list of supported releases for Ubuntu. |
505 | + |
506 | + :returns: list of supported releases |
507 | + """ |
508 | + |
509 | + @abstractmethod |
510 | + def get_default_release(self): |
511 | + """Gets the default release to use when a release is not |
512 | + explicit. |
513 | + |
514 | + :returns: default release to use |
515 | + """ |
516 | + |
517 | + @abstractmethod |
518 | + def format_release_choices(self, releases): |
519 | + """Formats the release choices that are presented to the user. |
520 | + |
521 | + :param releases: list of installed boot image releases |
522 | + :returns: Return Django "choices" list |
523 | + """ |
524 | + |
525 | + @abstractmethod |
526 | + def get_boot_image_purposes(self, arch, subarch, release, label): |
527 | + """Returns the supported purposes of a boot image. |
528 | + |
529 | + :param arch: Architecture of boot image. |
530 | + :param subarch: Sub-architecture of boot image. |
531 | + :param release: Release of boot image. |
532 | + :param label: Label of boot image. |
533 | + :returns: list of supported purposes |
534 | + """ |
535 | + |
536 | + |
537 | class HardwareDiscoverContext: |
538 | |
539 | __metaclass__ = ABCMeta |
540 | @@ -126,6 +187,10 @@ |
541 | """Registry for boot resource classes.""" |
542 | |
543 | |
544 | +class OperatingSystemRegistry(Registry): |
545 | + """Registry for operating system classes.""" |
546 | + |
547 | + |
548 | class PowerTypeRegistry(Registry): |
549 | """Registry for power type classes.""" |
550 | |
551 | @@ -147,3 +212,11 @@ |
552 | builtin_power_types = JSON_POWER_TYPE_PARAMETERS |
553 | for power_type in builtin_power_types: |
554 | PowerTypeRegistry.register_item(power_type['name'], power_type) |
555 | + |
556 | + |
557 | +from provisioningserver.driver.os_ubuntu import UbuntuOS |
558 | +builtin_osystems = [ |
559 | + UbuntuOS(), |
560 | + ] |
561 | +for osystem in builtin_osystems: |
562 | + OperatingSystemRegistry.register_item(osystem.name, osystem) |
563 | |
564 | === added file 'src/provisioningserver/driver/os_ubuntu.py' |
565 | --- src/provisioningserver/driver/os_ubuntu.py 1970-01-01 00:00:00 +0000 |
566 | +++ src/provisioningserver/driver/os_ubuntu.py 2014-05-13 18:46:09 +0000 |
567 | @@ -0,0 +1,89 @@ |
568 | +# Copyright 2014 Canonical Ltd. This software is licensed under the |
569 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
570 | + |
571 | +"""Ubuntu Operating System.""" |
572 | + |
573 | +from __future__ import ( |
574 | + absolute_import, |
575 | + print_function, |
576 | + unicode_literals, |
577 | + ) |
578 | + |
579 | +str = None |
580 | + |
581 | +__metaclass__ = type |
582 | +__all__ = [ |
583 | + "UbuntuOS", |
584 | + ] |
585 | + |
586 | +from provisioningserver.driver import ( |
587 | + BOOT_IMAGE_PURPOSE, |
588 | + OperatingSystem, |
589 | + ) |
590 | + |
591 | + |
592 | +DISTRO_SERIES_CHOICES = { |
593 | + 'precise': 'Ubuntu 12.04 LTS "Precise Pangolin"', |
594 | + 'quantal': 'Ubuntu 12.10 "Quantal Quetzal"', |
595 | + 'raring': 'Ubuntu 13.04 "Raring Ringtail"', |
596 | + 'saucy': 'Ubuntu 13.10 "Saucy Salamander"', |
597 | + 'trusty': 'Ubuntu 14.04 LTS "Trusty Tahr"', |
598 | +} |
599 | + |
600 | +COMMISIONING_DISTRO_SERIES = [ |
601 | + 'trusty', |
602 | +] |
603 | + |
604 | +DISTRO_SERIES_DEFAULT = 'trusty' |
605 | +COMMISIONING_DISTRO_SERIES_DEFAULT = 'trusty' |
606 | + |
607 | + |
608 | +class UbuntuOS(OperatingSystem): |
609 | + """Ubuntu operating system.""" |
610 | + |
611 | + name = "ubuntu" |
612 | + title = "Ubuntu" |
613 | + |
614 | + def get_boot_image_purposes(self, arch, subarch, release, label): |
615 | + """Gets the purpose of each boot image.""" |
616 | + return [ |
617 | + BOOT_IMAGE_PURPOSE.COMMISSIONING, |
618 | + BOOT_IMAGE_PURPOSE.INSTALL, |
619 | + BOOT_IMAGE_PURPOSE.XINSTALL |
620 | + ] |
621 | + |
622 | + def get_supported_releases(self): |
623 | + """Gets list of supported releases for Ubuntu.""" |
624 | + # To make this data better, could pull this information from |
625 | + # simplestreams. So only need to update simplestreams for a |
626 | + # new release. |
627 | + return DISTRO_SERIES_CHOICES.keys() |
628 | + |
629 | + def get_default_release(self): |
630 | + """Gets the default release to use when a release is not |
631 | + explicit.""" |
632 | + return DISTRO_SERIES_DEFAULT |
633 | + |
634 | + def get_supported_commissioning_releases(self): |
635 | + """Gets the supported commissioning releases for Ubuntu. This |
636 | + only exists on Ubuntu, because that is the only operating |
637 | + system that supports commissioning. |
638 | + """ |
639 | + return COMMISIONING_DISTRO_SERIES |
640 | + |
641 | + def get_default_commissioning_release(self): |
642 | + """Gets the default commissioning release for Ubuntu. This only exists |
643 | + on Ubuntu, because that is the only operating system that supports |
644 | + commissioning. |
645 | + """ |
646 | + return COMMISIONING_DISTRO_SERIES_DEFAULT |
647 | + |
648 | + def format_release_choices(self, releases): |
649 | + """Formats the release choices that are presented to the user.""" |
650 | + choices = [] |
651 | + releases = sorted(releases, reverse=True) |
652 | + for release in releases: |
653 | + title = DISTRO_SERIES_CHOICES.get(release) |
654 | + if title is not None: |
655 | + choices.append((release, title)) |
656 | + return choices |
657 | |
658 | === added file 'src/provisioningserver/driver/tests/test_os_ubuntu.py' |
659 | --- src/provisioningserver/driver/tests/test_os_ubuntu.py 1970-01-01 00:00:00 +0000 |
660 | +++ src/provisioningserver/driver/tests/test_os_ubuntu.py 2014-05-13 18:46:09 +0000 |
661 | @@ -0,0 +1,76 @@ |
662 | +# Copyright 2012-2014 Canonical Ltd. This software is licensed under the |
663 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
664 | + |
665 | +"""Tests for the UbuntuOS module.""" |
666 | + |
667 | +from __future__ import ( |
668 | + absolute_import, |
669 | + print_function, |
670 | + unicode_literals, |
671 | + ) |
672 | + |
673 | +str = None |
674 | + |
675 | +__metaclass__ = type |
676 | +__all__ = [] |
677 | + |
678 | +from itertools import product |
679 | +from maastesting.factory import factory |
680 | +from maastesting.testcase import MAASTestCase |
681 | +from provisioningserver.driver import BOOT_IMAGE_PURPOSE |
682 | +from provisioningserver.driver.os_ubuntu import ( |
683 | + COMMISIONING_DISTRO_SERIES, |
684 | + COMMISIONING_DISTRO_SERIES_DEFAULT, |
685 | + DISTRO_SERIES_CHOICES, |
686 | + DISTRO_SERIES_DEFAULT, |
687 | + UbuntuOS, |
688 | + ) |
689 | + |
690 | + |
691 | +class TestUbuntuOS(MAASTestCase): |
692 | + |
693 | + def test_get_boot_image_purposes(self): |
694 | + osystem = UbuntuOS() |
695 | + archs = [factory.make_name('arch') for _ in range(2)] |
696 | + subarchs = [factory.make_name('subarch') for _ in range(2)] |
697 | + releases = [factory.make_name('release') for _ in range(2)] |
698 | + labels = [factory.make_name('label') for _ in range(2)] |
699 | + for arch, subarch, release, label in product( |
700 | + archs, subarchs, releases, labels): |
701 | + expected = osystem.get_boot_image_purposes( |
702 | + arch, subarchs, release, label) |
703 | + self.assertIsInstance(expected, list) |
704 | + self.assertEqual(expected, [ |
705 | + BOOT_IMAGE_PURPOSE.COMMISSIONING, |
706 | + BOOT_IMAGE_PURPOSE.INSTALL, |
707 | + BOOT_IMAGE_PURPOSE.XINSTALL |
708 | + ]) |
709 | + |
710 | + def test_get_supported_releases(self): |
711 | + osystem = UbuntuOS() |
712 | + expected = osystem.get_supported_releases() |
713 | + self.assertIsInstance(expected, list) |
714 | + self.assertEqual(expected, DISTRO_SERIES_CHOICES.keys()) |
715 | + |
716 | + def test_get_default_release(self): |
717 | + osystem = UbuntuOS() |
718 | + expected = osystem.get_default_release() |
719 | + self.assertEqual(expected, DISTRO_SERIES_DEFAULT) |
720 | + |
721 | + def test_get_supported_commissioning_releases(self): |
722 | + osystem = UbuntuOS() |
723 | + expected = osystem.get_supported_commissioning_releases() |
724 | + self.assertIsInstance(expected, list) |
725 | + self.assertEqual(expected, COMMISIONING_DISTRO_SERIES) |
726 | + |
727 | + def test_default_commissioning_release(self): |
728 | + osystem = UbuntuOS() |
729 | + expected = osystem.get_default_commissioning_release() |
730 | + self.assertEqual(expected, COMMISIONING_DISTRO_SERIES_DEFAULT) |
731 | + |
732 | + def test_format_release_choices(self): |
733 | + osystem = UbuntuOS() |
734 | + releases = osystem.get_supported_releases() |
735 | + formatted = osystem.format_release_choices(releases) |
736 | + for name, title in formatted: |
737 | + self.assertEqual(DISTRO_SERIES_CHOICES[name], title) |
738 | |
739 | === modified file 'src/provisioningserver/driver/tests/test_registries.py' |
740 | --- src/provisioningserver/driver/tests/test_registries.py 2014-03-28 16:46:55 +0000 |
741 | +++ src/provisioningserver/driver/tests/test_registries.py 2014-05-13 18:46:09 +0000 |
742 | @@ -20,6 +20,7 @@ |
743 | Architecture, |
744 | ArchitectureRegistry, |
745 | BootResourceRegistry, |
746 | + OperatingSystemRegistry, |
747 | PowerTypeRegistry, |
748 | ) |
749 | from provisioningserver.utils.testing import RegistryFixture |
750 | @@ -68,6 +69,13 @@ |
751 | self.assertEqual( |
752 | None, ArchitectureRegistry.get_by_pxealias("stinkywinky")) |
753 | |
754 | + def test_operating_system_registry(self): |
755 | + self.assertItemsEqual([], OperatingSystemRegistry) |
756 | + OperatingSystemRegistry.register_item("resource", sentinel.resource) |
757 | + self.assertIn( |
758 | + sentinel.resource, |
759 | + (item for name, item in OperatingSystemRegistry)) |
760 | + |
761 | def test_power_type_registry(self): |
762 | self.assertItemsEqual([], PowerTypeRegistry) |
763 | PowerTypeRegistry.register_item("resource", sentinel.resource) |
764 | |
765 | === modified file 'src/provisioningserver/rpc/tests/test_clusterservice.py' |
766 | --- src/provisioningserver/rpc/tests/test_clusterservice.py 2014-05-13 18:43:40 +0000 |
767 | +++ src/provisioningserver/rpc/tests/test_clusterservice.py 2014-05-13 18:46:09 +0000 |
768 | @@ -34,6 +34,7 @@ |
769 | sentinel, |
770 | ) |
771 | from provisioningserver.boot import tftppath |
772 | +from provisioningserver.boot.tests.test_tftppath import make_osystem |
773 | from provisioningserver.power_schema import JSON_POWER_TYPE_PARAMETERS |
774 | from provisioningserver.rpc import ( |
775 | cluster, |
776 | @@ -179,6 +180,7 @@ |
777 | tftpdir = self.make_dir() |
778 | for options in product(osystems, archs, subarchs, releases, labels): |
779 | os.makedirs(os.path.join(tftpdir, *options)) |
780 | + make_osystem(self, options[0], purposes) |
781 | |
782 | # Ensure that list_boot_images() uses the above TFTP file tree. |
783 | self.useFixture(set_tftp_root(tftpdir)) |
Phew, took me a while to get through this. It mostly looks great, some minor
improvements can be made, but [4] and [5] are blockers for landing.
It's heading in the right direction though!
[1]
=== modified file 'src/maasserver /templates/ maasserver/ bootimage- list.html' templates/ maasserver/ bootimage- list.html 2014-04-23 16:09:39 +0000 templates/ maasserver/ bootimage- list.html 2014-04-28 05:57:14 +0000
<td>{{ bootimage.id }}</td> osystem_ title }}</td>
<td>{{ bootimage.release }}</td>
<td>{{ bootimage. architecture }}</td>
<td>{{ bootimage. subarchitecture }}</td>
--- src/maasserver/
+++ src/maasserver/
@@ -33,7 +33,7 @@
{% for bootimage in bootimage_list %}
<tr class="bootimage {% cycle 'even' 'odd' %}">
- <td>{{ bootimage.osystem }}</td>
+ <td>{{ bootimage.
This change needs a test in test_contains_ boot_image_ list().
[2]
+def make_osystem_ with_releases( testcase, with_releases=True, osystem_name=None,
+ releases=None):
+ """Generate an arbitrary operating system.
+
+ :param with_releases: Should the operating system include releases?
+ Defaults to `True`.
There's a few params left to document :)
+ """ make_name( 'os') make_name( 'release' ) for _ in range(3)]
+ if osystem_name is None:
+ osystem_name = factory.
+ if with_releases:
+ if releases is None:
+ releases = [factory.
I would simplify the parameters here and omit the "with_releases" parameter and
just allow callers to say releases=[].
Then the code becomes:
...