Esempio n. 1
0
class CartItem(core_models.UuidMixin, TimeStampedModel, RequestTypeMixin):
    user = models.ForeignKey(core_models.User, related_name='+', on_delete=models.CASCADE)
    offering = models.ForeignKey(Offering, related_name='+', on_delete=models.CASCADE)
    plan = models.ForeignKey('Plan', null=True, blank=True)
    attributes = BetterJSONField(blank=True, default=dict)
    limits = BetterJSONField(blank=True, default=dict)

    class Meta(object):
        ordering = ('created',)

    @property
    def estimate(self):
        return self.plan.get_estimate(self.limits)
Esempio n. 2
0
class EventTypesMixin(models.Model):
    """
    Mixin to add a event_types and event_groups fields.
    """
    class Meta:
        abstract = True

    event_types = BetterJSONField('List of event types')
    event_groups = BetterJSONField('List of event groups', default=list)

    @classmethod
    @lru_cache(maxsize=1)
    def get_all_models(cls):
        return [model for model in apps.get_models() if issubclass(model, cls)]
Esempio n. 3
0
class UserDetailsMixin(models.Model):
    """
    This mixin is shared by User and Invitation model. All fields are optional.
    User is populated with these details when invitation is approved.
    Note that civil_number and email fields are not included in this mixin
    because they have different constraints in User and Invitation model.
    """
    class Meta:
        abstract = True

    native_name = models.CharField(_('native name'),
                                   max_length=100,
                                   blank=True)
    phone_number = models.CharField(_('phone number'),
                                    max_length=255,
                                    blank=True)
    organization = models.CharField(_('organization'),
                                    max_length=255,
                                    blank=True)
    job_title = models.CharField(_('job title'), max_length=40, blank=True)
    affiliations = BetterJSONField(
        default=list,
        blank=True,
        help_text=
        "Person's affiliation within organization such as student, faculty, staff.",
    )

    def _process_saml2_affiliations(self, affiliations):
        """
        Due to djangosaml2 assumption that attributes list should have at most one element
        we have to implement custom method to process affiliations fetched from SAML2 IdP.
        See also: https://github.com/peppelinux/djangosaml2/issues/28
        """
        self.affiliations = affiliations
Esempio n. 4
0
class OfferingTemplate(core_models.UuidMixin,
                       structure_models.TimeStampedModel):
    name = models.CharField(_('name'), max_length=150)
    config = BetterJSONField()

    def __str__(self):
        return self.name
Esempio n. 5
0
class Event(UuidMixin):
    created = AutoCreatedField()
    event_type = models.CharField(max_length=100, db_index=True)
    message = models.TextField()
    context = BetterJSONField(blank=True)

    class Meta:
        ordering = ('-created', )
Esempio n. 6
0
class OrderItem(core_models.UuidMixin, core_models.ErrorMessageMixin,
                structure_models.TimeStampedModel, ScopeMixin):
    class States(object):
        PENDING = 1
        EXECUTING = 2
        DONE = 3
        ERRED = 4
        TERMINATED = 5

        CHOICES = (
            (PENDING, 'pending'),
            (EXECUTING, 'executing'),
            (DONE, 'done'),
            (ERRED, 'erred'),
            (TERMINATED, 'terminated'),
        )

        TERMINAL_STATES = set([DONE, ERRED, TERMINATED])

    order = models.ForeignKey(Order, related_name='items')
    offering = models.ForeignKey(Offering)
    attributes = BetterJSONField(blank=True, default=dict)
    cost = models.DecimalField(max_digits=22,
                               decimal_places=10,
                               null=True,
                               blank=True)
    plan = models.ForeignKey('Plan', null=True, blank=True)
    objects = managers.MixinManager('scope')
    state = FSMIntegerField(default=States.PENDING, choices=States.CHOICES)
    tracker = FieldTracker()

    class Meta(object):
        verbose_name = _('Order item')
        ordering = ('created', )

    @transition(field=state,
                source=[States.PENDING, States.ERRED],
                target=States.EXECUTING)
    def set_state_executing(self):
        pass

    @transition(field=state, source=States.EXECUTING, target=States.DONE)
    def set_state_done(self):
        pass

    @transition(field=state, source='*', target=States.ERRED)
    def set_state_erred(self):
        pass

    @transition(field=state, source='*', target=States.TERMINATED)
    def set_state_terminated(self):
        pass

    def set_state(self, state):
        getattr(self, 'set_state_' + state)()
        self.save(update_fields=['state'])
Esempio n. 7
0
class Attribute(TimeStampedModel):
    key = models.CharField(primary_key=True, max_length=255, validators=[InternalNameValidator])
    title = models.CharField(blank=False, max_length=255)
    section = models.ForeignKey(on_delete=models.CASCADE, to=Section, related_name='attributes')
    type = models.CharField(max_length=255, choices=ATTRIBUTE_TYPES)
    required = models.BooleanField(default=False, help_text=_('A value must be provided for the attribute.'))
    default = BetterJSONField(null=True, blank=True)

    def __str__(self):
        return str(self.title)
