Merge lp:~blake-rouse/maas/osystem-registry 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: 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
Reviewer Review Type Date Requested Status
Julian Edwards (community) Approve
Review via email: mp+217057@code.launchpad.net

Commit message

Added OperatingSystem, OperatingSystemRegistry, and UbuntuOS class.

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 OperatingSystemRegistry so the code base can find the supported operating systems. When finished this will remove the need for DISTRO_SERIES enums.

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.

To post a comment you must log in.
Revision history for this message
Julian Edwards (julian-edwards) wrote :
Download full text (12.9 KiB)

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'
  --- src/maasserver/templates/maasserver/bootimage-list.html 2014-04-23 16:09:39 +0000
  +++ src/maasserver/templates/maasserver/bootimage-list.html 2014-04-28 05:57:14 +0000
  @@ -33,7 +33,7 @@
           {% for bootimage in bootimage_list %}
             <tr class="bootimage {% cycle 'even' 'odd' %}">
               <td>{{ bootimage.id }}</td>
  - <td>{{ bootimage.osystem }}</td>
  + <td>{{ bootimage.osystem_title }}</td>
               <td>{{ bootimage.release }}</td>
               <td>{{ bootimage.architecture }}</td>
               <td>{{ bootimage.subarchitecture }}</td>

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 :)

  + """
  + if osystem_name is None:
  + osystem_name = factory.make_name('os')
  + if with_releases:
  + if releases is None:
  + releases = [factory.make_name('release') for _ in range(3)]

I would simplify the parameters here and omit the "with_releases" parameter and
just allow callers to say releases=[].

Then the code becomes:
 ...

review: Needs Fixing
Revision history for this message
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-compatible for now, so we don't need to engineer this for a future that is hard to predict in detail. We can embellish and refactor as the need arises.

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

Revision history for this message
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 OperatingSystemRegistry into the drivers/__init__.py. Removed the need for the osystems folder. The UbuntuOS is now in os_ubuntu.py under drivers, each new os can be labeled os_{{name}}.py

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

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

[4] Add bug comment to fix later.

[9] I will also fix in a later branch.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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))