Merge lp:~julian-edwards/maas/models-for-static-dhcp into lp:~maas-committers/maas/trunk

Proposed by Julian Edwards
Status: Merged
Approved by: Julian Edwards
Approved revision: no longer in the source branch.
Merged at revision: 2390
Proposed branch: lp:~julian-edwards/maas/models-for-static-dhcp
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 527 lines (+454/-2)
7 files modified
src/maasserver/enum.py (+16/-0)
src/maasserver/migrations/0081_ipaddress_table_and_static_dhcp_ranges.py (+326/-0)
src/maasserver/models/__init__.py (+5/-2)
src/maasserver/models/ipaddress.py (+53/-0)
src/maasserver/models/macaddress.py (+6/-0)
src/maasserver/models/macipaddresslink.py (+44/-0)
src/maasserver/models/nodegroupinterface.py (+4/-0)
To merge this branch: bzr merge lp:~julian-edwards/maas/models-for-static-dhcp
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) Approve
Review via email: mp+221155@code.launchpad.net

Commit message

New model "IPAddress" and static IP range fields on nodegroupinterface. This is to prepare the way for a static host dhcpd declaration that does not intersect with the dynamic range.

Description of the change

See https://docs.google.com/a/canonical.com/document/d/1JHbXCgkA0b42uu2t_uKLBnlxCzpaNmaQOPvPpGrDHDA/edit#

Essentially, this is just introducing a new table to store static ip addresses, but is done in such a way to support the future changes described in that doc that we also need.

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

I want to make more changes to this so I've marked it WIP.

Revision history for this message
Julian Edwards (julian-edwards) wrote :

I've made the IPAddress.mac_address nullable in the case where the IP is not for a Node.

Additionally, we should note that there is no sensible migration you can do to create a static range for DHCP, it requires admin intervention. The simplest thing to do is if there is no static range defined, then continue as before with only dynamic leases. But this will come later in some actual code!

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Please document IPAddress. It's really a static IP address, not just any dynamic-or-static IP address, right?

Why the foreign key from IPAddress to MACAddress in the first place? Wasn't that going to be a many-to-many relationship? Or is this transitional?

I'm not holding this up with my vote — whatever comes out, no harm done at this stage. Better to land and improve than to fork and languish. But to me, the "type" field affects the behaviour of the foreign key in too profound a way.

The biggest difference I see between the four types of relationship is what happens when a node is deallocated. If an IPAddress is "auto" or "extra," it goes down with the ship. If it's "unmanaged" or "sticky," it survives. I think those are different structural relationships, so maybe they should be two alternative foreign keys, with different cascading behaviours.

Of course my "biggest difference" only gets us to 2 types, out of 4. What are the other differences? It seems to me that:

 * "Auto" is like "extra" except it can't be deallocated separately. Could be a separate relationship, or just a boolean attribute.

 * "Unmanaged" is like "sticky" except the machine on the other end is not a node. But does that matter beyond creation and display? If you set an IP address for a machine's MAC address, then registered that machine as a node, ISTM the IP address would implicitly go from "unmanaged" to "sticky" without affecting the world in any way.

There may also be cardinality differences. I can't imagine attaching two MACs to the same "auto" address, but it might make sense for "extra" addresses. Splitting out the relationships may make the rules clearer, and easier to enforce as constraints.

review: Approve
Revision history for this message
Julian Edwards (julian-edwards) wrote :

Thanks for reviewing jtv.

On 01/06/14 23:14, Jeroen T. Vermeulen wrote:
> Review: Approve
>
> Please document IPAddress. It's really a static IP address, not just any dynamic-or-static IP address, right?

Woops, I had meant to do that and forgot. I've expanded the docstring
now, thanks for spotting!

> Why the foreign key from IPAddress to MACAddress in the first place? Wasn't that going to be a many-to-many relationship? Or is this transitional?

