Merge lp:~blake-rouse/maas/add-osystem-to-bootimage into lp:maas

Proposed by Blake Rouse
Status: Merged
Approved by: Blake Rouse
Approved revision: 2289
Merged at revision: 2313
Proposed branch: lp:~blake-rouse/maas/add-osystem-to-bootimage
Merge into: lp:maas
Diff against target: 1431 lines (+546/-111)
26 files modified
src/maasserver/api.py (+21/-17)
src/maasserver/migrations/0076_add_osystem_to_bootimage.py (+275/-0)
src/maasserver/models/bootimage.py (+41/-20)
src/maasserver/models/tests/test_bootimage.py (+100/-20)
src/maasserver/preseed.py (+5/-2)
src/maasserver/templates/maasserver/bootimage-list.html (+2/-0)
src/maasserver/testing/factory.py (+6/-3)
src/maasserver/tests/test_api_boot_images.py (+4/-1)
src/maasserver/tests/test_api_pxeconfig.py (+2/-0)
src/maasserver/tests/test_preseed.py (+13/-5)
src/maasserver/views/clusters.py (+2/-2)
src/maasserver/views/tests/test_boot_image_list.py (+1/-2)
src/metadataserver/tests/test_api.py (+1/-0)
src/provisioningserver/boot/__init__.py (+1/-1)
src/provisioningserver/boot/tests/test_pxe.py (+3/-2)
src/provisioningserver/boot/tests/test_tftppath.py (+6/-3)
src/provisioningserver/boot/tests/test_uefi.py (+1/-1)
src/provisioningserver/boot/tftppath.py (+11/-10)
src/provisioningserver/import_images/boot_resources.py (+16/-6)
src/provisioningserver/import_images/download_resources.py (+2/-1)
src/provisioningserver/import_images/tests/test_boot_resources.py (+5/-3)
src/provisioningserver/kernel_opts.py (+12/-4)
src/provisioningserver/rpc/cluster.py (+2/-1)
src/provisioningserver/rpc/tests/test_clusterservice.py (+5/-3)
src/provisioningserver/testing/boot_images.py (+5/-2)
src/provisioningserver/tests/test_kernel_opts.py (+4/-2)
To merge this branch: bzr merge lp:~blake-rouse/maas/add-osystem-to-bootimage
Reviewer Review Type Date Requested Status
Julian Edwards (community) Approve
Review via email: mp+216923@code.launchpad.net

Commit message

Adds an operating system field 'osystem' to the BootImage model and restructures the boot-resources folder to add another level for osystem.

Description of the change

This is the first 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 operating system field 'osystem' to the BootImage model. Selection of a BootImage is now based on the operating system to be booted. Currently this is hard coded to 'ubuntu', following changes remove this hard code. The WebUI is updated to show the 'osystem' field in the BootImage list.

The folder structure of the boot-resources folder has also added another level. Currently it is structured arch/subarch/release/label, the new structure puts those folders inside a folder with the operating system name. So the new structure is osystem/arch/subarch/release/label (e.g. ubuntu/amd64/generic/trusty/release).

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 :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

 review: approve

Wow, adding something as simple as an OS name was quite a lot of
changes! Thanks for doing this, it's a very nice branch.

I have a couple of niggles but it looks mostly OK to me. Please land
it once these are addressed.

[1]

> + def get_usable_osystems(self, nodegroup): + """Return
> the list of usable operating systems for a nodegroup. + """
> + query = BootImage.objects.filter(nodegroup=nodegroup) +
> return set(query.values_list('osystem', flat=True)) + + def
> get_usable_releases(self, nodegroup, osystem): + """Return
> the list of usable releases for a nodegroup and + operating
> system. + """ + query =
> BootImage.objects.filter(nodegroup=nodegroup, osystem=osystem) +
> releases = query.values_list('release', flat=True) + return
> set(releases)

Can you add tests for these new functions please. Also they are not
actually used anywhere, I presume a follow up branch makes use of them?

[2]

Please run "make lint" and fix all the errors it finds.

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1
Comment: Using GnuPG with Thunderbird - http://www.enigmail.net/

iEYEARECAAYFAlNYfxYACgkQWhGlTF8G/HefwwCfWwiomUOBgXKO3Z4mf/YGEfpL
VaEAniGAr/MpfihFXHwJBKOekqD2KGOp
=VxoG
-----END PGP SIGNATURE-----

review: Approve
Revision history for this message
Andres Rodriguez (andreserl) wrote :

BTW... How will this affect upgrades? My concern is that if we backport
this after upgrades things will be broken because the location of the
bootimages has now changed right? What should we do about it? Julian, Gavin
any thoughts?
On Apr 23, 2014 11:06 PM, "Julian Edwards" <email address hidden>
wrote:

> Review: Approve
>
> -----BEGIN PGP SIGNED MESSAGE-----
> Hash: SHA1
>
> review: approve
>
> Wow, adding something as simple as an OS name was quite a lot of
> changes! Thanks for doing this, it's a very nice branch.
>
> I have a couple of niggles but it looks mostly OK to me. Please land
> it once these are addressed.
>
> [1]
>
> > + def get_usable_osystems(self, nodegroup): + """Return
> > the list of usable operating systems for a nodegroup. + """
> > + query = BootImage.objects.filter(nodegroup=nodegroup) +
> > return set(query.values_list('osystem', flat=True)) + + def
> > get_usable_releases(self, nodegroup, osystem): + """Return
> > the list of usable releases for a nodegroup and + operating
> > system. + """ + query =
> > BootImage.objects.filter(nodegroup=nodegroup, osystem=osystem) +
> > releases = query.values_list('release', flat=True) + return
> > set(releases)
>
> Can you add tests for these new functions please. Also they are not
> actually used anywhere, I presume a follow up branch makes use of them?
>
> [2]
>
> Please run "make lint" and fix all the errors it finds.
>
>
> -----BEGIN PGP SIGNATURE-----
> Version: GnuPG v1
> Comment: Using GnuPG with Thunderbird - http://www.enigmail.net/
>
> iEYEARECAAYFAlNYfxYACgkQWhGlTF8G/HefwwCfWwiomUOBgXKO3Z4mf/YGEfpL
> VaEAniGAr/MpfihFXHwJBKOekqD2KGOp
> =VxoG
> -----END PGP SIGNATURE-----
>
> --
>
> https://code.launchpad.net/~blake-rouse/maas/add-osystem-to-bootimage/+merge/216923
> You are subscribed to branch lp:maas.
>

2287. By Blake Rouse

Added unittest for get_usable_osystems and get_usable_releases. Fixed lint issues.

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

I added test for get_usable_osystems and get_useable_releases, these methods get used in the following branches.

Andres you are correct in an issue on upgrade, this should be the only upgrade issue I can think of out of all of these branches. I don't think it would be to hard to handle, as we know the only operating system that is in boot-resources folder is Ubuntu.

So on upgrade move all folders in the boot-resources folder into a sub-folder named ubuntu, except for the grub folder it should not move. The migration already sets all BootImage items to ubuntu, so nothing more need to be done there. Only thing left to do is update the maas.tgt, with ubuntu in the path.

Revision history for this message
Andres Rodriguez (andreserl) wrote :

So what can we do to address this? This might also break fast path installer when it goes to download the root tarball because it looks for the tarball in a location that does not expect to have the osrelease. Gavin, Julian, do you think we can just avoid using osrelease in the boot-resources path? Or what can we do to move this forward and not break things?

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

It should not break fast path installer as all of those paths now include the operating system first. So upon installation the preseed that is sent will have the correct path including the operating system.

If you remove the os from the path, you make it really hard for the boot resources to be identified for other operating systems. As that first path should be the operating system.

Revision history for this message
Gavin Panella (allenap) wrote :

What's going to move those boot resources? Can it be done just-in-time? Or do we need to arrange for a run of an import?

My feeling is that we should empty the BootImages table in the migration (both ways) and schedule imports to run.

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On 25/04/14 20:24, Gavin Panella wrote:
> What's going to move those boot resources? Can it be done just-in-time? Or do we need to arrange for a run of an import?
>
> My feeling is that we should empty the BootImages table in the migration (both ways) and schedule imports to run.
>

We have the per-cluster built-in migration now, which is called on every
start-up. It can easily shuffle files around as needed, and issue API
calls to update the cluster's BootImages.

Revision history for this message
Gavin Panella (allenap) wrote :

> We have the per-cluster built-in migration now, which is called on every
> start-up. It can easily shuffle files around as needed, and issue API
> calls to update the cluster's BootImages.

I had forgotten about that. Indeed, that's the better solution.

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On 28/04/14 16:40, Gavin Panella wrote:
>> We have the per-cluster built-in migration now, which is called on every
>> start-up. It can easily shuffle files around as needed, and issue API
>> calls to update the cluster's BootImages.
>
> I had forgotten about that. Indeed, that's the better solution.
>

I should mention that we need to make doubly sure that this stuff gets
called from the right charm hooks!

2288. By Blake Rouse

Merge trunk.

2289. By Blake Rouse

