launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #14703
[Merge] lp:~julian-edwards/maas/backport-r1371 into lp:maas/1.2
Julian Edwards has proposed merging lp:~julian-edwards/maas/backport-r1371 into lp:maas/1.2.
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)
Requested reviews:
MAAS Maintainers (maas-maintainers)
Related bugs:
Bug #1081696 in MAAS: "The value of 'domain-name-servers' in the dhcp config is wrong."
https://bugs.launchpad.net/maas/+bug/1081696
Bug #1081701 in MAAS: "The metadata address mentioned in the preseed is wrong."
https://bugs.launchpad.net/maas/+bug/1081701
For more details, see:
https://code.launchpad.net/~julian-edwards/maas/backport-r1371/+merge/137751
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.
--
https://code.launchpad.net/~julian-edwards/maas/backport-r1371/+merge/137751
Your team MAAS Maintainers is requested to review the proposed merge of lp:~julian-edwards/maas/backport-r1371 into lp:maas/1.2.
=== modified file 'src/maasserver/api.py'
--- src/maasserver/api.py 2012-12-04 00:40:35 +0000
+++ src/maasserver/api.py 2012-12-04 04:32:20 +0000
@@ -165,7 +165,7 @@
from maasserver.utils import (
absolute_reverse,
build_absolute_uri,
- get_origin_ip,
+ find_nodegroup,
map_enum,
strip_domain,
)
@@ -649,9 +649,9 @@
# If 'nodegroup' is not explicitely specified, get the origin of the
# request to figure out which nodegroup the new node should be
# attached to.
- origin_ip = get_origin_ip(request)
- if origin_ip is not None:
- altered_query_data['nodegroup'] = origin_ip
+ nodegroup = find_nodegroup(request)
+ if nodegroup is not None:
+ altered_query_data['nodegroup'] = nodegroup
Form = get_node_create_form(request.user)
form = Form(altered_query_data)
@@ -1179,6 +1179,7 @@
raise ValidationError(form.errors)
else:
if existing_nodegroup.status == NODEGROUP_STATUS.ACCEPTED:
+ update_nodegroup_maas_url(existing_nodegroup, request)
# The nodegroup exists and is validated, return the RabbitMQ
return get_celery_credentials()
elif existing_nodegroup.status == NODEGROUP_STATUS.REJECTED:
@@ -1188,6 +1189,13 @@
"Awaiting admin approval.", status=httplib.ACCEPTED)
+def update_nodegroup_maas_url(nodegroup, request):
+ """Update `nodegroup.maas_url` from the given `request`."""
+ path = request.META["SCRIPT_NAME"]
+ nodegroup.maas_url = build_absolute_uri(request, path)
+ nodegroup.save()
+
+
class NodeGroupsHandler(OperationsHandler):
"""Manage NodeGroups."""
anonymous = AnonNodeGroupsHandler
@@ -1844,7 +1852,8 @@
# The node's hostname may include a domain, but we ignore that
# and use the one from the nodegroup instead.
hostname = strip_domain(node.hostname)
- domain = node.nodegroup.name
+ nodegroup = node.nodegroup
+ domain = nodegroup.name
else:
try:
pxelinux_arch = request.GET['arch']
@@ -1878,7 +1887,8 @@
# 1-1 mapping.
subarch = pxelinux_subarch
- preseed_url = compose_enlistment_preseed_url()
+ nodegroup = find_nodegroup(request)
+ preseed_url = compose_enlistment_preseed_url(nodegroup=nodegroup)
hostname = 'maas-enlist'
domain = Config.objects.get_config('enlistment_domain')
@@ -1888,7 +1898,7 @@
series = node.get_distro_series()
purpose = get_boot_purpose(node)
- server_address = get_maas_facing_server_address()
+ server_address = get_maas_facing_server_address(nodegroup=nodegroup)
cluster_address = get_mandatory_param(request.GET, "local")
params = KernelParameters(
=== modified file 'src/maasserver/compose_preseed.py'
--- src/maasserver/compose_preseed.py 2012-10-04 14:00:06 +0000
+++ src/maasserver/compose_preseed.py 2012-12-04 04:32:20 +0000
@@ -21,7 +21,7 @@
import yaml
-def compose_cloud_init_preseed(token):
+def compose_cloud_init_preseed(token, base_url=''):
"""Compose the preseed value for a node in any state but Commissioning."""
credentials = urlencode({
'oauth_consumer_key': token.consumer.key,
@@ -40,7 +40,8 @@
# ks_meta, and it gets fed straight into debconf.
preseed_items = [
('datasources', 'multiselect', 'MAAS'),
- ('maas-metadata-url', 'string', absolute_reverse('metadata')),
+ ('maas-metadata-url', 'string', absolute_reverse(
+ 'metadata', base_url=base_url)),
('maas-metadata-credentials', 'string', credentials),
('local-cloud-config', 'string', local_config)
]
@@ -54,12 +55,13 @@
for item_name, item_type, item_value in preseed_items)
-def compose_commissioning_preseed(token):
+def compose_commissioning_preseed(token, base_url=''):
"""Compose the preseed value for a Commissioning node."""
return "#cloud-config\n%s" % yaml.safe_dump({
'datasource': {
'MAAS': {
- 'metadata_url': absolute_reverse('metadata'),
+ 'metadata_url': absolute_reverse(
+ 'metadata', base_url=base_url),
'consumer_key': token.consumer.key,
'token_key': token.key,
'token_secret': token.secret,
@@ -86,7 +88,8 @@
# Circular import.
from metadataserver.models import NodeKey
token = NodeKey.objects.get_token_for_node(node)
+ base_url = node.nodegroup.maas_url
if node.status == NODE_STATUS.COMMISSIONING:
- return compose_commissioning_preseed(token)
+ return compose_commissioning_preseed(token, base_url)
else:
- return compose_cloud_init_preseed(token)
+ return compose_cloud_init_preseed(token, base_url)
=== modified file 'src/maasserver/dhcp.py'
--- src/maasserver/dhcp.py 2012-10-11 09:34:16 +0000
+++ src/maasserver/dhcp.py 2012-12-04 04:32:20 +0000
@@ -20,7 +20,6 @@
NODEGROUP_STATUS,
NODEGROUPINTERFACE_MANAGEMENT,
)
-from maasserver.server_address import get_maas_facing_server_address
from netaddr import IPAddress
from provisioningserver.tasks import (
restart_dhcp_server,
@@ -52,12 +51,6 @@
# server.
nodegroup.ensure_dhcp_key()
- # Use the server's address (which is where the central TFTP
- # server is) for the next_server setting. We'll want to proxy
- # it on the local worker later, and then we can use
- # next_server=self.worker_ip.
- next_server = get_maas_facing_server_address()
-
interface = nodegroup.get_managed_interface()
subnet = str(
IPAddress(interface.ip_range_low) &
@@ -66,13 +59,12 @@
options={'queue': nodegroup.work_queue})
task_kwargs = dict(
subnet=subnet,
- next_server=next_server,
omapi_key=nodegroup.dhcp_key,
subnet_mask=interface.subnet_mask,
dhcp_interfaces=interface.interface,
broadcast_ip=interface.broadcast_ip,
router_ip=interface.router_ip,
- dns_servers=get_dns_server_address(),
+ dns_servers=get_dns_server_address(nodegroup),
ip_range_low=interface.ip_range_low,
ip_range_high=interface.ip_range_high,
callback=reload_dhcp_server_subtask,
=== modified file 'src/maasserver/dns.py'
--- src/maasserver/dns.py 2012-12-03 07:29:53 +0000
+++ src/maasserver/dns.py 2012-12-04 04:32:20 +0000
@@ -105,14 +105,13 @@
logging.getLogger('maas').warn(WARNING_MESSAGE % ip)
-def get_dns_server_address():
+def get_dns_server_address(nodegroup=None):
"""Return the DNS server's IP address.
- That address is derived from DEFAULT_MAAS_URL in order to get a sensible
- default and at the same time give a possibility to the user to change this.
+ That address is derived from DEFAULT_MAAS_URL or nodegroup.maas_url.
"""
try:
- ip = get_maas_facing_server_address()
+ ip = get_maas_facing_server_address(nodegroup)
except socket.error as e:
raise DNSException(
"Unable to find MAAS server IP address: %s. "
=== modified file 'src/maasserver/fields.py'
--- src/maasserver/fields.py 2012-09-21 09:06:21 +0000
+++ src/maasserver/fields.py 2012-12-04 04:32:20 +0000
@@ -32,7 +32,6 @@
ModelChoiceField,
RegexField,
)
-from maasserver.utils.orm import get_one
import psycopg2.extensions
from south.modelsinspector import add_introspection_rules
@@ -85,25 +84,6 @@
else:
return "%s: %s" % (nodegroup.name, interface.ip)
- def find_nodegroup(self, ip_address):
- """Find the nodegroup whose subnet contains `ip_address`.
-
- The matching nodegroup may have multiple interfaces on the subnet,
- but there can be only one matching nodegroup.
- """
- # Avoid circular imports.
- from maasserver.models import NodeGroup
-
- return get_one(NodeGroup.objects.raw("""
- SELECT *
- FROM maasserver_nodegroup
- WHERE id IN (
- SELECT nodegroup_id
- FROM maasserver_nodegroupinterface
- WHERE (inet '%s' & subnet_mask) = (ip & subnet_mask)
- )
- """ % ip_address))
-
def clean(self, value):
"""Django method: provide expected output for various inputs.
@@ -126,11 +106,7 @@
elif isinstance(value, bytes) and '.' not in value:
nodegroup_id = int(value)
else:
- nodegroup = self.find_nodegroup(value)
- if nodegroup is None:
- raise ValidationError(
- "No known subnet contains %s." % value)
- nodegroup_id = nodegroup.id
+ raise ValidationError("Invalid nodegroup: %s." % value)
return super(NodeGroupFormField, self).clean(nodegroup_id)
=== added file 'src/maasserver/migrations/0045_add_tag_kernel_opts.py'
--- src/maasserver/migrations/0045_add_tag_kernel_opts.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/migrations/0045_add_tag_kernel_opts.py 2012-12-04 04:32:20 +0000
@@ -0,0 +1,203 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ # Adding field 'Tag.kernel_opts'
+ db.add_column(u'maasserver_tag', 'kernel_opts',
+ self.gf('django.db.models.fields.TextField')(null=True, blank=True),
+ keep_default=False)
+
+
+ def backwards(self, orm):
+ # Deleting field 'Tag.kernel_opts'
+ db.delete_column(u'maasserver_tag', 'kernel_opts')
+
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ u'maasserver.bootimage': {
+ 'Meta': {'unique_together': "((u'nodegroup', u'architecture', u'subarchitecture', u'release', u'purpose'),)", 'object_name': 'BootImage'},
+ 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
+ 'purpose': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'release': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'subarchitecture': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ u'maasserver.componenterror': {
+ 'Meta': {'object_name': 'ComponentError'},
+ 'component': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
+ },
+ u'maasserver.config': {
+ 'Meta': {'object_name': 'Config'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'value': ('maasserver.fields.JSONObjectField', [], {'null': 'True'})
+ },
+ u'maasserver.dhcplease': {
+ 'Meta': {'object_name': 'DHCPLease'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip': ('django.db.models.fields.IPAddressField', [], {'unique': 'True', 'max_length': '15'}),
+ 'mac': ('maasserver.fields.MACAddressField', [], {}),
+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"})
+ },
+ u'maasserver.filestorage': {
+ 'Meta': {'object_name': 'FileStorage'},
+ 'content': ('metadataserver.fields.BinaryField', [], {}),
+ 'filename': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ u'maasserver.macaddress': {
+ 'Meta': {'object_name': 'MACAddress'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'mac_address': ('maasserver.fields.MACAddressField', [], {'unique': 'True'}),
+ 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
+ },
+ u'maasserver.node': {
+ 'Meta': {'object_name': 'Node'},
+ 'after_commissioning_action': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'architecture': ('django.db.models.fields.CharField', [], {'default': "u'i386/generic'", 'max_length': '31'}),
+ 'cpu_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'distro_series': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '10', 'null': 'True', 'blank': 'True'}),
+ 'error': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
+ 'hardware_details': ('maasserver.fields.XMLField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
+ 'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'unique': 'True', 'max_length': '255', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'memory': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'netboot': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']", 'null': 'True'}),
+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'power_parameters': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
+ 'power_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '10', 'blank': 'True'}),
+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '10'}),
+ 'system_id': ('django.db.models.fields.CharField', [], {'default': "u'node-a776a79c-298b-11e2-90d1-080027748fea'", 'unique': 'True', 'max_length': '41'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Tag']", 'symmetrical': 'False'}),
+ 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'null': 'True'}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
+ },
+ u'maasserver.nodegroup': {
+ 'Meta': {'object_name': 'NodeGroup'},
+ 'api_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '18'}),
+ 'api_token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'unique': 'True'}),
+ 'cluster_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100', 'blank': 'True'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'dhcp_key': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}),
+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
+ 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'})
+ },
+ u'maasserver.nodegroupinterface': {
+ 'Meta': {'unique_together': "((u'nodegroup', u'interface'),)", 'object_name': 'NodeGroupInterface'},
+ 'broadcast_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'interface': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
+ 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+ 'ip_range_high': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
+ 'ip_range_low': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
+ 'management': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
+ 'router_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
+ 'subnet_mask': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
+ },
+ u'maasserver.sshkey': {
+ 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSHKey'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'key': ('django.db.models.fields.TextField', [], {}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ u'maasserver.tag': {
+ 'Meta': {'object_name': 'Tag'},
+ 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'definition': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'kernel_opts': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
+ },
+ u'maasserver.userprofile': {
+ 'Meta': {'object_name': 'UserProfile'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
+ },
+ 'piston.consumer': {
+ 'Meta': {'object_name': 'Consumer'},
+ 'description': ('django.db.models.fields.TextField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+ 'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '16'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'consumers'", 'null': 'True', 'to': "orm['auth.User']"})
+ },
+ 'piston.token': {
+ 'Meta': {'object_name': 'Token'},
+ 'callback': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'callback_confirmed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Consumer']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_approved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+ 'timestamp': ('django.db.models.fields.IntegerField', [], {'default': '1352369055L'}),
+ 'token_type': ('django.db.models.fields.IntegerField', [], {}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'tokens'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'verifier': ('django.db.models.fields.CharField', [], {'max_length': '10'})
+ }
+ }
+
+ complete_apps = ['maasserver']
\ No newline at end of file
=== added file 'src/maasserver/migrations/0046_add_nodegroup_maas_url.py'
--- src/maasserver/migrations/0046_add_nodegroup_maas_url.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/migrations/0046_add_nodegroup_maas_url.py 2012-12-04 04:32:20 +0000
@@ -0,0 +1,205 @@
+# -*- coding: utf-8 -*-
+import datetime
+
+from django.db import models
+from south.db import db
+from south.v2 import SchemaMigration
+
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ # Adding field 'NodeGroup.maas_url'
+ db.add_column(u'maasserver_nodegroup', 'maas_url',
+ self.gf('django.db.models.fields.CharField')(default=u'', max_length=255, blank=True),
+ keep_default=False)
+
+
+ def backwards(self, orm):
+ # Deleting field 'NodeGroup.maas_url'
+ db.delete_column(u'maasserver_nodegroup', 'maas_url')
+
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ u'maasserver.bootimage': {
+ 'Meta': {'unique_together': "((u'nodegroup', u'architecture', u'subarchitecture', u'release', u'purpose'),)", 'object_name': 'BootImage'},
+ 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
+ 'purpose': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'release': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'subarchitecture': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ u'maasserver.componenterror': {
+ 'Meta': {'object_name': 'ComponentError'},
+ 'component': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
+ },
+ u'maasserver.config': {
+ 'Meta': {'object_name': 'Config'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'value': ('maasserver.fields.JSONObjectField', [], {'null': 'True'})
+ },
+ u'maasserver.dhcplease': {
+ 'Meta': {'object_name': 'DHCPLease'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip': ('django.db.models.fields.IPAddressField', [], {'unique': 'True', 'max_length': '15'}),
+ 'mac': ('maasserver.fields.MACAddressField', [], {}),
+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"})
+ },
+ u'maasserver.filestorage': {
+ 'Meta': {'object_name': 'FileStorage'},
+ 'content': ('metadataserver.fields.BinaryField', [], {}),
+ 'filename': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ u'maasserver.macaddress': {
+ 'Meta': {'object_name': 'MACAddress'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'mac_address': ('maasserver.fields.MACAddressField', [], {'unique': 'True'}),
+ 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
+ },
+ u'maasserver.node': {
+ 'Meta': {'object_name': 'Node'},
+ 'after_commissioning_action': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'architecture': ('django.db.models.fields.CharField', [], {'default': "u'i386/generic'", 'max_length': '31'}),
+ 'cpu_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'distro_series': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '10', 'null': 'True', 'blank': 'True'}),
+ 'error': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
+ 'hardware_details': ('maasserver.fields.XMLField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
+ 'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'unique': 'True', 'max_length': '255', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'memory': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'netboot': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']", 'null': 'True'}),
+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'power_parameters': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
+ 'power_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '10', 'blank': 'True'}),
+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '10'}),
+ 'system_id': ('django.db.models.fields.CharField', [], {'default': "u'node-2cd56f00-3548-11e2-b1cb-9c4e363b1c94'", 'unique': 'True', 'max_length': '41'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Tag']", 'symmetrical': 'False'}),
+ 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'null': 'True'}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
+ },
+ u'maasserver.nodegroup': {
+ 'Meta': {'object_name': 'NodeGroup'},
+ 'api_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '18'}),
+ 'api_token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'unique': 'True'}),
+ 'cluster_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100', 'blank': 'True'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'dhcp_key': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'maas_url': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}),
+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
+ 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'})
+ },
+ u'maasserver.nodegroupinterface': {
+ 'Meta': {'unique_together': "((u'nodegroup', u'interface'),)", 'object_name': 'NodeGroupInterface'},
+ 'broadcast_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'interface': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
+ 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+ 'ip_range_high': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
+ 'ip_range_low': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
+ 'management': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
+ 'router_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
+ 'subnet_mask': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
+ },
+ u'maasserver.sshkey': {
+ 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSHKey'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'key': ('django.db.models.fields.TextField', [], {}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ u'maasserver.tag': {
+ 'Meta': {'object_name': 'Tag'},
+ 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'definition': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'kernel_opts': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
+ },
+ u'maasserver.userprofile': {
+ 'Meta': {'object_name': 'UserProfile'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
+ },
+ 'piston.consumer': {
+ 'Meta': {'object_name': 'Consumer'},
+ 'description': ('django.db.models.fields.TextField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+ 'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '16'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'consumers'", 'null': 'True', 'to': "orm['auth.User']"})
+ },
+ 'piston.token': {
+ 'Meta': {'object_name': 'Token'},
+ 'callback': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'callback_confirmed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Consumer']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_approved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+ 'timestamp': ('django.db.models.fields.IntegerField', [], {'default': '1353659487L'}),
+ 'token_type': ('django.db.models.fields.IntegerField', [], {}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'tokens'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'verifier': ('django.db.models.fields.CharField', [], {'max_length': '10'})
+ }
+ }
+
+ complete_apps = ['maasserver']
\ No newline at end of file
=== modified file 'src/maasserver/models/nodegroup.py'
--- src/maasserver/models/nodegroup.py 2012-12-04 00:25:39 +0000
+++ src/maasserver/models/nodegroup.py 2012-12-04 04:32:20 +0000
@@ -165,6 +165,11 @@
uuid = CharField(
max_length=36, unique=True, null=False, blank=False, editable=True)
+ # The URL where the cluster controller can access the region
+ # controller.
+ maas_url = CharField(
+ blank=True, editable=False, max_length=255, default='')
+
def __repr__(self):
return "<NodeGroup %s>" % self.uuid
=== modified file 'src/maasserver/preseed.py'
--- src/maasserver/preseed.py 2012-11-15 12:05:06 +0000
+++ src/maasserver/preseed.py 2012-12-04 04:32:20 +0000
@@ -37,22 +37,26 @@
GENERIC_FILENAME = 'generic'
-def get_enlist_preseed():
+def get_enlist_preseed(nodegroup=None):
"""Return the enlistment preseed.
+ :param nodegroup: The nodegroup used to generate the preseed.
:return: The rendered preseed string.
:rtype: basestring.
"""
- return render_preseed(None, PRESEED_TYPE.ENLIST)
-
-
-def get_enlist_userdata():
+ return render_enlistment_preseed(
+ PRESEED_TYPE.ENLIST, nodegroup=nodegroup)
+
+
+def get_enlist_userdata(nodegroup=None):
"""Return the enlistment preseed.
+ :param nodegroup: The nodegroup used to generate the preseed.
:return: The rendered enlistment user-data string.
:rtype: basestring.
"""
- return render_preseed(None, PRESEED_TYPE.ENLIST_USERDATA)
+ return render_enlistment_preseed(
+ PRESEED_TYPE.ENLIST_USERDATA, nodegroup=nodegroup)
def get_preseed(node):
@@ -200,42 +204,64 @@
return get_template(prefix, None, default=True)
-def get_preseed_context(node, release=''):
- """Return the context dictionary to be used to render preseed templates
- for this node.
+def get_preseed_context(release='', nodegroup=None):
+ """Return the node-independent context dictionary to be used to render
+ preseed templates.
- :param node: See `get_preseed_filenames`.
- :param prefix: See `get_preseed_filenames`.
:param release: See `get_preseed_filenames`.
+ :param nodegroup: The nodegroup used to generate the preseed.
:return: The context dictionary.
:rtype: dict.
"""
- server_host = get_maas_facing_server_host()
- context = {
+ server_host = get_maas_facing_server_host(nodegroup=nodegroup)
+ base_url = nodegroup.maas_url if nodegroup is not None else None
+ return {
'release': release,
'server_host': server_host,
- 'server_url': absolute_reverse('nodes_handler'),
- 'metadata_enlist_url': absolute_reverse('enlist'),
- }
- if node is not None:
- # Create the url and the url-data (POST parameters) used to turn off
- # PXE booting once the install of the node is finished.
- node_disable_pxe_url = absolute_reverse(
- 'metadata-node-by-id', args=['latest', node.system_id])
- node_disable_pxe_data = urlencode({'op': 'netboot_off'})
- node_context = {
- 'node': node,
- 'preseed_data': compose_preseed(node),
- 'node_disable_pxe_url': node_disable_pxe_url,
- 'node_disable_pxe_data': node_disable_pxe_data,
- }
- context.update(node_context)
-
- return context
+ 'server_url': absolute_reverse('nodes_handler', base_url=base_url),
+ 'metadata_enlist_url': absolute_reverse('enlist', base_url=base_url),
+ }
+
+
+def get_node_preseed_context(node, release=''):
+ """Return the node-dependent context dictionary to be used to render
+ preseed templates.
+
+ :param node: See `get_preseed_filenames`.
+ :param release: See `get_preseed_filenames`.
+ :return: The context dictionary.
+ :rtype: dict.
+ """
+ # Create the url and the url-data (POST parameters) used to turn off
+ # PXE booting once the install of the node is finished.
+ node_disable_pxe_url = absolute_reverse(
+ 'metadata-node-by-id', args=['latest', node.system_id],
+ base_url=node.nodegroup.maas_url)
+ node_disable_pxe_data = urlencode({'op': 'netboot_off'})
+ return {
+ 'node': node,
+ 'preseed_data': compose_preseed(node),
+ 'node_disable_pxe_url': node_disable_pxe_url,
+ 'node_disable_pxe_data': node_disable_pxe_data,
+ }
+
+
+def render_enlistment_preseed(prefix, release='', nodegroup=None):
+ """Return the enlistment preseed.
+
+ :param prefix: See `get_preseed_filenames`.
+ :param release: See `get_preseed_filenames`.
+ :param nodegroup: The nodegroup used to generate the preseed.
+ :return: The rendered preseed string.
+ :rtype: basestring.
+ """
+ template = load_preseed_template(None, prefix, release)
+ context = get_preseed_context(release, nodegroup=nodegroup)
+ return template.substitute(**context)
def render_preseed(node, prefix, release=''):
- """Find and load a `PreseedTemplate` for the given node.
+ """Return the preseed for the given node.
:param node: See `get_preseed_filenames`.
:param prefix: See `get_preseed_filenames`.
@@ -244,23 +270,30 @@
:rtype: basestring.
"""
template = load_preseed_template(node, prefix, release)
- context = get_preseed_context(node, release)
+ nodegroup = node.nodegroup
+ context = get_preseed_context(release, nodegroup=nodegroup)
+ context.update(get_node_preseed_context(node, release))
return template.substitute(**context)
-def compose_enlistment_preseed_url():
- """Compose enlistment preseed URL."""
+def compose_enlistment_preseed_url(nodegroup=None):
+ """Compose enlistment preseed URL.
+
+ :param nodegroup: The nodegroup used to generate the preseed.
+ """
# Always uses the latest version of the metadata API.
+ base_url = nodegroup.maas_url if nodegroup is not None else None
version = 'latest'
return absolute_reverse(
'metadata-enlist-preseed', args=[version],
- query={'op': 'get_enlist_preseed'})
+ query={'op': 'get_enlist_preseed'}, base_url=base_url)
def compose_preseed_url(node):
"""Compose a metadata URL for `node`'s preseed data."""
# Always uses the latest version of the metadata API.
version = 'latest'
+ base_url = node.nodegroup.maas_url
return absolute_reverse(
'metadata-node-by-id', args=[version, node.system_id],
- query={'op': 'get_preseed'})
+ query={'op': 'get_preseed'}, base_url=base_url)
=== modified file 'src/maasserver/server_address.py'
--- src/maasserver/server_address.py 2012-08-06 10:11:02 +0000
+++ src/maasserver/server_address.py 2012-12-04 04:32:20 +0000
@@ -22,23 +22,29 @@
from django.conf import settings
-def get_maas_facing_server_host():
+def get_maas_facing_server_host(nodegroup=None):
"""Return configured MAAS server hostname, for use by nodes or workers.
- :return: Hostname or IP address, exactly as configured in the
- DEFAULT_MAAS_URL setting.
+ :param nodegroup: The nodegroup from the point of view of which the
+ server host should be computed.
+ :return: Hostname or IP address, as configured in the DEFAULT_MAAS_URL
+ setting or as configured on nodegroup.maas_url.
"""
- return urlparse(settings.DEFAULT_MAAS_URL).hostname
-
-
-def get_maas_facing_server_address():
+ if nodegroup is None or not nodegroup.maas_url:
+ maas_url = settings.DEFAULT_MAAS_URL
+ else:
+ maas_url = nodegroup.maas_url
+ return urlparse(maas_url).hostname
+
+
+def get_maas_facing_server_address(nodegroup=None):
"""Return address where nodes and workers can reach the MAAS server.
- The address is taken from DEFAULT_MAAS_URL, which in turn is based on the
- server's primary IP address by default, but can be overridden for
- multi-interface servers where this guess is wrong.
+ The address is taken from DEFAULT_MAAS_URL or nodegroup.maas_url.
+ :param nodegroup: The nodegroup from the point of view of which the
+ server address should be computed.
:return: An IP address. If the configured URL uses a hostname, this
function will resolve that hostname.
"""
- return gethostbyname(get_maas_facing_server_host())
+ return gethostbyname(get_maas_facing_server_host(nodegroup))
=== modified file 'src/maasserver/templates/maasserver/enlist_preseed.html'
--- src/maasserver/templates/maasserver/enlist_preseed.html 2012-06-22 07:50:12 +0000
+++ src/maasserver/templates/maasserver/enlist_preseed.html 2012-12-04 04:32:20 +0000
@@ -5,6 +5,7 @@
{% block page-title %}Enlistment preseed{% endblock %}
{% block content %}
+ {{ warning_message }}
<pre>
{{ preseed }}
</pre>
=== modified file 'src/maasserver/testing/factory.py'
--- src/maasserver/testing/factory.py 2012-11-16 13:50:43 +0000
+++ src/maasserver/testing/factory.py 2012-12-04 04:32:20 +0000
@@ -157,7 +157,7 @@
router_ip=None, network=None, subnet_mask=None,
broadcast_ip=None, ip_range_low=None,
ip_range_high=None, interface=None, management=None,
- status=None, **kwargs):
+ status=None, maas_url='', **kwargs):
"""Create a :class:`NodeGroup`.
If network (an instance of IPNetwork) is provided, use it to populate
@@ -182,6 +182,7 @@
ng = NodeGroup.objects.new(
name=name, uuid=uuid, **interface_settings)
ng.status = status
+ ng.maas_url = maas_url
ng.save()
return ng
=== modified file 'src/maasserver/tests/test_api.py'
--- src/maasserver/tests/test_api.py 2012-12-04 01:02:21 +0000
+++ src/maasserver/tests/test_api.py 2012-12-04 04:32:20 +0000
@@ -49,7 +49,10 @@
EnvironmentVariableFixture,
Fixture,
)
-from maasserver import api
+from maasserver import (
+ api,
+ server_address,
+ )
from maasserver.api import (
describe,
DISPLAYED_NODEGROUP_FIELDS,
@@ -149,6 +152,7 @@
from testtools.matchers import (
AfterPreprocessing,
AllMatch,
+ Annotate,
Contains,
Equals,
Is,
@@ -758,7 +762,7 @@
NODE_AFTER_COMMISSIONING_ACTION.DEFAULT,
'mac_addresses': [factory.getRandomMACAddress()],
},
- HTTP_HOST=origin_ip + ':90')
+ REMOTE_ADDR=origin_ip)
self.assertEqual(httplib.OK, response.status_code, response.content)
parsed_result = json.loads(response.content)
node = Node.objects.get(system_id=parsed_result.get('system_id'))
@@ -3497,6 +3501,43 @@
compose_enlistment_preseed_url(),
json.loads(response.content)["preseed_url"])
+ def test_pxeconfig_enlistment_preseed_url_detects_request_origin(self):
+ self.silence_get_ephemeral_name()
+ hostname = factory.make_hostname()
+ ng_url = 'http://%s' % hostname
+ network = IPNetwork("10.1.1/24")
+ ip = factory.getRandomIPInNetwork(network)
+ self.patch(server_address, 'gethostbyname', Mock(return_value=ip))
+ factory.make_node_group(maas_url=ng_url, network=network)
+ params = self.get_default_params()
+
+ # Simulate that the request originates from ip by setting
+ # 'REMOTE_ADDR'.
+ response = self.client.get(
+ reverse('pxeconfig'), params, REMOTE_ADDR=ip)
+ self.assertThat(
+ json.loads(response.content)["preseed_url"],
+ StartsWith(ng_url))
+
+ def test_pxeconfig_enlistment_log_host_url_detects_request_origin(self):
+ self.silence_get_ephemeral_name()
+ hostname = factory.make_hostname()
+ ng_url = 'http://%s' % hostname
+ network = IPNetwork("10.1.1/24")
+ ip = factory.getRandomIPInNetwork(network)
+ mock = self.patch(
+ server_address, 'gethostbyname', Mock(return_value=ip))
+ factory.make_node_group(maas_url=ng_url, network=network)
+ params = self.get_default_params()
+
+ # Simulate that the request originates from ip by setting
+ # 'REMOTE_ADDR'.
+ response = self.client.get(
+ reverse('pxeconfig'), params, REMOTE_ADDR=ip)
+ self.assertEqual(
+ (ip, hostname),
+ (json.loads(response.content)["log_host"], mock.call_args[0][0]))
+
def test_pxeconfig_has_preseed_url_for_known_node(self):
params = self.get_mac_params()
node = MACAddress.objects.get(mac_address=params['mac']).node
@@ -3505,6 +3546,25 @@
compose_preseed_url(node),
json.loads(response.content)["preseed_url"])
+ def test_preseed_url_for_known_node_uses_nodegroup_maas_url(self):
+ ng_url = 'http://%s' % factory.make_name('host')
+ network = IPNetwork("10.1.1/24")
+ ip = factory.getRandomIPInNetwork(network)
+ self.patch(server_address, 'gethostbyname', Mock(return_value=ip))
+ nodegroup = factory.make_node_group(maas_url=ng_url, network=network)
+ params = self.get_mac_params()
+ node = MACAddress.objects.get(mac_address=params['mac']).node
+ node.nodegroup = nodegroup
+ node.save()
+
+ # Simulate that the request originates from ip by setting
+ # 'REMOTE_ADDR'.
+ response = self.client.get(
+ reverse('pxeconfig'), params, REMOTE_ADDR=ip)
+ self.assertThat(
+ json.loads(response.content)["preseed_url"],
+ StartsWith(ng_url))
+
def test_get_boot_purpose_unknown_node(self):
# A node that's not yet known to MAAS is assumed to be enlisting,
# which uses a "commissioning" image.
@@ -3795,6 +3855,87 @@
self.assertIn('application/json', response['Content-Type'])
self.assertEqual({'BROKER_URL': fake_broker_url}, parsed_result)
+ def assertSuccess(self, response):
+ """Assert that `response` was successful (i.e. HTTP 2xx)."""
+ self.assertThat(
+ {code for code in httplib.responses if code // 100 == 2},
+ Annotate(response, Contains(response.status_code)))
+
+ def test_register_new_nodegroup_does_not_record_maas_url(self):
+ # When registering a cluster, the URL with which the call was made
+ # (i.e. from the perspective of the cluster) is *not* recorded.
+ self.create_configured_master()
+ name = factory.make_name('name')
+ uuid = factory.getRandomUUID()
+ update_maas_url = self.patch(api, "update_nodegroup_maas_url")
+ response = self.client.post(
+ reverse('nodegroups_handler'),
+ {'op': 'register', 'name': name, 'uuid': uuid})
+ self.assertSuccess(response)
+ self.assertEqual([], update_maas_url.call_args_list)
+
+ def test_register_accepted_nodegroup_updates_maas_url(self):
+ # When registering an existing, accepted, cluster, the URL with which
+ # the call was made is updated.
+ self.create_configured_master()
+ nodegroup = factory.make_node_group(status=NODEGROUP_STATUS.ACCEPTED)
+ update_maas_url = self.patch(api, "update_nodegroup_maas_url")
+ response = self.client.post(
+ reverse('nodegroups_handler'),
+ {'op': 'register', 'uuid': nodegroup.uuid})
+ self.assertSuccess(response)
+ update_maas_url.assert_called_once_with(nodegroup, ANY)
+
+ def test_register_pending_nodegroup_does_not_update_maas_url(self):
+ # When registering an existing, pending, cluster, the URL with which
+ # the call was made is *not* updated.
+ self.create_configured_master()
+ nodegroup = factory.make_node_group(status=NODEGROUP_STATUS.PENDING)
+ update_maas_url = self.patch(api, "update_nodegroup_maas_url")
+ response = self.client.post(
+ reverse('nodegroups_handler'),
+ {'op': 'register', 'uuid': nodegroup.uuid})
+ self.assertSuccess(response)
+ self.assertEqual([], update_maas_url.call_args_list)
+
+ def test_register_rejected_nodegroup_does_not_update_maas_url(self):
+ # When registering an existing, pending, cluster, the URL with which
+ # the call was made is *not* updated.
+ self.create_configured_master()
+ nodegroup = factory.make_node_group(status=NODEGROUP_STATUS.REJECTED)
+ update_maas_url = self.patch(api, "update_nodegroup_maas_url")
+ response = self.client.post(
+ reverse('nodegroups_handler'),
+ {'op': 'register', 'uuid': nodegroup.uuid})
+ self.assertEqual(httplib.FORBIDDEN, response.status_code)
+ self.assertEqual([], update_maas_url.call_args_list)
+
+ def test_register_master_nodegroup_does_not_update_maas_url(self):
+ # When registering the master cluster, the URL with which the call was
+ # made is *not* updated.
+ name = factory.make_name('name')
+ update_maas_url = self.patch(api, "update_nodegroup_maas_url")
+ response = self.client.post(
+ reverse('nodegroups_handler'),
+ {'op': 'register', 'name': name, 'uuid': 'master'})
+ self.assertSuccess(response)
+ self.assertEqual([], update_maas_url.call_args_list)
+ # The new node group's maas_url field remains empty.
+ nodegroup = NodeGroup.objects.get(uuid='master')
+ self.assertEqual("", nodegroup.maas_url)
+
+
+class TestUpdateNodeGroupMAASURL(TestCase):
+ """Tests for `update_nodegroup_maas_url`."""
+
+ def test_update_from_request(self):
+ request_factory = RequestFactory(SCRIPT_NAME="/script")
+ request = request_factory.get(
+ "/script/path", SERVER_NAME="example.com")
+ nodegroup = factory.make_node_group()
+ api.update_nodegroup_maas_url(nodegroup, request)
+ self.assertEqual("http://example.com/script", nodegroup.maas_url)
+
def dict_subset(obj, fields):
"""Return a dict of a subset of the fields/values of an object."""
=== modified file 'src/maasserver/tests/test_dhcp.py'
--- src/maasserver/tests/test_dhcp.py 2012-10-11 09:39:49 +0000
+++ src/maasserver/tests/test_dhcp.py 2012-12-04 04:32:20 +0000
@@ -22,7 +22,6 @@
)
from maasserver.dns import get_dns_server_address
from maasserver.enum import NODEGROUP_STATUS
-from maasserver.server_address import get_maas_facing_server_address
from maasserver.testing.factory import factory
from maasserver.testing.testcase import TestCase
from maastesting.celery import CeleryFixture
@@ -81,10 +80,6 @@
param: getattr(interface, param)
for param in dhcp_params}
- # Currently all nodes use the central TFTP server. This will be
- # decentralized to use NodeGroup.worker_ip later.
- expected_params["next_server"] = get_maas_facing_server_address()
-
expected_params["omapi_key"] = nodegroup.dhcp_key
expected_params["dns_servers"] = get_dns_server_address()
expected_params["subnet"] = '192.168.100.0'
@@ -98,6 +93,22 @@
self.assertEqual(expected_params, result_params)
+ def test_dhcp_config_uses_dns_server_from_cluster_controller(self):
+ mocked_task = self.patch(dhcp, 'write_dhcp_config')
+ ip = factory.getRandomIPAddress()
+ maas_url = 'http://%s/' % ip
+ nodegroup = factory.make_node_group(
+ maas_url=maas_url,
+ status=NODEGROUP_STATUS.ACCEPTED,
+ dhcp_key=factory.getRandomString(),
+ interface=factory.make_name('eth'),
+ network=IPNetwork("192.168.102.0/22"))
+ self.patch(settings, "DHCP_CONNECT", True)
+ configure_dhcp(nodegroup)
+ kwargs = mocked_task.apply_async.call_args[1]['kwargs']
+
+ self.assertEqual(ip, kwargs['dns_servers'])
+
def test_configure_dhcp_restart_dhcp_server(self):
self.patch(tasks, "sudo_write_file")
mocked_check_call = self.patch(tasks, "check_call")
=== modified file 'src/maasserver/tests/test_dns.py'
--- src/maasserver/tests/test_dns.py 2012-12-03 07:29:53 +0000
+++ src/maasserver/tests/test_dns.py 2012-12-04 04:32:20 +0000
@@ -91,7 +91,7 @@
ip = factory.getRandomIPAddress()
resolver = FakeMethod(result=ip)
self.patch(server_address, 'gethostbyname', resolver)
- hostname = factory.getRandomString().lower()
+ hostname = factory.make_hostname()
self.patch_DEFAULT_MAAS_URL_with_random_values(hostname=hostname)
self.assertEqual(
(ip, [(hostname, )]),
@@ -114,6 +114,17 @@
call(dns.WARNING_MESSAGE % '127.0.0.1'),
logger.return_value.warn.call_args)
+ def test_get_dns_server_address_uses_nodegroup_maas_url(self):
+ ip = factory.getRandomIPAddress()
+ resolver = FakeMethod(result=ip)
+ self.patch(server_address, 'gethostbyname', resolver)
+ hostname = factory.make_hostname()
+ maas_url = 'http://%s' % hostname
+ nodegroup = factory.make_node_group(maas_url=maas_url)
+ self.assertEqual(
+ (ip, [(hostname, )]),
+ (dns.get_dns_server_address(nodegroup), resolver.extract_args()))
+
def test_is_dns_managed(self):
nodegroups_with_expected_results = {
factory.make_node_group(
=== modified file 'src/maasserver/tests/test_fields.py'
--- src/maasserver/tests/test_fields.py 2012-10-08 07:35:35 +0000
+++ src/maasserver/tests/test_fields.py 2012-12-04 04:32:20 +0000
@@ -21,7 +21,6 @@
from maasserver.models import (
MACAddress,
NodeGroup,
- nodegroupinterface,
NodeGroupInterface,
)
from maasserver.testing.factory import factory
@@ -33,7 +32,6 @@
JSONFieldModel,
XMLFieldModel,
)
-from netaddr import IPNetwork
class TestNodeGroupFormField(TestCase):
@@ -70,51 +68,6 @@
nodegroup,
NodeGroupFormField().clean("%s" % nodegroup.id))
- def test_clean_finds_nodegroup_by_network_address(self):
- nodegroup = factory.make_node_group(
- network=IPNetwork("192.168.28.1/24"))
- self.assertEqual(
- nodegroup,
- NodeGroupFormField().clean('192.168.28.0'))
-
- def test_find_nodegroup_looks_up_nodegroup_by_controller_ip(self):
- nodegroup = factory.make_node_group()
- self.assertEqual(
- nodegroup,
- NodeGroupFormField().clean(nodegroup.get_managed_interface().ip))
-
- def test_find_nodegroup_accepts_any_ip_in_nodegroup_subnet(self):
- nodegroup = factory.make_node_group(
- network=IPNetwork("192.168.41.0/24"))
- self.assertEqual(
- nodegroup,
- NodeGroupFormField().clean('192.168.41.199'))
-
- def test_find_nodegroup_reports_if_not_found(self):
- self.assertRaises(
- ValidationError,
- NodeGroupFormField().clean,
- factory.getRandomIPAddress())
-
- def test_find_nodegroup_reports_if_multiple_matches(self):
- self.patch(nodegroupinterface, "MINIMUM_NETMASK_BITS", 1)
- factory.make_node_group(network=IPNetwork("10/8"))
- factory.make_node_group(network=IPNetwork("10.1.1/24"))
- self.assertRaises(
- NodeGroup.MultipleObjectsReturned,
- NodeGroupFormField().clean, '10.1.1.2')
-
- def test_find_nodegroup_handles_multiple_matches_on_same_nodegroup(self):
- self.patch(nodegroupinterface, "MINIMUM_NETMASK_BITS", 1)
- nodegroup = factory.make_node_group(network=IPNetwork("10/8"))
- NodeGroupInterface.objects.create(
- nodegroup=nodegroup, ip='10.0.0.2', subnet_mask='255.0.0.0',
- broadcast_ip='10.0.0.1', interface='eth71')
- NodeGroupInterface.objects.create(
- nodegroup=nodegroup, ip='10.0.0.3', subnet_mask='255.0.0.0',
- broadcast_ip='10.0.0.2', interface='eth72')
- self.assertEqual(nodegroup, NodeGroupFormField().clean('10.0.0.9'))
-
class TestMACAddressField(TestCase):
=== modified file 'src/maasserver/tests/test_forms.py'
--- src/maasserver/tests/test_forms.py 2012-11-16 13:50:43 +0000
+++ src/maasserver/tests/test_forms.py 2012-12-04 04:32:20 +0000
@@ -203,14 +203,6 @@
NodeGroup.objects.ensure_master(),
NodeWithMACAddressesForm(self.make_params()).save().nodegroup)
- def test_sets_nodegroup_on_new_node_if_requested(self):
- nodegroup = factory.make_node_group(
- network=IPNetwork("192.168.14.0/24"), ip_range_low='192.168.14.2',
- ip_range_high='192.168.14.254', ip='192.168.14.1')
- form = NodeWithMACAddressesForm(
- self.make_params(nodegroup=nodegroup.get_managed_interface().ip))
- self.assertEqual(nodegroup, form.save().nodegroup)
-
def test_leaves_nodegroup_alone_if_unset_on_existing_node(self):
# Selecting a node group for a node is only supported on new
# nodes. You can't change it later.
=== modified file 'src/maasserver/tests/test_preseed.py'
--- src/maasserver/tests/test_preseed.py 2012-11-19 12:26:32 +0000
+++ src/maasserver/tests/test_preseed.py 2012-12-04 04:32:20 +0000
@@ -26,12 +26,14 @@
compose_preseed_url,
GENERIC_FILENAME,
get_enlist_preseed,
+ get_node_preseed_context,
get_preseed,
get_preseed_context,
get_preseed_filenames,
get_preseed_template,
load_preseed_template,
PreseedTemplate,
+ render_enlistment_preseed,
render_preseed,
split_subarch,
TemplateNotFoundError,
@@ -41,7 +43,10 @@
from maasserver.utils import map_enum
from testtools.matchers import (
AllMatch,
+ Contains,
IsInstance,
+ MatchesAll,
+ Not,
StartsWith,
)
@@ -289,26 +294,28 @@
"""Tests for `get_preseed_context`."""
def test_get_preseed_context_contains_keys(self):
- node = factory.make_node()
- release = factory.getRandomString()
- context = get_preseed_context(node, release)
- self.assertItemsEqual(
- ['node', 'release', 'metadata_enlist_url',
- 'server_host', 'server_url', 'preseed_data',
- 'node_disable_pxe_url', 'node_disable_pxe_data'],
- context)
-
- def test_get_preseed_context_if_node_None(self):
- # If the provided Node is None (when're in the context of an
- # enlistment preseed) the returned context does not include the
- # node context.
- release = factory.getRandomString()
- context = get_preseed_context(None, release)
+ release = factory.getRandomString()
+ nodegroup = factory.make_node_group(maas_url=factory.getRandomString())
+ context = get_preseed_context(release, nodegroup)
self.assertItemsEqual(
['release', 'metadata_enlist_url', 'server_host', 'server_url'],
context)
+class TestNodePreseedContext(TestCase):
+ """Tests for `get_node_preseed_context`."""
+
+ def test_get_node_preseed_context_contains_keys(self):
+ node = factory.make_node()
+ release = factory.getRandomString()
+ context = get_node_preseed_context(node, release)
+ self.assertItemsEqual(
+ ['node', 'preseed_data', 'node_disable_pxe_url',
+ 'node_disable_pxe_data',
+ ],
+ context)
+
+
class TestPreseedTemplate(TestCase):
"""Tests for class:`PreseedTemplate`."""
@@ -338,6 +345,37 @@
# error.
self.assertIsInstance(preseed, str)
+ def test_get_preseed_uses_nodegroup_maas_url(self):
+ ng_url = 'http://%s' % factory.make_hostname()
+ ng = factory.make_node_group(maas_url=ng_url)
+ maas_url = 'http://%s' % factory.make_hostname()
+ node = factory.make_node(
+ nodegroup=ng, status=NODE_STATUS.COMMISSIONING)
+ self.patch(settings, 'DEFAULT_MAAS_URL', maas_url)
+ preseed = render_preseed(node, self.preseed, "precise")
+ self.assertThat(
+ preseed, MatchesAll(*[Contains(ng_url), Not(Contains(maas_url))]))
+
+
+class TestRenderEnlistmentPreseed(TestCase):
+ """Tests for `render_enlistment_preseed`."""
+
+ def test_render_enlistment_preseed(self):
+ preseed = render_enlistment_preseed(PRESEED_TYPE.ENLIST, "precise")
+ # The test really is that the preseed is rendered without an
+ # error.
+ self.assertIsInstance(preseed, str)
+
+ def test_get_preseed_uses_nodegroup_maas_url(self):
+ ng_url = 'http://%s' % factory.make_hostname()
+ maas_url = 'http://%s' % factory.make_hostname()
+ self.patch(settings, 'DEFAULT_MAAS_URL', maas_url)
+ nodegroup = factory.make_node_group(maas_url=ng_url)
+ preseed = render_enlistment_preseed(
+ PRESEED_TYPE.ENLIST, "precise", nodegroup=nodegroup)
+ self.assertThat(
+ preseed, MatchesAll(*[Contains(ng_url), Not(Contains(maas_url))]))
+
class TestPreseedMethods(TestCase):
"""Tests for `get_enlist_preseed` and `get_preseed`.
=== modified file 'src/maasserver/tests/test_server_address.py'
--- src/maasserver/tests/test_server_address.py 2012-08-06 10:14:07 +0000
+++ src/maasserver/tests/test_server_address.py 2012-12-04 04:32:20 +0000
@@ -15,16 +15,16 @@
from django.conf import settings
from maasserver import server_address
from maasserver.server_address import get_maas_facing_server_address
-from maastesting.factory import factory
+from maasserver.testing.factory import factory
+from maasserver.testing.testcase import TestCase
from maastesting.fakemethod import FakeMethod
-from maastesting.testcase import TestCase
from netaddr import IPNetwork
class TestServerAddress(TestCase):
def make_hostname(self):
- return '%s.example.com' % factory.make_name('host').lower()
+ return '%s.example.com' % factory.make_hostname()
def set_DEFAULT_MAAS_URL(self, hostname=None, with_port=False):
"""Patch DEFAULT_MAAS_URL to be a (partly) random URL."""
@@ -48,6 +48,13 @@
self.set_DEFAULT_MAAS_URL(ip)
self.assertEqual(ip, server_address.get_maas_facing_server_host())
+ def test_get_maas_facing_server_host_returns_nodegroup_maas_url(self):
+ hostname = factory.make_hostname()
+ maas_url = 'http://%s' % hostname
+ nodegroup = factory.make_node_group(maas_url=maas_url)
+ self.assertEqual(
+ hostname, server_address.get_maas_facing_server_host(nodegroup))
+
def test_get_maas_facing_server_host_strips_out_port(self):
hostname = self.make_hostname()
self.set_DEFAULT_MAAS_URL(hostname, with_port=True)
@@ -64,6 +71,13 @@
self.set_DEFAULT_MAAS_URL(hostname=ip)
self.assertEqual(ip, get_maas_facing_server_address())
+ def test_get_maas_facing_server_address_returns_nodegroup_maas_url(self):
+ ip = factory.getRandomIPInNetwork(IPNetwork('127.0.0.0/8'))
+ maas_url = 'http://%s' % ip
+ nodegroup = factory.make_node_group(maas_url=maas_url)
+ self.assertEqual(
+ ip, server_address.get_maas_facing_server_host(nodegroup))
+
def test_get_maas_facing_server_address_resolves_hostname(self):
ip = factory.getRandomIPAddress()
resolver = FakeMethod(result=ip)
=== modified file 'src/maasserver/tests/test_views_nodes.py'
--- src/maasserver/tests/test_views_nodes.py 2012-12-03 07:08:59 +0000
+++ src/maasserver/tests/test_views_nodes.py 2012-12-04 04:32:20 +0000
@@ -619,6 +619,22 @@
for a in document.xpath("//div[@class='pagination']//a")])
+class NodeEnlistmentPreseedViewTest(LoggedInTestCase):
+
+ def test_enlistpreseedview_displays_preseed_data(self):
+ response = self.client.get(reverse('enlist-preseed-view'))
+ # Simply test that the preseed looks ok.
+ self.assertIn('metadata_url', response.content)
+
+ def test_enlistpreseedview_display_warning_about_url(self):
+ response = self.client.get(reverse('enlist-preseed-view'))
+ message_chunk = (
+ "The URL mentioned in the following enlistment preseed will "
+ "be different depending on"
+ )
+ self.assertIn(message_chunk, response.content)
+
+
class NodePreseedViewTest(LoggedInTestCase):
def test_preseedview_node_displays_preseed_data(self):
=== modified file 'src/maasserver/utils/__init__.py'
--- src/maasserver/utils/__init__.py 2012-11-22 15:59:37 +0000
+++ src/maasserver/utils/__init__.py 2012-12-04 04:32:20 +0000
@@ -13,14 +13,13 @@
__all__ = [
'absolute_reverse',
'build_absolute_uri',
+ 'find_nodegroup',
'get_db_state',
- 'get_origin_ip',
'ignore_unused',
'map_enum',
'strip_domain',
]
-import socket
from urllib import urlencode
from urlparse import urljoin
@@ -67,7 +66,7 @@
}
-def absolute_reverse(view_name, query=None, *args, **kwargs):
+def absolute_reverse(view_name, query=None, base_url=None, *args, **kwargs):
"""Return the absolute URL (i.e. including the URL scheme specifier and
the network location of the MAAS server). Internally this method simply
calls Django's 'reverse' method and prefixes the result of that call with
@@ -78,11 +77,14 @@
:param query: Optional query argument which will be passed down to
urllib.urlencode. The result of that call will be appended to the
resulting url.
+ :param base_url: Optional url used as base. If None is provided, then
+ settings.DEFAULT_MAAS_URL will be used.
:param args: Positional arguments for Django's 'reverse' method.
:param kwargs: Named arguments for Django's 'reverse' method.
"""
- url = urljoin(
- settings.DEFAULT_MAAS_URL, reverse(view_name, *args, **kwargs))
+ if not base_url:
+ base_url = settings.DEFAULT_MAAS_URL
+ url = urljoin(base_url, reverse(view_name, *args, **kwargs))
if query is not None:
url += '?%s' % urlencode(query, doseq=True)
return url
@@ -107,14 +109,25 @@
return hostname.split('.', 1)[0]
-def get_origin_ip(request):
- """Return the IP address of the originating host of the request.
+def find_nodegroup(request):
+ """Find the nodegroup whose subnet contains the IP Address of the
+ originating host of the request..
- Return the IP address obtained by resolving the host given by
- request.get_host().
+ The matching nodegroup may have multiple interfaces on the subnet,
+ but there can be only one matching nodegroup.
"""
- host = request.get_host().split(':')[0]
- try:
- return socket.gethostbyname(host)
- except socket.error:
- return None
+ # Circular imports.
+ from maasserver.models import NodeGroup
+ ip_address = request.META['REMOTE_ADDR']
+ if ip_address is not None:
+ query = NodeGroup.objects.raw("""
+ SELECT *
+ FROM maasserver_nodegroup
+ WHERE id IN (
+ SELECT nodegroup_id
+ FROM maasserver_nodegroupinterface
+ WHERE (inet %s & subnet_mask) = (ip & subnet_mask)
+ )
+ """, [ip_address])
+ return get_one(query)
+ return None
=== modified file 'src/maasserver/utils/tests/test_utils.py'
--- src/maasserver/utils/tests/test_utils.py 2012-11-22 15:59:37 +0000
+++ src/maasserver/utils/tests/test_utils.py 2012-12-04 04:32:20 +0000
@@ -12,7 +12,6 @@
__metaclass__ = type
__all__ = []
-import socket
from urllib import urlencode
from django.conf import settings
@@ -20,21 +19,23 @@
from django.http import HttpRequest
from django.test.client import RequestFactory
from maasserver.enum import NODE_STATUS_CHOICES
+from maasserver.models import (
+ NodeGroup,
+ nodegroupinterface,
+ NodeGroupInterface,
+ )
from maasserver.testing.factory import factory
from maasserver.testing.testcase import TestCase as DjangoTestCase
from maasserver.utils import (
absolute_reverse,
build_absolute_uri,
+ find_nodegroup,
get_db_state,
- get_origin_ip,
map_enum,
strip_domain,
)
from maastesting.testcase import TestCase
-from mock import (
- call,
- Mock,
- )
+from netaddr import IPNetwork
class TestEnum(TestCase):
@@ -74,13 +75,19 @@
class TestAbsoluteReverse(DjangoTestCase):
- def test_absolute_reverse_uses_DEFAULT_MAAS_URL(self):
+ def test_absolute_reverse_uses_DEFAULT_MAAS_URL_by_default(self):
maas_url = 'http://%s' % factory.getRandomString()
self.patch(settings, 'DEFAULT_MAAS_URL', maas_url)
absolute_url = absolute_reverse('settings')
expected_url = settings.DEFAULT_MAAS_URL + reverse('settings')
self.assertEqual(expected_url, absolute_url)
+ def test_absolute_reverse_uses_given_base_url(self):
+ maas_url = 'http://%s' % factory.getRandomString()
+ absolute_url = absolute_reverse('settings', base_url=maas_url)
+ expected_url = maas_url + reverse('settings')
+ self.assertEqual(expected_url, absolute_url)
+
def test_absolute_reverse_uses_query_string(self):
self.patch(settings, 'DEFAULT_MAAS_URL', '')
parameters = {factory.getRandomString(): factory.getRandomString()}
@@ -184,36 +191,52 @@
self.assertEqual(results, map(strip_domain, inputs))
-class TestGetOriginIP(TestCase):
-
- def get_request(self, server_name, server_port='80'):
- return RequestFactory().post(
- '/', SERVER_NAME=server_name, SERVER_PORT=server_port)
-
- def test_get_origin_ip_returns_ip(self):
- ip = factory.getRandomIPAddress()
- request = self.get_request(ip)
- self.assertEqual(ip, get_origin_ip(request))
-
- def test_get_origin_ip_strips_port(self):
- ip = factory.getRandomIPAddress()
- request = self.get_request(ip, '8888')
- self.assertEqual(ip, get_origin_ip(request))
-
- def test_get_origin_ip_resolves_hostname(self):
- ip = factory.getRandomIPAddress()
- hostname = factory.make_name('hostname')
- request = self.get_request(hostname)
- resolver = self.patch(socket, 'gethostbyname', Mock(return_value=ip))
- self.assertEqual(
- (ip, call(hostname)),
- (get_origin_ip(request), resolver.call_args))
-
- def test_get_origin_ip_returns_None_if_hostname_cannot_get_resolved(self):
- hostname = factory.make_name('hostname')
- request = self.get_request(hostname)
- resolver = self.patch(
- socket, 'gethostbyname', Mock(side_effect=socket.error))
- self.assertEqual(
- (None, call(hostname)),
- (get_origin_ip(request), resolver.call_args))
+def get_request(origin_ip):
+ return RequestFactory().post('/', REMOTE_ADDR=origin_ip)
+
+
+class TestFindNodegroup(DjangoTestCase):
+
+ def test_finds_nodegroup_by_network_address(self):
+ nodegroup = factory.make_node_group(
+ network=IPNetwork("192.168.28.1/24"))
+ self.assertEqual(
+ nodegroup,
+ find_nodegroup(get_request('192.168.28.0')))
+
+ def test_find_nodegroup_looks_up_nodegroup_by_controller_ip(self):
+ nodegroup = factory.make_node_group()
+ ip = nodegroup.get_managed_interface().ip
+ self.assertEqual(
+ nodegroup,
+ find_nodegroup(get_request(ip)))
+
+ def test_find_nodegroup_accepts_any_ip_in_nodegroup_subnet(self):
+ nodegroup = factory.make_node_group(
+ network=IPNetwork("192.168.41.0/24"))
+ self.assertEqual(
+ nodegroup,
+ find_nodegroup(get_request('192.168.41.199')))
+
+ def test_find_nodegroup_returns_None_if_not_found(self):
+ self.assertIsNone(
+ find_nodegroup(get_request(factory.getRandomIPAddress())))
+
+ def test_find_nodegroup_errors_if_multiple_matches(self):
+ self.patch(nodegroupinterface, "MINIMUM_NETMASK_BITS", 1)
+ factory.make_node_group(network=IPNetwork("10/8"))
+ factory.make_node_group(network=IPNetwork("10.1.1/24"))
+ self.assertRaises(
+ NodeGroup.MultipleObjectsReturned,
+ find_nodegroup, get_request('10.1.1.2'))
+
+ def test_find_nodegroup_handles_multiple_matches_on_same_nodegroup(self):
+ self.patch(nodegroupinterface, "MINIMUM_NETMASK_BITS", 1)
+ nodegroup = factory.make_node_group(network=IPNetwork("10/8"))
+ NodeGroupInterface.objects.create(
+ nodegroup=nodegroup, ip='10.0.0.2', subnet_mask='255.0.0.0',
+ broadcast_ip='10.0.0.1', interface='eth71')
+ NodeGroupInterface.objects.create(
+ nodegroup=nodegroup, ip='10.0.0.3', subnet_mask='255.0.0.0',
+ broadcast_ip='10.0.0.2', interface='eth72')
+ self.assertEqual(nodegroup, find_nodegroup(get_request('10.0.0.9')))
=== modified file 'src/maasserver/views/nodes.py'
--- src/maasserver/views/nodes.py 2012-12-03 07:08:59 +0000
+++ src/maasserver/views/nodes.py 2012-12-04 04:32:20 +0000
@@ -178,10 +178,18 @@
def enlist_preseed_view(request):
"""View method to display the enlistment preseed."""
+ warning_message = (
+ "The URL mentioned in the following enlistment preseed will "
+ "be different depending on which cluster controller is "
+ "responsible for the enlisting node. The URL shown here is for "
+ "nodes handled by the cluster controller located in the region "
+ "controller's network."
+ )
+ context = RequestContext(request, {'warning_message': warning_message})
return render_to_response(
'maasserver/enlist_preseed.html',
{'preseed': mark_safe(get_enlist_preseed())},
- context_instance=RequestContext(request))
+ context_instance=context)
class NodeViewMixin:
=== modified file 'src/maastesting/factory.py'
--- src/maastesting/factory.py 2012-09-10 14:50:56 +0000
+++ src/maastesting/factory.py 2012-12-04 04:32:20 +0000
@@ -148,6 +148,13 @@
return sep.join(
filter(None, [prefix, self.getRandomString(size=size)]))
+ def make_hostname(self, prefix='host', *args, **kwargs):
+ """Generate a random hostname.
+
+ The returned hostname is lowercase because python's urlparse
+ implicitely lowercases the hostnames."""
+ return self.make_name(prefix=prefix, *args, **kwargs).lower()
+
def make_names(self, *prefixes):
"""Generate random names.
=== modified file 'src/metadataserver/api.py'
--- src/metadataserver/api.py 2012-11-14 14:36:27 +0000
+++ src/metadataserver/api.py 2012-12-04 04:32:20 +0000
@@ -51,6 +51,7 @@
get_enlist_userdata,
get_preseed,
)
+from maasserver.utils import find_nodegroup
from maasserver.utils.orm import get_one
from metadataserver.models import (
NodeCommissionResult,
@@ -374,7 +375,9 @@
def read(self, request, version):
check_version(version)
- return HttpResponse(get_enlist_userdata(), mimetype="text/plain")
+ nodegroup = find_nodegroup(request)
+ return HttpResponse(
+ get_enlist_userdata(nodegroup=nodegroup), mimetype="text/plain")
class EnlistVersionIndexHandler(OperationsHandler):
@@ -391,7 +394,9 @@
@operation(idempotent=True)
def get_enlist_preseed(self, request, version=None):
"""Render and return a preseed script for enlistment."""
- return HttpResponse(get_enlist_preseed(), mimetype="text/plain")
+ nodegroup = find_nodegroup(request)
+ return HttpResponse(
+ get_enlist_preseed(nodegroup=nodegroup), mimetype="text/plain")
@operation(idempotent=True)
def get_preseed(self, request, version=None, system_id=None):
=== modified file 'src/metadataserver/tests/test_api.py'
--- src/metadataserver/tests/test_api.py 2012-11-14 14:36:27 +0000
+++ src/metadataserver/tests/test_api.py 2012-12-04 04:32:20 +0000
@@ -51,7 +51,14 @@
NodeUserData,
)
from metadataserver.nodeinituser import get_node_init_user
+from mock import Mock
+from netaddr import IPNetwork
from provisioningserver.enum import POWER_TYPE
+from testtools.matchers import (
+ Contains,
+ MatchesAll,
+ Not,
+ )
class TestHelpers(DjangoTestCase):
@@ -629,7 +636,7 @@
'metadata-enlist-preseed', args=['latest'])
# Fake the preseed so we're just exercising the view.
fake_preseed = factory.getRandomString()
- self.patch(api, "get_enlist_preseed", lambda: fake_preseed)
+ self.patch(api, "get_enlist_preseed", Mock(return_value=fake_preseed))
response = self.client.get(
anon_enlist_preseed_url, {'op': 'get_enlist_preseed'})
self.assertEqual(
@@ -641,6 +648,18 @@
response.content),
response)
+ def test_anonymous_get_enlist_preseed_detects_request_origin(self):
+ ng_url = 'http://%s' % factory.make_name('host')
+ network = IPNetwork("10.1.1/24")
+ ip = factory.getRandomIPInNetwork(network)
+ factory.make_node_group(maas_url=ng_url, network=network)
+ anon_enlist_preseed_url = reverse(
+ 'metadata-enlist-preseed', args=['latest'])
+ response = self.client.get(
+ anon_enlist_preseed_url, {'op': 'get_enlist_preseed'},
+ REMOTE_ADDR=ip)
+ self.assertThat(response.content, Contains(ng_url))
+
def test_anonymous_get_preseed(self):
# The preseed for a node can be obtained anonymously.
node = factory.make_node()
@@ -707,13 +726,27 @@
# instance-id must be available
ud_url = reverse('enlist-metadata-user-data', args=['latest'])
fake_preseed = factory.getRandomString()
- self.patch(api, "get_enlist_userdata", lambda: fake_preseed)
+ self.patch(
+ api, "get_enlist_userdata", Mock(return_value= fake_preseed))
response = self.client.get(ud_url)
self.assertEqual(
(httplib.OK, "text/plain", fake_preseed),
(response.status_code, response["Content-Type"], response.content),
response)
+ def test_get_userdata_detects_request_origin(self):
+ nodegroup_url = 'http://%s' % factory.make_name('host')
+ maas_url = 'http://%s' % factory.make_hostname()
+ self.patch(settings, 'DEFAULT_MAAS_URL', maas_url)
+ network = IPNetwork("10.1.1/24")
+ ip = factory.getRandomIPInNetwork(network)
+ factory.make_node_group(maas_url=nodegroup_url, network=network)
+ url = reverse('enlist-metadata-user-data', args=['latest'])
+ response = self.client.get(url, REMOTE_ADDR=ip)
+ self.assertThat(
+ response.content,
+ MatchesAll(Contains(nodegroup_url), Not(Contains(maas_url))))
+
def test_metadata_list(self):
# /enlist/latest/metadata request should list available keys
md_url = reverse('enlist-metadata-meta-data', args=['latest', ""])
=== modified file 'src/provisioningserver/dhcp/tests/test_config.py'
--- src/provisioningserver/dhcp/tests/test_config.py 2012-09-05 05:49:28 +0000
+++ src/provisioningserver/dhcp/tests/test_config.py 2012-12-04 04:32:20 +0000
@@ -27,7 +27,6 @@
{{omapi_key}}
{{subnet}}
{{subnet_mask}}
- {{next_server}}
{{broadcast_ip}}
{{dns_servers}}
{{router_ip}}
@@ -45,7 +44,6 @@
omapi_key="random",
subnet="10.0.0.0",
subnet_mask="255.0.0.0",
- next_server="10.0.0.1",
broadcast_ip="10.255.255.255",
dns_servers="10.1.0.1 10.1.0.2",
router_ip="10.0.0.2",
=== modified file 'src/provisioningserver/tests/test_auth.py'
--- src/provisioningserver/tests/test_auth.py 2012-11-26 02:54:49 +0000
+++ src/provisioningserver/tests/test_auth.py 2012-12-04 04:32:20 +0000
@@ -12,8 +12,6 @@
__metaclass__ = type
__all__ = []
-import os
-
from apiclient.creds import convert_tuple_to_string
from apiclient.testing.credentials import make_api_credentials
from fixtures import EnvironmentVariableFixture
=== modified file 'src/provisioningserver/tests/test_start_cluster_controller.py'
--- src/provisioningserver/tests/test_start_cluster_controller.py 2012-12-04 00:52:42 +0000
+++ src/provisioningserver/tests/test_start_cluster_controller.py 2012-12-04 04:32:20 +0000
@@ -295,4 +295,3 @@
MAAS_URL=server_url,
)
os.execvpe.assert_called_once_with(ANY, ANY, env=env)
-
=== modified file 'src/provisioningserver/tests/test_tasks.py'
--- src/provisioningserver/tests/test_tasks.py 2012-11-26 02:54:49 +0000
+++ src/provisioningserver/tests/test_tasks.py 2012-12-04 04:32:20 +0000
@@ -176,7 +176,6 @@
'omapi_key',
'subnet',
'subnet_mask',
- 'next_server',
'broadcast_ip',
'dns_servers',
'router_ip',
Follow ups