← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rvb/maas/use-new-nodegroupinterface into lp:maas

 

Raphaël Badin has proposed merging lp:~rvb/maas/use-new-nodegroupinterface into lp:maas with lp:~rvb/maas/add-nodegroup_interface as a prerequisite.

Requested reviews:
  MAAS Maintainers (maas-maintainers)

For more details, see:
https://code.launchpad.net/~rvb/maas/use-new-nodegroupinterface/+merge/123915

This branch changes the code so that it uses the network-related info on nodegroupinterface rather than using the old field on nodegroup.  It also removes the old fields on nodegroup.

This will be the basis for most of the future DHCP-related work so please review this with caution.  It goes without saying that I'll be very happy to improve that code based on any meaningful suggestion.

= Pre-imp =

This was discussed with Julian.

= Notes =

- Since we only want to support one managed interface, I've introduced the method 'get_managed_interface()' on nodegroup which returns the only managed interface on a nodegroup. Enforcing that there is only one managed nodegroup at a time will be done when we will get to creating method to actually modify that information.

- The nodegroup creation method (in nodegroup.py or in the test factory) have been extended to create a managed interface as well.  We will probably want to change that when we will want to support multiple interfaces but I think this is the best way to go forward for now.

- nodegroup.is_dhcp_enabled now simply delegates that check to the managed interface (if any).
-- 
https://code.launchpad.net/~rvb/maas/use-new-nodegroupinterface/+merge/123915
Your team MAAS Maintainers is requested to review the proposed merge of lp:~rvb/maas/use-new-nodegroupinterface into lp:maas.
=== modified file 'src/maasserver/dns.py'
--- src/maasserver/dns.py	2012-09-05 13:30:21 +0000
+++ src/maasserver/dns.py	2012-09-12 09:58:32 +0000
@@ -118,12 +118,13 @@
     if serial is None:
         serial = next_zone_serial()
     dns_ip = get_dns_server_address()
+    interface = nodegroup.get_managed_interface()
     return DNSZoneConfig(
         zone_name=nodegroup.name, serial=serial, dns_ip=dns_ip,
-        subnet_mask=nodegroup.subnet_mask,
-        broadcast_ip=nodegroup.broadcast_ip,
-        ip_range_low=nodegroup.ip_range_low,
-        ip_range_high=nodegroup.ip_range_high,
+        subnet_mask=interface.subnet_mask,
+        broadcast_ip=interface.broadcast_ip,
+        ip_range_low=interface.ip_range_low,
+        ip_range_high=interface.ip_range_high,
         mapping=DHCPLease.objects.get_hostname_ip_mapping(nodegroup))
 
 

=== modified file 'src/maasserver/dns_connect.py'
--- src/maasserver/dns_connect.py	2012-09-05 13:30:21 +0000
+++ src/maasserver/dns_connect.py	2012-09-12 09:58:32 +0000
@@ -24,6 +24,7 @@
     Config,
     Node,
     NodeGroup,
+    NodeGroupInterface,
     )
 from maasserver.signals import connect_to_field_change
 
@@ -41,7 +42,7 @@
 
 @receiver(post_save, sender=NodeGroup)
 def dns_post_save_NodeGroup(sender, instance, created, **kwargs):
-    """Create or update DNS zones related to the new nodegroup."""
+    """Create or update DNS zones related to the saved nodegroup."""
     from maasserver.dns import write_full_dns_config, add_zone
     if created:
         add_zone(instance)
@@ -49,6 +50,16 @@
         write_full_dns_config()
 
 
+@receiver(post_save, sender=NodeGroupInterface)
+def dns_post_save_NodeGroupInterface(sender, instance, created, **kwargs):
+    """Create or update DNS zones related to the saved nodegroupinterface."""
+    from maasserver.dns import write_full_dns_config, add_zone
+    if created:
+        add_zone(instance.nodegroup)
+    else:
+        write_full_dns_config()
+
+
 @receiver(post_delete, sender=NodeGroup)
 def dns_post_delete_NodeGroup(sender, instance, **kwargs):
     """Delete DNS zones related to the nodegroup."""

