Compare commits
8 commits
main
...
feat/updat
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b131edbbb1 | ||
![]() |
c1d0d7b918 | ||
![]() |
57d6c31ab6 | ||
![]() |
194a2f801e | ||
![]() |
23e988df9f | ||
![]() |
a1e020ae2d | ||
![]() |
72c28d2b57 | ||
![]() |
9391faec68 |
23 changed files with 744 additions and 50 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -280,4 +280,6 @@ Network Trash Folder
|
||||||
Temporary Items
|
Temporary Items
|
||||||
.apdisk
|
.apdisk
|
||||||
|
|
||||||
|
clouds.yaml
|
||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/macos
|
# End of https://www.toptal.com/developers/gitignore/api/macos
|
|
@ -1,3 +1,12 @@
|
||||||
|
Next version
|
||||||
|
--------------------------
|
||||||
|
- Feature: Project balance
|
||||||
|
- Feature: Prometeus metric export
|
||||||
|
- Feature: Unpaid Invoice handling
|
||||||
|
- Fix: Money maximum digit
|
||||||
|
- Fix: Fix monthly price calculation that use UTC timezone
|
||||||
|
- Add setup_cron.sh script to easily setup yuyu cronjob
|
||||||
|
|
||||||
v1.0.1
|
v1.0.1
|
||||||
---------------------------
|
---------------------------
|
||||||
- Fix: Support decimal in price (database migration needed)
|
- Fix: Support decimal in price (database migration needed)
|
||||||
|
|
265
README.md
265
README.md
|
@ -1,17 +1,21 @@
|
||||||
# Yuyu
|
# Yuyu
|
||||||
|
|
||||||
Yuyu provide ability to manage openstack billing by listening to every openstack event. Yuyu is a required component to use Yuyu Dashboard. There are 3 main component in Yuyu: API, Cron, Event Monitor
|
Yuyu provide ability to manage openstack billing by listening to every openstack event. Yuyu is a required component to use Yuyu Dashboard. There are 3 main component in Yuyu: API, Cron, Event Monitor
|
||||||
|
|
||||||
## Yuyu API
|
## Yuyu API
|
||||||
|
|
||||||
Main component to communicate with Yuyu Dashboard.
|
Main component to communicate with Yuyu Dashboard.
|
||||||
|
|
||||||
## Yuyu Cron
|
## Yuyu Cron
|
||||||
|
|
||||||
Provide invoice calculation and rolling capabilities that needed to run every month.
|
Provide invoice calculation and rolling capabilities that needed to run every month.
|
||||||
|
|
||||||
## Yuyu Event Monitor
|
## Yuyu Event Monitor
|
||||||
|
|
||||||
Monitor event from openstack to calculate billing spent.
|
Monitor event from openstack to calculate billing spent.
|
||||||
|
|
||||||
# System Requirement
|
# System Requirement
|
||||||
|
|
||||||
- Python 3
|
- Python 3
|
||||||
- Openstack
|
- Openstack
|
||||||
- Virtualenv
|
- Virtualenv
|
||||||
|
@ -20,6 +24,7 @@ Monitor event from openstack to calculate billing spent.
|
||||||
# Pre-Installation
|
# Pre-Installation
|
||||||
|
|
||||||
### Virtualenv
|
### Virtualenv
|
||||||
|
|
||||||
Make sure you installed virtualenv before installing Yuyu
|
Make sure you installed virtualenv before installing Yuyu
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
@ -31,13 +36,16 @@ pip3 install virtualenv
|
||||||
Billing is a time sensitive application, please make sure you set a correct time and timezone on you machine.
|
Billing is a time sensitive application, please make sure you set a correct time and timezone on you machine.
|
||||||
|
|
||||||
### Openstack Service Notification
|
### Openstack Service Notification
|
||||||
|
|
||||||
You need to enable notification for this openstack service:
|
You need to enable notification for this openstack service:
|
||||||
|
|
||||||
- Nova (nova.conf)
|
- Nova (nova.conf)
|
||||||
- Cinder (cinder.conf)
|
- Cinder (cinder.conf)
|
||||||
- Neutron (neutron.conf)
|
- Neutron (neutron.conf)
|
||||||
- Keystone (keystone.conf)
|
- Keystone (keystone.conf)
|
||||||
|
|
||||||
### Nova
|
### Nova
|
||||||
|
|
||||||
Add configuration below on `[oslo_messaging_notifications]`
|
Add configuration below on `[oslo_messaging_notifications]`
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -62,7 +70,9 @@ topics = notifications
|
||||||
```
|
```
|
||||||
|
|
||||||
### Kolla Note
|
### Kolla Note
|
||||||
If you using Kolla, please add configuration above on all service container. For example on Nova you should put the config on `nova-api`, `nova-scheduler`, etc.
|
|
||||||
|
If you using Kolla, please add configuration above on all service container. For example on Nova you should put the
|
||||||
|
config on `nova-api`, `nova-scheduler`, etc.
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
|
@ -75,6 +85,7 @@ cd yuyu
|
||||||
```
|
```
|
||||||
|
|
||||||
Then create virtualenv and activate it
|
Then create virtualenv and activate it
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
virtualenv env --python=python3.8
|
virtualenv env --python=python3.8
|
||||||
source env/bin/activate
|
source env/bin/activate
|
||||||
|
@ -87,7 +98,8 @@ Then create a configuration file, just copy from sample file and modify as your
|
||||||
cp yuyu/local_settings.py.sample yuyu/local_settings.py
|
cp yuyu/local_settings.py.sample yuyu/local_settings.py
|
||||||
```
|
```
|
||||||
|
|
||||||
Please read [Local Setting Configuration](#local-setting-configuration) to get to know about what configuration you should change.
|
Please read [Local Setting Configuration](#local-setting-configuration) to get to know about what configuration you
|
||||||
|
should change.
|
||||||
|
|
||||||
Then run the database migration
|
Then run the database migration
|
||||||
|
|
||||||
|
@ -104,23 +116,27 @@ python manage.py createsuperuser
|
||||||
## Local Setting Configuration
|
## Local Setting Configuration
|
||||||
|
|
||||||
### YUYU_NOTIFICATION_URL (required)
|
### YUYU_NOTIFICATION_URL (required)
|
||||||
|
|
||||||
A Messaging Queue URL that used by Openstack, usually it is a RabbitMQ URL.
|
A Messaging Queue URL that used by Openstack, usually it is a RabbitMQ URL.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```
|
```
|
||||||
YUYU_NOTIFICATION_URL = "rabbit://openstack:password@127.0.0.1:5672/"
|
YUYU_NOTIFICATION_URL = "rabbit://openstack:password@127.0.0.1:5672/"
|
||||||
```
|
```
|
||||||
|
|
||||||
### YUYU_NOTIFICATION_TOPICS (required)
|
### YUYU_NOTIFICATION_TOPICS (required)
|
||||||
|
|
||||||
A list of topic notification topic that is configured on each openstack service
|
A list of topic notification topic that is configured on each openstack service
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```
|
```
|
||||||
YUYU_NOTIFICATION_TOPICS = ["notifications"]
|
YUYU_NOTIFICATION_TOPICS = ["notifications"]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### DATABASE
|
### DATABASE
|
||||||
|
|
||||||
By default, it will use Sqlite. If you want to change it to other database please refer to Django Setting documentation.
|
By default, it will use Sqlite. If you want to change it to other database please refer to Django Setting documentation.
|
||||||
|
|
||||||
- https://docs.djangoproject.com/en/3.2/ref/settings/#databases
|
- https://docs.djangoproject.com/en/3.2/ref/settings/#databases
|
||||||
|
@ -137,12 +153,13 @@ To install Yuyu API, you need to execute this command.
|
||||||
This will install `yuyu_api` service
|
This will install `yuyu_api` service
|
||||||
|
|
||||||
To start the service use this command
|
To start the service use this command
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
systemctl enable yuyu_api
|
systemctl enable yuyu_api
|
||||||
systemctl start yuyu_api
|
systemctl start yuyu_api
|
||||||
```
|
```
|
||||||
|
|
||||||
An API server will be open on port `8182`.
|
An API server will be open on port `8182`.
|
||||||
|
|
||||||
## Event Monitor Installation
|
## Event Monitor Installation
|
||||||
|
|
||||||
|
@ -152,10 +169,10 @@ To install Yuyu API, you need to execute this command.
|
||||||
./bin/setup_event_monitor.sh
|
./bin/setup_event_monitor.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
This will install `yuyu_event_monitor` service
|
This will install `yuyu_event_monitor` service
|
||||||
|
|
||||||
To start the service use this command
|
To start the service use this command
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
systemctl enable yuyu_event_monitor
|
systemctl enable yuyu_event_monitor
|
||||||
systemctl start yuyu_event_monitor
|
systemctl start yuyu_event_monitor
|
||||||
|
@ -163,20 +180,15 @@ systemctl start yuyu_event_monitor
|
||||||
|
|
||||||
## Cron Installation
|
## Cron Installation
|
||||||
|
|
||||||
There is a cronjob that needed to be run every month on 00:01 AM. This cronjob will finish all in progress invoice and start new invoice for the next month.
|
There is a cronjob that needed to be run every day and every month. The cronjob is used to process the invoice and handling updaid invoice.
|
||||||
|
|
||||||
To install it, you can use `crontab -e`.
|
To install the cronjob, you need to execute this command
|
||||||
|
|
||||||
Put this expression on the crontab
|
```bash
|
||||||
|
./bin/setup_cron.sh
|
||||||
```
|
|
||||||
1 0 1 * * $yuyu_dir/bin/process_invoice.sh
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Replace $yuyu_dir with the directory of where yuyu is located. Example
|
You can make sure the cron is installed by checking `crontab -e`.
|
||||||
```
|
|
||||||
1 0 1 * * /var/yuyu/bin/process_invoice.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
# Updating Yuyu
|
# Updating Yuyu
|
||||||
|
|
||||||
|
@ -192,7 +204,7 @@ Activate the virtualenv.
|
||||||
source env/bin/activate
|
source env/bin/activate
|
||||||
```
|
```
|
||||||
|
|
||||||
Change the setting if needed.
|
Change the setting if needed.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
nano yuyu/local_settings.py
|
nano yuyu/local_settings.py
|
||||||
|
@ -215,4 +227,219 @@ Restart all the service
|
||||||
```bash
|
```bash
|
||||||
systemctl restart yuyu_api
|
systemctl restart yuyu_api
|
||||||
systemctl restart yuyu_event_monitor
|
systemctl restart yuyu_event_monitor
|
||||||
|
```
|
||||||
|
|
||||||
|
# Other Feature
|
||||||
|
|
||||||
|
Yuyu also have other feature that need to be setup to use.
|
||||||
|
|
||||||
|
## Unpaid Invoice Handling
|
||||||
|
|
||||||
|
Unpaid invoice handling can run a task on a specific day after invoice is issued and not yet paid.
|
||||||
|
|
||||||
|
For example, you can send email to remind customer to pay the invoice or delete all instance on a specific day.
|
||||||
|
|
||||||
|
Available action for unpaid invoice handling consist of:
|
||||||
|
|
||||||
|
- send_message : Sending a message to a customer
|
||||||
|
- pause_instance : Will pause compute instance
|
||||||
|
- suspend_instance : Will suspend compute instance
|
||||||
|
- stop_instance : Will stop compute instance
|
||||||
|
- delete_instance : Will delete compute, image, router, snapshot or volume
|
||||||
|
|
||||||
|
|
||||||
|
### Enabling Unpaid Invoice Handling
|
||||||
|
|
||||||
|
1. Download `clouds.yaml` configuration from Openstack API Dashboard
|
||||||
|
2. Make sure you also set the password in `clouds.yaml`. Example:
|
||||||
|
```yaml
|
||||||
|
clouds:
|
||||||
|
openstack:
|
||||||
|
auth:
|
||||||
|
auth_url: http://172.10.10.150:5000
|
||||||
|
username: "admin"
|
||||||
|
password: "your_password"
|
||||||
|
project_id: 0000000000000000000000000
|
||||||
|
project_name: "admin"
|
||||||
|
user_domain_name: "Default"
|
||||||
|
region_name: "ID"
|
||||||
|
interface: "public"
|
||||||
|
identity_api_version: 3
|
||||||
|
```
|
||||||
|
3. Put `clouds.yaml` in one of the following directory.
|
||||||
|
- Current Yuyu Directory
|
||||||
|
- ~/.config/openstack
|
||||||
|
- /etc/openstack
|
||||||
|
4. Setup the configuration in `local_settings.py`. See **Configuration** for detail.
|
||||||
|
|
||||||
|
Example config can follow, you can put any number of config or remove it completely to disable it.
|
||||||
|
```python
|
||||||
|
CLOUD_CONFIG_NAME = "openstack"
|
||||||
|
UNPAID_INVOICE_HANDLER_CONFIG = [
|
||||||
|
{
|
||||||
|
"day": 5,
|
||||||
|
"action": "send_message",
|
||||||
|
"message_title": "Your invoice has been expired. Please pay now!",
|
||||||
|
"message_short_description": "Your invoice has been expired. Please pay now!",
|
||||||
|
"message_content": "Your invoice has been expired. Please pay now!",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"day": 10,
|
||||||
|
"action": "stop_instance",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"day": 10,
|
||||||
|
"action": "send_message",
|
||||||
|
"message_title": "Your compute instance will be stopped",
|
||||||
|
"message_short_description": "Your compute instance will be stopped",
|
||||||
|
"message_content": "Your compute instance will be stopped because you have unpaid invoice",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"day": 15,
|
||||||
|
"action": "send_message",
|
||||||
|
"message_title": "All of your instance has been deleted",
|
||||||
|
"message_short_description": "All of your instance has been deleted",
|
||||||
|
"message_content": "All of your instance has been deleted because you have unpaid invoice",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"day": 15,
|
||||||
|
"action": "delete_instance",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
```
|
||||||
|
5. Setup `cronjob` to run the action every day
|
||||||
|
|
||||||
|
To install it, you need to execute this command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./bin/setup_cron.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
You can make sure the cron is installed by checking `crontab -e` and make sure `handle_unpaid_invoice.sh` is present
|
||||||
|
|
||||||
|
6. Check the connection to openstack with
|
||||||
|
```
|
||||||
|
./bin/check_openstack_connection.sh
|
||||||
|
```
|
||||||
|
Make sure it doesn't return error.
|
||||||
|
7. Done
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
To use Unpaid Invoice Handling you need to set up the following variable in `local_settings.py`
|
||||||
|
|
||||||
|
- CLOUD_CONFIG_NAME
|
||||||
|
- UNPAID_INVOICE_HANDLER_CONFIG
|
||||||
|
|
||||||
|
#### CLOUD_CONFIG_NAME
|
||||||
|
|
||||||
|
CLOUD_CONFIG_NAME Is configuration in `clouds.yaml` that you want to use to connect to openstack.
|
||||||
|
|
||||||
|
Example: Your `clouds.yaml` is
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
clouds:
|
||||||
|
openstack:
|
||||||
|
auth:
|
||||||
|
auth_url: http://172.10.10.150:5000
|
||||||
|
username: "admin"
|
||||||
|
password: "your_password"
|
||||||
|
project_id: 0000000000000000000000000
|
||||||
|
project_name: "admin"
|
||||||
|
user_domain_name: "Default"
|
||||||
|
region_name: "ID"
|
||||||
|
interface: "public"
|
||||||
|
identity_api_version: 3
|
||||||
|
```
|
||||||
|
|
||||||
|
You can put `openstack` as `CLOUD_CONFIG_NAME`
|
||||||
|
|
||||||
|
So it will be
|
||||||
|
|
||||||
|
```python
|
||||||
|
CLOUD_CONFIG_NAME = "openstack"
|
||||||
|
```
|
||||||
|
|
||||||
|
### UNPAID_INVOICE_HANDLER_CONFIG
|
||||||
|
|
||||||
|
UNPAID_INVOICE_HANDLER_CONFIG Is configuration for an action that will be run on a particular day after invoice is
|
||||||
|
issued and still unpaid
|
||||||
|
|
||||||
|
The available action that can be used is
|
||||||
|
|
||||||
|
- send_message : Sending a message to a customer
|
||||||
|
- stop_instance : Will stop, compute instance
|
||||||
|
- delete_instance : Will delete compute, image, router, snapshot or volume
|
||||||
|
|
||||||
|
UNPAID_INVOICE_HANDLER_CONFIG is list of dictionary, you can add as many config as you want to the list that will be run
|
||||||
|
on a particular day.
|
||||||
|
|
||||||
|
The format for config dictionary is
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"day": 0,
|
||||||
|
"action": "the_action",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The most important part is `day` and `action`.
|
||||||
|
|
||||||
|
- `day`: The day of the action that will be run. For example if you put `5` it will run in 5 day after invoice is issued
|
||||||
|
and still unpaid
|
||||||
|
- `action`: The action that you want to run. It can be `send_message`/`stop_instance`/`delete_instance`.
|
||||||
|
|
||||||
|
If you use `send_message` action, you need to add additional config.
|
||||||
|
|
||||||
|
- `message_title` : The title or subject of the message
|
||||||
|
- `message_short_description` : The short description of message
|
||||||
|
- `message_content`: The content of the message
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"day": 0,
|
||||||
|
"action": "send_message",
|
||||||
|
"message_title": "Title",
|
||||||
|
"message_short_description": "Short Description",
|
||||||
|
"message_content": "The Content",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This is example config that you can use as a reference
|
||||||
|
|
||||||
|
```python
|
||||||
|
CLOUD_CONFIG_NAME = "openstack"
|
||||||
|
UNPAID_INVOICE_HANDLER_CONFIG = [
|
||||||
|
{
|
||||||
|
"day": 5,
|
||||||
|
"action": "send_message",
|
||||||
|
"message_title": "Your invoice has been expired. Please pay now!",
|
||||||
|
"message_short_description": "Your invoice has been expired. Please pay now!",
|
||||||
|
"message_content": "Your invoice has been expired. Please pay now!",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"day": 10,
|
||||||
|
"action": "stop_instance",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"day": 10,
|
||||||
|
"action": "send_message",
|
||||||
|
"message_title": "Your compute instance will be stopped",
|
||||||
|
"message_short_description": "Your compute instance will be stopped",
|
||||||
|
"message_content": "Your compute instance will be stopped because you have unpaid invoice",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"day": 15,
|
||||||
|
"action": "send_message",
|
||||||
|
"message_title": "All of your instance has been deleted",
|
||||||
|
"message_short_description": "All of your instance has been deleted",
|
||||||
|
"message_content": "All of your instance has been deleted because you have unpaid invoice",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"day": 15,
|
||||||
|
"action": "delete_instance",
|
||||||
|
},
|
||||||
|
]
|
||||||
```
|
```
|
39
api/views.py
39
api/views.py
|
@ -26,6 +26,7 @@ from core.component.component import INVOICE_COMPONENT_MODEL
|
||||||
from core.exception import PriceNotFound
|
from core.exception import PriceNotFound
|
||||||
from core.models import Invoice, BillingProject, Notification, Balance, InvoiceInstance
|
from core.models import Invoice, BillingProject, Notification, Balance, InvoiceInstance
|
||||||
from core.notification import send_notification_from_template
|
from core.notification import send_notification_from_template
|
||||||
|
from core.utils.date_utils import current_localtime
|
||||||
from core.utils.dynamic_setting import (
|
from core.utils.dynamic_setting import (
|
||||||
get_dynamic_settings,
|
get_dynamic_settings,
|
||||||
get_dynamic_setting,
|
get_dynamic_setting,
|
||||||
|
@ -226,7 +227,7 @@ class InvoiceViewSet(viewsets.ModelViewSet):
|
||||||
).all()
|
).all()
|
||||||
for active_invoice in active_invoices:
|
for active_invoice in active_invoices:
|
||||||
self._close_active_invoice(
|
self._close_active_invoice(
|
||||||
active_invoice, timezone.now(), get_dynamic_setting(INVOICE_TAX)
|
active_invoice, current_localtime(), get_dynamic_setting(INVOICE_TAX)
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response({"status": "success"})
|
return Response({"status": "success"})
|
||||||
|
@ -249,10 +250,30 @@ class InvoiceViewSet(viewsets.ModelViewSet):
|
||||||
active_invoice.close(close_date, tax_percentage)
|
active_invoice.close(close_date, tax_percentage)
|
||||||
|
|
||||||
@action(detail=False, methods=["POST"])
|
@action(detail=False, methods=["POST"])
|
||||||
|
@transaction.atomic
|
||||||
def reset_billing(self, request):
|
def reset_billing(self, request):
|
||||||
self.handle_reset_billing()
|
set_dynamic_setting(BILLING_ENABLED, False)
|
||||||
|
|
||||||
|
BillingProject.objects.all().delete()
|
||||||
|
for name, handler in component.INVOICE_HANDLER.items():
|
||||||
|
handler.delete()
|
||||||
|
|
||||||
|
for name, model in component.PRICE_MODEL.items():
|
||||||
|
model.objects.all().delete()
|
||||||
return Response({"status": "success"})
|
return Response({"status": "success"})
|
||||||
|
|
||||||
|
@action(detail=False, methods=["POST"])
|
||||||
|
@transaction.atomic
|
||||||
|
def reset_transaction_data(self, request):
|
||||||
|
set_dynamic_setting(BILLING_ENABLED, False)
|
||||||
|
|
||||||
|
BillingProject.objects.all().delete()
|
||||||
|
for name, handler in component.INVOICE_HANDLER.items():
|
||||||
|
handler.delete()
|
||||||
|
|
||||||
|
return Response({"status": "success"})
|
||||||
|
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def handle_init_billing(self, data):
|
def handle_init_billing(self, data):
|
||||||
set_dynamic_setting(BILLING_ENABLED, True)
|
set_dynamic_setting(BILLING_ENABLED, True)
|
||||||
|
@ -260,11 +281,11 @@ class InvoiceViewSet(viewsets.ModelViewSet):
|
||||||
projects = {}
|
projects = {}
|
||||||
invoices = {}
|
invoices = {}
|
||||||
|
|
||||||
date_today = timezone.now()
|
date_today = current_localtime()
|
||||||
month_first_day = date_today.replace(
|
month_first_day = date_today.replace(
|
||||||
day=1, hour=0, minute=0, second=0, microsecond=0
|
day=1, hour=0, minute=0, second=0, microsecond=0
|
||||||
)
|
)
|
||||||
|
|
||||||
for name, handler in component.INVOICE_HANDLER.items():
|
for name, handler in component.INVOICE_HANDLER.items():
|
||||||
payloads = data[name]
|
payloads = data[name]
|
||||||
|
|
||||||
|
@ -294,16 +315,6 @@ class InvoiceViewSet(viewsets.ModelViewSet):
|
||||||
del payload["tenant_id"]
|
del payload["tenant_id"]
|
||||||
handler.create(payload, fallback_price=True)
|
handler.create(payload, fallback_price=True)
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def handle_reset_billing(self):
|
|
||||||
set_dynamic_setting(BILLING_ENABLED, False)
|
|
||||||
|
|
||||||
BillingProject.objects.all().delete()
|
|
||||||
for name, handler in component.INVOICE_HANDLER.items():
|
|
||||||
handler.delete()
|
|
||||||
|
|
||||||
for name, model in component.PRICE_MODEL.items():
|
|
||||||
model.objects.all().delete()
|
|
||||||
|
|
||||||
@action(detail=True)
|
@action(detail=True)
|
||||||
def finish(self, request, pk):
|
def finish(self, request, pk):
|
||||||
|
|
6
bin/check_openstack_connection.sh
Executable file
6
bin/check_openstack_connection.sh
Executable file
|
@ -0,0 +1,6 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||||
|
cd $SCRIPT_DIR || exit
|
||||||
|
cd ..
|
||||||
|
./env/bin/python manage.py check_openstack_connection
|
6
bin/handle_unpaid_invoice.sh
Executable file
6
bin/handle_unpaid_invoice.sh
Executable file
|
@ -0,0 +1,6 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||||
|
cd $SCRIPT_DIR || exit
|
||||||
|
cd ..
|
||||||
|
./env/bin/python manage.py handle_unpaid_invoice
|
23
bin/setup_cron.sh
Executable file
23
bin/setup_cron.sh
Executable file
|
@ -0,0 +1,23 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Get the directory of the current script
|
||||||
|
current_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
# Define the cron jobs
|
||||||
|
cron_job1="1 0 1 * * $current_dir/process_invoice.sh"
|
||||||
|
cron_job2="5 0 * * * $current_dir/handle_unpaid_invoice.sh"
|
||||||
|
|
||||||
|
# Define the identifiers
|
||||||
|
identifier1="process_invoice"
|
||||||
|
identifier2="handle_unpaid_invoice"
|
||||||
|
|
||||||
|
# Check if the cron jobs already exist
|
||||||
|
if ! crontab -l | grep -q "$identifier1"; then
|
||||||
|
# Add the cron job if it doesn't exist
|
||||||
|
(crontab -l; echo "$cron_job1") | crontab -
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! crontab -l | grep -q "$identifier2"; then
|
||||||
|
# Add the cron job if it doesn't exist
|
||||||
|
(crontab -l; echo "$cron_job2") | crontab -
|
||||||
|
fi
|
52
bin/yuyu_test.sh
Executable file
52
bin/yuyu_test.sh
Executable file
|
@ -0,0 +1,52 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
case $1 in
|
||||||
|
current_time)
|
||||||
|
date +"Current Time: %Y-%m-%d %H:%M:%S %Z"
|
||||||
|
;;
|
||||||
|
|
||||||
|
end_of_month)
|
||||||
|
current_month=$(date +%m)
|
||||||
|
current_year=$(date +%Y)
|
||||||
|
last_day=$(cal $current_month $current_year | awk 'NF {DAYS = $NF} END {print DAYS}')
|
||||||
|
end_of_month=$(date -d "$current_year-$current_month-$last_day 23:59:00" +"%Y-%m-%d %H:%M:%S %Z")
|
||||||
|
sudo systemctl stop systemd-timesyncd
|
||||||
|
sudo date -s "$end_of_month"
|
||||||
|
date +"Time set to end of the month: %Y-%m-%d %H:%M:%S %Z"
|
||||||
|
echo "Restarting cron"
|
||||||
|
sudo systemctl restart cron
|
||||||
|
;;
|
||||||
|
|
||||||
|
end_of_day)
|
||||||
|
days_offset=$2
|
||||||
|
end_of_day=$(date -d "+$days_offset days 23:59:00" +"%Y-%m-%d %H:%M:%S %Z")
|
||||||
|
sudo systemctl stop systemd-timesyncd
|
||||||
|
sudo date -s "$end_of_day"
|
||||||
|
date +"Time set to end of the day: %Y-%m-%d %H:%M:%S %Z"
|
||||||
|
echo "Restarting cron"
|
||||||
|
sudo systemctl restart cron
|
||||||
|
;;
|
||||||
|
|
||||||
|
add_minutes)
|
||||||
|
minutes_offset=$2
|
||||||
|
end_of_day=$(date -d "+$minutes_offset minutes" +"%Y-%m-%d %H:%M:%S %Z")
|
||||||
|
sudo systemctl stop systemd-timesyncd
|
||||||
|
sudo date -s "$end_of_day"
|
||||||
|
date +"Time set to: %Y-%m-%d %H:%M:%S %Z"
|
||||||
|
echo "Restarting cron"
|
||||||
|
sudo systemctl restart cron
|
||||||
|
;;
|
||||||
|
|
||||||
|
reset_time)
|
||||||
|
sudo systemctl stop systemd-timesyncd
|
||||||
|
sudo systemctl start systemd-timesyncd
|
||||||
|
date +"Time reset to NTP"
|
||||||
|
echo "Restarting cron"
|
||||||
|
sudo systemctl restart cron
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
echo "Usage: $0 {current_time|end_of_month|end_of_day <days_offset>|reset_time}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
|
@ -5,6 +5,7 @@ from django.utils import timezone
|
||||||
|
|
||||||
from core.models import Invoice, BillingProject
|
from core.models import Invoice, BillingProject
|
||||||
from core.component.base.invoice_handler import InvoiceHandler
|
from core.component.base.invoice_handler import InvoiceHandler
|
||||||
|
from core.utils.date_utils import current_localtime
|
||||||
|
|
||||||
|
|
||||||
class EventHandler(metaclass=abc.ABCMeta):
|
class EventHandler(metaclass=abc.ABCMeta):
|
||||||
|
@ -22,7 +23,7 @@ class EventHandler(metaclass=abc.ABCMeta):
|
||||||
invoice = Invoice.objects.filter(project__tenant_id=tenant_id, state=Invoice.InvoiceState.IN_PROGRESS).first()
|
invoice = Invoice.objects.filter(project__tenant_id=tenant_id, state=Invoice.InvoiceState.IN_PROGRESS).first()
|
||||||
if not invoice:
|
if not invoice:
|
||||||
project = BillingProject.objects.get_or_create(tenant_id=tenant_id)
|
project = BillingProject.objects.get_or_create(tenant_id=tenant_id)
|
||||||
date_today = timezone.now()
|
date_today = current_localtime()
|
||||||
month_first_day = date_today.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
month_first_day = date_today.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||||
invoice = Invoice.objects.create(
|
invoice = Invoice.objects.create(
|
||||||
project=project,
|
project=project,
|
||||||
|
@ -68,7 +69,7 @@ class EventHandler(metaclass=abc.ABCMeta):
|
||||||
instance = self.invoice_handler.get_active_instance(invoice, payload)
|
instance = self.invoice_handler.get_active_instance(invoice, payload)
|
||||||
if not instance:
|
if not instance:
|
||||||
payload['invoice'] = invoice
|
payload['invoice'] = invoice
|
||||||
payload['start_date'] = timezone.now()
|
payload['start_date'] = current_localtime()
|
||||||
|
|
||||||
self.invoice_handler.create(payload, fallback_price=True)
|
self.invoice_handler.create(payload, fallback_price=True)
|
||||||
|
|
||||||
|
@ -113,7 +114,7 @@ class EventHandler(metaclass=abc.ABCMeta):
|
||||||
|
|
||||||
if instance:
|
if instance:
|
||||||
if self.invoice_handler.is_price_dependency_changed(instance, payload):
|
if self.invoice_handler.is_price_dependency_changed(instance, payload):
|
||||||
self.invoice_handler.roll(instance, close_date=timezone.now(), update_payload=payload, fallback_price=True)
|
self.invoice_handler.roll(instance, close_date=current_localtime(), update_payload=payload, fallback_price=True)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if self.invoice_handler.is_informative_changed(instance, payload):
|
if self.invoice_handler.is_informative_changed(instance, payload):
|
||||||
|
|
|
@ -7,6 +7,7 @@ from djmoney.money import Money
|
||||||
from core.exception import PriceNotFound
|
from core.exception import PriceNotFound
|
||||||
from core.models import InvoiceComponentMixin, PriceMixin
|
from core.models import InvoiceComponentMixin, PriceMixin
|
||||||
from core.notification import send_notification
|
from core.notification import send_notification
|
||||||
|
from core.utils.date_utils import current_localtime
|
||||||
|
|
||||||
|
|
||||||
class InvoiceHandler(metaclass=abc.ABCMeta):
|
class InvoiceHandler(metaclass=abc.ABCMeta):
|
||||||
|
@ -130,7 +131,7 @@ class InvoiceHandler(metaclass=abc.ABCMeta):
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
self.update(instance, payload, save=False)
|
self.update(instance, payload, save=False)
|
||||||
instance.close(timezone.now()) # Close will also save the instance
|
instance.close(current_localtime()) # Close will also save the instance
|
||||||
|
|
||||||
def is_informative_changed(self, instance, payload):
|
def is_informative_changed(self, instance, payload):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -3,6 +3,8 @@ import logging
|
||||||
from core.models import BillingProject, Invoice
|
from core.models import BillingProject, Invoice
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from core.utils.date_utils import current_localtime
|
||||||
|
|
||||||
LOG = logging.getLogger("yuyu_notification")
|
LOG = logging.getLogger("yuyu_notification")
|
||||||
|
|
||||||
|
|
||||||
|
@ -18,7 +20,7 @@ class ProjectEventHandler:
|
||||||
self.init_first_invoice(project)
|
self.init_first_invoice(project)
|
||||||
|
|
||||||
def init_first_invoice(self, project):
|
def init_first_invoice(self, project):
|
||||||
date_today = timezone.now()
|
date_today = current_localtime()
|
||||||
month_first_day = date_today.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
month_first_day = date_today.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||||
Invoice.objects.create(
|
Invoice.objects.create(
|
||||||
project=project,
|
project=project,
|
||||||
|
|
|
@ -5,6 +5,7 @@ from oslo_messaging import NotificationResult
|
||||||
|
|
||||||
from core.component import component
|
from core.component import component
|
||||||
from core.component.project.event_handler import ProjectEventHandler
|
from core.component.project.event_handler import ProjectEventHandler
|
||||||
|
from core.feature.unpaid_invoice_handle.event_handler import UnpaidInvoiceEventHandler
|
||||||
from core.notification import send_notification
|
from core.notification import send_notification
|
||||||
from core.utils.dynamic_setting import get_dynamic_settings, BILLING_ENABLED, get_dynamic_setting
|
from core.utils.dynamic_setting import get_dynamic_settings, BILLING_ENABLED, get_dynamic_setting
|
||||||
from yuyu import settings
|
from yuyu import settings
|
||||||
|
@ -20,6 +21,7 @@ class EventEndpoint(object):
|
||||||
|
|
||||||
# Add handler for project event
|
# Add handler for project event
|
||||||
self.event_handler.append(ProjectEventHandler())
|
self.event_handler.append(ProjectEventHandler())
|
||||||
|
self.event_handler.append(UnpaidInvoiceEventHandler())
|
||||||
|
|
||||||
def info(self, ctxt, publisher_id, event_type, payload, metadata):
|
def info(self, ctxt, publisher_id, event_type, payload, metadata):
|
||||||
LOG.info("=== Event Received ===")
|
LOG.info("=== Event Received ===")
|
||||||
|
|
9
core/feature/unpaid_invoice_handle/actions.py
Normal file
9
core/feature/unpaid_invoice_handle/actions.py
Normal file
|
@ -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"
|
146
core/feature/unpaid_invoice_handle/command.py
Normal file
146
core/feature/unpaid_invoice_handle/command.py
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
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.date_utils import current_localtime
|
||||||
|
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
|
||||||
|
|
||||||
|
if not hasattr(settings, 'UNPAID_INVOICE_HANDLER_CONFIG'):
|
||||||
|
LOG.exception("Missing UNPAID_INVOICE_HANDLER_CONFIG in settings")
|
||||||
|
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 = current_localtime() - invoice.end_date
|
||||||
|
past_day = date_diff.days
|
||||||
|
LOG.info(f"Processing Unpaid Invoice {invoice.id} with past day {past_day}")
|
||||||
|
for config in schedule_config:
|
||||||
|
if config['day'] == past_day:
|
||||||
|
LOG.info('Running action')
|
||||||
|
self.run_action(invoice, config['action'], config)
|
||||||
|
|
||||||
|
def run_action(self, invoice, action, config=None):
|
||||||
|
LOG.info(f"Running action {action} with config {config}")
|
||||||
|
try:
|
||||||
|
if action == UnpainInvoiceAction.SEND_MESSAGE.value:
|
||||||
|
self._send_message(invoice, config)
|
||||||
|
|
||||||
|
if action == UnpainInvoiceAction.STOP_INSTANCE.value:
|
||||||
|
self._stop_component(invoice)
|
||||||
|
|
||||||
|
if action == UnpainInvoiceAction.SUSPEND_INSTANCE.value:
|
||||||
|
self._stop_component(invoice)
|
||||||
|
|
||||||
|
if action == UnpainInvoiceAction.PAUSE_INSTANCE.value:
|
||||||
|
self._stop_component(invoice)
|
||||||
|
|
||||||
|
if action == UnpainInvoiceAction.DELETE_INSTANCE.value:
|
||||||
|
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(all_projects=True, project_id=invoice.project.tenant_id):
|
||||||
|
LOG.info(f"Stopping server {server.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(all_projects=True, project_id=invoice.project.tenant_id):
|
||||||
|
LOG.info(f"Suspending server {server.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(all_projects=True, project_id=invoice.project.tenant_id):
|
||||||
|
LOG.info(f"Pausing server {server.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(all_projects=True, project_id=invoice.project.tenant_id):
|
||||||
|
LOG.info(f"Deleting server {server.id}")
|
||||||
|
|
||||||
|
conn.compute.delete_server(server)
|
||||||
|
|
||||||
|
# Delete Image
|
||||||
|
for image in conn.compute.images(all_projects=True, project_id=invoice.project.tenant_id):
|
||||||
|
LOG.info(f"Deleting image {image.id}")
|
||||||
|
conn.compute.delete_image(image)
|
||||||
|
|
||||||
|
# Delete Floating Ips
|
||||||
|
for ip in conn.network.ips(all_projects=True, project_id=invoice.project.tenant_id):
|
||||||
|
LOG.info(f"Deleting ip {ip.id}")
|
||||||
|
|
||||||
|
conn.network.delete_ip(ip)
|
||||||
|
|
||||||
|
# Delete Router
|
||||||
|
for route in conn.network.routers(all_projects=True, project_id=invoice.project.tenant_id):
|
||||||
|
LOG.info(f"Deleting route {route.id}")
|
||||||
|
|
||||||
|
conn.network.delete_router(route)
|
||||||
|
|
||||||
|
# Delete Volume
|
||||||
|
for volume in conn.block_storage.volumes(all_projects=True, project_id=invoice.project.tenant_id):
|
||||||
|
LOG.info(f"Deleting volume {volume.id}")
|
||||||
|
|
||||||
|
conn.block_storage.delete_volume(volume)
|
||||||
|
|
||||||
|
# Delete Snapshot
|
||||||
|
for snapshot in conn.block_storage.snapshots(all_projects=True, project_id=invoice.project.tenant_id):
|
||||||
|
LOG.info(f"Deleting snapshot {snapshot.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'],
|
||||||
|
)
|
70
core/feature/unpaid_invoice_handle/event_handler.py
Normal file
70
core/feature/unpaid_invoice_handle/event_handler.py
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
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 core.utils.date_utils import current_localtime
|
||||||
|
from yuyu import settings
|
||||||
|
|
||||||
|
LOG = logging.getLogger("yuyu")
|
||||||
|
|
||||||
|
class UnpaidInvoiceEventHandler:
|
||||||
|
filter_action = [
|
||||||
|
UnpainInvoiceAction.STOP_INSTANCE.value,
|
||||||
|
UnpainInvoiceAction.SUSPEND_INSTANCE.value,
|
||||||
|
UnpainInvoiceAction.PAUSE_INSTANCE.value,
|
||||||
|
UnpainInvoiceAction.DELETE_INSTANCE.value,
|
||||||
|
]
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
if not hasattr(settings, 'UNPAID_INVOICE_HANDLER_CONFIG'):
|
||||||
|
LOG.exception("Missing UNPAID_INVOICE_HANDLER_CONFIG in settings")
|
||||||
|
return
|
||||||
|
|
||||||
|
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 = current_localtime() - 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")
|
20
core/management/commands/check_openstack_connection.py
Normal file
20
core/management/commands/check_openstack_connection.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import openstack
|
||||||
|
from django.core.management import BaseCommand
|
||||||
|
|
||||||
|
from yuyu import settings
|
||||||
|
|
||||||
|
LOG = logging.getLogger("check_openstack_connection")
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Yuyu Check Openstack Connection'
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
print("Checking Openstack Connection")
|
||||||
|
print("Will try to list instance on the project that configured in `clouds.yaml`")
|
||||||
|
|
||||||
|
conn = openstack.connect(cloud=settings.CLOUD_CONFIG_NAME)
|
||||||
|
for server in conn.compute.servers(all_projects=True):
|
||||||
|
print(server.name)
|
21
core/management/commands/handle_unpaid_invoice.py
Normal file
21
core/management/commands/handle_unpaid_invoice.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import logging
|
||||||
|
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")
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
unpaid_invoice_command = UnpaidInvoiceHandlerCommand()
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
self.unpaid_invoice_command.handle()
|
|
@ -8,6 +8,7 @@ from django.utils import timezone
|
||||||
from core.component import component, labels
|
from core.component import component, labels
|
||||||
from core.models import Invoice, InvoiceComponentMixin, Balance
|
from core.models import Invoice, InvoiceComponentMixin, Balance
|
||||||
from core.notification import send_notification_from_template, send_notification
|
from core.notification import send_notification_from_template, send_notification
|
||||||
|
from core.utils.date_utils import current_localtime
|
||||||
from core.utils.dynamic_setting import get_dynamic_setting, BILLING_ENABLED, INVOICE_TAX, COMPANY_NAME, \
|
from core.utils.dynamic_setting import get_dynamic_setting, BILLING_ENABLED, INVOICE_TAX, COMPANY_NAME, \
|
||||||
COMPANY_ADDRESS, INVOICE_AUTO_DEDUCT_BALANCE
|
COMPANY_ADDRESS, INVOICE_AUTO_DEDUCT_BALANCE
|
||||||
from yuyu import settings
|
from yuyu import settings
|
||||||
|
@ -25,8 +26,11 @@ class Command(BaseCommand):
|
||||||
LOG.info("Billing not activated")
|
LOG.info("Billing not activated")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.close_date = timezone.now()
|
self.close_date = current_localtime()
|
||||||
|
LOG.info(f"Close Date: {self.close_date}")
|
||||||
|
|
||||||
self.tax_pertentage = get_dynamic_setting(INVOICE_TAX)
|
self.tax_pertentage = get_dynamic_setting(INVOICE_TAX)
|
||||||
|
LOG.info(f"Tax Percentage: {self.tax_pertentage}")
|
||||||
|
|
||||||
active_invoices = Invoice.objects.filter(state=Invoice.InvoiceState.IN_PROGRESS).all()
|
active_invoices = Invoice.objects.filter(state=Invoice.InvoiceState.IN_PROGRESS).all()
|
||||||
for active_invoice in active_invoices:
|
for active_invoice in active_invoices:
|
||||||
|
|
|
@ -13,6 +13,7 @@ from djmoney.money import Money
|
||||||
from core.component import labels
|
from core.component import labels
|
||||||
from core.component.labels import LABEL_INSTANCES, LABEL_IMAGES, LABEL_SNAPSHOTS, LABEL_ROUTERS, LABEL_FLOATING_IPS, \
|
from core.component.labels import LABEL_INSTANCES, LABEL_IMAGES, LABEL_SNAPSHOTS, LABEL_ROUTERS, LABEL_FLOATING_IPS, \
|
||||||
LABEL_VOLUMES
|
LABEL_VOLUMES
|
||||||
|
from core.utils.date_utils import DateTimeLocalField, current_localtime
|
||||||
from core.utils.model_utils import BaseModel, TimestampMixin, PriceMixin, InvoiceComponentMixin
|
from core.utils.model_utils import BaseModel, TimestampMixin, PriceMixin, InvoiceComponentMixin
|
||||||
|
|
||||||
LOG = logging.getLogger("yuyu")
|
LOG = logging.getLogger("yuyu")
|
||||||
|
@ -80,9 +81,9 @@ class Invoice(BaseModel, TimestampMixin):
|
||||||
FINISHED = 100
|
FINISHED = 100
|
||||||
|
|
||||||
project = models.ForeignKey('BillingProject', on_delete=models.CASCADE)
|
project = models.ForeignKey('BillingProject', on_delete=models.CASCADE)
|
||||||
start_date = models.DateTimeField()
|
start_date = DateTimeLocalField()
|
||||||
end_date = models.DateTimeField(default=None, blank=True, null=True)
|
end_date = DateTimeLocalField(default=None, blank=True, null=True)
|
||||||
finish_date = models.DateTimeField(default=None, blank=True, null=True)
|
finish_date = DateTimeLocalField(default=None, blank=True, null=True)
|
||||||
state = models.IntegerField(choices=InvoiceState.choices)
|
state = models.IntegerField(choices=InvoiceState.choices)
|
||||||
tax = MoneyField(max_digits=256, default=None, blank=True, null=True)
|
tax = MoneyField(max_digits=256, default=None, blank=True, null=True)
|
||||||
total = MoneyField(max_digits=256, default=None, blank=True, null=True)
|
total = MoneyField(max_digits=256, default=None, blank=True, null=True)
|
||||||
|
@ -127,12 +128,11 @@ class Invoice(BaseModel, TimestampMixin):
|
||||||
self.end_date = date
|
self.end_date = date
|
||||||
self.tax = tax_percentage * self.subtotal / 100
|
self.tax = tax_percentage * self.subtotal / 100
|
||||||
self.total = self.tax + self.subtotal
|
self.total = self.tax + self.subtotal
|
||||||
# TODO: Deduct balance
|
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def finish(self):
|
def finish(self):
|
||||||
self.state = Invoice.InvoiceState.FINISHED
|
self.state = Invoice.InvoiceState.FINISHED
|
||||||
self.finish_date = timezone.now()
|
self.finish_date = current_localtime()
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def rollback_to_unpaid(self):
|
def rollback_to_unpaid(self):
|
||||||
|
|
15
core/utils/date_utils.py
Normal file
15
core/utils/date_utils.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
from typing import Any
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
def current_localtime():
|
||||||
|
return timezone.localtime(timezone.now())
|
||||||
|
|
||||||
|
# A DateTimeField that return local time instead of UTC to make it easier to calculate duration
|
||||||
|
class DateTimeLocalField(models.DateTimeField):
|
||||||
|
def from_db_value(self, value, expression, connection) -> Any:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return timezone.localtime(value)
|
||||||
|
|
|
@ -5,6 +5,8 @@ from django.utils import timezone
|
||||||
from djmoney.models.fields import MoneyField
|
from djmoney.models.fields import MoneyField
|
||||||
from django.utils.timesince import timesince
|
from django.utils.timesince import timesince
|
||||||
|
|
||||||
|
from core.utils.date_utils import DateTimeLocalField, current_localtime
|
||||||
|
|
||||||
|
|
||||||
class BaseModel(models.Model):
|
class BaseModel(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -31,13 +33,13 @@ class InvoiceComponentMixin(TimestampMixin, PriceMixin):
|
||||||
"""
|
"""
|
||||||
Storing start time for price calculation
|
Storing start time for price calculation
|
||||||
"""
|
"""
|
||||||
start_date = models.DateTimeField()
|
start_date = DateTimeLocalField()
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Storing end time of the component, when component still active it will be None.
|
Storing end time of the component, when component still active it will be None.
|
||||||
It will be set when component is closed or rolled
|
It will be set when component is closed or rolled
|
||||||
"""
|
"""
|
||||||
end_date = models.DateTimeField(default=None, blank=True, null=True)
|
end_date = DateTimeLocalField(default=None, blank=True, null=True)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def adjusted_end_date(self):
|
def adjusted_end_date(self):
|
||||||
|
@ -46,7 +48,7 @@ class InvoiceComponentMixin(TimestampMixin, PriceMixin):
|
||||||
Basically it just return current time if end_date is None
|
Basically it just return current time if end_date is None
|
||||||
end_date will be set when invoice is finished every end of the month or when invoice component is rolled
|
end_date will be set when invoice is finished every end of the month or when invoice component is rolled
|
||||||
"""
|
"""
|
||||||
current_date = timezone.now()
|
current_date = current_localtime()
|
||||||
if self.end_date:
|
if self.end_date:
|
||||||
end_date = self.end_date
|
end_date = self.end_date
|
||||||
else:
|
else:
|
||||||
|
@ -84,7 +86,7 @@ class InvoiceComponentMixin(TimestampMixin, PriceMixin):
|
||||||
|
|
||||||
def close(self, date):
|
def close(self, date):
|
||||||
"""
|
"""
|
||||||
Close component the component
|
Close the component
|
||||||
"""
|
"""
|
||||||
self.end_date = date
|
self.end_date = date
|
||||||
self.save()
|
self.save()
|
||||||
|
|
|
@ -4,7 +4,7 @@ pytz==2021.1
|
||||||
sqlparse==0.4.1
|
sqlparse==0.4.1
|
||||||
oslo.messaging>=12.8.0
|
oslo.messaging>=12.8.0
|
||||||
django-money==2.0.1
|
django-money==2.0.1
|
||||||
openstacksdk==0.58.0
|
openstacksdk==2.0.0
|
||||||
djangorestframework==3.12.4
|
djangorestframework==3.12.4
|
||||||
django-filter==2.4.0
|
django-filter==2.4.0
|
||||||
Markdown==3.3.4
|
Markdown==3.3.4
|
||||||
|
|
|
@ -15,4 +15,69 @@ EMAIL_HOST = 'smtp.gmail.com'
|
||||||
EMAIL_HOST_USER = '<paste your gmail account here>'
|
EMAIL_HOST_USER = '<paste your gmail account here>'
|
||||||
EMAIL_HOST_PASSWORD = '<paste Google password or app password here>'
|
EMAIL_HOST_PASSWORD = '<paste Google password or app password here>'
|
||||||
EMAIL_PORT = 587
|
EMAIL_PORT = 587
|
||||||
EMAIL_USE_TLS = True
|
EMAIL_USE_TLS = True
|
||||||
|
|
||||||
|
"""
|
||||||
|
Unpaid Invoice Handler Configuration
|
||||||
|
|
||||||
|
CLOUD_CONFIG_NAME : Is configuration in clouds.yaml that you want to use to connect to openstack.
|
||||||
|
Example: Your clouds.yaml is
|
||||||
|
clouds:
|
||||||
|
openstack:
|
||||||
|
auth:
|
||||||
|
auth_url: http://172.10.10.150:5000
|
||||||
|
username: "admin"
|
||||||
|
password: "your_password"
|
||||||
|
project_id: 0000000000000000000000000
|
||||||
|
project_name: "admin"
|
||||||
|
user_domain_name: "Default"
|
||||||
|
region_name: "ID"
|
||||||
|
interface: "public"
|
||||||
|
identity_api_version: 3
|
||||||
|
|
||||||
|
You can put `openstack` as CLOUD_CONFIG_NAME
|
||||||
|
|
||||||
|
UNPAID_INVOICE_HANDLER_CONFIG : Is configuration for an action that will be run on a particular day after invoice is issued and still unpaid
|
||||||
|
The format is a list of dictionary that follow
|
||||||
|
{
|
||||||
|
"day": 0, # The day of the action that will be run, example if you put 5 it will run in 5 day after invoice is issued and still unpaid
|
||||||
|
"action": "send_message/stop_instance/delete_instance", # The action that you want to run
|
||||||
|
"message_title": "Title", # The title or subject of the message if you use `send_message` action
|
||||||
|
"message_short_description": "Short Description", # The short description of message if you use `send_message` action
|
||||||
|
"message_content": "Title", # The content of the message if you use `send_message` action
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
#
|
||||||
|
# CLOUD_CONFIG_NAME = "openstack"
|
||||||
|
# UNPAID_INVOICE_HANDLER_CONFIG = [
|
||||||
|
# {
|
||||||
|
# "day": 5,
|
||||||
|
# "action": "send_message",
|
||||||
|
# "message_title": "Your invoice has been expired. Please pay now!",
|
||||||
|
# "message_short_description": "Your invoice has been expired. Please pay now!",
|
||||||
|
# "message_content": "Your invoice has been expired. Please pay now!",
|
||||||
|
# },
|
||||||
|
# {
|
||||||
|
# "day": 10,
|
||||||
|
# "action": "stop_instance",
|
||||||
|
# },
|
||||||
|
# {
|
||||||
|
# "day": 10,
|
||||||
|
# "action": "send_message",
|
||||||
|
# "message_title": "Your compute instance will be stopped",
|
||||||
|
# "message_short_description": "Your compute instance will be stopped",
|
||||||
|
# "message_content": "Your compute instance will be stopped because you have unpaid invoice",
|
||||||
|
# },
|
||||||
|
# {
|
||||||
|
# "day": 15,
|
||||||
|
# "action": "send_message",
|
||||||
|
# "message_title": "All of your instance has been deleted",
|
||||||
|
# "message_short_description": "All of your instance has been deleted",
|
||||||
|
# "message_content": "All of your instance has been deleted because you have unpaid invoice",
|
||||||
|
# },
|
||||||
|
# {
|
||||||
|
# "day": 15,
|
||||||
|
# "action": "delete_instance",
|
||||||
|
# },
|
||||||
|
# ]
|
||||||
|
|
Loading…
Add table
Reference in a new issue