Merge lp:~racb/maas/subarch-model into lp:~maas-committers/maas/trunk

Proposed by Robie Basak
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: no longer in the source branch.
Merged at revision: 1105
Proposed branch: lp:~racb/maas/subarch-model
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 735 lines (+503/-49)
11 files modified
src/maasserver/api.py (+26/-3)
src/maasserver/enum.py (+6/-6)
src/maasserver/messages.py (+1/-1)
src/maasserver/migrations/0031_node_architecture_field_size.py (+190/-0)
src/maasserver/migrations/0032_node_subarch.py (+191/-0)
src/maasserver/models/node.py (+1/-1)
src/maasserver/preseed.py (+1/-0)
src/maasserver/tests/test_api.py (+70/-6)
src/maasserver/tests/test_node.py (+1/-1)
src/maasserver/tests/test_node_constraint_filter.py (+8/-6)
src/maasserver/tests/test_preseed.py (+8/-25)
To merge this branch: bzr merge lp:~racb/maas/subarch-model
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+126712@code.launchpad.net

Commit message

Add subarchitecture support to the Node model

This also requires an addition to the enlistment API. A new field "subarchitecture", if specified, specifies the subarchitecture of the machine being enlisted. The corresponding change in maas-enlist is tracked in bug 1056816.

To post a comment you must log in.
Revision history for this message
Martin Packman (gz) wrote :

Temporarily making constraints specify the full form "i386/generic" rather than just "i386" is fine. A follow up branch can do the fix we talked about that maps the main architecture strings to the set of all supported sub-architectures.

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

Looks good! A few comments, but they're all very minor.

[1]

+        architecture = factory.getRandomChoice(
+            filter(lambda choice: choice[0].endswith('/generic'),
+                   ARCHITECTURE_CHOICES))

On the whole we try to avoid filter and map, because comprehensions
are normally much easier to read, and because their behaviour changes
in Python 3. I think the above would clearer as:

        architecture = factory.getRandomChoice(
            [choice for choice in ARCHITECTURE_CHOICES
             if choice[0].endswith('/generic')])

[2]

+        parsed_result = json.loads(response.content)
+
+        self.assertEqual(httplib.OK, response.status_code)
+        self.assertIn('application/json', response['Content-Type'])

Don't parse the result until after the response has been checked; it
might fail and then we know less about the failure. How about:

        self.assertEqual(
            (httplib.OK, 'application/json'),
            (response.status_code, response['Content-Type']),
            response.content)
        parsed_result = json.loads(response.content)

If the response is not OK or the content-type is not JSON, this will
blow up with the mismatch and the response content as detail.

[3]

+        self.assertIn('text/plain; charset=utf-8', response['Content-Type'])

There's assertStartsWith too, if you just want to check that it's
text/plain.

[4]

+        self.assertEqual("Subarchitecture cannot be specified twice",

Sentences end in a full-stop... unless the convention elsewhere in the
codebase to not do that. Very minor point, but it makes a difference
to the overall polish.

[5]

+        self.assertEqual(expected_arch[0], params_out["arch"])
+        self.assertEqual(expected_arch[1], params_out["subarch"])

Failures would be more informative if this were done in a single
assertion:

        observed_arch = params_out["arch"], params_out["subarch"]
        self.assertEqual(expected_arch, observed_arch)

[6]

         node_template_name = "%s_%s_%s_%s" % (
-            prefix, node.architecture, release, node.hostname)
+            prefix, node.architecture.replace('/', '_'),
+            release, node.hostname)

Because the components are delimited by _, perhaps replace / in the
architecture with - or : instead?

review: Approve
Revision history for this message
Robie Basak (racb) wrote :

Thanks for the review, Gavin!

[1] Done

[2] This was the pattern followed before, but as I've touched every test that followed this pattern I've changed them all to check the response status. This should cause a test failure to be more useful to understand.

[3] assertIn is the pattern followed in all the existing tests, and this change only touches some of them. So I've kept this the same to stay consistent. I have dropped the "charset=utf-8" part though, as I don't think this is really what I'm testing.

[4] Done

[5] Done

[6] node_template_name must follow the template filename layout defined in src/maasserver/preseed.py which uses underscores to separate the architecture and subarchitecture - that's what it's testing.

Revision history for this message
Raphaël Badin (rvb) wrote :

Wait, migrations number 29 and 30 have been taken already. You need to recreate your migrations. That's because the entire state of the db is present in each transaction file.

You need to
- delete your migrations
- merge trunk
- regenerate your migrations, they should be numbered 31 and 32.

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 2012-09-28 12:29:50 +0000
3+++ src/maasserver/api.py 2012-09-28 17:03:22 +0000
4@@ -578,8 +578,31 @@
5 :rtype: :class:`maasserver.models.Node`.
6 :raises: ValidationError
7 """
8+
9+ # For backwards compatibilty reasons, requests may be sent with:
10+ # architecture with a '/' in it: use normally
11+ # architecture without a '/' and no subarchitecture: assume 'generic'
12+ # architecture without a '/' and a subarchitecture: use as specified
13+ # architecture with a '/' and a subarchitecture: error
14+ given_arch = request.data.get('architecture', None)
15+ given_subarch = request.data.get('subarchitecture', None)
16+ altered_query_data = request.data.copy()
17+ if given_arch and '/' in given_arch:
18+ if given_subarch:
19+ # architecture with a '/' and a subarchitecture: error
20+ raise ValidationError('Subarchitecture cannot be specified twice.')
21+ # architecture with a '/' in it: use normally
22+ elif given_arch:
23+ if given_subarch:
24+ # architecture without a '/' and a subarchitecture: use as specified
25+ altered_query_data['architecture'] = '/'.join([given_arch, given_subarch])
26+ del altered_query_data['subarchitecture']
27+ else:
28+ # architecture without a '/' and no subarchitecture: assume 'generic'
29+ altered_query_data['architecture'] += '/generic'
30+
31 Form = get_node_create_form(request.user)
32- form = Form(request.data)
33+ form = Form(altered_query_data)
34 if form.is_valid():
35 return form.save()
36 else:
37@@ -1514,12 +1537,12 @@
38 # Default to i386 as a works-for-all solution. This will not support
39 # non-x86 architectures, but for now this assumption holds.
40 node = None
41- arch, subarch = ARCHITECTURE.i386, "generic"
42+ arch, subarch = ARCHITECTURE.i386.split('/')
43 preseed_url = compose_enlistment_preseed_url()
44 hostname = 'maas-enlist'
45 else:
46 node = macaddress.node
47- arch, subarch = node.architecture, "generic"
48+ arch, subarch = node.architecture.split('/')
49 preseed_url = compose_preseed_url(node)
50 hostname = node.hostname
51
52
53=== modified file 'src/maasserver/enum.py'
54--- src/maasserver/enum.py 2012-09-19 09:50:46 +0000
55+++ src/maasserver/enum.py 2012-09-28 17:03:22 +0000
56@@ -104,18 +104,18 @@
57 class ARCHITECTURE:
58 """List of supported architectures."""
59 #:
60- i386 = 'i386'
61- #:
62- amd64 = 'amd64'
63- #:
64- armhf = 'armhf'
65+ i386 = 'i386/generic'
66+ #:
67+ amd64 = 'amd64/generic'
68+ #:
69+ armhf_highbank = 'armhf/highbank'
70
71
72 # Architecture names.
73 ARCHITECTURE_CHOICES = (
74 (ARCHITECTURE.i386, "i386"),
75 (ARCHITECTURE.amd64, "amd64"),
76- (ARCHITECTURE.armhf, "armhf"),
77+ (ARCHITECTURE.armhf_highbank, "armhf/highbank"),
78 )
79
80
81
82=== modified file 'src/maasserver/messages.py'
83--- src/maasserver/messages.py 2012-04-16 10:00:51 +0000
84+++ src/maasserver/messages.py 2012-09-28 17:03:22 +0000
85@@ -111,7 +111,7 @@
86 "instance": {
87 "hostname": "sun",
88 "system_id": "node-17ca41c2-6c39-11e1-a961-00219bd0a2de",
89- "architecture": "i386",
90+ "architecture": "i386/generic",
91 [...]
92 }
93 }
94
95=== added file 'src/maasserver/migrations/0031_node_architecture_field_size.py'
96--- src/maasserver/migrations/0031_node_architecture_field_size.py 1970-01-01 00:00:00 +0000
97+++ src/maasserver/migrations/0031_node_architecture_field_size.py 2012-09-28 17:03:22 +0000
98@@ -0,0 +1,190 @@
99+# -*- coding: utf-8 -*-
100+import datetime
101+from south.db import db
102+from south.v2 import SchemaMigration
103+from django.db import models
104+
105+
106+class Migration(SchemaMigration):
107+
108+ def forwards(self, orm):
109+
110+ # Changing field 'Node.architecture'
111+ db.alter_column(u'maasserver_node', 'architecture', self.gf('django.db.models.fields.CharField')(max_length=31))
112+
113+ def backwards(self, orm):
114+
115+ # Changing field 'Node.architecture'
116+ db.alter_column(u'maasserver_node', 'architecture', self.gf('django.db.models.fields.CharField')(max_length=10))
117+
118+ models = {
119+ 'auth.group': {
120+ 'Meta': {'object_name': 'Group'},
121+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
122+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
123+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
124+ },
125+ 'auth.permission': {
126+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
127+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
128+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
129+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
130+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
131+ },
132+ 'auth.user': {
133+ 'Meta': {'object_name': 'User'},
134+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
135+ 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75', 'blank': 'True'}),
136+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
137+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
138+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
139+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
140+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
141+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
142+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
143+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
144+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
145+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
146+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
147+ },
148+ 'contenttypes.contenttype': {
149+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
150+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
151+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
152+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
153+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
154+ },
155+ u'maasserver.bootimage': {
156+ 'Meta': {'unique_together': "((u'architecture', u'subarchitecture', u'release', u'purpose'),)", 'object_name': 'BootImage'},
157+ 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
158+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
159+ 'purpose': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
160+ 'release': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
161+ 'subarchitecture': ('django.db.models.fields.CharField', [], {'max_length': '255'})
162+ },
163+ u'maasserver.config': {
164+ 'Meta': {'object_name': 'Config'},
165+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
166+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
167+ 'value': ('maasserver.fields.JSONObjectField', [], {'null': 'True'})
168+ },
169+ u'maasserver.dhcplease': {
170+ 'Meta': {'object_name': 'DHCPLease'},
171+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
172+ 'ip': ('django.db.models.fields.IPAddressField', [], {'unique': 'True', 'max_length': '15'}),
173+ 'mac': ('maasserver.fields.MACAddressField', [], {}),
174+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"})
175+ },
176+ u'maasserver.filestorage': {
177+ 'Meta': {'object_name': 'FileStorage'},
178+ 'data': ('django.db.models.fields.files.FileField', [], {'max_length': '255'}),
179+ 'filename': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '200'}),
180+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
181+ },
182+ u'maasserver.macaddress': {
183+ 'Meta': {'object_name': 'MACAddress'},
184+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
185+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
186+ 'mac_address': ('maasserver.fields.MACAddressField', [], {'unique': 'True'}),
187+ 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}),
188+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
189+ },
190+ u'maasserver.node': {
191+ 'Meta': {'object_name': 'Node'},
192+ 'after_commissioning_action': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
193+ 'architecture': ('django.db.models.fields.CharField', [], {'default': "u'i386/generic'", 'max_length': '31'}),
194+ 'cpu_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
195+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
196+ 'distro_series': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '10', 'null': 'True', 'blank': 'True'}),
197+ 'error': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
198+ 'hardware_details': ('maasserver.fields.XMLField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
199+ 'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
200+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
201+ 'memory': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
202+ 'netboot': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
203+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']", 'null': 'True'}),
204+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
205+ 'power_parameters': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
206+ 'power_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '10', 'blank': 'True'}),
207+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '10'}),
208+ 'system_id': ('django.db.models.fields.CharField', [], {'default': "u'node-c99b0122-0980-11e2-aaba-525400f6f496'", 'unique': 'True', 'max_length': '41'}),
209+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Tag']", 'symmetrical': 'False'}),
210+ 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'null': 'True'}),
211+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
212+ },
213+ u'maasserver.nodegroup': {
214+ 'Meta': {'object_name': 'NodeGroup'},
215+ 'api_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '18'}),
216+ 'api_token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'unique': 'True'}),
217+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
218+ 'dhcp_key': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
219+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
220+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}),
221+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
222+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
223+ 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'})
224+ },
225+ u'maasserver.nodegroupinterface': {
226+ 'Meta': {'unique_together': "((u'nodegroup', u'interface'),)", 'object_name': 'NodeGroupInterface'},
227+ 'broadcast_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
228+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
229+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
230+ 'interface': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
231+ 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
232+ 'ip_range_high': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
233+ 'ip_range_low': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
234+ 'management': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
235+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
236+ 'router_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
237+ 'subnet_mask': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
238+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
239+ },
240+ u'maasserver.sshkey': {
241+ 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSHKey'},
242+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
243+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
244+ 'key': ('django.db.models.fields.TextField', [], {}),
245+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
246+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
247+ },
248+ u'maasserver.tag': {
249+ 'Meta': {'object_name': 'Tag'},
250+ 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
251+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
252+ 'definition': ('django.db.models.fields.TextField', [], {}),
253+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
254+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
255+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
256+ },
257+ u'maasserver.userprofile': {
258+ 'Meta': {'object_name': 'UserProfile'},
259+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
260+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
261+ },
262+ 'piston.consumer': {
263+ 'Meta': {'object_name': 'Consumer'},
264+ 'description': ('django.db.models.fields.TextField', [], {}),
265+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
266+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
267+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
268+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
269+ 'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '16'}),
270+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'consumers'", 'null': 'True', 'to': "orm['auth.User']"})
271+ },
272+ 'piston.token': {
273+ 'Meta': {'object_name': 'Token'},
274+ 'callback': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
275+ 'callback_confirmed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
276+ 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Consumer']"}),
277+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
278+ 'is_approved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
279+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
280+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
281+ 'timestamp': ('django.db.models.fields.IntegerField', [], {'default': '1348845951L'}),
282+ 'token_type': ('django.db.models.fields.IntegerField', [], {}),
283+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'tokens'", 'null': 'True', 'to': "orm['auth.User']"}),
284+ 'verifier': ('django.db.models.fields.CharField', [], {'max_length': '10'})
285+ }
286+ }
287+
288+ complete_apps = ['maasserver']
289\ No newline at end of file
290
291=== added file 'src/maasserver/migrations/0032_node_subarch.py'
292--- src/maasserver/migrations/0032_node_subarch.py 1970-01-01 00:00:00 +0000
293+++ src/maasserver/migrations/0032_node_subarch.py 2012-09-28 17:03:22 +0000
294@@ -0,0 +1,191 @@
295+# -*- coding: utf-8 -*-
296+import datetime
297+from south.db import db
298+from south.v2 import DataMigration
299+from django.db import models
300+
301+class Migration(DataMigration):
302+
303+ def forwards(self, orm):
304+ for node in orm['maasserver.Node'].objects.all():
305+ node.architecture += '/generic'
306+ node.save()
307+
308+ def backwards(self, orm):
309+ for node in orm['maasserver.Node'].objects.all():
310+ # Drop subarch for backwards migration. This is lossy.
311+ node.architecture = node.architecture.split('/')[0]
312+ node.save()
313+
314+ models = {
315+ 'auth.group': {
316+ 'Meta': {'object_name': 'Group'},
317+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
318+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
319+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
320+ },
321+ 'auth.permission': {
322+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
323+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
324+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
325+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
326+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
327+ },
328+ 'auth.user': {
329+ 'Meta': {'object_name': 'User'},
330+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
331+ 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75', 'blank': 'True'}),
332+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
333+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
334+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
335+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
336+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
337+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
338+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
339+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
340+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
341+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
342+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
343+ },
344+ 'contenttypes.contenttype': {
345+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
346+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
347+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
348+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
349+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
350+ },
351+ u'maasserver.bootimage': {
352+ 'Meta': {'unique_together': "((u'architecture', u'subarchitecture', u'release', u'purpose'),)", 'object_name': 'BootImage'},
353+ 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
354+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
355+ 'purpose': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
356+ 'release': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
357+ 'subarchitecture': ('django.db.models.fields.CharField', [], {'max_length': '255'})
358+ },
359+ u'maasserver.config': {
360+ 'Meta': {'object_name': 'Config'},
361+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
362+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
363+ 'value': ('maasserver.fields.JSONObjectField', [], {'null': 'True'})
364+ },
365+ u'maasserver.dhcplease': {
366+ 'Meta': {'object_name': 'DHCPLease'},
367+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
368+ 'ip': ('django.db.models.fields.IPAddressField', [], {'unique': 'True', 'max_length': '15'}),
369+ 'mac': ('maasserver.fields.MACAddressField', [], {}),
370+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"})
371+ },
372+ u'maasserver.filestorage': {
373+ 'Meta': {'object_name': 'FileStorage'},
374+ 'data': ('django.db.models.fields.files.FileField', [], {'max_length': '255'}),
375+ 'filename': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '200'}),
376+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
377+ },
378+ u'maasserver.macaddress': {
379+ 'Meta': {'object_name': 'MACAddress'},
380+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
381+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
382+ 'mac_address': ('maasserver.fields.MACAddressField', [], {'unique': 'True'}),
383+ 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}),
384+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
385+ },
386+ u'maasserver.node': {
387+ 'Meta': {'object_name': 'Node'},
388+ 'after_commissioning_action': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
389+ 'architecture': ('django.db.models.fields.CharField', [], {'default': "u'i386/generic'", 'max_length': '31'}),
390+ 'cpu_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
391+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
392+ 'distro_series': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '10', 'null': 'True', 'blank': 'True'}),
393+ 'error': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
394+ 'hardware_details': ('maasserver.fields.XMLField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
395+ 'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
396+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
397+ 'memory': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
398+ 'netboot': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
399+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']", 'null': 'True'}),
400+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
401+ 'power_parameters': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
402+ 'power_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '10', 'blank': 'True'}),
403+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '10'}),
404+ 'system_id': ('django.db.models.fields.CharField', [], {'default': "u'node-3c1542ee-0981-11e2-8be8-525400f6f496'", 'unique': 'True', 'max_length': '41'}),
405+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Tag']", 'symmetrical': 'False'}),
406+ 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'null': 'True'}),
407+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
408+ },
409+ u'maasserver.nodegroup': {
410+ 'Meta': {'object_name': 'NodeGroup'},
411+ 'api_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '18'}),
412+ 'api_token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'unique': 'True'}),
413+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
414+ 'dhcp_key': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
415+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
416+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}),
417+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
418+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
419+ 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'})
420+ },
421+ u'maasserver.nodegroupinterface': {
422+ 'Meta': {'unique_together': "((u'nodegroup', u'interface'),)", 'object_name': 'NodeGroupInterface'},
423+ 'broadcast_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
424+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
425+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
426+ 'interface': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
427+ 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
428+ 'ip_range_high': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
429+ 'ip_range_low': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
430+ 'management': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
431+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
432+ 'router_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
433+ 'subnet_mask': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
434+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
435+ },
436+ u'maasserver.sshkey': {
437+ 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSHKey'},
438+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
439+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
440+ 'key': ('django.db.models.fields.TextField', [], {}),
441+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
442+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
443+ },
444+ u'maasserver.tag': {
445+ 'Meta': {'object_name': 'Tag'},
446+ 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
447+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
448+ 'definition': ('django.db.models.fields.TextField', [], {}),
449+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
450+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
451+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
452+ },
453+ u'maasserver.userprofile': {
454+ 'Meta': {'object_name': 'UserProfile'},
455+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
456+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
457+ },
458+ 'piston.consumer': {
459+ 'Meta': {'object_name': 'Consumer'},
460+ 'description': ('django.db.models.fields.TextField', [], {}),
461+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
462+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
463+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
464+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
465+ 'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '16'}),
466+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'consumers'", 'null': 'True', 'to': "orm['auth.User']"})
467+ },
468+ 'piston.token': {
469+ 'Meta': {'object_name': 'Token'},
470+ 'callback': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
471+ 'callback_confirmed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
472+ 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Consumer']"}),
473+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
474+ 'is_approved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
475+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
476+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
477+ 'timestamp': ('django.db.models.fields.IntegerField', [], {'default': '1348846143L'}),
478+ 'token_type': ('django.db.models.fields.IntegerField', [], {}),
479+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'tokens'", 'null': 'True', 'to': "orm['auth.User']"}),
480+ 'verifier': ('django.db.models.fields.CharField', [], {'max_length': '10'})
481+ }
482+ }
483+
484+ complete_apps = ['maasserver']
485+ symmetrical = True
486
487=== modified file 'src/maasserver/models/node.py'
488--- src/maasserver/models/node.py 2012-09-28 10:30:51 +0000
489+++ src/maasserver/models/node.py 2012-09-28 17:03:22 +0000
490@@ -411,7 +411,7 @@
491 blank=True, default=None)
492
493 architecture = CharField(
494- max_length=10, choices=ARCHITECTURE_CHOICES, blank=False,
495+ max_length=31, choices=ARCHITECTURE_CHOICES, blank=False,
496 default=ARCHITECTURE.i386)
497
498 # Juju expects the following standard constraints, which are stored here
499
500=== modified file 'src/maasserver/preseed.py'
501--- src/maasserver/preseed.py 2012-09-21 07:37:56 +0000
502+++ src/maasserver/preseed.py 2012-09-28 17:03:22 +0000
503@@ -93,6 +93,7 @@
504 lookup order:
505 {prefix}_{node_architecture}_{node_subarchitecture}_{release}_{node_name}
506 {prefix}_{node_architecture}_{node_subarchitecture}_{release}
507+ {prefix}_{node_architecture}_{node_subarchitecture}
508 {prefix}_{node_architecture}
509 {prefix}
510 'generic'
511
512=== modified file 'src/maasserver/tests/test_api.py'
513--- src/maasserver/tests/test_api.py 2012-09-28 12:55:42 +0000
514+++ src/maasserver/tests/test_api.py 2012-09-28 17:03:22 +0000
515@@ -268,6 +268,50 @@
516 ]
517
518 def test_POST_new_creates_node(self):
519+ architecture = factory.getRandomChoice(ARCHITECTURE_CHOICES)
520+ response = self.client.post(
521+ self.get_uri('nodes/'),
522+ {
523+ 'op': 'new',
524+ 'hostname': 'diane',
525+ 'architecture': architecture,
526+ 'after_commissioning_action':
527+ NODE_AFTER_COMMISSIONING_ACTION.DEFAULT,
528+ 'mac_addresses': ['aa:bb:cc:dd:ee:ff', '22:bb:cc:dd:ee:ff'],
529+ })
530+
531+ self.assertEqual(httplib.OK, response.status_code)
532+ parsed_result = json.loads(response.content)
533+ self.assertIn('application/json', response['Content-Type'])
534+ self.assertEqual('diane', parsed_result['hostname'])
535+ self.assertNotEqual(0, len(parsed_result.get('system_id')))
536+ [diane] = Node.objects.filter(hostname='diane')
537+ self.assertEqual(architecture, diane.architecture)
538+
539+ def test_POST_new_creates_node_with_arch_only(self):
540+ architecture = factory.getRandomChoice(
541+ [choice for choice in ARCHITECTURE_CHOICES
542+ if choice[0].endswith('/generic')])
543+ response = self.client.post(
544+ self.get_uri('nodes/'),
545+ {
546+ 'op': 'new',
547+ 'hostname': 'diane',
548+ 'architecture': architecture.split('/')[0],
549+ 'after_commissioning_action':
550+ NODE_AFTER_COMMISSIONING_ACTION.DEFAULT,
551+ 'mac_addresses': ['aa:bb:cc:dd:ee:ff', '22:bb:cc:dd:ee:ff'],
552+ })
553+
554+ self.assertEqual(httplib.OK, response.status_code)
555+ parsed_result = json.loads(response.content)
556+ self.assertIn('application/json', response['Content-Type'])
557+ self.assertEqual('diane', parsed_result['hostname'])
558+ self.assertNotEqual(0, len(parsed_result.get('system_id')))
559+ [diane] = Node.objects.filter(hostname='diane')
560+ self.assertEqual(architecture, diane.architecture)
561+
562+ def test_POST_new_creates_node_with_subarchitecture(self):
563 # The API allows a Node to be created.
564 architecture = factory.getRandomChoice(ARCHITECTURE_CHOICES)
565 response = self.client.post(
566@@ -275,20 +319,39 @@
567 {
568 'op': 'new',
569 'hostname': 'diane',
570- 'architecture': architecture,
571+ 'architecture': architecture.split('/')[0],
572+ 'subarchitecture': architecture.split('/')[1],
573 'after_commissioning_action':
574 NODE_AFTER_COMMISSIONING_ACTION.DEFAULT,
575 'mac_addresses': ['aa:bb:cc:dd:ee:ff', '22:bb:cc:dd:ee:ff'],
576 })
577+
578+ self.assertEqual(httplib.OK, response.status_code)
579 parsed_result = json.loads(response.content)
580-
581- self.assertEqual(httplib.OK, response.status_code)
582 self.assertIn('application/json', response['Content-Type'])
583 self.assertEqual('diane', parsed_result['hostname'])
584 self.assertNotEqual(0, len(parsed_result.get('system_id')))
585 [diane] = Node.objects.filter(hostname='diane')
586 self.assertEqual(architecture, diane.architecture)
587
588+ def test_POST_new_fails_node_with_double_subarchitecture(self):
589+ architecture = factory.getRandomChoice(ARCHITECTURE_CHOICES)
590+ response = self.client.post(
591+ self.get_uri('nodes/'),
592+ {
593+ 'op': 'new',
594+ 'hostname': 'diane',
595+ 'architecture': architecture,
596+ 'subarchitecture': architecture.split('/')[1],
597+ 'after_commissioning_action':
598+ NODE_AFTER_COMMISSIONING_ACTION.DEFAULT,
599+ 'mac_addresses': ['aa:bb:cc:dd:ee:ff', '22:bb:cc:dd:ee:ff'],
600+ })
601+ self.assertEqual(httplib.BAD_REQUEST, response.status_code)
602+ self.assertIn('text/plain', response['Content-Type'])
603+ self.assertEqual("Subarchitecture cannot be specified twice.",
604+ response.content)
605+
606 def test_POST_new_power_type_defaults_to_asking_config(self):
607 architecture = factory.getRandomChoice(ARCHITECTURE_CHOICES)
608 response = self.client.post(
609@@ -1703,7 +1766,7 @@
610 status=NODE_STATUS.READY, architecture=ARCHITECTURE.i386)
611 response = self.client.post(self.get_uri('nodes/'), {
612 'op': 'acquire',
613- 'arch': 'i386',
614+ 'arch': 'i386/generic',
615 })
616 self.assertEqual(httplib.OK, response.status_code)
617 response_json = json.loads(response.content)
618@@ -2696,9 +2759,10 @@
619 def test_pxeconfig_defaults_to_i386_when_node_unknown(self):
620 # As a lowest-common-denominator, i386 is chosen when the node is not
621 # yet known to MAAS.
622+ expected_arch = tuple(ARCHITECTURE.i386.split('/'))
623 params_out = self.get_pxeconfig()
624- self.assertEqual(ARCHITECTURE.i386, params_out["arch"])
625- self.assertEqual("generic", params_out["subarch"])
626+ observed_arch = params_out["arch"], params_out["subarch"]
627+ self.assertEqual(expected_arch, observed_arch)
628
629 def get_without_param(self, param):
630 """Request a `pxeconfig()` response, but omit `param` from request."""
631
632=== modified file 'src/maasserver/tests/test_node.py'
633--- src/maasserver/tests/test_node.py 2012-09-28 14:38:32 +0000
634+++ src/maasserver/tests/test_node.py 2012-09-28 17:03:22 +0000
635@@ -702,7 +702,7 @@
636 nodes = [self.make_node(architecture=s)
637 for s in (ARCHITECTURE.amd64, ARCHITECTURE.i386)]
638 available_node = Node.objects.get_available_node_for_acquisition(
639- user, {'architecture': "i386"})
640+ user, {'architecture': "i386/generic"})
641 self.assertEqual(ARCHITECTURE.i386, available_node.architecture)
642 self.assertEqual(nodes[1], available_node)
643
644
645=== modified file 'src/maasserver/tests/test_node_constraint_filter.py'
646--- src/maasserver/tests/test_node_constraint_filter.py 2012-09-28 14:38:32 +0000
647+++ src/maasserver/tests/test_node_constraint_filter.py 2012-09-28 17:03:22 +0000
648@@ -42,9 +42,10 @@
649
650 def test_architecture(self):
651 node1 = factory.make_node(architecture=ARCHITECTURE.i386)
652- node2 = factory.make_node(architecture=ARCHITECTURE.armhf)
653- self.assertConstrainedNodes([node1], {'architecture': 'i386'})
654- self.assertConstrainedNodes([node2], {'architecture': 'armhf'})
655+ node2 = factory.make_node(architecture=ARCHITECTURE.armhf_highbank)
656+ self.assertConstrainedNodes([node1], {'architecture': 'i386/generic'})
657+ self.assertConstrainedNodes(
658+ [node2], {'architecture': 'armhf/highbank'})
659 self.assertConstrainedNodes([], {'architecture': 'sparc'})
660
661 def test_cpu_count(self):
662@@ -95,9 +96,10 @@
663 node_big.tags.add(tag_big)
664 node_small = factory.make_node(architecture=ARCHITECTURE.i386)
665 ignore_unused(node_small)
666- node_big_arm = factory.make_node(architecture=ARCHITECTURE.armhf)
667+ node_big_arm = factory.make_node(
668+ architecture=ARCHITECTURE.armhf_highbank)
669 node_big_arm.tags.add(tag_big)
670 self.assertConstrainedNodes([node_big, node_big_arm],
671 {'tags': 'big'})
672- self.assertConstrainedNodes([node_big],
673- {'architecture': 'i386', 'tags': 'big'})
674+ self.assertConstrainedNodes(
675+ [node_big], {'architecture': 'i386/generic', 'tags': 'big'})
676
677=== modified file 'src/maasserver/tests/test_preseed.py'
678--- src/maasserver/tests/test_preseed.py 2012-08-21 20:27:47 +0000
679+++ src/maasserver/tests/test_preseed.py 2012-09-28 17:03:22 +0000
680@@ -64,27 +64,7 @@
681 prefix = factory.getRandomString()
682 release = factory.getRandomString()
683 node = factory.make_node(hostname=hostname)
684- self.assertSequenceEqual(
685- [
686- '%s_%s_%s_%s' % (prefix, node.architecture, release, hostname),
687- '%s_%s_%s' % (prefix, node.architecture, release),
688- '%s_%s' % (prefix, node.architecture),
689- '%s' % prefix,
690- 'generic',
691- ],
692- list(get_preseed_filenames(node, prefix, release, default=True)))
693-
694- def test_get_preseed_filenames_returns_filenames_with_subarch(self):
695- arch = factory.getRandomString()
696- subarch = factory.getRandomString()
697- fake_arch = '%s/%s' % (arch, subarch)
698- hostname = factory.getRandomString()
699- prefix = factory.getRandomString()
700- release = factory.getRandomString()
701- node = factory.make_node(hostname=hostname)
702- # Set an architecture of the form '%s/%s' i.e. with a
703- # sub-architecture.
704- node.architecture = fake_arch
705+ arch, subarch = node.architecture.split('/')
706 self.assertSequenceEqual(
707 [
708 '%s_%s_%s_%s_%s' % (prefix, arch, subarch, release, hostname),
709@@ -110,11 +90,13 @@
710 hostname = factory.getRandomString()
711 release = factory.getRandomString()
712 node = factory.make_node(hostname=hostname)
713+ arch, subarch = node.architecture.split('/')
714 self.assertSequenceEqual(
715 [
716- '%s_%s_%s' % (node.architecture, release, hostname),
717- '%s_%s' % (node.architecture, release),
718- '%s' % node.architecture,
719+ '%s_%s_%s_%s' % (arch, subarch, release, hostname),
720+ '%s_%s_%s' % (arch, subarch, release),
721+ '%s_%s' % (arch, subarch),
722+ '%s' % arch,
723 ],
724 list(get_preseed_filenames(node, '', release)))
725
726@@ -266,7 +248,8 @@
727 self.create_template(self.location, prefix)
728 node = factory.make_node(hostname=factory.getRandomString())
729 node_template_name = "%s_%s_%s_%s" % (
730- prefix, node.architecture, release, node.hostname)
731+ prefix, node.architecture.replace('/', '_'),
732+ release, node.hostname)
733 # Create the node-specific template.
734 content = self.create_template(self.location, node_template_name)
735 template = load_preseed_template(node, prefix, release)