=== modified file 'src/maasserver/management/commands/config_master_dhcp.py'
--- src/maasserver/management/commands/config_master_dhcp.py	2012-09-05 13:30:21 +0000
+++ src/maasserver/management/commands/config_master_dhcp.py	2012-09-12 09:58:32 +0000
@@ -33,11 +33,9 @@
 
 
 dhcp_items = {
-    'dhcp_interfaces':
-        "Space-separated list of network interfaces that should service "
-        "DHCP requests.",
-    'subnet_mask': "Subnet mask, e.g. 255.0.0.0",
-    'broadcast_ip': "Broadcast address for this subnet, e.g. 10.255.255.255",
+    'interface': "The network interface that should service DHCP requests.",
+    'subnet_mask': "Subnet mask, e.g. 255.0.0.0.",
+    'broadcast_ip': "Broadcast address for this subnet, e.g. 10.255.255.255.",
     'router_ip': "Address of default gateway.",
     'ip_range_low': "Lowest IP address to assign to clients.",
     'ip_range_high': "Highest IP address to assign to clients.",
@@ -49,7 +47,7 @@
 
 # Django is weird with null strings.  Not all settings use None as the
 # not-set value.
-clear_settings['dhcp_interfaces'] = ''
+clear_settings['interface'] = ''
 
 
 def get_settings(options):

=== added file 'src/maasserver/migrations/0024_remove_unused_fields_in_nodegroup.py'
--- src/maasserver/migrations/0024_remove_unused_fields_in_nodegroup.py	1970-01-01 00:00:00 +0000
+++ src/maasserver/migrations/0024_remove_unused_fields_in_nodegroup.py	2012-09-12 09:58:32 +0000
@@ -0,0 +1,204 @@
+# encoding: 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):
+        
+        # Deleting field 'NodeGroup.ip_range_high'
+        db.delete_column(u'maasserver_nodegroup', 'ip_range_high')
+
+        # Deleting field 'NodeGroup.ip_range_low'
+        db.delete_column(u'maasserver_nodegroup', 'ip_range_low')
+
+        # Deleting field 'NodeGroup.broadcast_ip'
+        db.delete_column(u'maasserver_nodegroup', 'broadcast_ip')
+
+        # Deleting field 'NodeGroup.dhcp_interfaces'
+        db.delete_column(u'maasserver_nodegroup', 'dhcp_interfaces')
+
+        # Deleting field 'NodeGroup.worker_ip'
+        db.delete_column(u'maasserver_nodegroup', 'worker_ip')
+
+        # Deleting field 'NodeGroup.subnet_mask'
+        db.delete_column(u'maasserver_nodegroup', 'subnet_mask')
+
+        # Deleting field 'NodeGroup.router_ip'
+        db.delete_column(u'maasserver_nodegroup', 'router_ip')
+
+
+    def backwards(self, orm):
+        
+        # Adding field 'NodeGroup.ip_range_high'
+        db.add_column(u'maasserver_nodegroup', 'ip_range_high', self.gf('django.db.models.fields.IPAddressField')(default=u'', unique=True, max_length=15, null=True, blank=True), keep_default=False)
+
+        # Adding field 'NodeGroup.ip_range_low'
+        db.add_column(u'maasserver_nodegroup', 'ip_range_low', self.gf('django.db.models.fields.IPAddressField')(default=u'', unique=True, max_length=15, null=True, blank=True), keep_default=False)
+
+        # Adding field 'NodeGroup.broadcast_ip'
+        db.add_column(u'maasserver_nodegroup', 'broadcast_ip', self.gf('django.db.models.fields.IPAddressField')(default=u'', max_length=15, null=True, blank=True), keep_default=False)
+
+        # Adding field 'NodeGroup.dhcp_interfaces'
+        db.add_column(u'maasserver_nodegroup', 'dhcp_interfaces', self.gf('django.db.models.fields.CharField')(default=u'', max_length=255, blank=True), keep_default=False)
+
+        # Adding field 'NodeGroup.broadcast_ip'
+        db.add_column(u'maasserver_nodegroup', 'worker_ip', self.gf('django.db.models.fields.IPAddressField')(default=u'', max_length=15, null=True, blank=True), keep_default=False)
+
+        # Adding field 'NodeGroup.subnet_mask'
+        db.add_column(u'maasserver_nodegroup', 'subnet_mask', self.gf('django.db.models.fields.IPAddressField')(default=u'', max_length=15, null=True, blank=True), keep_default=False)
+
+        # Adding field 'NodeGroup.router_ip'
+        db.add_column(u'maasserver_nodegroup', 'router_ip', self.gf('django.db.models.fields.IPAddressField')(default=u'', max_length=15, null=True, blank=True), keep_default=False)
+
+
+    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.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'},
+            'data': ('django.db.models.fields.files.FileField', [], {'max_length': '255'}),
+            'filename': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '200'}),
+            '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'", 'max_length': '10'}),
+            'created': ('django.db.models.fields.DateTimeField', [], {}),
+            'error': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
+            'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            '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-aad83a08-fc14-11e1-be20-3c970e0e56dc'", 'unique': 'True', 'max_length': '41'}),
+            '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'}),
+            'created': ('django.db.models.fields.DateTimeField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            '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.IPAddressField', [], {'default': 'None', 'max_length': '15', '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.IPAddressField', [], {'max_length': '15'}),
+            'ip_range_high': ('django.db.models.fields.IPAddressField', [], {'default': 'None', 'max_length': '15', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+            'ip_range_low': ('django.db.models.fields.IPAddressField', [], {'default': 'None', 'max_length': '15', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+            'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
+            'router_ip': ('django.db.models.fields.IPAddressField', [], {'default': 'None', 'max_length': '15', 'null': 'True', 'blank': 'True'}),
+            'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'subnet_mask': ('django.db.models.fields.IPAddressField', [], {'default': 'None', 'max_length': '15', '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.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': '1347370137L'}),
+            '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']

=== modified file 'src/maasserver/models/nodegroup.py'
--- src/maasserver/models/nodegroup.py	2012-09-11 11:10:43 +0000
+++ src/maasserver/models/nodegroup.py	2012-09-12 09:58:32 +0000
@@ -19,18 +19,19 @@
     CharField,
     ForeignKey,
     IntegerField,
-    IPAddressField,
     Manager,
     )
 from maasserver import DefaultMeta
-from maasserver.dhcp import is_dhcp_management_enabled
 from maasserver.enum import (
     NODEGROUP_STATUS,
     NODEGROUP_STATUS_CHOICES,
+    NODEGROUPINTERFACE_STATUS,
     )
+from maasserver.models.nodegroupinterface import NodeGroupInterface
 from maasserver.models.timestampedmodel import TimestampedModel
 from maasserver.refresh_worker import refresh_worker
 from maasserver.server_address import get_maas_facing_server_address
+from maasserver.utils.orm import get_one
 from netaddr import IPAddress
 from piston.models import (
     KEY_SIZE,
@@ -50,20 +51,22 @@
     the model class it manages.
     """
 
-    def new(self, name, uuid, worker_ip, subnet_mask=None,
+    def new(self, name, uuid, ip, subnet_mask=None,
             broadcast_ip=None, router_ip=None, ip_range_low=None,
-            ip_range_high=None, dhcp_key='', dhcp_interfaces=''):
+            ip_range_high=None, dhcp_key='', interface='',
+            status=NODEGROUPINTERFACE_STATUS.UNMANAGED):
         """Create a :class:`NodeGroup` with the given parameters.
 
-        This method will generate API credentials for the nodegroup's
-        worker to use.
+        This method will:
+        - create the related NodeGroupInterface.
+        - generate API credentials for the nodegroup's worker to use.
         """
         # Avoid circular imports.
         from maasserver.models.user import create_auth_token
         from maasserver.worker_user import get_worker_user
 
         dhcp_values = [
-            dhcp_interfaces,
+            interface,
             subnet_mask,
             broadcast_ip,
             router_ip,
@@ -75,13 +78,15 @@
 
         api_token = create_auth_token(get_worker_user())
         nodegroup = NodeGroup(
-            name=name, uuid=uuid, worker_ip=worker_ip,
-            subnet_mask=subnet_mask, broadcast_ip=broadcast_ip,
-            router_ip=router_ip, ip_range_low=ip_range_low,
-            ip_range_high=ip_range_high, api_token=api_token,
-            api_key=api_token.key, dhcp_key=dhcp_key,
-            dhcp_interfaces=dhcp_interfaces)
+            name=name, uuid=uuid,
+            api_token=api_token, api_key=api_token.key, dhcp_key=dhcp_key)
         nodegroup.save()
+        nginterface = NodeGroupInterface(
+            nodegroup=nodegroup, ip=ip, subnet_mask=subnet_mask,
+            broadcast_ip=broadcast_ip, router_ip=router_ip,
+            interface=interface, ip_range_low=ip_range_low,
+            ip_range_high=ip_range_high, status=status)
+        nginterface.save()
         return nodegroup
 
     def ensure_master(self):
@@ -92,7 +97,7 @@
         try:
             master = self.get(name='master')
         except NodeGroup.DoesNotExist:
-            # The master did not ist yet; create it on demand.
+            # The master did not exist yet; create it on demand.
             master = self.new(
                 'master', 'master', '127.0.0.1', dhcp_key=generate_omapi_key())
 
@@ -133,33 +138,26 @@
         max_length=KEY_SIZE, null=False, blank=False, editable=False,
         unique=True)
 
+    dhcp_key = CharField(
+        blank=True, editable=False, max_length=255, default='')
+
     # Unique identifier of the worker.
     uuid = CharField(
         max_length=36, unique=True, null=False, blank=False, editable=False)
 
-    # Address of the worker.
-    worker_ip = IPAddressField(null=False, editable=True, unique=True)
-
-    # DHCP server settings.
-    dhcp_key = CharField(
-        blank=True, editable=False, max_length=255, default='')
-    # Network interface to serve DHCP on.
-    dhcp_interfaces = CharField(
-        blank=True, editable=False, max_length=255, default='')
-    subnet_mask = IPAddressField(
-        editable=True, unique=False, blank=True, null=True, default='')
-    broadcast_ip = IPAddressField(
-        editable=True, unique=False, blank=True, null=True, default='')
-    router_ip = IPAddressField(
-        editable=True, unique=False, blank=True, null=True, default='')
-    ip_range_low = IPAddressField(
-        editable=True, unique=True, blank=True, null=True, default='')
-    ip_range_high = IPAddressField(
-        editable=True, unique=True, blank=True, null=True, default='')
-
     def __repr__(self):
         return "<NodeGroup %r>" % self.name
 
+    def get_managed_interface(self):
+        """Return the configured interface for this nodegroup.
+
+        This is a temporary method that should be refactored once we add
+        proper support for multiple interfaces on a nodegroup.
+        """
+        return get_one(
+            NodeGroupInterface.objects.filter(
+                nodegroup=self, status=NODEGROUPINTERFACE_STATUS.MANAGED))
+
     def set_up_dhcp(self):
         """Write the DHCP configuration file and restart the DHCP server."""
         # Circular imports.
@@ -171,22 +169,18 @@
         # next_server=self.worker_ip.
         next_server = get_maas_facing_server_address()
 
+        interface = self.get_managed_interface()
         subnet = str(
-            IPAddress(self.ip_range_low) & IPAddress(self.subnet_mask))
+            IPAddress(interface.ip_range_low) &
+            IPAddress(interface.subnet_mask))
         write_dhcp_config.delay(
             subnet=subnet, next_server=next_server, omapi_key=self.dhcp_key,
-            subnet_mask=self.subnet_mask, broadcast_ip=self.broadcast_ip,
-            router_ip=self.router_ip, dns_servers=get_dns_server_address(),
-            ip_range_low=self.ip_range_low, ip_range_high=self.ip_range_high)
-
-    def is_dhcp_enabled(self):
-        """Is the DHCP for this nodegroup enabled?"""
-        return is_dhcp_management_enabled() and all([
-                self.subnet_mask,
-                self.broadcast_ip,
-                self.ip_range_low,
-                self.ip_range_high
-                ])
+            subnet_mask=interface.subnet_mask,
+            broadcast_ip=interface.broadcast_ip,
+            router_ip=interface.router_ip,
+            dns_servers=get_dns_server_address(),
+            ip_range_low=interface.ip_range_low,
+            ip_range_high=interface.ip_range_high)
 
     def add_dhcp_host_maps(self, new_leases):
         if self.is_dhcp_enabled() and len(new_leases) > 0:
@@ -195,3 +189,11 @@
             # use 127.0.0.1 as the DHCP server address.
             add_new_dhcp_host_map.delay(
                 new_leases, '127.0.0.1', self.dhcp_key)
+
+    def is_dhcp_enabled(self):
+        """Is the DHCP for this nodegroup enabled?"""
+        interface = self.get_managed_interface()
+        if interface is not None:
+            return interface.is_dhcp_enabled()
+        else:
+            return False

=== modified file 'src/maasserver/models/nodegroupinterface.py'
--- src/maasserver/models/nodegroupinterface.py	2012-09-12 09:58:32 +0000
+++ src/maasserver/models/nodegroupinterface.py	2012-09-12 09:58:32 +0000
@@ -22,6 +22,7 @@
     IPAddressField,
     )
 from maasserver import DefaultMeta
+from maasserver.dhcp import is_dhcp_management_enabled
 from maasserver.enum import (
     NODEGROUPINTERFACE_STATUS,
     NODEGROUPINTERFACE_STATUS_CHOICES,
@@ -61,3 +62,12 @@
 
     def __repr__(self):
         return "<NodeGroupInterface %r,%s>" % (self.nodegroup, self.interface)
+
+    def is_dhcp_enabled(self):
+        """Is the DHCP for this nodegroupinterface enabled?"""
+        return is_dhcp_management_enabled() and all([
+                self.subnet_mask,
+                self.broadcast_ip,
+                self.ip_range_low,
+                self.ip_range_high
+                ])

=== modified file 'src/maasserver/testing/factory.py'
--- src/maasserver/testing/factory.py	2012-09-10 14:50:56 +0000
+++ src/maasserver/testing/factory.py	2012-09-12 09:58:32 +0000
@@ -22,6 +22,7 @@
 from maasserver.enum import (
     ARCHITECTURE,
     NODE_STATUS,
+    NODEGROUPINTERFACE_STATUS,
     )
 from maasserver.models import (
     DHCPLease,
@@ -116,10 +117,11 @@
             Node.objects.filter(id=node.id).update(created=created)
         return reload_object(node)
 
-    def make_node_group(self, name=None, uuid=None, worker_ip=None,
+    def make_node_group(self, name=None, uuid=None, ip=None,
                         router_ip=None, network=None, subnet_mask=None,
                         broadcast_ip=None, ip_range_low=None,
-                        ip_range_high=None, dhcp_interfaces=None, **kwargs):
+                        ip_range_high=None, interface=None, status=None,
+                        **kwargs):
         """Create a :class:`NodeGroup`.
 
         If network (an instance of IPNetwork) is provided, use it to populate
@@ -130,6 +132,8 @@
         Otherwise, use the provided values for these values or use random IP
         addresses if they are not provided.
         """
+        if status is None:
+            status = NODEGROUPINTERFACE_STATUS.MANAGED
         if name is None:
             name = self.make_name('nodegroup')
         if uuid is None:
@@ -140,7 +144,7 @@
             ip_range_low = str(IPAddress(network.first))
             ip_range_high = str(IPAddress(network.last))
             router_ip = factory.getRandomIPInNetwork(network)
-            worker_ip = factory.getRandomIPInNetwork(network)
+            ip = factory.getRandomIPInNetwork(network)
         else:
             if subnet_mask is None:
                 subnet_mask = self.getRandomIPAddress()
@@ -152,16 +156,16 @@
                 ip_range_high = self.getRandomIPAddress()
             if router_ip is None:
                 router_ip = self.getRandomIPAddress()
-            if worker_ip is None:
-                worker_ip = self.getRandomIPAddress()
-        if dhcp_interfaces is None:
-            dhcp_interfaces = self.make_name('interface')
+            if ip is None:
+                ip = self.getRandomIPAddress()
+        if interface is None:
+            interface = self.make_name('interface')
         ng = NodeGroup.objects.new(
-            name=name, uuid=uuid, worker_ip=worker_ip,
+            name=name, uuid=uuid, ip=ip,
             subnet_mask=subnet_mask, broadcast_ip=broadcast_ip,
             router_ip=router_ip, ip_range_low=ip_range_low,
-            ip_range_high=ip_range_high, dhcp_interfaces=dhcp_interfaces,
-            **kwargs)
+            ip_range_high=ip_range_high, interface=interface,
+            status=status, **kwargs)
         ng.save()
         return ng
 

=== modified file 'src/maasserver/tests/test_commands_config_master_dhcp.py'
--- src/maasserver/tests/test_commands_config_master_dhcp.py	2012-09-05 13:30:21 +0000
+++ src/maasserver/tests/test_commands_config_master_dhcp.py	2012-09-12 09:58:32 +0000
@@ -38,7 +38,7 @@
 def make_dhcp_settings():
     """Return an arbitrary dict of DHCP settings."""
     return {
-        'dhcp_interfaces': factory.make_name('interface'),
+        'interface': factory.make_name('interface'),
         'subnet_mask': '255.255.0.0',
         'broadcast_ip': '10.111.255.255',
         'router_ip': factory.getRandomIPAddress(),

=== modified file 'src/maasserver/tests/test_dns.py'
--- src/maasserver/tests/test_dns.py	2012-09-05 13:30:21 +0000
+++ src/maasserver/tests/test_dns.py	2012-09-12 09:58:32 +0000
@@ -95,6 +95,7 @@
     def test_get_zone_creates_DNSZoneConfig(self):
         enable_dns_management()
         nodegroup = factory.make_node_group()
+        interface = nodegroup.get_managed_interface()
         serial = random.randint(1, 100)
         zone = dns.get_zone(nodegroup, serial)
         self.assertAttributes(
@@ -102,10 +103,10 @@
             dict(
                 zone_name=nodegroup.name,
                 serial=serial,
-                subnet_mask=nodegroup.subnet_mask,
-                broadcast_ip=nodegroup.broadcast_ip,
-                ip_range_low=nodegroup.ip_range_low,
-                ip_range_high=nodegroup.ip_range_high,
+                subnet_mask=interface.subnet_mask,
+                broadcast_ip=interface.broadcast_ip,
+                ip_range_low=interface.ip_range_low,
+                ip_range_high=interface.ip_range_high,
                 mapping=DHCPLease.objects.get_hostname_ip_mapping(nodegroup),
                 ))
 
@@ -146,10 +147,11 @@
         if nodegroup is None:
             nodegroup = factory.make_node_group(
                 network=IPNetwork('192.168.0.1/24'))
+        interface = nodegroup.get_managed_interface()
         node = factory.make_node(
             nodegroup=nodegroup, set_hostname=True)
         mac = factory.make_mac_address(node=node)
-        ips = IPRange(nodegroup.ip_range_low, nodegroup.ip_range_high)
+        ips = IPRange(interface.ip_range_low, interface.ip_range_high)
         lease_ip = str(islice(ips, lease_number, lease_number + 1).next())
         lease = factory.make_dhcp_lease(
             nodegroup=nodegroup, mac=mac.mac_address, ip=lease_ip)
@@ -205,8 +207,9 @@
         recorder = FakeMethod()
         self.patch(DNSZoneConfig, 'write_config', recorder)
         nodegroup = factory.make_node_group()
-        nodegroup.subnet_mask = None
-        nodegroup.save()
+        interface = nodegroup.get_managed_interface()
+        interface.subnet_mask = None
+        interface.save()
         self.patch(settings, 'DNS_CONNECT', True)
         dns.add_zone(nodegroup)
         self.assertEqual(0, recorder.call_count)
@@ -238,8 +241,9 @@
         recorder = FakeMethod()
         self.patch(DNSZoneConfig, 'write_config', recorder)
         nodegroup = factory.make_node_group()
-        nodegroup.subnet_mask = None
-        nodegroup.save()
+        interface = nodegroup.get_managed_interface()
+        interface.subnet_mask = None
+        interface.save()
         self.patch(settings, 'DNS_CONNECT', True)
         dns.change_dns_zones(nodegroup)
         self.assertEqual(0, recorder.call_count)
@@ -248,8 +252,9 @@
         recorder = FakeMethod()
         self.patch(DNSZoneConfig, 'write_config', recorder)
         nodegroup = factory.make_node_group()
-        nodegroup.subnet_mask = None
-        nodegroup.save()
+        interface = nodegroup.get_managed_interface()
+        interface.subnet_mask = None
+        interface.save()
         self.patch(settings, 'DNS_CONNECT', True)
         dns.write_full_dns_config()
         self.assertEqual(0, recorder.call_count)
@@ -305,17 +310,18 @@
         nodegroup = factory.make_node_group(network=network)
         self.assertDNSMatches(generated_hostname(ip), nodegroup.name, ip)
 
-    def test_edit_nodegroup_updates_DNS_zone(self):
+    def test_edit_nodegroupinterface_updates_DNS_zone(self):
         self.patch(settings, "DNS_CONNECT", True)
         old_network = IPNetwork('192.168.7.1/24')
         old_ip = factory.getRandomIPInNetwork(old_network)
         nodegroup = factory.make_node_group(network=old_network)
+        interface = nodegroup.get_managed_interface()
         # Edit nodegroup's network information to '192.168.44.1/24'
-        nodegroup.broadcast_ip = '192.168.44.255'
-        nodegroup.netmask = '255.255.255.0'
-        nodegroup.ip_range_low = '192.168.44.0'
-        nodegroup.ip_range_high = '192.168.44.255'
-        nodegroup.save()
+        interface.broadcast_ip = '192.168.44.255'
+        interface.netmask = '255.255.255.0'
+        interface.ip_range_low = '192.168.44.0'
+        interface.ip_range_high = '192.168.44.255'
+        interface.save()
         ip = factory.getRandomIPInNetwork(IPNetwork('192.168.44.1/24'))
         # The ip from the old network does not resolve anymore.
         self.assertEqual([''], self.dig_resolve(generated_hostname(old_ip)))

=== modified file 'src/maasserver/tests/test_nodegroup.py'
--- src/maasserver/tests/test_nodegroup.py	2012-09-11 11:10:43 +0000
+++ src/maasserver/tests/test_nodegroup.py	2012-09-12 09:58:32 +0000
@@ -15,10 +15,13 @@
 from django.conf import settings
 import maasserver
 from maasserver.dns import get_dns_server_address
-from maasserver.models import NodeGroup
+from maasserver.enum import NODEGROUPINTERFACE_STATUS
+from maasserver.models import (
+    NodeGroup,
+    NodeGroupInterface,
+    )
 from maasserver.server_address import get_maas_facing_server_address
 from maasserver.testing import (
-    disable_dhcp_management,
     enable_dhcp_management,
     reload_object,
     )
@@ -27,6 +30,7 @@
 from maasserver.worker_user import get_worker_user
 from maastesting.celery import CeleryFixture
 from maastesting.fakemethod import FakeMethod
+from mock import Mock
 from provisioningserver.omshell import (
     generate_omapi_key,
     Omshell,
@@ -41,7 +45,7 @@
 def make_dhcp_settings():
     """Create a dict of arbitrary nodegroup configuration parameters."""
     return {
-        'dhcp_interfaces': factory.make_name('interface'),
+        'interface': factory.make_name('interface'),
         'subnet_mask': '255.0.0.0',
         'broadcast_ip': '10.255.255.255',
         'router_ip': factory.getRandomIPAddress(),
@@ -151,10 +155,10 @@
 
     def test_ensure_master_preserves_existing_attributes(self):
         master = NodeGroup.objects.ensure_master()
-        ip = factory.getRandomIPAddress()
-        master.worker_ip = ip
+        key = factory.getRandomString()
+        master.dhcp_key = key
         master.save()
-        self.assertEqual(ip, NodeGroup.objects.ensure_master().worker_ip)
+        self.assertEqual(key, NodeGroup.objects.ensure_master().dhcp_key)
 
     def test_get_by_natural_key_looks_up_by_uuid(self):
         nodegroup = factory.make_node_group()
@@ -176,32 +180,22 @@
         ('celery', FixtureResource(CeleryFixture())),
         )
 
-    def test_is_dhcp_enabled_returns_True_if_fully_set_up(self):
-        enable_dhcp_management()
-        self.assertTrue(factory.make_node_group().is_dhcp_enabled())
-
-    def test_is_dhcp_enabled_returns_False_if_disabled(self):
-        disable_dhcp_management()
-        self.assertFalse(factory.make_node_group().is_dhcp_enabled())
-
-    def test_is_dhcp_enabled_returns_False_if_config_is_missing(self):
-        enable_dhcp_management()
-        required_fields = [
-            'subnet_mask', 'broadcast_ip', 'ip_range_low', 'ip_range_high']
-        # Map each required field's name to a nodegroup that has just
-        # that field set to None.
-        nodegroups = {
-            field: factory.make_node_group()
-            for field in required_fields}
-        for field, nodegroup in nodegroups.items():
-            setattr(nodegroup, field, None)
-            nodegroup.save()
-        # List any nodegroups from this mapping that have DHCP
-        # management enabled.  There should not be any.
-        self.assertEqual([], [
-            field
-            for field, nodegroup in nodegroups.items()
-                if nodegroup.is_dhcp_enabled()])
+    def test_is_dhcp_enabled_delegates_to_interface(self):
+        enable_dhcp_management()
+        nodegroup = factory.make_node_group()
+        configured = factory.getRandomBoolean()
+        recorder = Mock(return_value=configured)
+        self.patch(NodeGroupInterface, 'is_dhcp_enabled', recorder)
+        is_dhcp_enabled = nodegroup.is_dhcp_enabled()
+        self.assertEqual(
+            (configured, recorder.call_count), (is_dhcp_enabled, 1))
+
+    def test_is_dhcp_enabled_returns_False_if_interface_not_managed(self):
+        nodegroup = factory.make_node_group()
+        interface = nodegroup.get_managed_interface()
+        interface.status = NODEGROUPINTERFACE_STATUS.UNMANAGED
+        interface.save()
+        self.assertFalse(interface.is_dhcp_enabled())
 
     def test_set_up_dhcp_writes_dhcp_config(self):
         mocked_task = self.patch(
@@ -218,8 +212,9 @@
             'subnet_mask', 'broadcast_ip', 'router_ip',
             'ip_range_low', 'ip_range_high']
 
+        interface = nodegroup.get_managed_interface()
         expected_params = {
-            param: getattr(nodegroup, param)
+            param: getattr(interface, param)
             for param in dhcp_params}
 
         # Currently all nodes use the central TFTP server.  This will be
@@ -249,3 +244,24 @@
         leases = factory.make_random_leases()
         nodegroup.add_dhcp_host_maps(leases)
         self.assertEqual([], Omshell.create.extract_args())
+
+    def test_get_managed_interface_returns_managed_interface(self):
+        nodegroup = factory.make_node_group()
+        interface = nodegroup.nodegroupinterface_set.all()[0]
+        self.assertEqual(interface, nodegroup.get_managed_interface())
+
+    def test_get_managed_interface_does_not_return_unmanaged_interface(self):
+        nodegroup = factory.make_node_group()
+        interface = nodegroup.nodegroupinterface_set.all()[0]
+        interface.status = NODEGROUPINTERFACE_STATUS.UNMANAGED
+        interface.save()
+        self.assertIsNone(nodegroup.get_managed_interface())
+
+    def test_get_managed_interface_does_not_return_unrelated_interface(self):
+        nodegroup = factory.make_node_group()
+        # Create another nodegroup with a managed interface.
+        factory.make_node_group()
+        interface = nodegroup.nodegroupinterface_set.all()[0]
+        interface.status = NODEGROUPINTERFACE_STATUS.UNMANAGED
+        interface.save()
+        self.assertIsNone(nodegroup.get_managed_interface())

=== added file 'src/maasserver/tests/test_nodegroupinterface.py'
--- src/maasserver/tests/test_nodegroupinterface.py	1970-01-01 00:00:00 +0000
+++ src/maasserver/tests/test_nodegroupinterface.py	2012-09-12 09:58:32 +0000
@@ -0,0 +1,56 @@
+# Copyright 2012 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for the NodeGroupInterface model."""
+
+from __future__ import (
+    absolute_import,
+    print_function,
+    unicode_literals,
+    )
+
+__metaclass__ = type
+__all__ = []
+
+from maasserver.testing import (
+    disable_dhcp_management,
+    enable_dhcp_management,
+    )
+from maasserver.testing.factory import factory
+from maasserver.testing.testcase import TestCase
+from maastesting.celery import CeleryFixture
+from testresources import FixtureResource
+
+
+class TestNodeGroupInterface(TestCase):
+
+    resources = (
+        ('celery', FixtureResource(CeleryFixture())),
+        )
+
+    def test_is_dhcp_enabled_returns_True_if_fully_set_up(self):
+        enable_dhcp_management()
+        self.assertTrue(factory.make_node_group().is_dhcp_enabled())
+
+    def test_is_dhcp_enabled_returns_False_if_disabled(self):
+        disable_dhcp_management()
+        self.assertFalse(factory.make_node_group().is_dhcp_enabled())
+
+    def test_is_dhcp_enabled_returns_False_if_config_is_missing(self):
+        enable_dhcp_management()
+        required_fields = [
+            'subnet_mask', 'broadcast_ip', 'ip_range_low', 'ip_range_high']
+        # Map each required field's name to a nodegroupinterface that
+        # has just that field set to None.
+        nodegroupinterfaces = {
+            field: factory.make_node_group().get_managed_interface()
+            for field in required_fields}
+        for field, nodegroupinterface in nodegroupinterfaces.items():
+            setattr(nodegroupinterface, field, None)
+            nodegroupinterface.save()
+        # List any nodegroups from this mapping that have DHCP
+        # management enabled.  There should not be any.
+        self.assertEqual([], [
+            field
+            for field, nodegroupinterface in nodegroupinterfaces.items()
+                if nodegroupinterface.is_dhcp_enabled()])

=== modified file 'src/provisioningserver/tests/test_tasks.py'
--- src/provisioningserver/tests/test_tasks.py	2012-09-10 14:27:00 +0000
+++ src/provisioningserver/tests/test_tasks.py	2012-09-12 09:58:32 +0000
@@ -160,7 +160,7 @@
     def make_dhcp_config_params(self):
         """Fake up a dict of dhcp configuration parameters."""
         param_names = [
-            'dhcp_interfaces',
+            'interface',
             'omapi_key',
             'subnet',
             'subnet_mask',


Follow ups