Esempio n. 8
0
class OfferingTemplate(core_models.UuidMixin,
                       TimeStampedModel):
    name = models.CharField(_('name'), max_length=150)
    config = BetterJSONField()

    @classmethod
    def get_url_name(cls):
        return 'support-offering-template'

    def __str__(self):
        return self.name
Esempio n. 9
0
class OrderItem(core_models.UuidMixin, structure_models.TimeStampedModel):
    order = models.ForeignKey(Order, related_name='items')
    offering = models.ForeignKey(Offering)
    attributes = BetterJSONField(blank=True, default=dict)
    cost = models.DecimalField(max_digits=22,
                               decimal_places=10,
                               null=True,
                               blank=True)

    class Meta(object):
        verbose_name = _('Order item')
        ordering = ('created', )
Esempio n. 10
0
class CostEstimateMixin(models.Model):
    class Meta:
        abstract = True

    # Cost estimate is computed with respect to fixed plan components and usage-based limits
    cost = models.DecimalField(max_digits=22, decimal_places=10, null=True, blank=True)
    plan = models.ForeignKey(on_delete=models.CASCADE, to=Plan, null=True, blank=True)
    limits = BetterJSONField(blank=True, default=dict)

    def init_cost(self):
        if self.plan:
            self.cost = self.plan.get_estimate(self.limits)
Esempio n. 11
0
class CartItem(core_models.UuidMixin, TimeStampedModel, RequestTypeMixin,
               CostEstimateMixin):
    user = models.ForeignKey(core_models.User,
                             related_name='+',
                             on_delete=models.CASCADE)
    offering = models.ForeignKey(Offering,
                                 related_name='+',
                                 on_delete=models.CASCADE)
    attributes = BetterJSONField(blank=True, default=dict)

    class Meta(object):
        ordering = ('created', )
Esempio n. 12
0
class Offering(core_models.UuidMixin, core_models.NameMixin,
               core_models.DescribableMixin, quotas_models.QuotaModelMixin,
               structure_models.StructureModel,
               structure_models.TimeStampedModel):
    thumbnail = models.ImageField(
        upload_to='marketplace_service_offering_thumbnails',
        blank=True,
        null=True)
    full_description = models.TextField(blank=True)
    rating = models.IntegerField(
        null=True,
        validators=[MaxValueValidator(5),
                    MinValueValidator(1)],
        help_text=_('Rating is value from 1 to 5.'))
    category = models.ForeignKey(Category, related_name='offerings')
    customer = models.ForeignKey(structure_models.Customer,
                                 related_name='+',
                                 null=True)
    attributes = BetterJSONField(blank=True, default=dict)
    geolocations = JSONField(
        default=list,
        blank=True,
        help_text=_(
            'List of latitudes and longitudes. For example: '
            '[{"latitude": 123, "longitude": 345}, {"latitude": 456, "longitude": 678}]'
        ))
    is_active = models.BooleanField(default=True)

    native_name = models.CharField(max_length=160, default='', blank=True)
    native_description = models.CharField(max_length=500,
                                          default='',
                                          blank=True)

    class Permissions(object):
        customer_path = 'customer'

    class Meta(object):
        verbose_name = _('Offering')

    class Quotas(quotas_models.QuotaModelMixin.Quotas):
        order_item_count = quotas_fields.CounterQuotaField(
            target_models=lambda: [OrderItem],
            path_to_scope='offering',
        )

    def __str__(self):
        return six.text_type(self.name)

    @classmethod
    def get_url_name(cls):
        return 'marketplace-offering'
Esempio n. 13
0
class OfferingTemplate(core_models.UuidMixin, TimeStampedModel):
    name = models.CharField(_('name'), max_length=150)
    config = BetterJSONField()
    sort_order = models.PositiveSmallIntegerField(default=1)

    class Meta:
        ordering = ['sort_order', 'name']

    @classmethod
    def get_url_name(cls):
        return 'support-offering-template'

    def __str__(self):
        return self.name
Esempio n. 14
0
class CartItem(core_models.UuidMixin, TimeStampedModel, RequestTypeMixin):
    user = models.ForeignKey(core_models.User, related_name='+', on_delete=models.CASCADE)
    project = models.ForeignKey(structure_models.Project, related_name='+', on_delete=models.CASCADE)
    offering = models.ForeignKey(Offering, related_name='+', on_delete=models.CASCADE)
    attributes = BetterJSONField(blank=True, default=dict)

    class Permissions:
        customer_path = 'project__customer'
        project_path = 'project'

    class Meta:
        ordering = ('created',)

    def __str__(self):
        return 'user: %s, offering: %s' % (self.user.username, self.offering.name)
Esempio n. 15
0
class ResourceDetailsMixin(
        CostEstimateMixin,
        core_models.NameMixin,
        core_models.DescribableMixin,
):
    class Meta:
        abstract = True

    offering = models.ForeignKey(Offering,
                                 related_name='+',
                                 on_delete=models.PROTECT)
    attributes = BetterJSONField(blank=True, default=dict)
    end_date = models.DateField(
        null=True,
        blank=True,
        help_text=
        _('The date is inclusive. Once reached, a resource will be scheduled for termination.'
          ),
    )
