launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #15231
[Merge] lp:~rvb/maas/bug-1123986-1.2 into lp:maas/1.2
Raphaël Badin has proposed merging lp:~rvb/maas/bug-1123986-1.2 into lp:maas/1.2.
Commit message:
Backport revisions 1439, 1440, 1441, 1442 and 1443.
Requested reviews:
MAAS Maintainers (maas-maintainers)
Related bugs:
Bug #1123986 in MAAS: "multiple juju environments in maas"
https://bugs.launchpad.net/maas/+bug/1123986
For more details, see:
https://code.launchpad.net/~rvb/maas/bug-1123986-1.2/+merge/150410
--
https://code.launchpad.net/~rvb/maas/bug-1123986-1.2/+merge/150410
Your team MAAS Maintainers is requested to review the proposed merge of lp:~rvb/maas/bug-1123986-1.2 into lp:maas/1.2.
=== modified file 'src/maasserver/api.py'
--- src/maasserver/api.py 2013-02-14 15:06:58 +0000
+++ src/maasserver/api.py 2013-02-25 18:58:11 +0000
@@ -80,7 +80,10 @@
"store_node_power_parameters",
]
-from base64 import b64decode
+from base64 import (
+ b64decode,
+ b64encode,
+ )
from cStringIO import StringIO
from datetime import (
datetime,
@@ -1019,23 +1022,33 @@
return ('node_mac_handler', [node_system_id, mac_address])
-def get_file(handler, request):
+def get_file_by_name(handler, request):
"""Get a named file from the file storage.
:param filename: The exact name of the file you want to get.
:type filename: string
:return: The file is returned in the response content.
"""
- filename = request.GET.get("filename", None)
- if not filename:
- raise MAASAPIBadRequest("Filename not supplied")
+ filename = get_mandatory_param(request.GET, 'filename')
try:
- db_file = FileStorage.objects.get(filename=filename)
+ db_file = FileStorage.objects.filter(filename=filename).latest('id')
except FileStorage.DoesNotExist:
raise MAASAPINotFound("File not found")
return HttpResponse(db_file.content, status=httplib.OK)
+def get_file_by_key(handler, request):
+ """Get a file from the file storage using its key.
+
+ :param key: The exact key of the file you want to get.
+ :type key: string
+ :return: The file is returned in the response content.
+ """
+ key = get_mandatory_param(request.GET, 'key')
+ db_file = get_object_or_404(FileStorage, key=key)
+ return HttpResponse(db_file.content, status=httplib.OK)
+
+
class AnonFilesHandler(AnonymousOperationsHandler):
"""Anonymous file operations.
@@ -1050,7 +1063,10 @@
"""
create = read = update = delete = None
- get = operation(idempotent=True, exported_as='get')(get_file)
+ get_by_name = operation(
+ idempotent=True, exported_as='get')(get_file_by_name)
+ get_by_key = operation(
+ idempotent=True, exported_as='get_by_key')(get_file_by_key)
@classmethod
def resource_uri(cls, *args, **kwargs):
@@ -1059,16 +1075,54 @@
# DISPLAYED_FILES_FIELDS_OBJECT is the list of fields used when dumping
# lists of FileStorage objects.
-DISPLAYED_FILES_FIELDS = ('filename', )
-# DISPLAYED_FILES_FIELDS_OBJECT is the list of fields used when dumping
-# individual FileStorage objects.
-DISPLAYED_FILES_FIELDS_OBJECT = DISPLAYED_FILES_FIELDS + ('content', )
+DISPLAYED_FILES_FIELDS = ('filename', 'anon_resource_uri')
+
+
+def json_file_storage(stored_file, request):
+ # Convert stored_file into a json object: use the same fields used
+ # when serialising lists of object, plus the base64-encoded content.
+ dict_representation = {
+ fieldname: getattr(stored_file, fieldname)
+ for fieldname in DISPLAYED_FILES_FIELDS
+ }
+ # Encode the content as base64.
+ dict_representation['content'] = b64encode(
+ getattr(stored_file, 'content'))
+ # Emit the json for this object manually because, no matter what the
+ # piston documentation says, once a type is associated with a list
+ # of fields by piston's typemapper mechanism, there is no way to
+ # override that in a specific handler with 'fields' or 'exclude'.
+ emitter = JSONEmitter(dict_representation, typemapper, None)
+ stream = emitter.render(request)
+ return stream
+
+
+def get_owned_file_or_404(filename, user):
+ """Return the file named 'filename' owned by 'user'.
+
+ If there is no such file, try getting the corresponding unowned file
+ to be compatible with old versions of MAAS where all the files where
+ unowned.
+ """
+ stored_files = FileStorage.objects.filter(
+ filename=filename, owner=user)
+ # filename and owner are 'unique together', we can have either 0 or 1
+ # objects in 'stored_files'.
+ if len(stored_files) == 0:
+ # In order to support upgrading from installations where the notion
+ # of a file owner was not yet implemented, return non-owned files
+ # with this filename at a last resort option.
+ stored_files = FileStorage.objects.filter(
+ filename=filename, owner=None)
+ if len(stored_files) == 0:
+ raise Http404
+ return stored_files[0]
class FileHandler(OperationsHandler):
"""Manage a FileStorage object.
- The file is identified by its filename.
+ The file is identified by its filename and owner.
"""
model = FileStorage
fields = DISPLAYED_FILES_FIELDS
@@ -1078,21 +1132,18 @@
"""GET a FileStorage object as a json object.
The 'content' of the file is base64-encoded."""
- # 'content' is a BinaryField, its 'value' is a base64-encoded version
- # of its value.
- stored_files = FileStorage.objects.filter(filename=filename).values()
- # filename is a 'unique' field, we can have either 0 or 1 objects in
- # 'stored_files'.
- if len(stored_files) == 0:
- raise Http404
- stored_file = stored_files[0]
- # Emit the json for this object manually because, no matter what the
- # piston documentation says, once an type is associated with a list
- # of fields by piston's typemapper mechanism, there is no way to
- # override that in a specific handler with 'fields' or 'exclude'.
- emitter = JSONEmitter(
- stored_file, typemapper, None, DISPLAYED_FILES_FIELDS_OBJECT)
- stream = emitter.render(request)
+ try:
+ stored_file = get_owned_file_or_404(
+ filename=filename, user=request.user)
+ except Http404:
+ # In order to fix bug 1123986 we need to distinguish between
+ # a 404 returned when the file is not present and a 404 returned
+ # when the API endpoint is not present. We do this by setting
+ # a header: "Workaround: bug1123986".
+ response = HttpResponse("Not Found", status=404)
+ response["Workaround"] = "bug1123986"
+ return response
+ stream = json_file_storage(stored_file, request)
return HttpResponse(
stream, mimetype='application/json; charset=utf-8',
status=httplib.OK)
@@ -1100,7 +1151,8 @@
@operation(idempotent=False)
def delete(self, request, filename):
"""Delete a FileStorage object."""
- stored_file = get_object_or_404(FileStorage, filename=filename)
+ stored_file = get_owned_file_or_404(
+ filename=filename, user=request.user)
stored_file.delete()
return rc.DELETED
@@ -1117,7 +1169,10 @@
create = read = update = delete = None
anonymous = AnonFilesHandler
- get = operation(idempotent=True, exported_as='get')(get_file)
+ get_by_name = operation(
+ idempotent=True, exported_as='get')(get_file_by_name)
+ get_by_key = operation(
+ idempotent=True, exported_as='get_by_key')(get_file_by_key)
@operation(idempotent=False)
def add(self, request):
@@ -1141,7 +1196,7 @@
# As per the comment in FileStorage, this ought to deal in
# chunks instead of reading the file into memory, but large
# files are not expected.
- FileStorage.objects.save_file(filename, uploaded_file)
+ FileStorage.objects.save_file(filename, uploaded_file, request.user)
return HttpResponse('', status=httplib.CREATED)
@operation(idempotent=True)
@@ -1155,7 +1210,7 @@
:type prefix: string
"""
prefix = request.GET.get("prefix", None)
- files = FileStorage.objects.all()
+ files = FileStorage.objects.filter(owner=request.user)
if prefix is not None:
files = files.filter(filename__startswith=prefix)
files = files.order_by('filename')
=== added file 'src/maasserver/migrations/0047_add_owner_to_filestorage.py'
--- src/maasserver/migrations/0047_add_owner_to_filestorage.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/migrations/0047_add_owner_to_filestorage.py 2013-02-25 18:58:11 +0000
@@ -0,0 +1,218 @@
+# -*- 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):
+ # Removing unique constraint on 'FileStorage', fields ['filename']
+ db.delete_unique(u'maasserver_filestorage', ['filename'])
+
+ # Adding field 'FileStorage.owner'
+ db.add_column(u'maasserver_filestorage', 'owner',
+ self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['auth.User'], null=True, blank=True),
+ keep_default=False)
+
+ # Adding unique constraint on 'FileStorage', fields ['owner', 'filename']
+ db.create_unique(u'maasserver_filestorage', ['owner_id', 'filename'])
+
+
+ def backwards(self, orm):
+ # Removing unique constraint on 'FileStorage', fields ['owner', 'filename']
+ db.delete_unique(u'maasserver_filestorage', ['owner_id', 'filename'])
+
+ # Deleting field 'FileStorage.owner'
+ db.delete_column(u'maasserver_filestorage', 'owner_id')
+
+ # Adding unique constraint on 'FileStorage', fields ['filename']
+ db.create_unique(u'maasserver_filestorage', ['filename'])
+
+
+ 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'nodegroup', 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'}),
+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
+ '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': {'unique_together': "((u'filename', u'owner'),)", 'object_name': 'FileStorage'},
+ 'content': ('metadataserver.fields.BinaryField', [], {}),
+ 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'null': 'True', 'blank': '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''", 'unique': 'True', '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-dd7d4f0e-79b7-11e2-90c3-3c970e0e56dc'", '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'}),
+ 'maas_url': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': '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', [], {'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'kernel_opts': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': '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': '1361184137L'}),
+ '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']
\ No newline at end of file
=== added file 'src/maasserver/migrations/0048_add_key_to_filestorage.py'
--- src/maasserver/migrations/0048_add_key_to_filestorage.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/migrations/0048_add_key_to_filestorage.py 2013-02-25 18:58:11 +0000
@@ -0,0 +1,213 @@
+# -*- coding: utf-8 -*-
+import datetime
+
+from django.db import models
+from maasserver.models import filestorage
+from south.db import db
+from south.v2 import SchemaMigration
+
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ # Adding field 'FileStorage.key'
+ db.add_column(u'maasserver_filestorage', 'key',
+ self.gf('django.db.models.fields.CharField')(default=u'', max_length=36),
+ keep_default=False)
+
+ # Populate FileStorage.key with random keys.
+ for file_storage in orm.FileStorage.objects.all():
+ file_storage.key = filestorage.generate_filestorage_key()
+ file_storage.save()
+
+
+ def backwards(self, orm):
+ # Deleting field 'FileStorage.key'
+ db.delete_column(u'maasserver_filestorage', 'key')
+
+
+ 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'nodegroup', 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'}),
+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
+ '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': {'unique_together': "((u'filename', u'owner'),)", 'object_name': 'FileStorage'},
+ 'content': ('metadataserver.fields.BinaryField', [], {}),
+ 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'key': ('django.db.models.fields.CharField', [], {'default': "u'f8de970a-79ba-11e2-9997-3c970e0e56dc'", 'max_length': '36'}),
+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'null': 'True', 'blank': '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''", 'unique': 'True', '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-f8dfe132-79ba-11e2-9997-3c970e0e56dc'", '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'}),
+ 'maas_url': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': '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', [], {'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'kernel_opts': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': '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': '1361185471L'}),
+ '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/0049_filestorage_key_unique.py'
--- src/maasserver/migrations/0049_filestorage_key_unique.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/migrations/0049_filestorage_key_unique.py 2013-02-25 18:58:11 +0000
@@ -0,0 +1,205 @@
+# -*- 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):
+ # Adding unique constraint on 'FileStorage', fields ['key']
+ db.create_unique(u'maasserver_filestorage', ['key'])
+
+
+ def backwards(self, orm):
+ # Removing unique constraint on 'FileStorage', fields ['key']
+ db.delete_unique(u'maasserver_filestorage', ['key'])
+
+
+ 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'nodegroup', 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'}),
+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
+ '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': {'unique_together': "((u'filename', u'owner'),)", 'object_name': 'FileStorage'},
+ 'content': ('metadataserver.fields.BinaryField', [], {}),
+ 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'key': ('django.db.models.fields.CharField', [], {'default': "u'1cbbd0e8-79bb-11e2-a69f-3c970e0e56dc'", 'unique': 'True', 'max_length': '36'}),
+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'null': 'True', 'blank': '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''", 'unique': 'True', '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-1cbad472-79bb-11e2-a69f-3c970e0e56dc'", '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'}),
+ 'maas_url': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': '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', [], {'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'kernel_opts': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': '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': '1361185532L'}),
+ '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']
\ No newline at end of file
=== modified file 'src/maasserver/models/filestorage.py'
--- src/maasserver/models/filestorage.py 2012-10-25 13:40:55 +0000
+++ src/maasserver/models/filestorage.py 2013-02-25 18:58:11 +0000
@@ -15,11 +15,17 @@
]
+from uuid import uuid1
+
+from django.contrib.auth.models import User
+from django.core.urlresolvers import reverse
from django.db.models import (
CharField,
+ ForeignKey,
Manager,
Model,
)
+from django.utils.http import urlencode
from maasserver import DefaultMeta
from maasserver.models.cleansave import CleanSave
from metadataserver.fields import (
@@ -43,37 +49,57 @@
old file will continue without iterruption.
"""
- def save_file(self, filename, file_object):
+ def save_file(self, filename, file_object, owner):
"""Save the file to the database.
- If a file of that name already existed, it will be replaced by the
- new contents.
+ If a file of that name/owner already existed, it will be replaced by
+ the new contents.
"""
# This probably ought to read in chunks but large files are
# not expected.
content = Bin(file_object.read())
storage, created = self.get_or_create(
- filename=filename, defaults={'content': content})
+ filename=filename, owner=owner, defaults={'content': content})
if not created:
storage.content = content
storage.save()
return storage
+def generate_filestorage_key():
+ return '%s' % uuid1()
+
+
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 filename: A file name to use for the data being stored.
+ :ivar owner: This file's owner..
:ivar content: The file's actual data.
"""
class Meta(DefaultMeta):
"""Needed for South to recognize this model."""
+ unique_together = ('filename', 'owner')
- filename = CharField(max_length=255, unique=True, editable=False)
+ filename = CharField(max_length=255, unique=False, editable=False)
content = BinaryField(null=False)
+ # owner can be None: this is to support upgrading existing
+ # installations where the files were not linked to users yet.
+ owner = ForeignKey(
+ User, default=None, blank=True, null=True, editable=False)
+ key = CharField(
+ max_length=36, unique=True, default=generate_filestorage_key,
+ editable=False)
objects = FileStorageManager()
def __unicode__(self):
return self.filename
+
+ @property
+ def anon_resource_uri(self):
+ """URI where the content of the file can be retrieved anonymously."""
+ params = {'op': 'get_by_key', 'key': self.key}
+ url = '%s?%s' % (reverse('files_handler'), urlencode(params))
+ return url
=== modified file 'src/maasserver/testing/factory.py'
--- src/maasserver/testing/factory.py 2012-12-04 02:31:07 +0000
+++ src/maasserver/testing/factory.py 2013-02-25 18:58:11 +0000
@@ -56,6 +56,30 @@
class Factory(maastesting.factory.Factory):
+ def make_file_upload(self, name=None, content=None):
+ """Create a file-like object for upload in http POST or PUT.
+
+ To upload a file using the Django test client, just include a
+ parameter that maps not to a string, but to a file upload as
+ produced by this method.
+
+ :param name: Name of the file to be uploaded. If omitted, one will
+ be made up.
+ :type name: `unicode`
+ :param content: Contents for the uploaded file. If omitted, some
+ contents will be made up.
+ :type content: `bytes`
+ :return: A file-like object, with the requested `content` and `name`.
+ """
+ if content is None:
+ content = self.getRandomString().encode('ascii')
+ if name is None:
+ name = self.make_name('file')
+ assert isinstance(content, bytes)
+ upload = BytesIO(content)
+ upload.name = name
+ return upload
+
def getRandomEnum(self, enum, but_not=None):
"""Pick a random item from an enumeration class.
@@ -297,13 +321,9 @@
admin.save()
return admin
- def make_file_storage(self, filename=None, content=None):
- if filename is None:
- filename = self.getRandomString(100)
- if content is None:
- content = self.getRandomString(1024).encode('ascii')
-
- return FileStorage.objects.save_file(filename, BytesIO(content))
+ def make_file_storage(self, filename=None, content=None, owner=None):
+ fake_file = self.make_file_upload(filename, content)
+ return FileStorage.objects.save_file(fake_file.name, fake_file, owner)
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 2013-02-14 15:06:58 +0000
+++ src/maasserver/tests/test_api.py 2013-02-25 18:58:11 +0000
@@ -47,7 +47,10 @@
reverse,
get_script_prefix,
)
-from django.http import QueryDict
+from django.http import (
+ Http404,
+ QueryDict,
+ )
from django.test.client import RequestFactory
from django.utils.http import urlquote_plus
from fixtures import (
@@ -67,6 +70,7 @@
find_nodegroup_for_pxeconfig_request,
get_oauth_token,
get_overrided_query_dict,
+ get_owned_file_or_404,
store_node_power_parameters,
)
from maasserver.enum import (
@@ -2699,37 +2703,85 @@
return self.client.get(self.get_uri('files/'), params)
+class CompatibilityUtilitiesTest(TestCase):
+
+ def test_get_owned_file_or_404_returns_owned_file(self):
+ user = factory.make_user()
+ filename = factory.make_name('filename')
+ factory.make_file_storage(filename=filename, owner=None)
+ filestorage = factory.make_file_storage(filename=filename, owner=user)
+
+ self.assertEqual(filestorage, get_owned_file_or_404(filename, user))
+
+ def test_get_owned_file_or_404_fallsback_to_nonowned_file(self):
+ user = factory.make_user()
+ filename = factory.make_name('filename')
+ factory.make_file_storage(
+ filename=factory.make_name('filename'), owner=user)
+ filestorage = factory.make_file_storage(filename=filename, owner=None)
+
+ self.assertEqual(filestorage, get_owned_file_or_404(filename, user))
+
+ def test_get_owned_file_or_404_raises_404_if_no_file(self):
+ user = factory.make_user()
+ filename = factory.make_name('filename')
+ factory.make_file_storage(
+ filename=factory.make_name('filename'), owner=user)
+ factory.make_file_storage(
+ filename=filename, owner=factory.make_user())
+
+ self.assertRaises(Http404, get_owned_file_or_404, filename, user)
+
+
class AnonymousFileStorageAPITest(FileStorageAPITestMixin, AnonAPITestCase):
def test_get_works_anonymously(self):
- 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)
- self.assertEqual(b"give me rope", response.content)
+ storage = factory.make_file_storage()
+ response = self.make_API_GET_request("get", storage.filename)
+
+ self.assertEqual(storage.content, response.content)
+ self.assertEqual(httplib.OK, response.status_code)
+
+ def test_get_fetches_the_most_recent_file(self):
+ filename = factory.make_name('file')
+ factory.make_file_storage(filename=filename, owner=factory.make_user())
+ storage = factory.make_file_storage(
+ filename=filename, owner=factory.make_user())
+ response = self.make_API_GET_request("get", filename)
+
+ self.assertEqual(httplib.OK, response.status_code)
+ self.assertEqual(storage.content, response.content)
+
+ def test_get_by_key_works_anonymously(self):
+ storage = factory.make_file_storage()
+ response = self.client.get(
+ self.get_uri('files/'), {'key': storage.key, 'op': 'get_by_key'})
+
+ self.assertEqual(httplib.OK, response.status_code)
+ self.assertEqual(storage.content, response.content)
+
+ def test_anon_resource_uri_allows_anonymous_access(self):
+ storage = factory.make_file_storage()
+ response = self.client.get(storage.anon_resource_uri)
+ self.assertEqual(httplib.OK, response.status_code)
+ self.assertEqual(storage.content, response.content)
def test_anon_cannot_list_files(self):
- factory.make_file_storage(
- filename="filename", content=b"test content")
+ factory.make_file_storage()
response = self.make_API_GET_request("list")
# The 'list' operation is not available to anon users.
self.assertEqual(httplib.BAD_REQUEST, response.status_code)
def test_anon_cannot_get_file(self):
- filename = factory.make_name("file")
- factory.make_file_storage(
- filename=filename, content=b"test file content")
+ storage = factory.make_file_storage()
response = self.client.get(
- reverse('file_handler', args=[filename]))
+ reverse('file_handler', args=[storage.filename]))
self.assertEqual(httplib.UNAUTHORIZED, response.status_code)
def test_anon_cannot_delete_file(self):
- filename = factory.make_name('file')
- factory.make_file_storage(
- filename=filename, content=b"test content")
+ storage = factory.make_file_storage()
response = self.client.delete(
- reverse('file_handler', args=[filename]))
+ reverse('file_handler', args=[storage.filename]))
self.assertEqual(httplib.UNAUTHORIZED, response.status_code)
@@ -2808,7 +2860,7 @@
self.assertEqual(httplib.BAD_REQUEST, response.status_code)
self.assertIn('text/plain', response['Content-Type'])
- self.assertEqual("Filename not supplied", response.content)
+ self.assertEqual("No provided filename!", response.content)
def test_get_file_fails_with_missing_file(self):
response = self.make_API_GET_request("get", filename="missingfilename")
@@ -2821,19 +2873,28 @@
filenames = ["myfiles/a", "myfiles/z", "myfiles/b"]
for filename in filenames:
factory.make_file_storage(
- filename=filename, content=b"test content")
+ filename=filename, content=b"test content",
+ owner=self.logged_in_user)
response = self.make_API_GET_request("list")
self.assertEqual(httplib.OK, response.status_code)
parsed_results = json.loads(response.content)
filenames = [result['filename'] for result in parsed_results]
self.assertEqual(sorted(filenames), filenames)
+ def test_list_files_filters_by_owner(self):
+ factory.make_file_storage(owner=factory.make_user())
+ response = self.make_API_GET_request("list")
+ self.assertEqual(httplib.OK, response.status_code)
+ parsed_results = json.loads(response.content)
+ self.assertEqual([], parsed_results)
+
def test_list_files_lists_files_with_prefix(self):
filenames_with_prefix = ["prefix-file1", "prefix-file2"]
filenames = filenames_with_prefix + ["otherfile", "otherfile2"]
for filename in filenames:
factory.make_file_storage(
- filename=filename, content=b"test content")
+ filename=filename, content=b"test content",
+ owner=self.logged_in_user)
response = self.client.get(
self.get_uri('files/'), {"op": "list", "prefix": "prefix-"})
self.assertEqual(httplib.OK, response.status_code)
@@ -2843,7 +2904,8 @@
def test_list_files_does_not_include_file_content(self):
factory.make_file_storage(
- filename="filename", content=b"test content")
+ filename="filename", content=b"test content",
+ owner=self.logged_in_user)
response = self.make_API_GET_request("list")
parsed_results = json.loads(response.content)
self.assertNotIn('content', parsed_results[0].keys())
@@ -2851,7 +2913,8 @@
def test_files_resource_uri_are_url_escaped(self):
filename = "&*!c/%//filename/"
factory.make_file_storage(
- filename=filename, content=b"test content")
+ filename=filename, content=b"test content",
+ owner=self.logged_in_user)
response = self.make_API_GET_request("list")
parsed_results = json.loads(response.content)
resource_uri = parsed_results[0]['resource_uri']
@@ -2862,21 +2925,100 @@
def test_get_file_returns_file_object_with_content_base64_encoded(self):
filename = factory.make_name("file")
content = sample_binary_data
- factory.make_file_storage(filename=filename, content=content)
- response = self.client.get(
- reverse('file_handler', args=[filename]))
- parsed_result = json.loads(response.content)
- self.assertEqual(
- (filename, content),
- (
- parsed_result['filename'],
- b64decode(parsed_result['content'])
- ))
+ factory.make_file_storage(
+ filename=filename, content=content, owner=self.logged_in_user)
+ response = self.client.get(
+ reverse('file_handler', args=[filename]))
+ parsed_result = json.loads(response.content)
+ self.assertEqual(
+ (filename, content),
+ (
+ parsed_result['filename'],
+ b64decode(parsed_result['content'])
+ ))
+
+ def test_get_file_returns_owned_file(self):
+ # If both an owned file and a non-owned file are present (with the
+ # same name), the owned file is returned.
+ filename = factory.make_name("file")
+ factory.make_file_storage(filename=filename, owner=None)
+ content = sample_binary_data
+ storage = factory.make_file_storage(
+ filename=filename, content=content, owner=self.logged_in_user)
+ response = self.client.get(
+ reverse('file_handler', args=[filename]))
+ parsed_result = json.loads(response.content)
+ self.assertEqual(
+ (filename, storage.anon_resource_uri, content),
+ (
+ parsed_result['filename'],
+ parsed_result['anon_resource_uri'],
+ b64decode(parsed_result['content'])
+ ))
+
+ def test_get_file_returns_file_object_with_anon_owner(self):
+ # To be compatible with the MAAS provider present in Juju as of
+ # revision 616 (lp:juju), reading a file returns the non-owned files
+ # if no owned file can be found.
+ filename = factory.make_name("file")
+ content = sample_binary_data
+ factory.make_file_storage(
+ filename=filename, content=content, owner=None)
+ response = self.client.get(
+ reverse('file_handler', args=[filename]))
+ parsed_result = json.loads(response.content)
+ self.assertEqual(
+ (filename, content),
+ (
+ parsed_result['filename'],
+ b64decode(parsed_result['content'])
+ ))
+
+ def test_get_file_returning_404_file_includes_header(self):
+ # In order to fix bug 1123986 we need to distinguish between
+ # a 404 returned when the file is not present and a 404 returned
+ # when the API endpoint is not present. We do this by setting
+ # a header: "Workaround: bug1123986".
+ response = self.client.get(
+ reverse('file_handler', args=[factory.make_name("file")]))
+ self.assertThat(
+ (
+ response.status_code,
+ response.items(),
+ ),
+ MatchesListwise(
+ (
+ Equals(httplib.NOT_FOUND),
+ Contains(('Workaround', 'bug1123986')),
+ )),
+ response)
+
+ def test_delete_filters_by_owner(self):
+ storage = factory.make_file_storage(owner=factory.make_user())
+ response = self.client.delete(
+ reverse('file_handler', args=[storage.filename]))
+ self.assertEqual(httplib.NOT_FOUND, response.status_code)
+ files = FileStorage.objects.filter(filename=storage.filename)
+ self.assertEqual([storage], list(files))
def test_delete_file_deletes_file(self):
filename = factory.make_name('file')
factory.make_file_storage(
- filename=filename, content=b"test content")
+ filename=filename, content=b"test content",
+ owner=self.logged_in_user)
+ response = self.client.delete(
+ reverse('file_handler', args=[filename]))
+ self.assertEqual(httplib.NO_CONTENT, response.status_code)
+ files = FileStorage.objects.filter(filename=filename)
+ self.assertEqual([], list(files))
+
+ def test_delete_file_deletes_file_with_anon_owner(self):
+ # To be compatible with the MAAS provider present in Juju as of
+ # revision 616 (lp:juju), deleting a file deletes the non-owned file
+ # if no owned file can be found.
+ filename = factory.make_name('file')
+ factory.make_file_storage(
+ filename=filename, content=b"test content", owner=None)
response = self.client.delete(
reverse('file_handler', args=[filename]))
self.assertEqual(httplib.NO_CONTENT, response.status_code)
=== modified file 'src/maasserver/tests/test_filestorage.py'
--- src/maasserver/tests/test_filestorage.py 2012-10-25 13:40:55 +0000
+++ src/maasserver/tests/test_filestorage.py 2013-02-25 18:58:11 +0000
@@ -40,10 +40,12 @@
def test_save_file_creates_storage(self):
filename = factory.getRandomString()
content = self.make_data()
- storage = FileStorage.objects.save_file(filename, BytesIO(content))
+ user = factory.make_user()
+ storage = FileStorage.objects.save_file(
+ filename, BytesIO(content), user)
self.assertEqual(
- (filename, content),
- (storage.filename, storage.content))
+ (filename, content, user),
+ (storage.filename, storage.content, storage.owner))
def test_storage_can_be_retrieved(self):
filename = factory.getRandomString()
@@ -84,3 +86,13 @@
self.assertEqual(old_storage.filename, new_storage.filename)
self.assertEqual(
new_data, FileStorage.objects.get(filename=filename).content)
+
+ def test_key_gets_generated(self):
+ # The generated system_id looks good.
+ storage = factory.make_file_storage()
+ self.assertEqual(len(storage.key), 36)
+
+ def test_key_includes_random_part(self):
+ storage1 = factory.make_file_storage()
+ storage2 = factory.make_file_storage()
+ self.assertNotEqual(storage1.key, storage2.key)