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
=== modified file 'src/maasserver/testing/factory.py'
--- src/maasserver/testing/factory.py 2014-05-13 18:43:40 +0000
+++ src/maasserver/testing/factory.py 2014-05-13 18:46:09 +0000
@@ -60,6 +60,10 @@
60 )60 )
61from netaddr import IPAddress61from netaddr import IPAddress
6262
63# XXX 2014-05-13 blake-rouse bug=1319143
64# Need to not import directly, use RPC to info from cluster.
65from provisioningserver.driver import OperatingSystemRegistry
66
63# We have a limited number of public keys:67# We have a limited number of public keys:
64# src/maasserver/tests/data/test_rsa{0, 1, 2, 3, 4}.pub68# src/maasserver/tests/data/test_rsa{0, 1, 2, 3, 4}.pub
65MAX_PUBLIC_KEYS = 569MAX_PUBLIC_KEYS = 5
@@ -140,6 +144,21 @@
140 [choice for choice in list(get_power_types().keys())144 [choice for choice in list(get_power_types().keys())
141 if choice not in but_not])145 if choice not in but_not])
142146
147 def getRandomOS(self):
148 """Pick a random operating system from the registry."""
149 osystems = [obj for _, obj in OperatingSystemRegistry]
150 return random.choice(osystems)
151
152 def getRandomRelease(self, osystem):
153 """Pick a random release from operating system."""
154 releases = osystem.get_supported_releases()
155 return random.choice(releases)
156
157 def getRandomCommissioningRelease(self, osystem):
158 """Pick a random commissioning release from operating system."""
159 releases = osystem.get_supported_commissioning_releases()
160 return random.choice(releases)
161
143 def _save_node_unchecked(self, node):162 def _save_node_unchecked(self, node):
144 """Save a :class:`Node`, but circumvent status transition checks."""163 """Save a :class:`Node`, but circumvent status transition checks."""
145 valid_initial_states = NODE_TRANSITIONS[None]164 valid_initial_states = NODE_TRANSITIONS[None]
146165
=== added file 'src/maasserver/testing/osystems.py'
--- src/maasserver/testing/osystems.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/testing/osystems.py 2014-05-13 18:46:09 +0000
@@ -0,0 +1,90 @@
1# Copyright 2014 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Helpers for operating systems in testing."""
5
6from __future__ import (
7 absolute_import,
8 print_function,
9 unicode_literals,
10 )
11
12str = None
13
14__metaclass__ = type
15__all__ = [
16 'make_usable_osystem',
17 'patch_usable_osystems',
18 ]
19
20from random import randint
21
22from maasserver import forms
23from maasserver.testing.factory import factory
24from provisioningserver.driver import BOOT_IMAGE_PURPOSE
25from provisioningserver.boot.tests.test_tftppath import make_osystem
26
27
28def make_osystem_with_releases(testcase, osystem_name=None, releases=None):
29 """Generate an arbitrary operating system.
30
31 :param osystem_name: The operating system name. Useful in cases where
32 we need to test that not supplying an os works correctly.
33 :param releases: The list of releases name. Useful in cases where
34 we need to test that not supplying a release works correctly.
35 """
36 if osystem_name is None:
37 osystem_name = factory.make_name('os')
38 if releases is None:
39 releases = [factory.make_name('release') for _ in range(3)]
40
41 osystem = make_osystem(
42 testcase,
43 osystem_name,
44 [BOOT_IMAGE_PURPOSE.INSTALL, BOOT_IMAGE_PURPOSE.XINSTALL])
45 if releases is not None and releases != []:
46 osystem.fake_list = releases
47 return osystem
48
49
50def patch_usable_osystems(testcase, osystems=None, allow_empty=True):
51 """Set a fixed list of usable operating systems.
52
53 A usable operating system is one for which boot images are available.
54
55 :param testcase: A `TestCase` whose `patch` this function can use.
56 :param osystems: Optional list of operating systems. If omitted,
57 defaults to a list (which may be empty) of random operating systems.
58 """
59 start = 0
60 if allow_empty is False:
61 start = 1
62 if osystems is None:
63 osystems = [
64 make_osystem_with_releases(testcase)
65 for _ in range(randint(start, 2))
66 ]
67 distro_series = {}
68 for osystem in osystems:
69 distro_series[osystem.name] = osystem.get_supported_releases()
70 testcase.patch(forms, 'list_all_usable_osystems').return_value = osystems
71 testcase.patch(
72 forms, 'list_all_usable_releases').return_value = distro_series
73
74
75def make_usable_osystem(testcase, osystem_name=None, releases=None):
76 """Return arbitrary operating system, and make it "usable."
77
78 A usable operating system is one for which boot images are available.
79
80 :param testcase: A `TestCase` whose `patch` this function can pass to
81 `patch_usable_osystems`.
82 :param osystem_name: The operating system name. Useful in cases where
83 we need to test that not supplying an os works correctly.
84 :param releases: The list of releases name. Useful in cases where
85 we need to test that not supplying a release works correctly.
86 """
87 osystem = make_osystem_with_releases(
88 testcase, osystem_name=osystem_name, releases=releases)
89 patch_usable_osystems(testcase, [osystem])
90 return osystem
091
=== modified file 'src/maasserver/views/tests/test_boot_image_list.py'
--- src/maasserver/views/tests/test_boot_image_list.py 2014-04-24 13:52:17 +0000
+++ src/maasserver/views/tests/test_boot_image_list.py 2014-05-13 18:46:09 +0000
@@ -22,16 +22,22 @@
22from maasserver.testing.factory import factory22from maasserver.testing.factory import factory
23from maasserver.testing.testcase import MAASServerTestCase23from maasserver.testing.testcase import MAASServerTestCase
24from maasserver.views.clusters import BootImagesListView24from maasserver.views.clusters import BootImagesListView
25from provisioningserver.boot.tests.test_tftppath import make_osystem
25from testtools.matchers import ContainsAll26from testtools.matchers import ContainsAll
2627
2728
28class BootImageListTest(MAASServerTestCase):29class BootImageListTest(MAASServerTestCase):
2930
31 def setUp(self):
32 super(BootImageListTest, self).setUp()
33
30 def test_contains_boot_image_list(self):34 def test_contains_boot_image_list(self):
31 self.client_log_in(as_admin=True)35 self.client_log_in(as_admin=True)
32 nodegroup = factory.make_node_group()36 nodegroup = factory.make_node_group()
33 images = [37 boot_images = [
34 factory.make_boot_image(nodegroup=nodegroup) for _ in range(3)]38 factory.make_boot_image(nodegroup=nodegroup) for _ in range(3)]
39 for bi in boot_images:
40 make_osystem(self, bi.osystem, ['install'])
35 response = self.client.get(41 response = self.client.get(
36 reverse('cluster-bootimages-list', args=[nodegroup.uuid]))42 reverse('cluster-bootimages-list', args=[nodegroup.uuid]))
37 self.assertEqual(43 self.assertEqual(
@@ -44,8 +50,9 @@
44 image.release,50 image.release,
45 image.subarchitecture,51 image.subarchitecture,
46 image.architecture,52 image.architecture,
53 image.osystem,
47 '%s' % image.updated.year,54 '%s' % image.updated.year,
48 ] for image in images]55 ] for image in boot_images]
49 self.assertThat(56 self.assertThat(
50 response.content, ContainsAll(itertools.chain(*items_in_page)))57 response.content, ContainsAll(itertools.chain(*items_in_page)))
5158
@@ -54,7 +61,12 @@
54 self.client_log_in(as_admin=True)61 self.client_log_in(as_admin=True)
55 nodegroup = factory.make_node_group()62 nodegroup = factory.make_node_group()
56 # Create 4 images.63 # Create 4 images.
57 [factory.make_boot_image(nodegroup=nodegroup) for _ in range(4)]64 boot_images = [
65 factory.make_boot_image(nodegroup=nodegroup)
66 for _ in range(4)
67 ]
68 for bi in boot_images:
69 make_osystem(self, bi.osystem, ['install'])
58 response = self.client.get(70 response = self.client.get(
59 reverse('cluster-bootimages-list', args=[nodegroup.uuid]))71 reverse('cluster-bootimages-list', args=[nodegroup.uuid]))
60 self.assertEqual(httplib.OK, response.status_code)72 self.assertEqual(httplib.OK, response.status_code)
@@ -65,7 +77,9 @@
6577
66 def test_displays_warning_if_boot_image_list_is_empty(self):78 def test_displays_warning_if_boot_image_list_is_empty(self):
67 # Create boot images in another nodegroup.79 # Create boot images in another nodegroup.
68 [factory.make_boot_image() for _ in range(3)]80 boot_images = [factory.make_boot_image() for _ in range(3)]
81 for bi in boot_images:
82 make_osystem(self, bi.osystem, ['install'])
69 self.client_log_in(as_admin=True)83 self.client_log_in(as_admin=True)
70 nodegroup = factory.make_node_group()84 nodegroup = factory.make_node_group()
71 response = self.client.get(85 response = self.client.get(
7286
=== modified file 'src/maasserver/views/tests/test_clusters.py'
--- src/maasserver/views/tests/test_clusters.py 2014-04-24 13:52:17 +0000
+++ src/maasserver/views/tests/test_clusters.py 2014-05-13 18:46:09 +0000
@@ -41,6 +41,7 @@
41 ANY,41 ANY,
42 call,42 call,
43 )43 )
44from provisioningserver.boot.tests.test_tftppath import make_osystem
44from testtools.matchers import (45from testtools.matchers import (
45 AllMatch,46 AllMatch,
46 Contains,47 Contains,
@@ -309,7 +310,12 @@
309 def test_contains_link_to_boot_image_list(self):310 def test_contains_link_to_boot_image_list(self):
310 self.client_log_in(as_admin=True)311 self.client_log_in(as_admin=True)
311 nodegroup = factory.make_node_group()312 nodegroup = factory.make_node_group()
312 [factory.make_boot_image(nodegroup=nodegroup) for _ in range(3)]313 boot_images = [
314 factory.make_boot_image(nodegroup=nodegroup)
315 for _ in range(3)
316 ]
317 for bi in boot_images:
318 make_osystem(self, bi.osystem, ['install'])
313 response = self.client.get(319 response = self.client.get(
314 reverse('cluster-edit', args=[nodegroup.uuid]))320 reverse('cluster-edit', args=[nodegroup.uuid]))
315 self.assertEqual(321 self.assertEqual(
@@ -320,7 +326,9 @@
320326
321 def test_displays_warning_if_boot_image_list_is_empty(self):327 def test_displays_warning_if_boot_image_list_is_empty(self):
322 # Create boot images in another nodegroup.328 # Create boot images in another nodegroup.
323 [factory.make_boot_image() for _ in range(3)]329 boot_images = [factory.make_boot_image() for _ in range(3)]
330 for bi in boot_images:
331 make_osystem(self, bi.osystem, ['install'])
324 self.client_log_in(as_admin=True)332 self.client_log_in(as_admin=True)
325 nodegroup = factory.make_node_group()333 nodegroup = factory.make_node_group()
326 response = self.client.get(334 response = self.client.get(
327335
=== modified file 'src/provisioningserver/boot/__init__.py'
--- src/provisioningserver/boot/__init__.py 2014-04-23 16:09:39 +0000
+++ src/provisioningserver/boot/__init__.py 2014-05-13 18:46:09 +0000
@@ -168,7 +168,7 @@
168 """168 """
169 def image_dir(params):169 def image_dir(params):
170 return compose_image_path(170 return compose_image_path(
171 'ubuntu', params.arch, params.subarch,171 params.osystem, params.arch, params.subarch,
172 params.release, params.label)172 params.release, params.label)
173173
174 def initrd_path(params):174 def initrd_path(params):
175175
=== modified file 'src/provisioningserver/boot/tests/test_pxe.py'
--- src/provisioningserver/boot/tests/test_pxe.py 2014-04-23 16:09:39 +0000
+++ src/provisioningserver/boot/tests/test_pxe.py 2014-05-13 18:46:09 +0000
@@ -163,7 +163,7 @@
163 self.assertThat(output, StartsWith("DEFAULT "))163 self.assertThat(output, StartsWith("DEFAULT "))
164 # The PXE parameters are all set according to the options.164 # The PXE parameters are all set according to the options.
165 image_dir = compose_image_path(165 image_dir = compose_image_path(
166 osystem='ubuntu', arch=params.arch, subarch=params.subarch,166 osystem=params.osystem, arch=params.arch, subarch=params.subarch,
167 release=params.release, label=params.label)167 release=params.release, label=params.label)
168 self.assertThat(168 self.assertThat(
169 output, MatchesAll(169 output, MatchesAll(
@@ -243,9 +243,11 @@
243 method = PXEBootMethod()243 method = PXEBootMethod()
244 get_ephemeral_name = self.patch(kernel_opts, "get_ephemeral_name")244 get_ephemeral_name = self.patch(kernel_opts, "get_ephemeral_name")
245 get_ephemeral_name.return_value = factory.make_name("ephemeral")245 get_ephemeral_name.return_value = factory.make_name("ephemeral")
246 osystem = factory.make_name('osystem')
246 options = {247 options = {
247 "kernel_params": make_kernel_parameters(248 "kernel_params": make_kernel_parameters(
248 testcase=self, subarch="generic", purpose=self.purpose),249 testcase=self, osystem=osystem, subarch="generic",
250 purpose=self.purpose),
249 }251 }
250 output = method.render_config(**options)252 output = method.render_config(**options)
251 config = parse_pxe_config(output)253 config = parse_pxe_config(output)
@@ -269,7 +271,7 @@
269 self.assertThat(271 self.assertThat(
270 section, ContainsAll(("KERNEL", "INITRD", "APPEND")))272 section, ContainsAll(("KERNEL", "INITRD", "APPEND")))
271 contains_arch_path = StartsWith(273 contains_arch_path = StartsWith(
272 "ubuntu/%s/" % section_label)274 "%s/%s/" % (osystem, section_label))
273 self.assertThat(section["KERNEL"], contains_arch_path)275 self.assertThat(section["KERNEL"], contains_arch_path)
274 self.assertThat(section["INITRD"], contains_arch_path)276 self.assertThat(section["INITRD"], contains_arch_path)
275 self.assertIn("APPEND", section)277 self.assertIn("APPEND", section)
276278
=== modified file 'src/provisioningserver/boot/tests/test_tftppath.py'
--- src/provisioningserver/boot/tests/test_tftppath.py 2014-04-23 16:09:39 +0000
+++ src/provisioningserver/boot/tests/test_tftppath.py 2014-05-13 18:46:09 +0000
@@ -30,6 +30,10 @@
30 list_subdirs,30 list_subdirs,
31 locate_tftp_path,31 locate_tftp_path,
32 )32 )
33from provisioningserver.driver import (
34 OperatingSystem,
35 OperatingSystemRegistry,
36 )
33from provisioningserver.testing.boot_images import (37from provisioningserver.testing.boot_images import (
34 make_boot_image_storage_params,38 make_boot_image_storage_params,
35 )39 )
@@ -41,6 +45,40 @@
41from testtools.testcase import ExpectedException45from testtools.testcase import ExpectedException
4246
4347
48class FakeOS(OperatingSystem):
49
50 name = ""
51 title = ""
52
53 def __init__(self, name, purpose, releases=None):
54 self.name = name
55 self.title = name
56 self.purpose = purpose
57 if releases is None:
58 self.fake_list = [
59 factory.getRandomString()
60 for _ in range(3)
61 ]
62 else:
63 self.fake_list = releases
64
65 def get_boot_image_purposes(self, *args):
66 return self.purpose
67
68 def get_supported_releases(self):
69 return self.fake_list
70
71 def get_default_release(self):
72 return self.fake_list[0]
73
74 def format_release_choices(self, releases):
75 return [
76 (release, release)
77 for release in releases
78 if release in self.fake_list
79 ]
80
81
44def make_image(params, purpose):82def make_image(params, purpose):
45 """Describe an image as a dict similar to what `list_boot_images` returns.83 """Describe an image as a dict similar to what `list_boot_images` returns.
4684
@@ -51,6 +89,29 @@
51 return image89 return image
5290
5391
92def make_osystem(testcase, osystem, purpose):
93 """Makes the operating system class and registers it."""
94 if osystem not in OperatingSystemRegistry:
95 fake = FakeOS(osystem, purpose)
96 OperatingSystemRegistry.register_item(fake.name, fake)
97 testcase.addCleanup(
98 OperatingSystemRegistry.unregister_item, osystem)
99 return fake
100
101 else:
102
103 obj = OperatingSystemRegistry[osystem]
104 old_func = obj.get_boot_image_purposes
105 testcase.patch(obj, 'get_boot_image_purposes').return_value = purpose
106
107 def reset_func(obj, old_func):
108 obj.get_boot_image_purposes = old_func
109
110 testcase.addCleanup(reset_func, obj, old_func)
111
112 return obj
113
114
54class TestTFTPPath(MAASTestCase):115class TestTFTPPath(MAASTestCase):
55116
56 def setUp(self):117 def setUp(self):
@@ -123,22 +184,31 @@
123 params = make_boot_image_storage_params()184 params = make_boot_image_storage_params()
124 self.make_image_dir(params, self.tftproot)185 self.make_image_dir(params, self.tftproot)
125 purposes = ['install', 'commissioning', 'xinstall']186 purposes = ['install', 'commissioning', 'xinstall']
187 make_osystem(self, params['osystem'], purposes)
126 self.assertItemsEqual(188 self.assertItemsEqual(
127 [make_image(params, purpose) for purpose in purposes],189 [make_image(params, purpose) for purpose in purposes],
128 list_boot_images(self.tftproot))190 list_boot_images(self.tftproot))
129191
130 def test_list_boot_images_enumerates_boot_images(self):192 def test_list_boot_images_enumerates_boot_images(self):
193 purposes = ['install', 'commissioning', 'xinstall']
131 params = [make_boot_image_storage_params() for counter in range(3)]194 params = [make_boot_image_storage_params() for counter in range(3)]
132 for param in params:195 for param in params:
133 self.make_image_dir(param, self.tftproot)196 self.make_image_dir(param, self.tftproot)
197 make_osystem(self, param['osystem'], purposes)
134 self.assertItemsEqual(198 self.assertItemsEqual(
135 [199 [
136 make_image(param, purpose)200 make_image(param, purpose)
137 for param in params201 for param in params
138 for purpose in ['install', 'commissioning', 'xinstall']202 for purpose in purposes
139 ],203 ],
140 list_boot_images(self.tftproot))204 list_boot_images(self.tftproot))
141205
206 def test_list_boot_images_empty_on_missing_osystems(self):
207 params = [make_boot_image_storage_params() for counter in range(3)]
208 for param in params:
209 self.make_image_dir(param, self.tftproot)
210 self.assertItemsEqual([], list_boot_images(self.tftproot))
211
142 def test_is_visible_subdir_ignores_regular_files(self):212 def test_is_visible_subdir_ignores_regular_files(self):
143 plain_file = self.make_file()213 plain_file = self.make_file()
144 self.assertFalse(214 self.assertFalse(
145215
=== modified file 'src/provisioningserver/boot/tests/test_uefi.py'
--- src/provisioningserver/boot/tests/test_uefi.py 2014-04-23 16:09:39 +0000
+++ src/provisioningserver/boot/tests/test_uefi.py 2014-05-13 18:46:09 +0000
@@ -73,7 +73,7 @@
73 self.assertThat(output, StartsWith("set default=\"0\""))73 self.assertThat(output, StartsWith("set default=\"0\""))
74 # The UEFI parameters are all set according to the options.74 # The UEFI parameters are all set according to the options.
75 image_dir = compose_image_path(75 image_dir = compose_image_path(
76 osystem='ubuntu', arch=params.arch, subarch=params.subarch,76 osystem=params.osystem, arch=params.arch, subarch=params.subarch,
77 release=params.release, label=params.label)77 release=params.release, label=params.label)
7878
79 self.assertThat(79 self.assertThat(
8080
=== modified file 'src/provisioningserver/boot/tftppath.py'
--- src/provisioningserver/boot/tftppath.py 2014-04-23 16:09:39 +0000
+++ src/provisioningserver/boot/tftppath.py 2014-05-13 18:46:09 +0000
@@ -25,6 +25,7 @@
25from logging import getLogger25from logging import getLogger
26import os.path26import os.path
2727
28from provisioningserver.driver import OperatingSystemRegistry
2829
29logger = getLogger(__name__)30logger = getLogger(__name__)
3031
@@ -114,16 +115,16 @@
114def extract_image_params(path):115def extract_image_params(path):
115 """Represent a list of TFTP path elements as a list of boot-image dicts.116 """Represent a list of TFTP path elements as a list of boot-image dicts.
116117
117 The path must consist of a full [architecture, subarchitecture, release]118 The path must consist of a full [osystem, architecture, subarchitecture,
118 that identify a kind of boot that we may need an image for.119 release] that identify a kind of boot that we may need an image for.
119 """120 """
120 osystem, arch, subarch, release, label = path121 osystem, arch, subarch, release, label = path
121 # XXX: rvb 2014-03-24: The images import script currently imports all the122 osystem_obj = OperatingSystemRegistry.get_item(osystem, default=None)
122 # images for the configured selections (where a selection is an123 if osystem_obj is None:
123 # arch/subarch/series/label combination). When the import script grows the124 return []
124 # ability to import the images for a particular purpose, we need to change125
125 # this code to report what is actually present.126 purposes = osystem_obj.get_boot_image_purposes(
126 purposes = ['commissioning', 'install', 'xinstall']127 arch, subarch, release, label)
127 return [128 return [
128 dict(129 dict(
129 osystem=osystem, architecture=arch, subarchitecture=subarch,130 osystem=osystem, architecture=arch, subarchitecture=subarch,
130131
=== modified file 'src/provisioningserver/driver/__init__.py'
--- src/provisioningserver/driver/__init__.py 2014-03-28 16:46:55 +0000
+++ src/provisioningserver/driver/__init__.py 2014-05-13 18:46:09 +0000
@@ -16,17 +16,30 @@
16 "Architecture",16 "Architecture",
17 "ArchitectureRegistry",17 "ArchitectureRegistry",
18 "BootResource",18 "BootResource",
19 "OperatingSystem",
20 "OperatingSystemRegistry",
19 ]21 ]
2022
21from abc import (23from abc import (
22 ABCMeta,24 ABCMeta,
23 abstractmethod,25 abstractmethod,
26 abstractproperty,
24 )27 )
2528
26from provisioningserver.power_schema import JSON_POWER_TYPE_PARAMETERS29from provisioningserver.power_schema import JSON_POWER_TYPE_PARAMETERS
27from provisioningserver.utils.registry import Registry30from provisioningserver.utils.registry import Registry
2831
2932
33class BOOT_IMAGE_PURPOSE:
34 """The vocabulary of a `BootImage`'s purpose."""
35 #: Usable for commissioning
36 COMMISSIONING = 'commissioning'
37 #: Usable for install
38 INSTALL = 'install'
39 #: Usable for fast-path install
40 XINSTALL = 'xinstall'
41
42
30class Architecture:43class Architecture:
3144
32 def __init__(self, name, description, pxealiases=None,45 def __init__(self, name, description, pxealiases=None,
@@ -98,6 +111,54 @@
98 """111 """
99112
100113
114class OperatingSystem:
115 """Skeleton for an operating system."""
116
117 __metaclass__ = ABCMeta
118
119 @abstractproperty
120 def name(self):
121 """Name of the operating system."""
122
123 @abstractproperty
124 def title(self):
125 """Title of the operating system."""
126
127 @abstractmethod
128 def get_supported_releases(self):
129 """Gets list of supported releases for Ubuntu.
130
131 :returns: list of supported releases
132 """
133
134 @abstractmethod
135 def get_default_release(self):
136 """Gets the default release to use when a release is not
137 explicit.
138
139 :returns: default release to use
140 """
141
142 @abstractmethod
143 def format_release_choices(self, releases):
144 """Formats the release choices that are presented to the user.
145
146 :param releases: list of installed boot image releases
147 :returns: Return Django "choices" list
148 """
149
150 @abstractmethod
151 def get_boot_image_purposes(self, arch, subarch, release, label):
152 """Returns the supported purposes of a boot image.
153
154 :param arch: Architecture of boot image.
155 :param subarch: Sub-architecture of boot image.
156 :param release: Release of boot image.
157 :param label: Label of boot image.
158 :returns: list of supported purposes
159 """
160
161
101class HardwareDiscoverContext:162class HardwareDiscoverContext:
102163
103 __metaclass__ = ABCMeta164 __metaclass__ = ABCMeta
@@ -126,6 +187,10 @@
126 """Registry for boot resource classes."""187 """Registry for boot resource classes."""
127188
128189
190class OperatingSystemRegistry(Registry):
191 """Registry for operating system classes."""
192
193
129class PowerTypeRegistry(Registry):194class PowerTypeRegistry(Registry):
130 """Registry for power type classes."""195 """Registry for power type classes."""
131196
@@ -147,3 +212,11 @@
147builtin_power_types = JSON_POWER_TYPE_PARAMETERS212builtin_power_types = JSON_POWER_TYPE_PARAMETERS
148for power_type in builtin_power_types:213for power_type in builtin_power_types:
149 PowerTypeRegistry.register_item(power_type['name'], power_type)214 PowerTypeRegistry.register_item(power_type['name'], power_type)
215
216
217from provisioningserver.driver.os_ubuntu import UbuntuOS
218builtin_osystems = [
219 UbuntuOS(),
220 ]
221for osystem in builtin_osystems:
222 OperatingSystemRegistry.register_item(osystem.name, osystem)
150223
=== added file 'src/provisioningserver/driver/os_ubuntu.py'
--- src/provisioningserver/driver/os_ubuntu.py 1970-01-01 00:00:00 +0000
+++ src/provisioningserver/driver/os_ubuntu.py 2014-05-13 18:46:09 +0000
@@ -0,0 +1,89 @@
1# Copyright 2014 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Ubuntu Operating System."""
5
6from __future__ import (
7 absolute_import,
8 print_function,
9 unicode_literals,
10 )
11
12str = None
13
14__metaclass__ = type
15__all__ = [
16 "UbuntuOS",
17 ]
18
19from provisioningserver.driver import (
20 BOOT_IMAGE_PURPOSE,
21 OperatingSystem,
22 )
23
24
25DISTRO_SERIES_CHOICES = {
26 'precise': 'Ubuntu 12.04 LTS "Precise Pangolin"',
27 'quantal': 'Ubuntu 12.10 "Quantal Quetzal"',
28 'raring': 'Ubuntu 13.04 "Raring Ringtail"',
29 'saucy': 'Ubuntu 13.10 "Saucy Salamander"',
30 'trusty': 'Ubuntu 14.04 LTS "Trusty Tahr"',
31}
32
33COMMISIONING_DISTRO_SERIES = [
34 'trusty',
35]
36
37DISTRO_SERIES_DEFAULT = 'trusty'
38COMMISIONING_DISTRO_SERIES_DEFAULT = 'trusty'
39
40
41class UbuntuOS(OperatingSystem):
42 """Ubuntu operating system."""
43
44 name = "ubuntu"
45 title = "Ubuntu"
46
47 def get_boot_image_purposes(self, arch, subarch, release, label):
48 """Gets the purpose of each boot image."""
49 return [
50 BOOT_IMAGE_PURPOSE.COMMISSIONING,
51 BOOT_IMAGE_PURPOSE.INSTALL,
52 BOOT_IMAGE_PURPOSE.XINSTALL
53 ]
54
55 def get_supported_releases(self):
56 """Gets list of supported releases for Ubuntu."""
57 # To make this data better, could pull this information from
58 # simplestreams. So only need to update simplestreams for a
59 # new release.
60 return DISTRO_SERIES_CHOICES.keys()
61
62 def get_default_release(self):
63 """Gets the default release to use when a release is not
64 explicit."""
65 return DISTRO_SERIES_DEFAULT
66
67 def get_supported_commissioning_releases(self):
68 """Gets the supported commissioning releases for Ubuntu. This
69 only exists on Ubuntu, because that is the only operating
70 system that supports commissioning.
71 """
72 return COMMISIONING_DISTRO_SERIES
73
74 def get_default_commissioning_release(self):
75 """Gets the default commissioning release for Ubuntu. This only exists
76 on Ubuntu, because that is the only operating system that supports
77 commissioning.
78 """
79 return COMMISIONING_DISTRO_SERIES_DEFAULT
80
81 def format_release_choices(self, releases):
82 """Formats the release choices that are presented to the user."""
83 choices = []
84 releases = sorted(releases, reverse=True)
85 for release in releases:
86 title = DISTRO_SERIES_CHOICES.get(release)
87 if title is not None:
88 choices.append((release, title))
89 return choices
090
=== added file 'src/provisioningserver/driver/tests/test_os_ubuntu.py'
--- src/provisioningserver/driver/tests/test_os_ubuntu.py 1970-01-01 00:00:00 +0000
+++ src/provisioningserver/driver/tests/test_os_ubuntu.py 2014-05-13 18:46:09 +0000
@@ -0,0 +1,76 @@
1# Copyright 2012-2014 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for the UbuntuOS module."""
5
6from __future__ import (
7 absolute_import,
8 print_function,
9 unicode_literals,
10 )
11
12str = None
13
14__metaclass__ = type
15__all__ = []
16
17from itertools import product
18from maastesting.factory import factory
19from maastesting.testcase import MAASTestCase
20from provisioningserver.driver import BOOT_IMAGE_PURPOSE
21from provisioningserver.driver.os_ubuntu import (
22 COMMISIONING_DISTRO_SERIES,
23 COMMISIONING_DISTRO_SERIES_DEFAULT,
24 DISTRO_SERIES_CHOICES,
25 DISTRO_SERIES_DEFAULT,
26 UbuntuOS,
27 )
28
29
30class TestUbuntuOS(MAASTestCase):
31
32 def test_get_boot_image_purposes(self):
33 osystem = UbuntuOS()
34 archs = [factory.make_name('arch') for _ in range(2)]
35 subarchs = [factory.make_name('subarch') for _ in range(2)]
36 releases = [factory.make_name('release') for _ in range(2)]
37 labels = [factory.make_name('label') for _ in range(2)]
38 for arch, subarch, release, label in product(
39 archs, subarchs, releases, labels):
40 expected = osystem.get_boot_image_purposes(
41 arch, subarchs, release, label)
42 self.assertIsInstance(expected, list)
43 self.assertEqual(expected, [
44 BOOT_IMAGE_PURPOSE.COMMISSIONING,
45 BOOT_IMAGE_PURPOSE.INSTALL,
46 BOOT_IMAGE_PURPOSE.XINSTALL
47 ])
48
49 def test_get_supported_releases(self):
50 osystem = UbuntuOS()
51 expected = osystem.get_supported_releases()
52 self.assertIsInstance(expected, list)
53 self.assertEqual(expected, DISTRO_SERIES_CHOICES.keys())
54
55 def test_get_default_release(self):
56 osystem = UbuntuOS()
57 expected = osystem.get_default_release()
58 self.assertEqual(expected, DISTRO_SERIES_DEFAULT)
59
60 def test_get_supported_commissioning_releases(self):
61 osystem = UbuntuOS()
62 expected = osystem.get_supported_commissioning_releases()
63 self.assertIsInstance(expected, list)
64 self.assertEqual(expected, COMMISIONING_DISTRO_SERIES)
65
66 def test_default_commissioning_release(self):
67 osystem = UbuntuOS()
68 expected = osystem.get_default_commissioning_release()
69 self.assertEqual(expected, COMMISIONING_DISTRO_SERIES_DEFAULT)
70
71 def test_format_release_choices(self):
72 osystem = UbuntuOS()
73 releases = osystem.get_supported_releases()
74 formatted = osystem.format_release_choices(releases)
75 for name, title in formatted:
76 self.assertEqual(DISTRO_SERIES_CHOICES[name], title)
077
=== modified file 'src/provisioningserver/driver/tests/test_registries.py'
--- src/provisioningserver/driver/tests/test_registries.py 2014-03-28 16:46:55 +0000
+++ src/provisioningserver/driver/tests/test_registries.py 2014-05-13 18:46:09 +0000
@@ -20,6 +20,7 @@
20 Architecture,20 Architecture,
21 ArchitectureRegistry,21 ArchitectureRegistry,
22 BootResourceRegistry,22 BootResourceRegistry,
23 OperatingSystemRegistry,
23 PowerTypeRegistry,24 PowerTypeRegistry,
24 )25 )
25from provisioningserver.utils.testing import RegistryFixture26from provisioningserver.utils.testing import RegistryFixture
@@ -68,6 +69,13 @@
68 self.assertEqual(69 self.assertEqual(
69 None, ArchitectureRegistry.get_by_pxealias("stinkywinky"))70 None, ArchitectureRegistry.get_by_pxealias("stinkywinky"))
7071
72 def test_operating_system_registry(self):
73 self.assertItemsEqual([], OperatingSystemRegistry)
74 OperatingSystemRegistry.register_item("resource", sentinel.resource)
75 self.assertIn(
76 sentinel.resource,
77 (item for name, item in OperatingSystemRegistry))
78
71 def test_power_type_registry(self):79 def test_power_type_registry(self):
72 self.assertItemsEqual([], PowerTypeRegistry)80 self.assertItemsEqual([], PowerTypeRegistry)
73 PowerTypeRegistry.register_item("resource", sentinel.resource)81 PowerTypeRegistry.register_item("resource", sentinel.resource)
7482
=== modified file 'src/provisioningserver/rpc/tests/test_clusterservice.py'
--- src/provisioningserver/rpc/tests/test_clusterservice.py 2014-05-13 18:43:40 +0000
+++ src/provisioningserver/rpc/tests/test_clusterservice.py 2014-05-13 18:46:09 +0000
@@ -34,6 +34,7 @@
34 sentinel,34 sentinel,
35 )35 )
36from provisioningserver.boot import tftppath36from provisioningserver.boot import tftppath
37from provisioningserver.boot.tests.test_tftppath import make_osystem
37from provisioningserver.power_schema import JSON_POWER_TYPE_PARAMETERS38from provisioningserver.power_schema import JSON_POWER_TYPE_PARAMETERS
38from provisioningserver.rpc import (39from provisioningserver.rpc import (
39 cluster,40 cluster,
@@ -179,6 +180,7 @@
179 tftpdir = self.make_dir()180 tftpdir = self.make_dir()
180 for options in product(osystems, archs, subarchs, releases, labels):181 for options in product(osystems, archs, subarchs, releases, labels):
181 os.makedirs(os.path.join(tftpdir, *options))182 os.makedirs(os.path.join(tftpdir, *options))
183 make_osystem(self, options[0], purposes)
182184
183 # Ensure that list_boot_images() uses the above TFTP file tree.185 # Ensure that list_boot_images() uses the above TFTP file tree.
184 self.useFixture(set_tftp_root(tftpdir))186 self.useFixture(set_tftp_root(tftpdir))