Esempio n. 16
0
class Offering(
        core_models.BackendMixin,
        core_models.UuidMixin,
        core_models.NameMixin,
        core_models.DescribableMixin,
        quotas_models.QuotaModelMixin,
        structure_models.PermissionMixin,
        structure_models.StructureModel,
        TimeStampedModel,
        core_mixins.ScopeMixin,
        LoggableMixin,
        pid_mixins.DataciteMixin,
        CoordinatesMixin,
        structure_models.ImageModelMixin,
):
    class States:
        DRAFT = 1
        ACTIVE = 2
        PAUSED = 3
        ARCHIVED = 4

        CHOICES = (
            (DRAFT, 'Draft'),
            (ACTIVE, 'Active'),
            (PAUSED, 'Paused'),
            (ARCHIVED, 'Archived'),
        )

    thumbnail = models.FileField(
        upload_to='marketplace_service_offering_thumbnails',
        blank=True,
        null=True,
        validators=[ImageValidator],
    )
    full_description = models.TextField(blank=True)
    vendor_details = models.TextField(blank=True)
    rating = models.IntegerField(
        null=True,
        validators=[MaxValueValidator(5),
                    MinValueValidator(1)],
        help_text=_('Rating is value from 1 to 5.'),
    )
    category = models.ForeignKey(on_delete=models.CASCADE,
                                 to=Category,
                                 related_name='offerings')
    customer: structure_models.Customer = models.ForeignKey(
        on_delete=models.CASCADE,
        to=structure_models.Customer,
        related_name='+',
        null=True,
    )
    project = models.ForeignKey(
        on_delete=models.CASCADE,
        to=structure_models.Project,
        related_name='+',
        null=True,
        blank=True,
    )
    # Volume offering is linked with VPC offering via parent field
    parent = models.ForeignKey(on_delete=models.CASCADE,
                               to='Offering',
                               null=True,
                               blank=True)
    attributes = BetterJSONField(blank=True,
                                 default=dict,
                                 help_text=_('Fields describing Category.'))
    options = BetterJSONField(
        blank=True,
        default=dict,
        help_text=_('Fields describing Offering request form.'),
    )
    plugin_options = BetterJSONField(
        blank=True,
        default=dict,
        help_text=
        _('Public data used by specific plugin, such as storage mode for OpenStack.'
          ),
    )
    secret_options = BetterJSONField(
        blank=True,
        default=dict,
        help_text=
        _('Private data used by specific plugin, such as credentials and hooks.'
          ),
    )

    native_name = models.CharField(max_length=160, default='', blank=True)
    native_description = models.CharField(max_length=500,
                                          default='',
                                          blank=True)
    terms_of_service = models.TextField(blank=True)
    terms_of_service_link = models.URLField(blank=True)
    privacy_policy_link = models.URLField(blank=True)

    type = models.CharField(max_length=100)
    state = FSMIntegerField(default=States.DRAFT, choices=States.CHOICES)
    paused_reason = models.TextField(blank=True)
    divisions = models.ManyToManyField(structure_models.Division,
                                       related_name='offerings',
                                       blank=True)

    # If offering is not shared, it is available only to following user categories:
    # 1) staff user;
    # 2) global support user;
    # 3) users with active permission in original customer;
    # 4) users with active permission in related project.
    shared = models.BooleanField(default=True,
                                 help_text=_('Accessible to all customers.'))

    billable = models.BooleanField(
        default=True, help_text=_('Purchase and usage is invoiced.'))

    objects = managers.OfferingManager()
    tracker = FieldTracker()

    class Permissions:
        customer_path = 'customer'

    class Meta:
        verbose_name = _('Offering')

    class Quotas(quotas_models.QuotaModelMixin.Quotas):
        order_item_count = quotas_fields.CounterQuotaField(
            target_models=lambda: [OrderItem],
            path_to_scope='offering',
        )

    @transition(field=state,
                source=[States.DRAFT, States.PAUSED],
                target=States.ACTIVE)
    def activate(self):
        pass

    @transition(field=state, source=States.ACTIVE, target=States.PAUSED)
    def pause(self):
        pass

    @transition(field=state, source=States.PAUSED, target=States.ACTIVE)
    def unpause(self):
        pass

    @transition(field=state, source='*', target=States.ARCHIVED)
    def archive(self):
        pass

    @transition(field=state, source='*', target=States.DRAFT)
    def draft(self):
        pass

    def __str__(self):
        return str(self.name)

    @classmethod
    def get_url_name(cls):
        return 'marketplace-offering'

    @cached_property
    def component_factors(self):
        # get factor from plugin components
        plugin_components = plugins.manager.get_components(self.type)
        return {c.type: c.factor for c in plugin_components}

    @cached_property
    def is_usage_based(self):
        return self.components.filter(
            billing_type=OfferingComponent.BillingTypes.USAGE, ).exists()

    def get_limit_components(self):
        components = self.components.filter(
            billing_type=OfferingComponent.BillingTypes.LIMIT)
        return {component.type: component for component in components}

    @cached_property
    def is_limit_based(self):
        if not plugins.manager.can_update_limits(self.type):
            return False
        if not self.components.filter(
                billing_type=OfferingComponent.BillingTypes.LIMIT).exists():
            return False
        return True

    @property
    def is_private(self):
        return not self.billable and not self.shared

    def get_datacite_title(self):
        return self.name

    def get_datacite_creators_name(self):
        return self.customer.name

    def get_datacite_description(self):
        return self.description

    def get_datacite_publication_year(self):
        return self.created.year

    def get_datacite_url(self):
        return core_utils.format_homeport_link(
            'marketplace-public-offering/{offering_uuid}/',
            offering_uuid=self.uuid.hex)

    def can_manage_role(self, user, role=None, timestamp=False):
        if not self.shared:
            return False
        return user.is_staff or self.customer.has_user(
            user, structure_models.CustomerRole.OWNER, timestamp)

    def get_users(self, role=None):
        query = Q(offeringpermission__offering=self,
                  offeringpermission__is_active=True)
        return User.objects.filter(query).order_by('username')
