Merge lp:~blake-rouse/maas/large-object-store into lp:~maas-committers/maas/trunk

Proposed by Blake Rouse
Status: Merged
Approved by: Blake Rouse
Approved revision: no longer in the source branch.
Merged at revision: 2658
Proposed branch: lp:~blake-rouse/maas/large-object-store
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 320 lines (+233/-1)
4 files modified
src/maasserver/fields.py (+115/-0)
src/maasserver/tests/models.py (+6/-0)
src/maasserver/tests/test_api_node.py (+1/-1)
src/maasserver/tests/test_fields.py (+111/-0)
To merge this branch: bzr merge lp:~blake-rouse/maas/large-object-store
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+229938@code.launchpad.net

Commit message

New LargeObjectField, that stores data into postgres large object storage.

Description of the change

This will be used for storing the boot resource files inside of the regions db. This will allow for easy HA, once that is enabled.

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

This looks good. I think it needs a bit more test coverage, so I'm kind of +0.7 right now.

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

Made the requested changes.

Revision history for this message
Gavin Panella (allenap) :
Revision history for this message
Gavin Panella (allenap) wrote :

Tip top.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/maasserver/fields.py'
--- src/maasserver/fields.py 2014-07-08 07:33:43 +0000
+++ src/maasserver/fields.py 2014-08-07 18:42:43 +0000
@@ -31,12 +31,15 @@
3131
32from django.core.exceptions import ValidationError32from django.core.exceptions import ValidationError
33from django.core.validators import RegexValidator33from django.core.validators import RegexValidator
34from django.db import connections
34from django.db.models import (35from django.db.models import (
35 BinaryField,36 BinaryField,
36 Field,37 Field,
37 GenericIPAddressField,38 GenericIPAddressField,
39 IntegerField,
38 SubfieldBase,40 SubfieldBase,
39 )41 )
42from django.db.models.fields.subclassing import Creator
40from django.forms import (43from django.forms import (
41 CharField,44 CharField,
42 ModelChoiceField,45 ModelChoiceField,
@@ -93,6 +96,7 @@
93 "^maasserver\.fields\.XMLField",96 "^maasserver\.fields\.XMLField",
94 "^maasserver\.fields\.EditableBinaryField",97 "^maasserver\.fields\.EditableBinaryField",
95 "^maasserver\.fields\.MAASIPAddressField",98 "^maasserver\.fields\.MAASIPAddressField",
99 "^maasserver\.fields\.LargeObjectField",
96 ])100 ])
97101
98102
@@ -412,3 +416,114 @@
412 and force a 'inet' type field.416 and force a 'inet' type field.
413 """417 """
414 return 'inet'418 return 'inet'
419
420
421class LargeObjectFile(object):
422 """Large object file.
423
424 Proxy the access from this object to psycopg2.
425 """
426 def __init__(self, oid=0, field=None, instance=None, block_size=(1 << 16)):
427 self.oid = oid
428 self.field = field
429 self.instance = instance
430 self.block_size = block_size
431 self._lobject = None
432
433 def __getattr__(self, name):
434 if self._lobject is None:
435 raise IOError("LargeObjectFile is not opened.")
436 return getattr(self._lobject, name)
437
438 def __enter__(self, *args, **kwargs):
439 return self
440
441 def __exit__(self, *args, **kwargs):
442 self.close()
443
444 def __iter__(self):
445 return self
446
447 def open(self, mode="rwb", new_file=None, using="default"):
448 """Opens the internal large object instance."""
449 connection = connections[using]
450 self._lobject = connection.connection.lobject(
451 self.oid, mode, 0, new_file)
452 self.oid = self._lobject.oid
453 return self
454
455 def unlink(self):
456 """Removes the large object."""
457 if self._lobject is None:
458 # Need to open the lobject so we get a reference to it in the
459 # database, to perform the unlink.
460 self.open()
461 self.close()
462 self._lobject.unlink()
463 self._lobject = None
464 self.oid = 0
465
466 def next(self):
467 r = self.read(self.block_size)
468 if len(r) == 0:
469 raise StopIteration
470 return r
471
472
473class LargeObjectDescriptor(Creator):
474 """LargeObjectField descriptor."""
475
476 def __set__(self, instance, value):
477 value = self.field.to_python(value)
478 if value is not None:
479 if not isinstance(value, LargeObjectFile):
480 value = LargeObjectFile(value, self.field, instance)
481 instance.__dict__[self.field.name] = value
482
483
484class LargeObjectField(IntegerField):
485 """A field that stores large amounts of data into postgres large object
486 storage.
487
488 Internally the field on the model is an `oid` field, that returns a proxy
489 to the referenced large object.
490 """
491
492 def __init__(self, *args, **kwargs):
493 self.block_size = kwargs.pop('block_size', 1 << 16)
494 super(LargeObjectField, self).__init__(*args, **kwargs)
495
496 def db_type(self, connection):
497 """Returns the database column data type for LargeObjectField."""
498 # oid is the column type postgres uses to reference a large object
499 return 'oid'
500
501 def contribute_to_class(self, cls, name):
502 """Set the descriptor for the large object."""
503 super(LargeObjectField, self).contribute_to_class(cls, name)
504 setattr(cls, self.name, LargeObjectDescriptor(self))
505
506 def get_db_prep_value(self, value, connection=None, prepared=False):
507 """python -> db: `oid` value"""
508 if value is None:
509 return None
510 if isinstance(value, LargeObjectFile):
511 if value.oid > 0:
512 return value.oid
513 raise AssertionError(
514 "LargeObjectFile's oid must be greater than 0.")
515 raise AssertionError(
516 "Invalid LargeObjectField value (expected LargeObjectFile): '%s'"
517 % repr(value))
518
519 def to_python(self, value):
520 """db -> python: `LargeObjectFile`"""
521 if value is None:
522 return None
523 elif isinstance(value, LargeObjectFile):
524 return value
525 elif isinstance(value, (int, long)):
526 return LargeObjectFile(value, self, self.model, self.block_size)
527 raise AssertionError(
528 "Invalid LargeObjectField value (expected integer): '%s'"
529 % repr(value))
415530
=== modified file 'src/maasserver/tests/models.py'
--- src/maasserver/tests/models.py 2014-07-08 07:52:05 +0000
+++ src/maasserver/tests/models.py 2014-08-07 18:42:43 +0000
@@ -25,6 +25,7 @@
25 )25 )
26from maasserver.fields import (26from maasserver.fields import (
27 JSONObjectField,27 JSONObjectField,
28 LargeObjectField,
28 MAASIPAddressField,29 MAASIPAddressField,
29 XMLField,30 XMLField,
30 )31 )
@@ -73,3 +74,8 @@
7374
74class MAASIPAddressFieldModel(Model):75class MAASIPAddressFieldModel(Model):
75 ip_address = MAASIPAddressField()76 ip_address = MAASIPAddressField()
77
78
79class LargeObjectFieldModel(Model):
80 name = CharField(max_length=255, unique=False)
81 large_object = LargeObjectField(block_size=10)
7682
=== modified file 'src/maasserver/tests/test_api_node.py'
--- src/maasserver/tests/test_api_node.py 2014-08-05 10:13:38 +0000
+++ src/maasserver/tests/test_api_node.py 2014-08-07 18:42:43 +0000
@@ -18,7 +18,6 @@
18from cStringIO import StringIO18from cStringIO import StringIO
19import httplib19import httplib
20import json20import json
21from netaddr import IPAddress
22import sys21import sys
2322
24import bson23import bson
@@ -53,6 +52,7 @@
53 NodeUserData,52 NodeUserData,
54 )53 )
55from metadataserver.nodeinituser import get_node_init_user54from metadataserver.nodeinituser import get_node_init_user
55from netaddr import IPAddress
56from provisioningserver.utils import map_enum56from provisioningserver.utils import map_enum
5757
5858
5959
=== modified file 'src/maasserver/tests/test_fields.py'
--- src/maasserver/tests/test_fields.py 2014-07-16 14:12:13 +0000
+++ src/maasserver/tests/test_fields.py 2014-08-07 18:42:43 +0000
@@ -27,6 +27,8 @@
27from maasserver.enum import NODEGROUPINTERFACE_MANAGEMENT27from maasserver.enum import NODEGROUPINTERFACE_MANAGEMENT
28from maasserver.fields import (28from maasserver.fields import (
29 EditableBinaryField,29 EditableBinaryField,
30 LargeObjectField,
31 LargeObjectFile,
30 MAC,32 MAC,
31 NodeGroupFormField,33 NodeGroupFormField,
32 register_mac_type,34 register_mac_type,
@@ -42,10 +44,13 @@
42from maasserver.testing.testcase import MAASServerTestCase44from maasserver.testing.testcase import MAASServerTestCase
43from maasserver.tests.models import (45from maasserver.tests.models import (
44 JSONFieldModel,46 JSONFieldModel,
47 LargeObjectFieldModel,
45 MAASIPAddressFieldModel,48 MAASIPAddressFieldModel,
46 XMLFieldModel,49 XMLFieldModel,
47 )50 )
48from maastesting.djangotestcase import TestModelMixin51from maastesting.djangotestcase import TestModelMixin
52from maastesting.matchers import MockCalledOnceWith
53from psycopg2 import OperationalError
49from psycopg2.extensions import ISQLQuote54from psycopg2.extensions import ISQLQuote
5055
5156
@@ -401,3 +406,109 @@
401 results = MAASIPAddressFieldModel.objects.filter(406 results = MAASIPAddressFieldModel.objects.filter(
402 ip_address__lte='192.0.2.100')407 ip_address__lte='192.0.2.100')
403 self.assertItemsEqual([ip_object], results)408 self.assertItemsEqual([ip_object], results)
409
410
411class TestLargeObjectField(TestModelMixin, MAASServerTestCase):
412
413 app = 'maasserver.tests'
414
415 def test_stores_data(self):
416 data = factory.make_string()
417 test_name = factory.make_name('name')
418 test_instance = LargeObjectFieldModel(name=test_name)
419 large_object = LargeObjectFile()
420 with large_object.open('wb') as stream:
421 stream.write(data)
422 test_instance.large_object = large_object
423 test_instance.save()
424 test_instance = LargeObjectFieldModel.objects.get(name=test_name)
425 with test_instance.large_object.open('rb') as stream:
426 saved_data = stream.read()
427 self.assertEqual(data, saved_data)
428
429 def test_with_exit_calls_close(self):
430 data = factory.make_string()
431 large_object = LargeObjectFile()
432 with large_object.open('wb') as stream:
433 self.addCleanup(large_object.close)
434 mock_close = self.patch(large_object, 'close')
435 stream.write(data)
436 self.assertThat(mock_close, MockCalledOnceWith())
437
438 def test_unlink(self):
439 data = factory.make_string()
440 large_object = LargeObjectFile()
441 with large_object.open('wb') as stream:
442 stream.write(data)
443 oid = large_object.oid
444 large_object.unlink()
445 self.assertEqual(0, large_object.oid)
446 self.assertRaises(
447 OperationalError,
448 connection.connection.lobject, oid)
449
450 def test_interates_on_block_size(self):
451 # String size is multiple of block_size in the testing model
452 data = factory.make_string(10 * 2)
453 test_name = factory.make_name('name')
454 test_instance = LargeObjectFieldModel(name=test_name)
455 large_object = LargeObjectFile()
456 with large_object.open('wb') as stream:
457 stream.write(data)
458 test_instance.large_object = large_object
459 test_instance.save()
460 test_instance = LargeObjectFieldModel.objects.get(name=test_name)
461 with test_instance.large_object.open('rb') as stream:
462 offset = 0
463 for block in stream:
464 self.assertEqual(data[offset:offset + 10], block)
465 offset += 10
466
467 def test_get_db_prep_value_returns_None_when_value_None(self):
468 field = LargeObjectField()
469 self.assertEqual(None, field.get_db_prep_value(None))
470
471 def test_get_db_prep_value_returns_oid_when_value_LargeObjectFile(self):
472 oid = randint(1, 100)
473 field = LargeObjectField()
474 obj_file = LargeObjectFile()
475 obj_file.oid = oid
476 self.assertEqual(oid, field.get_db_prep_value(obj_file))
477
478 def test_get_db_prep_value_raises_error_when_oid_less_than_zero(self):
479 oid = randint(-100, 0)
480 field = LargeObjectField()
481 obj_file = LargeObjectFile()
482 obj_file.oid = oid
483 self.assertRaises(AssertionError, field.get_db_prep_value, obj_file)
484
485 def test_get_db_prep_value_raises_error_when_not_LargeObjectFile(self):
486 field = LargeObjectField()
487 self.assertRaises(
488 AssertionError, field.get_db_prep_value, factory.make_string())
489
490 def test_to_python_returns_None_when_value_None(self):
491 field = LargeObjectField()
492 self.assertEqual(None, field.to_python(None))
493
494 def test_to_python_returns_value_when_value_LargeObjectFile(self):
495 field = LargeObjectField()
496 obj_file = LargeObjectFile()
497 self.assertEqual(obj_file, field.to_python(obj_file))
498
499 def test_to_python_returns_LargeObjectFile_when_value_int(self):
500 oid = randint(1, 100)
501 field = LargeObjectField()
502 obj_file = field.to_python(oid)
503 self.assertEqual(oid, obj_file.oid)
504
505 def test_to_python_returns_LargeObjectFile_when_value_long(self):
506 oid = long(randint(1, 100))
507 field = LargeObjectField()
508 obj_file = field.to_python(oid)
509 self.assertEqual(oid, obj_file.oid)
510
511 def test_to_python_raises_error_when_not_valid_type(self):
512 field = LargeObjectField()
513 self.assertRaises(
514 AssertionError, field.to_python, factory.make_string())