See the maas-devel thread for gory details, but the upshot is that we
potentially need to have many IPs per MAC (VLANs, NIC aliases etc), but
we don't need to model many MACs per IP (VIPs) as we'll do that by
having a per-user IP.

> I'm not holding this up with my vote — whatever comes out, no harm done at this stage. Better to land and improve than to fork and languish. But to me, the "type" field affects the behaviour of the foreign key in too profound a way.
>
> The biggest difference I see between the four types of relationship is what happens when a node is deallocated. If an IPAddress is "auto" or "extra," it goes down with the ship. If it's "unmanaged" or "sticky," it survives. I think those are different structural relationships, so maybe they should be two alternative foreign keys, with different cascading behaviours.

Again please see the maas-devel thread for politics behind this :(

> Of course my "biggest difference" only gets us to 2 types, out of 4. What are the other differences? It seems to me that:
>
> * "Auto" is like "extra" except it can't be deallocated separately. Could be a separate relationship, or just a boolean attribute.

Whenever you have a boolean attribute, an enum is nearly always what you
want instead, and better. Bools as column types are for mugs IMO, they
restrict what you can do, and I've seen many times where the LP schema
had migrations to move bools to enums as the original design had not
been thought through properly.

> * "Unmanaged" is like "sticky" except the machine on the other end is not a node. But does that matter beyond creation and display? If you set an IP address for a machine's MAC address, then registered that machine as a node, ISTM the IP address would implicitly go from "unmanaged" to "sticky" without affecting the world in any way.

You might have a point here. Let's leave it for now since it doesn't
affect the model itself and is a small implementation detail. We can
always change it later very easily and we'll encapsulate all this detail
inside the manager class anyway.

> There may also be cardinality differences. I can't imagine attaching two MACs to the same "auto" address, but it might make sense for "extra" addresses. Splitting out the relationships may make the rules clearer, and easier to enforce as constraints.

There is no way to attach more than one MAC to an IP with this model.

Revision history for this message
Julian Edwards (julian-edwards) wrote :

I've now changed this a bit to use a link table between MACAddress and
IPAddress and put MACAddress.ip_addresses as a ManyToMany field. It
will still be one-to-many rather than many-to-many because I made
IPAddress on the link table unique.

This brings some benefits:

 A) no more nullable FK on IPAddress
 B) we can put metadata on the link table (which I have done, I've added
nic_alias. See the discussion on maas-devel)
 C) we get a more natural MACAddress.ip_addresses field. Referring to
IPAddress.macaddress_set will always only give one result, though.