Esempio n. 17
0
class Offering(core_models.UuidMixin, core_models.NameMixin,
               core_models.DescribableMixin, quotas_models.QuotaModelMixin,
               structure_models.StructureModel,
               structure_models.TimeStampedModel, ScopeMixin):
    class States(object):
        DRAFT = 1
        ACTIVE = 2
        PAUSED = 3
        ARCHIVED = 4

        CHOICES = (
            (DRAFT, 'Draft'),
            (ACTIVE, 'Active'),
            (PAUSED, 'Paused'),
            (ARCHIVED, 'Archived'),
        )

    thumbnail = models.FileField(
        upload_to='marketplace_service_offering_thumbnails',
        blank=True,
        null=True,
        validators=[VectorizedImageValidator])
    full_description = models.TextField(blank=True)
    vendor_details = models.TextField(blank=True)
    rating = models.IntegerField(
        null=True,
        validators=[MaxValueValidator(5),
                    MinValueValidator(1)],
        help_text=_('Rating is value from 1 to 5.'))
    category = models.ForeignKey(Category, related_name='offerings')
    customer = models.ForeignKey(structure_models.Customer,
                                 related_name='+',
                                 null=True)
    attributes = BetterJSONField(blank=True,
                                 default=dict,
                                 help_text=_('Fields describing Category.'))
    options = BetterJSONField(
        blank=True,
        default=dict,
        help_text=_('Fields describing Offering request form.'))
    geolocations = JSONField(
        default=list,
        blank=True,
        help_text=_(
            'List of latitudes and longitudes. For example: '
            '[{"latitude": 123, "longitude": 345}, {"latitude": 456, "longitude": 678}]'
        ))

    native_name = models.CharField(max_length=160, default='', blank=True)
    native_description = models.CharField(max_length=500,
                                          default='',
                                          blank=True)

    type = models.CharField(max_length=100)
    state = FSMIntegerField(default=States.DRAFT, choices=States.CHOICES)

    # If offering is not shared, it is available only to following user categories:
    # 1) staff user;
    # 2) global support user;
    # 3) users with active permission in original customer;
    # 4) users with active permission in allowed customers and nested projects.
    shared = models.BooleanField(default=False,
                                 help_text=_('Anybody can use it'))
    allowed_customers = models.ManyToManyField(structure_models.Customer,
                                               blank=True)

    objects = managers.MixinManager('scope')
    tracker = FieldTracker()

    class Permissions(object):
        customer_path = 'customer'

    class Meta(object):
        verbose_name = _('Offering')

    class Quotas(quotas_models.QuotaModelMixin.Quotas):
        order_item_count = quotas_fields.CounterQuotaField(
            target_models=lambda: [OrderItem],
            path_to_scope='offering',
        )

    @transition(field=state, source=States.DRAFT, target=States.ACTIVE)
    def activate(self):
        pass

    @transition(field=state, source=States.ACTIVE, target=States.PAUSED)
    def pause(self):
        pass

    @transition(field=state, source='*', target=States.ARCHIVED)
    def archive(self):
        pass

    def __str__(self):
        return six.text_type(self.name)

    @classmethod
    def get_url_name(cls):
        return 'marketplace-offering'