Fix broken migration.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/api.py'
2--- src/maasserver/api.py 2014-04-28 07:41:51 +0000
3+++ src/maasserver/api.py 2014-05-02 19:22:04 +0000
4@@ -2440,7 +2440,7 @@
5 # current series. If nothing is found, fall back to i386 like
6 # we used to. LP #1181334
7 image = BootImage.objects.get_default_arch_image_in_nodegroup(
8- nodegroup, series, purpose=purpose)
9+ nodegroup, 'ubuntu', series, purpose=purpose)
10 if image is None:
11 arch = 'i386'
12 else:
13@@ -2453,7 +2453,7 @@
14 # (which should never happen in reality but may happen in tests), we
15 # fall back to using 'no-such-image' as our default.
16 latest_image = BootImage.objects.get_latest_image(
17- nodegroup, arch, subarch, series, purpose)
18+ nodegroup, 'ubuntu', arch, subarch, series, purpose)
19 if latest_image is None:
20 # XXX 2014-03-18 gmb bug=1294131:
21 # We really ought to raise an exception here so that client
22@@ -2494,8 +2494,8 @@
23 cluster_address = get_mandatory_param(request.GET, "local")
24
25 params = KernelParameters(
26- arch=arch, subarch=subarch, release=series, label=label,
27- purpose=purpose, hostname=hostname, domain=domain,
28+ osystem='ubuntu', arch=arch, subarch=subarch, release=series,
29+ label=label, purpose=purpose, hostname=hostname, domain=domain,
30 preseed_url=preseed_url, log_host=server_address,
31 fs_host=cluster_address, extra_opts=extra_kernel_opts)
32
33@@ -2536,10 +2536,11 @@
34 This function has a counterpart, `summarise_boot_image_dict`. The two
35 return the same value for the same boot image.
36
37- :return: A tuple of the image's architecture, subarchitecture, release,
38- label, and purpose.
39+ :return: A tuple of the image's osystem, architecture, subarchitecture,
40+ release, label, and purpose.
41 """
42 return (
43+ image_object.osystem,
44 image_object.architecture,
45 image_object.subarchitecture,
46 image_object.release,
47@@ -2554,10 +2555,11 @@
48 This is the counterpart to `summarise_boot_image_object`. The two return
49 the same value for the same boot image.
50
51- :return: A tuple of the image's architecture, subarchitecture, release,
52- label, and purpose.
53+ :return: A tuple of the image's osystem, architecture, subarchitecture,
54+ release, label, and purpose.
55 """
56 return (
57+ image_dict['osystem'],
58 image_dict['architecture'],
59 image_dict.get('subarchitecture', 'generic'),
60 image_dict['release'],
61@@ -2600,10 +2602,11 @@
62 `summarise_stored_images`.
63 """
64 new_images = reported_images - stored_images
65- for arch, subarch, release, label, purpose in new_images:
66+ for osystem, arch, subarch, release, label, purpose in new_images:
67 BootImage.objects.register_image(
68- nodegroup=nodegroup, architecture=arch, subarchitecture=subarch,
69- release=release, purpose=purpose, label=label)
70+ nodegroup=nodegroup, osystem=osystem, architecture=arch,
71+ subarchitecture=subarch, release=release, purpose=purpose,
72+ label=label)
73
74
75 def prune_boot_images(nodegroup, reported_images, stored_images):
76@@ -2620,15 +2623,16 @@
77 `summarise_stored_images`.
78 """
79 removed_images = stored_images - reported_images
80- for arch, subarch, release, label, purpose in removed_images:
81+ for osystem, arch, subarch, release, label, purpose in removed_images:
82 db_images = BootImage.objects.filter(
83- architecture=arch, subarchitecture=subarch,
84+ osystem=osystem, architecture=arch, subarchitecture=subarch,
85 release=release, label=label, purpose=purpose)
86 db_images.delete()
87
88
89 DISPLAYED_BOOTIMAGE_FIELDS = (
90 'id',
91+ 'osystem',
92 'release',
93 'architecture',
94 'subarchitecture',
95@@ -2694,10 +2698,10 @@
96 :param uuid: The UUID of the cluster for which the images are
97 being reported.
98 :param images: A list of dicts, each describing a boot image with
99- these properties: `architecture`, `subarchitecture`, `release`,
100- `purpose`, and optionally, `label` (which defaults to "release").
101- These should match the code that determines TFTP paths for these
102- images.
103+ these properties: `os`, `architecture`, `subarchitecture`,
104+ `release`, `purpose`, and optionally, `label` (which defaults
105+ to "release"). These should match the code that determines TFTP
106+ paths for these images.
107 """
108 nodegroup = get_object_or_404(NodeGroup, uuid=uuid)
109 check_nodegroup_access(request, nodegroup)
110
111=== added file 'src/maasserver/migrations/0076_add_osystem_to_bootimage.py'
112--- src/maasserver/migrations/0076_add_osystem_to_bootimage.py 1970-01-01 00:00:00 +0000
113+++ src/maasserver/migrations/0076_add_osystem_to_bootimage.py 2014-05-02 19:22:04 +0000
114@@ -0,0 +1,275 @@
115+# -*- coding: utf-8 -*-
116+from south.utils import datetime_utils as datetime
117+from south.db import db
118+from south.v2 import SchemaMigration
119+from django.db import models
120+
121+
122+class Migration(SchemaMigration):
123+
124+ def forwards(self, orm):
125+ # Removing unique constraint on 'BootImage', fields ['subarchitecture', 'label', 'architecture', 'release', 'nodegroup', 'purpose']
126+ db.delete_unique(u'maasserver_bootimage', ['subarchitecture', 'label', 'architecture', 'release', 'nodegroup_id', 'purpose'])
127+
128+ # Adding field 'BootImage.osystem'
129+ db.add_column(u'maasserver_bootimage', 'osystem',
130+ self.gf('django.db.models.fields.CharField')(default='ubuntu', max_length=255),
131+ keep_default=False)
132+
133+ # Adding unique constraint on 'BootImage', fields ['subarchitecture', 'osystem', 'label', 'architecture', 'release', 'nodegroup', 'purpose']
134+ db.create_unique(u'maasserver_bootimage', ['subarchitecture', 'osystem', 'label', 'architecture', 'release', 'nodegroup_id', 'purpose'])
135+
136+
137+ def backwards(self, orm):
138+ # Removing unique constraint on 'BootImage', fields ['subarchitecture', 'osystem', 'label', 'architecture', 'release', 'nodegroup', 'purpose']
139+ db.delete_unique(u'maasserver_bootimage', ['subarchitecture', 'osystem', 'label', 'architecture', 'release', 'nodegroup_id', 'purpose'])
140+
141+ # Deleting field 'BootImage.osystem'
142+ db.delete_column(u'maasserver_bootimage', 'osystem')
143+
144+ # Adding unique constraint on 'BootImage', fields ['subarchitecture', 'label', 'architecture', 'release', 'nodegroup', 'purpose']
145+ db.create_unique(u'maasserver_bootimage', ['subarchitecture', 'label', 'architecture', 'release', 'nodegroup_id', 'purpose'])
146+
147+
148+ models = {
149+ u'auth.group': {
150+ 'Meta': {'object_name': 'Group'},
151+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
152+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
153+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
154+ },
155+ u'auth.permission': {
156+ 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
157+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
158+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
159+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
160+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
161+ },
162+ u'auth.user': {
163+ 'Meta': {'object_name': 'User'},
164+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
165+ 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75', 'blank': 'True'}),
166+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
167+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
168+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
169+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
170+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
171+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
172+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
173+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
174+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
175+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
176+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
177+ },
178+ u'contenttypes.contenttype': {
179+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
180+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
181+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
182+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
183+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
184+ },
185+ u'maasserver.bootimage': {
186+ 'Meta': {'unique_together': "((u'nodegroup', u'osystem', u'architecture', u'subarchitecture', u'release', u'purpose', u'label'),)", 'object_name': 'BootImage'},
187+ 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
188+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
189+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
190+ 'label': ('django.db.models.fields.CharField', [], {'default': "u'release'", 'max_length': '255'}),
191+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
192+ 'osystem': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
193+ 'purpose': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
194+ 'release': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
195+ 'subarchitecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
196+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
197+ },
198+ u'maasserver.bootsource': {
199+ 'Meta': {'object_name': 'BootSource'},
200+ 'cluster': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']", 'null': 'True'}),
201+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
202+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
203+ 'keyring_data': ('django.db.models.fields.BinaryField', [], {'blank': 'True'}),
204+ 'keyring_filename': ('django.db.models.fields.FilePathField', [], {'max_length': '100', 'blank': 'True'}),
205+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
206+ 'url': ('django.db.models.fields.URLField', [], {'max_length': '200'})
207+ },
208+ u'maasserver.bootsourceselection': {
209+ 'Meta': {'object_name': 'BootSourceSelection'},
210+ 'arches': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'text'", 'null': 'True', 'blank': 'True'}),
211+ 'boot_source': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.BootSource']"}),
212+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
213+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
214+ 'labels': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'text'", 'null': 'True', 'blank': 'True'}),
215+ 'release': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'null': 'True', 'blank': 'True'}),
216+ 'subarches': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'text'", 'null': 'True', 'blank': 'True'}),
217+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
218+ },
219+ u'maasserver.componenterror': {
220+ 'Meta': {'object_name': 'ComponentError'},
221+ 'component': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
222+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
223+ 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000'}),
224+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
225+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
226+ },
227+ u'maasserver.config': {
228+ 'Meta': {'object_name': 'Config'},
229+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
230+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
231+ 'value': ('maasserver.fields.JSONObjectField', [], {'null': 'True'})
232+ },
233+ u'maasserver.dhcplease': {
234+ 'Meta': {'object_name': 'DHCPLease'},
235+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
236+ 'ip': ('django.db.models.fields.IPAddressField', [], {'unique': 'True', 'max_length': '15'}),
237+ 'mac': ('maasserver.fields.MACAddressField', [], {}),
238+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"})
239+ },
240+ u'maasserver.downloadprogress': {
241+ 'Meta': {'object_name': 'DownloadProgress'},
242+ 'bytes_downloaded': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
243+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
244+ 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000', 'blank': 'True'}),
245+ 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
246+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
247+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
248+ 'size': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
249+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
250+ },
251+ u'maasserver.filestorage': {
252+ 'Meta': {'unique_together': "((u'filename', u'owner'),)", 'object_name': 'FileStorage'},
253+ 'content': ('metadataserver.fields.BinaryField', [], {'blank': 'True'}),
254+ 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
255+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
256+ 'key': ('django.db.models.fields.CharField', [], {'default': "u'26215e0a-cafa-11e3-8554-bcee7b78dc5b'", 'unique': 'True', 'max_length': '36'}),
257+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
258+ },
259+ u'maasserver.macaddress': {
260+ 'Meta': {'object_name': 'MACAddress'},
261+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
262+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
263+ 'mac_address': ('maasserver.fields.MACAddressField', [], {'unique': 'True'}),
264+ 'networks': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Network']", 'symmetrical': 'False', 'blank': 'True'}),
265+ 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}),
266+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
267+ },
268+ u'maasserver.network': {
269+ 'Meta': {'object_name': 'Network'},
270+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
271+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
272+ 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'unique': 'True', 'max_length': '39'}),
273+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
274+ 'netmask': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
275+ 'vlan_tag': ('django.db.models.fields.PositiveSmallIntegerField', [], {'unique': 'True', 'null': 'True', 'blank': 'True'})
276+ },
277+ u'maasserver.node': {
278+ 'Meta': {'object_name': 'Node'},
279+ 'agent_name': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'null': 'True', 'blank': 'True'}),
280+ 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '31'}),
281+ 'cpu_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
282+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
283+ 'distro_series': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'null': 'True', 'blank': 'True'}),
284+ 'error': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
285+ 'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'unique': 'True', 'max_length': '255', 'blank': 'True'}),
286+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
287+ 'memory': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
288+ 'netboot': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
289+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']", 'null': 'True'}),
290+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}),
291+ 'power_parameters': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
292+ 'power_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '10', 'blank': 'True'}),
293+ 'routers': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'macaddr'", 'null': 'True', 'blank': 'True'}),
294+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '10'}),
295+ 'storage': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
296+ 'system_id': ('django.db.models.fields.CharField', [], {'default': "u'node-26226a84-cafa-11e3-8554-bcee7b78dc5b'", 'unique': 'True', 'max_length': '41'}),
297+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Tag']", 'symmetrical': 'False'}),
298+ 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Token']", 'null': 'True'}),
299+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
300+ 'zone': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Zone']", 'on_delete': 'models.SET_DEFAULT'})
301+ },
302+ u'maasserver.nodegroup': {
303+ 'Meta': {'object_name': 'NodeGroup'},
304+ 'api_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '18'}),
305+ 'api_token': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Token']", 'unique': 'True'}),
306+ 'cluster_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100', 'blank': 'True'}),
307+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
308+ 'dhcp_key': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
309+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
310+ 'maas_url': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
311+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}),
312+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
313+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
314+ 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'})
315+ },
316+ u'maasserver.nodegroupinterface': {
317+ 'Meta': {'unique_together': "((u'nodegroup', u'interface'),)", 'object_name': 'NodeGroupInterface'},
318+ 'broadcast_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
319+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
320+ 'foreign_dhcp_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
321+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
322+ 'interface': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
323+ 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
324+ 'ip_range_high': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
325+ 'ip_range_low': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
326+ 'management': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
327+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
328+ 'router_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
329+ 'subnet_mask': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
330+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
331+ },
332+ u'maasserver.sshkey': {
333+ 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSHKey'},
334+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
335+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
336+ 'key': ('django.db.models.fields.TextField', [], {}),
337+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
338+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
339+ },
340+ u'maasserver.tag': {
341+ 'Meta': {'object_name': 'Tag'},
342+ 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
343+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
344+ 'definition': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
345+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
346+ 'kernel_opts': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
347+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
348+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
349+ },
350+ u'maasserver.userprofile': {
351+ 'Meta': {'object_name': 'UserProfile'},
352+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
353+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'})
354+ },
355+ u'maasserver.zone': {
356+ 'Meta': {'ordering': "[u'name']", 'object_name': 'Zone'},
357+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
358+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
359+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
360+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
361+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
362+ },
363+ u'piston.consumer': {
364+ 'Meta': {'object_name': 'Consumer'},
365+ 'description': ('django.db.models.fields.TextField', [], {}),
366+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
367+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
368+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
369+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
370+ 'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '16'}),
371+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'consumers'", 'null': 'True', 'to': u"orm['auth.User']"})
372+ },
373+ u'piston.token': {
374+ 'Meta': {'object_name': 'Token'},
375+ 'callback': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
376+ 'callback_confirmed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
377+ 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Consumer']"}),
378+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
379+ 'is_approved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
380+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
381+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
382+ 'timestamp': ('django.db.models.fields.IntegerField', [], {'default': '1398266138L'}),
383+ 'token_type': ('django.db.models.fields.IntegerField', [], {}),
384+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'tokens'", 'null': 'True', 'to': u"orm['auth.User']"}),
385+ 'verifier': ('django.db.models.fields.CharField', [], {'max_length': '10'})
386+ }
387+ }
388+
389+ complete_apps = ['maasserver']
390\ No newline at end of file
391
392=== modified file 'src/maasserver/models/bootimage.py'
393--- src/maasserver/models/bootimage.py 2014-03-26 16:01:34 +0000
394+++ src/maasserver/models/bootimage.py 2014-05-02 19:22:04 +0000
395@@ -34,44 +34,47 @@
396 Don't import or instantiate this directly; access as `BootImage.objects`.
397 """
398
399- def get_by_natural_key(self, nodegroup, architecture, subarchitecture,
400- release, purpose, label):
401+ def get_by_natural_key(self, nodegroup, osystem, architecture,
402+ subarchitecture, release, purpose, label):
403 """Look up a specific image."""
404 return self.get(
405- nodegroup=nodegroup, architecture=architecture,
406+ nodegroup=nodegroup, osystem=osystem, architecture=architecture,
407 subarchitecture=subarchitecture, release=release,
408 purpose=purpose, label=label)
409
410- def register_image(self, nodegroup, architecture, subarchitecture,
411+ def register_image(self, nodegroup, osystem, architecture, subarchitecture,
412 release, purpose, label):
413 """Register an image if it wasn't already registered."""
414 self.get_or_create(
415- nodegroup=nodegroup, architecture=architecture,
416+ nodegroup=nodegroup, osystem=osystem, architecture=architecture,
417 subarchitecture=subarchitecture, release=release,
418 purpose=purpose, label=label)
419
420- def have_image(self, nodegroup, architecture, subarchitecture, release,
421- purpose, label=None):
422+ def have_image(self, nodegroup, osystem, architecture, subarchitecture,
423+ release, purpose, label=None):
424 """Is an image for the given kind of boot available?"""
425 if label is None:
426 label = "release"
427 try:
428 self.get_by_natural_key(
429- nodegroup=nodegroup, architecture=architecture,
430- subarchitecture=subarchitecture, release=release,
431- purpose=purpose, label=label)
432+ nodegroup=nodegroup, osystem=osystem,
433+ architecture=architecture, subarchitecture=subarchitecture,
434+ release=release, purpose=purpose, label=label)
435 return True
436 except BootImage.DoesNotExist:
437 return False
438
439- def get_default_arch_image_in_nodegroup(self, nodegroup, series, purpose):
440- """Return any image for the given nodegroup, series, and purpose.
441+ def get_default_arch_image_in_nodegroup(self, nodegroup, osystem, series,
442+ purpose):
443+ """Return any image for the given nodegroup, osystem, series,
444+ and purpose.
445
446 Prefers `i386` images if available. Returns `None` if no images match
447 requirements.
448 """
449 images = BootImage.objects.filter(
450- release=series, nodegroup=nodegroup, purpose=purpose)
451+ osystem=osystem, release=series, nodegroup=nodegroup,
452+ purpose=purpose)
453 for image in images:
454 # Prefer i386, any available subarchitecture (usually just
455 # "generic"). It will work for most cases where we don't know
456@@ -106,14 +109,28 @@
457 nodegroup, 'install')
458 return arches_commissioning & arches_install
459
460- def get_latest_image(self, nodegroup, architecture, subarchitecture,
461- release, purpose):
462+ def get_latest_image(self, nodegroup, osystem, architecture,
463+ subarchitecture, release, purpose):
464 """Return the latest image for a set of criteria."""
465 return BootImage.objects.filter(
466- nodegroup=nodegroup, architecture=architecture,
467+ nodegroup=nodegroup, osystem=osystem, architecture=architecture,
468 subarchitecture=subarchitecture, release=release,
469 purpose=purpose).order_by('id').last()
470
471+ def get_usable_osystems(self, nodegroup):
472+ """Return the list of usable operating systems for a nodegroup.
473+ """
474+ query = BootImage.objects.filter(nodegroup=nodegroup)
475+ return set(query.values_list('osystem', flat=True))
476+
477+ def get_usable_releases(self, nodegroup, osystem):
478+ """Return the list of usable releases for a nodegroup and
479+ operating system.
480+ """
481+ query = BootImage.objects.filter(nodegroup=nodegroup, osystem=osystem)
482+ releases = query.values_list('release', flat=True)
483+ return set(releases)
484+
485
486 class BootImage(TimestampedModel):
487 """Available boot image (i.e. kernel and initrd).
488@@ -131,8 +148,8 @@
489
490 class Meta(DefaultMeta):
491 unique_together = (
492- ('nodegroup', 'architecture', 'subarchitecture', 'release',
493- 'purpose', 'label'),
494+ ('nodegroup', 'osystem', 'architecture', 'subarchitecture',
495+ 'release', 'purpose', 'label'),
496 )
497
498 objects = BootImageManager()
499@@ -140,6 +157,9 @@
500 # Nodegroup (cluster controller) that has the images.
501 nodegroup = ForeignKey(NodeGroup, null=False, editable=False, unique=False)
502
503+ # Operating system (e.g. "ubuntu") that the image boots.
504+ osystem = CharField(max_length=255, blank=False, editable=False)
505+
506 # System architecture (e.g. "i386") that the image is for.
507 architecture = CharField(max_length=255, blank=False, editable=False)
508
509@@ -148,7 +168,7 @@
510 # such as i386 and amd64, we use "generic").
511 subarchitecture = CharField(max_length=255, blank=False, editable=False)
512
513- # Ubuntu release (e.g. "precise") that the image boots.
514+ # OS release (e.g. "precise") that the image boots.
515 release = CharField(max_length=255, blank=False, editable=False)
516
517 # Boot purpose (e.g. "commissioning" or "install") that the image is for.
518@@ -159,7 +179,8 @@
519 max_length=255, blank=False, editable=False, default="release")
520
521 def __repr__(self):
522- return "<BootImage %s/%s-%s-%s-%s>" % (
523+ return "<BootImage %s-%s/%s-%s-%s-%s>" % (
524+ self.osystem,
525 self.architecture,
526 self.subarchitecture,
527 self.release,
528
529=== modified file 'src/maasserver/models/tests/test_bootimage.py'
530--- src/maasserver/models/tests/test_bootimage.py 2014-03-18 14:39:11 +0000
531+++ src/maasserver/models/tests/test_bootimage.py 2014-05-02 19:22:04 +0000
532@@ -50,69 +50,79 @@
533 self.assertTrue(BootImage.objects.have_image(nodegroup, **params))
534
535 def test_default_arch_image_returns_None_if_no_images_match(self):
536+ osystem = Config.objects.get_config('commissioning_osystem')
537 series = Config.objects.get_config('commissioning_distro_series')
538 result = BootImage.objects.get_default_arch_image_in_nodegroup(
539- factory.make_node_group(), series, factory.make_name('purpose'))
540+ factory.make_node_group(), osystem, series,
541+ factory.make_name('purpose'))
542 self.assertIsNone(result)
543
544 def test_default_arch_image_returns_only_matching_image(self):
545 nodegroup = factory.make_node_group()
546+ osystem = factory.make_name('os')
547 series = factory.make_name('series')
548 label = factory.make_name('label')
549 arch = factory.make_name('arch')
550 purpose = factory.make_name("purpose")
551 factory.make_boot_image(
552- architecture=arch, release=series, label=label,
553+ osystem=osystem, architecture=arch,
554+ release=series, label=label,
555 nodegroup=nodegroup, purpose=purpose)
556 result = BootImage.objects.get_default_arch_image_in_nodegroup(
557- nodegroup, series, purpose=purpose)
558+ nodegroup, osystem, series, purpose=purpose)
559 self.assertEqual(result.architecture, arch)
560
561 def test_default_arch_image_prefers_i386(self):
562 nodegroup = factory.make_node_group()
563+ osystem = factory.make_name('os')
564 series = factory.make_name('series')
565 label = factory.make_name('label')
566 purpose = factory.make_name("purpose")
567 for arch in ['amd64', 'axp', 'i386', 'm88k']:
568 factory.make_boot_image(
569- architecture=arch, release=series, nodegroup=nodegroup,
570+ osystem=osystem, architecture=arch,
571+ release=series, nodegroup=nodegroup,
572 purpose=purpose, label=label)
573 result = BootImage.objects.get_default_arch_image_in_nodegroup(
574- nodegroup, series, purpose=purpose)
575+ nodegroup, osystem, series, purpose=purpose)
576 self.assertEqual(result.architecture, "i386")
577
578 def test_default_arch_image_returns_arbitrary_pick_if_all_else_fails(self):
579 nodegroup = factory.make_node_group()
580+ osystem = factory.make_name('os')
581 series = factory.make_name('series')
582 label = factory.make_name('label')
583 purpose = factory.make_name("purpose")
584 images = [
585 factory.make_boot_image(
586- architecture=factory.make_name('arch'), release=series,
587- label=label, nodegroup=nodegroup, purpose=purpose)
588+ osystem=osystem, architecture=factory.make_name('arch'),
589+ release=series, label=label, nodegroup=nodegroup,
590+ purpose=purpose)
591 for _ in range(3)
592 ]
593 self.assertIn(
594 BootImage.objects.get_default_arch_image_in_nodegroup(
595- nodegroup, series, purpose=purpose),
596+ nodegroup, osystem, series, purpose=purpose),
597 images)
598
599 def test_default_arch_image_copes_with_subarches(self):
600 nodegroup = factory.make_node_group()
601 arch = 'i386'
602+ osystem = factory.make_name('os')
603 series = factory.make_name('series')
604 label = factory.make_name('label')
605 purpose = factory.make_name("purpose")
606 images = [
607 factory.make_boot_image(
608- architecture=arch, subarchitecture=factory.make_name('sub'),
609+ osystem=osystem, architecture=arch,
610+ subarchitecture=factory.make_name('sub'),
611 release=series, label=label, nodegroup=nodegroup,
612 purpose=purpose)
613 for _ in range(3)
614 ]
615 self.assertIn(
616 BootImage.objects.get_default_arch_image_in_nodegroup(
617- nodegroup, series, purpose=purpose),
618+ nodegroup, osystem, series, purpose=purpose),
619 images)
620
621 def test_get_usable_architectures_returns_supported_arches(self):
622@@ -164,54 +174,62 @@
623 BootImage.objects.get_usable_architectures(nodegroup))
624
625 def test_get_latest_image_returns_latest_image_for_criteria(self):
626+ osystem = factory.make_name('os')
627 arch = factory.make_name('arch')
628 subarch = factory.make_name('sub')
629 release = factory.make_name('release')
630 nodegroup = factory.make_node_group()
631 purpose = factory.make_name("purpose")
632 boot_image = factory.make_boot_image(
633- nodegroup=nodegroup, architecture=arch,
634+ nodegroup=nodegroup, osystem=osystem, architecture=arch,
635 subarchitecture=subarch, release=release, purpose=purpose,
636 label=factory.make_name('label'))
637 self.assertEqual(
638 boot_image,
639 BootImage.objects.get_latest_image(
640- nodegroup, arch, subarch, release, purpose))
641+ nodegroup, osystem, arch, subarch, release, purpose))
642
643 def test_get_latest_image_doesnt_return_images_for_other_purposes(self):
644+ osystem = factory.make_name('os')
645 arch = factory.make_name('arch')
646 subarch = factory.make_name('sub')
647 release = factory.make_name('release')
648 nodegroup = factory.make_node_group()
649 purpose = factory.make_name("purpose")
650 relevant_image = factory.make_boot_image(
651- nodegroup=nodegroup, architecture=arch,
652+ nodegroup=nodegroup, osystem=osystem, architecture=arch,
653 subarchitecture=subarch, release=release, purpose=purpose,
654 label=factory.make_name('label'))
655
656 # Create a bunch of more recent but irrelevant BootImages..
657 factory.make_boot_image(
658- nodegroup=factory.make_node_group(), architecture=arch,
659- subarchitecture=subarch, release=release,
660+ nodegroup=factory.make_node_group(), osystem=osystem,
661+ architecture=arch, subarchitecture=subarch, release=release,
662 purpose=purpose, label=factory.make_name('label'))
663 factory.make_boot_image(
664- nodegroup=nodegroup,
665+ nodegroup=nodegroup, osystem=osystem,
666 architecture=factory.make_name('arch'),
667 subarchitecture=subarch, release=release, purpose=purpose,
668 label=factory.make_name('label'))
669 factory.make_boot_image(
670- nodegroup=nodegroup, architecture=arch,
671+ nodegroup=nodegroup, osystem=osystem, architecture=arch,
672 subarchitecture=factory.make_name('subarch'),
673 release=release, purpose=purpose,
674 label=factory.make_name('label'))
675 factory.make_boot_image(
676- nodegroup=nodegroup,
677+ nodegroup=nodegroup, osystem=osystem,
678 architecture=factory.make_name('arch'),
679 subarchitecture=subarch,
680 release=factory.make_name('release'), purpose=purpose,
681 label=factory.make_name('label'))
682 factory.make_boot_image(
683- nodegroup=nodegroup,
684+ nodegroup=nodegroup, osystem=osystem,
685+ architecture=factory.make_name('arch'),
686+ subarchitecture=subarch, release=release,
687+ purpose=factory.make_name('purpose'),
688+ label=factory.make_name('label'))
689+ factory.make_boot_image(
690+ nodegroup=nodegroup, osystem=factory.make_name('os'),
691 architecture=factory.make_name('arch'),
692 subarchitecture=subarch, release=release,
693 purpose=factory.make_name('purpose'),
694@@ -220,4 +238,66 @@
695 self.assertEqual(
696 relevant_image,
697 BootImage.objects.get_latest_image(
698- nodegroup, arch, subarch, release, purpose))
699+ nodegroup, osystem, arch, subarch, release, purpose))
700+
701+ def test_get_usable_osystems_returns_supported_osystems(self):
702+ nodegroup = factory.make_node_group()
703+ osystems = [
704+ factory.make_name('os'),
705+ factory.make_name('os'),
706+ ]
707+ for osystem in osystems:
708+ factory.make_boot_image(
709+ osystem=osystem,
710+ nodegroup=nodegroup)
711+ self.assertItemsEqual(
712+ osystems,
713+ BootImage.objects.get_usable_osystems(nodegroup))
714+
715+ def test_get_usable_osystems_uses_given_nodegroup(self):
716+ nodegroup = factory.make_node_group()
717+ osystem = factory.make_name('os')
718+ factory.make_boot_image(
719+ osystem=osystem, nodegroup=nodegroup)
720+ self.assertItemsEqual(
721+ [],
722+ BootImage.objects.get_usable_osystems(
723+ factory.make_node_group()))
724+
725+ def test_get_usable_releases_returns_supported_releases(self):
726+ nodegroup = factory.make_node_group()
727+ osystem = factory.make_name('os')
728+ releases = [
729+ factory.make_name('release'),
730+ factory.make_name('release'),
731+ ]
732+ for release in releases:
733+ factory.make_boot_image(
734+ osystem=osystem,
735+ release=release,
736+ nodegroup=nodegroup)
737+ self.assertItemsEqual(
738+ releases,
739+ BootImage.objects.get_usable_releases(nodegroup, osystem))
740+
741+ def test_get_usable_releases_uses_given_nodegroup(self):
742+ nodegroup = factory.make_node_group()
743+ osystem = factory.make_name('os')
744+ release = factory.make_name('release')
745+ factory.make_boot_image(
746+ osystem=osystem, release=release, nodegroup=nodegroup)
747+ self.assertItemsEqual(
748+ [],
749+ BootImage.objects.get_usable_releases(
750+ factory.make_node_group(), osystem))
751+
752+ def test_get_usable_releases_uses_given_osystem(self):
753+ nodegroup = factory.make_node_group()
754+ osystem = factory.make_name('os')
755+ release = factory.make_name('release')
756+ factory.make_boot_image(
757+ osystem=osystem, release=release, nodegroup=nodegroup)
758+ self.assertItemsEqual(
759+ [],
760+ BootImage.objects.get_usable_releases(
761+ factory.make_node_group(), factory.make_name('os')))
762
763=== modified file 'src/maasserver/preseed.py'
764--- src/maasserver/preseed.py 2014-04-10 13:43:33 +0000
765+++ src/maasserver/preseed.py 2014-05-02 19:22:04 +0000
766@@ -93,6 +93,7 @@
767
768 def get_curtin_installer_url(node):
769 """Return the URL where curtin on the node can download its installer."""
770+ osystem = 'ubuntu'
771 series = node.get_distro_series()
772 cluster_host = pick_cluster_controller_address(node)
773 # XXX rvb(?): The path shouldn't be hardcoded like this, but rather synced
774@@ -100,18 +101,20 @@
775 arch, subarch = node.architecture.split('/')
776 purpose = 'xinstall'
777 image = BootImage.objects.get_latest_image(
778- node.nodegroup, arch, subarch, series, purpose)
779+ node.nodegroup, osystem, arch, subarch, series, purpose)
780 if image is None:
781 raise MAASAPIException(
782 "Error generating the URL of curtin's root-tgz file. "
783 "No image could be found for the given selection: "
784- "arch=%s, subarch=%s, series=%s, purpose=%s." % (
785+ "os=%s, arch=%s, subarch=%s, series=%s, purpose=%s." % (
786+ osystem,
787 arch,
788 subarch,
789 series,
790 purpose
791 ))
792 dyn_uri = '/'.join([
793+ osystem,
794 arch,
795 subarch,
796 series,
797
798=== modified file 'src/maasserver/templates/maasserver/bootimage-list.html'
799--- src/maasserver/templates/maasserver/bootimage-list.html 2014-03-27 07:39:38 +0000
800+++ src/maasserver/templates/maasserver/bootimage-list.html 2014-05-02 19:22:04 +0000
801@@ -20,6 +20,7 @@
802 <thead>
803 <tr>
804 <th>ID</th>
805+ <th>OS</th>
806 <th>Release</th>
807 <th>Architecture</th>
808 <th>Subarchitecture</th>
809@@ -32,6 +33,7 @@
810 {% for bootimage in bootimage_list %}
811 <tr class="bootimage {% cycle 'even' 'odd' %}">
812 <td>{{ bootimage.id }}</td>
813+ <td>{{ bootimage.osystem }}</td>
814 <td>{{ bootimage.release }}</td>
815 <td>{{ bootimage.architecture }}</td>
816 <td>{{ bootimage.subarchitecture }}</td>
817
818=== modified file 'src/maasserver/testing/factory.py'
819--- src/maasserver/testing/factory.py 2014-04-04 06:46:05 +0000
820+++ src/maasserver/testing/factory.py 2014-05-02 19:22:04 +0000
821@@ -427,9 +427,11 @@
822 return "OAuth " + ", ".join([
823 '%s="%s"' % (key, value) for key, value in items.items()])
824
825- def make_boot_image(self, architecture=None, subarchitecture=None,
826- release=None, purpose=None, nodegroup=None,
827- label=None):
828+ def make_boot_image(self, osystem=None, architecture=None,
829+ subarchitecture=None, release=None, purpose=None,
830+ nodegroup=None, label=None):
831+ if osystem is None:
832+ osystem = self.make_name('os')
833 if architecture is None:
834 architecture = self.make_name('architecture')
835 if subarchitecture is None:
836@@ -444,6 +446,7 @@
837 label = self.make_name('label')
838 return BootImage.objects.create(
839 nodegroup=nodegroup,
840+ osystem=osystem,
841 architecture=architecture,
842 subarchitecture=subarchitecture,
843 release=release,
844
845=== modified file 'src/maasserver/tests/test_api_boot_images.py'
846--- src/maasserver/tests/test_api_boot_images.py 2014-03-21 19:01:40 +0000
847+++ src/maasserver/tests/test_api_boot_images.py 2014-05-02 19:22:04 +0000
848@@ -134,6 +134,7 @@
849 image = factory.make_boot_image()
850 self.assertEqual(
851 (
852+ image.osystem,
853 image.architecture,
854 image.subarchitecture,
855 image.release,
856@@ -146,6 +147,7 @@
857 image = make_boot_image_params()
858 self.assertEqual(
859 (
860+ image['osystem'],
861 image['architecture'],
862 image['subarchitecture'],
863 image['release'],
864@@ -158,12 +160,13 @@
865 image = make_boot_image_params()
866 del image['subarchitecture']
867 del image['label']
868- _, subarchitecture, _, label, _ = summarise_boot_image_dict(image)
869+ _, _, subarchitecture, _, label, _ = summarise_boot_image_dict(image)
870 self.assertEqual(('generic', 'release'), (subarchitecture, label))
871
872 def test_summarise_boot_image_functions_are_compatible(self):
873 image_dict = make_boot_image_params()
874 image_obj = factory.make_boot_image(
875+ osystem=image_dict['osystem'],
876 architecture=image_dict['architecture'],
877 subarchitecture=image_dict['subarchitecture'],
878 release=image_dict['release'], label=image_dict['label'],
879
880=== modified file 'src/maasserver/tests/test_api_pxeconfig.py'
881--- src/maasserver/tests/test_api_pxeconfig.py 2014-03-27 04:15:45 +0000
882+++ src/maasserver/tests/test_api_pxeconfig.py 2014-05-02 19:22:04 +0000
883@@ -133,9 +133,11 @@
884 self.assertEqual(value, response_dict['extra_opts'])
885
886 def test_pxeconfig_uses_present_boot_image(self):
887+ osystem = 'ubuntu'
888 release = Config.objects.get_config('commissioning_distro_series')
889 nodegroup = factory.make_node_group()
890 factory.make_boot_image(
891+ osystem=osystem,
892 architecture="amd64", release=release, nodegroup=nodegroup,
893 purpose="commissioning")
894 params = self.get_default_params()
895
896=== modified file 'src/maasserver/tests/test_preseed.py'
897--- src/maasserver/tests/test_preseed.py 2014-04-21 11:43:26 +0000
898+++ src/maasserver/tests/test_preseed.py 2014-05-02 19:22:04 +0000
899@@ -590,6 +590,7 @@
900 node = factory.make_node()
901 arch, subarch = node.architecture.split('/')
902 factory.make_boot_image(
903+ osystem='ubuntu',
904 architecture=arch, subarchitecture=subarch,
905 release=node.get_distro_series(), purpose='xinstall',
906 nodegroup=node.nodegroup)
907@@ -677,27 +678,32 @@
908 self.assertIn('cloud-init', context['curtin_preseed'])
909
910 def test_get_curtin_installer_url_returns_url(self):
911+ osystem = 'ubuntu'
912 # Exclude DISTRO_SERIES.default. It's a special value that defers
913 # to a run-time setting which we don't provide in this test.
914 series = factory.getRandomEnum(
915 DISTRO_SERIES, but_not=DISTRO_SERIES.default)
916 architecture = make_usable_architecture(self)
917 node = factory.make_node(
918- architecture=architecture, distro_series=series)
919+ architecture=architecture,
920+ distro_series=series)
921 arch, subarch = architecture.split('/')
922 boot_image = factory.make_boot_image(
923- architecture=arch, subarchitecture=subarch, release=series,
924+ osystem=osystem, architecture=arch,
925+ subarchitecture=subarch, release=series,
926 purpose='xinstall', nodegroup=node.nodegroup)
927
928 installer_url = get_curtin_installer_url(node)
929
930 [interface] = node.nodegroup.get_managed_interfaces()
931 self.assertEqual(
932- 'http://%s/MAAS/static/images/%s/%s/%s/%s/root-tgz' % (
933- interface.ip, arch, subarch, series, boot_image.label),
934+ 'http://%s/MAAS/static/images/%s/%s/%s/%s/%s/root-tgz' % (
935+ interface.ip, osystem, arch, subarch,
936+ series, boot_image.label),
937 installer_url)
938
939 def test_get_curtin_installer_url_fails_if_no_boot_image(self):
940+ osystem = 'ubuntu'
941 series = factory.getRandomEnum(
942 DISTRO_SERIES, but_not=DISTRO_SERIES.default)
943 architecture = make_usable_architecture(self)
944@@ -705,6 +711,7 @@
945 architecture=architecture, distro_series=series)
946 # Generate a boot image with a different arch/subarch.
947 factory.make_boot_image(
948+ osystem=osystem,
949 architecture=factory.make_name('arch'),
950 subarchitecture=factory.make_name('subarch'), release=series,
951 purpose='xinstall', nodegroup=node.nodegroup)
952@@ -714,7 +721,8 @@
953 arch, subarch = architecture.split('/')
954 msg = (
955 "No image could be found for the given selection: "
956- "arch=%s, subarch=%s, series=%s, purpose=xinstall." % (
957+ "os=%s, arch=%s, subarch=%s, series=%s, purpose=xinstall." % (
958+ osystem,
959 arch,
960 subarch,
961 node.get_distro_series(),
962
963=== modified file 'src/maasserver/views/clusters.py'
964--- src/maasserver/views/clusters.py 2014-04-03 11:20:03 +0000
965+++ src/maasserver/views/clusters.py 2014-05-02 19:22:04 +0000
966@@ -280,5 +280,5 @@
967 nodegroup = self.get_nodegroup()
968 # A sorted bootimages list.
969 return nodegroup.bootimage_set.all().order_by(
970- '-release', 'architecture', 'subarchitecture', 'purpose',
971- 'label')
972+ 'osystem', '-release', 'architecture', 'subarchitecture',
973+ 'purpose', 'label')
974
975=== modified file 'src/maasserver/views/tests/test_boot_image_list.py'
976--- src/maasserver/views/tests/test_boot_image_list.py 2014-04-02 13:53:19 +0000
977+++ src/maasserver/views/tests/test_boot_image_list.py 2014-05-02 19:22:04 +0000
978@@ -54,8 +54,7 @@
979 self.client_log_in(as_admin=True)
980 nodegroup = factory.make_node_group()
981 # Create 4 images.
982- [
983- factory.make_boot_image(nodegroup=nodegroup) for _ in range(4)]
984+ [factory.make_boot_image(nodegroup=nodegroup) for _ in range(4)]
985 response = self.client.get(
986 reverse('cluster-bootimages-list', args=[nodegroup.uuid]))
987 self.assertEqual(httplib.OK, response.status_code)
988
989=== modified file 'src/metadataserver/tests/test_api.py'
990--- src/metadataserver/tests/test_api.py 2014-03-24 13:02:28 +0000
991+++ src/metadataserver/tests/test_api.py 2014-05-02 19:22:04 +0000
992@@ -394,6 +394,7 @@
993 node = factory.make_node()
994 arch, subarch = node.architecture.split('/')
995 factory.make_boot_image(
996+ osystem='ubuntu',
997 architecture=arch, subarchitecture=subarch,
998 release=node.get_distro_series(), purpose='xinstall',
999 nodegroup=node.nodegroup)
1000
1001=== modified file 'src/provisioningserver/boot/__init__.py'
1002--- src/provisioningserver/boot/__init__.py 2014-03-28 16:46:55 +0000
1003+++ src/provisioningserver/boot/__init__.py 2014-05-02 19:22:04 +0000
1004@@ -168,7 +168,7 @@
1005 """
1006 def image_dir(params):
1007 return compose_image_path(
1008- params.arch, params.subarch,
1009+ 'ubuntu', params.arch, params.subarch,
1010 params.release, params.label)
1011
1012 def initrd_path(params):
1013
1014=== modified file 'src/provisioningserver/boot/tests/test_pxe.py'
1015--- src/provisioningserver/boot/tests/test_pxe.py 2014-03-28 04:31:32 +0000
1016+++ src/provisioningserver/boot/tests/test_pxe.py 2014-05-02 19:22:04 +0000
1017@@ -163,7 +163,7 @@
1018 self.assertThat(output, StartsWith("DEFAULT "))
1019 # The PXE parameters are all set according to the options.
1020 image_dir = compose_image_path(
1021- arch=params.arch, subarch=params.subarch,
1022+ osystem='ubuntu', arch=params.arch, subarch=params.subarch,
1023 release=params.release, label=params.label)
1024 self.assertThat(
1025 output, MatchesAll(
1026@@ -268,7 +268,8 @@
1027 section = config[section_label]
1028 self.assertThat(
1029 section, ContainsAll(("KERNEL", "INITRD", "APPEND")))
1030- contains_arch_path = StartsWith("%s/" % section_label)
1031+ contains_arch_path = StartsWith(
1032+ "ubuntu/%s/" % section_label)
1033 self.assertThat(section["KERNEL"], contains_arch_path)
1034 self.assertThat(section["INITRD"], contains_arch_path)
1035 self.assertIn("APPEND", section)
1036
1037=== modified file 'src/provisioningserver/boot/tests/test_tftppath.py'
1038--- src/provisioningserver/boot/tests/test_tftppath.py 2014-04-03 09:26:31 +0000
1039+++ src/provisioningserver/boot/tests/test_tftppath.py 2014-05-02 19:22:04 +0000
1040@@ -62,6 +62,7 @@
1041 """Fake a boot image matching `image_params` under `tftproot`."""
1042 image_dir = locate_tftp_path(
1043 compose_image_path(
1044+ osystem=image_params['osystem'],
1045 arch=image_params['architecture'],
1046 subarch=image_params['subarchitecture'],
1047 release=image_params['release'],
1048@@ -72,21 +73,23 @@
1049 factory.make_file(image_dir, 'initrd.gz')
1050
1051 def test_compose_image_path_follows_storage_directory_layout(self):
1052+ osystem = factory.make_name('osystem')
1053 arch = factory.make_name('arch')
1054 subarch = factory.make_name('subarch')
1055 release = factory.make_name('release')
1056 label = factory.make_name('label')
1057 self.assertEqual(
1058- '%s/%s/%s/%s' % (arch, subarch, release, label),
1059- compose_image_path(arch, subarch, release, label))
1060+ '%s/%s/%s/%s/%s' % (osystem, arch, subarch, release, label),
1061+ compose_image_path(osystem, arch, subarch, release, label))
1062
1063 def test_compose_image_path_does_not_include_tftp_root(self):
1064+ osystem = factory.make_name('osystem')
1065 arch = factory.make_name('arch')
1066 subarch = factory.make_name('subarch')
1067 release = factory.make_name('release')
1068 label = factory.make_name('label')
1069 self.assertThat(
1070- compose_image_path(arch, subarch, release, label),
1071+ compose_image_path(osystem, arch, subarch, release, label),
1072 Not(StartsWith(self.tftproot)))
1073
1074 def test_locate_tftp_path_prefixes_tftp_root(self):
1075
1076=== modified file 'src/provisioningserver/boot/tests/test_uefi.py'
1077--- src/provisioningserver/boot/tests/test_uefi.py 2014-03-28 19:03:46 +0000
1078+++ src/provisioningserver/boot/tests/test_uefi.py 2014-05-02 19:22:04 +0000
1079@@ -73,7 +73,7 @@
1080 self.assertThat(output, StartsWith("set default=\"0\""))
1081 # The UEFI parameters are all set according to the options.
1082 image_dir = compose_image_path(
1083- arch=params.arch, subarch=params.subarch,
1084+ osystem='ubuntu', arch=params.arch, subarch=params.subarch,
1085 release=params.release, label=params.label)
1086
1087 self.assertThat(
1088
1089=== modified file 'src/provisioningserver/boot/tftppath.py'
1090--- src/provisioningserver/boot/tftppath.py 2014-04-03 16:36:15 +0000
1091+++ src/provisioningserver/boot/tftppath.py 2014-05-02 19:22:04 +0000
1092@@ -29,12 +29,13 @@
1093 logger = getLogger(__name__)
1094
1095
1096-def compose_image_path(arch, subarch, release, label):
1097+def compose_image_path(osystem, arch, subarch, release, label):
1098 """Compose the TFTP path for a PXE kernel/initrd directory.
1099
1100 The path returned is relative to the TFTP root, as it would be
1101 identified by clients on the network.
1102
1103+ :param osystem: Operating system.
1104 :param arch: Main machine architecture.
1105 :param subarch: Sub-architecture, or "generic" if there is none.
1106 :param release: Operating system release, e.g. "precise".
1107@@ -43,7 +44,7 @@
1108 kernel and initrd) as exposed over TFTP.
1109 """
1110 # This is a TFTP path, not a local filesystem path, so hard-code the slash.
1111- return '/'.join([arch, subarch, release, label])
1112+ return '/'.join([osystem, arch, subarch, release, label])
1113
1114
1115 def locate_tftp_path(path, tftproot):
1116@@ -116,7 +117,7 @@
1117 The path must consist of a full [architecture, subarchitecture, release]
1118 that identify a kind of boot that we may need an image for.
1119 """
1120- arch, subarch, release, label = path
1121+ osystem, arch, subarch, release, label = path
1122 # XXX: rvb 2014-03-24: The images import script currently imports all the
1123 # images for the configured selections (where a selection is an
1124 # arch/subarch/series/label combination). When the import script grows the
1125@@ -125,7 +126,7 @@
1126 purposes = ['commissioning', 'install', 'xinstall']
1127 return [
1128 dict(
1129- architecture=arch, subarchitecture=subarch,
1130+ osystem=osystem, architecture=arch, subarchitecture=subarch,
1131 release=release, label=label, purpose=purpose)
1132 for purpose in purposes
1133 ]
1134@@ -139,9 +140,9 @@
1135 `report_boot_images` API call.
1136 """
1137 # The sub-directories directly under tftproot, if they contain
1138- # images, represent architectures.
1139+ # images, represent operating systems.
1140 try:
1141- potential_archs = list_subdirs(tftproot)
1142+ potential_osystems = list_subdirs(tftproot)
1143 except OSError as exception:
1144 if exception.errno == errno.ENOENT:
1145 # Directory does not exist, so return empty list.
1146@@ -153,12 +154,12 @@
1147
1148 # Starting point for iteration: paths that contain only the
1149 # top-level subdirectory of tftproot, i.e. the architecture name.
1150- paths = [[subdir] for subdir in potential_archs]
1151+ paths = [[subdir] for subdir in potential_osystems]
1152
1153 # Extend paths deeper into the filesystem, through the levels that
1154- # represent sub-architecture, release, and label. Any directory
1155- # that doesn't extend this deep isn't a boot image.
1156- for level in ['subarch', 'release', 'label']:
1157+ # represent architecture, sub-architecture, release, and label.
1158+ # Any directory that doesn't extend this deep isn't a boot image.
1159+ for level in ['arch', 'subarch', 'release', 'label']:
1160 paths = drill_down(tftproot, paths)
1161
1162 # Each path we find this way should be a boot image.
1163
1164=== modified file 'src/provisioningserver/import_images/boot_resources.py'
1165--- src/provisioningserver/import_images/boot_resources.py 2014-04-25 10:42:10 +0000
1166+++ src/provisioningserver/import_images/boot_resources.py 2014-05-02 19:22:04 +0000
1167@@ -45,7 +45,7 @@
1168 """Raised when the config file for the script doesn't exist."""
1169
1170
1171-def tgt_entry(arch, subarch, release, label, image):
1172+def tgt_entry(osystem, arch, subarch, release, label, image):
1173 """Generate tgt target used to commission arch/subarch with release
1174
1175 Tgt target used to commission arch/subarch machine with a specific Ubuntu
1176@@ -59,6 +59,7 @@
1177 use the same inode for different tgt targets (even read-only targets which
1178 looks like a bug to me) without this option enabled.
1179
1180+ :param osystem: Operating System name we generate tgt target for
1181 :param arch: Architecture name we generate tgt target for
1182 :param subarch: Subarchitecture name we generate tgt target for
1183 :param release: Ubuntu release we generate tgt target for
1184@@ -67,7 +68,13 @@
1185 :return Tgt entry which can be written to tgt-admin configuration file
1186 """
1187 prefix = 'iqn.2004-05.com.ubuntu:maas'
1188- target_name = 'ephemeral-%s-%s-%s-%s' % (arch, subarch, release, label)
1189+ target_name = 'ephemeral-%s-%s-%s-%s-%s' % (
1190+ osystem,
1191+ arch,
1192+ subarch,
1193+ release,
1194+ label
1195+ )
1196 entry = dedent("""\
1197 <target {prefix}:{target_name}>
1198 readonly 1
1199@@ -110,17 +117,20 @@
1200 # Use a set to make sure we don't register duplicate entries in tgt.
1201 entries = set()
1202 for item in list_boot_images(snapshot_path):
1203+ osystem = item['osystem']
1204 arch = item['architecture']
1205 subarch = item['subarchitecture']
1206 release = item['release']
1207 label = item['label']
1208- entries.add((arch, subarch, release, label))
1209+ entries.add((osystem, arch, subarch, release, label))
1210 tgt_entries = []
1211- for arch, subarch, release, label in sorted(entries):
1212+ for osystem, arch, subarch, release, label in sorted(entries):
1213 root_image = os.path.join(
1214- snapshot_path, arch, subarch, release, label, 'root-image')
1215+ snapshot_path, osystem, arch, subarch,
1216+ release, label, 'root-image')
1217 if os.path.isfile(root_image):
1218- entry = tgt_entry(arch, subarch, release, label, root_image)
1219+ entry = tgt_entry(
1220+ osystem, arch, subarch, release, label, root_image)
1221 tgt_entries.append(entry)
1222 text = ''.join(tgt_entries)
1223 return text.encode('utf-8')
1224
1225=== modified file 'src/provisioningserver/import_images/download_resources.py'
1226--- src/provisioningserver/import_images/download_resources.py 2014-04-28 09:16:32 +0000
1227+++ src/provisioningserver/import_images/download_resources.py 2014-05-02 19:22:04 +0000
1228@@ -251,6 +251,7 @@
1229 """
1230 storage_path = os.path.abspath(storage_path)
1231 snapshot_path = compose_snapshot_path(storage_path)
1232+ ubuntu_path = os.path.join(snapshot_path, 'ubuntu')
1233 # Use a FileStore as our ObjectStore implementation. It will write to the
1234 # cache directory.
1235 cache_path = os.path.join(storage_path, 'cache')
1236@@ -260,7 +261,7 @@
1237
1238 for source in sources:
1239 download_boot_resources(
1240- source['path'], store, snapshot_path, product_mapping,
1241+ source['path'], store, ubuntu_path, product_mapping,
1242 keyring=source['keyring'])
1243
1244 return snapshot_path
1245
1246=== modified file 'src/provisioningserver/import_images/tests/test_boot_resources.py'
1247--- src/provisioningserver/import_images/tests/test_boot_resources.py 2014-04-25 10:42:10 +0000
1248+++ src/provisioningserver/import_images/tests/test_boot_resources.py 2014-05-02 19:22:04 +0000
1249@@ -51,9 +51,10 @@
1250
1251 def test_generates_one_target(self):
1252 spec = make_image_spec()
1253+ osystem = factory.make_name('osystem')
1254 image = self.make_file()
1255 entry = boot_resources.tgt_entry(
1256- spec.arch, spec.subarch, spec.release, spec.label, image)
1257+ osystem, spec.arch, spec.subarch, spec.release, spec.label, image)
1258 # The entry looks a bit like XML, but isn't well-formed. So don't try
1259 # to parse it as such!
1260 self.assertIn('<target iqn.2004-05.com.ubuntu:maas:', entry)
1261@@ -63,8 +64,9 @@
1262 def test_produces_suitable_output_for_tgt_admin(self):
1263 spec = make_image_spec()
1264 image = self.make_file()
1265+ osystem = factory.make_name('osystem')
1266 entry = boot_resources.tgt_entry(
1267- spec.arch, spec.subarch, spec.release, spec.label, image)
1268+ osystem, spec.arch, spec.subarch, spec.release, spec.label, image)
1269 config = self.make_file(contents=entry)
1270 # Pretend to be root, but without requiring the actual privileges and
1271 # without prompting for a password. In that state, run tgt-admin.
1272@@ -276,7 +278,7 @@
1273 self.assertThat(os.path.join(current, 'maas.meta'), FileExists())
1274 self.assertThat(os.path.join(current, 'maas.tgt'), FileExists())
1275 self.assertThat(
1276- os.path.join(current, arch, subarch, release, label),
1277+ os.path.join(current, 'ubuntu', arch, subarch, release, label),
1278 DirExists())
1279
1280 # Verify the contents of the "meta" file.
1281
1282=== modified file 'src/provisioningserver/kernel_opts.py'
1283--- src/provisioningserver/kernel_opts.py 2014-03-20 10:44:56 +0000
1284+++ src/provisioningserver/kernel_opts.py 2014-05-02 19:22:04 +0000
1285@@ -30,9 +30,10 @@
1286
1287 KernelParametersBase = namedtuple(
1288 "KernelParametersBase", (
1289+ "osystem", # Operating system, e.g. "ubuntu"
1290 "arch", # Machine architecture, e.g. "i386"
1291 "subarch", # Machine subarchitecture, e.g. "generic"
1292- "release", # Ubuntu release, e.g. "precise"
1293+ "release", # OS release, e.g. "precise"
1294 "label", # Image label, e.g. "release"
1295 "purpose", # Boot purpose, e.g. "commissioning"
1296 "hostname", # Machine hostname, e.g. "coleman"
1297@@ -90,9 +91,15 @@
1298 ISCSI_TARGET_NAME_PREFIX = "iqn.2004-05.com.ubuntu:maas"
1299
1300
1301-def get_ephemeral_name(arch, subarch, release, label):
1302+def get_ephemeral_name(osystem, arch, subarch, release, label):
1303 """Return the name of the most recent ephemeral image."""
1304- return "ephemeral-%s-%s-%s-%s" % (arch, subarch, release, label)
1305+ return "ephemeral-%s-%s-%s-%s-%s" % (
1306+ osystem,
1307+ arch,
1308+ subarch,
1309+ release,
1310+ label
1311+ )
1312
1313
1314 def compose_hostname_opts(params):
1315@@ -119,7 +126,8 @@
1316 # These are kernel parameters read by the ephemeral environment.
1317 tname = prefix_target_name(
1318 get_ephemeral_name(
1319- params.arch, params.subarch, params.release, params.label))
1320+ params.osystem, params.arch, params.subarch,
1321+ params.release, params.label))
1322 kernel_params = [
1323 # Read by the open-iscsi initramfs code.
1324 "iscsi_target_name=%s" % tname,
1325
1326=== modified file 'src/provisioningserver/rpc/cluster.py'
1327--- src/provisioningserver/rpc/cluster.py 2014-03-20 22:36:32 +0000
1328+++ src/provisioningserver/rpc/cluster.py 2014-05-02 19:22:04 +0000
1329@@ -33,7 +33,8 @@
1330 arguments = []
1331 response = [
1332 (b"images", amp.AmpList(
1333- [(b"architecture", amp.Unicode()),
1334+ [(b"osystem", amp.Unicode()),
1335+ (b"architecture", amp.Unicode()),
1336 (b"subarchitecture", amp.Unicode()),
1337 (b"release", amp.Unicode()),
1338 (b"label", amp.Unicode()),
1339
1340=== modified file 'src/provisioningserver/rpc/tests/test_clusterservice.py'
1341--- src/provisioningserver/rpc/tests/test_clusterservice.py 2014-03-28 04:31:32 +0000
1342+++ src/provisioningserver/rpc/tests/test_clusterservice.py 2014-05-02 19:22:04 +0000
1343@@ -171,6 +171,7 @@
1344 # serialised correctly.
1345
1346 # Example boot image definitions.
1347+ osystems = "ubuntu", "centos"
1348 archs = "i386", "amd64"
1349 subarchs = "generic", "special"
1350 releases = "precise", "trusty"
1351@@ -179,7 +180,7 @@
1352
1353 # Create a TFTP file tree with a variety of subdirectories.
1354 tftpdir = self.make_dir()
1355- for options in product(archs, subarchs, releases, labels):
1356+ for options in product(osystems, archs, subarchs, releases, labels):
1357 os.makedirs(os.path.join(tftpdir, *options))
1358
1359 # Ensure that list_boot_images() uses the above TFTP file tree.
1360@@ -187,14 +188,15 @@
1361
1362 expected_images = [
1363 {
1364+ "osystem": osystem,
1365 "architecture": arch,
1366 "subarchitecture": subarch,
1367 "release": release,
1368 "label": label,
1369 "purpose": purpose,
1370 }
1371- for arch, subarch, release, label, purpose in product(
1372- archs, subarchs, releases, labels, purposes)
1373+ for osystem, arch, subarch, release, label, purpose in product(
1374+ osystems, archs, subarchs, releases, labels, purposes)
1375 ]
1376
1377 response = yield call_responder(Cluster(), cluster.ListBootImages, {})
1378
1379=== modified file 'src/provisioningserver/testing/boot_images.py'
1380--- src/provisioningserver/testing/boot_images.py 2014-03-21 03:21:57 +0000
1381+++ src/provisioningserver/testing/boot_images.py 2014-05-02 19:22:04 +0000
1382@@ -23,10 +23,11 @@
1383 """Create an arbitrary dict of boot-image parameters.
1384
1385 These are the parameters that together describe a kind of boot for
1386- which we may need a kernel and initrd: architecture,
1387+ which we may need a kernel and initrd: operating system, architecture,
1388 sub-architecture, Ubuntu release, boot purpose, and release label.
1389 """
1390 return dict(
1391+ osystem=factory.make_name('osystem'),
1392 architecture=factory.make_name('architecture'),
1393 subarchitecture=factory.make_name('subarchitecture'),
1394 release=factory.make_name('release'),
1395@@ -39,9 +40,11 @@
1396 """Create a dict of boot-image parameters as used to store the image.
1397
1398 These are the parameters that together describe a path to store a boot
1399- image: architecture, sub-architecture, Ubuntu release, and release label.
1400+ image: operating system, architecture, sub-architecture, Ubuntu release,
1401+ and release label.
1402 """
1403 return dict(
1404+ osystem=factory.make_name('osystem'),
1405 architecture=factory.make_name('architecture'),
1406 subarchitecture=factory.make_name('subarchitecture'),
1407 release=factory.make_name('release'),
1408
1409=== modified file 'src/provisioningserver/tests/test_kernel_opts.py'
1410--- src/provisioningserver/tests/test_kernel_opts.py 2014-03-28 16:46:55 +0000
1411+++ src/provisioningserver/tests/test_kernel_opts.py 2014-05-02 19:22:04 +0000
1412@@ -228,7 +228,8 @@
1413 # options for a "xinstall" node.
1414 params = self.make_kernel_parameters(purpose="xinstall")
1415 ephemeral_name = get_ephemeral_name(
1416- params.arch, params.subarch, params.release, params.label)
1417+ params.osystem, params.arch, params.subarch,
1418+ params.release, params.label)
1419 self.assertThat(
1420 compose_kernel_command_line(params),
1421 ContainsAll([
1422@@ -243,7 +244,8 @@
1423 # options for a "commissioning" node.
1424 params = self.make_kernel_parameters(purpose="commissioning")
1425 ephemeral_name = get_ephemeral_name(
1426- params.arch, params.subarch, params.release, params.label)
1427+ params.osystem, params.arch, params.subarch,
1428+ params.release, params.label)
1429 self.assertThat(
1430 compose_kernel_command_line(params),
1431 ContainsAll([