Merge lp:~julian-edwards/maas/dup-boot-source-selections-bug-1360280 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: 3361
Proposed branch: lp:~julian-edwards/maas/dup-boot-source-selections-bug-1360280
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 867 lines (+832/-0)
4 files modified
src/maasserver/migrations/0115_unique_boot_source_selections.py (+433/-0)
src/maasserver/migrations/0116_unique_boot_source_selections.py (+373/-0)
src/maasserver/models/bootsourceselection.py (+1/-0)
src/maasserver/tests/test_forms_bootsourceselection.py (+25/-0)
To merge this branch: bzr merge lp:~julian-edwards/maas/dup-boot-source-selections-bug-1360280
Reviewer Review Type Date Requested Status
Graham Binns (community) Approve
Review via email: mp+241235@code.launchpad.net

Commit message

Make os, release and boot_source unique for BootSourceSelection to stop duplicate entries.

Description of the change

Add a unique index across boot_source, os and release on BootSourceSelections.

This will obviously potentially fail a migration if someone has dupes already, but this really needs to go in. Since nobody was around to do a pre-imp I'm throwing this up as-is to discuss ideas:

 1. Land it as-is and if it fails add a release note telling someone to remove the dupes manually (so they get to choose which to keep)
 2. Add code to the migration to force-delete all but the newest entry for each boot source.

Anything else?

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) wrote :

ISTR Blake saying that we had to coalesce the entries for one OS/release rather than just dropping all-but-one, because the entries may all have different arches, subarches and labels — all of which are perfectly valid choices, but which need to be recorded in a single row, not several.

So, if we don't want to have settings go away surprisingly, (1) is a good option.

That said, could we add a persistent, user-clearable error* after doing (2) that says something like "MAAS's records of your boot source settings may have changed; check the Images tab to ensure that your boot image settings are correct."

*Not sure we actually have the facility to do this; I was thinking of the persistent error stuff, but that all seems to be auto-cleared or not at all.

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

On Monday 10 Nov 2014 07:18:40 you wrote:
> ISTR Blake saying that we had to coalesce the entries for one OS/release
> rather than just dropping all-but-one, because the entries may all have
> different arches, subarches and labels — all of which are perfectly valid
> choices, but which need to be recorded in a single row, not several.
>
> So, if we don't want to have settings go away surprisingly, (1) is a good
> option.
>
> That said, could we add a persistent, user-clearable error* after doing (2)
> that says something like "MAAS's records of your boot source settings may
> have changed; check the Images tab to ensure that your boot image settings
> are correct."
>
> *Not sure we actually have the facility to do this; I was thinking of the
> persistent error stuff, but that all seems to be auto-cleared or not at
> all.

Actually I hadn't considered coalescing them - good point! I'll write some
runes to do that later.

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

Right, coalescing migration added, care to take another look?

I have tested this reasonably thoroughly in the dev environment, I used this script to poke values in http://paste.ubuntu.com/8935209/ and the end result looked exactly right (see comments in that script).

Note that there's two migrations as you can't do data and schema changes in the same migration.

Revision history for this message
Graham Binns (gmb) wrote :

Tip top. Really nice work, Jools!

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (20.7 KiB)

The attempt to merge lp:~julian-edwards/maas/dup-boot-source-selections-bug-1360280 into lp:maas failed. Below is the output from the failed tests.

Ign http://nova.clouds.archive.ubuntu.com trusty InRelease
Ign http://security.ubuntu.com trusty-security InRelease
Get:1 http://security.ubuntu.com trusty-security Release.gpg [933 B]
Ign http://nova.clouds.archive.ubuntu.com trusty-updates InRelease
Get:2 http://security.ubuntu.com trusty-security Release [62.0 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty Release.gpg
Get:3 http://nova.clouds.archive.ubuntu.com trusty-updates Release.gpg [933 B]
Hit http://nova.clouds.archive.ubuntu.com trusty Release
Get:4 http://nova.clouds.archive.ubuntu.com trusty-updates Release [62.0 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty/main Sources
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Sources
Hit http://nova.clouds.archive.ubuntu.com trusty/main amd64 Packages
Get:5 http://security.ubuntu.com trusty-security/main Sources [49.5 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty/universe amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en
Get:6 http://nova.clouds.archive.ubuntu.com trusty-updates/main Sources [136 kB]
Get:7 http://security.ubuntu.com trusty-security/universe Sources [13.1 kB]
Get:8 http://security.ubuntu.com trusty-security/main amd64 Packages [153 kB]
Get:9 http://nova.clouds.archive.ubuntu.com trusty-updates/universe Sources [89.5 kB]
Get:10 http://security.ubuntu.com trusty-security/universe amd64 Packages [60.5 kB]
Get:11 http://nova.clouds.archive.ubuntu.com trusty-updates/main amd64 Packages [356 kB]
Hit http://security.ubuntu.com trusty-security/main Translation-en
Hit http://security.ubuntu.com trusty-security/universe Translation-en
Get:12 http://nova.clouds.archive.ubuntu.com trusty-updates/universe amd64 Packages [217 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Translation-en
Ign http://nova.clouds.archive.ubuntu.com trusty/main Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en_US
Fetched 1,200 kB in 2s (411 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
     --no-install-recommends install apache2 authbind bind9 bind9utils build-essential bzr-builddeb curl daemontools debhelper dh-apport distro-info dnsutils firefox freeipmi-tools gjs ipython isc-dhcp-common libjs-raphael libjs-yui3-full libjs-yui3-min libpq-dev make pep8 postgresql pyflakes python-amqplib python-bzrlib python-celery python-convoy python-crochet python-cssselect python-curtin python-dev python-distro-info python-django python-django-piston python-django-south python-djorm-ext-pgarray python-docutils python-extras python-fixtures python-flake8 python-formencode python-hivex python-httplib2 python-jinja2 python-jsonschema python-lockfile python-lxml python-mimeparse python-mock python-netaddr python-netifaces python-nose python-oauth python-oops python-oops-amqp python-oops-datedir-repo python-oops-twisted p...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'src/maasserver/migrations/0115_unique_boot_source_selections.py'
2--- src/maasserver/migrations/0115_unique_boot_source_selections.py 1970-01-01 00:00:00 +0000
3+++ src/maasserver/migrations/0115_unique_boot_source_selections.py 2014-11-12 00:21:52 +0000
4@@ -0,0 +1,433 @@
5+from collections import namedtuple
6+
7+from django.db import models
8+from south.db import db
9+# -*- coding: utf-8 -*-
10+from south.utils import datetime_utils as datetime
11+from south.v2 import SchemaMigration
12+
13+
14+Selection = namedtuple('Selection', ['boot_source', 'os', 'release'])
15+Values = namedtuple('Values', ['arches', 'subarches', 'labels'])
16+
17+
18+class Migration(SchemaMigration):
19+
20+ def forwards(self, orm):
21+ # Coalesce all the arch/subarch/label fields for duplicated
22+ # records across release/os/boot_source. Do this by using the
23+ # Selection as the key in a dict containing Values.
24+ boot_sources = dict()
25+ for bss in orm['maasserver.bootsourceselection'].objects.all():
26+ selection = Selection(
27+ boot_source=bss.boot_source,
28+ os=bss.os,
29+ release=bss.release,
30+ )
31+ values = Values(
32+ arches=set(bss.arches),
33+ subarches=set(bss.subarches),
34+ labels=set(bss.labels),
35+ )
36+ if selection not in boot_sources:
37+ # New boot_source/os/release combo.
38+ boot_sources[selection] = values
39+ else:
40+ # Merge with existing.
41+ arches_set = boot_sources[selection].arches
42+ subarches_set = boot_sources[selection].subarches
43+ labels_set = boot_sources[selection].labels
44+
45+ boot_sources[selection] = Values(
46+ arches=arches_set.union(values.arches),
47+ subarches=subarches_set.union(values.subarches),
48+ labels=labels_set.union(values.labels),
49+ )
50+
51+ # Delete all the existing rows in the table. Nothing currently
52+ # has BootSourceSelection as a foreign key, so Django's default
53+ # behaviour of ON DELETE CASCADE should not delete anything
54+ # else here.
55+ orm['maasserver.bootsourceselection'].objects.all().delete()
56+
57+ now = datetime.datetime.utcnow()
58+ # Add the coalesced records.
59+ for selection, values in boot_sources.viewitems():
60+ bss = orm['maasserver.BootSourceSelection'](
61+ created=now,
62+ updated=now,
63+ boot_source=selection.boot_source,
64+ os=selection.os,
65+ release=selection.release,
66+ arches=list(values.arches),
67+ subarches=list(values.subarches),
68+ labels=list(values.labels),
69+ )
70+ bss.save()
71+
72+ # This happens in the next migration as you cannot do data and
73+ # schema in the same one.
74+ # Adding unique constraint on 'BootSourceSelection', fields ['release', 'boot_source', 'os']
75+ #db.create_unique(u'maasserver_bootsourceselection', ['release', 'boot_source_id', 'os'])
76+
77+
78+ def backwards(self, orm):
79+ # Removing unique constraint on 'BootSourceSelection', fields ['release', 'boot_source', 'os']
80+ #db.delete_unique(u'maasserver_bootsourceselection', ['release', 'boot_source_id', 'os'])
81+ pass
82+
83+
84+ models = {
85+ u'auth.group': {
86+ 'Meta': {'object_name': 'Group'},
87+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
88+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
89+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
90+ },
91+ u'auth.permission': {
92+ 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
93+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
94+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
95+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
96+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
97+ },
98+ u'auth.user': {
99+ 'Meta': {'object_name': 'User'},
100+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
101+ 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75', 'blank': 'True'}),
102+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
103+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
104+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
105+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
106+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
107+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
108+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
109+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
110+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
111+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
112+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
113+ },
114+ u'contenttypes.contenttype': {
115+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
116+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
117+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
118+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
119+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
120+ },
121+ u'maasserver.bootresource': {
122+ 'Meta': {'unique_together': "((u'name', u'architecture'),)", 'object_name': 'BootResource'},
123+ 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
124+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
125+ 'extra': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
126+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
127+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
128+ 'rtype': ('django.db.models.fields.IntegerField', [], {'max_length': '10'}),
129+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
130+ },
131+ u'maasserver.bootresourcefile': {
132+ 'Meta': {'unique_together': "((u'resource_set', u'filetype'),)", 'object_name': 'BootResourceFile'},
133+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
134+ 'extra': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
135+ 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
136+ 'filetype': ('django.db.models.fields.CharField', [], {'default': "u'root-tgz'", 'max_length': '20'}),
137+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
138+ 'largefile': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.LargeFile']"}),
139+ 'resource_set': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'files'", 'to': u"orm['maasserver.BootResourceSet']"}),
140+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
141+ },
142+ u'maasserver.bootresourceset': {
143+ 'Meta': {'unique_together': "((u'resource', u'version'),)", 'object_name': 'BootResourceSet'},
144+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
145+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
146+ 'label': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
147+ 'resource': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'sets'", 'to': u"orm['maasserver.BootResource']"}),
148+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
149+ 'version': ('django.db.models.fields.CharField', [], {'max_length': '255'})
150+ },
151+ u'maasserver.bootsource': {
152+ 'Meta': {'object_name': 'BootSource'},
153+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
154+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
155+ 'keyring_data': ('maasserver.fields.EditableBinaryField', [], {'blank': 'True'}),
156+ 'keyring_filename': ('django.db.models.fields.FilePathField', [], {'max_length': '100', 'blank': 'True'}),
157+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
158+ 'url': ('django.db.models.fields.URLField', [], {'unique': 'True', 'max_length': '200'})
159+ },
160+ u'maasserver.bootsourcecache': {
161+ 'Meta': {'object_name': 'BootSourceCache'},
162+ 'arch': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
163+ 'boot_source': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.BootSource']"}),
164+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
165+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
166+ 'label': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
167+ 'os': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
168+ 'release': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
169+ 'subarch': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
170+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
171+ },
172+ u'maasserver.bootsourceselection': {
173+ 'Meta': {'unique_together': "((u'boot_source', u'os', u'release'),)", 'object_name': 'BootSourceSelection'},
174+ 'arches': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'text'", 'null': 'True', 'blank': 'True'}),
175+ 'boot_source': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.BootSource']"}),
176+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
177+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
178+ 'labels': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'text'", 'null': 'True', 'blank': 'True'}),
179+ 'os': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'blank': 'True'}),
180+ 'release': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'blank': 'True'}),
181+ 'subarches': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'text'", 'null': 'True', 'blank': 'True'}),
182+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
183+ },
184+ u'maasserver.candidatename': {
185+ 'Meta': {'unique_together': "((u'name', u'position'),)", 'object_name': 'CandidateName'},
186+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
187+ 'name': ('django.db.models.fields.SlugField', [], {'max_length': '50'}),
188+ 'position': ('django.db.models.fields.IntegerField', [], {})
189+ },
190+ u'maasserver.componenterror': {
191+ 'Meta': {'object_name': 'ComponentError'},
192+ 'component': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
193+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
194+ 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000'}),
195+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
196+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
197+ },
198+ u'maasserver.config': {
199+ 'Meta': {'object_name': 'Config'},
200+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
201+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
202+ 'value': ('maasserver.fields.JSONObjectField', [], {'null': 'True'})
203+ },
204+ u'maasserver.dhcplease': {
205+ 'Meta': {'object_name': 'DHCPLease'},
206+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
207+ 'ip': ('maasserver.fields.MAASIPAddressField', [], {'unique': 'True', 'max_length': '39'}),
208+ 'mac': ('maasserver.fields.MACAddressField', [], {}),
209+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"})
210+ },
211+ u'maasserver.downloadprogress': {
212+ 'Meta': {'object_name': 'DownloadProgress'},
213+ 'bytes_downloaded': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
214+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
215+ 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000', 'blank': 'True'}),
216+ 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
217+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
218+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
219+ 'size': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
220+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
221+ },
222+ u'maasserver.event': {
223+ 'Meta': {'object_name': 'Event'},
224+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
225+ 'description': ('django.db.models.fields.TextField', [], {'default': "u''", 'blank': 'True'}),
226+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
227+ 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}),
228+ 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.EventType']"}),
229+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
230+ },
231+ u'maasserver.eventtype': {
232+ 'Meta': {'object_name': 'EventType'},
233+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
234+ 'description': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
235+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
236+ 'level': ('django.db.models.fields.IntegerField', [], {}),
237+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
238+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
239+ },
240+ u'maasserver.filestorage': {
241+ 'Meta': {'unique_together': "((u'filename', u'owner'),)", 'object_name': 'FileStorage'},
242+ 'content': ('metadataserver.fields.BinaryField', [], {'blank': 'True'}),
243+ 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
244+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
245+ 'key': ('django.db.models.fields.CharField', [], {'default': "u'0121a28a-6899-11e4-b353-0026c71eea0e'", 'unique': 'True', 'max_length': '36'}),
246+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
247+ },
248+ u'maasserver.largefile': {
249+ 'Meta': {'object_name': 'LargeFile'},
250+ 'content': ('maasserver.fields.LargeObjectField', [], {}),
251+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
252+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
253+ 'sha256': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'}),
254+ 'total_size': ('django.db.models.fields.BigIntegerField', [], {}),
255+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
256+ },
257+ u'maasserver.licensekey': {
258+ 'Meta': {'unique_together': "((u'osystem', u'distro_series'),)", 'object_name': 'LicenseKey'},
259+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
260+ 'distro_series': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
261+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
262+ 'license_key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
263+ 'osystem': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
264+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
265+ },
266+ u'maasserver.macaddress': {
267+ 'Meta': {'ordering': "(u'created',)", 'object_name': 'MACAddress'},
268+ 'cluster_interface': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['maasserver.NodeGroupInterface']", 'null': 'True', 'blank': 'True'}),
269+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
270+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
271+ 'ip_addresses': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.StaticIPAddress']", 'symmetrical': 'False', 'through': u"orm['maasserver.MACStaticIPAddressLink']", 'blank': 'True'}),
272+ 'mac_address': ('maasserver.fields.MACAddressField', [], {'unique': 'True'}),
273+ 'networks': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Network']", 'symmetrical': 'False', 'blank': 'True'}),
274+ 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}),
275+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
276+ },
277+ u'maasserver.macstaticipaddresslink': {
278+ 'Meta': {'unique_together': "((u'ip_address', u'mac_address'),)", 'object_name': 'MACStaticIPAddressLink'},
279+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
280+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
281+ 'ip_address': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.StaticIPAddress']", 'unique': 'True'}),
282+ 'mac_address': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.MACAddress']"}),
283+ 'nic_alias': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
284+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
285+ },
286+ u'maasserver.network': {
287+ 'Meta': {'object_name': 'Network'},
288+ 'default_gateway': ('maasserver.fields.MAASIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
289+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
290+ 'dns_servers': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
291+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
292+ 'ip': ('maasserver.fields.MAASIPAddressField', [], {'unique': 'True', 'max_length': '39'}),
293+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
294+ 'netmask': ('maasserver.fields.MAASIPAddressField', [], {'max_length': '39'}),
295+ 'vlan_tag': ('django.db.models.fields.PositiveSmallIntegerField', [], {'unique': 'True', 'null': 'True', 'blank': 'True'})
296+ },
297+ u'maasserver.node': {
298+ 'Meta': {'object_name': 'Node'},
299+ 'agent_name': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'null': 'True', 'blank': 'True'}),
300+ 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '31'}),
301+ 'boot_type': ('django.db.models.fields.CharField', [], {'default': "u'fastpath'", 'max_length': '20'}),
302+ 'cpu_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
303+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
304+ 'disable_ipv4': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
305+ 'distro_series': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'blank': 'True'}),
306+ 'error': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
307+ 'error_description': ('django.db.models.fields.TextField', [], {'default': "u''", 'blank': 'True'}),
308+ 'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'unique': 'True', 'max_length': '255', 'blank': 'True'}),
309+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
310+ 'license_key': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}),
311+ 'memory': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
312+ 'netboot': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
313+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']", 'null': 'True'}),
314+ 'osystem': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'blank': 'True'}),
315+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}),
316+ 'power_parameters': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
317+ 'power_state': ('django.db.models.fields.CharField', [], {'default': "u'unknown'", 'max_length': '10'}),
318+ 'power_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '10', 'blank': 'True'}),
319+ 'pxe_mac': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "u'+'", 'null': 'True', 'blank': 'True', 'to': u"orm['maasserver.MACAddress']"}),
320+ 'routers': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'macaddr'", 'null': 'True', 'blank': 'True'}),
321+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '10'}),
322+ 'storage': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
323+ 'system_id': ('django.db.models.fields.CharField', [], {'default': "u'node-012616a8-6899-11e4-b353-0026c71eea0e'", 'unique': 'True', 'max_length': '41'}),
324+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Tag']", 'symmetrical': 'False'}),
325+ 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Token']", 'null': 'True'}),
326+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
327+ 'zone': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Zone']", 'on_delete': 'models.SET_DEFAULT'})
328+ },
329+ u'maasserver.nodegroup': {
330+ 'Meta': {'object_name': 'NodeGroup'},
331+ 'api_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '18'}),
332+ 'api_token': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Token']", 'unique': 'True'}),
333+ 'cluster_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100', 'blank': 'True'}),
334+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
335+ 'default_disable_ipv4': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
336+ 'dhcp_key': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
337+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
338+ 'maas_url': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
339+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}),
340+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
341+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
342+ 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'})
343+ },
344+ u'maasserver.nodegroupinterface': {
345+ 'Meta': {'unique_together': "((u'nodegroup', u'name'),)", 'object_name': 'NodeGroupInterface'},
346+ 'broadcast_ip': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
347+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
348+ 'foreign_dhcp_ip': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
349+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
350+ 'interface': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
351+ 'ip': ('maasserver.fields.MAASIPAddressField', [], {'max_length': '39'}),
352+ 'ip_range_high': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
353+ 'ip_range_low': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
354+ 'management': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
355+ 'name': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
356+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
357+ 'router_ip': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
358+ 'static_ip_range_high': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
359+ 'static_ip_range_low': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
360+ 'subnet_mask': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
361+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
362+ },
363+ u'maasserver.sshkey': {
364+ 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSHKey'},
365+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
366+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
367+ 'key': ('django.db.models.fields.TextField', [], {}),
368+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
369+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
370+ },
371+ u'maasserver.sslkey': {
372+ 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSLKey'},
373+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
374+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
375+ 'key': ('django.db.models.fields.TextField', [], {}),
376+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
377+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
378+ },
379+ u'maasserver.staticipaddress': {
380+ 'Meta': {'object_name': 'StaticIPAddress'},
381+ 'alloc_type': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
382+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
383+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
384+ 'ip': ('maasserver.fields.MAASIPAddressField', [], {'unique': 'True', 'max_length': '39'}),
385+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
386+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
387+ },
388+ u'maasserver.tag': {
389+ 'Meta': {'object_name': 'Tag'},
390+ 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
391+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
392+ 'definition': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
393+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
394+ 'kernel_opts': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
395+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
396+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
397+ },
398+ u'maasserver.userprofile': {
399+ 'Meta': {'object_name': 'UserProfile'},
400+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
401+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'})
402+ },
403+ u'maasserver.zone': {
404+ 'Meta': {'ordering': "[u'name']", 'object_name': 'Zone'},
405+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
406+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
407+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
408+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
409+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
410+ },
411+ u'piston.consumer': {
412+ 'Meta': {'object_name': 'Consumer'},
413+ 'description': ('django.db.models.fields.TextField', [], {}),
414+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
415+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
416+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
417+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
418+ 'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '16'}),
419+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'consumers'", 'null': 'True', 'to': u"orm['auth.User']"})
420+ },
421+ u'piston.token': {
422+ 'Meta': {'object_name': 'Token'},
423+ 'callback': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
424+ 'callback_confirmed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
425+ 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Consumer']"}),
426+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
427+ 'is_approved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
428+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
429+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
430+ 'timestamp': ('django.db.models.fields.IntegerField', [], {'default': '1415596707L'}),
431+ 'token_type': ('django.db.models.fields.IntegerField', [], {}),
432+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'tokens'", 'null': 'True', 'to': u"orm['auth.User']"}),
433+ 'verifier': ('django.db.models.fields.CharField', [], {'max_length': '10'})
434+ }
435+ }
436+
437+ complete_apps = ['maasserver']
438
439=== added file 'src/maasserver/migrations/0116_unique_boot_source_selections.py'
440--- src/maasserver/migrations/0116_unique_boot_source_selections.py 1970-01-01 00:00:00 +0000
441+++ src/maasserver/migrations/0116_unique_boot_source_selections.py 2014-11-12 00:21:52 +0000
442@@ -0,0 +1,373 @@
443+from django.db import models
444+from south.db import db
445+# -*- coding: utf-8 -*-
446+from south.utils import datetime_utils as datetime
447+from south.v2 import SchemaMigration
448+
449+
450+class Migration(SchemaMigration):
451+
452+ def forwards(self, orm):
453+ # Adding unique constraint on 'BootSourceSelection', fields ['release', 'boot_source', 'os']
454+ db.create_unique(u'maasserver_bootsourceselection', ['release', 'boot_source_id', 'os'])
455+
456+
457+ def backwards(self, orm):
458+ # Removing unique constraint on 'BootSourceSelection', fields ['release', 'boot_source', 'os']
459+ db.delete_unique(u'maasserver_bootsourceselection', ['release', 'boot_source_id', 'os'])
460+
461+
462+ models = {
463+ u'auth.group': {
464+ 'Meta': {'object_name': 'Group'},
465+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
466+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
467+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
468+ },
469+ u'auth.permission': {
470+ 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
471+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
472+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
473+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
474+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
475+ },
476+ u'auth.user': {
477+ 'Meta': {'object_name': 'User'},
478+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
479+ 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75', 'blank': 'True'}),
480+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
481+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
482+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
483+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
484+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
485+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
486+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
487+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
488+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
489+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
490+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
491+ },
492+ u'contenttypes.contenttype': {
493+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
494+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
495+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
496+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
497+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
498+ },
499+ u'maasserver.bootresource': {
500+ 'Meta': {'unique_together': "((u'name', u'architecture'),)", 'object_name': 'BootResource'},
501+ 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
502+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
503+ 'extra': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
504+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
505+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
506+ 'rtype': ('django.db.models.fields.IntegerField', [], {'max_length': '10'}),
507+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
508+ },
509+ u'maasserver.bootresourcefile': {
510+ 'Meta': {'unique_together': "((u'resource_set', u'filetype'),)", 'object_name': 'BootResourceFile'},
511+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
512+ 'extra': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
513+ 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
514+ 'filetype': ('django.db.models.fields.CharField', [], {'default': "u'root-tgz'", 'max_length': '20'}),
515+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
516+ 'largefile': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.LargeFile']"}),
517+ 'resource_set': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'files'", 'to': u"orm['maasserver.BootResourceSet']"}),
518+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
519+ },
520+ u'maasserver.bootresourceset': {
521+ 'Meta': {'unique_together': "((u'resource', u'version'),)", 'object_name': 'BootResourceSet'},
522+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
523+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
524+ 'label': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
525+ 'resource': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'sets'", 'to': u"orm['maasserver.BootResource']"}),
526+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
527+ 'version': ('django.db.models.fields.CharField', [], {'max_length': '255'})
528+ },
529+ u'maasserver.bootsource': {
530+ 'Meta': {'object_name': 'BootSource'},
531+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
532+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
533+ 'keyring_data': ('maasserver.fields.EditableBinaryField', [], {'blank': 'True'}),
534+ 'keyring_filename': ('django.db.models.fields.FilePathField', [], {'max_length': '100', 'blank': 'True'}),
535+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
536+ 'url': ('django.db.models.fields.URLField', [], {'unique': 'True', 'max_length': '200'})
537+ },
538+ u'maasserver.bootsourcecache': {
539+ 'Meta': {'object_name': 'BootSourceCache'},
540+ 'arch': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
541+ 'boot_source': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.BootSource']"}),
542+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
543+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
544+ 'label': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
545+ 'os': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
546+ 'release': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
547+ 'subarch': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
548+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
549+ },
550+ u'maasserver.bootsourceselection': {
551+ 'Meta': {'unique_together': "((u'boot_source', u'os', u'release'),)", 'object_name': 'BootSourceSelection'},
552+ 'arches': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'text'", 'null': 'True', 'blank': 'True'}),
553+ 'boot_source': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.BootSource']"}),
554+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
555+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
556+ 'labels': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'text'", 'null': 'True', 'blank': 'True'}),
557+ 'os': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'blank': 'True'}),
558+ 'release': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'blank': 'True'}),
559+ 'subarches': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'text'", 'null': 'True', 'blank': 'True'}),
560+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
561+ },
562+ u'maasserver.candidatename': {
563+ 'Meta': {'unique_together': "((u'name', u'position'),)", 'object_name': 'CandidateName'},
564+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
565+ 'name': ('django.db.models.fields.SlugField', [], {'max_length': '50'}),
566+ 'position': ('django.db.models.fields.IntegerField', [], {})
567+ },
568+ u'maasserver.componenterror': {
569+ 'Meta': {'object_name': 'ComponentError'},
570+ 'component': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
571+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
572+ 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000'}),
573+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
574+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
575+ },
576+ u'maasserver.config': {
577+ 'Meta': {'object_name': 'Config'},
578+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
579+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
580+ 'value': ('maasserver.fields.JSONObjectField', [], {'null': 'True'})
581+ },
582+ u'maasserver.dhcplease': {
583+ 'Meta': {'object_name': 'DHCPLease'},
584+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
585+ 'ip': ('maasserver.fields.MAASIPAddressField', [], {'unique': 'True', 'max_length': '39'}),
586+ 'mac': ('maasserver.fields.MACAddressField', [], {}),
587+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"})
588+ },
589+ u'maasserver.downloadprogress': {
590+ 'Meta': {'object_name': 'DownloadProgress'},
591+ 'bytes_downloaded': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
592+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
593+ 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000', 'blank': 'True'}),
594+ 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
595+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
596+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
597+ 'size': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
598+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
599+ },
600+ u'maasserver.event': {
601+ 'Meta': {'object_name': 'Event'},
602+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
603+ 'description': ('django.db.models.fields.TextField', [], {'default': "u''", 'blank': 'True'}),
604+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
605+ 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}),
606+ 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.EventType']"}),
607+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
608+ },
609+ u'maasserver.eventtype': {
610+ 'Meta': {'object_name': 'EventType'},
611+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
612+ 'description': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
613+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
614+ 'level': ('django.db.models.fields.IntegerField', [], {}),
615+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
616+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
617+ },
618+ u'maasserver.filestorage': {
619+ 'Meta': {'unique_together': "((u'filename', u'owner'),)", 'object_name': 'FileStorage'},
620+ 'content': ('metadataserver.fields.BinaryField', [], {'blank': 'True'}),
621+ 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
622+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
623+ 'key': ('django.db.models.fields.CharField', [], {'default': "u'0121a28a-6899-11e4-b353-0026c71eea0e'", 'unique': 'True', 'max_length': '36'}),
624+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
625+ },
626+ u'maasserver.largefile': {
627+ 'Meta': {'object_name': 'LargeFile'},
628+ 'content': ('maasserver.fields.LargeObjectField', [], {}),
629+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
630+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
631+ 'sha256': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'}),
632+ 'total_size': ('django.db.models.fields.BigIntegerField', [], {}),
633+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
634+ },
635+ u'maasserver.licensekey': {
636+ 'Meta': {'unique_together': "((u'osystem', u'distro_series'),)", 'object_name': 'LicenseKey'},
637+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
638+ 'distro_series': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
639+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
640+ 'license_key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
641+ 'osystem': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
642+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
643+ },
644+ u'maasserver.macaddress': {
645+ 'Meta': {'ordering': "(u'created',)", 'object_name': 'MACAddress'},
646+ 'cluster_interface': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['maasserver.NodeGroupInterface']", 'null': 'True', 'blank': 'True'}),
647+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
648+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
649+ 'ip_addresses': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.StaticIPAddress']", 'symmetrical': 'False', 'through': u"orm['maasserver.MACStaticIPAddressLink']", 'blank': 'True'}),
650+ 'mac_address': ('maasserver.fields.MACAddressField', [], {'unique': 'True'}),
651+ 'networks': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Network']", 'symmetrical': 'False', 'blank': 'True'}),
652+ 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}),
653+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
654+ },
655+ u'maasserver.macstaticipaddresslink': {
656+ 'Meta': {'unique_together': "((u'ip_address', u'mac_address'),)", 'object_name': 'MACStaticIPAddressLink'},
657+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
658+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
659+ 'ip_address': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.StaticIPAddress']", 'unique': 'True'}),
660+ 'mac_address': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.MACAddress']"}),
661+ 'nic_alias': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
662+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
663+ },
664+ u'maasserver.network': {
665+ 'Meta': {'object_name': 'Network'},
666+ 'default_gateway': ('maasserver.fields.MAASIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
667+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
668+ 'dns_servers': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
669+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
670+ 'ip': ('maasserver.fields.MAASIPAddressField', [], {'unique': 'True', 'max_length': '39'}),
671+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
672+ 'netmask': ('maasserver.fields.MAASIPAddressField', [], {'max_length': '39'}),
673+ 'vlan_tag': ('django.db.models.fields.PositiveSmallIntegerField', [], {'unique': 'True', 'null': 'True', 'blank': 'True'})
674+ },
675+ u'maasserver.node': {
676+ 'Meta': {'object_name': 'Node'},
677+ 'agent_name': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'null': 'True', 'blank': 'True'}),
678+ 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '31'}),
679+ 'boot_type': ('django.db.models.fields.CharField', [], {'default': "u'fastpath'", 'max_length': '20'}),
680+ 'cpu_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
681+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
682+ 'disable_ipv4': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
683+ 'distro_series': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'blank': 'True'}),
684+ 'error': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
685+ 'error_description': ('django.db.models.fields.TextField', [], {'default': "u''", 'blank': 'True'}),
686+ 'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'unique': 'True', 'max_length': '255', 'blank': 'True'}),
687+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
688+ 'license_key': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}),
689+ 'memory': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
690+ 'netboot': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
691+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']", 'null': 'True'}),
692+ 'osystem': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'blank': 'True'}),
693+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}),
694+ 'power_parameters': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
695+ 'power_state': ('django.db.models.fields.CharField', [], {'default': "u'unknown'", 'max_length': '10'}),
696+ 'power_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '10', 'blank': 'True'}),
697+ 'pxe_mac': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "u'+'", 'null': 'True', 'blank': 'True', 'to': u"orm['maasserver.MACAddress']"}),
698+ 'routers': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'macaddr'", 'null': 'True', 'blank': 'True'}),
699+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '10'}),
700+ 'storage': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
701+ 'system_id': ('django.db.models.fields.CharField', [], {'default': "u'node-012616a8-6899-11e4-b353-0026c71eea0e'", 'unique': 'True', 'max_length': '41'}),
702+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Tag']", 'symmetrical': 'False'}),
703+ 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Token']", 'null': 'True'}),
704+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
705+ 'zone': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Zone']", 'on_delete': 'models.SET_DEFAULT'})
706+ },
707+ u'maasserver.nodegroup': {
708+ 'Meta': {'object_name': 'NodeGroup'},
709+ 'api_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '18'}),
710+ 'api_token': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Token']", 'unique': 'True'}),
711+ 'cluster_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100', 'blank': 'True'}),
712+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
713+ 'default_disable_ipv4': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
714+ 'dhcp_key': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
715+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
716+ 'maas_url': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
717+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}),
718+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
719+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
720+ 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'})
721+ },
722+ u'maasserver.nodegroupinterface': {
723+ 'Meta': {'unique_together': "((u'nodegroup', u'name'),)", 'object_name': 'NodeGroupInterface'},
724+ 'broadcast_ip': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
725+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
726+ 'foreign_dhcp_ip': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
727+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
728+ 'interface': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
729+ 'ip': ('maasserver.fields.MAASIPAddressField', [], {'max_length': '39'}),
730+ 'ip_range_high': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
731+ 'ip_range_low': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
732+ 'management': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
733+ 'name': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
734+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
735+ 'router_ip': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
736+ 'static_ip_range_high': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
737+ 'static_ip_range_low': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
738+ 'subnet_mask': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
739+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
740+ },
741+ u'maasserver.sshkey': {
742+ 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSHKey'},
743+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
744+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
745+ 'key': ('django.db.models.fields.TextField', [], {}),
746+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
747+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
748+ },
749+ u'maasserver.sslkey': {
750+ 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSLKey'},
751+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
752+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
753+ 'key': ('django.db.models.fields.TextField', [], {}),
754+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
755+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
756+ },
757+ u'maasserver.staticipaddress': {
758+ 'Meta': {'object_name': 'StaticIPAddress'},
759+ 'alloc_type': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
760+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
761+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
762+ 'ip': ('maasserver.fields.MAASIPAddressField', [], {'unique': 'True', 'max_length': '39'}),
763+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
764+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
765+ },
766+ u'maasserver.tag': {
767+ 'Meta': {'object_name': 'Tag'},
768+ 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
769+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
770+ 'definition': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
771+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
772+ 'kernel_opts': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
773+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
774+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
775+ },
776+ u'maasserver.userprofile': {
777+ 'Meta': {'object_name': 'UserProfile'},
778+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
779+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'})
780+ },
781+ u'maasserver.zone': {
782+ 'Meta': {'ordering': "[u'name']", 'object_name': 'Zone'},
783+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
784+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
785+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
786+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
787+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
788+ },
789+ u'piston.consumer': {
790+ 'Meta': {'object_name': 'Consumer'},
791+ 'description': ('django.db.models.fields.TextField', [], {}),
792+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
793+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
794+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
795+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
796+ 'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '16'}),
797+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'consumers'", 'null': 'True', 'to': u"orm['auth.User']"})
798+ },
799+ u'piston.token': {
800+ 'Meta': {'object_name': 'Token'},
801+ 'callback': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
802+ 'callback_confirmed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
803+ 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Consumer']"}),
804+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
805+ 'is_approved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
806+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
807+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
808+ 'timestamp': ('django.db.models.fields.IntegerField', [], {'default': '1415596707L'}),
809+ 'token_type': ('django.db.models.fields.IntegerField', [], {}),
810+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'tokens'", 'null': 'True', 'to': u"orm['auth.User']"}),
811+ 'verifier': ('django.db.models.fields.CharField', [], {'max_length': '10'})
812+ }
813+ }
814+
815+ complete_apps = ['maasserver']
816
817=== modified file 'src/maasserver/models/bootsourceselection.py'
818--- src/maasserver/models/bootsourceselection.py 2014-08-25 14:08:13 +0000
819+++ src/maasserver/models/bootsourceselection.py 2014-11-12 00:21:52 +0000
820@@ -37,6 +37,7 @@
821
822 class Meta(DefaultMeta):
823 """Needed for South to recognize this model."""
824+ unique_together = ("boot_source", "os", "release")
825
826 objects = BootSourceSelectionManager()
827
828
829=== modified file 'src/maasserver/tests/test_forms_bootsourceselection.py'
830--- src/maasserver/tests/test_forms_bootsourceselection.py 2014-09-10 16:20:31 +0000
831+++ src/maasserver/tests/test_forms_bootsourceselection.py 2014-11-12 00:21:52 +0000
832@@ -14,6 +14,7 @@
833 __metaclass__ = type
834 __all__ = []
835
836+from django.core.exceptions import ValidationError
837 from maasserver.forms import BootSourceSelectionForm
838 from maasserver.testing.factory import factory
839 from maasserver.testing.orm import reload_object
840@@ -54,3 +55,27 @@
841 self.assertTrue(form.is_valid(), form._errors)
842 boot_source_selection = form.save()
843 self.assertAttributes(boot_source_selection, params)
844+
845+ def test_cannot_create_duplicate_entry(self):
846+ boot_source = factory.make_BootSource()
847+ params = {
848+ 'os': factory.make_name('os'),
849+ 'release': factory.make_name('release'),
850+ 'arches': [factory.make_name('arch'), factory.make_name('arch')],
851+ 'subarches': [
852+ factory.make_name('subarch'), factory.make_name('subarch')],
853+ 'labels': [factory.make_name('label'), factory.make_name('label')],
854+ }
855+ form = BootSourceSelectionForm(boot_source=boot_source, data=params)
856+ self.assertTrue(form.is_valid(), form._errors)
857+ form.save()
858+
859+ # Duplicates should be detected for the same boot_source, os and
860+ # release, the other fields are irrelevant.
861+ dup_params = {
862+ 'os': params['os'],
863+ 'release': params['release'],
864+ }
865+ form = BootSourceSelectionForm(
866+ boot_source=boot_source, data=dup_params)
867+ self.assertRaises(ValidationError, form.save)