Esempio n. 18
0
class OrderItem(core_models.UuidMixin,
                core_models.ErrorMessageMixin,
                RequestTypeMixin,
                TimeStampedModel):
    class States(object):
        PENDING = 1
        EXECUTING = 2
        DONE = 3
        ERRED = 4

        CHOICES = (
            (PENDING, 'pending'),
            (EXECUTING, 'executing'),
            (DONE, 'done'),
            (ERRED, 'erred'),
        )

        TERMINAL_STATES = {DONE, ERRED}

    order = models.ForeignKey(Order, related_name='items')
    offering = models.ForeignKey(Offering)
    attributes = BetterJSONField(blank=True, default=dict)
    limits = BetterJSONField(blank=True, null=True, default=dict)
    cost = models.DecimalField(max_digits=22, decimal_places=10, null=True, blank=True)
    plan = models.ForeignKey('Plan', null=True, blank=True)
    resource = models.ForeignKey(Resource, null=True, blank=True)
    state = FSMIntegerField(default=States.PENDING, choices=States.CHOICES)
    tracker = FieldTracker()

    class Permissions(object):
        customer_path = 'order__project__customer'
        project_path = 'order__project'

    class Meta(object):
        verbose_name = _('Order item')
        ordering = ('created',)

    @classmethod
    def get_url_name(cls):
        return 'marketplace-order-item'

    @transition(field=state, source=[States.PENDING, States.ERRED], target=States.EXECUTING)
    def set_state_executing(self):
        pass

    @transition(field=state, source=States.EXECUTING, target=States.DONE)
    def set_state_done(self):
        pass

    @transition(field=state, source='*', target=States.ERRED)
    def set_state_erred(self):
        pass

    def clean(self):
        offering = self.offering
        customer = self.order.project.customer

        if offering.shared:
            return

        if offering.customer == customer:
            return

        if offering.allowed_customers.filter(pk=customer.pk).exists():
            return

        raise ValidationError(
            _('Offering "%s" is not allowed in organization "%s".') % (offering.name, customer.name)
        )

    def init_cost(self):
        if self.plan:
            self.cost = self.plan.get_estimate(self.limits)
Esempio n. 19
0
class Resource(core_models.UuidMixin, TimeStampedModel, ScopeMixin):
    """
    Core resource is abstract model, marketplace resource is not abstract,
    therefore we don't need to compromise database query efficiency when
    we are getting a list of all resources.

    While migration from ad-hoc resources to marketplace as single entry point is pending,
    the core resource model may continue to be used in plugins and referenced via
    generic foreign key, and marketplace resource is going to be used as consolidated
    model for synchronization with external plugins.

    Eventually it is expected that core resource model is going to be superseded by
    marketplace resource model as a primary mean.
    """
    class States(object):
        CREATING = 1
        OK = 2
        ERRED = 3
        UPDATING = 4
        TERMINATING = 5
        TERMINATED = 6

        CHOICES = (
            (CREATING, 'Creating'),
            (OK, 'OK'),
            (ERRED, 'Erred'),
            (UPDATING, 'Updating'),
            (TERMINATING, 'Terminating'),
            (TERMINATED, 'Terminated'),
        )

    class Permissions(object):
        customer_path = 'project__customer'
        project_path = 'project'

    state = FSMIntegerField(default=States.CREATING, choices=States.CHOICES)
    project = models.ForeignKey(structure_models.Project, on_delete=models.CASCADE)
    offering = models.ForeignKey(Offering, related_name='+', on_delete=models.PROTECT)
    plan = models.ForeignKey(Plan, null=True, blank=True)
    attributes = BetterJSONField(blank=True, default=dict)
    limits = BetterJSONField(blank=True, default=dict)
    tracker = FieldTracker()
    objects = managers.MixinManager('scope')

    @transition(field=state, source=[States.CREATING, States.UPDATING], target=States.OK)
    def set_state_ok(self):
        pass

    @transition(field=state, source='*', target=States.ERRED)
    def set_state_erred(self):
        pass

    @transition(field=state, source='*', target=States.UPDATING)
    def set_state_updating(self):
        pass

    @transition(field=state, source='*', target=States.TERMINATING)
    def set_state_terminating(self):
        pass

    @transition(field=state, source='*', target=States.TERMINATED)
    def set_state_terminated(self):
        pass

    @property
    def backend_uuid(self):
        if self.scope:
            return self.scope.uuid

    @property
    def backend_type(self):
        if self.scope:
            return self.scope.get_scope_type()

    def init_quotas(self):
        if self.limits:
            components_map = self.offering.get_usage_components()
            for key, value in self.limits.items():
                component = components_map.get(key)
                if component:
                    ComponentQuota.objects.create(
                        resource=self,
                        component=component,
                        limit=value
                    )
