Merge lp:~julian-edwards/maas/backport-r1371 into lp:maas/1.2

Proposed by Julian Edwards
Status: Merged
Approved by: Julian Edwards
Approved revision: no longer in the source branch.
Merged at revision: 1332
Proposed branch: lp:~julian-edwards/maas/backport-r1371
Merge into: lp:maas/1.2
Diff against target: 1902 lines (+939/-246)
30 files modified
src/maasserver/api.py (+17/-7)
src/maasserver/compose_preseed.py (+9/-6)
src/maasserver/dhcp.py (+1/-9)
src/maasserver/dns.py (+3/-4)
src/maasserver/fields.py (+1/-25)
src/maasserver/migrations/0045_add_tag_kernel_opts.py (+203/-0)
src/maasserver/migrations/0046_add_nodegroup_maas_url.py (+205/-0)
src/maasserver/models/nodegroup.py (+5/-0)
src/maasserver/preseed.py (+70/-37)
src/maasserver/server_address.py (+17/-11)
src/maasserver/templates/maasserver/enlist_preseed.html (+1/-0)
src/maasserver/testing/factory.py (+2/-1)
src/maasserver/tests/test_api.py (+143/-2)
src/maasserver/tests/test_dhcp.py (+16/-5)
src/maasserver/tests/test_dns.py (+12/-1)
src/maasserver/tests/test_fields.py (+0/-47)
src/maasserver/tests/test_forms.py (+0/-8)
src/maasserver/tests/test_preseed.py (+53/-15)
src/maasserver/tests/test_server_address.py (+17/-3)
src/maasserver/tests/test_views_nodes.py (+16/-0)
src/maasserver/utils/__init__.py (+27/-14)
src/maasserver/utils/tests/test_utils.py (+63/-40)
src/maasserver/views/nodes.py (+9/-1)
src/maastesting/factory.py (+7/-0)
src/metadataserver/api.py (+7/-2)
src/metadataserver/tests/test_api.py (+35/-2)
src/provisioningserver/dhcp/tests/test_config.py (+0/-2)
src/provisioningserver/tests/test_auth.py (+0/-2)
src/provisioningserver/tests/test_start_cluster_controller.py (+0/-1)
src/provisioningserver/tests/test_tasks.py (+0/-1)
To merge this branch: bzr merge lp:~julian-edwards/maas/backport-r1371
Reviewer Review Type Date Requested Status
Julian Edwards (community) Approve
Review via email: mp+137751@code.launchpad.net

Commit message

Fix metadata address mentioned in the preseed. Additionally fix value of 'domain-name-servers' in the dhcp config for non-local clusters. (Backport of r1371, r1376, r1378, r1379, r1380, r1381, r1383, r1384)

Description of the change

Mega branch that covers all the backports needed for bug 1081701 and bug 1081696 (I had to do both together because the latter introduced changes that later revisions in the former depended on). It's mechanical but some conflicts had to be resolved where backported branches were changing things that are only in trunk.

To post a comment you must log in.
Revision history for this message
Julian Edwards (julian-edwards) :
review: Approve

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-12-04 00:40:35 +0000
3+++ src/maasserver/api.py 2012-12-04 04:32:20 +0000
4@@ -165,7 +165,7 @@
5 from maasserver.utils import (
6 absolute_reverse,
7 build_absolute_uri,
8- get_origin_ip,
9+ find_nodegroup,
10 map_enum,
11 strip_domain,
12 )
13@@ -649,9 +649,9 @@
14 # If 'nodegroup' is not explicitely specified, get the origin of the
15 # request to figure out which nodegroup the new node should be
16 # attached to.
17- origin_ip = get_origin_ip(request)
18- if origin_ip is not None:
19- altered_query_data['nodegroup'] = origin_ip
20+ nodegroup = find_nodegroup(request)
21+ if nodegroup is not None:
22+ altered_query_data['nodegroup'] = nodegroup
23
24 Form = get_node_create_form(request.user)
25 form = Form(altered_query_data)
26@@ -1179,6 +1179,7 @@
27 raise ValidationError(form.errors)
28 else:
29 if existing_nodegroup.status == NODEGROUP_STATUS.ACCEPTED:
30+ update_nodegroup_maas_url(existing_nodegroup, request)
31 # The nodegroup exists and is validated, return the RabbitMQ
32 return get_celery_credentials()
33 elif existing_nodegroup.status == NODEGROUP_STATUS.REJECTED:
34@@ -1188,6 +1189,13 @@
35 "Awaiting admin approval.", status=httplib.ACCEPTED)
36
37
38+def update_nodegroup_maas_url(nodegroup, request):
39+ """Update `nodegroup.maas_url` from the given `request`."""
40+ path = request.META["SCRIPT_NAME"]
41+ nodegroup.maas_url = build_absolute_uri(request, path)
42+ nodegroup.save()
43+
44+
45 class NodeGroupsHandler(OperationsHandler):
46 """Manage NodeGroups."""
47 anonymous = AnonNodeGroupsHandler
48@@ -1844,7 +1852,8 @@
49 # The node's hostname may include a domain, but we ignore that
50 # and use the one from the nodegroup instead.
51 hostname = strip_domain(node.hostname)
52- domain = node.nodegroup.name
53+ nodegroup = node.nodegroup
54+ domain = nodegroup.name
55 else:
56 try:
57 pxelinux_arch = request.GET['arch']
58@@ -1878,7 +1887,8 @@
59 # 1-1 mapping.
60 subarch = pxelinux_subarch
61
62- preseed_url = compose_enlistment_preseed_url()
63+ nodegroup = find_nodegroup(request)
64+ preseed_url = compose_enlistment_preseed_url(nodegroup=nodegroup)
65 hostname = 'maas-enlist'
66 domain = Config.objects.get_config('enlistment_domain')
67
68@@ -1888,7 +1898,7 @@
69 series = node.get_distro_series()
70
71 purpose = get_boot_purpose(node)
72- server_address = get_maas_facing_server_address()
73+ server_address = get_maas_facing_server_address(nodegroup=nodegroup)
74 cluster_address = get_mandatory_param(request.GET, "local")
75
76 params = KernelParameters(
77
78=== modified file 'src/maasserver/compose_preseed.py'
79--- src/maasserver/compose_preseed.py 2012-10-04 14:00:06 +0000
80+++ src/maasserver/compose_preseed.py 2012-12-04 04:32:20 +0000
81@@ -21,7 +21,7 @@
82 import yaml
83
84
85-def compose_cloud_init_preseed(token):
86+def compose_cloud_init_preseed(token, base_url=''):
87 """Compose the preseed value for a node in any state but Commissioning."""
88 credentials = urlencode({
89 'oauth_consumer_key': token.consumer.key,
90@@ -40,7 +40,8 @@
91 # ks_meta, and it gets fed straight into debconf.
92 preseed_items = [
93 ('datasources', 'multiselect', 'MAAS'),
94- ('maas-metadata-url', 'string', absolute_reverse('metadata')),
95+ ('maas-metadata-url', 'string', absolute_reverse(
96+ 'metadata', base_url=base_url)),
97 ('maas-metadata-credentials', 'string', credentials),
98 ('local-cloud-config', 'string', local_config)
99 ]
100@@ -54,12 +55,13 @@
101 for item_name, item_type, item_value in preseed_items)
102
103
104-def compose_commissioning_preseed(token):
105+def compose_commissioning_preseed(token, base_url=''):
106 """Compose the preseed value for a Commissioning node."""
107 return "#cloud-config\n%s" % yaml.safe_dump({
108 'datasource': {
109 'MAAS': {
110- 'metadata_url': absolute_reverse('metadata'),
111+ 'metadata_url': absolute_reverse(
112+ 'metadata', base_url=base_url),
113 'consumer_key': token.consumer.key,
114 'token_key': token.key,
115 'token_secret': token.secret,
116@@ -86,7 +88,8 @@
117 # Circular import.
118 from metadataserver.models import NodeKey
119 token = NodeKey.objects.get_token_for_node(node)
120+ base_url = node.nodegroup.maas_url
121 if node.status == NODE_STATUS.COMMISSIONING:
122- return compose_commissioning_preseed(token)
123+ return compose_commissioning_preseed(token, base_url)
124 else:
125- return compose_cloud_init_preseed(token)
126+ return compose_cloud_init_preseed(token, base_url)
127
128=== modified file 'src/maasserver/dhcp.py'
129--- src/maasserver/dhcp.py 2012-10-11 09:34:16 +0000
130+++ src/maasserver/dhcp.py 2012-12-04 04:32:20 +0000
131@@ -20,7 +20,6 @@
132 NODEGROUP_STATUS,
133 NODEGROUPINTERFACE_MANAGEMENT,
134 )
135-from maasserver.server_address import get_maas_facing_server_address
136 from netaddr import IPAddress
137 from provisioningserver.tasks import (
138 restart_dhcp_server,
139@@ -52,12 +51,6 @@
140 # server.
141 nodegroup.ensure_dhcp_key()
142
143- # Use the server's address (which is where the central TFTP
144- # server is) for the next_server setting. We'll want to proxy
145- # it on the local worker later, and then we can use
146- # next_server=self.worker_ip.
147- next_server = get_maas_facing_server_address()
148-
149 interface = nodegroup.get_managed_interface()
150 subnet = str(
151 IPAddress(interface.ip_range_low) &
152@@ -66,13 +59,12 @@
153 options={'queue': nodegroup.work_queue})
154 task_kwargs = dict(
155 subnet=subnet,
156- next_server=next_server,
157 omapi_key=nodegroup.dhcp_key,
158 subnet_mask=interface.subnet_mask,
159 dhcp_interfaces=interface.interface,
160 broadcast_ip=interface.broadcast_ip,
161 router_ip=interface.router_ip,
162- dns_servers=get_dns_server_address(),
163+ dns_servers=get_dns_server_address(nodegroup),
164 ip_range_low=interface.ip_range_low,
165 ip_range_high=interface.ip_range_high,
166 callback=reload_dhcp_server_subtask,
167
168=== modified file 'src/maasserver/dns.py'
169--- src/maasserver/dns.py 2012-12-03 07:29:53 +0000
170+++ src/maasserver/dns.py 2012-12-04 04:32:20 +0000
171@@ -105,14 +105,13 @@
172 logging.getLogger('maas').warn(WARNING_MESSAGE % ip)
173
174
175-def get_dns_server_address():
176+def get_dns_server_address(nodegroup=None):
177 """Return the DNS server's IP address.
178
179- That address is derived from DEFAULT_MAAS_URL in order to get a sensible
180- default and at the same time give a possibility to the user to change this.
181+ That address is derived from DEFAULT_MAAS_URL or nodegroup.maas_url.
182 """
183 try:
184- ip = get_maas_facing_server_address()
185+ ip = get_maas_facing_server_address(nodegroup)
186 except socket.error as e:
187 raise DNSException(
188 "Unable to find MAAS server IP address: %s. "
189
190=== modified file 'src/maasserver/fields.py'
191--- src/maasserver/fields.py 2012-09-21 09:06:21 +0000
192+++ src/maasserver/fields.py 2012-12-04 04:32:20 +0000
193@@ -32,7 +32,6 @@
194 ModelChoiceField,
195 RegexField,
196 )
197-from maasserver.utils.orm import get_one
198 import psycopg2.extensions
199 from south.modelsinspector import add_introspection_rules
200
201@@ -85,25 +84,6 @@
202 else:
203 return "%s: %s" % (nodegroup.name, interface.ip)
204
205- def find_nodegroup(self, ip_address):
206- """Find the nodegroup whose subnet contains `ip_address`.
207-
208- The matching nodegroup may have multiple interfaces on the subnet,
209- but there can be only one matching nodegroup.
210- """
211- # Avoid circular imports.
212- from maasserver.models import NodeGroup
213-
214- return get_one(NodeGroup.objects.raw("""
215- SELECT *
216- FROM maasserver_nodegroup
217- WHERE id IN (
218- SELECT nodegroup_id
219- FROM maasserver_nodegroupinterface
220- WHERE (inet '%s' & subnet_mask) = (ip & subnet_mask)
221- )
222- """ % ip_address))
223-
224 def clean(self, value):
225 """Django method: provide expected output for various inputs.
226
227@@ -126,11 +106,7 @@
228 elif isinstance(value, bytes) and '.' not in value:
229 nodegroup_id = int(value)
230 else:
231- nodegroup = self.find_nodegroup(value)
232- if nodegroup is None:
233- raise ValidationError(
234- "No known subnet contains %s." % value)
235- nodegroup_id = nodegroup.id
236+ raise ValidationError("Invalid nodegroup: %s." % value)
237 return super(NodeGroupFormField, self).clean(nodegroup_id)
238
239
240
241=== added file 'src/maasserver/migrations/0045_add_tag_kernel_opts.py'
242--- src/maasserver/migrations/0045_add_tag_kernel_opts.py 1970-01-01 00:00:00 +0000
243+++ src/maasserver/migrations/0045_add_tag_kernel_opts.py 2012-12-04 04:32:20 +0000
244@@ -0,0 +1,203 @@
245+# -*- coding: utf-8 -*-
246+import datetime
247+from south.db import db
248+from south.v2 import SchemaMigration
249+from django.db import models
250+
251+
252+class Migration(SchemaMigration):
253+
254+ def forwards(self, orm):
255+ # Adding field 'Tag.kernel_opts'
256+ db.add_column(u'maasserver_tag', 'kernel_opts',
257+ self.gf('django.db.models.fields.TextField')(null=True, blank=True),
258+ keep_default=False)
259+
260+
261+ def backwards(self, orm):
262+ # Deleting field 'Tag.kernel_opts'
263+ db.delete_column(u'maasserver_tag', 'kernel_opts')
264+
265+
266+ models = {
267+ 'auth.group': {
268+ 'Meta': {'object_name': 'Group'},
269+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
270+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
271+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
272+ },
273+ 'auth.permission': {
274+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
275+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
276+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
277+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
278+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
279+ },
280+ 'auth.user': {
281+ 'Meta': {'object_name': 'User'},
282+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
283+ 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75', 'blank': 'True'}),
284+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
285+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
286+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
287+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
288+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
289+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
290+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
291+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
292+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
293+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
294+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
295+ },
296+ 'contenttypes.contenttype': {
297+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
298+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
299+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
300+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
301+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
302+ },
303+ u'maasserver.bootimage': {
304+ 'Meta': {'unique_together': "((u'nodegroup', u'architecture', u'subarchitecture', u'release', u'purpose'),)", 'object_name': 'BootImage'},
305+ 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
306+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
307+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
308+ 'purpose': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
309+ 'release': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
310+ 'subarchitecture': ('django.db.models.fields.CharField', [], {'max_length': '255'})
311+ },
312+ u'maasserver.componenterror': {
313+ 'Meta': {'object_name': 'ComponentError'},
314+ 'component': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
315+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
316+ 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000'}),
317+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
318+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
319+ },
320+ u'maasserver.config': {
321+ 'Meta': {'object_name': 'Config'},
322+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
323+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
324+ 'value': ('maasserver.fields.JSONObjectField', [], {'null': 'True'})
325+ },
326+ u'maasserver.dhcplease': {
327+ 'Meta': {'object_name': 'DHCPLease'},
328+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
329+ 'ip': ('django.db.models.fields.IPAddressField', [], {'unique': 'True', 'max_length': '15'}),
330+ 'mac': ('maasserver.fields.MACAddressField', [], {}),
331+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"})
332+ },
333+ u'maasserver.filestorage': {
334+ 'Meta': {'object_name': 'FileStorage'},
335+ 'content': ('metadataserver.fields.BinaryField', [], {}),
336+ 'filename': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
337+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
338+ },
339+ u'maasserver.macaddress': {
340+ 'Meta': {'object_name': 'MACAddress'},
341+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
342+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
343+ 'mac_address': ('maasserver.fields.MACAddressField', [], {'unique': 'True'}),
344+ 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}),
345+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
346+ },
347+ u'maasserver.node': {
348+ 'Meta': {'object_name': 'Node'},
349+ 'after_commissioning_action': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
350+ 'architecture': ('django.db.models.fields.CharField', [], {'default': "u'i386/generic'", 'max_length': '31'}),
351+ 'cpu_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
352+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
353+ 'distro_series': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '10', 'null': 'True', 'blank': 'True'}),
354+ 'error': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
355+ 'hardware_details': ('maasserver.fields.XMLField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
356+ 'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'unique': 'True', 'max_length': '255', 'blank': 'True'}),
357+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
358+ 'memory': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
359+ 'netboot': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
360+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']", 'null': 'True'}),
361+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
362+ 'power_parameters': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
363+ 'power_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '10', 'blank': 'True'}),
364+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '10'}),
365+ 'system_id': ('django.db.models.fields.CharField', [], {'default': "u'node-a776a79c-298b-11e2-90d1-080027748fea'", 'unique': 'True', 'max_length': '41'}),
366+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Tag']", 'symmetrical': 'False'}),
367+ 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'null': 'True'}),
368+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
369+ },
370+ u'maasserver.nodegroup': {
371+ 'Meta': {'object_name': 'NodeGroup'},
372+ 'api_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '18'}),
373+ 'api_token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'unique': 'True'}),
374+ 'cluster_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100', 'blank': 'True'}),
375+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
376+ 'dhcp_key': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
377+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
378+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}),
379+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
380+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
381+ 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'})
382+ },
383+ u'maasserver.nodegroupinterface': {
384+ 'Meta': {'unique_together': "((u'nodegroup', u'interface'),)", 'object_name': 'NodeGroupInterface'},
385+ 'broadcast_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
386+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
387+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
388+ 'interface': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
389+ 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
390+ 'ip_range_high': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
391+ 'ip_range_low': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
392+ 'management': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
393+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
394+ 'router_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
395+ 'subnet_mask': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
396+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
397+ },
398+ u'maasserver.sshkey': {
399+ 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSHKey'},
400+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
401+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
402+ 'key': ('django.db.models.fields.TextField', [], {}),
403+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
404+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
405+ },
406+ u'maasserver.tag': {
407+ 'Meta': {'object_name': 'Tag'},
408+ 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
409+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
410+ 'definition': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
411+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
412+ 'kernel_opts': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
413+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
414+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
415+ },
416+ u'maasserver.userprofile': {
417+ 'Meta': {'object_name': 'UserProfile'},
418+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
419+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
420+ },
421+ 'piston.consumer': {
422+ 'Meta': {'object_name': 'Consumer'},
423+ 'description': ('django.db.models.fields.TextField', [], {}),
424+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
425+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
426+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
427+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
428+ 'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '16'}),
429+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'consumers'", 'null': 'True', 'to': "orm['auth.User']"})
430+ },
431+ 'piston.token': {
432+ 'Meta': {'object_name': 'Token'},
433+ 'callback': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
434+ 'callback_confirmed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
435+ 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Consumer']"}),
436+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
437+ 'is_approved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
438+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
439+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
440+ 'timestamp': ('django.db.models.fields.IntegerField', [], {'default': '1352369055L'}),
441+ 'token_type': ('django.db.models.fields.IntegerField', [], {}),
442+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'tokens'", 'null': 'True', 'to': "orm['auth.User']"}),
443+ 'verifier': ('django.db.models.fields.CharField', [], {'max_length': '10'})
444+ }
445+ }
446+
447+ complete_apps = ['maasserver']
448\ No newline at end of file
449
450=== added file 'src/maasserver/migrations/0046_add_nodegroup_maas_url.py'
451--- src/maasserver/migrations/0046_add_nodegroup_maas_url.py 1970-01-01 00:00:00 +0000
452+++ src/maasserver/migrations/0046_add_nodegroup_maas_url.py 2012-12-04 04:32:20 +0000
453@@ -0,0 +1,205 @@
454+# -*- coding: utf-8 -*-
455+import datetime
456+
457+from django.db import models
458+from south.db import db
459+from south.v2 import SchemaMigration
460+
461+
462+class Migration(SchemaMigration):
463+
464+ def forwards(self, orm):
465+ # Adding field 'NodeGroup.maas_url'
466+ db.add_column(u'maasserver_nodegroup', 'maas_url',
467+ self.gf('django.db.models.fields.CharField')(default=u'', max_length=255, blank=True),
468+ keep_default=False)
469+
470+
471+ def backwards(self, orm):
472+ # Deleting field 'NodeGroup.maas_url'
473+ db.delete_column(u'maasserver_nodegroup', 'maas_url')
474+
475+
476+ models = {
477+ 'auth.group': {
478+ 'Meta': {'object_name': 'Group'},
479+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
480+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
481+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
482+ },
483+ 'auth.permission': {
484+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
485+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
486+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
487+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
488+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
489+ },
490+ 'auth.user': {
491+ 'Meta': {'object_name': 'User'},
492+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
493+ 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75', 'blank': 'True'}),
494+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
495+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
496+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
497+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
498+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
499+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
500+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
501+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
502+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
503+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
504+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
505+ },
506+ 'contenttypes.contenttype': {
507+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
508+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
509+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
510+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
511+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
512+ },
513+ u'maasserver.bootimage': {
514+ 'Meta': {'unique_together': "((u'nodegroup', u'architecture', u'subarchitecture', u'release', u'purpose'),)", 'object_name': 'BootImage'},
515+ 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
516+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
517+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
518+ 'purpose': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
519+ 'release': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
520+ 'subarchitecture': ('django.db.models.fields.CharField', [], {'max_length': '255'})
521+ },
522+ u'maasserver.componenterror': {
523+ 'Meta': {'object_name': 'ComponentError'},
524+ 'component': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
525+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
526+ 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000'}),
527+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
528+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
529+ },
530+ u'maasserver.config': {
531+ 'Meta': {'object_name': 'Config'},
532+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
533+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
534+ 'value': ('maasserver.fields.JSONObjectField', [], {'null': 'True'})
535+ },
536+ u'maasserver.dhcplease': {
537+ 'Meta': {'object_name': 'DHCPLease'},
538+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
539+ 'ip': ('django.db.models.fields.IPAddressField', [], {'unique': 'True', 'max_length': '15'}),
540+ 'mac': ('maasserver.fields.MACAddressField', [], {}),
541+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"})
542+ },
543+ u'maasserver.filestorage': {
544+ 'Meta': {'object_name': 'FileStorage'},
545+ 'content': ('metadataserver.fields.BinaryField', [], {}),
546+ 'filename': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
547+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
548+ },
549+ u'maasserver.macaddress': {
550+ 'Meta': {'object_name': 'MACAddress'},
551+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
552+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
553+ 'mac_address': ('maasserver.fields.MACAddressField', [], {'unique': 'True'}),
554+ 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}),
555+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
556+ },
557+ u'maasserver.node': {
558+ 'Meta': {'object_name': 'Node'},
559+ 'after_commissioning_action': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
560+ 'architecture': ('django.db.models.fields.CharField', [], {'default': "u'i386/generic'", 'max_length': '31'}),
561+ 'cpu_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
562+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
563+ 'distro_series': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '10', 'null': 'True', 'blank': 'True'}),
564+ 'error': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
565+ 'hardware_details': ('maasserver.fields.XMLField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
566+ 'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'unique': 'True', 'max_length': '255', 'blank': 'True'}),
567+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
568+ 'memory': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
569+ 'netboot': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
570+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']", 'null': 'True'}),
571+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
572+ 'power_parameters': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
573+ 'power_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '10', 'blank': 'True'}),
574+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '10'}),
575+ 'system_id': ('django.db.models.fields.CharField', [], {'default': "u'node-2cd56f00-3548-11e2-b1cb-9c4e363b1c94'", 'unique': 'True', 'max_length': '41'}),
576+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Tag']", 'symmetrical': 'False'}),
577+ 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'null': 'True'}),
578+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
579+ },
580+ u'maasserver.nodegroup': {
581+ 'Meta': {'object_name': 'NodeGroup'},
582+ 'api_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '18'}),
583+ 'api_token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'unique': 'True'}),
584+ 'cluster_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100', 'blank': 'True'}),
585+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
586+ 'dhcp_key': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
587+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
588+ 'maas_url': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
589+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}),
590+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
591+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
592+ 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'})
593+ },
594+ u'maasserver.nodegroupinterface': {
595+ 'Meta': {'unique_together': "((u'nodegroup', u'interface'),)", 'object_name': 'NodeGroupInterface'},
596+ 'broadcast_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
597+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
598+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
599+ 'interface': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
600+ 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
601+ 'ip_range_high': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
602+ 'ip_range_low': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
603+ 'management': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
604+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
605+ 'router_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
606+ 'subnet_mask': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
607+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
608+ },
609+ u'maasserver.sshkey': {
610+ 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSHKey'},
611+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
612+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
613+ 'key': ('django.db.models.fields.TextField', [], {}),
614+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
615+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
616+ },
617+ u'maasserver.tag': {
618+ 'Meta': {'object_name': 'Tag'},
619+ 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
620+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
621+ 'definition': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
622+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
623+ 'kernel_opts': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
624+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
625+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
626+ },
627+ u'maasserver.userprofile': {
628+ 'Meta': {'object_name': 'UserProfile'},
629+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
630+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
631+ },
632+ 'piston.consumer': {
633+ 'Meta': {'object_name': 'Consumer'},
634+ 'description': ('django.db.models.fields.TextField', [], {}),
635+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
636+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
637+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
638+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
639+ 'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '16'}),
640+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'consumers'", 'null': 'True', 'to': "orm['auth.User']"})
641+ },
642+ 'piston.token': {
643+ 'Meta': {'object_name': 'Token'},
644+ 'callback': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
645+ 'callback_confirmed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
646+ 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Consumer']"}),
647+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
648+ 'is_approved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
649+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
650+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
651+ 'timestamp': ('django.db.models.fields.IntegerField', [], {'default': '1353659487L'}),
652+ 'token_type': ('django.db.models.fields.IntegerField', [], {}),
653+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'tokens'", 'null': 'True', 'to': "orm['auth.User']"}),
654+ 'verifier': ('django.db.models.fields.CharField', [], {'max_length': '10'})
655+ }
656+ }
657+
658+ complete_apps = ['maasserver']
659\ No newline at end of file
660
661=== modified file 'src/maasserver/models/nodegroup.py'
662--- src/maasserver/models/nodegroup.py 2012-12-04 00:25:39 +0000
663+++ src/maasserver/models/nodegroup.py 2012-12-04 04:32:20 +0000
664@@ -165,6 +165,11 @@
665 uuid = CharField(
666 max_length=36, unique=True, null=False, blank=False, editable=True)
667
668+ # The URL where the cluster controller can access the region
669+ # controller.
670+ maas_url = CharField(
671+ blank=True, editable=False, max_length=255, default='')
672+
673 def __repr__(self):
674 return "<NodeGroup %s>" % self.uuid
675
676
677=== modified file 'src/maasserver/preseed.py'
678--- src/maasserver/preseed.py 2012-11-15 12:05:06 +0000
679+++ src/maasserver/preseed.py 2012-12-04 04:32:20 +0000
680@@ -37,22 +37,26 @@
681 GENERIC_FILENAME = 'generic'
682
683
684-def get_enlist_preseed():
685+def get_enlist_preseed(nodegroup=None):
686 """Return the enlistment preseed.
687
688+ :param nodegroup: The nodegroup used to generate the preseed.
689 :return: The rendered preseed string.
690 :rtype: basestring.
691 """
692- return render_preseed(None, PRESEED_TYPE.ENLIST)
693-
694-
695-def get_enlist_userdata():
696+ return render_enlistment_preseed(
697+ PRESEED_TYPE.ENLIST, nodegroup=nodegroup)
698+
699+
700+def get_enlist_userdata(nodegroup=None):
701 """Return the enlistment preseed.
702
703+ :param nodegroup: The nodegroup used to generate the preseed.
704 :return: The rendered enlistment user-data string.
705 :rtype: basestring.
706 """
707- return render_preseed(None, PRESEED_TYPE.ENLIST_USERDATA)
708+ return render_enlistment_preseed(
709+ PRESEED_TYPE.ENLIST_USERDATA, nodegroup=nodegroup)
710
711
712 def get_preseed(node):
713@@ -200,42 +204,64 @@
714 return get_template(prefix, None, default=True)
715
716
717-def get_preseed_context(node, release=''):
718- """Return the context dictionary to be used to render preseed templates
719- for this node.
720+def get_preseed_context(release='', nodegroup=None):
721+ """Return the node-independent context dictionary to be used to render
722+ preseed templates.
723
724- :param node: See `get_preseed_filenames`.
725- :param prefix: See `get_preseed_filenames`.
726 :param release: See `get_preseed_filenames`.
727+ :param nodegroup: The nodegroup used to generate the preseed.
728 :return: The context dictionary.
729 :rtype: dict.
730 """
731- server_host = get_maas_facing_server_host()
732- context = {
733+ server_host = get_maas_facing_server_host(nodegroup=nodegroup)
734+ base_url = nodegroup.maas_url if nodegroup is not None else None
735+ return {
736 'release': release,
737 'server_host': server_host,
738- 'server_url': absolute_reverse('nodes_handler'),
739- 'metadata_enlist_url': absolute_reverse('enlist'),
740- }
741- if node is not None:
742- # Create the url and the url-data (POST parameters) used to turn off
743- # PXE booting once the install of the node is finished.
744- node_disable_pxe_url = absolute_reverse(
745- 'metadata-node-by-id', args=['latest', node.system_id])
746- node_disable_pxe_data = urlencode({'op': 'netboot_off'})
747- node_context = {
748- 'node': node,
749- 'preseed_data': compose_preseed(node),
750- 'node_disable_pxe_url': node_disable_pxe_url,
751- 'node_disable_pxe_data': node_disable_pxe_data,
752- }
753- context.update(node_context)
754-
755- return context
756+ 'server_url': absolute_reverse('nodes_handler', base_url=base_url),
757+ 'metadata_enlist_url': absolute_reverse('enlist', base_url=base_url),
758+ }
759+
760+
761+def get_node_preseed_context(node, release=''):
762+ """Return the node-dependent context dictionary to be used to render
763+ preseed templates.
764+
765+ :param node: See `get_preseed_filenames`.
766+ :param release: See `get_preseed_filenames`.
767+ :return: The context dictionary.
768+ :rtype: dict.
769+ """
770+ # Create the url and the url-data (POST parameters) used to turn off
771+ # PXE booting once the install of the node is finished.
772+ node_disable_pxe_url = absolute_reverse(
773+ 'metadata-node-by-id', args=['latest', node.system_id],
774+ base_url=node.nodegroup.maas_url)
775+ node_disable_pxe_data = urlencode({'op': 'netboot_off'})
776+ return {
777+ 'node': node,
778+ 'preseed_data': compose_preseed(node),
779+ 'node_disable_pxe_url': node_disable_pxe_url,
780+ 'node_disable_pxe_data': node_disable_pxe_data,
781+ }
782+
783+
784+def render_enlistment_preseed(prefix, release='', nodegroup=None):
785+ """Return the enlistment preseed.
786+
787+ :param prefix: See `get_preseed_filenames`.
788+ :param release: See `get_preseed_filenames`.
789+ :param nodegroup: The nodegroup used to generate the preseed.
790+ :return: The rendered preseed string.
791+ :rtype: basestring.
792+ """
793+ template = load_preseed_template(None, prefix, release)
794+ context = get_preseed_context(release, nodegroup=nodegroup)
795+ return template.substitute(**context)
796
797
798 def render_preseed(node, prefix, release=''):
799- """Find and load a `PreseedTemplate` for the given node.
800+ """Return the preseed for the given node.
801
802 :param node: See `get_preseed_filenames`.
803 :param prefix: See `get_preseed_filenames`.
804@@ -244,23 +270,30 @@
805 :rtype: basestring.
806 """
807 template = load_preseed_template(node, prefix, release)
808- context = get_preseed_context(node, release)
809+ nodegroup = node.nodegroup
810+ context = get_preseed_context(release, nodegroup=nodegroup)
811+ context.update(get_node_preseed_context(node, release))
812 return template.substitute(**context)
813
814
815-def compose_enlistment_preseed_url():
816- """Compose enlistment preseed URL."""
817+def compose_enlistment_preseed_url(nodegroup=None):
818+ """Compose enlistment preseed URL.
819+
820+ :param nodegroup: The nodegroup used to generate the preseed.
821+ """
822 # Always uses the latest version of the metadata API.
823+ base_url = nodegroup.maas_url if nodegroup is not None else None
824 version = 'latest'
825 return absolute_reverse(
826 'metadata-enlist-preseed', args=[version],
827- query={'op': 'get_enlist_preseed'})
828+ query={'op': 'get_enlist_preseed'}, base_url=base_url)
829
830
831 def compose_preseed_url(node):
832 """Compose a metadata URL for `node`'s preseed data."""
833 # Always uses the latest version of the metadata API.
834 version = 'latest'
835+ base_url = node.nodegroup.maas_url
836 return absolute_reverse(
837 'metadata-node-by-id', args=[version, node.system_id],
838- query={'op': 'get_preseed'})
839+ query={'op': 'get_preseed'}, base_url=base_url)
840
841=== modified file 'src/maasserver/server_address.py'
842--- src/maasserver/server_address.py 2012-08-06 10:11:02 +0000
843+++ src/maasserver/server_address.py 2012-12-04 04:32:20 +0000
844@@ -22,23 +22,29 @@
845 from django.conf import settings
846
847
848-def get_maas_facing_server_host():
849+def get_maas_facing_server_host(nodegroup=None):
850 """Return configured MAAS server hostname, for use by nodes or workers.
851
852- :return: Hostname or IP address, exactly as configured in the
853- DEFAULT_MAAS_URL setting.
854+ :param nodegroup: The nodegroup from the point of view of which the
855+ server host should be computed.
856+ :return: Hostname or IP address, as configured in the DEFAULT_MAAS_URL
857+ setting or as configured on nodegroup.maas_url.
858 """
859- return urlparse(settings.DEFAULT_MAAS_URL).hostname
860-
861-
862-def get_maas_facing_server_address():
863+ if nodegroup is None or not nodegroup.maas_url:
864+ maas_url = settings.DEFAULT_MAAS_URL
865+ else:
866+ maas_url = nodegroup.maas_url
867+ return urlparse(maas_url).hostname
868+
869+
870+def get_maas_facing_server_address(nodegroup=None):
871 """Return address where nodes and workers can reach the MAAS server.
872
873- The address is taken from DEFAULT_MAAS_URL, which in turn is based on the
874- server's primary IP address by default, but can be overridden for
875- multi-interface servers where this guess is wrong.
876+ The address is taken from DEFAULT_MAAS_URL or nodegroup.maas_url.
877
878+ :param nodegroup: The nodegroup from the point of view of which the
879+ server address should be computed.
880 :return: An IP address. If the configured URL uses a hostname, this
881 function will resolve that hostname.
882 """
883- return gethostbyname(get_maas_facing_server_host())
884+ return gethostbyname(get_maas_facing_server_host(nodegroup))
885
886=== modified file 'src/maasserver/templates/maasserver/enlist_preseed.html'
887--- src/maasserver/templates/maasserver/enlist_preseed.html 2012-06-22 07:50:12 +0000
888+++ src/maasserver/templates/maasserver/enlist_preseed.html 2012-12-04 04:32:20 +0000
889@@ -5,6 +5,7 @@
890 {% block page-title %}Enlistment preseed{% endblock %}
891
892 {% block content %}
893+ {{ warning_message }}
894 <pre>
895 {{ preseed }}
896 </pre>
897
898=== modified file 'src/maasserver/testing/factory.py'
899--- src/maasserver/testing/factory.py 2012-11-16 13:50:43 +0000
900+++ src/maasserver/testing/factory.py 2012-12-04 04:32:20 +0000
901@@ -157,7 +157,7 @@
902 router_ip=None, network=None, subnet_mask=None,
903 broadcast_ip=None, ip_range_low=None,
904 ip_range_high=None, interface=None, management=None,
905- status=None, **kwargs):
906+ status=None, maas_url='', **kwargs):
907 """Create a :class:`NodeGroup`.
908
909 If network (an instance of IPNetwork) is provided, use it to populate
910@@ -182,6 +182,7 @@
911 ng = NodeGroup.objects.new(
912 name=name, uuid=uuid, **interface_settings)
913 ng.status = status
914+ ng.maas_url = maas_url
915 ng.save()
916 return ng
917
918
919=== modified file 'src/maasserver/tests/test_api.py'
920--- src/maasserver/tests/test_api.py 2012-12-04 01:02:21 +0000
921+++ src/maasserver/tests/test_api.py 2012-12-04 04:32:20 +0000
922@@ -49,7 +49,10 @@
923 EnvironmentVariableFixture,
924 Fixture,
925 )
926-from maasserver import api
927+from maasserver import (
928+ api,
929+ server_address,
930+ )
931 from maasserver.api import (
932 describe,
933 DISPLAYED_NODEGROUP_FIELDS,
934@@ -149,6 +152,7 @@
935 from testtools.matchers import (
936 AfterPreprocessing,
937 AllMatch,
938+ Annotate,
939 Contains,
940 Equals,
941 Is,
942@@ -758,7 +762,7 @@
943 NODE_AFTER_COMMISSIONING_ACTION.DEFAULT,
944 'mac_addresses': [factory.getRandomMACAddress()],
945 },
946- HTTP_HOST=origin_ip + ':90')
947+ REMOTE_ADDR=origin_ip)
948 self.assertEqual(httplib.OK, response.status_code, response.content)
949 parsed_result = json.loads(response.content)
950 node = Node.objects.get(system_id=parsed_result.get('system_id'))
951@@ -3497,6 +3501,43 @@
952 compose_enlistment_preseed_url(),
953 json.loads(response.content)["preseed_url"])
954
955+ def test_pxeconfig_enlistment_preseed_url_detects_request_origin(self):
956+ self.silence_get_ephemeral_name()
957+ hostname = factory.make_hostname()
958+ ng_url = 'http://%s' % hostname
959+ network = IPNetwork("10.1.1/24")
960+ ip = factory.getRandomIPInNetwork(network)
961+ self.patch(server_address, 'gethostbyname', Mock(return_value=ip))
962+ factory.make_node_group(maas_url=ng_url, network=network)
963+ params = self.get_default_params()
964+
965+ # Simulate that the request originates from ip by setting
966+ # 'REMOTE_ADDR'.
967+ response = self.client.get(
968+ reverse('pxeconfig'), params, REMOTE_ADDR=ip)
969+ self.assertThat(
970+ json.loads(response.content)["preseed_url"],
971+ StartsWith(ng_url))
972+
973+ def test_pxeconfig_enlistment_log_host_url_detects_request_origin(self):
974+ self.silence_get_ephemeral_name()
975+ hostname = factory.make_hostname()
976+ ng_url = 'http://%s' % hostname
977+ network = IPNetwork("10.1.1/24")
978+ ip = factory.getRandomIPInNetwork(network)
979+ mock = self.patch(
980+ server_address, 'gethostbyname', Mock(return_value=ip))
981+ factory.make_node_group(maas_url=ng_url, network=network)
982+ params = self.get_default_params()
983+
984+ # Simulate that the request originates from ip by setting
985+ # 'REMOTE_ADDR'.
986+ response = self.client.get(
987+ reverse('pxeconfig'), params, REMOTE_ADDR=ip)
988+ self.assertEqual(
989+ (ip, hostname),
990+ (json.loads(response.content)["log_host"], mock.call_args[0][0]))
991+
992 def test_pxeconfig_has_preseed_url_for_known_node(self):
993 params = self.get_mac_params()
994 node = MACAddress.objects.get(mac_address=params['mac']).node
995@@ -3505,6 +3546,25 @@
996 compose_preseed_url(node),
997 json.loads(response.content)["preseed_url"])
998
999+ def test_preseed_url_for_known_node_uses_nodegroup_maas_url(self):
1000+ ng_url = 'http://%s' % factory.make_name('host')
1001+ network = IPNetwork("10.1.1/24")
1002+ ip = factory.getRandomIPInNetwork(network)
1003+ self.patch(server_address, 'gethostbyname', Mock(return_value=ip))
1004+ nodegroup = factory.make_node_group(maas_url=ng_url, network=network)
1005+ params = self.get_mac_params()
1006+ node = MACAddress.objects.get(mac_address=params['mac']).node
1007+ node.nodegroup = nodegroup
1008+ node.save()
1009+
1010+ # Simulate that the request originates from ip by setting
1011+ # 'REMOTE_ADDR'.
1012+ response = self.client.get(
1013+ reverse('pxeconfig'), params, REMOTE_ADDR=ip)
1014+ self.assertThat(
1015+ json.loads(response.content)["preseed_url"],
1016+ StartsWith(ng_url))
1017+
1018 def test_get_boot_purpose_unknown_node(self):
1019 # A node that's not yet known to MAAS is assumed to be enlisting,
1020 # which uses a "commissioning" image.
1021@@ -3795,6 +3855,87 @@
1022 self.assertIn('application/json', response['Content-Type'])
1023 self.assertEqual({'BROKER_URL': fake_broker_url}, parsed_result)
1024
1025+ def assertSuccess(self, response):
1026+ """Assert that `response` was successful (i.e. HTTP 2xx)."""
1027+ self.assertThat(
1028+ {code for code in httplib.responses if code // 100 == 2},
1029+ Annotate(response, Contains(response.status_code)))
1030+
1031+ def test_register_new_nodegroup_does_not_record_maas_url(self):
1032+ # When registering a cluster, the URL with which the call was made
1033+ # (i.e. from the perspective of the cluster) is *not* recorded.
1034+ self.create_configured_master()
1035+ name = factory.make_name('name')
1036+ uuid = factory.getRandomUUID()
1037+ update_maas_url = self.patch(api, "update_nodegroup_maas_url")
1038+ response = self.client.post(
1039+ reverse('nodegroups_handler'),
1040+ {'op': 'register', 'name': name, 'uuid': uuid})
1041+ self.assertSuccess(response)
1042+ self.assertEqual([], update_maas_url.call_args_list)
1043+
1044+ def test_register_accepted_nodegroup_updates_maas_url(self):
1045+ # When registering an existing, accepted, cluster, the URL with which
1046+ # the call was made is updated.
1047+ self.create_configured_master()
1048+ nodegroup = factory.make_node_group(status=NODEGROUP_STATUS.ACCEPTED)
1049+ update_maas_url = self.patch(api, "update_nodegroup_maas_url")
1050+ response = self.client.post(
1051+ reverse('nodegroups_handler'),
1052+ {'op': 'register', 'uuid': nodegroup.uuid})
1053+ self.assertSuccess(response)
1054+ update_maas_url.assert_called_once_with(nodegroup, ANY)
1055+
1056+ def test_register_pending_nodegroup_does_not_update_maas_url(self):
1057+ # When registering an existing, pending, cluster, the URL with which
1058+ # the call was made is *not* updated.
1059+ self.create_configured_master()
1060+ nodegroup = factory.make_node_group(status=NODEGROUP_STATUS.PENDING)
1061+ update_maas_url = self.patch(api, "update_nodegroup_maas_url")
1062+ response = self.client.post(
1063+ reverse('nodegroups_handler'),
1064+ {'op': 'register', 'uuid': nodegroup.uuid})
1065+ self.assertSuccess(response)
1066+ self.assertEqual([], update_maas_url.call_args_list)
1067+
1068+ def test_register_rejected_nodegroup_does_not_update_maas_url(self):
1069+ # When registering an existing, pending, cluster, the URL with which
1070+ # the call was made is *not* updated.
1071+ self.create_configured_master()
1072+ nodegroup = factory.make_node_group(status=NODEGROUP_STATUS.REJECTED)
1073+ update_maas_url = self.patch(api, "update_nodegroup_maas_url")
1074+ response = self.client.post(
1075+ reverse('nodegroups_handler'),
1076+ {'op': 'register', 'uuid': nodegroup.uuid})
1077+ self.assertEqual(httplib.FORBIDDEN, response.status_code)
1078+ self.assertEqual([], update_maas_url.call_args_list)
1079+
1080+ def test_register_master_nodegroup_does_not_update_maas_url(self):
1081+ # When registering the master cluster, the URL with which the call was
1082+ # made is *not* updated.
1083+ name = factory.make_name('name')
1084+ update_maas_url = self.patch(api, "update_nodegroup_maas_url")
1085+ response = self.client.post(
1086+ reverse('nodegroups_handler'),
1087+ {'op': 'register', 'name': name, 'uuid': 'master'})
1088+ self.assertSuccess(response)
1089+ self.assertEqual([], update_maas_url.call_args_list)
1090+ # The new node group's maas_url field remains empty.
1091+ nodegroup = NodeGroup.objects.get(uuid='master')
1092+ self.assertEqual("", nodegroup.maas_url)
1093+
1094+
1095+class TestUpdateNodeGroupMAASURL(TestCase):
1096+ """Tests for `update_nodegroup_maas_url`."""
1097+
1098+ def test_update_from_request(self):
1099+ request_factory = RequestFactory(SCRIPT_NAME="/script")
1100+ request = request_factory.get(
1101+ "/script/path", SERVER_NAME="example.com")
1102+ nodegroup = factory.make_node_group()
1103+ api.update_nodegroup_maas_url(nodegroup, request)
1104+ self.assertEqual("http://example.com/script", nodegroup.maas_url)
1105+
1106
1107 def dict_subset(obj, fields):
1108 """Return a dict of a subset of the fields/values of an object."""
1109
1110=== modified file 'src/maasserver/tests/test_dhcp.py'
1111--- src/maasserver/tests/test_dhcp.py 2012-10-11 09:39:49 +0000
1112+++ src/maasserver/tests/test_dhcp.py 2012-12-04 04:32:20 +0000
1113@@ -22,7 +22,6 @@
1114 )
1115 from maasserver.dns import get_dns_server_address
1116 from maasserver.enum import NODEGROUP_STATUS
1117-from maasserver.server_address import get_maas_facing_server_address
1118 from maasserver.testing.factory import factory
1119 from maasserver.testing.testcase import TestCase
1120 from maastesting.celery import CeleryFixture
1121@@ -81,10 +80,6 @@
1122 param: getattr(interface, param)
1123 for param in dhcp_params}
1124
1125- # Currently all nodes use the central TFTP server. This will be
1126- # decentralized to use NodeGroup.worker_ip later.
1127- expected_params["next_server"] = get_maas_facing_server_address()
1128-
1129 expected_params["omapi_key"] = nodegroup.dhcp_key
1130 expected_params["dns_servers"] = get_dns_server_address()
1131 expected_params["subnet"] = '192.168.100.0'
1132@@ -98,6 +93,22 @@
1133
1134 self.assertEqual(expected_params, result_params)
1135
1136+ def test_dhcp_config_uses_dns_server_from_cluster_controller(self):
1137+ mocked_task = self.patch(dhcp, 'write_dhcp_config')
1138+ ip = factory.getRandomIPAddress()
1139+ maas_url = 'http://%s/' % ip
1140+ nodegroup = factory.make_node_group(
1141+ maas_url=maas_url,
1142+ status=NODEGROUP_STATUS.ACCEPTED,
1143+ dhcp_key=factory.getRandomString(),
1144+ interface=factory.make_name('eth'),
1145+ network=IPNetwork("192.168.102.0/22"))
1146+ self.patch(settings, "DHCP_CONNECT", True)
1147+ configure_dhcp(nodegroup)
1148+ kwargs = mocked_task.apply_async.call_args[1]['kwargs']
1149+
1150+ self.assertEqual(ip, kwargs['dns_servers'])
1151+
1152 def test_configure_dhcp_restart_dhcp_server(self):
1153 self.patch(tasks, "sudo_write_file")
1154 mocked_check_call = self.patch(tasks, "check_call")
1155
1156=== modified file 'src/maasserver/tests/test_dns.py'
1157--- src/maasserver/tests/test_dns.py 2012-12-03 07:29:53 +0000
1158+++ src/maasserver/tests/test_dns.py 2012-12-04 04:32:20 +0000
1159@@ -91,7 +91,7 @@
1160 ip = factory.getRandomIPAddress()
1161 resolver = FakeMethod(result=ip)
1162 self.patch(server_address, 'gethostbyname', resolver)
1163- hostname = factory.getRandomString().lower()
1164+ hostname = factory.make_hostname()
1165 self.patch_DEFAULT_MAAS_URL_with_random_values(hostname=hostname)
1166 self.assertEqual(
1167 (ip, [(hostname, )]),
1168@@ -114,6 +114,17 @@
1169 call(dns.WARNING_MESSAGE % '127.0.0.1'),
1170 logger.return_value.warn.call_args)
1171
1172+ def test_get_dns_server_address_uses_nodegroup_maas_url(self):
1173+ ip = factory.getRandomIPAddress()
1174+ resolver = FakeMethod(result=ip)
1175+ self.patch(server_address, 'gethostbyname', resolver)
1176+ hostname = factory.make_hostname()
1177+ maas_url = 'http://%s' % hostname
1178+ nodegroup = factory.make_node_group(maas_url=maas_url)
1179+ self.assertEqual(
1180+ (ip, [(hostname, )]),
1181+ (dns.get_dns_server_address(nodegroup), resolver.extract_args()))
1182+
1183 def test_is_dns_managed(self):
1184 nodegroups_with_expected_results = {
1185 factory.make_node_group(
1186
1187=== modified file 'src/maasserver/tests/test_fields.py'
1188--- src/maasserver/tests/test_fields.py 2012-10-08 07:35:35 +0000
1189+++ src/maasserver/tests/test_fields.py 2012-12-04 04:32:20 +0000
1190@@ -21,7 +21,6 @@
1191 from maasserver.models import (
1192 MACAddress,
1193 NodeGroup,
1194- nodegroupinterface,
1195 NodeGroupInterface,
1196 )
1197 from maasserver.testing.factory import factory
1198@@ -33,7 +32,6 @@
1199 JSONFieldModel,
1200 XMLFieldModel,
1201 )
1202-from netaddr import IPNetwork
1203
1204
1205 class TestNodeGroupFormField(TestCase):
1206@@ -70,51 +68,6 @@
1207 nodegroup,
1208 NodeGroupFormField().clean("%s" % nodegroup.id))
1209
1210- def test_clean_finds_nodegroup_by_network_address(self):
1211- nodegroup = factory.make_node_group(
1212- network=IPNetwork("192.168.28.1/24"))
1213- self.assertEqual(
1214- nodegroup,
1215- NodeGroupFormField().clean('192.168.28.0'))
1216-
1217- def test_find_nodegroup_looks_up_nodegroup_by_controller_ip(self):
1218- nodegroup = factory.make_node_group()
1219- self.assertEqual(
1220- nodegroup,
1221- NodeGroupFormField().clean(nodegroup.get_managed_interface().ip))
1222-
1223- def test_find_nodegroup_accepts_any_ip_in_nodegroup_subnet(self):
1224- nodegroup = factory.make_node_group(
1225- network=IPNetwork("192.168.41.0/24"))
1226- self.assertEqual(
1227- nodegroup,
1228- NodeGroupFormField().clean('192.168.41.199'))
1229-
1230- def test_find_nodegroup_reports_if_not_found(self):
1231- self.assertRaises(
1232- ValidationError,
1233- NodeGroupFormField().clean,
1234- factory.getRandomIPAddress())
1235-
1236- def test_find_nodegroup_reports_if_multiple_matches(self):
1237- self.patch(nodegroupinterface, "MINIMUM_NETMASK_BITS", 1)
1238- factory.make_node_group(network=IPNetwork("10/8"))
1239- factory.make_node_group(network=IPNetwork("10.1.1/24"))
1240- self.assertRaises(
1241- NodeGroup.MultipleObjectsReturned,
1242- NodeGroupFormField().clean, '10.1.1.2')
1243-
1244- def test_find_nodegroup_handles_multiple_matches_on_same_nodegroup(self):
1245- self.patch(nodegroupinterface, "MINIMUM_NETMASK_BITS", 1)
1246- nodegroup = factory.make_node_group(network=IPNetwork("10/8"))
1247- NodeGroupInterface.objects.create(
1248- nodegroup=nodegroup, ip='10.0.0.2', subnet_mask='255.0.0.0',
1249- broadcast_ip='10.0.0.1', interface='eth71')
1250- NodeGroupInterface.objects.create(
1251- nodegroup=nodegroup, ip='10.0.0.3', subnet_mask='255.0.0.0',
1252- broadcast_ip='10.0.0.2', interface='eth72')
1253- self.assertEqual(nodegroup, NodeGroupFormField().clean('10.0.0.9'))
1254-
1255
1256 class TestMACAddressField(TestCase):
1257
1258
1259=== modified file 'src/maasserver/tests/test_forms.py'
1260--- src/maasserver/tests/test_forms.py 2012-11-16 13:50:43 +0000
1261+++ src/maasserver/tests/test_forms.py 2012-12-04 04:32:20 +0000
1262@@ -203,14 +203,6 @@
1263 NodeGroup.objects.ensure_master(),
1264 NodeWithMACAddressesForm(self.make_params()).save().nodegroup)
1265
1266- def test_sets_nodegroup_on_new_node_if_requested(self):
1267- nodegroup = factory.make_node_group(
1268- network=IPNetwork("192.168.14.0/24"), ip_range_low='192.168.14.2',
1269- ip_range_high='192.168.14.254', ip='192.168.14.1')
1270- form = NodeWithMACAddressesForm(
1271- self.make_params(nodegroup=nodegroup.get_managed_interface().ip))
1272- self.assertEqual(nodegroup, form.save().nodegroup)
1273-
1274 def test_leaves_nodegroup_alone_if_unset_on_existing_node(self):
1275 # Selecting a node group for a node is only supported on new
1276 # nodes. You can't change it later.
1277
1278=== modified file 'src/maasserver/tests/test_preseed.py'
1279--- src/maasserver/tests/test_preseed.py 2012-11-19 12:26:32 +0000
1280+++ src/maasserver/tests/test_preseed.py 2012-12-04 04:32:20 +0000
1281@@ -26,12 +26,14 @@
1282 compose_preseed_url,
1283 GENERIC_FILENAME,
1284 get_enlist_preseed,
1285+ get_node_preseed_context,
1286 get_preseed,
1287 get_preseed_context,
1288 get_preseed_filenames,
1289 get_preseed_template,
1290 load_preseed_template,
1291 PreseedTemplate,
1292+ render_enlistment_preseed,
1293 render_preseed,
1294 split_subarch,
1295 TemplateNotFoundError,
1296@@ -41,7 +43,10 @@
1297 from maasserver.utils import map_enum
1298 from testtools.matchers import (
1299 AllMatch,
1300+ Contains,
1301 IsInstance,
1302+ MatchesAll,
1303+ Not,
1304 StartsWith,
1305 )
1306
1307@@ -289,26 +294,28 @@
1308 """Tests for `get_preseed_context`."""
1309
1310 def test_get_preseed_context_contains_keys(self):
1311- node = factory.make_node()
1312- release = factory.getRandomString()
1313- context = get_preseed_context(node, release)
1314- self.assertItemsEqual(
1315- ['node', 'release', 'metadata_enlist_url',
1316- 'server_host', 'server_url', 'preseed_data',
1317- 'node_disable_pxe_url', 'node_disable_pxe_data'],
1318- context)
1319-
1320- def test_get_preseed_context_if_node_None(self):
1321- # If the provided Node is None (when're in the context of an
1322- # enlistment preseed) the returned context does not include the
1323- # node context.
1324- release = factory.getRandomString()
1325- context = get_preseed_context(None, release)
1326+ release = factory.getRandomString()
1327+ nodegroup = factory.make_node_group(maas_url=factory.getRandomString())
1328+ context = get_preseed_context(release, nodegroup)
1329 self.assertItemsEqual(
1330 ['release', 'metadata_enlist_url', 'server_host', 'server_url'],
1331 context)
1332
1333
1334+class TestNodePreseedContext(TestCase):
1335+ """Tests for `get_node_preseed_context`."""
1336+
1337+ def test_get_node_preseed_context_contains_keys(self):
1338+ node = factory.make_node()
1339+ release = factory.getRandomString()
1340+ context = get_node_preseed_context(node, release)
1341+ self.assertItemsEqual(
1342+ ['node', 'preseed_data', 'node_disable_pxe_url',
1343+ 'node_disable_pxe_data',
1344+ ],
1345+ context)
1346+
1347+
1348 class TestPreseedTemplate(TestCase):
1349 """Tests for class:`PreseedTemplate`."""
1350
1351@@ -338,6 +345,37 @@
1352 # error.
1353 self.assertIsInstance(preseed, str)
1354
1355+ def test_get_preseed_uses_nodegroup_maas_url(self):
1356+ ng_url = 'http://%s' % factory.make_hostname()
1357+ ng = factory.make_node_group(maas_url=ng_url)
1358+ maas_url = 'http://%s' % factory.make_hostname()
1359+ node = factory.make_node(
1360+ nodegroup=ng, status=NODE_STATUS.COMMISSIONING)
1361+ self.patch(settings, 'DEFAULT_MAAS_URL', maas_url)
1362+ preseed = render_preseed(node, self.preseed, "precise")
1363+ self.assertThat(
1364+ preseed, MatchesAll(*[Contains(ng_url), Not(Contains(maas_url))]))
1365+
1366+
1367+class TestRenderEnlistmentPreseed(TestCase):
1368+ """Tests for `render_enlistment_preseed`."""
1369+
1370+ def test_render_enlistment_preseed(self):
1371+ preseed = render_enlistment_preseed(PRESEED_TYPE.ENLIST, "precise")
1372+ # The test really is that the preseed is rendered without an
1373+ # error.
1374+ self.assertIsInstance(preseed, str)
1375+
1376+ def test_get_preseed_uses_nodegroup_maas_url(self):
1377+ ng_url = 'http://%s' % factory.make_hostname()
1378+ maas_url = 'http://%s' % factory.make_hostname()
1379+ self.patch(settings, 'DEFAULT_MAAS_URL', maas_url)
1380+ nodegroup = factory.make_node_group(maas_url=ng_url)
1381+ preseed = render_enlistment_preseed(
1382+ PRESEED_TYPE.ENLIST, "precise", nodegroup=nodegroup)
1383+ self.assertThat(
1384+ preseed, MatchesAll(*[Contains(ng_url), Not(Contains(maas_url))]))
1385+
1386
1387 class TestPreseedMethods(TestCase):
1388 """Tests for `get_enlist_preseed` and `get_preseed`.
1389
1390=== modified file 'src/maasserver/tests/test_server_address.py'
1391--- src/maasserver/tests/test_server_address.py 2012-08-06 10:14:07 +0000
1392+++ src/maasserver/tests/test_server_address.py 2012-12-04 04:32:20 +0000
1393@@ -15,16 +15,16 @@
1394 from django.conf import settings
1395 from maasserver import server_address
1396 from maasserver.server_address import get_maas_facing_server_address
1397-from maastesting.factory import factory
1398+from maasserver.testing.factory import factory
1399+from maasserver.testing.testcase import TestCase
1400 from maastesting.fakemethod import FakeMethod
1401-from maastesting.testcase import TestCase
1402 from netaddr import IPNetwork
1403
1404
1405 class TestServerAddress(TestCase):
1406
1407 def make_hostname(self):
1408- return '%s.example.com' % factory.make_name('host').lower()
1409+ return '%s.example.com' % factory.make_hostname()
1410
1411 def set_DEFAULT_MAAS_URL(self, hostname=None, with_port=False):
1412 """Patch DEFAULT_MAAS_URL to be a (partly) random URL."""
1413@@ -48,6 +48,13 @@
1414 self.set_DEFAULT_MAAS_URL(ip)
1415 self.assertEqual(ip, server_address.get_maas_facing_server_host())
1416
1417+ def test_get_maas_facing_server_host_returns_nodegroup_maas_url(self):
1418+ hostname = factory.make_hostname()
1419+ maas_url = 'http://%s' % hostname
1420+ nodegroup = factory.make_node_group(maas_url=maas_url)
1421+ self.assertEqual(
1422+ hostname, server_address.get_maas_facing_server_host(nodegroup))
1423+
1424 def test_get_maas_facing_server_host_strips_out_port(self):
1425 hostname = self.make_hostname()
1426 self.set_DEFAULT_MAAS_URL(hostname, with_port=True)
1427@@ -64,6 +71,13 @@
1428 self.set_DEFAULT_MAAS_URL(hostname=ip)
1429 self.assertEqual(ip, get_maas_facing_server_address())
1430
1431+ def test_get_maas_facing_server_address_returns_nodegroup_maas_url(self):
1432+ ip = factory.getRandomIPInNetwork(IPNetwork('127.0.0.0/8'))
1433+ maas_url = 'http://%s' % ip
1434+ nodegroup = factory.make_node_group(maas_url=maas_url)
1435+ self.assertEqual(
1436+ ip, server_address.get_maas_facing_server_host(nodegroup))
1437+
1438 def test_get_maas_facing_server_address_resolves_hostname(self):
1439 ip = factory.getRandomIPAddress()
1440 resolver = FakeMethod(result=ip)
1441
1442=== modified file 'src/maasserver/tests/test_views_nodes.py'
1443--- src/maasserver/tests/test_views_nodes.py 2012-12-03 07:08:59 +0000
1444+++ src/maasserver/tests/test_views_nodes.py 2012-12-04 04:32:20 +0000
1445@@ -619,6 +619,22 @@
1446 for a in document.xpath("//div[@class='pagination']//a")])
1447
1448
1449+class NodeEnlistmentPreseedViewTest(LoggedInTestCase):
1450+
1451+ def test_enlistpreseedview_displays_preseed_data(self):
1452+ response = self.client.get(reverse('enlist-preseed-view'))
1453+ # Simply test that the preseed looks ok.
1454+ self.assertIn('metadata_url', response.content)
1455+
1456+ def test_enlistpreseedview_display_warning_about_url(self):
1457+ response = self.client.get(reverse('enlist-preseed-view'))
1458+ message_chunk = (
1459+ "The URL mentioned in the following enlistment preseed will "
1460+ "be different depending on"
1461+ )
1462+ self.assertIn(message_chunk, response.content)
1463+
1464+
1465 class NodePreseedViewTest(LoggedInTestCase):
1466
1467 def test_preseedview_node_displays_preseed_data(self):
1468
1469=== modified file 'src/maasserver/utils/__init__.py'
1470--- src/maasserver/utils/__init__.py 2012-11-22 15:59:37 +0000
1471+++ src/maasserver/utils/__init__.py 2012-12-04 04:32:20 +0000
1472@@ -13,14 +13,13 @@
1473 __all__ = [
1474 'absolute_reverse',
1475 'build_absolute_uri',
1476+ 'find_nodegroup',
1477 'get_db_state',
1478- 'get_origin_ip',
1479 'ignore_unused',
1480 'map_enum',
1481 'strip_domain',
1482 ]
1483
1484-import socket
1485 from urllib import urlencode
1486 from urlparse import urljoin
1487
1488@@ -67,7 +66,7 @@
1489 }
1490
1491
1492-def absolute_reverse(view_name, query=None, *args, **kwargs):
1493+def absolute_reverse(view_name, query=None, base_url=None, *args, **kwargs):
1494 """Return the absolute URL (i.e. including the URL scheme specifier and
1495 the network location of the MAAS server). Internally this method simply
1496 calls Django's 'reverse' method and prefixes the result of that call with
1497@@ -78,11 +77,14 @@
1498 :param query: Optional query argument which will be passed down to
1499 urllib.urlencode. The result of that call will be appended to the
1500 resulting url.
1501+ :param base_url: Optional url used as base. If None is provided, then
1502+ settings.DEFAULT_MAAS_URL will be used.
1503 :param args: Positional arguments for Django's 'reverse' method.
1504 :param kwargs: Named arguments for Django's 'reverse' method.
1505 """
1506- url = urljoin(
1507- settings.DEFAULT_MAAS_URL, reverse(view_name, *args, **kwargs))
1508+ if not base_url:
1509+ base_url = settings.DEFAULT_MAAS_URL
1510+ url = urljoin(base_url, reverse(view_name, *args, **kwargs))
1511 if query is not None:
1512 url += '?%s' % urlencode(query, doseq=True)
1513 return url
1514@@ -107,14 +109,25 @@
1515 return hostname.split('.', 1)[0]
1516
1517
1518-def get_origin_ip(request):
1519- """Return the IP address of the originating host of the request.
1520+def find_nodegroup(request):
1521+ """Find the nodegroup whose subnet contains the IP Address of the
1522+ originating host of the request..
1523
1524- Return the IP address obtained by resolving the host given by
1525- request.get_host().
1526+ The matching nodegroup may have multiple interfaces on the subnet,
1527+ but there can be only one matching nodegroup.
1528 """
1529- host = request.get_host().split(':')[0]
1530- try:
1531- return socket.gethostbyname(host)
1532- except socket.error:
1533- return None
1534+ # Circular imports.
1535+ from maasserver.models import NodeGroup
1536+ ip_address = request.META['REMOTE_ADDR']
1537+ if ip_address is not None:
1538+ query = NodeGroup.objects.raw("""
1539+ SELECT *
1540+ FROM maasserver_nodegroup
1541+ WHERE id IN (
1542+ SELECT nodegroup_id
1543+ FROM maasserver_nodegroupinterface
1544+ WHERE (inet %s & subnet_mask) = (ip & subnet_mask)
1545+ )
1546+ """, [ip_address])
1547+ return get_one(query)
1548+ return None
1549
1550=== modified file 'src/maasserver/utils/tests/test_utils.py'
1551--- src/maasserver/utils/tests/test_utils.py 2012-11-22 15:59:37 +0000
1552+++ src/maasserver/utils/tests/test_utils.py 2012-12-04 04:32:20 +0000
1553@@ -12,7 +12,6 @@
1554 __metaclass__ = type
1555 __all__ = []
1556
1557-import socket
1558 from urllib import urlencode
1559
1560 from django.conf import settings
1561@@ -20,21 +19,23 @@
1562 from django.http import HttpRequest
1563 from django.test.client import RequestFactory
1564 from maasserver.enum import NODE_STATUS_CHOICES
1565+from maasserver.models import (
1566+ NodeGroup,
1567+ nodegroupinterface,
1568+ NodeGroupInterface,
1569+ )
1570 from maasserver.testing.factory import factory
1571 from maasserver.testing.testcase import TestCase as DjangoTestCase
1572 from maasserver.utils import (
1573 absolute_reverse,
1574 build_absolute_uri,
1575+ find_nodegroup,
1576 get_db_state,
1577- get_origin_ip,
1578 map_enum,
1579 strip_domain,
1580 )
1581 from maastesting.testcase import TestCase
1582-from mock import (
1583- call,
1584- Mock,
1585- )
1586+from netaddr import IPNetwork
1587
1588
1589 class TestEnum(TestCase):
1590@@ -74,13 +75,19 @@
1591
1592 class TestAbsoluteReverse(DjangoTestCase):
1593
1594- def test_absolute_reverse_uses_DEFAULT_MAAS_URL(self):
1595+ def test_absolute_reverse_uses_DEFAULT_MAAS_URL_by_default(self):
1596 maas_url = 'http://%s' % factory.getRandomString()
1597 self.patch(settings, 'DEFAULT_MAAS_URL', maas_url)
1598 absolute_url = absolute_reverse('settings')
1599 expected_url = settings.DEFAULT_MAAS_URL + reverse('settings')
1600 self.assertEqual(expected_url, absolute_url)
1601
1602+ def test_absolute_reverse_uses_given_base_url(self):
1603+ maas_url = 'http://%s' % factory.getRandomString()
1604+ absolute_url = absolute_reverse('settings', base_url=maas_url)
1605+ expected_url = maas_url + reverse('settings')
1606+ self.assertEqual(expected_url, absolute_url)
1607+
1608 def test_absolute_reverse_uses_query_string(self):
1609 self.patch(settings, 'DEFAULT_MAAS_URL', '')
1610 parameters = {factory.getRandomString(): factory.getRandomString()}
1611@@ -184,36 +191,52 @@
1612 self.assertEqual(results, map(strip_domain, inputs))
1613
1614
1615-class TestGetOriginIP(TestCase):
1616-
1617- def get_request(self, server_name, server_port='80'):
1618- return RequestFactory().post(
1619- '/', SERVER_NAME=server_name, SERVER_PORT=server_port)
1620-
1621- def test_get_origin_ip_returns_ip(self):
1622- ip = factory.getRandomIPAddress()
1623- request = self.get_request(ip)
1624- self.assertEqual(ip, get_origin_ip(request))
1625-
1626- def test_get_origin_ip_strips_port(self):
1627- ip = factory.getRandomIPAddress()
1628- request = self.get_request(ip, '8888')
1629- self.assertEqual(ip, get_origin_ip(request))
1630-
1631- def test_get_origin_ip_resolves_hostname(self):
1632- ip = factory.getRandomIPAddress()
1633- hostname = factory.make_name('hostname')
1634- request = self.get_request(hostname)
1635- resolver = self.patch(socket, 'gethostbyname', Mock(return_value=ip))
1636- self.assertEqual(
1637- (ip, call(hostname)),
1638- (get_origin_ip(request), resolver.call_args))
1639-
1640- def test_get_origin_ip_returns_None_if_hostname_cannot_get_resolved(self):
1641- hostname = factory.make_name('hostname')
1642- request = self.get_request(hostname)
1643- resolver = self.patch(
1644- socket, 'gethostbyname', Mock(side_effect=socket.error))
1645- self.assertEqual(
1646- (None, call(hostname)),
1647- (get_origin_ip(request), resolver.call_args))
1648+def get_request(origin_ip):
1649+ return RequestFactory().post('/', REMOTE_ADDR=origin_ip)
1650+
1651+
1652+class TestFindNodegroup(DjangoTestCase):
1653+
1654+ def test_finds_nodegroup_by_network_address(self):
1655+ nodegroup = factory.make_node_group(
1656+ network=IPNetwork("192.168.28.1/24"))
1657+ self.assertEqual(
1658+ nodegroup,
1659+ find_nodegroup(get_request('192.168.28.0')))
1660+
1661+ def test_find_nodegroup_looks_up_nodegroup_by_controller_ip(self):
1662+ nodegroup = factory.make_node_group()
1663+ ip = nodegroup.get_managed_interface().ip
1664+ self.assertEqual(
1665+ nodegroup,
1666+ find_nodegroup(get_request(ip)))
1667+
1668+ def test_find_nodegroup_accepts_any_ip_in_nodegroup_subnet(self):
1669+ nodegroup = factory.make_node_group(
1670+ network=IPNetwork("192.168.41.0/24"))
1671+ self.assertEqual(
1672+ nodegroup,
1673+ find_nodegroup(get_request('192.168.41.199')))
1674+
1675+ def test_find_nodegroup_returns_None_if_not_found(self):
1676+ self.assertIsNone(
1677+ find_nodegroup(get_request(factory.getRandomIPAddress())))
1678+
1679+ def test_find_nodegroup_errors_if_multiple_matches(self):
1680+ self.patch(nodegroupinterface, "MINIMUM_NETMASK_BITS", 1)
1681+ factory.make_node_group(network=IPNetwork("10/8"))
1682+ factory.make_node_group(network=IPNetwork("10.1.1/24"))
1683+ self.assertRaises(
1684+ NodeGroup.MultipleObjectsReturned,
1685+ find_nodegroup, get_request('10.1.1.2'))
1686+
1687+ def test_find_nodegroup_handles_multiple_matches_on_same_nodegroup(self):
1688+ self.patch(nodegroupinterface, "MINIMUM_NETMASK_BITS", 1)
1689+ nodegroup = factory.make_node_group(network=IPNetwork("10/8"))
1690+ NodeGroupInterface.objects.create(
1691+ nodegroup=nodegroup, ip='10.0.0.2', subnet_mask='255.0.0.0',
1692+ broadcast_ip='10.0.0.1', interface='eth71')
1693+ NodeGroupInterface.objects.create(
1694+ nodegroup=nodegroup, ip='10.0.0.3', subnet_mask='255.0.0.0',
1695+ broadcast_ip='10.0.0.2', interface='eth72')
1696+ self.assertEqual(nodegroup, find_nodegroup(get_request('10.0.0.9')))
1697
1698=== modified file 'src/maasserver/views/nodes.py'
1699--- src/maasserver/views/nodes.py 2012-12-03 07:08:59 +0000
1700+++ src/maasserver/views/nodes.py 2012-12-04 04:32:20 +0000
1701@@ -178,10 +178,18 @@
1702
1703 def enlist_preseed_view(request):
1704 """View method to display the enlistment preseed."""
1705+ warning_message = (
1706+ "The URL mentioned in the following enlistment preseed will "
1707+ "be different depending on which cluster controller is "
1708+ "responsible for the enlisting node. The URL shown here is for "
1709+ "nodes handled by the cluster controller located in the region "
1710+ "controller's network."
1711+ )
1712+ context = RequestContext(request, {'warning_message': warning_message})
1713 return render_to_response(
1714 'maasserver/enlist_preseed.html',
1715 {'preseed': mark_safe(get_enlist_preseed())},
1716- context_instance=RequestContext(request))
1717+ context_instance=context)
1718
1719
1720 class NodeViewMixin:
1721
1722=== modified file 'src/maastesting/factory.py'
1723--- src/maastesting/factory.py 2012-09-10 14:50:56 +0000
1724+++ src/maastesting/factory.py 2012-12-04 04:32:20 +0000
1725@@ -148,6 +148,13 @@
1726 return sep.join(
1727 filter(None, [prefix, self.getRandomString(size=size)]))
1728
1729+ def make_hostname(self, prefix='host', *args, **kwargs):
1730+ """Generate a random hostname.
1731+
1732+ The returned hostname is lowercase because python's urlparse
1733+ implicitely lowercases the hostnames."""
1734+ return self.make_name(prefix=prefix, *args, **kwargs).lower()
1735+
1736 def make_names(self, *prefixes):
1737 """Generate random names.
1738
1739
1740=== modified file 'src/metadataserver/api.py'
1741--- src/metadataserver/api.py 2012-11-14 14:36:27 +0000
1742+++ src/metadataserver/api.py 2012-12-04 04:32:20 +0000
1743@@ -51,6 +51,7 @@
1744 get_enlist_userdata,
1745 get_preseed,
1746 )
1747+from maasserver.utils import find_nodegroup
1748 from maasserver.utils.orm import get_one
1749 from metadataserver.models import (
1750 NodeCommissionResult,
1751@@ -374,7 +375,9 @@
1752
1753 def read(self, request, version):
1754 check_version(version)
1755- return HttpResponse(get_enlist_userdata(), mimetype="text/plain")
1756+ nodegroup = find_nodegroup(request)
1757+ return HttpResponse(
1758+ get_enlist_userdata(nodegroup=nodegroup), mimetype="text/plain")
1759
1760
1761 class EnlistVersionIndexHandler(OperationsHandler):
1762@@ -391,7 +394,9 @@
1763 @operation(idempotent=True)
1764 def get_enlist_preseed(self, request, version=None):
1765 """Render and return a preseed script for enlistment."""
1766- return HttpResponse(get_enlist_preseed(), mimetype="text/plain")
1767+ nodegroup = find_nodegroup(request)
1768+ return HttpResponse(
1769+ get_enlist_preseed(nodegroup=nodegroup), mimetype="text/plain")
1770
1771 @operation(idempotent=True)
1772 def get_preseed(self, request, version=None, system_id=None):
1773
1774=== modified file 'src/metadataserver/tests/test_api.py'
1775--- src/metadataserver/tests/test_api.py 2012-11-14 14:36:27 +0000
1776+++ src/metadataserver/tests/test_api.py 2012-12-04 04:32:20 +0000
1777@@ -51,7 +51,14 @@
1778 NodeUserData,
1779 )
1780 from metadataserver.nodeinituser import get_node_init_user
1781+from mock import Mock
1782+from netaddr import IPNetwork
1783 from provisioningserver.enum import POWER_TYPE
1784+from testtools.matchers import (
1785+ Contains,
1786+ MatchesAll,
1787+ Not,
1788+ )
1789
1790
1791 class TestHelpers(DjangoTestCase):
1792@@ -629,7 +636,7 @@
1793 'metadata-enlist-preseed', args=['latest'])
1794 # Fake the preseed so we're just exercising the view.
1795 fake_preseed = factory.getRandomString()
1796- self.patch(api, "get_enlist_preseed", lambda: fake_preseed)
1797+ self.patch(api, "get_enlist_preseed", Mock(return_value=fake_preseed))
1798 response = self.client.get(
1799 anon_enlist_preseed_url, {'op': 'get_enlist_preseed'})
1800 self.assertEqual(
1801@@ -641,6 +648,18 @@
1802 response.content),
1803 response)
1804
1805+ def test_anonymous_get_enlist_preseed_detects_request_origin(self):
1806+ ng_url = 'http://%s' % factory.make_name('host')
1807+ network = IPNetwork("10.1.1/24")
1808+ ip = factory.getRandomIPInNetwork(network)
1809+ factory.make_node_group(maas_url=ng_url, network=network)
1810+ anon_enlist_preseed_url = reverse(
1811+ 'metadata-enlist-preseed', args=['latest'])
1812+ response = self.client.get(
1813+ anon_enlist_preseed_url, {'op': 'get_enlist_preseed'},
1814+ REMOTE_ADDR=ip)
1815+ self.assertThat(response.content, Contains(ng_url))
1816+
1817 def test_anonymous_get_preseed(self):
1818 # The preseed for a node can be obtained anonymously.
1819 node = factory.make_node()
1820@@ -707,13 +726,27 @@
1821 # instance-id must be available
1822 ud_url = reverse('enlist-metadata-user-data', args=['latest'])
1823 fake_preseed = factory.getRandomString()
1824- self.patch(api, "get_enlist_userdata", lambda: fake_preseed)
1825+ self.patch(
1826+ api, "get_enlist_userdata", Mock(return_value= fake_preseed))
1827 response = self.client.get(ud_url)
1828 self.assertEqual(
1829 (httplib.OK, "text/plain", fake_preseed),
1830 (response.status_code, response["Content-Type"], response.content),
1831 response)
1832
1833+ def test_get_userdata_detects_request_origin(self):
1834+ nodegroup_url = 'http://%s' % factory.make_name('host')
1835+ maas_url = 'http://%s' % factory.make_hostname()
1836+ self.patch(settings, 'DEFAULT_MAAS_URL', maas_url)
1837+ network = IPNetwork("10.1.1/24")
1838+ ip = factory.getRandomIPInNetwork(network)
1839+ factory.make_node_group(maas_url=nodegroup_url, network=network)
1840+ url = reverse('enlist-metadata-user-data', args=['latest'])
1841+ response = self.client.get(url, REMOTE_ADDR=ip)
1842+ self.assertThat(
1843+ response.content,
1844+ MatchesAll(Contains(nodegroup_url), Not(Contains(maas_url))))
1845+
1846 def test_metadata_list(self):
1847 # /enlist/latest/metadata request should list available keys
1848 md_url = reverse('enlist-metadata-meta-data', args=['latest', ""])
1849
1850=== modified file 'src/provisioningserver/dhcp/tests/test_config.py'
1851--- src/provisioningserver/dhcp/tests/test_config.py 2012-09-05 05:49:28 +0000
1852+++ src/provisioningserver/dhcp/tests/test_config.py 2012-12-04 04:32:20 +0000
1853@@ -27,7 +27,6 @@
1854 {{omapi_key}}
1855 {{subnet}}
1856 {{subnet_mask}}
1857- {{next_server}}
1858 {{broadcast_ip}}
1859 {{dns_servers}}
1860 {{router_ip}}
1861@@ -45,7 +44,6 @@
1862 omapi_key="random",
1863 subnet="10.0.0.0",
1864 subnet_mask="255.0.0.0",
1865- next_server="10.0.0.1",
1866 broadcast_ip="10.255.255.255",
1867 dns_servers="10.1.0.1 10.1.0.2",
1868 router_ip="10.0.0.2",
1869
1870=== modified file 'src/provisioningserver/tests/test_auth.py'
1871--- src/provisioningserver/tests/test_auth.py 2012-11-26 02:54:49 +0000
1872+++ src/provisioningserver/tests/test_auth.py 2012-12-04 04:32:20 +0000
1873@@ -12,8 +12,6 @@
1874 __metaclass__ = type
1875 __all__ = []
1876
1877-import os
1878-
1879 from apiclient.creds import convert_tuple_to_string
1880 from apiclient.testing.credentials import make_api_credentials
1881 from fixtures import EnvironmentVariableFixture
1882
1883=== modified file 'src/provisioningserver/tests/test_start_cluster_controller.py'
1884--- src/provisioningserver/tests/test_start_cluster_controller.py 2012-12-04 00:52:42 +0000
1885+++ src/provisioningserver/tests/test_start_cluster_controller.py 2012-12-04 04:32:20 +0000
1886@@ -295,4 +295,3 @@
1887 MAAS_URL=server_url,
1888 )
1889 os.execvpe.assert_called_once_with(ANY, ANY, env=env)
1890-
1891
1892=== modified file 'src/provisioningserver/tests/test_tasks.py'
1893--- src/provisioningserver/tests/test_tasks.py 2012-11-26 02:54:49 +0000
1894+++ src/provisioningserver/tests/test_tasks.py 2012-12-04 04:32:20 +0000
1895@@ -176,7 +176,6 @@
1896 'omapi_key',
1897 'subnet',
1898 'subnet_mask',
1899- 'next_server',
1900 'broadcast_ip',
1901 'dns_servers',
1902 'router_ip',

Subscribers

People subscribed via source and target branches

to status/vote changes: