Merge lp:~rvb/maas/add-vlan-fabric into lp:maas/trunk

Proposed by Raphaël Badin on 2015-06-11
Status: Merged
Approved by: Raphaël Badin on 2015-06-15
Approved revision: 3975
Merged at revision: 4015
Proposed branch: lp:~rvb/maas/add-vlan-fabric
Merge into: lp:maas/trunk
Diff against target: 965 lines (+879/-2)
7 files modified
src/maasserver/migrations/0139_add_vlan_fabric.py (+494/-0)
src/maasserver/models/__init__.py (+5/-2)
src/maasserver/models/fabric.py (+113/-0)
src/maasserver/models/tests/test_fabric.py (+85/-0)
src/maasserver/models/tests/test_vlan.py (+79/-0)
src/maasserver/models/vlan.py (+81/-0)
src/maasserver/testing/factory.py (+22/-0)
To merge this branch: bzr merge lp:~rvb/maas/add-vlan-fabric
Reviewer Review Type Date Requested Status
Gavin Panella (community) 2015-06-11 Approve on 2015-06-11
Review via email: mp+261693@code.launchpad.net

Commit message

Add VLAN and Fabric models.

Description of the change

These models are only connect to each other right now. Connections to the new Interface model and the ClusterInterface model will be added in separate branches.

To post a comment you must log in.
Gavin Panella (allenap) wrote :

Lots of questions!

review: Needs Information
Mike Pontillo (mpontillo) wrote :

A few comments.

Mike Pontillo (mpontillo) wrote :

Minor clarification.

Raphaël Badin (rvb) :
lp:~rvb/maas/add-vlan-fabric updated on 2015-06-11
3975. By Raphaël Badin on 2015-06-11

Review fixes.

Gavin Panella (allenap) wrote :