Esempio n. 20
0
class Offering(
        core_models.UuidMixin,
        core_models.NameMixin,
        core_models.DescribableMixin,
        quotas_models.QuotaModelMixin,
        structure_models.StructureModel,
        TimeStampedModel,
        core_mixins.ScopeMixin,
        LoggableMixin,
        pid_mixins.DataciteMixin,
):
    class States:
        DRAFT = 1
        ACTIVE = 2
        PAUSED = 3
        ARCHIVED = 4

        CHOICES = (
            (DRAFT, 'Draft'),
            (ACTIVE, 'Active'),
            (PAUSED, 'Paused'),
            (ARCHIVED, 'Archived'),
        )

    thumbnail = models.FileField(
        upload_to='marketplace_service_offering_thumbnails',
        blank=True,
        null=True,
        validators=[ImageValidator],
    )
    full_description = models.TextField(blank=True)
    vendor_details = models.TextField(blank=True)
    rating = models.IntegerField(
        null=True,
        validators=[MaxValueValidator(5),
                    MinValueValidator(1)],
        help_text=_('Rating is value from 1 to 5.'),
    )
    category = models.ForeignKey(on_delete=models.CASCADE,
                                 to=Category,
                                 related_name='offerings')
    customer = models.ForeignKey(
        on_delete=models.CASCADE,
        to=structure_models.Customer,
        related_name='+',
        null=True,
    )
    # Volume offering is linked with VPC offering via parent field
    parent = models.ForeignKey(on_delete=models.CASCADE,
                               to='Offering',
                               null=True,
                               blank=True)
    attributes = BetterJSONField(blank=True,
                                 default=dict,
                                 help_text=_('Fields describing Category.'))
    options = BetterJSONField(
        blank=True,
        default=dict,
        help_text=_('Fields describing Offering request form.'),
    )
    plugin_options = BetterJSONField(
        blank=True,
        default=dict,
        help_text=
        _('Public data used by specific plugin, such as storage mode for OpenStack.'
          ),
    )
    secret_options = BetterJSONField(
        blank=True,
        default=dict,
        help_text=
        _('Private data used by specific plugin, such as credentials and hooks.'
          ),
    )
    geolocations = JSONField(
        default=list,
        blank=True,
        help_text=_(
            'List of latitudes and longitudes. For example: '
            '[{"latitude": 123, "longitude": 345}, {"latitude": 456, "longitude": 678}]'
        ),
    )

    native_name = models.CharField(max_length=160, default='', blank=True)
    native_description = models.CharField(max_length=500,
                                          default='',
                                          blank=True)
    terms_of_service = models.TextField(blank=True)

    type = models.CharField(max_length=100)
    state = FSMIntegerField(default=States.DRAFT, choices=States.CHOICES)
    paused_reason = models.TextField(blank=True)

    # If offering is not shared, it is available only to following user categories:
    # 1) staff user;
    # 2) global support user;
    # 3) users with active permission in original customer;
    # 4) users with active permission in allowed customers and nested projects.
    shared = models.BooleanField(default=True,
                                 help_text=_('Accessible to all customers.'))
    allowed_customers = models.ManyToManyField(structure_models.Customer,
                                               blank=True)

    billable = models.BooleanField(
        default=True, help_text=_('Purchase and usage is invoiced.'))
    backend_id = models.CharField(max_length=255, blank=True)

    objects = managers.OfferingManager()
    tracker = FieldTracker()

    class Permissions:
        customer_path = 'customer'

    class Meta:
        verbose_name = _('Offering')

    class Quotas(quotas_models.QuotaModelMixin.Quotas):
        order_item_count = quotas_fields.CounterQuotaField(
            target_models=lambda: [OrderItem],
            path_to_scope='offering',
        )

    @transition(field=state,
                source=[States.DRAFT, States.PAUSED],
                target=States.ACTIVE)
    def activate(self):
        pass

    @transition(field=state, source=States.ACTIVE, target=States.PAUSED)
    def pause(self):
        pass

    @transition(field=state, source='*', target=States.ARCHIVED)
    def archive(self):
        pass

    def __str__(self):
        return str(self.name)

    @classmethod
    def get_url_name(cls):
        return 'marketplace-offering'

    def get_usage_components(self):
        components = self.components.filter(
            billing_type=OfferingComponent.BillingTypes.USAGE)
        return {component.type: component for component in components}

    @cached_property
    def is_usage_based(self):
        return self.components.filter(
            billing_type=OfferingComponent.BillingTypes.USAGE,
            use_limit_for_billing=False,
        ).exists()

    @property
    def is_private(self):
        return not self.billable and not self.shared

    def get_datacite_title(self):
        return self.name

    def get_datacite_creators_name(self):
        return self.customer.name

    def get_datacite_description(self):
        return self.description

    def get_datacite_publication_year(self):
        return self.created.year

    def get_datacite_url(self):
        return settings.WALDUR_MARKETPLACE['OFFERING_LINK_TEMPLATE'].format(
            offering_uuid=self.uuid.hex)
Esempio n. 21
0
class Resource(
        ResourceDetailsMixin,
        core_models.UuidMixin,
        core_models.BackendMixin,
        TimeStampedModel,
        core_mixins.ScopeMixin,
        structure_models.StructureLoggableMixin,
):
    """
    Core resource is abstract model, marketplace resource is not abstract,
    therefore we don't need to compromise database query efficiency when
    we are getting a list of all resources.

    While migration from ad-hoc resources to marketplace as single entry point is pending,
    the core resource model may continue to be used in plugins and referenced via
    generic foreign key, and marketplace resource is going to be used as consolidated
    model for synchronization with external plugins.

    Eventually it is expected that core resource model is going to be superseded by
    marketplace resource model as a primary mean.
    """
    class States:
        CREATING = 1
        OK = 2
        ERRED = 3
        UPDATING = 4
        TERMINATING = 5
        TERMINATED = 6

        CHOICES = (
            (CREATING, 'Creating'),
            (OK, 'OK'),
            (ERRED, 'Erred'),
            (UPDATING, 'Updating'),
            (TERMINATING, 'Terminating'),
            (TERMINATED, 'Terminated'),
        )

    class Permissions:
        customer_path = 'project__customer'
        project_path = 'project'

    state = FSMIntegerField(default=States.CREATING, choices=States.CHOICES)
    project = models.ForeignKey(structure_models.Project,
                                on_delete=models.CASCADE)
    backend_metadata = BetterJSONField(blank=True, default=dict)
    report = BetterJSONField(blank=True, null=True)
    current_usages = BetterJSONField(blank=True, default=dict)
    tracker = FieldTracker()
    objects = managers.MixinManager('scope')

    @property
    def customer(self):
        return self.project.customer

    @transition(
        field=state,
        source=[States.ERRED, States.CREATING, States.UPDATING],
        target=States.OK,
    )
    def set_state_ok(self):
        pass

    @transition(field=state, source='*', target=States.ERRED)
    def set_state_erred(self):
        pass

    @transition(field=state, source='*', target=States.UPDATING)
    def set_state_updating(self):
        pass

    @transition(field=state, source='*', target=States.TERMINATING)
    def set_state_terminating(self):
        pass

    @transition(field=state, source='*', target=States.TERMINATED)
    def set_state_terminated(self):
        pass

    @property
    def backend_uuid(self):
        if self.scope:
            return self.scope.uuid

    @property
    def backend_type(self):
        if self.scope:
            scope_type = self.scope.get_scope_type()
            return scope_type if scope_type else 'Marketplace.Resource'

    def init_quotas(self):
        if self.limits:
            components_map = self.offering.get_limit_components()
            for key, value in self.limits.items():
                component = components_map.get(key)
                if component:
                    ComponentQuota.objects.create(resource=self,
                                                  component=component,
                                                  limit=value)

    def get_log_fields(self):
        return (
            'uuid',
            'name',
            'project',
            'offering',
            'created',
            'modified',
            'attributes',
            'cost',
            'plan',
            'limits',
            'get_state_display',
            'backend_metadata',
            'backend_uuid',
            'backend_type',
        )

    @property
    def invoice_registrator_key(self):
        return self.offering.type

    @classmethod
    def get_scope_type(cls):
        return 'Marketplace.Resource'

    @classmethod
    def get_url_name(cls):
        return 'marketplace-resource'

    @property
    def is_expired(self):
        return self.end_date and self.end_date <= timezone.datetime.today(
        ).date()

    def __str__(self):
        if self.name:
            return f'{self.name} ({self.offering.name})'
        if self.scope:
            return f'{self.name} ({self.content_type} / {self.object_id})'

        return f'{self.uuid} ({self.offering.name})'
Esempio n. 22
0
class User(
        LoggableMixin,
        UuidMixin,
        DescribableMixin,
        AbstractBaseUser,
        UserDetailsMixin,
        PermissionsMixin,
):
    username = models.CharField(
        _('username'),
        max_length=128,
        unique=True,
        help_text=_('Required. 128 characters or fewer. Letters, numbers and '
                    '@/./+/-/_ characters'),
        validators=[
            validators.RegexValidator(re.compile(r'^[\w.@+-]+$'),
                                      _('Enter a valid username.'), 'invalid')
        ],
    )
    # Civil number is nullable on purpose, otherwise
    # it wouldn't be possible to put a unique constraint on it
    civil_number = models.CharField(
        _('civil number'),
        max_length=50,
        unique=True,
        blank=True,
        null=True,
        default=None,
    )
    email = models.EmailField(_('email address'), max_length=75, blank=True)

    is_staff = models.BooleanField(
        _('staff status'),
        default=False,
        help_text=_('Designates whether the user can log into this admin '
                    'site.'),
    )
    is_active = models.BooleanField(
        _('active'),
        default=True,
        help_text=_('Designates whether this user should be treated as '
                    'active. Unselect this instead of deleting accounts.'),
    )
    is_support = models.BooleanField(
        _('support status'),
        default=False,
        help_text=_('Designates whether the user is a global support user.'),
    )
    date_joined = models.DateTimeField(_('date joined'),
                                       default=django_timezone.now)
    registration_method = models.CharField(
        _('registration method'),
        max_length=50,
        default='default',
        blank=True,
        help_text=_('Indicates what registration method were used.'),
    )
    agreement_date = models.DateTimeField(
        _('agreement date'),
        blank=True,
        null=True,
        help_text=_('Indicates when the user has agreed with the policy.'),
    )
    preferred_language = models.CharField(max_length=10, blank=True)
    competence = models.CharField(max_length=255, blank=True)
    token_lifetime = models.PositiveIntegerField(
        null=True,
        help_text=_('Token lifetime in seconds.'),
        validators=[validators.MinValueValidator(60)],
    )
    details = BetterJSONField(
        blank=True,
        default=dict,
        help_text=_('Extra details from authentication backend.'),
    )
    backend_id = models.CharField(max_length=255, blank=True)

    tracker = FieldTracker()
    objects = UserManager()

    USERNAME_FIELD = 'username'
    REQUIRED_FIELDS = ['email']

    class Meta:
        verbose_name = _('user')
        verbose_name_plural = _('users')

    def get_log_fields(self):
        return (
            'uuid',
            'full_name',
            'native_name',
            self.USERNAME_FIELD,
            'is_staff',
            'is_support',
            'token_lifetime',
        )

    def get_full_name(self):
        # This method is used in django-reversion as name of revision creator.
        return self.full_name

    def get_short_name(self):
        # This method is used in django-reversion as name of revision creator.
        return self.full_name

    def email_user(self, subject, message, from_email=None):
        """
        Sends an email to this User.
        """
        send_mail(subject, message, from_email, [self.email])

    @classmethod
    def get_permitted_objects(cls, user):
        from waldur_core.structure.filters import filter_visible_users

        queryset = User.objects.all()
        if user.is_staff or user.is_support:
            return queryset
        else:
            return filter_visible_users(queryset, user)

    def clean(self):
        super(User, self).clean()
        # User email has to be unique or empty
        if (self.email and User.objects.filter(email=self.email).exclude(
                id=self.id).exists()):
            raise ValidationError({
                'email':
                _('User with email "%s" already exists.') % self.email
            })

    @transaction.atomic
    def create_request_for_update_email(self, email):
        if User.objects.filter(email=email).exclude(id=self.id).exists():
            raise ValidationError(
                {'email': _('User with email "%s" already exists.') % email})

        ChangeEmailRequest.objects.filter(user=self).delete()
        change_request = ChangeEmailRequest.objects.create(
            user=self,
            email=email,
        )
        return change_request

    def __str__(self):
        if self.full_name:
            return '%s (%s)' % (self.get_username(), self.full_name)

        return self.get_username()
