def recalculate_estimate(recalculate_total=False): """ Recalculate price of consumables that were used by resource until now. Regular task. It is too expensive to calculate consumed price on each request, so we store cached price each hour. If recalculate_total is True - task also recalculates total estimate for current month. """ # Celery does not import server.urls and does not discover cost tracking modules. # So they should be discovered implicitly. CostTrackingRegister.autodiscover() # Step 1. Recalculate resources estimates. for resource_model in CostTrackingRegister.registered_resources: for resource in resource_model.objects.all(): _update_resource_consumed(resource, recalculate_total=recalculate_total) # Step 2. Move from down to top and recalculate consumed estimate for each # object based on its children. ancestors_models = [ m for m in models.PriceEstimate.get_estimated_models() if not issubclass(m, structure_models.ResourceMixin) ] for model in ancestors_models: for ancestor in model.objects.all(): _update_ancestor_consumed(ancestor)
def reinit_configurations(self, request): """ Re-initialize configuration for resource if it has been changed. This method should be called if resource consumption strategy was changed. """ now = timezone.now() # Step 1. Collect all resources with changed configuration. changed_resources = [] for resource_model in CostTrackingRegister.registered_resources: for resource in resource_model.objects.all(): try: pe = models.PriceEstimate.objects.get( scope=resource, month=now.month, year=now.year ) except models.PriceEstimate.DoesNotExist: changed_resources.append(resource) else: new_configuration = CostTrackingRegister.get_configuration(resource) if new_configuration != pe.consumption_details.configuration: changed_resources.append(resource) # Step 2. Re-init configuration and recalculate estimate for changed resources. for resource in changed_resources: models.PriceEstimate.update_resource_estimate( resource, CostTrackingRegister.get_configuration(resource) ) message = _('Configuration was reinitialized for %(count)s resources') % { 'count': len(changed_resources) } self.message_user(request, message) return redirect(reverse('admin:cost_tracking_defaultpricelistitem_changelist'))
def setUp(self): CostTrackingRegister.register_strategy( factories.TestNewInstanceCostTrackingStrategy) resource_content_type = ContentType.objects.get_for_model( TestNewInstance) self.price_list_item = models.DefaultPriceListItem.objects.create( item_type='storage', key='1 MB', value=0.5, resource_content_type=resource_content_type)
def delete_not_registered(self, request): deleted_items_names = [] for price_list_item in models.DefaultPriceListItem.objects.all(): try: resource_class = price_list_item.resource_content_type.model_class( ) consumable_items = CostTrackingRegister.get_consumable_items( resource_class) next(item for item in consumable_items if item.key == price_list_item.key and item.item_type == price_list_item.item_type) except (ResourceNotRegisteredError, StopIteration): deleted_items_names.append(price_list_item.name) price_list_item.delete() if deleted_items_names: message = ungettext( _('Price item was deleted: %s.') % deleted_items_names[0], _('Price items were deleted: %s.') % ', '.join(item for item in deleted_items_names), len(deleted_items_names)) self.message_user(request, message) else: self.message_user( request, _('Nothing to delete. All default price items are registered.') ) return redirect( reverse('admin:cost_tracking_defaultpricelistitem_changelist'))
def setUp(self): resource_content_type = ContentType.objects.get_for_model( TestNewInstance) self.price_list_item = models.DefaultPriceListItem.objects.create( item_type='storage', key='1 MB', resource_content_type=resource_content_type, value=2) CostTrackingRegister.register_strategy( factories.TestNewInstanceCostTrackingStrategy) self.start_time = datetime.datetime(2016, 8, 8, 11, 0) with freeze_time(self.start_time): self.resource = structure_factories.TestNewInstanceFactory( disk=20 * 1024) self.spl = self.resource.service_project_link self.project = self.spl.project self.customer = self.project.customer self.service = self.spl.service
def resource_quota_update(sender, instance, **kwargs): """ Update resource consumption details and price estimate if its configuration has changed """ quota = instance resource = quota.scope try: new_configuration = CostTrackingRegister.get_configuration(resource) except ResourceNotRegisteredError: return models.PriceEstimate.update_resource_estimate( resource, new_configuration, raise_exception=not _is_in_celery_task())
def handle(self, *args, **options): today = timezone.now() with transaction.atomic(): # Delete current month price estimates models.PriceEstimate.objects.filter(month=today.month, year=today.year).delete() # Create new estimates for resources and ancestors for resource_model in CostTrackingRegister.registered_resources: for resource in resource_model.objects.all(): configuration = CostTrackingRegister.get_configuration(resource) date = max(core_utils.month_start(today), resource.created) models.PriceEstimate.create_historical(resource, configuration, date) # recalculate consumed estimate tasks.recalculate_estimate()
def resource_update(sender, instance, created=False, **kwargs): """ Update resource consumption details and price estimate if its configuration has changed. Create estimates for previous months if resource was created not in current month. """ resource = instance try: new_configuration = CostTrackingRegister.get_configuration(resource) except ResourceNotRegisteredError: return models.PriceEstimate.update_resource_estimate( resource, new_configuration, raise_exception=not _is_in_celery_task()) # Try to create historical price estimates if created: _create_historical_estimates(resource, new_configuration)
from django.conf.urls import include, url from django.contrib import admin from django.views.generic import TemplateView from waldur_core.core import WaldurExtension from waldur_core.core import views as core_views from waldur_core.core.routers import SortedDefaultRouter as DefaultRouter from waldur_core.core.schemas import WaldurSchemaView from waldur_core.cost_tracking import urls as cost_tracking_urls, CostTrackingRegister from waldur_core.logging import urls as logging_urls from waldur_core.monitoring import urls as monitoring_urls from waldur_core.quotas import urls as quotas_urls from waldur_core.structure import urls as structure_urls from waldur_core.users import urls as users_urls CostTrackingRegister.autodiscover() router = DefaultRouter() cost_tracking_urls.register_in(router) logging_urls.register_in(router) monitoring_urls.register_in(router) quotas_urls.register_in(router) structure_urls.register_in(router) users_urls.register_in(router) urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^admintools/', include('admin_tools.urls')), ] if settings.WALDUR_CORE.get('EXTENSIONS_AUTOREGISTER'):
from . import models class InstanceStrategy(CostTrackingStrategy): resource_class = models.Instance class Types: FLAVOR = 'flavor' @classmethod def get_consumable_items(cls): return [ ConsumableItem( item_type=cls.Types.FLAVOR, key=size.backend_id, default_price=size.price, ) for size in models.Size.objects.all() ] @classmethod def get_configuration(cls, instance): consumables = {} if instance.state != models.Instance.States.ERRED: consumables[ConsumableItem(item_type=cls.Types.FLAVOR, key=instance.size_backend_id)] = 1 return consumables CostTrackingRegister.register_strategy(InstanceStrategy)
from waldur_core.cost_tracking import CostTrackingRegister, CostTrackingStrategy, ConsumableItem from . import models class DropletStrategy(CostTrackingStrategy): resource_class = models.Droplet class Types: FLAVOR = 'flavor' @classmethod def get_consumable_items(cls): return [ConsumableItem(item_type=cls.Types.FLAVOR, key=size.name, default_price=size.price) for size in models.Size.objects.all()] @classmethod def get_configuration(cls, droplet): consumables = {} if droplet.state != models.Droplet.States.ERRED and droplet.size_name: consumables[ConsumableItem(item_type=cls.Types.FLAVOR, key=droplet.size_name)] = 1 return consumables CostTrackingRegister.register_strategy(DropletStrategy)
class TenantStrategy(CostTrackingStrategy): resource_class = openstack_models.Tenant @classmethod def get_consumable_items(cls): for package_template in models.PackageTemplate.objects.all(): yield utils.get_consumable_item(package_template) @classmethod def get_configuration(cls, tenant): configuration = {} if tenant.state != tenant.States.ERRED: if 'package_name' not in tenant.extra_configuration: logger.debug( 'Package name is not defined in configuration of tenant %s, (PK: %s)', tenant.name, tenant.pk, ) else: package_name = tenant.extra_configuration['package_name'] configuration = { ConsumableItem(item_type=utils.Types.PACKAGE_TEMPLATE, key=package_name): 1, } return configuration CostTrackingRegister.register_strategy(TenantStrategy)
def setUp(self): CostTrackingRegister.register_strategy( factories.TestNewInstanceCostTrackingStrategy)
@classmethod def get_consumable_items(cls): for flavor_name in set(models.Flavor.objects.all().values_list( 'name', flat=True)): yield utils.get_consumable_item(flavor_name) @classmethod def get_configuration(cls, instance): consumables = {} if instance.state != models.Instance.States.ERRED: consumables[ConsumableItem(item_type=cls.Types.FLAVOR, key=instance.flavor_name)] = 1 return consumables CostTrackingRegister.register_strategy(InstanceStrategy) class VolumeStrategy(CostTrackingStrategy): resource_class = models.Volume class Types: STORAGE = PriceItemTypes.STORAGE class Keys: STORAGE = '1 GB' @classmethod def get_consumable_items(cls): return [ ConsumableItem(
from waldur_core.cost_tracking import CostTrackingStrategy, ConsumableItem, CostTrackingRegister from . import models, backend class AzureCostTrackingStrategy(CostTrackingStrategy): resource_class = models.VirtualMachine class Types(object): FLAVOR = 'flavor' @classmethod def get_consumable_items(cls): return [ ConsumableItem(item_type=cls.Types.FLAVOR, key=size.name, default_price=size.price) for size in backend.SizeQueryset() ] @classmethod def get_configuration(cls, virtual_machine): consumables = {} if virtual_machine.state != models.VirtualMachine.States.ERRED and virtual_machine.image_name: consumables[ConsumableItem(item_type=cls.Types.FLAVOR, key=virtual_machine.image_name)] = 1 return consumables CostTrackingRegister.register_strategy(AzureCostTrackingStrategy)