Merge lp:~blake-rouse/maas/add-osystem-to-bootimage into lp:~maas-committers/maas/trunk
- add-osystem-to-bootimage
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Blake Rouse | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 2313 | ||||
Proposed branch: | lp:~blake-rouse/maas/add-osystem-to-bootimage | ||||
Merge into: | lp:~maas-committers/maas/trunk | ||||
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 | ||||
Related bugs: |
|
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/
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.
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_
> > the list of usable operating systems for a nodegroup. + """
> > + query = BootImage.
> > return set(query.
> > get_usable_
> > the list of usable releases for a nodegroup and + operating
> > system. + """ + query =
> > BootImage.
> > releases = query.values_
> > 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://
>
> iEYEARECAAYFAlN
> VaEAniGAr/
> =VxoG
> -----END PGP SIGNATURE-----
>
> --
>
> https:/
> You are subscribed to branch lp:maas.
>
Blake Rouse (blake-rouse) wrote : | # |
I added test for get_usable_osystems and get_useable_
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.
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?
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.
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.
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.
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.
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!
Preview Diff
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([ |
-----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 objects. filter( nodegroup= nodegroup) + values_ list('osystem' , flat=True)) + + def releases( self, nodegroup, osystem): + """Return objects. filter( nodegroup= nodegroup, osystem=osystem) + list('release' , flat=True) + return
> the list of usable operating systems for a nodegroup. + """
> + query = BootImage.
> return set(query.
> get_usable_
> the list of usable releases for a nodegroup and + operating
> system. + """ + query =
> BootImage.
> releases = query.values_
> 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----- www.enigmail. net/
YfxYACgkQWhGlTF 8G/HefwwCfWwiom UOBgXKO3Z4mf/ YGEfpL MpfihFXHwJBKOek qD2KGOp
Version: GnuPG v1
Comment: Using GnuPG with Thunderbird - http://
iEYEARECAAYFAlN
VaEAniGAr/
=VxoG
-----END PGP SIGNATURE-----