Esempio n. 23
0
class OrderItem(
        core_models.UuidMixin,
        core_models.ErrorMessageMixin,
        RequestTypeMixin,
        structure_models.StructureLoggableMixin,
        TimeStampedModel,
):
    class States:
        PENDING = 1
        EXECUTING = 2
        DONE = 3
        ERRED = 4
        TERMINATED = 5
        TERMINATING = 6

        CHOICES = (
            (PENDING, 'pending'),
            (EXECUTING, 'executing'),
            (DONE, 'done'),
            (ERRED, 'erred'),
            (TERMINATED, 'terminated'),
            (TERMINATING, 'terminating'),
        )

        TERMINAL_STATES = {DONE, ERRED}

    order = models.ForeignKey(on_delete=models.CASCADE,
                              to=Order,
                              related_name='items')
    offering = models.ForeignKey(on_delete=models.CASCADE, to=Offering)
    attributes = BetterJSONField(blank=True, default=dict)
    old_plan = models.ForeignKey(on_delete=models.CASCADE,
                                 to=Plan,
                                 related_name='+',
                                 null=True,
                                 blank=True)
    resource = models.ForeignKey(on_delete=models.CASCADE,
                                 to=Resource,
                                 null=True,
                                 blank=True)
    state = FSMIntegerField(default=States.PENDING, choices=States.CHOICES)
    activated = models.DateTimeField(_('activation date'),
                                     null=True,
                                     blank=True)
    output = models.TextField(blank=True)
    tracker = FieldTracker()

    class Permissions:
        customer_path = 'order__project__customer'
        project_path = 'order__project'

    class Meta:
        verbose_name = _('Order item')
        ordering = ('created', )

    @classmethod
    def get_url_name(cls):
        return 'marketplace-order-item'

    @transition(field=state,
                source=[States.PENDING, States.ERRED],
                target=States.EXECUTING)
    def set_state_executing(self):
        pass

    @transition(field=state, source=States.EXECUTING, target=States.DONE)
    def set_state_done(self):
        self.activated = timezone.now()
        self.save()

    @transition(field=state, source='*', target=States.ERRED)
    def set_state_erred(self):
        pass

    @transition(field=state, source='*', target=States.TERMINATED)
    def set_state_terminated(self):
        pass

    @transition(
        field=state,
        source=[States.PENDING, States.EXECUTING],
        target=States.TERMINATING,
    )
    def set_state_terminating(self):
        pass

    def clean(self):
        offering = self.offering
        customer = self.order.project.customer

        if offering.shared:
            return

        if offering.customer == customer:
            return

        if offering.allowed_customers.filter(pk=customer.pk).exists():
            return

        raise ValidationError(
            _('Offering "%s" is not allowed in organization "%s".') %
            (offering.name, customer.name))

    def get_log_fields(self):
        return (
            'uuid',
            'created',
            'modified',
            'cost',
            'limits',
            'attributes',
            'offering',
            'resource',
            'plan',
            'get_state_display',
            'get_type_display',
        )

    @property
    def safe_attributes(self):
        """
        Get attributes excluding secret attributes, such as username and password.
        """
        secret_attributes = plugins.manager.get_secret_attributes(
            self.offering.type)
        attributes = self.attributes or {}
        return {
            key: attributes[key]
            for key in attributes.keys() if key not in secret_attributes
        }

    def __str__(self):
        return 'type: %s, created_by: %s' % (
            self.get_type_display(),
            self.order.created_by,
        )