From a1e020ae2ddae6c0d4c5b3ad0165008fb64bb17c Mon Sep 17 00:00:00 2001 From: Setyo Nugroho Date: Tue, 23 May 2023 07:17:59 +0000 Subject: [PATCH] Update invoice handling --- core/feature/unpaid_invoice_handle/actions.py | 9 ++ core/feature/unpaid_invoice_handle/command.py | 122 ++++++++++++++++++ .../unpaid_invoice_handle/event_handler.py | 64 +++++++++ .../commands/handle_unpaid_invoice.py | 81 +----------- 4 files changed, 200 insertions(+), 76 deletions(-) create mode 100644 core/feature/unpaid_invoice_handle/actions.py create mode 100644 core/feature/unpaid_invoice_handle/command.py create mode 100644 core/feature/unpaid_invoice_handle/event_handler.py diff --git a/core/feature/unpaid_invoice_handle/actions.py b/core/feature/unpaid_invoice_handle/actions.py new file mode 100644 index 0000000..afb93eb --- /dev/null +++ b/core/feature/unpaid_invoice_handle/actions.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class UnpainInvoiceAction(Enum): + SEND_MESSAGE = "send_message" + STOP_INSTANCE = "stop_instance" + SUSPEND_INSTANCE = "suspend_instance" + PAUSE_INSTANCE = "pause_instance" + DELETE_INSTANCE = "delete_instance" diff --git a/core/feature/unpaid_invoice_handle/command.py b/core/feature/unpaid_invoice_handle/command.py new file mode 100644 index 0000000..4d71f5c --- /dev/null +++ b/core/feature/unpaid_invoice_handle/command.py @@ -0,0 +1,122 @@ +import logging +import traceback + +import openstack +from django.utils import timezone +from core.feature.unpaid_invoice_handle.actions import UnpainInvoiceAction + +from core.models import Invoice +from core.notification import send_notification +from core.utils.dynamic_setting import get_dynamic_setting, BILLING_ENABLED +from yuyu import settings + + +LOG = logging.getLogger("yuyu") + +class UnpaidInvoiceHandlerCommand: + def handle(self): + LOG.info("Processing Unpaid Invoice") + + # Initialize connection + if not get_dynamic_setting(BILLING_ENABLED): + return + + expired_unpaid_invoices = Invoice.objects.filter(state=Invoice.InvoiceState.UNPAID).all() + for invoice in expired_unpaid_invoices: + self.run_action_on_config(invoice) + + LOG.info("Processing Unpaid Invoice Done") + + def run_action_on_config(self, invoice): + schedule_config = settings.UNPAID_INVOICE_HANDLER_CONFIG + date_diff = timezone.now() - invoice.end_date + past_day = date_diff.days + for config in schedule_config: + if config['day'] == past_day: + self.run_action(invoice, config['action'], config) + + def run_action(self, invoice, action, config=None): + try: + if action == UnpainInvoiceAction.SEND_MESSAGE: + self._send_message(invoice, config) + + if action == UnpainInvoiceAction.STOP_INSTANCE: + self._stop_component(invoice) + + if action == UnpainInvoiceAction.SUSPEND_INSTANCE: + self._stop_component(invoice) + + if action == UnpainInvoiceAction.PAUSE_INSTANCE: + self._stop_component(invoice) + + if action == UnpainInvoiceAction.DELETE_INSTANCE: + self._delete_component(invoice) + except Exception: + LOG.exception("Failed to process Unpaid Invoice") + send_notification( + project=None, + title='[Error] Error when processing unpaid invoice', + short_description=f'There is an error when processing unpaid invoice', + content=f'There is an error when handling unpaid invoice \n {traceback.format_exc()}', + ) + + def _stop_component(self, invoice: Invoice): + LOG.info("Stopping Component") + conn = openstack.connect(cloud=settings.CLOUD_CONFIG_NAME) + + # Stop Instance + for server in conn.compute.servers(project_id=invoice.project.tenant_id): + conn.compute.stop_server(server) + + def _suspend_component(self, invoice: Invoice): + LOG.info("Suspending Component") + conn = openstack.connect(cloud=settings.CLOUD_CONFIG_NAME) + + # Suspend Instance + for server in conn.compute.servers(project_id=invoice.project.tenant_id): + conn.compute.suspend_server(server) + + def _pause_component(self, invoice: Invoice): + LOG.info("Pausing Component") + conn = openstack.connect(cloud=settings.CLOUD_CONFIG_NAME) + + # Pause Instance + for server in conn.compute.servers(project_id=invoice.project.tenant_id): + conn.compute.pause_server(server) + + def _delete_component(self, invoice: Invoice): + LOG.info("Deleting Component") + conn = openstack.connect(cloud=settings.CLOUD_CONFIG_NAME) + + # Delete Instance + for server in conn.compute.servers(project_id=invoice.project.tenant_id): + conn.compute.delete_server(server) + + # Delete Image + for image in conn.compute.images(project_id=invoice.project.tenant_id): + conn.compute.delete_image(image) + + # Delete Floating Ips + for ip in conn.network.ips(project_id=invoice.project.tenant_id): + conn.network.delete_ip(ip) + + # Delete Router + for route in conn.network.routers(project_id=invoice.project.tenant_id): + conn.network.delete_router(route) + + # Delete Volume + for volume in conn.block_storage.volumes(project_id=invoice.project.tenant_id): + conn.block_storage.delete_volume(volume) + + # Delete Snapshot + for snapshot in conn.block_storage.snapshots(project_id=invoice.project.tenant_id): + conn.block_storage.delete_snapshot(snapshot) + + def _send_message(self, invoice: Invoice, message_config): + LOG.info("Sending Message") + send_notification( + project=invoice.project, + title=message_config['message_title'], + short_description=message_config['message_short_description'], + content=message_config['message_content'], + ) \ No newline at end of file diff --git a/core/feature/unpaid_invoice_handle/event_handler.py b/core/feature/unpaid_invoice_handle/event_handler.py new file mode 100644 index 0000000..844489d --- /dev/null +++ b/core/feature/unpaid_invoice_handle/event_handler.py @@ -0,0 +1,64 @@ +from datetime import timezone +import logging +from core.feature.unpaid_invoice_handle.actions import UnpainInvoiceAction +from core.feature.unpaid_invoice_handle.command import UnpaidInvoiceHandlerCommand +from core.models import Invoice +from yuyu import settings + +LOG = logging.getLogger("yuyu") + +class UnpaidInvoiceEventHandler: + filter_action = [ + UnpainInvoiceAction.STOP_INSTANCE, + UnpainInvoiceAction.SUSPEND_INSTANCE, + UnpainInvoiceAction.PAUSE_INSTANCE, + UnpainInvoiceAction.DELETE_INSTANCE, + ] + + def filter_event_project_id(event_type, raw_payload): + if event_type == 'floatingip.create.end': + return raw_payload['floatingip']['tenant_id'] + if event_type == 'image.activate': + return raw_payload['owner'] + if event_type == 'compute.instance.update': + return raw_payload['tenant_id'] + if event_type == 'router.create.end': + return raw_payload['router']['tenant_id'] + if event_type == 'router.update.end': + return raw_payload['router']['tenant_id'] + if event_type == 'snapshot.create.end': + return raw_payload['tenant_id'] + if event_type == 'volume.create.end': + return raw_payload['tenant_id'] + + return None + + def handle(self, event_type, raw_payload): + try: + LOG.exception("Processing Unpaid Invoice Event") + schedule_config = settings.UNPAID_INVOICE_HANDLER_CONFIG + command = UnpaidInvoiceHandlerCommand() + project_id = self.filter_event_project_id(event_type, raw_payload) + if project_id: + # Fetch All Unpaid Invoice + unpaid_invoice = Invoice.objects.filter(project__tenant_id=project_id, state=Invoice.InvoiceState.UNPAID).first() + used_config = {} + used_day = 0 + + if unpaid_invoice: + # Calculate Days + # Find last config that has beed runned in past days + date_diff = timezone.now() - unpaid_invoice.end_date + past_day = date_diff.days + for config in schedule_config: + if config['action'] not in self.filter_action: + continue + + if config['day'] <= past_day and config['day'] > used_day: + used_day = config['day'] + used_config = config + + if used_config: + command.run_action(unpaid_invoice, used_config['action'], config) + except Exception: + LOG.exception("Failed to process Unpaid Invoice Event") diff --git a/core/management/commands/handle_unpaid_invoice.py b/core/management/commands/handle_unpaid_invoice.py index 97a003b..732516e 100644 --- a/core/management/commands/handle_unpaid_invoice.py +++ b/core/management/commands/handle_unpaid_invoice.py @@ -4,89 +4,18 @@ import traceback import openstack from django.core.management import BaseCommand from django.utils import timezone +from core.feature.unpaid_invoice_handle.command import UnpaidInvoiceHandlerCommand from core.models import Invoice from core.notification import send_notification from core.utils.dynamic_setting import get_dynamic_setting, BILLING_ENABLED from yuyu import settings -LOG = logging.getLogger("yuyu_unpaid_invoice_handler") +LOG = logging.getLogger("yuyu") class Command(BaseCommand): - help = 'Yuyu Unpaid Invoice Handler' - + unpaid_invoice_command = UnpaidInvoiceHandlerCommand() + def handle(self, *args, **options): - print("Processing Unpaid Invoice") - # Initialize connection - if not get_dynamic_setting(BILLING_ENABLED): - return - - schedule_config = settings.UNPAID_INVOICE_HANDLER_CONFIG - expired_unpaid_invoices = Invoice.objects.filter(state=Invoice.InvoiceState.UNPAID).all() - for invoice in expired_unpaid_invoices: - date_diff = timezone.now() - invoice.end_date - past_day = date_diff.days - for config in schedule_config: - if config['day'] == past_day: - try: - if config['action'] == 'send_message': - self._send_message(invoice, config) - - if config['action'] == 'stop_instance': - self._stop_component(invoice) - - if config['action'] == 'delete_instance': - self._delete_component(invoice) - except Exception: - send_notification( - project=None, - title='[Error] Error when processing unpaid invoice', - short_description=f'There is an error when processing unpaid invoice', - content=f'There is an error when handling unpaid invoice \n {traceback.format_exc()}', - ) - - print("Processing Done") - - def _stop_component(self, invoice: Invoice): - conn = openstack.connect(cloud=settings.CLOUD_CONFIG_NAME) - - # Stop Instance - for server in conn.compute.servers(project_id=invoice.project.tenant_id): - conn.compute.stop_server(server) - - def _delete_component(self, invoice: Invoice): - conn = openstack.connect(cloud=settings.CLOUD_CONFIG_NAME) - - # Delete Instance - for server in conn.compute.servers(project_id=invoice.project.tenant_id): - conn.compute.delete_server(server) - - # Delete Image - for image in conn.compute.images(project_id=invoice.project.tenant_id): - conn.compute.delete_image(image) - - # Delete Floating Ips - for ip in conn.network.ips(project_id=invoice.project.tenant_id): - conn.network.delete_ip(ip) - - # Delete Router - for route in conn.network.routers(project_id=invoice.project.tenant_id): - conn.network.delete_router(route) - - # Delete Volume - for volume in conn.block_storage.volumes(project_id=invoice.project.tenant_id): - conn.block_storage.delete_volume(volume) - - # Delete Snapshot - for snapshot in conn.block_storage.snapshots(project_id=invoice.project.tenant_id): - conn.block_storage.delete_snapshot(snapshot) - - def _send_message(self, invoice: Invoice, message_config): - print('Sending Message') - send_notification( - project=invoice.project, - title=message_config['message_title'], - short_description=message_config['message_short_description'], - content=message_config['message_content'], - ) + self.unpaid_invoice_command.handle() \ No newline at end of file