Merge lp:~jtv/maas/bug-1069734 into lp:maas/trunk

Proposed by Jeroen T. Vermeulen on 2012-10-26
Status: Merged
Approved by: Jeroen T. Vermeulen on 2012-10-26
Approved revision: 1310
Merged at revision: 1310
Proposed branch: lp:~jtv/maas/bug-1069734
Merge into: lp:maas/trunk
Diff against target: 1191 lines (+690/-305)
12 files modified
etc/cron.d/maas-gc (+2/-6)
setup.py (+0/-2)
src/maasserver/api.py (+1/-1)
src/maasserver/management/commands/gc.py (+0/-24)
src/maasserver/migrations/0039_add_filestorage_content.py (+233/-0)
src/maasserver/migrations/0040_make_filestorage_data_not_null.py (+203/-0)
src/maasserver/migrations/0041_remove_filestorage_data.py (+213/-0)
src/maasserver/models/filestorage.py (+15/-92)
src/maasserver/testing/factory.py (+4/-4)
src/maasserver/tests/test_api.py (+4/-2)
src/maasserver/tests/test_commands.py (+0/-11)
src/maasserver/tests/test_filestorage.py (+15/-163)
To merge this branch: bzr merge lp:~jtv/maas/bug-1069734
Reviewer Review Type Date Requested Status
John A Meinel 2012-10-26 Approve on 2012-10-26
Review via email: mp+131551@code.launchpad.net

Commit message

Forward-port 1.2 r1272 to trunk: move region file storage into the database.

Description of the change

This fixes a problem that was breaking region-controller setups that had multiple instances of the app server. The fix was originally written against, and landed on, the 1.2 branch (for Quantal). It was forward-ported to trunk without changes. Even the sequence numbers on the database migrations still fit.

Jeroen

To post a comment you must log in.
John A Meinel (jameinel) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'etc/cron.d/maas-gc'
2--- etc/cron.d/maas-gc 2012-08-03 16:33:05 +0000
3+++ etc/cron.d/maas-gc 2012-10-26 08:37:31 +0000
4@@ -1,8 +1,4 @@
5 # Perform daily background cleanups in MAAS.
6 #
7-# The "maas gc" command is for garbage-collection, such as deleting uploaded
8-# files from Juju's file storage API, and in the future commissioning logs,
9-# that have been superseded by newer ones. (This isn't done immediately
10-# when the files are overwritten because (1) the transaction that overwrites
11-# them may fail, and (2) a file may still be in use when it's overwritten.)
12-0 0 * * * root /usr/sbin/maas gc &> /dev/null
13+# This currently does nothing: maas-gc is no longer needed.
14+#0 0 * * * root /usr/sbin/maas gc &> /dev/null
15
16=== modified file 'setup.py'
17--- setup.py 2012-10-01 22:56:46 +0000
18+++ setup.py 2012-10-26 08:37:31 +0000
19@@ -65,8 +65,6 @@
20 'etc/maas/commissioning-user-data',
21 'contrib/maas-http.conf',
22 'contrib/maas_local_settings.py']),
23- ('/etc/cron.d',
24- ['etc/cron.d/maas-gc']),
25 ('/usr/share/maas',
26 ['contrib/wsgi.py',
27 'etc/celeryconfig.py',
28
29=== modified file 'src/maasserver/api.py'
30--- src/maasserver/api.py 2012-10-26 07:59:12 +0000
31+++ src/maasserver/api.py 2012-10-26 08:37:31 +0000
32@@ -985,7 +985,7 @@
33 db_file = FileStorage.objects.get(filename=filename)
34 except FileStorage.DoesNotExist:
35 raise MAASAPINotFound("File not found")
36- return HttpResponse(db_file.data.read(), status=httplib.OK)
37+ return HttpResponse(db_file.content, status=httplib.OK)
38
39
40 class AnonFilesHandler(AnonymousOperationsHandler):
41
42=== removed file 'src/maasserver/management/commands/gc.py'
43--- src/maasserver/management/commands/gc.py 2012-04-16 10:00:51 +0000
44+++ src/maasserver/management/commands/gc.py 1970-01-01 00:00:00 +0000
45@@ -1,24 +0,0 @@
46-# Copyright 2012 Canonical Ltd. This software is licensed under the
47-# GNU Affero General Public License version 3 (see the file LICENSE).
48-
49-"""Custom django command: garabge-collect."""
50-
51-from __future__ import (
52- absolute_import,
53- print_function,
54- unicode_literals,
55- )
56-
57-__metaclass__ = type
58-__all__ = [
59- 'Command',
60- ]
61-
62-
63-from django.core.management.base import BaseCommand
64-from maasserver.models import FileStorage
65-
66-
67-class Command(BaseCommand):
68- def handle(self, *args, **options):
69- FileStorage.objects.collect_garbage()
70
71=== added file 'src/maasserver/migrations/0039_add_filestorage_content.py'
72--- src/maasserver/migrations/0039_add_filestorage_content.py 1970-01-01 00:00:00 +0000
73+++ src/maasserver/migrations/0039_add_filestorage_content.py 2012-10-26 08:37:31 +0000
74@@ -0,0 +1,233 @@
75+# -*- coding: utf-8 -*-
76+from base64 import b64encode
77+import datetime
78+import os.path
79+
80+from django.conf import settings
81+from django.db import models
82+from south.db import db
83+from south.v2 import SchemaMigration
84+
85+
86+def get_unmigrated_filestorages(orm):
87+ """Find FileStorage objects whose data needs migrating."""
88+ return orm['maasserver.FileStorage'].objects.filter(content=None)
89+
90+
91+def read_file(storage):
92+ """Read file contents from a FileStorage."""
93+ return storage.data.read()
94+
95+
96+def copy_files_into_database(orm):
97+ """Copy file contents into the "content" field."""
98+ for storage in get_unmigrated_filestorages(orm):
99+ raw_content = read_file(storage)
100+ storage.content = b64encode(raw_content).decode('ascii')
101+ storage.save()
102+
103+
104+class Migration(SchemaMigration):
105+
106+ def forwards(self, orm):
107+ # Adding field 'FileStorage.content'
108+ db.add_column(u'maasserver_filestorage', 'content',
109+ self.gf('metadataserver.fields.BinaryField')(null=True),
110+ keep_default=False)
111+
112+ # Changing field 'FileStorage.filename'
113+ db.alter_column(u'maasserver_filestorage', 'filename', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255))
114+
115+ # Effecting data migration. Not deleting the old files yet; the
116+ # database transaction might still abort for whatever reason.
117+ copy_files_into_database(orm)
118+
119+ def backwards(self, orm):
120+ # Deleting field 'FileStorage.content'
121+ db.delete_column(u'maasserver_filestorage', 'content')
122+
123+
124+ # Changing field 'FileStorage.filename'
125+ db.alter_column(u'maasserver_filestorage', 'filename', self.gf('django.db.models.fields.CharField')(max_length=200, unique=True))
126+
127+ models = {
128+ 'auth.group': {
129+ 'Meta': {'object_name': 'Group'},
130+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
131+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
132+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
133+ },
134+ 'auth.permission': {
135+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
136+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
137+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
138+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
139+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
140+ },
141+ 'auth.user': {
142+ 'Meta': {'object_name': 'User'},
143+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
144+ 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75', 'blank': 'True'}),
145+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
146+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
147+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
148+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
149+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
150+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
151+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
152+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
153+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
154+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
155+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
156+ },
157+ 'contenttypes.contenttype': {
158+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
159+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
160+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
161+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
162+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
163+ },
164+ u'maasserver.bootimage': {
165+ 'Meta': {'unique_together': "((u'architecture', u'subarchitecture', u'release', u'purpose'),)", 'object_name': 'BootImage'},
166+ 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
167+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
168+ 'purpose': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
169+ 'release': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
170+ 'subarchitecture': ('django.db.models.fields.CharField', [], {'max_length': '255'})
171+ },
172+ u'maasserver.componenterror': {
173+ 'Meta': {'object_name': 'ComponentError'},
174+ 'component': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
175+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
176+ 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000'}),
177+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
178+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
179+ },
180+ u'maasserver.config': {
181+ 'Meta': {'object_name': 'Config'},
182+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
183+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
184+ 'value': ('maasserver.fields.JSONObjectField', [], {'null': 'True'})
185+ },
186+ u'maasserver.dhcplease': {
187+ 'Meta': {'object_name': 'DHCPLease'},
188+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
189+ 'ip': ('django.db.models.fields.IPAddressField', [], {'unique': 'True', 'max_length': '15'}),
190+ 'mac': ('maasserver.fields.MACAddressField', [], {}),
191+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"})
192+ },
193+ u'maasserver.filestorage': {
194+ 'Meta': {'object_name': 'FileStorage'},
195+ 'content': ('metadataserver.fields.BinaryField', [], {'null': 'True'}),
196+ 'data': ('django.db.models.fields.files.FileField', [], {'max_length': '255'}),
197+ 'filename': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
198+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
199+ },
200+ u'maasserver.macaddress': {
201+ 'Meta': {'object_name': 'MACAddress'},
202+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
203+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
204+ 'mac_address': ('maasserver.fields.MACAddressField', [], {'unique': 'True'}),
205+ 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}),
206+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
207+ },
208+ u'maasserver.node': {
209+ 'Meta': {'object_name': 'Node'},
210+ 'after_commissioning_action': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
211+ 'architecture': ('django.db.models.fields.CharField', [], {'default': "u'i386/generic'", 'max_length': '31'}),
212+ 'cpu_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
213+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
214+ 'distro_series': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '10', 'null': 'True', 'blank': 'True'}),
215+ 'error': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
216+ 'hardware_details': ('maasserver.fields.XMLField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
217+ 'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
218+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
219+ 'memory': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
220+ 'netboot': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
221+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']", 'null': 'True'}),
222+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
223+ 'power_parameters': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
224+ 'power_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '10', 'blank': 'True'}),
225+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '10'}),
226+ 'system_id': ('django.db.models.fields.CharField', [], {'default': "u'node-b588ce50-1ea0-11e2-946f-002608dc6120'", 'unique': 'True', 'max_length': '41'}),
227+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Tag']", 'symmetrical': 'False'}),
228+ 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'null': 'True'}),
229+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
230+ },
231+ u'maasserver.nodegroup': {
232+ 'Meta': {'object_name': 'NodeGroup'},
233+ 'api_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '18'}),
234+ 'api_token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'unique': 'True'}),
235+ 'cluster_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100', 'blank': 'True'}),
236+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
237+ 'dhcp_key': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
238+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
239+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}),
240+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
241+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
242+ 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'})
243+ },
244+ u'maasserver.nodegroupinterface': {
245+ 'Meta': {'unique_together': "((u'nodegroup', u'interface'),)", 'object_name': 'NodeGroupInterface'},
246+ 'broadcast_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
247+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
248+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
249+ 'interface': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
250+ 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
251+ 'ip_range_high': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
252+ 'ip_range_low': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
253+ 'management': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
254+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
255+ 'router_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
256+ 'subnet_mask': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
257+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
258+ },
259+ u'maasserver.sshkey': {
260+ 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSHKey'},
261+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
262+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
263+ 'key': ('django.db.models.fields.TextField', [], {}),
264+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
265+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
266+ },
267+ u'maasserver.tag': {
268+ 'Meta': {'object_name': 'Tag'},
269+ 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
270+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
271+ 'definition': ('django.db.models.fields.TextField', [], {}),
272+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
273+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
274+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
275+ },
276+ u'maasserver.userprofile': {
277+ 'Meta': {'object_name': 'UserProfile'},
278+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
279+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
280+ },
281+ 'piston.consumer': {
282+ 'Meta': {'object_name': 'Consumer'},
283+ 'description': ('django.db.models.fields.TextField', [], {}),
284+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
285+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
286+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
287+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
288+ 'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '16'}),
289+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'consumers'", 'null': 'True', 'to': "orm['auth.User']"})
290+ },
291+ 'piston.token': {
292+ 'Meta': {'object_name': 'Token'},
293+ 'callback': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
294+ 'callback_confirmed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
295+ 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Consumer']"}),
296+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
297+ 'is_approved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
298+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
299+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
300+ 'timestamp': ('django.db.models.fields.IntegerField', [], {'default': '1351168636L'}),
301+ 'token_type': ('django.db.models.fields.IntegerField', [], {}),
302+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'tokens'", 'null': 'True', 'to': "orm['auth.User']"}),
303+ 'verifier': ('django.db.models.fields.CharField', [], {'max_length': '10'})
304+ }
305+ }
306+
307+ complete_apps = ['maasserver']
308
309=== added file 'src/maasserver/migrations/0040_make_filestorage_data_not_null.py'
310--- src/maasserver/migrations/0040_make_filestorage_data_not_null.py 1970-01-01 00:00:00 +0000
311+++ src/maasserver/migrations/0040_make_filestorage_data_not_null.py 2012-10-26 08:37:31 +0000
312@@ -0,0 +1,203 @@
313+# -*- coding: utf-8 -*-
314+import datetime
315+
316+from django.db import models
317+from south.db import db
318+from south.v2 import SchemaMigration
319+
320+
321+class Migration(SchemaMigration):
322+
323+ def forwards(self, orm):
324+
325+ # Changing field 'FileStorage.content'
326+ # Disallow NULLs. The previous migration should have
327+ # initialized the column for all existing rows.
328+ db.alter_column(u'maasserver_filestorage', 'content', self.gf('metadataserver.fields.BinaryField')(null=False))
329+
330+ def backwards(self, orm):
331+
332+ # Changing field 'FileStorage.content'
333+ db.alter_column(u'maasserver_filestorage', 'content', self.gf('metadataserver.fields.BinaryField')(null=True))
334+
335+ models = {
336+ 'auth.group': {
337+ 'Meta': {'object_name': 'Group'},
338+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
339+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
340+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
341+ },
342+ 'auth.permission': {
343+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
344+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
345+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
346+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
347+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
348+ },
349+ 'auth.user': {
350+ 'Meta': {'object_name': 'User'},
351+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
352+ 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75', 'blank': 'True'}),
353+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
354+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
355+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
356+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
357+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
358+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
359+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
360+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
361+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
362+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
363+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
364+ },
365+ 'contenttypes.contenttype': {
366+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
367+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
368+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
369+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
370+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
371+ },
372+ u'maasserver.bootimage': {
373+ 'Meta': {'unique_together': "((u'architecture', u'subarchitecture', u'release', u'purpose'),)", 'object_name': 'BootImage'},
374+ 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
375+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
376+ 'purpose': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
377+ 'release': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
378+ 'subarchitecture': ('django.db.models.fields.CharField', [], {'max_length': '255'})
379+ },
380+ u'maasserver.componenterror': {
381+ 'Meta': {'object_name': 'ComponentError'},
382+ 'component': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
383+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
384+ 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000'}),
385+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
386+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
387+ },
388+ u'maasserver.config': {
389+ 'Meta': {'object_name': 'Config'},
390+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
391+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
392+ 'value': ('maasserver.fields.JSONObjectField', [], {'null': 'True'})
393+ },
394+ u'maasserver.dhcplease': {
395+ 'Meta': {'object_name': 'DHCPLease'},
396+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
397+ 'ip': ('django.db.models.fields.IPAddressField', [], {'unique': 'True', 'max_length': '15'}),
398+ 'mac': ('maasserver.fields.MACAddressField', [], {}),
399+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"})
400+ },
401+ u'maasserver.filestorage': {
402+ 'Meta': {'object_name': 'FileStorage'},
403+ 'content': ('metadataserver.fields.BinaryField', [], {}),
404+ 'data': ('django.db.models.fields.files.FileField', [], {'max_length': '255'}),
405+ 'filename': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
406+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
407+ },
408+ u'maasserver.macaddress': {
409+ 'Meta': {'object_name': 'MACAddress'},
410+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
411+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
412+ 'mac_address': ('maasserver.fields.MACAddressField', [], {'unique': 'True'}),
413+ 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}),
414+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
415+ },
416+ u'maasserver.node': {
417+ 'Meta': {'object_name': 'Node'},
418+ 'after_commissioning_action': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
419+ 'architecture': ('django.db.models.fields.CharField', [], {'default': "u'i386/generic'", 'max_length': '31'}),
420+ 'cpu_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
421+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
422+ 'distro_series': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '10', 'null': 'True', 'blank': 'True'}),
423+ 'error': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
424+ 'hardware_details': ('maasserver.fields.XMLField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
425+ 'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
426+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
427+ 'memory': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
428+ 'netboot': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
429+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']", 'null': 'True'}),
430+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
431+ 'power_parameters': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
432+ 'power_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '10', 'blank': 'True'}),
433+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '10'}),
434+ 'system_id': ('django.db.models.fields.CharField', [], {'default': "u'node-766d0f92-1ea5-11e2-9dee-002608dc6120'", 'unique': 'True', 'max_length': '41'}),
435+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Tag']", 'symmetrical': 'False'}),
436+ 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'null': 'True'}),
437+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
438+ },
439+ u'maasserver.nodegroup': {
440+ 'Meta': {'object_name': 'NodeGroup'},
441+ 'api_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '18'}),
442+ 'api_token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'unique': 'True'}),
443+ 'cluster_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100', 'blank': 'True'}),
444+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
445+ 'dhcp_key': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
446+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
447+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}),
448+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
449+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
450+ 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'})
451+ },
452+ u'maasserver.nodegroupinterface': {
453+ 'Meta': {'unique_together': "((u'nodegroup', u'interface'),)", 'object_name': 'NodeGroupInterface'},
454+ 'broadcast_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
455+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
456+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
457+ 'interface': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
458+ 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
459+ 'ip_range_high': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
460+ 'ip_range_low': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
461+ 'management': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
462+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
463+ 'router_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
464+ 'subnet_mask': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
465+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
466+ },
467+ u'maasserver.sshkey': {
468+ 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSHKey'},
469+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
470+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
471+ 'key': ('django.db.models.fields.TextField', [], {}),
472+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
473+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
474+ },
475+ u'maasserver.tag': {
476+ 'Meta': {'object_name': 'Tag'},
477+ 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
478+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
479+ 'definition': ('django.db.models.fields.TextField', [], {}),
480+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
481+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
482+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
483+ },
484+ u'maasserver.userprofile': {
485+ 'Meta': {'object_name': 'UserProfile'},
486+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
487+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
488+ },
489+ 'piston.consumer': {
490+ 'Meta': {'object_name': 'Consumer'},
491+ 'description': ('django.db.models.fields.TextField', [], {}),
492+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
493+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
494+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
495+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
496+ 'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '16'}),
497+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'consumers'", 'null': 'True', 'to': "orm['auth.User']"})
498+ },
499+ 'piston.token': {
500+ 'Meta': {'object_name': 'Token'},
501+ 'callback': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
502+ 'callback_confirmed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
503+ 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Consumer']"}),
504+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
505+ 'is_approved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
506+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
507+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
508+ 'timestamp': ('django.db.models.fields.IntegerField', [], {'default': '1351170650L'}),
509+ 'token_type': ('django.db.models.fields.IntegerField', [], {}),
510+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'tokens'", 'null': 'True', 'to': "orm['auth.User']"}),
511+ 'verifier': ('django.db.models.fields.CharField', [], {'max_length': '10'})
512+ }
513+ }
514+
515+ complete_apps = ['maasserver']
516
517=== added file 'src/maasserver/migrations/0041_remove_filestorage_data.py'
518--- src/maasserver/migrations/0041_remove_filestorage_data.py 1970-01-01 00:00:00 +0000
519+++ src/maasserver/migrations/0041_remove_filestorage_data.py 2012-10-26 08:37:31 +0000
520@@ -0,0 +1,213 @@
521+# -*- coding: utf-8 -*-
522+import datetime
523+import os.path
524+from shutil import rmtree
525+
526+from django.conf import settings
527+from django.db import models
528+from south.db import db
529+from south.v2 import SchemaMigration
530+
531+# The sub-directory of MEDIA_ROOT where FileStorage used to store its
532+# files. This duplicates the value of the now-removed
533+# FileStorage.upload_dir.
534+upload_dir = 'storage'
535+
536+
537+class Migration(SchemaMigration):
538+
539+ def forwards(self, orm):
540+ # Deleting field 'FileStorage.data'
541+ db.delete_column(u'maasserver_filestorage', 'data')
542+
543+ # Cleaning up any obsolete FileStorage files.
544+ if settings.MEDIA_ROOT and os.path.isdir(settings.MEDIA_ROOT):
545+ rmtree(
546+ os.path.join(settings.MEDIA_ROOT, upload_dir),
547+ ignore_errors=True)
548+
549+ def backwards(self, orm):
550+
551+ # User chose to not deal with backwards NULL issues for 'FileStorage.data'
552+ raise RuntimeError("Cannot reverse this migration. 'FileStorage.data' and its values cannot be restored.")
553+
554+ models = {
555+ 'auth.group': {
556+ 'Meta': {'object_name': 'Group'},
557+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
558+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
559+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
560+ },
561+ 'auth.permission': {
562+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
563+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
564+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
565+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
566+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
567+ },
568+ 'auth.user': {
569+ 'Meta': {'object_name': 'User'},
570+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
571+ 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75', 'blank': 'True'}),
572+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
573+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
574+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
575+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
576+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
577+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
578+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
579+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
580+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
581+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
582+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
583+ },
584+ 'contenttypes.contenttype': {
585+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
586+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
587+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
588+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
589+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
590+ },
591+ u'maasserver.bootimage': {
592+ 'Meta': {'unique_together': "((u'architecture', u'subarchitecture', u'release', u'purpose'),)", 'object_name': 'BootImage'},
593+ 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
594+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
595+ 'purpose': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
596+ 'release': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
597+ 'subarchitecture': ('django.db.models.fields.CharField', [], {'max_length': '255'})
598+ },
599+ u'maasserver.componenterror': {
600+ 'Meta': {'object_name': 'ComponentError'},
601+ 'component': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
602+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
603+ 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000'}),
604+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
605+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
606+ },
607+ u'maasserver.config': {
608+ 'Meta': {'object_name': 'Config'},
609+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
610+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
611+ 'value': ('maasserver.fields.JSONObjectField', [], {'null': 'True'})
612+ },
613+ u'maasserver.dhcplease': {
614+ 'Meta': {'object_name': 'DHCPLease'},
615+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
616+ 'ip': ('django.db.models.fields.IPAddressField', [], {'unique': 'True', 'max_length': '15'}),
617+ 'mac': ('maasserver.fields.MACAddressField', [], {}),
618+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"})
619+ },
620+ u'maasserver.filestorage': {
621+ 'Meta': {'object_name': 'FileStorage'},
622+ 'content': ('metadataserver.fields.BinaryField', [], {}),
623+ 'filename': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
624+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
625+ },
626+ u'maasserver.macaddress': {
627+ 'Meta': {'object_name': 'MACAddress'},
628+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
629+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
630+ 'mac_address': ('maasserver.fields.MACAddressField', [], {'unique': 'True'}),
631+ 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}),
632+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
633+ },
634+ u'maasserver.node': {
635+ 'Meta': {'object_name': 'Node'},
636+ 'after_commissioning_action': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
637+ 'architecture': ('django.db.models.fields.CharField', [], {'default': "u'i386/generic'", 'max_length': '31'}),
638+ 'cpu_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
639+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
640+ 'distro_series': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '10', 'null': 'True', 'blank': 'True'}),
641+ 'error': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
642+ 'hardware_details': ('maasserver.fields.XMLField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
643+ 'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
644+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
645+ 'memory': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
646+ 'netboot': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
647+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']", 'null': 'True'}),
648+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
649+ 'power_parameters': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
650+ 'power_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '10', 'blank': 'True'}),
651+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '10'}),
652+ 'system_id': ('django.db.models.fields.CharField', [], {'default': "u'node-da71dfcc-1ea5-11e2-8763-002608dc6120'", 'unique': 'True', 'max_length': '41'}),
653+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Tag']", 'symmetrical': 'False'}),
654+ 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'null': 'True'}),
655+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
656+ },
657+ u'maasserver.nodegroup': {
658+ 'Meta': {'object_name': 'NodeGroup'},
659+ 'api_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '18'}),
660+ 'api_token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'unique': 'True'}),
661+ 'cluster_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100', 'blank': 'True'}),
662+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
663+ 'dhcp_key': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
664+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
665+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}),
666+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
667+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
668+ 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'})
669+ },
670+ u'maasserver.nodegroupinterface': {
671+ 'Meta': {'unique_together': "((u'nodegroup', u'interface'),)", 'object_name': 'NodeGroupInterface'},
672+ 'broadcast_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
673+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
674+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
675+ 'interface': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
676+ 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
677+ 'ip_range_high': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
678+ 'ip_range_low': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
679+ 'management': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
680+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
681+ 'router_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
682+ 'subnet_mask': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
683+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
684+ },
685+ u'maasserver.sshkey': {
686+ 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSHKey'},
687+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
688+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
689+ 'key': ('django.db.models.fields.TextField', [], {}),
690+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
691+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
692+ },
693+ u'maasserver.tag': {
694+ 'Meta': {'object_name': 'Tag'},
695+ 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
696+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
697+ 'definition': ('django.db.models.fields.TextField', [], {}),
698+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
699+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
700+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
701+ },
702+ u'maasserver.userprofile': {
703+ 'Meta': {'object_name': 'UserProfile'},
704+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
705+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
706+ },
707+ 'piston.consumer': {
708+ 'Meta': {'object_name': 'Consumer'},
709+ 'description': ('django.db.models.fields.TextField', [], {}),
710+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
711+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
712+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
713+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
714+ 'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '16'}),
715+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'consumers'", 'null': 'True', 'to': "orm['auth.User']"})
716+ },
717+ 'piston.token': {
718+ 'Meta': {'object_name': 'Token'},
719+ 'callback': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
720+ 'callback_confirmed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
721+ 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Consumer']"}),
722+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
723+ 'is_approved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
724+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
725+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
726+ 'timestamp': ('django.db.models.fields.IntegerField', [], {'default': '1351170840L'}),
727+ 'token_type': ('django.db.models.fields.IntegerField', [], {}),
728+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'tokens'", 'null': 'True', 'to': "orm['auth.User']"}),
729+ 'verifier': ('django.db.models.fields.CharField', [], {'max_length': '10'})
730+ }
731+ }
732+
733+ complete_apps = ['maasserver']
734
735=== modified file 'src/maasserver/models/filestorage.py'
736--- src/maasserver/models/filestorage.py 2012-08-24 10:28:29 +0000
737+++ src/maasserver/models/filestorage.py 2012-10-26 08:37:31 +0000
738@@ -15,22 +15,17 @@
739 ]
740
741
742-from errno import ENOENT
743-import os
744-import time
745-
746-from django.conf import settings
747-from django.core.files.base import ContentFile
748-from django.core.files.storage import FileSystemStorage
749 from django.db.models import (
750 CharField,
751- FileField,
752 Manager,
753 Model,
754 )
755 from maasserver import DefaultMeta
756 from maasserver.models.cleansave import CleanSave
757-from maasserver.utils.orm import get_one
758+from metadataserver.fields import (
759+ Bin,
760+ BinaryField,
761+ )
762
763
764 class FileStorageManager(Manager):
765@@ -47,108 +42,36 @@
766 original file will not be affected. Also, any ongoing reads from the
767 old file will continue without iterruption.
768 """
769- # The time, in seconds, that an unreferenced file is allowed to
770- # persist in order to satisfy ongoing requests.
771- grace_time = 12 * 60 * 60
772-
773- def get_existing_storage(self, filename):
774- """Return an existing `FileStorage` of this name, or None."""
775- return get_one(self.filter(filename=filename))
776
777 def save_file(self, filename, file_object):
778- """Save the file to the filesystem and persist to the database.
779-
780- The file will end up in MEDIA_ROOT/storage/
781+ """Save the file to the database.
782
783 If a file of that name already existed, it will be replaced by the
784 new contents.
785 """
786 # This probably ought to read in chunks but large files are
787- # not expected. Also note that uploading a file with the same
788- # name as an existing one will cause that file to be written
789- # with a new generated name, and the old one remains where it
790- # is. See https://code.djangoproject.com/ticket/6157 - the
791- # Django devs consider deleting things dangerous ... ha.
792- # HOWEVER - this operation would need to be atomic anyway so
793- # it's safest left how it is for now (reads can overlap with
794- # writes from Juju).
795- content = ContentFile(file_object.read())
796-
797- storage = self.get_existing_storage(filename)
798- if storage is None:
799- storage = FileStorage(filename=filename)
800- storage.data.save(filename, content)
801+ # not expected.
802+ content = Bin(file_object.read())
803+ storage, created = self.get_or_create(
804+ filename=filename, defaults={'content': content})
805+ if not created:
806+ storage.content = content
807+ storage.save()
808 return storage
809
810- def list_stored_files(self):
811- """Find the files stored in the filesystem."""
812- dirs, files = FileStorage.storage.listdir(FileStorage.upload_dir)
813- return [
814- os.path.join(FileStorage.upload_dir, filename)
815- for filename in files]
816-
817- def list_referenced_files(self):
818- """Find the names of files that are referenced from `FileStorage`.
819-
820- :return: All file paths within MEDIA ROOT (relative to MEDIA_ROOT)
821- that have `FileStorage` entries referencing them.
822- :rtype: frozenset
823- """
824- return frozenset(
825- file_storage.data.name
826- for file_storage in self.all())
827-
828- def is_old(self, storage_filename):
829- """Is the named file in the filesystem storage old enough to be dead?
830-
831- :param storage_filename: The name under which the file is stored in
832- the filesystem, relative to MEDIA_ROOT. This need not be the
833- same name as its filename as stored in the `FileStorage` object.
834- It includes the name of the upload directory.
835- """
836- file_path = os.path.join(settings.MEDIA_ROOT, storage_filename)
837- mtime = os.stat(file_path).st_mtime
838- expiry = mtime + self.grace_time
839- return expiry <= time.time()
840-
841- def collect_garbage(self):
842- """Clean up stored files that are no longer accessible."""
843- # Avoid circular imports.
844- from maasserver.models import logger
845-
846- try:
847- stored_files = self.list_stored_files()
848- except OSError as e:
849- if e.errno != ENOENT:
850- raise
851- logger.info(
852- "Upload directory does not exist yet. "
853- "Skipping garbage collection.")
854- return
855- referenced_files = self.list_referenced_files()
856- for path in stored_files:
857- if path not in referenced_files and self.is_old(path):
858- FileStorage.storage.delete(path)
859-
860
861 class FileStorage(CleanSave, Model):
862 """A simple file storage keyed on file name.
863
864 :ivar filename: A unique file name to use for the data being stored.
865- :ivar data: The file's actual data.
866+ :ivar content: The file's actual data.
867 """
868
869 class Meta(DefaultMeta):
870 """Needed for South to recognize this model."""
871
872- storage = FileSystemStorage()
873-
874- upload_dir = "storage"
875-
876- # Unix filenames can be longer than this (e.g. 255 bytes), but leave
877- # some extra room for the full path, as well as a versioning suffix.
878- filename = CharField(max_length=200, unique=True, editable=False)
879- data = FileField(upload_to=upload_dir, storage=storage, max_length=255)
880+ filename = CharField(max_length=255, unique=True, editable=False)
881+ content = BinaryField(null=False)
882
883 objects = FileStorageManager()
884
885
886=== modified file 'src/maasserver/testing/factory.py'
887--- src/maasserver/testing/factory.py 2012-10-24 14:53:02 +0000
888+++ src/maasserver/testing/factory.py 2012-10-26 08:37:31 +0000
889@@ -296,13 +296,13 @@
890 admin.save()
891 return admin
892
893- def make_file_storage(self, filename=None, data=None):
894+ def make_file_storage(self, filename=None, content=None):
895 if filename is None:
896 filename = self.getRandomString(100)
897- if data is None:
898- data = self.getRandomString(1024).encode('ascii')
899+ if content is None:
900+ content = self.getRandomString(1024).encode('ascii')
901
902- return FileStorage.objects.save_file(filename, BytesIO(data))
903+ return FileStorage.objects.save_file(filename, BytesIO(content))
904
905 def make_oauth_header(self, **kwargs):
906 """Fake an OAuth authorization header.
907
908=== modified file 'src/maasserver/tests/test_api.py'
909--- src/maasserver/tests/test_api.py 2012-10-26 07:59:12 +0000
910+++ src/maasserver/tests/test_api.py 2012-10-26 08:37:31 +0000
911@@ -2515,7 +2515,8 @@
912 class AnonymousFileStorageAPITest(FileStorageAPITestMixin, AnonAPITestCase):
913
914 def test_get_works_anonymously(self):
915- factory.make_file_storage(filename="foofilers", data=b"give me rope")
916+ factory.make_file_storage(
917+ filename="foofilers", content=b"give me rope")
918 response = self.make_API_GET_request("get", "foofilers")
919
920 self.assertEqual(httplib.OK, response.status_code)
921@@ -2585,7 +2586,8 @@
922 self.assertEqual("file two", response.content)
923
924 def test_get_file_succeeds(self):
925- factory.make_file_storage(filename="foofilers", data=b"give me rope")
926+ factory.make_file_storage(
927+ filename="foofilers", content=b"give me rope")
928 response = self.make_API_GET_request("get", "foofilers")
929
930 self.assertEqual(httplib.OK, response.status_code)
931
932=== modified file 'src/maasserver/tests/test_commands.py'
933--- src/maasserver/tests/test_commands.py 2012-08-24 10:28:29 +0000
934+++ src/maasserver/tests/test_commands.py 2012-10-26 08:37:31 +0000
935@@ -14,13 +14,10 @@
936
937 from codecs import getwriter
938 from io import BytesIO
939-import os
940
941-from django.conf import settings
942 from django.contrib.auth.models import User
943 from django.core.cache import cache
944 from django.core.management import call_command
945-from maasserver.models import FileStorage
946 from maasserver.testing.factory import factory
947 from maasserver.utils.orm import get_one
948 from maastesting.djangotestcase import DjangoTestCase
949@@ -33,14 +30,6 @@
950 in a command's code, it should be extracted and unit-tested separately.
951 """
952
953- def test_gc(self):
954- upload_dir = os.path.join(settings.MEDIA_ROOT, FileStorage.upload_dir)
955- os.makedirs(upload_dir)
956- self.addCleanup(os.removedirs, upload_dir)
957- call_command('gc')
958- # The test is that we get here without errors.
959- pass
960-
961 def test_generate_api_doc(self):
962 out = BytesIO()
963 stdout = getwriter("UTF-8")(out)
964
965=== modified file 'src/maasserver/tests/test_filestorage.py'
966--- src/maasserver/tests/test_filestorage.py 2012-06-25 09:03:14 +0000
967+++ src/maasserver/tests/test_filestorage.py 2012-10-26 08:37:31 +0000
968@@ -14,45 +14,15 @@
969
970 import codecs
971 from io import BytesIO
972-import os
973-import shutil
974
975-from django.conf import settings
976 from maasserver.models import FileStorage
977 from maasserver.testing.factory import factory
978 from maasserver.testing.testcase import TestCase
979-from maastesting.utils import age_file
980-from testtools.matchers import (
981- GreaterThan,
982- LessThan,
983- )
984
985
986 class FileStorageTest(TestCase):
987 """Testing of the :class:`FileStorage` model."""
988
989- def make_upload_dir(self):
990- """Create the upload directory, and arrange for eventual deletion.
991-
992- The directory must not already exist. If it does, this method will
993- fail rather than arrange for deletion of a directory that may
994- contain meaningful data.
995-
996- :return: Absolute path to the `FileStorage` upload directory. This
997- is the directory where the actual files are stored.
998- """
999- media_root = settings.MEDIA_ROOT
1000- self.assertFalse(os.path.exists(media_root), "See media/README")
1001- self.addCleanup(shutil.rmtree, media_root, ignore_errors=True)
1002- os.mkdir(media_root)
1003- upload_dir = os.path.join(media_root, FileStorage.upload_dir)
1004- os.mkdir(upload_dir)
1005- return upload_dir
1006-
1007- def get_media_path(self, filename):
1008- """Get the path to a given stored file, relative to MEDIA_ROOT."""
1009- return os.path.join(FileStorage.upload_dir, filename)
1010-
1011 def make_data(self, including_text='data'):
1012 """Return arbitrary data.
1013
1014@@ -67,40 +37,24 @@
1015 text = "%s %s" % (including_text, factory.getRandomString())
1016 return text.encode('ascii')
1017
1018- def test_get_existing_storage_returns_None_if_none_found(self):
1019- nonexistent_file = factory.getRandomString()
1020- self.assertIsNone(
1021- FileStorage.objects.get_existing_storage(nonexistent_file))
1022-
1023- def test_get_existing_storage_finds_FileStorage(self):
1024- self.make_upload_dir()
1025- storage = factory.make_file_storage()
1026- self.assertEqual(
1027- storage,
1028- FileStorage.objects.get_existing_storage(storage.filename))
1029-
1030 def test_save_file_creates_storage(self):
1031- self.make_upload_dir()
1032 filename = factory.getRandomString()
1033- data = self.make_data()
1034- storage = FileStorage.objects.save_file(filename, BytesIO(data))
1035+ content = self.make_data()
1036+ storage = FileStorage.objects.save_file(filename, BytesIO(content))
1037 self.assertEqual(
1038- (filename, data),
1039- (storage.filename, storage.data.read()))
1040+ (filename, content),
1041+ (storage.filename, storage.content))
1042
1043 def test_storage_can_be_retrieved(self):
1044- self.make_upload_dir()
1045 filename = factory.getRandomString()
1046- data = self.make_data()
1047- factory.make_file_storage(filename=filename, data=data)
1048+ content = self.make_data()
1049+ factory.make_file_storage(filename=filename, content=content)
1050 storage = FileStorage.objects.get(filename=filename)
1051 self.assertEqual(
1052- (filename, data),
1053- (storage.filename, storage.data.read()))
1054+ (filename, content),
1055+ (storage.filename, storage.content))
1056
1057 def test_stores_binary_data(self):
1058- self.make_upload_dir()
1059-
1060 # This horrible binary data could never, ever, under any
1061 # encoding known to man be interpreted as text(1). Switch the
1062 # bytes of the byte-order mark around and by design you get an
1063@@ -114,121 +68,19 @@
1064
1065 # And yet, because FileStorage supports binary data, it comes
1066 # out intact.
1067- storage = factory.make_file_storage(filename="x", data=binary_data)
1068- self.assertEqual(binary_data, storage.data.read())
1069+ storage = factory.make_file_storage(filename="x", content=binary_data)
1070+ self.assertEqual(binary_data, storage.content)
1071
1072 def test_overwrites_file(self):
1073 # If a file of the same name has already been stored, the
1074 # reference to the old data gets overwritten with one to the new
1075- # data. They are actually different files on the filesystem.
1076- self.make_upload_dir()
1077+ # data.
1078 filename = factory.make_name('filename')
1079 old_storage = factory.make_file_storage(
1080- filename=filename, data=self.make_data('old data'))
1081+ filename=filename, content=self.make_data('old data'))
1082 new_data = self.make_data('new-data')
1083 new_storage = factory.make_file_storage(
1084- filename=filename, data=new_data)
1085- self.assertNotEqual(old_storage.data.name, new_storage.data.name)
1086+ filename=filename, content=new_data)
1087+ self.assertEqual(old_storage.filename, new_storage.filename)
1088 self.assertEqual(
1089- new_data, FileStorage.objects.get(filename=filename).data.read())
1090-
1091- def test_list_stored_files_lists_files(self):
1092- filename = factory.getRandomString()
1093- factory.make_file(
1094- location=self.make_upload_dir(), name=filename,
1095- contents=self.make_data())
1096- self.assertIn(
1097- self.get_media_path(filename),
1098- FileStorage.objects.list_stored_files())
1099-
1100- def test_list_stored_files_includes_referenced_files(self):
1101- self.make_upload_dir()
1102- storage = factory.make_file_storage()
1103- self.assertIn(
1104- storage.data.name, FileStorage.objects.list_stored_files())
1105-
1106- def test_list_referenced_files_lists_FileStorage_files(self):
1107- self.make_upload_dir()
1108- storage = factory.make_file_storage()
1109- self.assertIn(
1110- storage.data.name, FileStorage.objects.list_referenced_files())
1111-
1112- def test_list_referenced_files_excludes_unreferenced_files(self):
1113- filename = factory.getRandomString()
1114- factory.make_file(
1115- location=self.make_upload_dir(), name=filename,
1116- contents=self.make_data())
1117- self.assertNotIn(
1118- self.get_media_path(filename),
1119- FileStorage.objects.list_referenced_files())
1120-
1121- def test_list_referenced_files_uses_file_name_not_FileStorage_name(self):
1122- self.make_upload_dir()
1123- filename = factory.getRandomString()
1124- # The filename we're going to use is already taken. The file
1125- # we'll be looking at will have to have a different name.
1126- factory.make_file_storage(filename=filename)
1127- storage = factory.make_file_storage(filename=filename)
1128- # It's the name of the file, not the FileStorage.filename, that
1129- # is in list_referenced_files.
1130- self.assertIn(
1131- storage.data.name, FileStorage.objects.list_referenced_files())
1132-
1133- def test_is_old_returns_False_for_recent_file(self):
1134- filename = factory.getRandomString()
1135- path = factory.make_file(
1136- location=self.make_upload_dir(), name=filename,
1137- contents=self.make_data())
1138- age_file(path, FileStorage.objects.grace_time - 60)
1139- self.assertFalse(
1140- FileStorage.objects.is_old(self.get_media_path(filename)))
1141-
1142- def test_is_old_returns_True_for_old_file(self):
1143- filename = factory.getRandomString()
1144- path = factory.make_file(
1145- location=self.make_upload_dir(), name=filename,
1146- contents=self.make_data())
1147- age_file(path, FileStorage.objects.grace_time + 1)
1148- self.assertTrue(
1149- FileStorage.objects.is_old(self.get_media_path(filename)))
1150-
1151- def test_collect_garbage_deletes_garbage(self):
1152- filename = factory.getRandomString()
1153- path = factory.make_file(
1154- location=self.make_upload_dir(), name=filename,
1155- contents=self.make_data())
1156- age_file(path, FileStorage.objects.grace_time + 1)
1157- FileStorage.objects.collect_garbage()
1158- self.assertFalse(
1159- FileStorage.storage.exists(self.get_media_path(filename)))
1160-
1161- def test_grace_time_is_generous_but_not_unlimited(self):
1162- # Grace time for garbage collection is long enough that it won't
1163- # expire while the request that wrote it is still being handled.
1164- # But it won't keep a file around for ages. For instance, it'll
1165- # be more than 20 seconds, but less than a day.
1166- self.assertThat(FileStorage.objects.grace_time, GreaterThan(20))
1167- self.assertThat(FileStorage.objects.grace_time, LessThan(24 * 60 * 60))
1168-
1169- def test_collect_garbage_leaves_recent_files_alone(self):
1170- filename = factory.getRandomString()
1171- factory.make_file(
1172- location=self.make_upload_dir(), name=filename,
1173- contents=self.make_data())
1174- FileStorage.objects.collect_garbage()
1175- self.assertTrue(
1176- FileStorage.storage.exists(self.get_media_path(filename)))
1177-
1178- def test_collect_garbage_leaves_referenced_files_alone(self):
1179- self.make_upload_dir()
1180- storage = factory.make_file_storage()
1181- age_file(storage.data.path, FileStorage.objects.grace_time + 1)
1182- FileStorage.objects.collect_garbage()
1183- self.assertTrue(FileStorage.storage.exists(storage.data.name))
1184-
1185- def test_collect_garbage_tolerates_missing_upload_dir(self):
1186- # When MAAS is freshly installed, the upload directory is still
1187- # missing. But...
1188- FileStorage.objects.collect_garbage()
1189- # ...we get through garbage collection without breakage.
1190- pass
1191+ new_data, FileStorage.objects.get(filename=filename).content)