Merge lp:~blake-rouse/maas/largefile-model into lp:~maas-committers/maas/trunk

Proposed by Blake Rouse
Status: Merged
Merged at revision: 2662
Proposed branch: lp:~blake-rouse/maas/largefile-model
Merge into: lp:~maas-committers/maas/trunk
Prerequisite: lp:~blake-rouse/maas/large-object-store
Diff against target: 650 lines (+575/-4)
5 files modified
src/maasserver/migrations/0097_add_largefile_model.py (+344/-0)
src/maasserver/models/__init__.py (+5/-3)
src/maasserver/models/largefile.py (+108/-0)
src/maasserver/models/tests/test_largefile.py (+92/-0)
src/maasserver/testing/factory.py (+26/-1)
To merge this branch: bzr merge lp:~blake-rouse/maas/largefile-model
Reviewer Review Type Date Requested Status
Graham Binns (community) Approve
Jason Hobbs (community) Needs Information
Review via email: mp+229949@code.launchpad.net

This proposal supersedes a proposal from 2014-08-07.

Commit message

Added LargeFile model, that will be used by the incoming BootResource model.

Description of the change

This will be used by the BootResource models to hold the data for the boot images. This design allows multiple files from simplestreams to reference the same content, which is almost always the case for "boot-kernel" and "di-kernel".

Also provides the ability to get the current progress of the file being stored. This will allow the UI to show progress will downloading the boot images.

To post a comment you must log in.
Revision history for this message
Jason Hobbs (jason-hobbs) wrote :

This mostly looks good - I just have a few questions inline, and some minor things to change. If you have good answers, I'll approve.

review: Needs Information
Revision history for this message
Blake Rouse (blake-rouse) :
Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Thursday 07 Aug 2014 14:37:22 you wrote:
> Also provides the ability to get the current progress of the file being
> stored. This will allow the UI to show progress will downloading the boot
> images.

Just a small point, but see src/maasserver/models/downloadprogress.py and
consider whether you need to use that or deprecate it (it's not complete
IIRC).

Revision history for this message
Jason Hobbs (jason-hobbs) wrote :

On Thu, Aug 7, 2014 at 9:51 PM, Blake Rouse <email address hidden>
wrote:

> '\0' terminating character.
>
> > + ('sha256', self.gf('django.db.models.fields.CharField')(unique=True,
> max_length=65)),
>

What '\0' terminating character? This isn't C...

Jason

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

Thanks for this branch Blake; couple of nitpicks from me, but no blockers.

review: Approve
Revision history for this message
Jason Hobbs (jason-hobbs) wrote :
Download full text (41.0 KiB)

This branch should not land with the sha256sum field size 65. It should
definitely be 64.

On Fri, Aug 8, 2014 at 12:46 PM, Graham Binns <email address hidden>
wrote:

> Review: Approve
>
> Thanks for this branch Blake; couple of nitpicks from me, but no blockers.
>
> Diff comments:
>
> > === added file 'src/maasserver/migrations/0097_add_largefile_model.py'
> > --- src/maasserver/migrations/0097_add_largefile_model.py 1970-01-01
> 00:00:00 +0000
> > +++ src/maasserver/migrations/0097_add_largefile_model.py 2014-08-07
> 19:58:22 +0000
> > @@ -0,0 +1,344 @@
> > +from django.db import models
> > +from south.db import db
> > +# -*- coding: utf-8 -*-
> > +from south.utils import datetime_utils as datetime
> > +from south.v2 import SchemaMigration
> > +
> > +
> > +class Migration(SchemaMigration):
> > +
> > + def forwards(self, orm):
> > + # Adding model 'LargeFile'
> > + db.create_table(u'maasserver_largefile', (
> > + (u'id', self.gf
> ('django.db.models.fields.AutoField')(primary_key=True)),
> > + ('created', self.gf
> ('django.db.models.fields.DateTimeField')()),
> > + ('updated', self.gf
> ('django.db.models.fields.DateTimeField')()),
> > + ('sha256', self.gf('django.db.models.fields.CharField')(unique=True,
> max_length=65)),
> > + ('total_size', self.gf
> ('django.db.models.fields.IntegerField')()),
> > + ('content', self.gf
> ('maasserver.fields.LargeObjectField')()),
> > + ))
> > + db.send_create_signal(u'maasserver', ['LargeFile'])
> > +
> > +
> > + def backwards(self, orm):
> > + # Deleting model 'LargeFile'
> > + db.delete_table(u'maasserver_largefile')
> > +
> > +
> > + models = {
> > + u'auth.group': {
> > + 'Meta': {'object_name': 'Group'},
> > + u'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':
> u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
> > + },
> > + u'auth.permission': {
> > + 'Meta': {'ordering': "(u'content_type__app_label',
> u'content_type__model', u'codename')", 'unique_together':
> "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
> > + 'codename': ('django.db.models.fields.CharField', [],
> {'max_length': '100'}),
> > + 'content_type':
> ('django.db.models.fields.related.ForeignKey', [], {'to':
> u"orm['contenttypes.ContentType']"}),
> > + u'id': ('django.db.models.fields.AutoField', [],
> {'primary_key': 'True'}),
> > + 'name': ('django.db.models.fields.CharField', [],
> {'max_length': '50'})
> > + },
> > + u'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': 'Tr...

Revision history for this message
Blake Rouse (blake-rouse) wrote :

Jason,

Changed sha256 field to 64 length.

This command reports it as 65, 'sha256sum setup.py | awk '{print $1}' | wc'. But you are correct python does not need the '\0'.

Revision history for this message
Jason Hobbs (jason-hobbs) wrote :

thanks!

On Fri, Aug 8, 2014 at 2:19 PM, Blake Rouse <email address hidden>
wrote:

> Jason,
>
> Changed sha256 field to 64 length.
>
> This command reports it as 65, 'sha256sum setup.py | awk '{print $1}' |
> wc'. But you are correct python does not need the '\0'.
> --
> https://code.launchpad.net/~blake-rouse/maas/largefile-model/+merge/229949
> You are reviewing the proposed merge of
> lp:~blake-rouse/maas/largefile-model into lp:maas.
>

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'src/maasserver/migrations/0097_add_largefile_model.py'
--- src/maasserver/migrations/0097_add_largefile_model.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/migrations/0097_add_largefile_model.py 2014-08-08 12:16:36 +0000
@@ -0,0 +1,344 @@
1from django.db import models
2from south.db import db
3# -*- coding: utf-8 -*-
4from south.utils import datetime_utils as datetime
5from south.v2 import SchemaMigration
6
7
8class Migration(SchemaMigration):
9
10 def forwards(self, orm):
11 # Adding model 'LargeFile'
12 db.create_table(u'maasserver_largefile', (
13 (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
14 ('created', self.gf('django.db.models.fields.DateTimeField')()),
15 ('updated', self.gf('django.db.models.fields.DateTimeField')()),
16 ('sha256', self.gf('django.db.models.fields.CharField')(unique=True, max_length=64)),
17 ('total_size', self.gf('django.db.models.fields.IntegerField')()),
18 ('content', self.gf('maasserver.fields.LargeObjectField')()),
19 ))
20 db.send_create_signal(u'maasserver', ['LargeFile'])
21
22
23 def backwards(self, orm):
24 # Deleting model 'LargeFile'
25 db.delete_table(u'maasserver_largefile')
26
27
28 models = {
29 u'auth.group': {
30 'Meta': {'object_name': 'Group'},
31 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
32 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
33 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
34 },
35 u'auth.permission': {
36 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
37 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
38 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
39 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
40 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
41 },
42 u'auth.user': {
43 'Meta': {'object_name': 'User'},
44 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
45 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75', 'blank': 'True'}),
46 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
47 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
48 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
49 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
50 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
51 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
52 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
53 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
54 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
55 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
56 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
57 },
58 u'contenttypes.contenttype': {
59 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
60 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
61 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
62 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
63 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
64 },
65 u'maasserver.bootimage': {
66 'Meta': {'unique_together': "((u'nodegroup', u'osystem', u'architecture', u'subarchitecture', u'release', u'purpose', u'label'),)", 'object_name': 'BootImage'},
67 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
68 'created': ('django.db.models.fields.DateTimeField', [], {}),
69 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
70 'label': ('django.db.models.fields.CharField', [], {'default': "u'release'", 'max_length': '255'}),
71 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
72 'osystem': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
73 'purpose': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
74 'release': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
75 'subarchitecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
76 'supported_subarches': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
77 'updated': ('django.db.models.fields.DateTimeField', [], {}),
78 'xinstall_path': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'null': 'True', 'blank': 'True'}),
79 'xinstall_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '30', 'null': 'True', 'blank': 'True'})
80 },
81 u'maasserver.bootsource': {
82 'Meta': {'object_name': 'BootSource'},
83 'cluster': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']", 'null': 'True'}),
84 'created': ('django.db.models.fields.DateTimeField', [], {}),
85 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
86 'keyring_data': ('maasserver.fields.EditableBinaryField', [], {'blank': 'True'}),
87 'keyring_filename': ('django.db.models.fields.FilePathField', [], {'max_length': '100', 'blank': 'True'}),
88 'updated': ('django.db.models.fields.DateTimeField', [], {}),
89 'url': ('django.db.models.fields.URLField', [], {'max_length': '200'})
90 },
91 u'maasserver.bootsourceselection': {
92 'Meta': {'object_name': 'BootSourceSelection'},
93 'arches': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'text'", 'null': 'True', 'blank': 'True'}),
94 'boot_source': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.BootSource']"}),
95 'created': ('django.db.models.fields.DateTimeField', [], {}),
96 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
97 'labels': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'text'", 'null': 'True', 'blank': 'True'}),
98 'release': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'blank': 'True'}),
99 'subarches': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'text'", 'null': 'True', 'blank': 'True'}),
100 'updated': ('django.db.models.fields.DateTimeField', [], {})
101 },
102 u'maasserver.componenterror': {
103 'Meta': {'object_name': 'ComponentError'},
104 'component': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
105 'created': ('django.db.models.fields.DateTimeField', [], {}),
106 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000'}),
107 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
108 'updated': ('django.db.models.fields.DateTimeField', [], {})
109 },
110 u'maasserver.config': {
111 'Meta': {'object_name': 'Config'},
112 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
113 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
114 'value': ('maasserver.fields.JSONObjectField', [], {'null': 'True'})
115 },
116 u'maasserver.dhcplease': {
117 'Meta': {'object_name': 'DHCPLease'},
118 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
119 'ip': ('maasserver.fields.MAASIPAddressField', [], {'unique': 'True', 'max_length': '39'}),
120 'mac': ('maasserver.fields.MACAddressField', [], {}),
121 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"})
122 },
123 u'maasserver.downloadprogress': {
124 'Meta': {'object_name': 'DownloadProgress'},
125 'bytes_downloaded': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
126 'created': ('django.db.models.fields.DateTimeField', [], {}),
127 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000', 'blank': 'True'}),
128 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
129 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
130 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
131 'size': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
132 'updated': ('django.db.models.fields.DateTimeField', [], {})
133 },
134 u'maasserver.event': {
135 'Meta': {'object_name': 'Event'},
136 'created': ('django.db.models.fields.DateTimeField', [], {}),
137 'description': ('django.db.models.fields.TextField', [], {'default': "u''", 'blank': 'True'}),
138 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
139 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}),
140 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.EventType']"}),
141 'updated': ('django.db.models.fields.DateTimeField', [], {})
142 },
143 u'maasserver.eventtype': {
144 'Meta': {'object_name': 'EventType'},
145 'created': ('django.db.models.fields.DateTimeField', [], {}),
146 'description': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
147 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
148 'level': ('django.db.models.fields.IntegerField', [], {}),
149 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
150 'updated': ('django.db.models.fields.DateTimeField', [], {})
151 },
152 u'maasserver.filestorage': {
153 'Meta': {'unique_together': "((u'filename', u'owner'),)", 'object_name': 'FileStorage'},
154 'content': ('metadataserver.fields.BinaryField', [], {'blank': 'True'}),
155 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
156 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
157 'key': ('django.db.models.fields.CharField', [], {'default': "u'aa7f5dec-1e3c-11e4-87da-bcee7b78dc5b'", 'unique': 'True', 'max_length': '36'}),
158 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
159 },
160 u'maasserver.largefile': {
161 'Meta': {'object_name': 'LargeFile'},
162 'content': ('maasserver.fields.LargeObjectField', [], {}),
163 'created': ('django.db.models.fields.DateTimeField', [], {}),
164 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
165 'sha256': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'}),
166 'total_size': ('django.db.models.fields.IntegerField', [], {}),
167 'updated': ('django.db.models.fields.DateTimeField', [], {})
168 },
169 u'maasserver.licensekey': {
170 'Meta': {'unique_together': "((u'osystem', u'distro_series'),)", 'object_name': 'LicenseKey'},
171 'created': ('django.db.models.fields.DateTimeField', [], {}),
172 'distro_series': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
173 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
174 'license_key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
175 'osystem': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
176 'updated': ('django.db.models.fields.DateTimeField', [], {})
177 },
178 u'maasserver.macaddress': {
179 'Meta': {'ordering': "(u'created',)", 'object_name': 'MACAddress'},
180 'cluster_interface': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['maasserver.NodeGroupInterface']", 'null': 'True', 'blank': 'True'}),
181 'created': ('django.db.models.fields.DateTimeField', [], {}),
182 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
183 'ip_addresses': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.StaticIPAddress']", 'symmetrical': 'False', 'through': u"orm['maasserver.MACStaticIPAddressLink']", 'blank': 'True'}),
184 'mac_address': ('maasserver.fields.MACAddressField', [], {'unique': 'True'}),
185 'networks': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Network']", 'symmetrical': 'False', 'blank': 'True'}),
186 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}),
187 'updated': ('django.db.models.fields.DateTimeField', [], {})
188 },
189 u'maasserver.macstaticipaddresslink': {
190 'Meta': {'unique_together': "((u'ip_address', u'mac_address'),)", 'object_name': 'MACStaticIPAddressLink'},
191 'created': ('django.db.models.fields.DateTimeField', [], {}),
192 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
193 'ip_address': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.StaticIPAddress']", 'unique': 'True'}),
194 'mac_address': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.MACAddress']"}),
195 'nic_alias': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
196 'updated': ('django.db.models.fields.DateTimeField', [], {})
197 },
198 u'maasserver.network': {
199 'Meta': {'object_name': 'Network'},
200 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
201 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
202 'ip': ('maasserver.fields.MAASIPAddressField', [], {'unique': 'True', 'max_length': '39'}),
203 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
204 'netmask': ('maasserver.fields.MAASIPAddressField', [], {'max_length': '39'}),
205 'vlan_tag': ('django.db.models.fields.PositiveSmallIntegerField', [], {'unique': 'True', 'null': 'True', 'blank': 'True'})
206 },
207 u'maasserver.node': {
208 'Meta': {'object_name': 'Node'},
209 'agent_name': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'null': 'True', 'blank': 'True'}),
210 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '31'}),
211 'boot_type': ('django.db.models.fields.CharField', [], {'default': "u'fastpath'", 'max_length': '20'}),
212 'cpu_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
213 'created': ('django.db.models.fields.DateTimeField', [], {}),
214 'distro_series': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'blank': 'True'}),
215 'error': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
216 'error_description': ('django.db.models.fields.TextField', [], {'default': "u''", 'blank': 'True'}),
217 'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'unique': 'True', 'max_length': '255', 'blank': 'True'}),
218 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
219 'license_key': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}),
220 'memory': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
221 'netboot': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
222 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']", 'null': 'True'}),
223 'osystem': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'blank': 'True'}),
224 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}),
225 'power_parameters': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
226 'power_state': ('django.db.models.fields.CharField', [], {'default': "u'unknown'", 'max_length': '10'}),
227 'power_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '10', 'blank': 'True'}),
228 'routers': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'macaddr'", 'null': 'True', 'blank': 'True'}),
229 'status': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '10'}),
230 'storage': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
231 'system_id': ('django.db.models.fields.CharField', [], {'default': "u'node-aa7fb684-1e3c-11e4-87da-bcee7b78dc5b'", 'unique': 'True', 'max_length': '41'}),
232 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Tag']", 'symmetrical': 'False'}),
233 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Token']", 'null': 'True'}),
234 'updated': ('django.db.models.fields.DateTimeField', [], {}),
235 'zone': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Zone']", 'on_delete': 'models.SET_DEFAULT'})
236 },
237 u'maasserver.nodegroup': {
238 'Meta': {'object_name': 'NodeGroup'},
239 'api_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '18'}),
240 'api_token': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Token']", 'unique': 'True'}),
241 'cluster_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100', 'blank': 'True'}),
242 'created': ('django.db.models.fields.DateTimeField', [], {}),
243 'dhcp_key': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
244 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
245 'maas_url': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
246 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}),
247 'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
248 'updated': ('django.db.models.fields.DateTimeField', [], {}),
249 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'})
250 },
251 u'maasserver.nodegroupinterface': {
252 'Meta': {'unique_together': "((u'nodegroup', u'name'),)", 'object_name': 'NodeGroupInterface'},
253 'broadcast_ip': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
254 'created': ('django.db.models.fields.DateTimeField', [], {}),
255 'foreign_dhcp_ip': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
256 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
257 'interface': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
258 'ip': ('maasserver.fields.MAASIPAddressField', [], {'max_length': '39'}),
259 'ip_range_high': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
260 'ip_range_low': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
261 'management': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
262 'name': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
263 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
264 'router_ip': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
265 'static_ip_range_high': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
266 'static_ip_range_low': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
267 'subnet_mask': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
268 'updated': ('django.db.models.fields.DateTimeField', [], {})
269 },
270 u'maasserver.sshkey': {
271 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSHKey'},
272 'created': ('django.db.models.fields.DateTimeField', [], {}),
273 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
274 'key': ('django.db.models.fields.TextField', [], {}),
275 'updated': ('django.db.models.fields.DateTimeField', [], {}),
276 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
277 },
278 u'maasserver.sslkey': {
279 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSLKey'},
280 'created': ('django.db.models.fields.DateTimeField', [], {}),
281 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
282 'key': ('django.db.models.fields.TextField', [], {}),
283 'updated': ('django.db.models.fields.DateTimeField', [], {}),
284 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
285 },
286 u'maasserver.staticipaddress': {
287 'Meta': {'object_name': 'StaticIPAddress'},
288 'alloc_type': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
289 'created': ('django.db.models.fields.DateTimeField', [], {}),
290 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
291 'ip': ('maasserver.fields.MAASIPAddressField', [], {'unique': 'True', 'max_length': '39'}),
292 'updated': ('django.db.models.fields.DateTimeField', [], {}),
293 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
294 },
295 u'maasserver.tag': {
296 'Meta': {'object_name': 'Tag'},
297 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
298 'created': ('django.db.models.fields.DateTimeField', [], {}),
299 'definition': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
300 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
301 'kernel_opts': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
302 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
303 'updated': ('django.db.models.fields.DateTimeField', [], {})
304 },
305 u'maasserver.userprofile': {
306 'Meta': {'object_name': 'UserProfile'},
307 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
308 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'})
309 },
310 u'maasserver.zone': {
311 'Meta': {'ordering': "[u'name']", 'object_name': 'Zone'},
312 'created': ('django.db.models.fields.DateTimeField', [], {}),
313 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
314 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
315 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
316 'updated': ('django.db.models.fields.DateTimeField', [], {})
317 },
318 u'piston.consumer': {
319 'Meta': {'object_name': 'Consumer'},
320 'description': ('django.db.models.fields.TextField', [], {}),
321 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
322 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
323 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
324 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
325 'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '16'}),
326 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'consumers'", 'null': 'True', 'to': u"orm['auth.User']"})
327 },
328 u'piston.token': {
329 'Meta': {'object_name': 'Token'},
330 'callback': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
331 'callback_confirmed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
332 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Consumer']"}),
333 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
334 'is_approved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
335 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
336 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
337 'timestamp': ('django.db.models.fields.IntegerField', [], {'default': '1407420663L'}),
338 'token_type': ('django.db.models.fields.IntegerField', [], {}),
339 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'tokens'", 'null': 'True', 'to': u"orm['auth.User']"}),
340 'verifier': ('django.db.models.fields.CharField', [], {'max_length': '10'})
341 }
342 }
343
344 complete_apps = ['maasserver']
0345
=== modified file 'src/maasserver/models/__init__.py'
--- src/maasserver/models/__init__.py 2014-07-18 15:45:22 +0000
+++ src/maasserver/models/__init__.py 2014-08-08 12:16:36 +0000
@@ -22,6 +22,7 @@
22 'DownloadProgress',22 'DownloadProgress',
23 'Event',23 'Event',
24 'FileStorage',24 'FileStorage',
25 'LargeFile',
25 'LicenseKey',26 'LicenseKey',
26 'logger',27 'logger',
27 'MACAddress',28 'MACAddress',
@@ -56,6 +57,7 @@
56from maasserver.models.event import Event57from maasserver.models.event import Event
57from maasserver.models.eventtype import EventType58from maasserver.models.eventtype import EventType
58from maasserver.models.filestorage import FileStorage59from maasserver.models.filestorage import FileStorage
60from maasserver.models.largefile import LargeFile
59from maasserver.models.licensekey import LicenseKey61from maasserver.models.licensekey import LicenseKey
60from maasserver.models.macaddress import MACAddress62from maasserver.models.macaddress import MACAddress
61from maasserver.models.macipaddresslink import MACStaticIPAddressLink63from maasserver.models.macipaddresslink import MACStaticIPAddressLink
@@ -77,9 +79,9 @@
77# export in __all__.79# export in __all__.
78ignore_unused(80ignore_unused(
79 BootImage, ComponentError, Config, DHCPLease, DownloadProgress,81 BootImage, ComponentError, Config, DHCPLease, DownloadProgress,
80 Event, EventType, FileStorage, LicenseKey, StaticIPAddress, MACAddress,82 Event, EventType, FileStorage, LargeFile, LicenseKey, StaticIPAddress,
81 MACStaticIPAddressLink, Network, NodeGroup, SSHKey, Tag, UserProfile,83 MACAddress, MACStaticIPAddressLink, Network, NodeGroup, SSHKey, Tag,
82 NodeGroupInterface, Zone, logger)84 UserProfile, NodeGroupInterface, Zone, logger)
8385
8486
85# Connect the 'create_user' method to the post save signal of User.87# Connect the 'create_user' method to the post save signal of User.
8688
=== added file 'src/maasserver/models/largefile.py'
--- src/maasserver/models/largefile.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/models/largefile.py 2014-08-08 12:16:36 +0000
@@ -0,0 +1,108 @@
1# Copyright 2014 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Large file storage."""
5
6from __future__ import (
7 absolute_import,
8 print_function,
9 unicode_literals,
10 )
11
12str = None
13
14__metaclass__ = type
15__all__ = [
16 'LargeFile',
17 ]
18
19import os
20
21from django.db.models import (
22 BigIntegerField,
23 CharField,
24 Manager,
25 )
26from django.db.models.signals import post_delete
27from django.dispatch import receiver
28from maasserver import DefaultMeta
29from maasserver.fields import LargeObjectField
30from maasserver.models.cleansave import CleanSave
31from maasserver.models.timestampedmodel import TimestampedModel
32from maasserver.utils.orm import get_one
33
34
35class FileStorageManager(Manager):
36 """Manager for `LargeFile` objects."""
37
38 def has_file(self, sha256):
39 """True if file with sha256 value exists."""
40 return self.filter(sha256=sha256).exists()
41
42 def get_file(self, sha256):
43 """Return file based on SHA256 value."""
44 return get_one(self.filter(sha256=sha256))
45
46
47class LargeFile(CleanSave, TimestampedModel):
48 """Files that are stored in the large object storage.
49
50 Only unique files are stored in the database, as only one sha256 value
51 can exist per file. This provides data deduplication on the file level.
52
53 Currently only used by `BootResourceFile`. This speeds up the import
54 process by only saving unique files.
55
56 :ivar sha256: Calculated SHA256 value of `content`.
57 :ivar total_size: Final size of `content`. The data might currently
58 be saving, so total_size could be larger than `size`. `size` should
59 never be larger than `total_size`.
60 :ivar content: File data.
61 """
62
63 class Meta(DefaultMeta):
64 """Needed for South to recognize this model."""
65
66 objects = FileStorageManager()
67
68 sha256 = CharField(max_length=64, unique=True, editable=False)
69
70 total_size = BigIntegerField(editable=False)
71
72 # content is stored directly in the database, in the large object storage.
73 # Max file storage size is 4TB.
74 content = LargeObjectField()
75
76 @property
77 def size(self):
78 """Size of content."""
79 with self.content.open('rb') as stream:
80 stream.seek(0, os.SEEK_END)
81 size = stream.tell()
82 return size
83
84 @property
85 def progress(self):
86 """Precentage of `content` saved."""
87 if self.size <= 0:
88 # Handle division of zero
89 return 0
90 return self.total_size / float(self.size)
91
92 @property
93 def complete(self):
94 """`content` has been completely saved."""
95 return (self.total_size == self.size)
96
97
98@receiver(post_delete)
99def delete_large_object(sender, instance, **kwargs):
100 """Delete the large object when the `LargeFile` is deleted.
101
102 This is done using the `post_delete` signal instead of overriding delete
103 on `LargeFile`, so it works correctly for both the model and
104 `QuerySet`.
105 """
106 if sender == LargeFile:
107 if instance.content is not None:
108 instance.content.unlink()
0109
=== added file 'src/maasserver/models/tests/test_largefile.py'
--- src/maasserver/models/tests/test_largefile.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/models/tests/test_largefile.py 2014-08-08 12:16:36 +0000
@@ -0,0 +1,92 @@
1# Copyright 2014 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for :class:`LargeFile`."""
5
6from __future__ import (
7 absolute_import,
8 print_function,
9 unicode_literals,
10 )
11
12str = None
13
14__metaclass__ = type
15__all__ = []
16
17from random import randint
18
19from maasserver.models.largefile import LargeFile
20from maasserver.testing.factory import factory
21from maasserver.testing.testcase import MAASServerTestCase
22from maastesting.matchers import MockCalledOnceWith
23
24
25class TestLargeFileManager(MAASServerTestCase):
26
27 def test_has_file(self):
28 largefile = factory.make_large_file()
29 self.assertTrue(LargeFile.objects.has_file(largefile.sha256))
30
31 def test_get_file(self):
32 largefile = factory.make_large_file()
33 obj = LargeFile.objects.get_file(largefile.sha256)
34 self.assertEqual(largefile, obj)
35
36
37class TestLargeFile(MAASServerTestCase):
38
39 def test_content(self):
40 size = randint(512, 1024)
41 content = factory.make_string(size=size)
42 largefile = factory.make_large_file(content, size=size)
43 with largefile.content.open('rb') as stream:
44 data = stream.read()
45 self.assertEqual(content, data)
46
47 def test_empty_content(self):
48 size = 0
49 content = ""
50 largefile = factory.make_large_file(content, size=size)
51 with largefile.content.open('rb') as stream:
52 data = stream.read()
53 self.assertEqual(content, data)
54
55 def test_size(self):
56 size = randint(512, 1024)
57 total_size = randint(1025, 2048)
58 content = factory.make_string(size=size)
59 largefile = factory.make_large_file(content, size=total_size)
60 self.assertEqual(size, largefile.size)
61
62 def test_progress(self):
63 size = randint(512, 1024)
64 total_size = randint(1025, 2048)
65 content = factory.make_string(size=size)
66 largefile = factory.make_large_file(content, size=total_size)
67 self.assertEqual(total_size / float(size), largefile.progress)
68
69 def test_progress_of_empty_file(self):
70 size = 0
71 content = ""
72 largefile = factory.make_large_file(content, size=size)
73 self.assertEqual(0, largefile.progress)
74
75 def test_complete_returns_False_when_content_incomplete(self):
76 size = randint(512, 1024)
77 total_size = randint(1025, 2048)
78 content = factory.make_string(size=size)
79 largefile = factory.make_large_file(content, size=total_size)
80 self.assertFalse(largefile.complete)
81
82 def test_complete_returns_True_when_content_is_complete(self):
83 largefile = factory.make_large_file()
84 self.assertTrue(largefile.complete)
85
86 def test_delete_calls_unlink_on_content(self):
87 largefile = factory.make_large_file()
88 content = largefile.content
89 self.addCleanup(content.unlink)
90 unlink_mock = self.patch(content, 'unlink')
91 largefile.delete()
92 self.assertThat(unlink_mock, MockCalledOnceWith())
093
=== modified file 'src/maasserver/testing/factory.py'
--- src/maasserver/testing/factory.py 2014-08-01 11:19:44 +0000
+++ src/maasserver/testing/factory.py 2014-08-08 12:16:36 +0000
@@ -17,6 +17,7 @@
17 "Messages",17 "Messages",
18 ]18 ]
1919
20import hashlib
20from io import BytesIO21from io import BytesIO
21import logging22import logging
22import random23import random
@@ -32,7 +33,10 @@
32 NODEGROUPINTERFACE_MANAGEMENT,33 NODEGROUPINTERFACE_MANAGEMENT,
33 POWER_STATE,34 POWER_STATE,
34 )35 )
35from maasserver.fields import MAC36from maasserver.fields import (
37 LargeObjectFile,
38 MAC,
39 )
36from maasserver.models import (40from maasserver.models import (
37 BootImage,41 BootImage,
38 BootSource,42 BootSource,
@@ -42,6 +46,7 @@
42 Event,46 Event,
43 EventType,47 EventType,
44 FileStorage,48 FileStorage,
49 LargeFile,
45 LicenseKey,50 LicenseKey,
46 MACAddress,51 MACAddress,
47 MACStaticIPAddressLink,52 MACStaticIPAddressLink,
@@ -950,6 +955,26 @@
950 type = self.make_event_type()955 type = self.make_event_type()
951 return Event.objects.create(node=node, type=type)956 return Event.objects.create(node=node, type=type)
952957
958 def make_large_file(self, content=None, size=512):
959 """Create `LargeFile`.
960
961 :param content: Data to store in large file object.
962 :param size: Size of `content`. If `content` is None
963 then it will be a random string of this size. If content is
964 provided and `size` is not the same length, then it will
965 be an inprogress file.
966 """
967 if content is None:
968 content = factory.make_string(size=size)
969 sha256 = hashlib.sha256()
970 sha256.update(content)
971 sha256 = sha256.hexdigest()
972 largeobject = LargeObjectFile()
973 with largeobject.open('wb') as stream:
974 stream.write(content)
975 return LargeFile.objects.create(
976 sha256=sha256, total_size=size, content=largeobject)
977
953978
954# Create factory singleton.979# Create factory singleton.
955factory = Factory()980factory = Factory()