I'm going to land this because I don't want to be blocked and it's only
slightly different really. We can revisit if necessary of course.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/enum.py'
2--- src/maasserver/enum.py 2014-05-01 20:18:14 +0000
3+++ src/maasserver/enum.py 2014-06-02 05:21:22 +0000
4@@ -14,6 +14,7 @@
5 __metaclass__ = type
6 __all__ = [
7 'COMPONENT',
8+ 'IPADDRESS_TYPE',
9 'NODEGROUP_STATUS',
10 'NODEGROUP_STATUS_CHOICES',
11 'NODEGROUPINTERFACE_MANAGEMENT',
12@@ -150,3 +151,18 @@
13
14 NODEGROUPINTERFACE_MANAGEMENT_CHOICES_DICT = (
15 OrderedDict(NODEGROUPINTERFACE_MANAGEMENT_CHOICES))
16+
17+
18+class IPADDRESS_TYPE:
19+ """The vocabulary of possible types of `IPAddress`."""
20+ # Automatically assigned.
21+ AUTO = 0
22+
23+ # Pre-assigned and permanent until removed.
24+ STICKY = 1
25+
26+ # Not associated to hardware managed by MAAS.
27+ UNMANAGED = 2
28+
29+ # Additional IP requested by a user for a node.
30+ EXTRA = 3
31
32=== added file 'src/maasserver/migrations/0081_ipaddress_table_and_static_dhcp_ranges.py'
33--- src/maasserver/migrations/0081_ipaddress_table_and_static_dhcp_ranges.py 1970-01-01 00:00:00 +0000
34+++ src/maasserver/migrations/0081_ipaddress_table_and_static_dhcp_ranges.py 2014-06-02 05:21:22 +0000
35@@ -0,0 +1,326 @@
36+from django.db import models
37+from south.db import db
38+# -*- coding: utf-8 -*-
39+from south.utils import datetime_utils as datetime
40+from south.v2 import SchemaMigration
41+
42+
43+class Migration(SchemaMigration):
44+
45+ def forwards(self, orm):
46+ # Adding model 'IPAddress'
47+ db.create_table(u'maasserver_ipaddress', (
48+ (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
49+ ('created', self.gf('django.db.models.fields.DateTimeField')()),
50+ ('updated', self.gf('django.db.models.fields.DateTimeField')()),
51+ ('ip', self.gf('django.db.models.fields.GenericIPAddressField')(unique=True, max_length=39)),
52+ ('type', self.gf('django.db.models.fields.IntegerField')(default=0)),
53+ ))
54+ db.send_create_signal(u'maasserver', ['IPAddress'])
55+
56+ # Adding model 'MACIPAddressLink'
57+ db.create_table(u'maasserver_macipaddresslink', (
58+ (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
59+ ('created', self.gf('django.db.models.fields.DateTimeField')()),
60+ ('updated', self.gf('django.db.models.fields.DateTimeField')()),
61+ ('mac_address', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['maasserver.MACAddress'])),
62+ ('ip_address', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['maasserver.IPAddress'], unique=True)),
63+ ('nic_alias', self.gf('django.db.models.fields.IntegerField')(default=None, null=True, blank=True)),
64+ ))
65+ db.send_create_signal(u'maasserver', ['MACIPAddressLink'])
66+
67+ # Adding unique constraint on 'MACIPAddressLink', fields ['ip_address', 'mac_address']
68+ db.create_unique(u'maasserver_macipaddresslink', ['ip_address_id', 'mac_address_id'])
69+
70+ # Adding field 'NodeGroupInterface.static_ip_range_low'
71+ db.add_column(u'maasserver_nodegroupinterface', 'static_ip_range_low',
72+ self.gf('django.db.models.fields.GenericIPAddressField')(default=None, max_length=39, null=True, blank=True),
73+ keep_default=False)
74+
75+ # Adding field 'NodeGroupInterface.static_ip_range_high'
76+ db.add_column(u'maasserver_nodegroupinterface', 'static_ip_range_high',
77+ self.gf('django.db.models.fields.GenericIPAddressField')(default=None, max_length=39, null=True, blank=True),
78+ keep_default=False)
79+
80+
81+ def backwards(self, orm):
82+ # Removing unique constraint on 'MACIPAddressLink', fields ['ip_address', 'mac_address']
83+ db.delete_unique(u'maasserver_macipaddresslink', ['ip_address_id', 'mac_address_id'])
84+
85+ # Deleting model 'IPAddress'
86+ db.delete_table(u'maasserver_ipaddress')
87+
88+ # Deleting model 'MACIPAddressLink'
89+ db.delete_table(u'maasserver_macipaddresslink')
90+
91+ # Deleting field 'NodeGroupInterface.static_ip_range_low'
92+ db.delete_column(u'maasserver_nodegroupinterface', 'static_ip_range_low')
93+
94+ # Deleting field 'NodeGroupInterface.static_ip_range_high'
95+ db.delete_column(u'maasserver_nodegroupinterface', 'static_ip_range_high')
96+
97+
98+ models = {
99+ u'auth.group': {
100+ 'Meta': {'object_name': 'Group'},
101+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
102+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
103+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
104+ },
105+ u'auth.permission': {
106+ 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
107+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
108+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
109+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
110+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
111+ },
112+ u'auth.user': {
113+ 'Meta': {'object_name': 'User'},
114+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
115+ 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75', 'blank': 'True'}),
116+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
117+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
118+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
119+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
120+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
121+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
122+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
123+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
124+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
125+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
126+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
127+ },
128+ u'contenttypes.contenttype': {
129+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
130+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
131+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
132+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
133+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
134+ },
135+ u'maasserver.bootimage': {
136+ 'Meta': {'unique_together': "((u'nodegroup', u'osystem', u'architecture', u'subarchitecture', u'release', u'purpose', u'label'),)", 'object_name': 'BootImage'},
137+ 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
138+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
139+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
140+ 'label': ('django.db.models.fields.CharField', [], {'default': "u'release'", 'max_length': '255'}),
141+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
142+ 'osystem': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
143+ 'purpose': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
144+ 'release': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
145+ 'subarchitecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
146+ 'supported_subarches': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
147+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
148+ },
149+ u'maasserver.bootsource': {
150+ 'Meta': {'object_name': 'BootSource'},
151+ 'cluster': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']", 'null': 'True'}),
152+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
153+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
154+ 'keyring_data': ('maasserver.fields.EditableBinaryField', [], {'blank': 'True'}),
155+ 'keyring_filename': ('django.db.models.fields.FilePathField', [], {'max_length': '100', 'blank': 'True'}),
156+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
157+ 'url': ('django.db.models.fields.URLField', [], {'max_length': '200'})
158+ },
159+ u'maasserver.bootsourceselection': {
160+ 'Meta': {'object_name': 'BootSourceSelection'},
161+ 'arches': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'text'", 'null': 'True', 'blank': 'True'}),
162+ 'boot_source': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.BootSource']"}),
163+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
164+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
165+ 'labels': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'text'", 'null': 'True', 'blank': 'True'}),
166+ 'release': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'blank': 'True'}),
167+ 'subarches': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'text'", 'null': 'True', 'blank': 'True'}),
168+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
169+ },
170+ u'maasserver.componenterror': {
171+ 'Meta': {'object_name': 'ComponentError'},
172+ 'component': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
173+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
174+ 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000'}),
175+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
176+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
177+ },
178+ u'maasserver.config': {
179+ 'Meta': {'object_name': 'Config'},
180+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
181+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
182+ 'value': ('maasserver.fields.JSONObjectField', [], {'null': 'True'})
183+ },
184+ u'maasserver.dhcplease': {
185+ 'Meta': {'object_name': 'DHCPLease'},
186+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
187+ 'ip': ('django.db.models.fields.IPAddressField', [], {'unique': 'True', 'max_length': '15'}),
188+ 'mac': ('maasserver.fields.MACAddressField', [], {}),
189+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"})
190+ },
191+ u'maasserver.downloadprogress': {
192+ 'Meta': {'object_name': 'DownloadProgress'},
193+ 'bytes_downloaded': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
194+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
195+ 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000', 'blank': 'True'}),
196+ 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
197+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
198+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
199+ 'size': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
200+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
201+ },
202+ u'maasserver.filestorage': {
203+ 'Meta': {'unique_together': "((u'filename', u'owner'),)", 'object_name': 'FileStorage'},
204+ 'content': ('metadataserver.fields.BinaryField', [], {'blank': 'True'}),
205+ 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
206+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
207+ 'key': ('django.db.models.fields.CharField', [], {'default': "u'23d4ee34-ea01-11e3-8dfe-002215205ce8'", 'unique': 'True', 'max_length': '36'}),
208+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
209+ },
210+ u'maasserver.ipaddress': {
211+ 'Meta': {'object_name': 'IPAddress'},
212+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
213+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
214+ 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'unique': 'True', 'max_length': '39'}),
215+ 'type': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
216+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
217+ },
218+ u'maasserver.macaddress': {
219+ 'Meta': {'object_name': 'MACAddress'},
220+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
221+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
222+ 'ip_addresses': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.IPAddress']", 'through': u"orm['maasserver.MACIPAddressLink']", 'symmetrical': 'False'}),
223+ 'mac_address': ('maasserver.fields.MACAddressField', [], {'unique': 'True'}),
224+ 'networks': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Network']", 'symmetrical': 'False', 'blank': 'True'}),
225+ 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}),
226+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
227+ },
228+ u'maasserver.macipaddresslink': {
229+ 'Meta': {'unique_together': "((u'ip_address', u'mac_address'),)", 'object_name': 'MACIPAddressLink'},
230+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
231+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
232+ 'ip_address': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.IPAddress']", 'unique': 'True'}),
233+ 'mac_address': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.MACAddress']"}),
234+ 'nic_alias': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
235+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
236+ },
237+ u'maasserver.network': {
238+ 'Meta': {'object_name': 'Network'},
239+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
240+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
241+ 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'unique': 'True', 'max_length': '39'}),
242+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
243+ 'netmask': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
244+ 'vlan_tag': ('django.db.models.fields.PositiveSmallIntegerField', [], {'unique': 'True', 'null': 'True', 'blank': 'True'})
245+ },
246+ u'maasserver.node': {
247+ 'Meta': {'object_name': 'Node'},
248+ 'agent_name': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'null': 'True', 'blank': 'True'}),
249+ 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '31'}),
250+ 'cpu_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
251+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
252+ 'distro_series': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'blank': 'True'}),
253+ 'error': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
254+ 'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'unique': 'True', 'max_length': '255', 'blank': 'True'}),
255+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
256+ 'memory': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
257+ 'netboot': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
258+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']", 'null': 'True'}),
259+ 'osystem': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'blank': 'True'}),
260+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}),
261+ 'power_parameters': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
262+ 'power_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '10', 'blank': 'True'}),
263+ 'routers': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'macaddr'", 'null': 'True', 'blank': 'True'}),
264+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '10'}),
265+ 'storage': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
266+ 'system_id': ('django.db.models.fields.CharField', [], {'default': "u'node-23d247e2-ea01-11e3-8dfe-002215205ce8'", 'unique': 'True', 'max_length': '41'}),
267+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Tag']", 'symmetrical': 'False'}),
268+ 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Token']", 'null': 'True'}),
269+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
270+ 'zone': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Zone']", 'on_delete': 'models.SET_DEFAULT'})
271+ },
272+ u'maasserver.nodegroup': {
273+ 'Meta': {'object_name': 'NodeGroup'},
274+ 'api_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '18'}),
275+ 'api_token': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Token']", 'unique': 'True'}),
276+ 'cluster_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100', 'blank': 'True'}),
277+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
278+ 'dhcp_key': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
279+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
280+ 'maas_url': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
281+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}),
282+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
283+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
284+ 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'})
285+ },
286+ u'maasserver.nodegroupinterface': {
287+ 'Meta': {'unique_together': "((u'nodegroup', u'interface'),)", 'object_name': 'NodeGroupInterface'},
288+ 'broadcast_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
289+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
290+ 'foreign_dhcp_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
291+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
292+ 'interface': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
293+ 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
294+ 'ip_range_high': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
295+ 'ip_range_low': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
296+ 'management': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
297+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
298+ 'router_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
299+ 'static_ip_range_high': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
300+ 'static_ip_range_low': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
301+ 'subnet_mask': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
302+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
303+ },
304+ u'maasserver.sshkey': {
305+ 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSHKey'},
306+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
307+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
308+ 'key': ('django.db.models.fields.TextField', [], {}),
309+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
310+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
311+ },
312+ u'maasserver.tag': {
313+ 'Meta': {'object_name': 'Tag'},
314+ 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
315+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
316+ 'definition': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
317+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
318+ 'kernel_opts': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
319+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
320+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
321+ },
322+ u'maasserver.userprofile': {
323+ 'Meta': {'object_name': 'UserProfile'},
324+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
325+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'})
326+ },
327+ u'maasserver.zone': {
328+ 'Meta': {'ordering': "[u'name']", 'object_name': 'Zone'},
329+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
330+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
331+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
332+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
333+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
334+ },
335+ u'piston.consumer': {
336+ 'Meta': {'object_name': 'Consumer'},
337+ 'description': ('django.db.models.fields.TextField', [], {}),
338+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
339+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
340+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
341+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
342+ 'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '16'}),
343+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'consumers'", 'null': 'True', 'to': u"orm['auth.User']"})
344+ },
345+ u'piston.token': {
346+ 'Meta': {'object_name': 'Token'},
347+ 'callback': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
348+ 'callback_confirmed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
349+ 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Consumer']"}),
350+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
351+ 'is_approved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
352+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
353+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
354+ 'timestamp': ('django.db.models.fields.IntegerField', [], {'default': '1401677636L'}),
355+ 'token_type': ('django.db.models.fields.IntegerField', [], {}),
356+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'tokens'", 'null': 'True', 'to': u"orm['auth.User']"}),
357+ 'verifier': ('django.db.models.fields.CharField', [], {'max_length': '10'})
358+ }
359+ }
360+
361+ complete_apps = ['maasserver']
362\ No newline at end of file
363
364=== modified file 'src/maasserver/models/__init__.py'
365--- src/maasserver/models/__init__.py 2014-05-16 09:00:55 +0000
366+++ src/maasserver/models/__init__.py 2014-06-02 05:21:22 +0000
367@@ -51,7 +51,9 @@
368 from maasserver.models.dhcplease import DHCPLease
369 from maasserver.models.downloadprogress import DownloadProgress
370 from maasserver.models.filestorage import FileStorage
371+from maasserver.models.ipaddress import IPAddress
372 from maasserver.models.macaddress import MACAddress
373+from maasserver.models.macipaddresslink import MACIPAddressLink
374 from maasserver.models.network import Network
375 from maasserver.models.node import Node
376 from maasserver.models.nodegroup import NodeGroup
377@@ -68,8 +70,9 @@
378 # export in __all__.
379 ignore_unused(
380 BootImage, ComponentError, Config, DHCPLease, DownloadProgress,
381- FileStorage, MACAddress, Network, NodeGroup, SSHKey, Tag, UserProfile,
382- NodeGroupInterface, Zone, logger)
383+ FileStorage, IPAddress, MACAddress, MACIPAddressLink, Network,
384+ NodeGroup, SSHKey, Tag, UserProfile, NodeGroupInterface, Zone,
385+ logger)
386
387
388 # Connect the 'create_user' method to the post save signal of User.
389
390=== added file 'src/maasserver/models/ipaddress.py'
391--- src/maasserver/models/ipaddress.py 1970-01-01 00:00:00 +0000
392+++ src/maasserver/models/ipaddress.py 2014-06-02 05:21:22 +0000
393@@ -0,0 +1,53 @@
394+# Copyright 2014 Canonical Ltd. This software is licensed under the
395+# GNU Affero General Public License version 3 (see the file LICENSE).
396+
397+"""Model definition for IPAddress.
398+
399+Contains all the in-use static IP addresses that are allocated by MAAS.
400+Generally speaking, these are written out to the DHCP server as "host"
401+blocks which will tie MACs into a specific IP. The IPs are separate
402+from the dynamic range that the DHCP server itself allocates to unknown
403+clients.
404+"""
405+
406+from __future__ import (
407+ absolute_import,
408+ print_function,
409+ unicode_literals,
410+ )
411+
412+str = None
413+
414+__metaclass__ = type
415+__all__ = [
416+ 'IPAddress',
417+ ]
418+
419+
420+from django.db.models import (
421+ GenericIPAddressField,
422+ IntegerField,
423+ )
424+from maasserver import DefaultMeta
425+from maasserver.enum import IPADDRESS_TYPE
426+from maasserver.models.cleansave import CleanSave
427+from maasserver.models.timestampedmodel import TimestampedModel
428+
429+
430+class IPAddress(CleanSave, TimestampedModel):
431+
432+ class Meta(DefaultMeta):
433+ verbose_name = "IP Address"
434+ verbose_name_plural = "IP Addresses"
435+
436+ ip = GenericIPAddressField(
437+ unique=True, null=False, editable=False, blank=False)
438+
439+ # The MACIPAddressLink table is used to link IPAddress to
440+ # MACAddress. See MACAddress.ip_addresses.
441+
442+ type = IntegerField(
443+ editable=False, null=False, blank=False, default=IPADDRESS_TYPE.AUTO)
444+
445+ def __unicode__(self):
446+ return "<IPAddress %s>" % self.ip
447
448=== modified file 'src/maasserver/models/macaddress.py'
449--- src/maasserver/models/macaddress.py 2014-02-27 23:37:18 +0000
450+++ src/maasserver/models/macaddress.py 2014-06-02 05:21:22 +0000
451@@ -50,6 +50,12 @@
452
453 networks = ManyToManyField('maasserver.Network', blank=True)
454
455+ ip_addresses = ManyToManyField(
456+ 'maasserver.IPAddress', through='maasserver.MACIPAddressLink',
457+ blank=True)
458+
459+ # future columns: tags, nic_name, metadata, bonding info
460+
461 objects = BulkManager()
462
463 class Meta(DefaultMeta):
464
465=== added file 'src/maasserver/models/macipaddresslink.py'
466--- src/maasserver/models/macipaddresslink.py 1970-01-01 00:00:00 +0000
467+++ src/maasserver/models/macipaddresslink.py 2014-06-02 05:21:22 +0000
468@@ -0,0 +1,44 @@
469+# Copyright 2014 Canonical Ltd. This software is licensed under the
470+# GNU Affero General Public License version 3 (see the file LICENSE).
471+
472+"""Model definition for MACIPAddressLink.
473+
474+Maintains a relationship between MACAddress and IPAddress. This is defined
475+instead of using Django's auto-generated link table because it also contains
476+additional metadata about the link, such as a NIC alias.
477+"""
478+
479+from __future__ import (
480+ absolute_import,
481+ print_function,
482+ unicode_literals,
483+ )
484+
485+str = None
486+
487+__metaclass__ = type
488+__all__ = [
489+ 'MACIPAddressLink',
490+ ]
491+
492+
493+from django.db.models import (
494+ ForeignKey,
495+ IntegerField,
496+ )
497+from maasserver import DefaultMeta
498+from maasserver.models.cleansave import CleanSave
499+from maasserver.models.timestampedmodel import TimestampedModel
500+
501+
502+class MACIPAddressLink(CleanSave, TimestampedModel):
503+
504+ class Meta(DefaultMeta):
505+ unique_together = ('ip_address', 'mac_address')
506+
507+ mac_address = ForeignKey('maasserver.MACAddress')
508+ ip_address = ForeignKey('maasserver.IPAddress', unique=True)
509+
510+ # Optional NIC alias for multi-homed NICs (e.g. 'eth0:1')
511+ nic_alias = IntegerField(
512+ editable=True, null=True, blank=True, default=None)
513
514=== modified file 'src/maasserver/models/nodegroupinterface.py'
515--- src/maasserver/models/nodegroupinterface.py 2014-03-31 08:19:12 +0000
516+++ src/maasserver/models/nodegroupinterface.py 2014-06-02 05:21:22 +0000
517@@ -78,6 +78,10 @@
518 editable=True, unique=False, blank=True, null=True, default=None)
519 ip_range_high = GenericIPAddressField(
520 editable=True, unique=False, blank=True, null=True, default=None)
521+ static_ip_range_low = GenericIPAddressField(
522+ editable=True, unique=False, blank=True, null=True, default=None)
523+ static_ip_range_high = GenericIPAddressField(
524+ editable=True, unique=False, blank=True, null=True, default=None)
525
526 # Foreign DHCP server address, if any, that was detected on this
527 # interface.