launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #13773
[Merge] lp:~jtv/maas/bug-1069734 into lp:maas
Jeroen T. Vermeulen has proposed merging lp:~jtv/maas/bug-1069734 into lp:maas.
Commit message:
Forward-port 1.2 r1272 to trunk: move region file storage into the database.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1069734 in MAAS: "Filestorage is unique to each appserver instance"
https://bugs.launchpad.net/maas/+bug/1069734
For more details, see:
https://code.launchpad.net/~jtv/maas/bug-1069734/+merge/131551
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
--
https://code.launchpad.net/~jtv/maas/bug-1069734/+merge/131551
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~jtv/maas/bug-1069734 into lp:maas.
=== modified file 'etc/cron.d/maas-gc'
--- etc/cron.d/maas-gc 2012-08-03 16:33:05 +0000
+++ etc/cron.d/maas-gc 2012-10-26 08:37:31 +0000
@@ -1,8 +1,4 @@
# Perform daily background cleanups in MAAS.
#
-# The "maas gc" command is for garbage-collection, such as deleting uploaded
-# files from Juju's file storage API, and in the future commissioning logs,
-# that have been superseded by newer ones. (This isn't done immediately
-# when the files are overwritten because (1) the transaction that overwrites
-# them may fail, and (2) a file may still be in use when it's overwritten.)
-0 0 * * * root /usr/sbin/maas gc &> /dev/null
+# This currently does nothing: maas-gc is no longer needed.
+#0 0 * * * root /usr/sbin/maas gc &> /dev/null
=== modified file 'setup.py'
--- setup.py 2012-10-01 22:56:46 +0000
+++ setup.py 2012-10-26 08:37:31 +0000
@@ -65,8 +65,6 @@
'etc/maas/commissioning-user-data',
'contrib/maas-http.conf',
'contrib/maas_local_settings.py']),
- ('/etc/cron.d',
- ['etc/cron.d/maas-gc']),
('/usr/share/maas',
['contrib/wsgi.py',
'etc/celeryconfig.py',
=== modified file 'src/maasserver/api.py'
--- src/maasserver/api.py 2012-10-26 07:59:12 +0000
+++ src/maasserver/api.py 2012-10-26 08:37:31 +0000
@@ -985,7 +985,7 @@
db_file = FileStorage.objects.get(filename=filename)
except FileStorage.DoesNotExist:
raise MAASAPINotFound("File not found")
- return HttpResponse(db_file.data.read(), status=httplib.OK)
+ return HttpResponse(db_file.content, status=httplib.OK)
class AnonFilesHandler(AnonymousOperationsHandler):
=== removed file 'src/maasserver/management/commands/gc.py'
--- src/maasserver/management/commands/gc.py 2012-04-16 10:00:51 +0000
+++ src/maasserver/management/commands/gc.py 1970-01-01 00:00:00 +0000
@@ -1,24 +0,0 @@
-# Copyright 2012 Canonical Ltd. This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Custom django command: garabge-collect."""
-
-from __future__ import (
- absolute_import,
- print_function,
- unicode_literals,
- )
-
-__metaclass__ = type
-__all__ = [
- 'Command',
- ]
-
-
-from django.core.management.base import BaseCommand
-from maasserver.models import FileStorage
-
-
-class Command(BaseCommand):
- def handle(self, *args, **options):
- FileStorage.objects.collect_garbage()
=== added file 'src/maasserver/migrations/0039_add_filestorage_content.py'
--- src/maasserver/migrations/0039_add_filestorage_content.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/migrations/0039_add_filestorage_content.py 2012-10-26 08:37:31 +0000
@@ -0,0 +1,233 @@
+# -*- coding: utf-8 -*-
+from base64 import b64encode
+import datetime
+import os.path
+
+from django.conf import settings
+from django.db import models
+from south.db import db
+from south.v2 import SchemaMigration
+
+
+def get_unmigrated_filestorages(orm):
+ """Find FileStorage objects whose data needs migrating."""
+ return orm['maasserver.FileStorage'].objects.filter(content=None)
+
+
+def read_file(storage):
+ """Read file contents from a FileStorage."""
+ return storage.data.read()
+
+
+def copy_files_into_database(orm):
+ """Copy file contents into the "content" field."""
+ for storage in get_unmigrated_filestorages(orm):
+ raw_content = read_file(storage)
+ storage.content = b64encode(raw_content).decode('ascii')
+ storage.save()
+
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ # Adding field 'FileStorage.content'
+ db.add_column(u'maasserver_filestorage', 'content',
+ self.gf('metadataserver.fields.BinaryField')(null=True),
+ keep_default=False)
+
+ # Changing field 'FileStorage.filename'
+ db.alter_column(u'maasserver_filestorage', 'filename', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255))
+
+ # Effecting data migration. Not deleting the old files yet; the
+ # database transaction might still abort for whatever reason.
+ copy_files_into_database(orm)
+
+ def backwards(self, orm):
+ # Deleting field 'FileStorage.content'
+ db.delete_column(u'maasserver_filestorage', 'content')
+
+
+ # Changing field 'FileStorage.filename'
+ db.alter_column(u'maasserver_filestorage', 'filename', self.gf('django.db.models.fields.CharField')(max_length=200, unique=True))
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ u'maasserver.bootimage': {
+ 'Meta': {'unique_together': "((u'architecture', u'subarchitecture', u'release', u'purpose'),)", 'object_name': 'BootImage'},
+ 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'purpose': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'release': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'subarchitecture': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ u'maasserver.componenterror': {
+ 'Meta': {'object_name': 'ComponentError'},
+ 'component': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
+ },
+ u'maasserver.config': {
+ 'Meta': {'object_name': 'Config'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'value': ('maasserver.fields.JSONObjectField', [], {'null': 'True'})
+ },
+ u'maasserver.dhcplease': {
+ 'Meta': {'object_name': 'DHCPLease'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip': ('django.db.models.fields.IPAddressField', [], {'unique': 'True', 'max_length': '15'}),
+ 'mac': ('maasserver.fields.MACAddressField', [], {}),
+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"})
+ },
+ u'maasserver.filestorage': {
+ 'Meta': {'object_name': 'FileStorage'},
+ 'content': ('metadataserver.fields.BinaryField', [], {'null': 'True'}),
+ 'data': ('django.db.models.fields.files.FileField', [], {'max_length': '255'}),
+ 'filename': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ u'maasserver.macaddress': {
+ 'Meta': {'object_name': 'MACAddress'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'mac_address': ('maasserver.fields.MACAddressField', [], {'unique': 'True'}),
+ 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
+ },
+ u'maasserver.node': {
+ 'Meta': {'object_name': 'Node'},
+ 'after_commissioning_action': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'architecture': ('django.db.models.fields.CharField', [], {'default': "u'i386/generic'", 'max_length': '31'}),
+ 'cpu_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'distro_series': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '10', 'null': 'True', 'blank': 'True'}),
+ 'error': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
+ 'hardware_details': ('maasserver.fields.XMLField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
+ 'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'memory': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'netboot': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']", 'null': 'True'}),
+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'power_parameters': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
+ 'power_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '10', 'blank': 'True'}),
+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '10'}),
+ 'system_id': ('django.db.models.fields.CharField', [], {'default': "u'node-b588ce50-1ea0-11e2-946f-002608dc6120'", 'unique': 'True', 'max_length': '41'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Tag']", 'symmetrical': 'False'}),
+ 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'null': 'True'}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
+ },
+ u'maasserver.nodegroup': {
+ 'Meta': {'object_name': 'NodeGroup'},
+ 'api_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '18'}),
+ 'api_token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'unique': 'True'}),
+ 'cluster_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100', 'blank': 'True'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'dhcp_key': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}),
+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
+ 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'})
+ },
+ u'maasserver.nodegroupinterface': {
+ 'Meta': {'unique_together': "((u'nodegroup', u'interface'),)", 'object_name': 'NodeGroupInterface'},
+ 'broadcast_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'interface': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
+ 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+ 'ip_range_high': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
+ 'ip_range_low': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
+ 'management': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
+ 'router_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
+ 'subnet_mask': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
+ },
+ u'maasserver.sshkey': {
+ 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSHKey'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'key': ('django.db.models.fields.TextField', [], {}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ u'maasserver.tag': {
+ 'Meta': {'object_name': 'Tag'},
+ 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'definition': ('django.db.models.fields.TextField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
+ },
+ u'maasserver.userprofile': {
+ 'Meta': {'object_name': 'UserProfile'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
+ },
+ 'piston.consumer': {
+ 'Meta': {'object_name': 'Consumer'},
+ 'description': ('django.db.models.fields.TextField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+ 'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '16'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'consumers'", 'null': 'True', 'to': "orm['auth.User']"})
+ },
+ 'piston.token': {
+ 'Meta': {'object_name': 'Token'},
+ 'callback': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'callback_confirmed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Consumer']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_approved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+ 'timestamp': ('django.db.models.fields.IntegerField', [], {'default': '1351168636L'}),
+ 'token_type': ('django.db.models.fields.IntegerField', [], {}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'tokens'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'verifier': ('django.db.models.fields.CharField', [], {'max_length': '10'})
+ }
+ }
+
+ complete_apps = ['maasserver']
=== added file 'src/maasserver/migrations/0040_make_filestorage_data_not_null.py'
--- src/maasserver/migrations/0040_make_filestorage_data_not_null.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/migrations/0040_make_filestorage_data_not_null.py 2012-10-26 08:37:31 +0000
@@ -0,0 +1,203 @@
+# -*- coding: utf-8 -*-
+import datetime
+
+from django.db import models
+from south.db import db
+from south.v2 import SchemaMigration
+
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Changing field 'FileStorage.content'
+ # Disallow NULLs. The previous migration should have
+ # initialized the column for all existing rows.
+ db.alter_column(u'maasserver_filestorage', 'content', self.gf('metadataserver.fields.BinaryField')(null=False))
+
+ def backwards(self, orm):
+
+ # Changing field 'FileStorage.content'
+ db.alter_column(u'maasserver_filestorage', 'content', self.gf('metadataserver.fields.BinaryField')(null=True))
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ u'maasserver.bootimage': {
+ 'Meta': {'unique_together': "((u'architecture', u'subarchitecture', u'release', u'purpose'),)", 'object_name': 'BootImage'},
+ 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'purpose': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'release': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'subarchitecture': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ u'maasserver.componenterror': {
+ 'Meta': {'object_name': 'ComponentError'},
+ 'component': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
+ },
+ u'maasserver.config': {
+ 'Meta': {'object_name': 'Config'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'value': ('maasserver.fields.JSONObjectField', [], {'null': 'True'})
+ },
+ u'maasserver.dhcplease': {
+ 'Meta': {'object_name': 'DHCPLease'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip': ('django.db.models.fields.IPAddressField', [], {'unique': 'True', 'max_length': '15'}),
+ 'mac': ('maasserver.fields.MACAddressField', [], {}),
+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"})
+ },
+ u'maasserver.filestorage': {
+ 'Meta': {'object_name': 'FileStorage'},
+ 'content': ('metadataserver.fields.BinaryField', [], {}),
+ 'data': ('django.db.models.fields.files.FileField', [], {'max_length': '255'}),
+ 'filename': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ u'maasserver.macaddress': {
+ 'Meta': {'object_name': 'MACAddress'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'mac_address': ('maasserver.fields.MACAddressField', [], {'unique': 'True'}),
+ 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
+ },
+ u'maasserver.node': {
+ 'Meta': {'object_name': 'Node'},
+ 'after_commissioning_action': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'architecture': ('django.db.models.fields.CharField', [], {'default': "u'i386/generic'", 'max_length': '31'}),
+ 'cpu_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'distro_series': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '10', 'null': 'True', 'blank': 'True'}),
+ 'error': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
+ 'hardware_details': ('maasserver.fields.XMLField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
+ 'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'memory': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'netboot': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']", 'null': 'True'}),
+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'power_parameters': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
+ 'power_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '10', 'blank': 'True'}),
+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '10'}),
+ 'system_id': ('django.db.models.fields.CharField', [], {'default': "u'node-766d0f92-1ea5-11e2-9dee-002608dc6120'", 'unique': 'True', 'max_length': '41'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Tag']", 'symmetrical': 'False'}),
+ 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'null': 'True'}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
+ },
+ u'maasserver.nodegroup': {
+ 'Meta': {'object_name': 'NodeGroup'},
+ 'api_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '18'}),
+ 'api_token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'unique': 'True'}),
+ 'cluster_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100', 'blank': 'True'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'dhcp_key': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}),
+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
+ 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'})
+ },
+ u'maasserver.nodegroupinterface': {
+ 'Meta': {'unique_together': "((u'nodegroup', u'interface'),)", 'object_name': 'NodeGroupInterface'},
+ 'broadcast_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'interface': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
+ 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+ 'ip_range_high': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
+ 'ip_range_low': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
+ 'management': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
+ 'router_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
+ 'subnet_mask': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
+ },
+ u'maasserver.sshkey': {
+ 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSHKey'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'key': ('django.db.models.fields.TextField', [], {}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ u'maasserver.tag': {
+ 'Meta': {'object_name': 'Tag'},
+ 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'definition': ('django.db.models.fields.TextField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
+ },
+ u'maasserver.userprofile': {
+ 'Meta': {'object_name': 'UserProfile'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
+ },
+ 'piston.consumer': {
+ 'Meta': {'object_name': 'Consumer'},
+ 'description': ('django.db.models.fields.TextField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+ 'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '16'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'consumers'", 'null': 'True', 'to': "orm['auth.User']"})
+ },
+ 'piston.token': {
+ 'Meta': {'object_name': 'Token'},
+ 'callback': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'callback_confirmed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Consumer']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_approved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+ 'timestamp': ('django.db.models.fields.IntegerField', [], {'default': '1351170650L'}),
+ 'token_type': ('django.db.models.fields.IntegerField', [], {}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'tokens'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'verifier': ('django.db.models.fields.CharField', [], {'max_length': '10'})
+ }
+ }
+
+ complete_apps = ['maasserver']
=== added file 'src/maasserver/migrations/0041_remove_filestorage_data.py'
--- src/maasserver/migrations/0041_remove_filestorage_data.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/migrations/0041_remove_filestorage_data.py 2012-10-26 08:37:31 +0000
@@ -0,0 +1,213 @@
+# -*- coding: utf-8 -*-
+import datetime
+import os.path
+from shutil import rmtree
+
+from django.conf import settings
+from django.db import models
+from south.db import db
+from south.v2 import SchemaMigration
+
+# The sub-directory of MEDIA_ROOT where FileStorage used to store its
+# files. This duplicates the value of the now-removed
+# FileStorage.upload_dir.
+upload_dir = 'storage'
+
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ # Deleting field 'FileStorage.data'
+ db.delete_column(u'maasserver_filestorage', 'data')
+
+ # Cleaning up any obsolete FileStorage files.
+ if settings.MEDIA_ROOT and os.path.isdir(settings.MEDIA_ROOT):
+ rmtree(
+ os.path.join(settings.MEDIA_ROOT, upload_dir),
+ ignore_errors=True)
+
+ def backwards(self, orm):
+
+ # User chose to not deal with backwards NULL issues for 'FileStorage.data'
+ raise RuntimeError("Cannot reverse this migration. 'FileStorage.data' and its values cannot be restored.")
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ u'maasserver.bootimage': {
+ 'Meta': {'unique_together': "((u'architecture', u'subarchitecture', u'release', u'purpose'),)", 'object_name': 'BootImage'},
+ 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'purpose': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'release': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'subarchitecture': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ u'maasserver.componenterror': {
+ 'Meta': {'object_name': 'ComponentError'},
+ 'component': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
+ },
+ u'maasserver.config': {
+ 'Meta': {'object_name': 'Config'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'value': ('maasserver.fields.JSONObjectField', [], {'null': 'True'})
+ },
+ u'maasserver.dhcplease': {
+ 'Meta': {'object_name': 'DHCPLease'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip': ('django.db.models.fields.IPAddressField', [], {'unique': 'True', 'max_length': '15'}),
+ 'mac': ('maasserver.fields.MACAddressField', [], {}),
+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"})
+ },
+ u'maasserver.filestorage': {
+ 'Meta': {'object_name': 'FileStorage'},
+ 'content': ('metadataserver.fields.BinaryField', [], {}),
+ 'filename': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ u'maasserver.macaddress': {
+ 'Meta': {'object_name': 'MACAddress'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'mac_address': ('maasserver.fields.MACAddressField', [], {'unique': 'True'}),
+ 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
+ },
+ u'maasserver.node': {
+ 'Meta': {'object_name': 'Node'},
+ 'after_commissioning_action': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'architecture': ('django.db.models.fields.CharField', [], {'default': "u'i386/generic'", 'max_length': '31'}),
+ 'cpu_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'distro_series': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '10', 'null': 'True', 'blank': 'True'}),
+ 'error': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
+ 'hardware_details': ('maasserver.fields.XMLField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
+ 'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'memory': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'netboot': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']", 'null': 'True'}),
+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'power_parameters': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
+ 'power_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '10', 'blank': 'True'}),
+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '10'}),
+ 'system_id': ('django.db.models.fields.CharField', [], {'default': "u'node-da71dfcc-1ea5-11e2-8763-002608dc6120'", 'unique': 'True', 'max_length': '41'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Tag']", 'symmetrical': 'False'}),
+ 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'null': 'True'}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
+ },
+ u'maasserver.nodegroup': {
+ 'Meta': {'object_name': 'NodeGroup'},
+ 'api_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '18'}),
+ 'api_token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'unique': 'True'}),
+ 'cluster_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100', 'blank': 'True'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'dhcp_key': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}),
+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
+ 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'})
+ },
+ u'maasserver.nodegroupinterface': {
+ 'Meta': {'unique_together': "((u'nodegroup', u'interface'),)", 'object_name': 'NodeGroupInterface'},
+ 'broadcast_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'interface': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
+ 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+ 'ip_range_high': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
+ 'ip_range_low': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
+ 'management': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
+ 'router_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
+ 'subnet_mask': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
+ },
+ u'maasserver.sshkey': {
+ 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSHKey'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'key': ('django.db.models.fields.TextField', [], {}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ u'maasserver.tag': {
+ 'Meta': {'object_name': 'Tag'},
+ 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'definition': ('django.db.models.fields.TextField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
+ },
+ u'maasserver.userprofile': {
+ 'Meta': {'object_name': 'UserProfile'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
+ },
+ 'piston.consumer': {
+ 'Meta': {'object_name': 'Consumer'},
+ 'description': ('django.db.models.fields.TextField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+ 'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '16'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'consumers'", 'null': 'True', 'to': "orm['auth.User']"})
+ },
+ 'piston.token': {
+ 'Meta': {'object_name': 'Token'},
+ 'callback': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'callback_confirmed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Consumer']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_approved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+ 'timestamp': ('django.db.models.fields.IntegerField', [], {'default': '1351170840L'}),
+ 'token_type': ('django.db.models.fields.IntegerField', [], {}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'tokens'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'verifier': ('django.db.models.fields.CharField', [], {'max_length': '10'})
+ }
+ }
+
+ complete_apps = ['maasserver']
=== modified file 'src/maasserver/models/filestorage.py'
--- src/maasserver/models/filestorage.py 2012-08-24 10:28:29 +0000
+++ src/maasserver/models/filestorage.py 2012-10-26 08:37:31 +0000
@@ -15,22 +15,17 @@
]
-from errno import ENOENT
-import os
-import time
-
-from django.conf import settings
-from django.core.files.base import ContentFile
-from django.core.files.storage import FileSystemStorage
from django.db.models import (
CharField,
- FileField,
Manager,
Model,
)
from maasserver import DefaultMeta
from maasserver.models.cleansave import CleanSave
-from maasserver.utils.orm import get_one
+from metadataserver.fields import (
+ Bin,
+ BinaryField,
+ )
class FileStorageManager(Manager):
@@ -47,108 +42,36 @@
original file will not be affected. Also, any ongoing reads from the
old file will continue without iterruption.
"""
- # The time, in seconds, that an unreferenced file is allowed to
- # persist in order to satisfy ongoing requests.
- grace_time = 12 * 60 * 60
-
- def get_existing_storage(self, filename):
- """Return an existing `FileStorage` of this name, or None."""
- return get_one(self.filter(filename=filename))
def save_file(self, filename, file_object):
- """Save the file to the filesystem and persist to the database.
-
- The file will end up in MEDIA_ROOT/storage/
+ """Save the file to the database.
If a file of that name already existed, it will be replaced by the
new contents.
"""
# This probably ought to read in chunks but large files are
- # not expected. Also note that uploading a file with the same
- # name as an existing one will cause that file to be written
- # with a new generated name, and the old one remains where it
- # is. See https://code.djangoproject.com/ticket/6157 - the
- # Django devs consider deleting things dangerous ... ha.
- # HOWEVER - this operation would need to be atomic anyway so
- # it's safest left how it is for now (reads can overlap with
- # writes from Juju).
- content = ContentFile(file_object.read())
-
- storage = self.get_existing_storage(filename)
- if storage is None:
- storage = FileStorage(filename=filename)
- storage.data.save(filename, content)
+ # not expected.
+ content = Bin(file_object.read())
+ storage, created = self.get_or_create(
+ filename=filename, defaults={'content': content})
+ if not created:
+ storage.content = content
+ storage.save()
return storage
- def list_stored_files(self):
- """Find the files stored in the filesystem."""
- dirs, files = FileStorage.storage.listdir(FileStorage.upload_dir)
- return [
- os.path.join(FileStorage.upload_dir, filename)
- for filename in files]
-
- def list_referenced_files(self):
- """Find the names of files that are referenced from `FileStorage`.
-
- :return: All file paths within MEDIA ROOT (relative to MEDIA_ROOT)
- that have `FileStorage` entries referencing them.
- :rtype: frozenset
- """
- return frozenset(
- file_storage.data.name
- for file_storage in self.all())
-
- def is_old(self, storage_filename):
- """Is the named file in the filesystem storage old enough to be dead?
-
- :param storage_filename: The name under which the file is stored in
- the filesystem, relative to MEDIA_ROOT. This need not be the
- same name as its filename as stored in the `FileStorage` object.
- It includes the name of the upload directory.
- """
- file_path = os.path.join(settings.MEDIA_ROOT, storage_filename)
- mtime = os.stat(file_path).st_mtime
- expiry = mtime + self.grace_time
- return expiry <= time.time()
-
- def collect_garbage(self):
- """Clean up stored files that are no longer accessible."""
- # Avoid circular imports.
- from maasserver.models import logger
-
- try:
- stored_files = self.list_stored_files()
- except OSError as e:
- if e.errno != ENOENT:
- raise
- logger.info(
- "Upload directory does not exist yet. "
- "Skipping garbage collection.")
- return
- referenced_files = self.list_referenced_files()
- for path in stored_files:
- if path not in referenced_files and self.is_old(path):
- FileStorage.storage.delete(path)
-
class FileStorage(CleanSave, Model):
"""A simple file storage keyed on file name.
:ivar filename: A unique file name to use for the data being stored.
- :ivar data: The file's actual data.
+ :ivar content: The file's actual data.
"""
class Meta(DefaultMeta):
"""Needed for South to recognize this model."""
- storage = FileSystemStorage()
-
- upload_dir = "storage"
-
- # Unix filenames can be longer than this (e.g. 255 bytes), but leave
- # some extra room for the full path, as well as a versioning suffix.
- filename = CharField(max_length=200, unique=True, editable=False)
- data = FileField(upload_to=upload_dir, storage=storage, max_length=255)
+ filename = CharField(max_length=255, unique=True, editable=False)
+ content = BinaryField(null=False)
objects = FileStorageManager()
=== modified file 'src/maasserver/testing/factory.py'
--- src/maasserver/testing/factory.py 2012-10-24 14:53:02 +0000
+++ src/maasserver/testing/factory.py 2012-10-26 08:37:31 +0000
@@ -296,13 +296,13 @@
admin.save()
return admin
- def make_file_storage(self, filename=None, data=None):
+ def make_file_storage(self, filename=None, content=None):
if filename is None:
filename = self.getRandomString(100)
- if data is None:
- data = self.getRandomString(1024).encode('ascii')
+ if content is None:
+ content = self.getRandomString(1024).encode('ascii')
- return FileStorage.objects.save_file(filename, BytesIO(data))
+ return FileStorage.objects.save_file(filename, BytesIO(content))
def make_oauth_header(self, **kwargs):
"""Fake an OAuth authorization header.
=== modified file 'src/maasserver/tests/test_api.py'
--- src/maasserver/tests/test_api.py 2012-10-26 07:59:12 +0000
+++ src/maasserver/tests/test_api.py 2012-10-26 08:37:31 +0000
@@ -2515,7 +2515,8 @@
class AnonymousFileStorageAPITest(FileStorageAPITestMixin, AnonAPITestCase):
def test_get_works_anonymously(self):
- factory.make_file_storage(filename="foofilers", data=b"give me rope")
+ factory.make_file_storage(
+ filename="foofilers", content=b"give me rope")
response = self.make_API_GET_request("get", "foofilers")
self.assertEqual(httplib.OK, response.status_code)
@@ -2585,7 +2586,8 @@
self.assertEqual("file two", response.content)
def test_get_file_succeeds(self):
- factory.make_file_storage(filename="foofilers", data=b"give me rope")
+ factory.make_file_storage(
+ filename="foofilers", content=b"give me rope")
response = self.make_API_GET_request("get", "foofilers")
self.assertEqual(httplib.OK, response.status_code)
=== modified file 'src/maasserver/tests/test_commands.py'
--- src/maasserver/tests/test_commands.py 2012-08-24 10:28:29 +0000
+++ src/maasserver/tests/test_commands.py 2012-10-26 08:37:31 +0000
@@ -14,13 +14,10 @@
from codecs import getwriter
from io import BytesIO
-import os
-from django.conf import settings
from django.contrib.auth.models import User
from django.core.cache import cache
from django.core.management import call_command
-from maasserver.models import FileStorage
from maasserver.testing.factory import factory
from maasserver.utils.orm import get_one
from maastesting.djangotestcase import DjangoTestCase
@@ -33,14 +30,6 @@
in a command's code, it should be extracted and unit-tested separately.
"""
- def test_gc(self):
- upload_dir = os.path.join(settings.MEDIA_ROOT, FileStorage.upload_dir)
- os.makedirs(upload_dir)
- self.addCleanup(os.removedirs, upload_dir)
- call_command('gc')
- # The test is that we get here without errors.
- pass
-
def test_generate_api_doc(self):
out = BytesIO()
stdout = getwriter("UTF-8")(out)
=== modified file 'src/maasserver/tests/test_filestorage.py'
--- src/maasserver/tests/test_filestorage.py 2012-06-25 09:03:14 +0000
+++ src/maasserver/tests/test_filestorage.py 2012-10-26 08:37:31 +0000
@@ -14,45 +14,15 @@
import codecs
from io import BytesIO
-import os
-import shutil
-from django.conf import settings
from maasserver.models import FileStorage
from maasserver.testing.factory import factory
from maasserver.testing.testcase import TestCase
-from maastesting.utils import age_file
-from testtools.matchers import (
- GreaterThan,
- LessThan,
- )
class FileStorageTest(TestCase):
"""Testing of the :class:`FileStorage` model."""
- def make_upload_dir(self):
- """Create the upload directory, and arrange for eventual deletion.
-
- The directory must not already exist. If it does, this method will
- fail rather than arrange for deletion of a directory that may
- contain meaningful data.
-
- :return: Absolute path to the `FileStorage` upload directory. This
- is the directory where the actual files are stored.
- """
- media_root = settings.MEDIA_ROOT
- self.assertFalse(os.path.exists(media_root), "See media/README")
- self.addCleanup(shutil.rmtree, media_root, ignore_errors=True)
- os.mkdir(media_root)
- upload_dir = os.path.join(media_root, FileStorage.upload_dir)
- os.mkdir(upload_dir)
- return upload_dir
-
- def get_media_path(self, filename):
- """Get the path to a given stored file, relative to MEDIA_ROOT."""
- return os.path.join(FileStorage.upload_dir, filename)
-
def make_data(self, including_text='data'):
"""Return arbitrary data.
@@ -67,40 +37,24 @@
text = "%s %s" % (including_text, factory.getRandomString())
return text.encode('ascii')
- def test_get_existing_storage_returns_None_if_none_found(self):
- nonexistent_file = factory.getRandomString()
- self.assertIsNone(
- FileStorage.objects.get_existing_storage(nonexistent_file))
-
- def test_get_existing_storage_finds_FileStorage(self):
- self.make_upload_dir()
- storage = factory.make_file_storage()
- self.assertEqual(
- storage,
- FileStorage.objects.get_existing_storage(storage.filename))
-
def test_save_file_creates_storage(self):
- self.make_upload_dir()
filename = factory.getRandomString()
- data = self.make_data()
- storage = FileStorage.objects.save_file(filename, BytesIO(data))
+ content = self.make_data()
+ storage = FileStorage.objects.save_file(filename, BytesIO(content))
self.assertEqual(
- (filename, data),
- (storage.filename, storage.data.read()))
+ (filename, content),
+ (storage.filename, storage.content))
def test_storage_can_be_retrieved(self):
- self.make_upload_dir()
filename = factory.getRandomString()
- data = self.make_data()
- factory.make_file_storage(filename=filename, data=data)
+ content = self.make_data()
+ factory.make_file_storage(filename=filename, content=content)
storage = FileStorage.objects.get(filename=filename)
self.assertEqual(
- (filename, data),
- (storage.filename, storage.data.read()))
+ (filename, content),
+ (storage.filename, storage.content))
def test_stores_binary_data(self):
- self.make_upload_dir()
-
# This horrible binary data could never, ever, under any
# encoding known to man be interpreted as text(1). Switch the
# bytes of the byte-order mark around and by design you get an
@@ -114,121 +68,19 @@
# And yet, because FileStorage supports binary data, it comes
# out intact.
- storage = factory.make_file_storage(filename="x", data=binary_data)
- self.assertEqual(binary_data, storage.data.read())
+ storage = factory.make_file_storage(filename="x", content=binary_data)
+ self.assertEqual(binary_data, storage.content)
def test_overwrites_file(self):
# If a file of the same name has already been stored, the
# reference to the old data gets overwritten with one to the new
- # data. They are actually different files on the filesystem.
- self.make_upload_dir()
+ # data.
filename = factory.make_name('filename')
old_storage = factory.make_file_storage(
- filename=filename, data=self.make_data('old data'))
+ filename=filename, content=self.make_data('old data'))
new_data = self.make_data('new-data')
new_storage = factory.make_file_storage(
- filename=filename, data=new_data)
- self.assertNotEqual(old_storage.data.name, new_storage.data.name)
+ filename=filename, content=new_data)
+ self.assertEqual(old_storage.filename, new_storage.filename)
self.assertEqual(
- new_data, FileStorage.objects.get(filename=filename).data.read())
-
- def test_list_stored_files_lists_files(self):
- filename = factory.getRandomString()
- factory.make_file(
- location=self.make_upload_dir(), name=filename,
- contents=self.make_data())
- self.assertIn(
- self.get_media_path(filename),
- FileStorage.objects.list_stored_files())
-
- def test_list_stored_files_includes_referenced_files(self):
- self.make_upload_dir()
- storage = factory.make_file_storage()
- self.assertIn(
- storage.data.name, FileStorage.objects.list_stored_files())
-
- def test_list_referenced_files_lists_FileStorage_files(self):
- self.make_upload_dir()
- storage = factory.make_file_storage()
- self.assertIn(
- storage.data.name, FileStorage.objects.list_referenced_files())
-
- def test_list_referenced_files_excludes_unreferenced_files(self):
- filename = factory.getRandomString()
- factory.make_file(
- location=self.make_upload_dir(), name=filename,
- contents=self.make_data())
- self.assertNotIn(
- self.get_media_path(filename),
- FileStorage.objects.list_referenced_files())
-
- def test_list_referenced_files_uses_file_name_not_FileStorage_name(self):
- self.make_upload_dir()
- filename = factory.getRandomString()
- # The filename we're going to use is already taken. The file
- # we'll be looking at will have to have a different name.
- factory.make_file_storage(filename=filename)
- storage = factory.make_file_storage(filename=filename)
- # It's the name of the file, not the FileStorage.filename, that
- # is in list_referenced_files.
- self.assertIn(
- storage.data.name, FileStorage.objects.list_referenced_files())
-
- def test_is_old_returns_False_for_recent_file(self):
- filename = factory.getRandomString()
- path = factory.make_file(
- location=self.make_upload_dir(), name=filename,
- contents=self.make_data())
- age_file(path, FileStorage.objects.grace_time - 60)
- self.assertFalse(
- FileStorage.objects.is_old(self.get_media_path(filename)))
-
- def test_is_old_returns_True_for_old_file(self):
- filename = factory.getRandomString()
- path = factory.make_file(
- location=self.make_upload_dir(), name=filename,
- contents=self.make_data())
- age_file(path, FileStorage.objects.grace_time + 1)
- self.assertTrue(
- FileStorage.objects.is_old(self.get_media_path(filename)))
-
- def test_collect_garbage_deletes_garbage(self):
- filename = factory.getRandomString()
- path = factory.make_file(
- location=self.make_upload_dir(), name=filename,
- contents=self.make_data())
- age_file(path, FileStorage.objects.grace_time + 1)
- FileStorage.objects.collect_garbage()
- self.assertFalse(
- FileStorage.storage.exists(self.get_media_path(filename)))
-
- def test_grace_time_is_generous_but_not_unlimited(self):
- # Grace time for garbage collection is long enough that it won't
- # expire while the request that wrote it is still being handled.
- # But it won't keep a file around for ages. For instance, it'll
- # be more than 20 seconds, but less than a day.
- self.assertThat(FileStorage.objects.grace_time, GreaterThan(20))
- self.assertThat(FileStorage.objects.grace_time, LessThan(24 * 60 * 60))
-
- def test_collect_garbage_leaves_recent_files_alone(self):
- filename = factory.getRandomString()
- factory.make_file(
- location=self.make_upload_dir(), name=filename,
- contents=self.make_data())
- FileStorage.objects.collect_garbage()
- self.assertTrue(
- FileStorage.storage.exists(self.get_media_path(filename)))
-
- def test_collect_garbage_leaves_referenced_files_alone(self):
- self.make_upload_dir()
- storage = factory.make_file_storage()
- age_file(storage.data.path, FileStorage.objects.grace_time + 1)
- FileStorage.objects.collect_garbage()
- self.assertTrue(FileStorage.storage.exists(storage.data.name))
-
- def test_collect_garbage_tolerates_missing_upload_dir(self):
- # When MAAS is freshly installed, the upload directory is still
- # missing. But...
- FileStorage.objects.collect_garbage()
- # ...we get through garbage collection without breakage.
- pass
+ new_data, FileStorage.objects.get(filename=filename).content)