Thanks for all your replies. Looks good, +1.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'src/maasserver/migrations/0139_add_vlan_fabric.py'
2--- src/maasserver/migrations/0139_add_vlan_fabric.py 1970-01-01 00:00:00 +0000
3+++ src/maasserver/migrations/0139_add_vlan_fabric.py 2015-06-11 14:44:57 +0000
4@@ -0,0 +1,494 @@
5+from django.db import models
6+from south.db import db
7+# -*- coding: utf-8 -*-
8+from south.utils import datetime_utils as datetime
9+from south.v2 import SchemaMigration
10+
11+
12+class Migration(SchemaMigration):
13+
14+ def forwards(self, orm):
15+ # Adding model 'VLAN'
16+ db.create_table(u'maasserver_vlan', (
17+ (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
18+ ('created', self.gf('django.db.models.fields.DateTimeField')()),
19+ ('updated', self.gf('django.db.models.fields.DateTimeField')()),
20+ ('name', self.gf('django.db.models.fields.CharField')(max_length=256)),
21+ ('vid', self.gf('django.db.models.fields.IntegerField')()),
22+ ('fabric', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['maasserver.Fabric'])),
23+ ))
24+ db.send_create_signal(u'maasserver', ['VLAN'])
25+
26+ # Adding unique constraint on 'VLAN', fields ['vid', 'fabric']
27+ db.create_unique(u'maasserver_vlan', ['vid', 'fabric_id'])
28+
29+ # Adding unique constraint on 'VLAN', fields ['name', 'fabric']
30+ db.create_unique(u'maasserver_vlan', ['name', 'fabric_id'])
31+
32+ # Adding model 'Fabric'
33+ db.create_table(u'maasserver_fabric', (
34+ (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
35+ ('created', self.gf('django.db.models.fields.DateTimeField')()),
36+ ('updated', self.gf('django.db.models.fields.DateTimeField')()),
37+ ('name', self.gf('django.db.models.fields.CharField')(unique=True, max_length=256)),
38+ ('default_vlan', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name=u'+', null=True, to=orm['maasserver.VLAN'])),
39+ ))
40+ db.send_create_signal(u'maasserver', ['Fabric'])
41+
42+
43+ def backwards(self, orm):
44+ # Removing unique constraint on 'VLAN', fields ['name', 'fabric']
45+ db.delete_unique(u'maasserver_vlan', ['name', 'fabric_id'])
46+
47+ # Removing unique constraint on 'VLAN', fields ['vid', 'fabric']
48+ db.delete_unique(u'maasserver_vlan', ['vid', 'fabric_id'])
49+
50+ # Deleting model 'VLAN'
51+ db.delete_table(u'maasserver_vlan')
52+
53+ # Deleting model 'Fabric'
54+ db.delete_table(u'maasserver_fabric')
55+
56+
57+ models = {
58+ u'auth.group': {
59+ 'Meta': {'object_name': 'Group'},
60+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
61+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
62+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
63+ },
64+ u'auth.permission': {
65+ 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
66+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
67+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
68+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
69+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
70+ },
71+ u'auth.user': {
72+ 'Meta': {'object_name': 'User'},
73+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
74+ 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75', 'blank': 'True'}),
75+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
76+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
77+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
78+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
79+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
80+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
81+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
82+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
83+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
84+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
85+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
86+ },
87+ u'contenttypes.contenttype': {
88+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
89+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
90+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
91+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
92+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
93+ },
94+ u'maasserver.blockdevice': {
95+ 'Meta': {'ordering': "[u'id']", 'unique_together': "((u'node', u'path'),)", 'object_name': 'BlockDevice'},
96+ 'block_size': ('django.db.models.fields.IntegerField', [], {}),
97+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
98+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
99+ 'id_path': ('django.db.models.fields.FilePathField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
100+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
101+ 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}),
102+ 'path': ('django.db.models.fields.FilePathField', [], {'max_length': '100'}),
103+ 'size': ('django.db.models.fields.BigIntegerField', [], {}),
104+ 'tags': ('djorm_pgarray.fields.ArrayField', [], {'default': '[]', 'dbtype': "u'text'", 'blank': 'True'}),
105+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
106+ },
107+ u'maasserver.bootresource': {
108+ 'Meta': {'unique_together': "((u'name', u'architecture'),)", 'object_name': 'BootResource'},
109+ 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
110+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
111+ 'extra': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
112+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
113+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
114+ 'rtype': ('django.db.models.fields.IntegerField', [], {'max_length': '10'}),
115+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
116+ },
117+ u'maasserver.bootresourcefile': {
118+ 'Meta': {'unique_together': "((u'resource_set', u'filetype'),)", 'object_name': 'BootResourceFile'},
119+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
120+ 'extra': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
121+ 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
122+ 'filetype': ('django.db.models.fields.CharField', [], {'default': "u'root-tgz'", 'max_length': '20'}),
123+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
124+ 'largefile': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.LargeFile']"}),
125+ 'resource_set': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'files'", 'to': u"orm['maasserver.BootResourceSet']"}),
126+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
127+ },
128+ u'maasserver.bootresourceset': {
129+ 'Meta': {'unique_together': "((u'resource', u'version'),)", 'object_name': 'BootResourceSet'},
130+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
131+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
132+ 'label': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
133+ 'resource': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'sets'", 'to': u"orm['maasserver.BootResource']"}),
134+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
135+ 'version': ('django.db.models.fields.CharField', [], {'max_length': '255'})
136+ },
137+ u'maasserver.bootsource': {
138+ 'Meta': {'object_name': 'BootSource'},
139+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
140+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
141+ 'keyring_data': ('maasserver.fields.EditableBinaryField', [], {'blank': 'True'}),
142+ 'keyring_filename': ('django.db.models.fields.FilePathField', [], {'max_length': '100', 'blank': 'True'}),
143+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
144+ 'url': ('django.db.models.fields.URLField', [], {'unique': 'True', 'max_length': '200'})
145+ },
146+ u'maasserver.bootsourcecache': {
147+ 'Meta': {'object_name': 'BootSourceCache'},
148+ 'arch': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
149+ 'boot_source': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.BootSource']"}),
150+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
151+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
152+ 'label': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
153+ 'os': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
154+ 'release': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
155+ 'subarch': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
156+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
157+ },
158+ u'maasserver.bootsourceselection': {
159+ 'Meta': {'unique_together': "((u'boot_source', u'os', u'release'),)", 'object_name': 'BootSourceSelection'},
160+ 'arches': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'text'", 'null': 'True', 'blank': 'True'}),
161+ 'boot_source': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.BootSource']"}),
162+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
163+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
164+ 'labels': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'text'", 'null': 'True', 'blank': 'True'}),
165+ 'os': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', '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.candidatename': {
171+ 'Meta': {'unique_together': "((u'name', u'position'),)", 'object_name': 'CandidateName'},
172+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
173+ 'name': ('django.db.models.fields.SlugField', [], {'max_length': '50'}),
174+ 'position': ('django.db.models.fields.IntegerField', [], {})
175+ },
176+ u'maasserver.componenterror': {
177+ 'Meta': {'object_name': 'ComponentError'},
178+ 'component': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
179+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
180+ 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000'}),
181+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
182+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
183+ },
184+ u'maasserver.config': {
185+ 'Meta': {'object_name': 'Config'},
186+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
187+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
188+ 'value': ('maasserver.fields.JSONObjectField', [], {'null': 'True'})
189+ },
190+ u'maasserver.dhcplease': {
191+ 'Meta': {'object_name': 'DHCPLease'},
192+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
193+ 'ip': ('maasserver.fields.MAASIPAddressField', [], {'unique': 'True', 'max_length': '39'}),
194+ 'mac': ('maasserver.fields.MACAddressField', [], {}),
195+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"})
196+ },
197+ u'maasserver.downloadprogress': {
198+ 'Meta': {'object_name': 'DownloadProgress'},
199+ 'bytes_downloaded': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
200+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
201+ 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000', 'blank': 'True'}),
202+ 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
203+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
204+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
205+ 'size': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
206+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
207+ },
208+ u'maasserver.event': {
209+ 'Meta': {'object_name': 'Event'},
210+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
211+ 'description': ('django.db.models.fields.TextField', [], {'default': "u''", 'blank': 'True'}),
212+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
213+ 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}),
214+ 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.EventType']"}),
215+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
216+ },
217+ u'maasserver.eventtype': {
218+ 'Meta': {'object_name': 'EventType'},
219+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
220+ 'description': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
221+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
222+ 'level': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
223+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
224+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
225+ },
226+ u'maasserver.fabric': {
227+ 'Meta': {'object_name': 'Fabric'},
228+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
229+ 'default_vlan': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "u'+'", 'null': 'True', 'to': u"orm['maasserver.VLAN']"}),
230+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
231+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
232+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
233+ },
234+ u'maasserver.filestorage': {
235+ 'Meta': {'unique_together': "((u'filename', u'owner'),)", 'object_name': 'FileStorage'},
236+ 'content': ('metadataserver.fields.BinaryField', [], {'blank': 'True'}),
237+ 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
238+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
239+ 'key': ('django.db.models.fields.CharField', [], {'default': "u'4bab59ce-1017-11e5-9b34-3c970e0e56dc'", 'unique': 'True', 'max_length': '36'}),
240+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
241+ },
242+ u'maasserver.filesystem': {
243+ 'Meta': {'object_name': 'Filesystem'},
244+ 'block_device': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.BlockDevice']", 'null': 'True', 'blank': 'True'}),
245+ 'create_params': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
246+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
247+ 'filesystem_group': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "u'filesystems'", 'null': 'True', 'to': u"orm['maasserver.FilesystemGroup']"}),
248+ 'fstype': ('django.db.models.fields.CharField', [], {'default': "u'ext4'", 'max_length': '20'}),
249+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
250+ 'mount_params': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
251+ 'mount_point': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
252+ 'partition': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Partition']", 'null': 'True', 'blank': 'True'}),
253+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
254+ 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'})
255+ },
256+ u'maasserver.filesystemgroup': {
257+ 'Meta': {'object_name': 'FilesystemGroup'},
258+ 'create_params': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
259+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
260+ 'group_type': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
261+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
262+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
263+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
264+ 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'})
265+ },
266+ u'maasserver.largefile': {
267+ 'Meta': {'object_name': 'LargeFile'},
268+ 'content': ('maasserver.fields.LargeObjectField', [], {}),
269+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
270+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
271+ 'sha256': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'}),
272+ 'total_size': ('django.db.models.fields.BigIntegerField', [], {}),
273+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
274+ },
275+ u'maasserver.licensekey': {
276+ 'Meta': {'unique_together': "((u'osystem', u'distro_series'),)", 'object_name': 'LicenseKey'},
277+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
278+ 'distro_series': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
279+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
280+ 'license_key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
281+ 'osystem': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
282+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
283+ },
284+ u'maasserver.macaddress': {
285+ 'Meta': {'ordering': "(u'created',)", 'object_name': 'MACAddress'},
286+ 'cluster_interface': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['maasserver.NodeGroupInterface']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
287+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
288+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
289+ 'ip_addresses': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.StaticIPAddress']", 'symmetrical': 'False', 'through': u"orm['maasserver.MACStaticIPAddressLink']", 'blank': 'True'}),
290+ 'mac_address': ('maasserver.fields.MACAddressField', [], {'unique': 'True'}),
291+ 'networks': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Network']", 'symmetrical': 'False', 'blank': 'True'}),
292+ 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']", 'null': 'True', 'blank': 'True'}),
293+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
294+ },
295+ u'maasserver.macstaticipaddresslink': {
296+ 'Meta': {'unique_together': "((u'ip_address', u'mac_address'),)", 'object_name': 'MACStaticIPAddressLink'},
297+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
298+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
299+ 'ip_address': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.StaticIPAddress']", 'unique': 'True'}),
300+ 'mac_address': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.MACAddress']"}),
301+ 'nic_alias': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
302+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
303+ },
304+ u'maasserver.network': {
305+ 'Meta': {'object_name': 'Network'},
306+ 'default_gateway': ('maasserver.fields.MAASIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
307+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
308+ 'dns_servers': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
309+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
310+ 'ip': ('maasserver.fields.MAASIPAddressField', [], {'unique': 'True', 'max_length': '39'}),
311+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
312+ 'netmask': ('maasserver.fields.MAASIPAddressField', [], {'max_length': '39'}),
313+ 'vlan_tag': ('django.db.models.fields.PositiveSmallIntegerField', [], {'unique': 'True', 'null': 'True', 'blank': 'True'})
314+ },
315+ u'maasserver.node': {
316+ 'Meta': {'object_name': 'Node'},
317+ 'agent_name': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'null': 'True', 'blank': 'True'}),
318+ 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '31', 'null': 'True', 'blank': 'True'}),
319+ 'boot_type': ('django.db.models.fields.CharField', [], {'default': "u'fastpath'", 'max_length': '20'}),
320+ 'cpu_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
321+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
322+ 'disable_ipv4': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
323+ 'distro_series': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'blank': 'True'}),
324+ 'error': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
325+ 'error_description': ('django.db.models.fields.TextField', [], {'default': "u''", 'blank': 'True'}),
326+ 'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'unique': 'True', 'max_length': '255', 'blank': 'True'}),
327+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
328+ 'installable': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
329+ 'license_key': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}),
330+ 'memory': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
331+ 'netboot': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
332+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']", 'null': 'True'}),
333+ 'osystem': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'blank': 'True'}),
334+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}),
335+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "u'children'", 'null': 'True', 'blank': 'True', 'to': u"orm['maasserver.Node']"}),
336+ 'power_parameters': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
337+ 'power_state': ('django.db.models.fields.CharField', [], {'default': "u'unknown'", 'max_length': '10'}),
338+ 'power_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '10', 'blank': 'True'}),
339+ 'pxe_mac': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'+'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': u"orm['maasserver.MACAddress']", 'blank': 'True', 'null': 'True'}),
340+ 'routers': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'macaddr'", 'null': 'True', 'blank': 'True'}),
341+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '10'}),
342+ 'swap_size': ('django.db.models.fields.BigIntegerField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
343+ 'system_id': ('django.db.models.fields.CharField', [], {'default': "u'node-4bae813a-1017-11e5-9b34-3c970e0e56dc'", 'unique': 'True', 'max_length': '41'}),
344+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Tag']", 'symmetrical': 'False'}),
345+ 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Token']", 'null': 'True'}),
346+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
347+ 'zone': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Zone']", 'on_delete': 'models.SET_DEFAULT'})
348+ },
349+ u'maasserver.nodegroup': {
350+ 'Meta': {'object_name': 'NodeGroup'},
351+ 'api_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '18'}),
352+ 'api_token': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Token']", 'unique': 'True'}),
353+ 'cluster_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100', 'blank': 'True'}),
354+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
355+ 'default_disable_ipv4': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
356+ 'dhcp_key': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
357+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
358+ 'maas_url': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
359+ 'name': ('maasserver.models.nodegroup.DomainNameField', [], {'max_length': '80', 'blank': 'True'}),
360+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
361+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
362+ 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'})
363+ },
364+ u'maasserver.nodegroupinterface': {
365+ 'Meta': {'unique_together': "((u'nodegroup', u'name'),)", 'object_name': 'NodeGroupInterface'},
366+ 'broadcast_ip': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
367+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
368+ 'foreign_dhcp_ip': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
369+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
370+ 'interface': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
371+ 'ip': ('maasserver.fields.MAASIPAddressField', [], {'max_length': '39'}),
372+ 'ip_range_high': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
373+ 'ip_range_low': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
374+ 'management': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
375+ 'name': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
376+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
377+ 'router_ip': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
378+ 'static_ip_range_high': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
379+ 'static_ip_range_low': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
380+ 'subnet_mask': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
381+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
382+ },
383+ u'maasserver.partition': {
384+ 'Meta': {'object_name': 'Partition'},
385+ 'bootable': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
386+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
387+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
388+ 'partition_table': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'partitions'", 'to': u"orm['maasserver.PartitionTable']"}),
389+ 'size': ('django.db.models.fields.BigIntegerField', [], {}),
390+ 'start_offset': ('django.db.models.fields.BigIntegerField', [], {}),
391+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
392+ 'uuid': ('django.db.models.fields.CharField', [], {'max_length': '36', 'unique': 'True', 'null': 'True', 'blank': 'True'})
393+ },
394+ u'maasserver.partitiontable': {
395+ 'Meta': {'object_name': 'PartitionTable'},
396+ 'block_device': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.BlockDevice']"}),
397+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
398+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
399+ 'table_type': ('django.db.models.fields.CharField', [], {'default': "u'GPT'", 'max_length': '20'}),
400+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
401+ },
402+ u'maasserver.physicalblockdevice': {
403+ 'Meta': {'ordering': "[u'id']", 'object_name': 'PhysicalBlockDevice', '_ormbases': [u'maasserver.BlockDevice']},
404+ u'blockdevice_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['maasserver.BlockDevice']", 'unique': 'True', 'primary_key': 'True'}),
405+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
406+ 'serial': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'})
407+ },
408+ u'maasserver.sshkey': {
409+ 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSHKey'},
410+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
411+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
412+ 'key': ('django.db.models.fields.TextField', [], {}),
413+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
414+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
415+ },
416+ u'maasserver.sslkey': {
417+ 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSLKey'},
418+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
419+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
420+ 'key': ('django.db.models.fields.TextField', [], {}),
421+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
422+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
423+ },
424+ u'maasserver.staticipaddress': {
425+ 'Meta': {'object_name': 'StaticIPAddress'},
426+ 'alloc_type': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
427+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
428+ 'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'null': 'True', 'blank': 'True'}),
429+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
430+ 'ip': ('maasserver.fields.MAASIPAddressField', [], {'unique': 'True', 'max_length': '39'}),
431+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
432+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
433+ },
434+ u'maasserver.tag': {
435+ 'Meta': {'object_name': 'Tag'},
436+ 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
437+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
438+ 'definition': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
439+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
440+ 'kernel_opts': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
441+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
442+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
443+ },
444+ u'maasserver.userprofile': {
445+ 'Meta': {'object_name': 'UserProfile'},
446+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
447+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'})
448+ },
449+ u'maasserver.virtualblockdevice': {
450+ 'Meta': {'ordering': "[u'id']", 'object_name': 'VirtualBlockDevice', '_ormbases': [u'maasserver.BlockDevice']},
451+ u'blockdevice_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['maasserver.BlockDevice']", 'unique': 'True', 'primary_key': 'True'}),
452+ 'filesystem_group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'virtual_devices'", 'to': u"orm['maasserver.FilesystemGroup']"}),
453+ 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'})
454+ },
455+ u'maasserver.vlan': {
456+ 'Meta': {'unique_together': "((u'vid', u'fabric'), (u'name', u'fabric'))", 'object_name': 'VLAN'},
457+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
458+ 'fabric': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Fabric']"}),
459+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
460+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
461+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
462+ 'vid': ('django.db.models.fields.IntegerField', [], {})
463+ },
464+ u'maasserver.zone': {
465+ 'Meta': {'ordering': "[u'name']", 'object_name': 'Zone'},
466+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
467+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
468+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
469+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
470+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
471+ },
472+ u'piston.consumer': {
473+ 'Meta': {'object_name': 'Consumer'},
474+ 'description': ('django.db.models.fields.TextField', [], {}),
475+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
476+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
477+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
478+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
479+ 'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '16'}),
480+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'consumers'", 'null': 'True', 'to': u"orm['auth.User']"})
481+ },
482+ u'piston.token': {
483+ 'Meta': {'object_name': 'Token'},
484+ 'callback': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
485+ 'callback_confirmed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
486+ 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Consumer']"}),
487+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
488+ 'is_approved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
489+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
490+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
491+ 'timestamp': ('django.db.models.fields.IntegerField', [], {'default': '1434012793L'}),
492+ 'token_type': ('django.db.models.fields.IntegerField', [], {}),
493+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'tokens'", 'null': 'True', 'to': u"orm['auth.User']"}),
494+ 'verifier': ('django.db.models.fields.CharField', [], {'max_length': '10'})
495+ }
496+ }
497+
498+ complete_apps = ['maasserver']
499\ No newline at end of file
500
501=== modified file 'src/maasserver/models/__init__.py'
502--- src/maasserver/models/__init__.py 2015-06-09 20:16:46 +0000
503+++ src/maasserver/models/__init__.py 2015-06-11 14:44:57 +0000
504@@ -29,6 +29,7 @@
505 'FileStorage',
506 'Filesystem',
507 'FilesystemGroup',
508+ 'Interface',
509 'LargeFile',
510 'LicenseKey',
511 'logger',
512@@ -72,6 +73,7 @@
513 from maasserver.models.downloadprogress import DownloadProgress
514 from maasserver.models.event import Event
515 from maasserver.models.eventtype import EventType
516+from maasserver.models.fabric import Fabric
517 from maasserver.models.filestorage import FileStorage
518 from maasserver.models.filesystem import Filesystem
519 from maasserver.models.filesystemgroup import FilesystemGroup
520@@ -96,6 +98,7 @@
521 from maasserver.models.user import create_user
522 from maasserver.models.userprofile import UserProfile
523 from maasserver.models.virtualblockdevice import VirtualBlockDevice
524+from maasserver.models.vlan import VLAN
525 from maasserver.models.zone import Zone
526 from maasserver.utils import ignore_unused
527 from piston.doc import HandlerDocumentation
528@@ -105,10 +108,10 @@
529 ignore_unused(
530 BootResource, BootResourceFile, BootResourceSet, CandidateName,
531 ComponentError, Config, DHCPLease, DownloadProgress, Event, EventType,
532- FileStorage, Filesystem, FilesystemGroup, LargeFile, LicenseKey,
533+ Fabric, FileStorage, Filesystem, FilesystemGroup, LargeFile, LicenseKey,
534 StaticIPAddress, MACAddress, MACStaticIPAddressLink, Network, NodeGroup,
535 NodeGroupInterface, Partition, PartitionTable, SSHKey, Tag, UserProfile,
536- VirtualBlockDevice, Zone, logger)
537+ VirtualBlockDevice, VLAN, Zone, logger)
538
539
540 # Connect the 'create_user' method to the post save signal of User.
541
542=== added file 'src/maasserver/models/fabric.py'
543--- src/maasserver/models/fabric.py 1970-01-01 00:00:00 +0000
544+++ src/maasserver/models/fabric.py 2015-06-11 14:44:57 +0000
545@@ -0,0 +1,113 @@
546+# Copyright 2015 Canonical Ltd. This software is licensed under the
547+# GNU Affero General Public License version 3 (see the file LICENSE).
548+
549+"""Fabric objects."""
550+
551+from __future__ import (
552+ absolute_import,
553+ print_function,
554+ unicode_literals,
555+ )
556+
557+str = None
558+
559+__metaclass__ = type
560+__all__ = [
561+ "DEFAULT_FABRIC_NAME",
562+ "Fabric",
563+ "FABRIC_NAME_VALIDATOR",
564+ ]
565+
566+import datetime
567+
568+from django.core.exceptions import ValidationError
569+from django.core.validators import RegexValidator
570+from django.db.models import (
571+ CharField,
572+ ForeignKey,
573+ Manager,
574+)
575+from maasserver import DefaultMeta
576+from maasserver.models.cleansave import CleanSave
577+from maasserver.models.timestampedmodel import TimestampedModel
578+
579+
580+FABRIC_NAME_VALIDATOR = RegexValidator('^[ \w-]+$')
581+
582+# Name of the special, default fabric. This fabric cannot be deleted.
583+DEFAULT_FABRIC_NAME = 'Default fabric'
584+
585+
586+class FabricManager(Manager):
587+ """Manager for :class:`Fabric` model."""
588+
589+ def get_default_fabric(self):
590+ """Return the default fabric."""
591+ now = datetime.datetime.now()
592+ fabric, _ = self.get_or_create(
593+ id=0,
594+ defaults={
595+ 'id': 0,
596+ 'name': DEFAULT_FABRIC_NAME,
597+ 'created': now,
598+ 'updated': now,
599+ }
600+ )
601+ return fabric
602+
603+
604+class Fabric(CleanSave, TimestampedModel):
605+ """A `Fabric`.
606+
607+ :ivar name: The short-human-identifiable name for this fabric.
608+ :ivar objects: An instance of the class :class:`FabricManager`.
609+ """
610+
611+ class Meta(DefaultMeta):
612+ """Needed for South to recognize this model."""
613+ verbose_name = "Fabric"
614+ verbose_name_plural = "Fabrics"
615+
616+ objects = FabricManager()
617+
618+ name = CharField(
619+ max_length=256, unique=True, editable=True,
620+ validators=[FABRIC_NAME_VALIDATOR])
621+
622+ default_vlan = ForeignKey(
623+ 'VLAN', blank=True, null=True, editable=True, related_name='+')
624+
625+ def __unicode__(self):
626+ return "name=%s" % self.name
627+
628+ def is_default(self):
629+ """Is this the default fabric?"""
630+ return self.id == 0
631+
632+ def clean(self, *args, **kwargs):
633+ wrong_fabric = (
634+ self.default_vlan_id is not None and
635+ self.default_vlan.fabric != self)
636+ if wrong_fabric:
637+ raise ValidationError(
638+ {'default_vlan':
639+ ["Can't set a default VLAN that's not in this fabric."]})
640+ super(Fabric, self).clean(*args, **kwargs)
641+
642+ def delete(self):
643+ if self.is_default():
644+ raise ValidationError(
645+ "This fabric is the default fabric, it cannot be deleted.")
646+ super(Fabric, self).delete()
647+
648+ def save(self, *args, **kwargs):
649+ created = self.id is None
650+ super(Fabric, self).save(*args, **kwargs)
651+ # Create default VLAN if this is a fabric creation.
652+ if created:
653+ from maasserver.models.vlan import (
654+ VLAN, DEFAULT_VLAN_NAME, DEFAULT_VID)
655+ default_vlan = VLAN.objects.create(
656+ name=DEFAULT_VLAN_NAME, vid=DEFAULT_VID, fabric=self)
657+ self.default_vlan = default_vlan
658+ self.save()
659
660=== added file 'src/maasserver/models/tests/test_fabric.py'
661--- src/maasserver/models/tests/test_fabric.py 1970-01-01 00:00:00 +0000
662+++ src/maasserver/models/tests/test_fabric.py 2015-06-11 14:44:57 +0000
663@@ -0,0 +1,85 @@
664+# Copyright 2015 Canonical Ltd. This software is licensed under the
665+# GNU Affero General Public License version 3 (see the file LICENSE).
666+
667+"""Tests for the Fabric model."""
668+
669+from __future__ import (
670+ absolute_import,
671+ print_function,
672+ unicode_literals,
673+ )
674+
675+str = None
676+
677+__metaclass__ = type
678+__all__ = []
679+
680+
681+from django.core.exceptions import ValidationError
682+from maasserver.models.fabric import (
683+ DEFAULT_FABRIC_NAME,
684+ Fabric,
685+)
686+from maasserver.models.vlan import (
687+ DEFAULT_VID,
688+ DEFAULT_VLAN_NAME,
689+)
690+from maasserver.testing.factory import factory
691+from maasserver.testing.testcase import MAASServerTestCase
692+from testtools.matchers import MatchesStructure
693+from testtools.testcase import ExpectedException
694+
695+
696+class FabricTest(MAASServerTestCase):
697+
698+ def test_creates_fabric_with_default_vlan(self):
699+ name = factory.make_name('name')
700+ fabric = factory.make_Fabric(name=name)
701+ self.assertEqual(name, fabric.name)
702+ default_vlan = fabric.default_vlan
703+ self.assertThat(default_vlan, MatchesStructure.byEquality(
704+ vid=DEFAULT_VID, name=DEFAULT_VLAN_NAME))
705+
706+ def test_get_default_fabric_creates_default_fabric(self):
707+ default_fabric = Fabric.objects.get_default_fabric()
708+ self.assertThat(default_fabric, MatchesStructure.byEquality(
709+ id=0, name=DEFAULT_FABRIC_NAME))
710+
711+ def test_get_default_fabric_is_idempotent(self):
712+ default_fabric = Fabric.objects.get_default_fabric()
713+ default_fabric2 = Fabric.objects.get_default_fabric()
714+ self.assertEqual(default_fabric.id, default_fabric2.id)
715+
716+ def test_is_default_detects_default_fabric(self):
717+ default_fabric = Fabric.objects.get_default_fabric()
718+ self.assertTrue(default_fabric.is_default())
719+
720+ def test_is_default_detects_non_default_fabric(self):
721+ name = factory.make_name('name')
722+ fabric = factory.make_Fabric(name=name)
723+ self.assertFalse(fabric.is_default())
724+
725+ def test_cant_delete_default_fabric(self):
726+ default_fabric = Fabric.objects.get_default_fabric()
727+ with ExpectedException(ValidationError):
728+ default_fabric.delete()
729+
730+ def test_can_delete_non_default_fabric(self):
731+ name = factory.make_name('name')
732+ fabric = factory.make_Fabric(name=name)
733+ fabric.delete()
734+ self.assertItemsEqual([], Fabric.objects.all())
735+
736+ def test_save_rejects_default_vlan_not_in_fabric(self):
737+ vlan = factory.make_VLAN()
738+ fabric = factory.make_Fabric()
739+ fabric.default_vlan = vlan
740+ with ExpectedException(ValidationError):
741+ fabric.save()
742+
743+ def test_save_accepts_default_vlan_in_fabric(self):
744+ fabric = factory.make_Fabric()
745+ vlan = factory.make_VLAN(fabric=fabric)
746+ fabric.default_vlan = vlan
747+ # No exception.
748+ self.assertIsNone(fabric.save())
749
750=== added file 'src/maasserver/models/tests/test_vlan.py'
751--- src/maasserver/models/tests/test_vlan.py 1970-01-01 00:00:00 +0000
752+++ src/maasserver/models/tests/test_vlan.py 2015-06-11 14:44:57 +0000
753@@ -0,0 +1,79 @@
754+# Copyright 2015 Canonical Ltd. This software is licensed under the
755+# GNU Affero General Public License version 3 (see the file LICENSE).
756+
757+"""Tests for the VLAN model."""
758+
759+from __future__ import (
760+ absolute_import,
761+ print_function,
762+ unicode_literals,
763+ )
764+
765+str = None
766+
767+__metaclass__ = type
768+__all__ = []
769+
770+import random
771+
772+from django.core.exceptions import ValidationError
773+from maasserver.models.vlan import VLAN
774+from maasserver.testing.factory import factory
775+from maasserver.testing.testcase import MAASServerTestCase
776+from testtools.matchers import MatchesStructure
777+from testtools.testcase import ExpectedException
778+
779+
780+class VLANTest(MAASServerTestCase):
781+
782+ def test_creates_vlan(self):
783+ name = factory.make_name('name')
784+ vid = random.randint(3, 55)
785+ fabric = factory.make_Fabric()
786+ vlan = VLAN(vid=vid, name=name, fabric=fabric)
787+ vlan.save()
788+ self.assertThat(vlan, MatchesStructure.byEquality(
789+ vid=vid, name=name))
790+
791+ def test_is_fabric_default_detects_default_vlan(self):
792+ fabric = factory.make_Fabric()
793+ vlan = factory.make_VLAN(fabric=fabric)
794+ fabric.default_vlan = vlan
795+ fabric.save()
796+ self.assertTrue(vlan.is_fabric_default())
797+
798+ def test_is_fabric_default_detects_non_default_vlan(self):
799+ vlan = factory.make_VLAN()
800+ self.assertFalse(vlan.is_fabric_default())
801+
802+
803+class VLANVidValidationTest(MAASServerTestCase):
804+
805+ scenarios = [
806+ ('0', {'vid': 0, 'valid': True}),
807+ ('12', {'vid': 12, 'valid': True}),
808+ ('250', {'vid': 250, 'valid': True}),
809+ ('3000', {'vid': 3000, 'valid': True}),
810+ ('4095', {'vid': 4095, 'valid': True}),
811+ ('-23', {'vid': -23, 'valid': False}),
812+ ('4096', {'vid': 4096, 'valid': False}),
813+ ('10000', {'vid': 10000, 'valid': False}),
814+ ]
815+
816+ def test_validates_vid(self):
817+ fabric = factory.make_Fabric()
818+ # Remove the auto-created default VLAN so that
819+ # we can create it in this test.
820+ default_vlan = fabric.default_vlan
821+ fabric.default_vlan = None
822+ fabric.save()
823+ default_vlan.delete()
824+ name = factory.make_name('name')
825+ vlan = VLAN(vid=self.vid, name=name, fabric=fabric)
826+ if self.valid:
827+ # No exception.
828+ self.assertIsNone(vlan.save())
829+
830+ else:
831+ with ExpectedException(ValidationError):
832+ vlan.save()
833
834=== added file 'src/maasserver/models/vlan.py'
835--- src/maasserver/models/vlan.py 1970-01-01 00:00:00 +0000
836+++ src/maasserver/models/vlan.py 2015-06-11 14:44:57 +0000
837@@ -0,0 +1,81 @@
838+# Copyright 2015 Canonical Ltd. This software is licensed under the
839+# GNU Affero General Public License version 3 (see the file LICENSE).
840+
841+"""VLAN objects."""
842+
843+from __future__ import (
844+ absolute_import,
845+ print_function,
846+ unicode_literals,
847+ )
848+
849+str = None
850+
851+__metaclass__ = type
852+__all__ = [
853+ "DEFAULT_VID",
854+ "DEFAULT_VLAN_NAME",
855+ "Fabric",
856+ ]
857+
858+
859+from django.core.exceptions import ValidationError
860+from django.core.validators import RegexValidator
861+from django.db.models import (
862+ CharField,
863+ ForeignKey,
864+ IntegerField,
865+)
866+from maasserver import DefaultMeta
867+from maasserver.models.cleansave import CleanSave
868+from maasserver.models.fabric import Fabric
869+from maasserver.models.timestampedmodel import TimestampedModel
870+
871+
872+VLAN_NAME_VALIDATOR = RegexValidator('^[ \w-]+$')
873+
874+DEFAULT_VLAN_NAME = 'Default VLAN'
875+DEFAULT_VID = 0
876+
877+
878+class VLAN(CleanSave, TimestampedModel):
879+ """A `VLAN`.
880+
881+ :ivar name: The short-human-identifiable name for this VLAN.
882+ :ivar vid: The VLAN ID of this VLAN.
883+ :ivar fabric: The `Fabric` this VLAN belongs to.
884+ """
885+
886+ class Meta(DefaultMeta):
887+ """Needed for South to recognize this model."""
888+ verbose_name = "VLAN"
889+ verbose_name_plural = "VLANs"
890+ unique_together = (
891+ ('vid', 'fabric'),
892+ ('name', 'fabric'),
893+ )
894+
895+ name = CharField(
896+ max_length=256, editable=True, validators=[VLAN_NAME_VALIDATOR])
897+
898+ vid = IntegerField(editable=True)
899+
900+ fabric = ForeignKey(
901+ 'Fabric', blank=False, editable=True)
902+
903+ def __unicode__(self):
904+ return "name=%s, vid=%d, fabric=%s" % (
905+ self.name, self.vid, self.fabric.name)
906+
907+ def clean_vid(self):
908+ if self.vid < 0 or self.vid > 4095:
909+ raise ValidationError(
910+ {'vid':
911+ ["Vid must be between 0 and 4095."]})
912+
913+ def clean(self):
914+ self.clean_vid()
915+
916+ def is_fabric_default(self):
917+ """Is this the default VLAN in the fabric?"""
918+ return self.fabric.default_vlan == self
919
920=== modified file 'src/maasserver/testing/factory.py'
921--- src/maasserver/testing/factory.py 2015-05-20 13:51:49 +0000
922+++ src/maasserver/testing/factory.py 2015-06-11 14:44:57 +0000
923@@ -56,6 +56,7 @@
924 DownloadProgress,
925 Event,
926 EventType,
927+ Fabric,
928 FileStorage,
929 Filesystem,
930 FilesystemGroup,
931@@ -75,6 +76,7 @@
932 StaticIPAddress,
933 Tag,
934 VirtualBlockDevice,
935+ VLAN,
936 Zone,
937 )
938 from maasserver.models.bootresourceset import (
939@@ -581,6 +583,26 @@
940 key.save()
941 return key
942
943+ def make_Fabric(self, name=None):
944+ if name is None:
945+ name = self.make_name('fabric')
946+ fabric = Fabric(name=name)
947+ fabric.save()
948+ return fabric
949+
950+ def make_VLAN(self, name=None, vid=None, fabric=None):
951+ assert vid != 0, "VID=0 VLANs are auto-created"
952+ if name is None:
953+ name = self.make_name('vlan')
954+ if vid is None:
955+ # Don't create the vid=0 VLAN, it's auto-created.
956+ vid = random.randint(1, 4095)
957+ if fabric is None:
958+ fabric = self.make_Fabric()
959+ vlan = VLAN(name=name, vid=vid, fabric=fabric)
960+ vlan.save()
961+ return vlan
962+
963 def make_Tag(self, name=None, definition=None, comment='',
964 kernel_opts=None, created=None, updated=None):
965 if name is None: