From 65a670afce0a18b625da4eb0891af425fec4d818 Mon Sep 17 00:00:00 2001 From: Scott Baker Date: Thu, 8 May 2014 22:14:13 -0700 Subject: [PATCH] Jude's latest changes to syndicate models and admin --- planetstack/syndicate/admin.py | 174 +++++++++++++++++++------------- planetstack/syndicate/models.py | 101 +++++++++++------- 2 files changed, 171 insertions(+), 104 deletions(-) diff --git a/planetstack/syndicate/admin.py b/planetstack/syndicate/admin.py index 85c3ebd..fd306a2 100644 --- a/planetstack/syndicate/admin.py +++ b/planetstack/syndicate/admin.py @@ -14,6 +14,7 @@ from core.admin import ReadOnlyTabularInline,ReadOnlyAwareAdmin,SingletonAdmin,S from suit.widgets import LinkedSelect from bitfield import BitField from bitfield.forms import BitFieldCheckboxSelectMultiple +from django.core.exceptions import ValidationError, ObjectDoesNotExist class SyndicateServiceAdmin(SingletonAdmin,ReadOnlyAwareAdmin): model = SyndicateService @@ -31,6 +32,7 @@ class SyndicateServiceAdmin(SingletonAdmin,ReadOnlyAwareAdmin): ('serviceattrs','Additional Attributes'), ) + class VolumeAccessRightForUserROInline(ReadOnlyTabularInline): model = VolumeAccessRight extra = 0 @@ -47,40 +49,9 @@ class VolumeAccessRightInline(PlStackTabularInline): model = VolumeAccessRight extra = 0 suit_classes = 'suit-tab suit-tab-volumeAccessRights' - -class VolumeAccessRightAdmin(ReadOnlyAwareAdmin): - model = VolumeAccessRight - - formfield_overrides = { BitField: {'widget': BitFieldCheckboxSelectMultiple},} - list_display = ['owner_id', 'volume'] - user_readonly_fields = ['owner_id','volume','gateway_caps'] - user_readonly_inlines = [] - -class VolumeAccessRequestForUserROInline(ReadOnlyTabularInline): - model = VolumeAccessRequest - extra = 0 - suit_classes = 'suit-tab suit-tab-volumeAccessRequests' - fields = ['volume', 'message'] - -class VolumeAccessRequestROInline(ReadOnlyTabularInline): - model = VolumeAccessRequest - extra = 0 - suit_classes = 'suit-tab suit-tab-volumeAccessRequests' - fields = ['owner_id', 'message'] - -class VolumeAccessRequestInline(PlStackTabularInline): - model = VolumeAccessRequest - extra = 0 - suit_classes = 'suit-tab suit-tab-volumeAccessRequests' - fields = ['owner_id', 'message'] - -class VolumeAccessRequestAdmin(ReadOnlyAwareAdmin): - model = VolumeAccessRequest - - formfield_overrides = { BitField: {'widget': BitFieldCheckboxSelectMultiple},} - list_display = ['owner_id', 'volume', 'message'] - user_readonly_fields = ['volume','owner_id','message','message', 'gateway_caps'] - user_readonly_inlines = [] + formfield_overrides = { + BitField: {'widget': BitFieldCheckboxSelectMultiple} + } class VolumeInline(PlStackTabularInline): model = Volume @@ -94,56 +65,123 @@ class VolumeROInline(ReadOnlyTabularInline): suit_classes = 'suit-tab suit-tab-volumes' fields = ['name', 'owner_id'] + +class VolumeSliceFormSet( forms.models.BaseInlineFormSet ): + # verify that our VolumeSlice is valid + + @classmethod + def verify_unchanged( cls, volume_pk, slice_pk, field_name, new_value ): + vs = None + try: + vs = VolumeSlice.objects.get( volume_id=volume_pk, slice_id=slice_pk ) + except ObjectDoesNotExist, dne: + return True, None + + old_value = getattr( vs, field_name ) + if old_value != new_value: + return False, old_value + else: + return True, None + + + def clean( self ): + for form in self.forms: + # check each inline's cleaned data, if it's valid + cleaned_data = None + try: + if form.cleaned_data: + cleaned_data = form.cleaned_data + except AttributeError: + continue + + # verify that the ports haven't changed + volume_pk = cleaned_data['volume_id'].pk + slice_pk = cleaned_data['slice_id'].pk + + if not cleaned_data.has_key('peer_portnum'): + raise ValidationError("Missing client peer-to-peer cache port number") + + if not cleaned_data.has_key('replicate_portnum'): + raise ValidationError("Missing replication service port number") + + rc1, old_peer_port = VolumeSliceFormSet.verify_unchanged( volume_pk, slice_pk, 'peer_portnum', cleaned_data['peer_portnum'] ) + rc2, old_replicate_port = VolumeSliceFormSet.verify_unchanged( volume_pk, slice_pk, 'replicate_portnum', cleaned_data['replicate_portnum'] ) + + err1str = "" + err2str = "" + if not rc1: + err1str = "change %s back to %s" % (cleaned_data['peer_portnum'], old_peer_port) + if not rc2: + err2str = " and change %s back to %s" % (cleaned_data['replicate_portnum'], old_replicate_port ) + + if not rc1 or not rc2: + raise ValidationError("Port numbers cannot be changed once they are set. Please %s %s" % (err1str, err2str)) + + + +class VolumeSliceInline(PlStackTabularInline): + model = VolumeSlice + extra = 0 + suit_classes = 'suit-tab suit-tab-volumeSlices' + fields = ['volume_id', 'slice_id', 'gateway_caps', 'peer_portnum', 'replicate_portnum'] + formfield_overrides = { BitField: {'widget': BitFieldCheckboxSelectMultiple},} + + formset = VolumeSliceFormSet + + readonly_fields = ['credentials_blob'] + + +class VolumeSliceROInline(ReadOnlyTabularInline): + model = VolumeSlice + extra = 0 + suit_classes = 'suit-tab suit-tab-volumeSlices' + fields = ['volume_id', 'slice_id', 'gateway_caps', 'peer_portnum', 'replicate_portnum'] + formfield_overrides = { BitField: {'widget': BitFieldCheckboxSelectMultiple},} + + formset = VolumeSliceFormSet + + readonly_fields = ['credentials_blob'] + + class VolumeAdmin(ReadOnlyAwareAdmin): model = Volume - read_only_fields = ['blockSize'] + + def get_readonly_fields(self, request, obj=None ): + always_readonly = [] + if obj == None: + # all fields are editable on add + return always_readonly + + else: + # can't change owner, slice id, or block size on update + return ['blocksize', 'owner_id'] + always_readonly + + list_display = ['name', 'owner_id'] formfield_overrides = { BitField: {'widget': BitFieldCheckboxSelectMultiple},} - detailsFieldList = ['name', 'owner_id', 'description','file_quota','blocksize', 'private','archive', 'default_gateway_caps' ] - keyList = ['metadata_public_key','metadata_private_key','api_public_key'] - + #detailsFieldList = ['name', 'owner_id', 'description','file_quota','blocksize', 'private','archive', 'default_gateway_caps' ] + detailsFieldList = ['name', 'owner_id', 'description','blocksize', 'private','archive', 'default_gateway_caps' ] + fieldsets = [ (None, {'fields': detailsFieldList, 'classes':['suit-tab suit-tab-general']}), - (None, {'fields': keyList, 'classes':['suit-tab suit-tab-volumeKeys']}), + #(None, {'fields': keyList, 'classes':['suit-tab suit-tab-volumeKeys']}), ] - inlines = [VolumeAccessRightInline, VolumeAccessRequestInline] - - user_readonly_fields = ['name','owner_id','description','blocksize','private','metadata_public_key','metadata_private_key','api_public_key','file_quota','default_gateway_caps'] - user_readonly_inlines = [VolumeAccessRightROInline, VolumeAccessRequestROInline] + inlines = [VolumeAccessRightInline, VolumeSliceInline] - suit_form_tabs =(('general', 'Volume Details'), - ('volumeKeys', 'Access Keys'), - ('volumeAccessRequests', 'Volume Access Requests'), - ('volumeAccessRights', 'Volume Access Rights'), - ) + user_readonly_fields = ['name','owner_id','description','blocksize','private','default_gateway_caps'] - - -class SyndicateUserAdmin(ReadOnlyAwareAdmin): - model = SyndicateUser - verbose_name = "Users" - verbose_name = "Users" - list_display = ['user','is_admin', 'max_volumes'] - inlines = [VolumeInline,VolumeAccessRequestInline,VolumeAccessRightInline] - user_readonly_fields = ['user','is_admin','max_volumes','max_UGs','max_RGs','max_AGs'] - user_readonly_inlines = [VolumeROInline,VolumeAccessRequestForUserROInline,VolumeAccessRightForUserROInline] - - fieldsets = [ - (None, {'fields': ['user','is_admin','max_volumes','max_UGs','max_RGs','max_AGs'], 'classes':['suit-tab suit-tab-general']}), - ] + user_readonly_inlines = [VolumeAccessRightROInline, VolumeSliceROInline] suit_form_tabs =(('general', 'Volume Details'), - ('volumes', 'Volumes'), - ('volumeAccessRequests', 'Volume Access Requests'), + #('volumeKeys', 'Access Keys'), + ('volumeSlices', 'Slices'), ('volumeAccessRights', 'Volume Access Rights'), ) + +# left panel: admin.site.register(SyndicateService, SyndicateServiceAdmin) -admin.site.register(VolumeAccessRight, VolumeAccessRightAdmin) -admin.site.register(VolumeAccessRequest, VolumeAccessRequestAdmin) admin.site.register(Volume, VolumeAdmin) -admin.site.register(SyndicateUser, SyndicateUserAdmin) - diff --git a/planetstack/syndicate/models.py b/planetstack/syndicate/models.py index efc08c6..656e881 100644 --- a/planetstack/syndicate/models.py +++ b/planetstack/syndicate/models.py @@ -1,8 +1,9 @@ -from core.models import User,Site,Service,SingletonModel,PlCoreBase +from core.models import User,Site,Service,SingletonModel,PlCoreBase,Slice import os from django.db import models from django.forms.models import model_to_dict from bitfield import BitField +from django.core.exceptions import ValidationError # Create your models here. @@ -14,51 +15,79 @@ class SyndicateService(SingletonModel,Service): def __unicode__(self): return u'Syndicate Service' -class SyndicateUser(models.Model): - user = models.ForeignKey(User) - is_admin = models.BooleanField(default=False, help_text="Indicates this user has Administrative purposes for the Syndicate Service") - max_volumes = models.PositiveIntegerField(help_text="Maximum number of Volumes this user may create.", default=1) - max_UGs = models.PositiveIntegerField(help_text="Maximum number of User Gateways this user may create.", default=500) - max_RGs = models.PositiveIntegerField(help_text="Maximum number of Replica Gateways this user may create.", default=500) - max_AGs = models.PositiveIntegerField(help_text="Maximum number of Aquisition Gateways this user may create.", default=10) - - def __unicode__(self): return self.user.email - -class Volume(models.Model): +class SyndicatePrincipal(PlCoreBase): + class Meta: + app_label = "syndicate" + + # for now, this is a user email address + principal_id = models.TextField() + public_key_pem = models.TextField() + sealed_private_key = models.TextField() + + def __unicode__self(self): return "%s" % self.principal_id + + +class Volume(PlCoreBase): + class Meta: + app_label = "syndicate" + name = models.CharField(max_length=64, help_text="Human-readable, searchable name of the Volume") - owner_id = models.ForeignKey(SyndicateUser, verbose_name='Owner') + + owner_id = models.ForeignKey(User, verbose_name='Owner') + description = models.TextField(null=True, blank=True,max_length=130, help_text="Human-readable description of what this Volume is used for.") blocksize = models.PositiveIntegerField(help_text="Number of bytes per block.") private = models.BooleanField(default=True, help_text="Indicates if the Volume is visible to users other than the Volume Owner and Syndicate Administrators.") - archive = models.BooleanField(default=True, help_text="Indicates if this Volume is read-only, and only an Aquisition Gateway owned by the Volume owner (or Syndicate admin) can write to it.") - metadata_public_key = models.TextField(null=True, blank=True, max_length=1024, help_text="Public key Gateways will use to verify the authenticity of metadata from this Volume") - metadata_private_key = models.TextField(null=True, blank=True, max_length=1024, help_text="Private key the Volume should use to sign metadata served to Gateways") - api_public_key = models.TextField(null=True, blank=True, max_length=1024, help_text="Public key used to verify writes to these fields from Volume owner") - - file_quota = models.IntegerField(help_text='Maximum number of files and directories allowed in this Volume (-1 means "unlimited")') + archive = models.BooleanField(default=False, help_text="Indicates if this Volume is read-only, and only an Aquisition Gateway owned by the Volume owner (or Syndicate admin) can write to it.") - default_gateway_caps = BitField(flags=('GATEWAY_CAP_READ_DATA','GATEWAY_CAP_READ_METADATA', 'GATEWAY_CAP_WRITE_DATA', 'GATEWAY_CAP_WRITE_METADATA', 'GATEWAY_CAP_COORDINATE'), verbose_name='Default Gateway Capabilities') - #default_gateway_caps = models.PositiveIntegerField(verbose_name='Default Gateway Capabilities') - #default_gateway_caps2 = models.CharField(max_length=32,null=True,default = "readonly", verbose_name='Default Gateway Capabilities') + CAP_READ_DATA = 1 + CAP_WRITE_DATA = 2 + CAP_HOST_DATA = 4 + + # NOTE: preserve order of capabilities here... + default_gateway_caps = BitField(flags=("read data", "write data", "host files"), verbose_name='Default User Capabilities') def __unicode__(self): return self.name -class VolumeAccessRight(models.Model): - owner_id = models.ForeignKey(SyndicateUser, verbose_name='user') - volume = models.ForeignKey(Volume) - gateway_caps = BitField(flags=('GATEWAY_CAP_READ_DATA','GATEWAY_CAP_READ_METADATA', 'GATEWAY_CAP_WRITE_DATA', 'GATEWAY_CAP_WRITE_METADATA', 'GATEWAY_CAP_COORDINATE'), verbose_name='Gateway Capabilities') - #gateway_caps = models.PositiveIntegerField(verbose_name='Gateway Capabilities') - #gateway_caps2 = models.CharField(max_length=32, default='readonly',null=True,verbose_name='Default Gateway Capabilities') - def __unicode__(self): return self.owner_id.user.email +class VolumeAccessRight(PlCoreBase): + class Meta: + app_label = "syndicate" -class VolumeAccessRequest(models.Model): - owner_id = models.ForeignKey(SyndicateUser, verbose_name='user') + owner_id = models.ForeignKey(User, verbose_name='user') + volume = models.ForeignKey(Volume) - message = models.TextField(null=True, blank=True, max_length=1024, help_text="Description of why the user wants access to the volume.") - gateway_caps = BitField(flags=('GATEWAY_CAP_READ_DATA','GATEWAY_CAP_READ_METADATA', 'GATEWAY_CAP_WRITE_DATA', 'GATEWAY_CAP_WRITE_METADATA', 'GATEWAY_CAP_COORDINATE'), verbose_name='Gateway Capabilities') - #gateway_caps = models.PositiveIntegerField(verbose_name='Gateway Capabilities') - #gateway_caps2 = models.CharField(max_length=32,default='readonly',null=True,verbose_name='Default Gateway Capabilities') + gateway_caps = BitField(flags=("read data", "write data", "host files"), verbose_name="User Capabilities") + + def __unicode__(self): return "%s-%s" % (self.owner_id.email, self.volume.name) + + +class VolumeSlice(PlCoreBase): + class Meta: + app_label = "syndicate" + + volume_id = models.ForeignKey(Volume, verbose_name="Volume") + slice_id = models.ForeignKey(Slice, verbose_name="Slice") + gateway_caps = BitField(flags=("read data", "write data", "host files"), verbose_name="Slice Capabilities") + + peer_portnum = models.PositiveIntegerField(help_text="User Gateway port", verbose_name="Client peer-to-peer cache port") + replicate_portnum = models.PositiveIntegerField(help_text="Replica Gateway port", verbose_name="Replication service port") + + credentials_blob = models.TextField(null=True, blank=True, help_text="Encrypted slice credentials") + + def __unicode__(self): return "%s-%s" % (self.volume_id.name, self.slice_id.name) + + def clean(self): + """ + Verify that our fields are in order: + * peer_portnum and replicate_portnum have to be valid port numbers between 1025 and 65534 + * peer_portnum and replicate_portnum cannot be changed once set. + """ + + if self.peer_portnum < 1025 or self.peer_portnum > 65534: + raise ValidationError( "Client peer-to-peer cache port number must be between 1025 and 65534" ) + + if self.replicate_portnum < 1025 or self.replicate_portnum > 65534: + raise ValidationError( "Replication service port number must be between 1025 and 65534" ) - def __unicode__(self): return self.owner_id.user.email -- 2.47.0