initial commit

This commit is contained in:
Setyo Nugroho 2022-05-13 14:01:58 +07:00
commit 8fbcba3976
97 changed files with 3570 additions and 0 deletions

31
.gitignore vendored Normal file
View file

@ -0,0 +1,31 @@
.DS_Store
*/CACHE/*
static
build/*
*.pyc
*.pyo
*.so
*~
*.db
local_settings.py
*.jpg.*
*.tmproj
*.sqlite
media/*
env/*
logs/*
*.sqlite3
projects/logs/*
# IDE Setting
.vscode/*
.idea/*
deploy_log/*
**.ipynb
.vscode
.vim
.ipynb_checkpoints
hosts

31
README.md Normal file
View file

@ -0,0 +1,31 @@
# Yuyu
## Setup
Make sure you know where Horizon is located
Run this command
```bash
./setup_yuyu.sh
```
Enter horizon location and press ENTER.
Activate Horzon Virtual Environment if exist.
Install Yuyu Dashboard Depencencies with
```
pip3 install -r requirements.txt
```
Add this config to your horizon `local_settings.py`
```bash
YUYU_URL="http://yuyu_server_url:8182"
CURRENCIES = ('IDR', 'USD')
DEFAULT_CURRENCY = "IDR"
```
Then restart Horizon.

21
remove_yuyu.sh Executable file
View file

@ -0,0 +1,21 @@
#!/bin/bash
echo "Specify Horizon Location (ex: /etc/horizon): "
read horizon_path
root=`pwd -P`
echo "Removing yuyu Symlink"
rm $horizon_path/openstack_dashboard/dashboards/yuyu
rm $horizon_path/openstack_dashboard/local/enabled/_6100_yuyu.py
rm $horizon_path/openstack_dashboard/local/enabled/_6101_admin_billing_panel_group.py
rm $horizon_path/openstack_dashboard/local/enabled/_6102_admin_billing_overview.py
rm $horizon_path/openstack_dashboard/local/enabled/_6103_admin_billing_price_configuration.py
rm $horizon_path/openstack_dashboard/local/enabled/_6104_admin_billing_setting.py
rm $horizon_path/openstack_dashboard/local/enabled/_6104_admin_billing_setting.py
rm $horizon_path/openstack_dashboard/local/enabled/_6105_admin_billing_projects_invoice.py
rm $horizon_path/openstack_dashboard/local/enabled/_6111_project_billing_panel_group.py
rm $horizon_path/openstack_dashboard/local/enabled/_6112_project_billing_overview.py
rm $horizon_path/openstack_dashboard/local/enabled/_6113_project_billing_usage_cost.py
rm $horizon_path/openstack_dashboard/local/enabled/_6114_project_billing_invoice.py
echo "Yuyu Removal Done"

2
requirements.txt Normal file
View file

@ -0,0 +1,2 @@
django-money==2.0.1
python-dateutil==2.8.2

23
setup_yuyu.sh Executable file
View file

@ -0,0 +1,23 @@
#!/bin/bash
echo "Specify Horizon Location (ex: /etc/horizon): "
read horizon_path
root=`pwd -P`
echo "Creating Symlink"
ln -sf $root/yuyu $horizon_path/openstack_dashboard/dashboards
ln -sf $root/yuyu/local/enabled/_6100_yuyu.py $horizon_path/openstack_dashboard/local/enabled/_6100_yuyu.py
ln -sf $root/yuyu/local/enabled/_6101_admin_billing_panel_group.py $horizon_path/openstack_dashboard/local/enabled/_6101_admin_billing_panel_group.py
ln -sf $root/yuyu/local/enabled/_6102_admin_billing_overview.py $horizon_path/openstack_dashboard/local/enabled/_6102_admin_billing_overview.py
ln -sf $root/yuyu/local/enabled/_6103_admin_billing_price_configuration.py $horizon_path/openstack_dashboard/local/enabled/_6103_admin_billing_price_configuration.py
ln -sf $root/yuyu/local/enabled/_6104_admin_billing_setting.py $horizon_path/openstack_dashboard/local/enabled/_6104_admin_billing_setting.py
ln -sf $root/yuyu/local/enabled/_6104_admin_billing_setting.py $horizon_path/openstack_dashboard/local/enabled/_6104_admin_billing_setting.py
ln -sf $root/yuyu/local/enabled/_6105_admin_billing_projects_invoice.py $horizon_path/openstack_dashboard/local/enabled/_6105_admin_billing_projects_invoice.py
ln -sf $root/yuyu/local/enabled/_6111_project_billing_panel_group.py $horizon_path/openstack_dashboard/local/enabled/_6111_project_billing_panel_group.py
ln -sf $root/yuyu/local/enabled/_6112_project_billing_overview.py $horizon_path/openstack_dashboard/local/enabled/_6112_project_billing_overview.py
ln -sf $root/yuyu/local/enabled/_6113_project_billing_usage_cost.py $horizon_path/openstack_dashboard/local/enabled/_6113_project_billing_usage_cost.py
ln -sf $root/yuyu/local/enabled/_6114_project_billing_invoice.py $horizon_path/openstack_dashboard/local/enabled/_6114_project_billing_invoice.py
echo "Symlink Creation Done"
echo "Now you can configure and yuyu dashboard"

0
yuyu/__init__.py Normal file
View file

View file

View file

@ -0,0 +1,20 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.utils.translation import ugettext_lazy as _
import horizon
class BillingOverview(horizon.Panel):
name = _("Billing Overview")
slug = "billing_overview"

View file

@ -0,0 +1,132 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Billing Setting" %}{% endblock %}
{% block main %}
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">This Month Total Resource Allocation</h3>
</div>
<div class="panel-body">
<canvas id="totalResourceChart" height="300"></canvas>
</div>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Active Resource Allocation</h3>
</div>
<div class="panel-body">
<canvas id="activeResourceChart" height="300"></canvas>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">This Month All Resource Price Charged</h3>
</div>
<div class="panel-body">
<canvas id="totalResourcePriceChart" height="300"></canvas>
</div>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Active Resource Price Charged</h3>
</div>
<div class="panel-body">
<canvas id="activeResourcePriceChart" height="300"></canvas>
</div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.0/chart.min.js"
integrity="sha512-TW5s0IT/IppJtu76UbysrBH9Hy/5X41OTAbQuffZFU6lQ1rdcLHzpU5BzVvr/YFykoiMYZVWlr/PX1mDcfM9Qg=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script type="text/javascript">
function showBarChart(id, title, data) {
const ctx = document.getElementById(id).getContext('2d');
const myChart = new Chart(ctx, {
type: 'bar',
data: {
labels: data.label,
datasets: [{
label: title,
data: data.data,
backgroundColor: [
'rgba(255, 99, 132, 0.2)',
'rgba(54, 162, 235, 0.2)',
'rgba(255, 206, 86, 0.2)',
'rgba(75, 192, 192, 0.2)',
'rgba(153, 102, 255, 0.2)',
'rgba(255, 159, 64, 0.2)'
],
borderColor: [
'rgba(255, 99, 132, 1)',
'rgba(54, 162, 235, 1)',
'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)',
'rgba(153, 102, 255, 1)',
'rgba(255, 159, 64, 1)'
],
borderWidth: 1
}]
},
options: {
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
}
}
});
}
function showPieChart(id, title, data) {
const ctx = document.getElementById(id).getContext('2d');
const myChart = new Chart(ctx, {
type: 'pie',
data: {
labels: data.label,
datasets: [{
label: title,
data: data.data,
backgroundColor: [
'rgba(255, 99, 132, 0.2)',
'rgba(54, 162, 235, 0.2)',
'rgba(255, 206, 86, 0.2)',
'rgba(75, 192, 192, 0.2)',
'rgba(153, 102, 255, 0.2)',
'rgba(255, 159, 64, 0.2)'
],
borderColor: [
'rgba(255, 99, 132, 1)',
'rgba(54, 162, 235, 1)',
'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)',
'rgba(153, 102, 255, 1)',
'rgba(255, 159, 64, 1)'
],
borderWidth: 1
}]
},
options: {
maintainAspectRatio: false,
}
});
}
showBarChart('totalResourceChart', 'This Month Total Resource Allocation', JSON.parse('{{ total_resource_json | safe }}'));
showPieChart('activeResourceChart', 'Active Resource Allocation', JSON.parse('{{ active_resource_json | safe }}'));
showBarChart('totalResourcePriceChart', 'This Month All Resource Price Charged', JSON.parse('{{ price_total_resource_json | safe }}'));
showPieChart('activeResourcePriceChart', 'Active Resource Price Charged', JSON.parse('{{ price_active_resource_json | safe }}'));
</script>
{% endblock %}

View file

@ -0,0 +1,19 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
]

View file

@ -0,0 +1,34 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
from django import shortcuts
from django.utils.translation import ugettext_lazy as _
from horizon import views
from openstack_dashboard.dashboards.yuyu.cases.admin_overview_use_case import AdminOverviewUseCase
class IndexView(views.APIView):
page_title = _("Billing Overview")
template_name = "admin/billing_overview/index.html"
overview_uc = AdminOverviewUseCase()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['total_resource_json'] = json.dumps(self.overview_uc.total_resource(self.request))
context['active_resource_json'] = json.dumps(self.overview_uc.active_resource(self.request))
context['price_total_resource_json'] = json.dumps(self.overview_uc.price_total_resource(self.request))
context['price_active_resource_json'] = json.dumps(self.overview_uc.price_active_resource(self.request))
return context

View file

View file

@ -0,0 +1,20 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.utils.translation import ugettext_lazy as _
import horizon
class BillingSetting(horizon.Panel):
name = _("Billing Setting")
slug = "billing_setting"

View file

@ -0,0 +1,22 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Billing Setting" %}{% endblock %}
{% block main %}
{% include "admin/price_configuration/missing_prices.html" %}
<div class="row">
<div class="col-sm-12">
{% if setting.billing_enabled %}
<h1>Billing Enabled</h1> <br/>
<a href="{% url 'horizon:admin:billing_setting:disable_billing' %}" class="btn btn-primary">Disable Billing</a>
{% else %}
<h1>Billing Disabled</h1> <br/>
<p>Please make sure all price is already configured before enable billing</p>
<a href="{% url 'horizon:admin:billing_setting:enable_billing' %}" class="btn btn-primary {% if missing_price.has_missing %} disabled {% endif %}">Enable</a>
<a href="{% url 'horizon:admin:billing_setting:reset_billing' %}" class="btn btn-danger">Reset Billing Data</a>
{% endif %}
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,23 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^enable_billing$', views.EnableBillingView.as_view(), name='enable_billing'),
url(r'^disable_billing$', views.DisableBillingView.as_view(), name='disable_billing'),
url(r'^reset_billing$', views.ResetBillingView.as_view(), name='reset_billing'),
]

View file

@ -0,0 +1,68 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django import shortcuts
from django.utils.translation import ugettext_lazy as _
from horizon import views, exceptions, messages
from openstack_dashboard.dashboards.yuyu.cases.invoice_use_case import InvoiceUseCase
from openstack_dashboard.dashboards.yuyu.cases.setting_use_case import SettingUseCase
from openstack_dashboard.dashboards.yuyu.core.utils.price_checker import has_missing_price
class IndexView(views.APIView):
page_title = _("Setting")
template_name = "admin/billing_setting/index.html"
setting_uc = SettingUseCase()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['setting'] = self.setting_uc.get_settings(self.request)
context['missing_price'] = has_missing_price(self.request)
return context
class EnableBillingView(views.APIView):
invoice_uc = InvoiceUseCase()
def get(self, request, *args, **kwargs):
try:
self.invoice_uc.enable_billing(request)
except Exception:
exceptions.handle(self.request,
_("Unable to enable billing, Please check your price configuration"))
return shortcuts.redirect("horizon:admin:billing_setting:index")
class DisableBillingView(views.APIView):
invoice_uc = InvoiceUseCase()
def get(self, request, *args, **kwargs):
try:
self.invoice_uc.disable_billing(request)
except Exception:
exceptions.handle(self.request,
_("Unable to disable billing"))
return shortcuts.redirect("horizon:admin:billing_setting:index")
class ResetBillingView(views.APIView):
invoice_uc = InvoiceUseCase()
def get(self, request, *args, **kwargs):
try:
self.invoice_uc.reset_billing(request)
messages.success(request, _("Data successfully Reset."))
except Exception:
exceptions.handle(self.request,
_("Unable to reset billing"))
return shortcuts.redirect("horizon:admin:billing_setting:index")

View file

@ -0,0 +1,65 @@
from django.utils.translation import ugettext_lazy as _
from horizon import forms
from openstack_dashboard.dashboards.yuyu.cases.flavor_price_use_case import FlavorPriceUseCase
from openstack_dashboard.dashboards.yuyu.cases.floating_ip_price_use_case import FloatingIpPriceUseCase
from openstack_dashboard.dashboards.yuyu.cases.router_price_use_case import RouterPriceUseCase
from openstack_dashboard.dashboards.yuyu.cases.snapshot_price_use_case import SnapshotPriceUseCase
from openstack_dashboard.dashboards.yuyu.cases.volume_price_use_case import VolumePriceUseCase
from openstack_dashboard.dashboards.yuyu.cases.image_price_use_case import ImagePriceUseCase
from openstack_dashboard.dashboards.yuyu.core.pricing_admin.forms import BasePriceForm
class FlavorPriceForm(BasePriceForm):
flavor = forms.ThemableChoiceField(label=_("Flavor"))
USE_CASE = FlavorPriceUseCase()
NAME = "Flavor Price"
def __init__(self, request, *args, **kwargs):
super().__init__(request, *args, **kwargs)
flavor_list = kwargs.get('initial', {}).get('flavor_list', [])
self.fields['flavor'].choices = flavor_list
def to_payload(self, data):
payload = super().to_payload(data)
payload["flavor_id"] = data["flavor"]
return payload
class VolumePriceForm(BasePriceForm):
volume_type = forms.ThemableChoiceField(label=_("Volume Type"))
USE_CASE = VolumePriceUseCase()
NAME = "Volume Price"
def __init__(self, request, *args, **kwargs):
super().__init__(request, *args, **kwargs)
volume_type_list = kwargs.get('initial', {}).get('volume_type_list', [])
self.fields['volume_type'].choices = volume_type_list
def to_payload(self, data):
payload = super().to_payload(data)
payload["volume_type_id"] = data["volume_type"]
return payload
class FloatingIpPriceForm(BasePriceForm):
USE_CASE = FloatingIpPriceUseCase()
NAME = "Floating IP Price"
class RouterPriceForm(BasePriceForm):
USE_CASE = RouterPriceUseCase()
NAME = "Router Price"
class SnapshotPriceForm(BasePriceForm):
USE_CASE = SnapshotPriceUseCase()
NAME = "Snapshot Price"
class ImagePriceForm(BasePriceForm):
USE_CASE = ImagePriceUseCase()
NAME = "Image Price"

View file

@ -0,0 +1,20 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.utils.translation import ugettext_lazy as _
import horizon
class PriceConfiguration(horizon.Panel):
name = _("Price Configuration")
slug = "price_configuration"

View file

@ -0,0 +1,245 @@
from django.utils.translation import ugettext_lazy as _
from horizon import tables
from openstack_dashboard.dashboards.yuyu.cases.flavor_price_use_case import FlavorPriceUseCase
from openstack_dashboard.dashboards.yuyu.cases.floating_ip_price_use_case import FloatingIpPriceUseCase
from openstack_dashboard.dashboards.yuyu.cases.volume_price_use_case import VolumePriceUseCase
from openstack_dashboard.dashboards.yuyu.cases.router_price_use_case import RouterPriceUseCase
from openstack_dashboard.dashboards.yuyu.cases.snapshot_price_use_case import SnapshotPriceUseCase
from openstack_dashboard.dashboards.yuyu.cases.image_price_use_case import ImagePriceUseCase
from openstack_dashboard.dashboards.yuyu.core.pricing_admin.tables import BasePriceTable, BaseCreatePrice, \
BaseEditPrice, BaseDeletePrice
def create_create_action(type_name, **kwargs):
return type(type_name, (BaseCreatePrice,), kwargs)
def create_filter_action(type_name, **kwargs):
return type(type_name, (tables.FilterAction,), kwargs)
def create_edit_action(type_name, **kwargs):
return type(type_name, (BaseEditPrice,), kwargs)
def create_delete_action(type_name, **kwargs):
return type(type_name, (BaseDeletePrice,), kwargs)
class FlavorPriceTable(BasePriceTable):
name = tables.WrappingColumn('name', verbose_name=_('Flavor Name'))
def get_object_display(self, datum):
return datum['name']
class Meta(object):
name = "flavor_price"
verbose_name = _("Flavor Price")
table_actions = (
create_filter_action(
type_name="FlavorPriceFilter",
name="flavor_price_filter"
),
create_create_action(
type_name="FlavorPriceCreate",
name="flavor_price_create",
verbose_name=_("Create Flavor Price"),
url="horizon:admin:price_configuration:flavor_price_create"
)
)
row_actions = (
create_edit_action(
type_name="FlavorPriceUpdate",
name="flavor_price_edit",
verbose_name=_("Edit Flavor Price"),
url="horizon:admin:price_configuration:flavor_price_update"
),
create_delete_action(
type_name="FlavorPriceDelete",
use_case=FlavorPriceUseCase(),
single_action_label="Flavor Price",
plural_action_label="Flavor Prices"
)
)
class VolumePriceTable(BasePriceTable):
name = tables.WrappingColumn('name', verbose_name=_('Volume Type Name'))
def get_object_display(self, datum):
return datum['name']
class Meta(object):
name = "volume_price"
verbose_name = _("Volume Price")
table_actions = (
create_filter_action(
type_name="VolumePriceFilter",
name="volume_price_filter"
),
create_create_action(
type_name="VolumePriceCreate",
name="create_volume_price",
verbose_name=_("Create Volume Price"),
url="horizon:admin:price_configuration:volume_price_create"
)
)
row_actions = (
create_edit_action(
type_name="VolumePriceEdit",
name="update_volume_price",
verbose_name=_("Edit Volume Price"),
url="horizon:admin:price_configuration:volume_price_update"
),
create_delete_action(
type_name="VolumePriceDelete",
use_case=VolumePriceUseCase(),
single_action_label="Volume Price",
plural_action_label="Volume Prices"
)
)
class FloatingIpPriceTable(BasePriceTable):
def get_object_display(self, datum):
return datum['id']
class Meta(object):
name = "floating_ip_price"
verbose_name = _("Floating IP Price")
table_actions = (
create_filter_action(
type_name="FloatingIpPriceFilter",
name="floating_ip_price_filter"
),
create_create_action(
type_name="FloatingIpPriceCreate",
name="create_floating_ip_price",
verbose_name=_("Create Floating IP Price"),
url="horizon:admin:price_configuration:floating_ip_price_create",
single_data=True,
)
)
row_actions = (
create_edit_action(
type_name="FloatingIpPriceEdit",
name="update_floating_ip_price",
verbose_name=_("Edit Floating IP Price"),
url="horizon:admin:price_configuration:floating_ip_price_update"
),
create_delete_action(
type_name="FloatingIpPriceDelete",
use_case=FloatingIpPriceUseCase(),
single_action_label="Floating IP Price",
plural_action_label="Floating IP Prices"
)
)
class RouterPriceTable(BasePriceTable):
def get_object_display(self, datum):
return datum['id']
class Meta(object):
name = "router_price"
verbose_name = _("Router Price")
table_actions = (
create_filter_action(
type_name="RouterPriceFilter",
name="router_price_filter"
),
create_create_action(
type_name="RouterPriceCreate",
name="create_router_price",
verbose_name=_("Create Router Price"),
url="horizon:admin:price_configuration:router_price_create",
single_data=True,
)
)
row_actions = (
create_edit_action(
type_name="RouterPriceEdit",
name="update_router_price",
verbose_name=_("Edit Router Price"),
url="horizon:admin:price_configuration:router_price_update"
),
create_delete_action(
type_name="RouterPriceDelete",
use_case=RouterPriceUseCase(),
single_action_label="Router Price",
plural_action_label="Router Prices"
)
)
class SnapshotPriceTable(BasePriceTable):
def get_object_display(self, datum):
return datum['id']
class Meta(object):
name = "snapshot_price"
verbose_name = _("Snapshot Price")
table_actions = (
create_filter_action(
type_name="SnapshotFilter",
name="snapshot_price_filter"
),
create_create_action(
type_name="SnapshotCreate",
name="create_snapshot_price",
verbose_name=_("Create Snapshot Price"),
url="horizon:admin:price_configuration:snapshot_price_create",
single_data=True,
)
)
row_actions = (
create_edit_action(
type_name="SnapshotEdit",
name="update_snapshot_price",
verbose_name=_("Edit Snapshot Price"),
url="horizon:admin:price_configuration:snapshot_price_update"
),
create_delete_action(
type_name="SnapshotDelete",
use_case=SnapshotPriceUseCase(),
single_action_label="Snapshot Price",
plural_action_label="Snapshot Prices"
)
)
class ImagePriceTable(BasePriceTable):
def get_object_display(self, datum):
return datum['id']
class Meta(object):
name = "image_price"
verbose_name = _("Image Price")
table_actions = (
create_filter_action(
type_name="ImageFilter",
name="image_price_filter"
),
create_create_action(
type_name="ImageCreate",
name="create_image_price",
verbose_name=_("Create Image Price"),
url="horizon:admin:price_configuration:image_price_create",
single_data=True,
)
)
row_actions = (
create_edit_action(
type_name="ImageEdit",
name="update_image_price",
verbose_name=_("Edit Image Price"),
url="horizon:admin:price_configuration:image_price_update"
),
create_delete_action(
type_name="ImageDelete",
use_case=ImagePriceUseCase(),
single_action_label="Image Price",
plural_action_label="Image Prices"
)
)

View file

@ -0,0 +1,130 @@
from horizon import exceptions, tables, tabs
from openstack_dashboard.dashboards.yuyu.admin.price_configuration.tables import FlavorPriceTable, VolumePriceTable, \
FloatingIpPriceTable, RouterPriceTable, SnapshotPriceTable, ImagePriceTable
from django.utils.translation import ugettext_lazy as _
from openstack_dashboard.dashboards.yuyu.cases.flavor_price_use_case import FlavorPriceUseCase
from openstack_dashboard.dashboards.yuyu.cases.floating_ip_price_use_case import FloatingIpPriceUseCase
from openstack_dashboard.dashboards.yuyu.cases.volume_price_use_case import VolumePriceUseCase
from openstack_dashboard.dashboards.yuyu.cases.router_price_use_case import RouterPriceUseCase
from openstack_dashboard.dashboards.yuyu.cases.snapshot_price_use_case import SnapshotPriceUseCase
from openstack_dashboard.dashboards.yuyu.cases.image_price_use_case import ImagePriceUseCase
class FlavorTab(tabs.TableTab):
table_classes = (FlavorPriceTable,)
name = _("Flavor")
slug = "flavor"
template_name = 'horizon/common/_detail_table.html'
flavor_price_uc = FlavorPriceUseCase()
def get_flavor_price_data(self):
try:
data = self.flavor_price_uc.list(self.request)
return data
except Exception:
error_message = _('Unable to get flavor prices')
exceptions.handle(self.request, error_message)
return []
class VolumeTab(tabs.TableTab):
table_classes = (VolumePriceTable,)
name = _("Volume")
slug = "volume"
template_name = 'horizon/common/_detail_table.html'
volume_price_uc = VolumePriceUseCase()
def get_volume_price_data(self):
try:
data = self.volume_price_uc.list(self.request)
return data
except Exception:
error_message = _('Unable to get volume prices')
exceptions.handle(self.request, error_message)
return []
class FloatingIpTab(tabs.TableTab):
table_classes = (FloatingIpPriceTable,)
name = _("Floating IP")
slug = "floating_ip"
template_name = 'horizon/common/_detail_table.html'
fip_price_uc = FloatingIpPriceUseCase()
def get_floating_ip_price_data(self):
try:
data = self.fip_price_uc.list(self.request)
return data
except Exception:
error_message = _('Unable to get floating IP prices')
exceptions.handle(self.request, error_message)
return []
class RouterTab(tabs.TableTab):
table_classes = (RouterPriceTable,)
name = _("Router")
slug = "router"
template_name = 'horizon/common/_detail_table.html'
price_uc = RouterPriceUseCase()
def get_router_price_data(self):
try:
data = self.price_uc.list(self.request)
return data
except Exception:
error_message = _('Unable to get router prices')
exceptions.handle(self.request, error_message)
return []
class SnapshotTab(tabs.TableTab):
table_classes = (SnapshotPriceTable,)
name = _("Snapshot")
slug = "snapshot"
template_name = 'horizon/common/_detail_table.html'
price_uc = SnapshotPriceUseCase()
def get_snapshot_price_data(self):
try:
data = self.price_uc.list(self.request)
return data
except Exception:
error_message = _('Unable to get snapshot prices')
exceptions.handle(self.request, error_message)
return []
class ImageTab(tabs.TableTab):
table_classes = (ImagePriceTable,)
name = _("Image")
slug = "image"
template_name = 'horizon/common/_detail_table.html'
price_uc = ImagePriceUseCase()
def get_image_price_data(self):
try:
data = self.price_uc.list(self.request)
return data
except Exception:
error_message = _('Unable to get image prices')
exceptions.handle(self.request, error_message)
return []
class PriceConfigurationTabs(tabs.TabGroup):
slug = "price_config"
tabs = (FlavorTab, VolumeTab, FloatingIpTab, RouterTab, SnapshotTab, ImageTab, )
sticky = True

View file

@ -0,0 +1,8 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block modal-body-right %}
<h3>{% trans "Description:" %}</h3>
<p>{% trans 'Create/update a flavor price.' %}</p>
{% endblock %}

View file

@ -0,0 +1,8 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block modal-body-right %}
<h3>{% trans "Description:" %}</h3>
<p>{% trans 'Create/update a floating IP price for each allocation.' %}</p>
{% endblock %}

View file

@ -0,0 +1,8 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block modal-body-right %}
<h3>{% trans "Description:" %}</h3>
<p>{% trans 'Create/update a image price for each 1GB of space.' %}</p>
{% endblock %}

View file

@ -0,0 +1,8 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block modal-body-right %}
<h3>{% trans "Description:" %}</h3>
<p>{% trans 'Create/update a router price for each allocation that have external network.' %}</p>
{% endblock %}

View file

@ -0,0 +1,8 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block modal-body-right %}
<h3>{% trans "Description:" %}</h3>
<p>{% trans 'Create/update a snapshot price for each 1GB of space.' %}</p>
{% endblock %}

View file

@ -0,0 +1,8 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block modal-body-right %}
<h3>{% trans "Description:" %}</h3>
<p>{% trans 'Create/update a volume price for each 1GB of space.' %}</p>
{% endblock %}

View file

@ -0,0 +1,8 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Create/Update Flavor Price" %}{% endblock %}
{% block main %}
{% include "admin/price_configuration/_create_flavor.html" %}
{% endblock %}

View file

@ -0,0 +1,8 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Create/Update Floating IP Price" %}{% endblock %}
{% block main %}
{% include "admin/price_configuration/_create_floating_ip.html" %}
{% endblock %}

View file

@ -0,0 +1,8 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Create/Update Image Price" %}{% endblock %}
{% block main %}
{% include "admin/price_configuration/_create_image.html" %}
{% endblock %}

View file

@ -0,0 +1,8 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Create/Update Router Price" %}{% endblock %}
{% block main %}
{% include "admin/price_configuration/_create_router.html" %}
{% endblock %}

View file

@ -0,0 +1,8 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Create/Update Snapshot Price" %}{% endblock %}
{% block main %}
{% include "admin/price_configuration/_create_snapshot.html" %}
{% endblock %}

View file

@ -0,0 +1,8 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Create/Update Volume Price" %}{% endblock %}
{% block main %}
{% include "admin/price_configuration/_create_volume.html" %}
{% endblock %}

View file

@ -0,0 +1,13 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Price Configuration" %}{% endblock %}
{% block main %}
{% include "admin/price_configuration/missing_prices.html" %}
<div class="row">
<div class="col-sm-12">
{{ tab_group.render }}
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,70 @@
{% if missing_price.flavor %}
<div class="row">
<div class="col-md-12">
<div class="alert alert-danger alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
Flavor price not complete
</div>
</div>
</div>
{% endif %}
{% if missing_price.volume %}
<div class="row">
<div class="col-md-12">
<div class="alert alert-danger alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
Volume price not complete
</div>
</div>
</div>
{% endif %}
{% if missing_price.fip %}
<div class="row">
<div class="col-md-12">
<div class="alert alert-danger alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
Floating IP price not complete
</div>
</div>
</div>
{% endif %}
{% if missing_price.router %}
<div class="row">
<div class="col-md-12">
<div class="alert alert-danger alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
Router IP price not complete
</div>
</div>
</div>
{% endif %}
{% if missing_price.snapshot %}
<div class="row">
<div class="col-md-12">
<div class="alert alert-danger alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
Snapshot price not complete
</div>
</div>
</div>
{% endif %}
{% if missing_price.image %}
<div class="row">
<div class="col-md-12">
<div class="alert alert-danger alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
Image price not complete
</div>
</div>
</div>
{% endif %}

View file

@ -0,0 +1,35 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^flavor_price/create/$', views.FlavorPriceAddFormView.as_view(), name='flavor_price_create'),
url(r'^flavor_price/update/(?P<id>[^/]+)/$', views.FlavorPriceUpdateFormView.as_view(), name='flavor_price_update'),
url(r'^volume_price/create/$', views.VolumePriceAddFormView.as_view(), name='volume_price_create'),
url(r'^volume_price/update/(?P<id>[^/]+)/$', views.VolumePriceUpdateFormView.as_view(), name='volume_price_update'),
url(r'^floating_ip_price/create/$', views.FloatingIpPriceAddFormView.as_view(), name='floating_ip_price_create'),
url(r'^floating_ip_price/update/(?P<id>[^/]+)/$', views.FloatingIpPriceUpdateFormView.as_view(),
name='floating_ip_price_update'),
url(r'^router_price/create/$', views.RouterPriceAddFormView.as_view(), name='router_price_create'),
url(r'^router_price/update/(?P<id>[^/]+)/$', views.RouterPriceUpdateFormView.as_view(),
name='router_price_update'),
url(r'^snapshot_price/create/$', views.SnapshotPriceAddFormView.as_view(), name='snapshot_price_create'),
url(r'^snapshot_price/update/(?P<id>[^/]+)/$', views.SnapshotPriceUpdateFormView.as_view(),
name='snapshot_price_update'),
url(r'^image_price/create/$', views.ImagePriceAddFormView.as_view(), name='image_price_create'),
url(r'^image_price/update/(?P<id>[^/]+)/$', views.ImagePriceUpdateFormView.as_view(),
name='image_price_update'),
]

View file

@ -0,0 +1,378 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.urls import reverse_lazy, reverse
from django.utils.translation import ugettext_lazy as _
from neutronclient.common import exceptions as neutron_exc
from horizon import exceptions, tabs
from horizon import forms
from openstack_dashboard import api
from openstack_dashboard.dashboards.yuyu.cases.flavor_price_use_case import FlavorPriceUseCase
from openstack_dashboard.dashboards.yuyu.cases.floating_ip_price_use_case import FloatingIpPriceUseCase
from openstack_dashboard.dashboards.yuyu.cases.volume_price_use_case import VolumePriceUseCase
from openstack_dashboard.dashboards.yuyu.cases.router_price_use_case import RouterPriceUseCase
from openstack_dashboard.dashboards.yuyu.cases.snapshot_price_use_case import SnapshotPriceUseCase
from openstack_dashboard.dashboards.yuyu.cases.image_price_use_case import ImagePriceUseCase
from .forms import FlavorPriceForm, VolumePriceForm, FloatingIpPriceForm, RouterPriceForm, SnapshotPriceForm, ImagePriceForm
from .tabs import PriceConfigurationTabs
from ...core.utils.price_checker import has_missing_price
class IndexView(tabs.TabbedTableView):
tab_group_class = PriceConfigurationTabs
page_title = _("Price Configuration")
template_name = "admin/price_configuration/index.html"
def get_context_data(self, **kwargs):
context = super(IndexView, self).get_context_data(**kwargs)
context['missing_price'] = has_missing_price(self.request)
return context
class FlavorPriceAddFormView(forms.ModalFormView):
form_class = FlavorPriceForm
form_id = "flavor_price_form"
page_title = _("Create Flavor Price")
submit_label = _("Create Flavor Price")
submit_url = reverse_lazy("horizon:admin:price_configuration:flavor_price_create")
success_url = reverse_lazy("horizon:admin:price_configuration:index")
template_name = 'admin/price_configuration/create_flavor.html'
flavor_price_uc = FlavorPriceUseCase()
def get_object_display(self, obj):
return obj.flavor_id
def get_initial(self):
added_ids = []
flavors = []
try:
added_ids = map(lambda x: x['flavor_id'], self.flavor_price_uc.list(self.request))
flavors = api.nova.flavor_list(self.request)
except neutron_exc.ConnectionFailed:
exceptions.handle(self.request)
except Exception:
exceptions.handle(self.request,
_("Unable to retrieve flavors."))
flavor_list = []
for flavor in flavors:
if flavor.id not in added_ids:
flavor_list.append((flavor.id, flavor.name))
if not flavor_list:
flavor_list = [(None, _("No flavors available"))]
return {'flavor_list': flavor_list}
class FlavorPriceUpdateFormView(forms.ModalFormView):
form_class = FlavorPriceForm
form_id = "flavor_price_form_update"
page_title = _("Flavor Price")
submit_label = _("Update Flavor Price")
submit_url = "horizon:admin:price_configuration:flavor_price_update"
success_url = reverse_lazy("horizon:admin:price_configuration:index")
template_name = 'admin/price_configuration/create_flavor.html'
flavor_price_uc = FlavorPriceUseCase()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
args = (self.kwargs['id'],)
context['submit_url'] = reverse(self.submit_url, args=args)
return context
def get_object_display(self, obj):
return obj.flavor_id
def get_initial(self):
try:
flavor_price = self.flavor_price_uc.get(self.request, self.kwargs['id'])
except Exception:
flavor_price = None
exceptions.handle(self.request,
_("Unable to retrieve flavor price."))
return {
'model_id': flavor_price['id'],
'flavor_list': [(flavor_price['flavor_id'], flavor_price['name'])],
'hourly_price': flavor_price['hourly_price'],
'monthly_price': flavor_price['monthly_price']
}
class VolumePriceAddFormView(forms.ModalFormView):
form_class = VolumePriceForm
form_id = "volume_price_form"
page_title = _("Create Volume Price")
submit_label = _("Create Volume Price")
submit_url = reverse_lazy("horizon:admin:price_configuration:volume_price_create")
success_url = reverse_lazy("horizon:admin:price_configuration:index")
template_name = 'admin/price_configuration/create_volume.html'
volume_price_uc = VolumePriceUseCase()
def get_object_display(self, obj):
return obj.volume_type_id
def get_initial(self):
added_ids = []
volumes = []
try:
added_ids = map(lambda x: x['volume_type_id'], self.volume_price_uc.list(self.request))
volumes = api.cinder.volume_type_list(self.request)
except neutron_exc.ConnectionFailed:
exceptions.handle(self.request)
except Exception:
exceptions.handle(self.request,
_("Unable to retrieve volumes."))
volume_list = []
for d in volumes:
if d.id not in added_ids:
volume_list.append((d.id, d.name))
if not volume_list:
volume_list = [(None, _("No volume type available"))]
return {'volume_type_list': volume_list}
class VolumePriceUpdateFormView(forms.ModalFormView):
form_class = VolumePriceForm
form_id = "volume_price_form_update"
page_title = _("Volume Price")
submit_label = _("Update Volume Price")
submit_url = "horizon:admin:price_configuration:volume_price_update"
success_url = reverse_lazy("horizon:admin:price_configuration:index")
template_name = 'admin/price_configuration/create_volume.html'
volume_price_uc = VolumePriceUseCase()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
args = (self.kwargs['id'],)
context['submit_url'] = reverse(self.submit_url, args=args)
return context
def get_object_display(self, obj):
return obj.volume_type_id
def get_initial(self):
try:
volume = self.volume_price_uc.get(self.request, self.kwargs['id'])
except Exception:
volume = None
exceptions.handle(self.request,
_("Unable to retrieve volume price."))
return {
'model_id': volume['id'],
'volume_type_list': [(volume['volume_type_id'], volume['name'])],
'hourly_price': volume['hourly_price'],
'monthly_price': volume['monthly_price']
}
class FloatingIpPriceAddFormView(forms.ModalFormView):
form_class = FloatingIpPriceForm
form_id = "floating_ip_price_form"
page_title = _("Create Floating IP Price")
submit_label = _("Create Floating IP Price")
submit_url = reverse_lazy("horizon:admin:price_configuration:floating_ip_price_create")
success_url = reverse_lazy("horizon:admin:price_configuration:index")
template_name = 'admin/price_configuration/create_floating_ip.html'
def get_object_display(self, obj):
return obj.id
class FloatingIpPriceUpdateFormView(forms.ModalFormView):
form_class = FloatingIpPriceForm
form_id = "floating_ip_price_form_update"
page_title = _("Floating IP Price")
submit_label = _("Update Floating IP Price")
submit_url = "horizon:admin:price_configuration:floating_ip_price_update"
success_url = reverse_lazy("horizon:admin:price_configuration:index")
template_name = 'admin/price_configuration/create_floating_ip.html'
fip_price_uc = FloatingIpPriceUseCase()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
args = (self.kwargs['id'],)
context['submit_url'] = reverse(self.submit_url, args=args)
return context
def get_object_display(self, obj):
return obj.id
def get_initial(self):
try:
volume = self.fip_price_uc.get(self.request, self.kwargs['id'])
except Exception:
volume = None
exceptions.handle(self.request,
_("Unable to retrieve floating ip price."))
return {
'model_id': volume['id'],
'hourly_price': volume['hourly_price'],
'monthly_price': volume['monthly_price']
}
class RouterPriceAddFormView(forms.ModalFormView):
form_class = RouterPriceForm
form_id = "router_price_form"
page_title = _("Create Router Price")
submit_label = _("Create Router Price")
submit_url = reverse_lazy("horizon:admin:price_configuration:router_price_create")
success_url = reverse_lazy("horizon:admin:price_configuration:index")
template_name = 'admin/price_configuration/create_router.html'
def get_object_display(self, obj):
return obj.id
class RouterPriceUpdateFormView(forms.ModalFormView):
form_class = RouterPriceForm
form_id = "router_price_form_update"
page_title = _("Router Price")
submit_label = _("Update Router Price")
submit_url = "horizon:admin:price_configuration:router_price_update"
success_url = reverse_lazy("horizon:admin:price_configuration:index")
template_name = 'admin/price_configuration/create_router.html'
price_uc = RouterPriceUseCase()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
args = (self.kwargs['id'],)
context['submit_url'] = reverse(self.submit_url, args=args)
return context
def get_object_display(self, obj):
return obj.id
def get_initial(self):
try:
volume = self.price_uc.get(self.request, self.kwargs['id'])
except Exception:
volume = None
exceptions.handle(self.request,
_("Unable to retrieve router price."))
return {
'model_id': volume['id'],
'hourly_price': volume['hourly_price'],
'monthly_price': volume['monthly_price']
}
class SnapshotPriceAddFormView(forms.ModalFormView):
form_class = SnapshotPriceForm
form_id = "snapshot_form"
page_title = _("Create Snapshot Price")
submit_label = _("Create Snapshot Price")
submit_url = reverse_lazy("horizon:admin:price_configuration:snapshot_price_create")
success_url = reverse_lazy("horizon:admin:price_configuration:index")
template_name = 'admin/price_configuration/create_snapshot.html'
def get_object_display(self, obj):
return obj.id
class SnapshotPriceUpdateFormView(forms.ModalFormView):
form_class = SnapshotPriceForm
form_id = "snapshot_form_update"
page_title = _("Snapshot Price")
submit_label = _("Update Snapshot Price")
submit_url = "horizon:admin:price_configuration:snapshot_price_update"
success_url = reverse_lazy("horizon:admin:price_configuration:index")
template_name = 'admin/price_configuration/create_snapshot.html'
price_uc = SnapshotPriceUseCase()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
args = (self.kwargs['id'],)
context['submit_url'] = reverse(self.submit_url, args=args)
return context
def get_object_display(self, obj):
return obj.id
def get_initial(self):
try:
volume = self.price_uc.get(self.request, self.kwargs['id'])
except Exception:
volume = None
exceptions.handle(self.request,
_("Unable to retrieve snapshot price."))
return {
'model_id': volume['id'],
'hourly_price': volume['hourly_price'],
'monthly_price': volume['monthly_price']
}
class ImagePriceAddFormView(forms.ModalFormView):
form_class = ImagePriceForm
form_id = "snapshot_form"
page_title = _("Create Image Price")
submit_label = _("Create Image Price")
submit_url = reverse_lazy("horizon:admin:price_configuration:image_price_create")
success_url = reverse_lazy("horizon:admin:price_configuration:index")
template_name = 'admin/price_configuration/create_image.html'
def get_object_display(self, obj):
return obj.id
class ImagePriceUpdateFormView(forms.ModalFormView):
form_class = ImagePriceForm
form_id = "image_form_update"
page_title = _("Image Price")
submit_label = _("Update Image Price")
submit_url = "horizon:admin:price_configuration:image_price_update"
success_url = reverse_lazy("horizon:admin:price_configuration:index")
template_name = 'admin/price_configuration/create_image.html'
price_uc = ImagePriceUseCase()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
args = (self.kwargs['id'],)
context['submit_url'] = reverse(self.submit_url, args=args)
return context
def get_object_display(self, obj):
return obj.id
def get_initial(self):
try:
volume = self.price_uc.get(self.request, self.kwargs['id'])
except Exception:
volume = None
exceptions.handle(self.request,
_("Unable to retrieve image price."))
return {
'model_id': volume['id'],
'hourly_price': volume['hourly_price'],
'monthly_price': volume['monthly_price']
}

View file

View file

@ -0,0 +1,20 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.utils.translation import ugettext_lazy as _
import horizon
class ProjectsInvoice(horizon.Panel):
name = _("Projects Invoice")
slug = "projects_invoice"

View file

@ -0,0 +1,44 @@
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from horizon import tables
class InvoiceAction(tables.LinkAction):
name = "invoice"
verbose_name = "Invoice"
def get_link_url(self, datum=None, *args, **kwargs):
return reverse("horizon:admin:projects_invoice:download_pdf", kwargs={
"id": datum['id'],
"project_id": datum['project_id'],
})
class UsageCostAction(tables.LinkAction):
name = "usage_cost"
verbose_name = "Usage Cost"
def get_link_url(self, datum=None, *args, **kwargs):
return reverse("horizon:admin:projects_invoice:usage_cost", kwargs={
"id": datum['id'],
"project_id": datum['project_id'],
})
class InvoiceTable(tables.DataTable):
date = tables.WrappingColumn('date', verbose_name=_('Date'))
state = tables.WrappingColumn('state', verbose_name=_('State'))
total = tables.Column('total', verbose_name=_('Total'))
def get_object_id(self, datum):
return datum['id']
def get_object_display(self, datum):
return datum['date']
class Meta(object):
name = "invoice_table"
hidden_title = True
verbose_name = _("Invoice")
row_actions = (InvoiceAction, UsageCostAction)

View file

@ -0,0 +1,66 @@
{% extends 'base.html' %}
{% block title %}{{ page_title }}{% endblock %}
{% block main %}
{% if invoice %}
{% if invoice.state == 2 %}
<a class="btn btn-primary"
href="{% url 'horizon:admin:projects_invoice:finish_invoice' invoice.id %}?next={{ request.path }}">Set
to Finished</a>
{% endif %}
{% if invoice.state == 100 %}
<a class="btn btn-danger"
href="{% url 'horizon:admin:projects_invoice:rollback_to_unpaid' invoice.id %}?next={{ request.path }}">Rollback
to Unpaid</a>
{% endif %}
<br/>
<br/>
<div>
<dl class="dl-horizontal">
<dt>Invoice Month</dt>
<dd>{{ invoice.start_date|date:"M Y" }}</dd>
<dt>Invoice State</dt>
<dd>{{ invoice.state_text }}</dd>
<dt>Subtotal</dt>
<dd>{{ invoice.subtotal_money }}</dd>
{% if invoice.state != 1 %}
<dt>Tax</dt>
<dd>{{ invoice.tax_money }}</dd>
<dt>Total</dt>
<dd>{{ invoice.total_money }}</dd>
{% endif %}
</dl>
</div>
<div id="instance-cost">
{{ instance_cost_table.render }}
</div>
<div id="volume-cost">
{{ volume_cost_table.render }}
</div>
<div id="floating-ip-cost">
{{ floating_ip_cost_table.render }}
</div>
<div id="router-cost">
{{ router_cost_table.render }}
</div>
<div id="snapshot-cost">
{{ snapshot_cost_table.render }}
</div>
<div id="image-cost">
{{ image_cost_table.render }}
</div>
{% else %}
<h1>Billing not enabled or you don't have any usage yet</h1> <br/>
{% endif %}
{% endblock %}
{% block js %}
{{ block.super }}
<script type="text/javascript">
function onInvoiceChange(val) {
var search = "?invoice_id=" + val;
window.location.href = window.location.protocol + "//" + window.location.host + window.location.pathname + search;
}
</script>
{% endblock %}

View file

@ -0,0 +1,123 @@
{% extends 'base.html' %}
{% block title %}{{ page_title }}{% endblock %}
{% block main %}
<button onclick="javascript:downloadPdf();" class="btn btn-default">Download PDF</button>
{% if invoice.state == 2 %}
<a class="btn btn-primary" href="{% url 'horizon:admin:projects_invoice:finish_invoice' invoice.id %}?next={{ request.path }}">Set to Finished</a>
{% endif %}
{% if invoice.state == 100 %}
<a class="btn btn-danger" href="{% url 'horizon:admin:projects_invoice:rollback_to_unpaid' invoice.id %}?next={{ request.path }}">Rollback to Unpaid</a>
{% endif %}
<br/>
<br/>
<div id="invoice">
<div>
<dl>
<dt>Invoice Month</dt>
<dd>
<h3>{{ invoice.start_date|date:"M Y" }}</h3>
</dd>
</dl>
<h5>Invoice State: {{ invoice.state_text }}</h5>
</div>
<table class="table table-striped">
<thead>
<tr>
<th>Component</th>
<th>Total Cost</th>
</tr>
</thead>
<tbody>
<tr>
<td>Instance</td>
<td>{{ instance_cost }}</td>
</tr>
<tr>
<td>Volume</td>
<td>{{ volume_cost }}</td>
</tr>
<tr>
<td>Floating IP</td>
<td>{{ fip_cost }}</td>
</tr>
<tr>
<td>Router</td>
<td>{{ router_cost }}</td>
</tr>
<tr>
<td>Snapshot</td>
<td>{{ snapshot_cost }}</td>
</tr>
<tr>
<td>Image</td>
<td>{{ image_cost }}</td>
</tr>
<tr>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td><b>Subtotal:</b> {{ invoice.subtotal_money }}</td>
</tr>
{% if invoice.state != 1 %}
<tr>
<td></td>
<td><b>Tax:</b> {{ invoice.tax_money }}</td>
</tr>
<tr>
<td></td>
<td><b>Total:</b> {{ invoice.total_money }}</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
{% endblock %}
{% block js %}
{{ block.super }}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/1.3.2/jspdf.min.js"></script>
<script type="text/javascript">
function downloadPdf() {
var pdf = new jsPDF('p', 'pt', 'letter');
// source can be HTML-formatted string, or a reference
// to an actual DOM element from which the text will be scraped.
source = $('#invoice')[0];
// we support special element handlers. Register them with jQuery-style
// ID selector for either ID or node name. ("#iAmID", "div", "span" etc.)
// There is no support for any other type of selectors
// (class, of compound) at this time.
specialElementHandlers = {
// element with id of "bypass" - jQuery style selector
'#bypassme': function (element, renderer) {
// true = "handled elsewhere, bypass text extraction"
return true
}
};
margins = {
top: 80,
bottom: 60,
left: 40,
width: 522
};
// all coords and widths are in jsPDF instance's declared units
// 'inches' in this case
pdf.fromHTML(
source, // HTML string or DOM elem ref.
margins.left, // x coord
margins.top, { // y coord
'width': margins.width, // max width of content on PDF
'elementHandlers': specialElementHandlers
},
function (dispose) {
// dispose: object with X, Y of the last line add to the PDF
// this allow the insertion of new lines after html
pdf.save('invoice.pdf');
}, margins);
}
</script>
{% endblock %}

View file

@ -0,0 +1,17 @@
{% load horizon %}
{% jstemplate %}
{% extends 'base.html' %}
{% load i18n %}
{% block title %}
{% trans "Price_Volume_Panel" %}
{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Price_Volume_Panel") %}
{% endblock page_header %}
{% block main %}
{% endblock %}
{% endjstemplate %}

View file

@ -0,0 +1,31 @@
{% extends 'base.html' %}
{% block title %}{{ page_title }}{% endblock %}
{% block main %}
<div>
<dl class="dl-horizontal">
<dt>Project: </dt>
<dd>
<select id="project_select" onchange="onProjectSelect(this.value)">
{% for i in project_list %}
<option
value='{"project_id": "{{ i.id }}", "project_name": "{{ i.name }}"}'
{% if i.id == current_project_id %}selected {% endif %}
>{{ i.name }}</option>
{% endfor %}
</select>
</dd>
</dl>
</div>
{{ table.render }}
{% endblock %}
{% block js %}
{{ block.super }}
<script type="text/javascript">
function onProjectSelect(val) {
console.log(val)
const data = JSON.parse(val);
var search = "?project_id=" + data.project_id + "&project_name=" + data.project_name;
window.location.href = window.location.protocol + "//" + window.location.host + window.location.pathname + search;
}
</script>
{% endblock %}

View file

@ -0,0 +1,24 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.conf.urls import url
from openstack_dashboard.dashboards.yuyu.admin.projects_invoice import views
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^invoice/pdf/(?P<project_id>[^/]+)/(?P<id>[^/]+)/$', views.InvoiceView.as_view(), name='download_pdf'),
url(r'^invoice/usage/(?P<project_id>[^/]+)/(?P<id>[^/]+)/$', views.UsageCostView.as_view(), name='usage_cost'),
url(r'^invoice/finish/(?P<id>[^/]+)/$', views.FinishInvoice.as_view(), name='finish_invoice'),
url(r'^invoice/rollback_to_unpaid/(?P<id>[^/]+)/$', views.RollbackToUnpaidInvoice.as_view(),
name='rollback_to_unpaid'),
]

View file

@ -0,0 +1,252 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import datetime
import dateutil.parser
from dateutil.tz import tzutc
from django import shortcuts
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.utils.timesince import timesince
from django.utils import formats
from django.utils.translation import ugettext_lazy as _
from djmoney.money import Money
from horizon import exceptions
from horizon import tables
from horizon import views
from openstack_dashboard import api
from openstack_dashboard.dashboards.yuyu.cases.invoice_use_case import InvoiceUseCase
from .tables import InvoiceTable
from ...core.usage_cost.tables import InstanceCostTable, VolumeCostTable, FloatingIpCostTable, RouterCostTable, \
SnapshotCostTable, ImageCostTable
from ...core.utils.invoice_utils import state_to_text
class IndexView(tables.DataTableView):
table_class = InvoiceTable
page_title = _("Invoice")
template_name = "admin/projects_invoice/invoice_table.html"
invoice_uc = InvoiceUseCase()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['project_list'], _ = api.keystone.tenant_list(self.request, user=self.request.user.id)
context['current_project_id'] = self.request.GET.get('project_id', self.request.user.project_id)
context['current_project_name'] = self.request.GET.get('project_name', self.request.user.project_id)
print(context['project_list'])
return context
def get_data(self):
project_id = self.request.GET.get('project_id', self.request.user.project_id)
try:
data = []
for d in self.invoice_uc.get_simple_list(self.request, project_id):
data.append({
'id': d['id'],
'project_id': project_id,
'date': formats.date_format(d['start_date'], 'M Y'),
'state': state_to_text(d['state']),
'total': d['total_money'] or d['subtotal_money']
})
return list(data)
except Exception as e:
error_message = _('Unable to get invoice')
exceptions.handle(self.request, error_message)
return []
class InvoiceView(views.APIView):
page_title = _("Invoice")
template_name = "admin/projects_invoice/download_pdf.html"
invoice_uc = InvoiceUseCase()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
invoice = self.invoice_uc.get_invoice(self.request, self.kwargs['id'], tenant_id=self.kwargs['project_id'])
context['invoice'] = invoice
context['instance_cost'] = self.get_sum_price(invoice, 'instances')
context['volume_cost'] = self.get_sum_price(invoice, 'volumes')
context['fip_cost'] = self.get_sum_price(invoice, 'floating_ips')
context['router_cost'] = self.get_sum_price(invoice, 'routers')
context['snapshot_cost'] = self.get_sum_price(invoice, 'snapshots')
context['image_cost'] = self.get_sum_price(invoice, 'images')
return context
def get_sum_price(self, invoice, key):
instance_prices = map(
lambda x: Money(amount=x['price_charged'], currency=x['price_charged_currency']),
invoice.get(key, [])
)
return sum(instance_prices)
class UsageCostView(tables.MultiTableView):
table_classes = (
InstanceCostTable, VolumeCostTable, FloatingIpCostTable, RouterCostTable, SnapshotCostTable, ImageCostTable)
page_title = _("Usage Cost")
template_name = "admin/projects_invoice/cost_tables.html"
invoice_uc = InvoiceUseCase()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def get(self, request, *args, **kwargs):
request.invoice = self.invoice_uc.get_invoice(self.request, self.kwargs['id'],
tenant_id=self.kwargs['project_id'])
return super(UsageCostView, self).get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['invoice'] = self.request.invoice
return context
def get_instance_cost_data(self):
try:
datas = map(lambda x: {
"id": x['id'],
"name": x['name'],
"flavor": api.nova.flavor_get(self.request, x['flavor_id']).name,
"usage": timesince(
dateutil.parser.isoparse(x['start_date']),
dateutil.parser.isoparse(x['adjusted_end_date'])
),
"cost": Money(amount=x['price_charged'], currency=x['price_charged_currency'])
}, self.request.invoice.get('instances', []))
return datas
except Exception:
error_message = _('Unable to get instance cost')
exceptions.handle(self.request, error_message)
return []
def get_volume_cost_data(self):
try:
datas = map(lambda x: {
"id": x['id'],
"name": x['volume_name'],
"usage": timesince(
dateutil.parser.isoparse(x['start_date']),
dateutil.parser.isoparse(x['adjusted_end_date'])
),
'type': api.cinder.volume_type_get(self.request, x['volume_type_id']).name,
'size': x['space_allocation_gb'],
"cost": Money(amount=x['price_charged'], currency=x['price_charged_currency'])
}, self.request.invoice.get('volumes', []))
return datas
except Exception:
error_message = _('Unable to get volume cost')
exceptions.handle(self.request, error_message)
return []
def get_floating_ip_cost_data(self):
try:
datas = map(lambda x: {
"id": x['id'],
"name": x['ip'],
"usage": timesince(
dateutil.parser.isoparse(x['start_date']),
dateutil.parser.isoparse(x['adjusted_end_date'])
),
"cost": Money(amount=x['price_charged'], currency=x['price_charged_currency'])
}, self.request.invoice.get('floating_ips', []))
return datas
except Exception:
error_message = _('Unable to get floating ip cost')
exceptions.handle(self.request, error_message)
return []
def get_router_cost_data(self):
try:
datas = map(lambda x: {
"id": x['id'],
"name": x['name'],
"usage": timesince(
dateutil.parser.isoparse(x['start_date']),
dateutil.parser.isoparse(x['adjusted_end_date'])
),
"cost": Money(amount=x['price_charged'], currency=x['price_charged_currency'])
}, self.request.invoice.get('routers', []))
return datas
except Exception:
error_message = _('Unable to get router cost')
exceptions.handle(self.request, error_message)
return []
def get_snapshot_cost_data(self):
try:
datas = map(lambda x: {
"id": x['id'],
"name": x['name'],
'size': x['space_allocation_gb'],
"usage": timesince(
dateutil.parser.isoparse(x['start_date']),
dateutil.parser.isoparse(x['adjusted_end_date'])
),
"cost": Money(amount=x['price_charged'], currency=x['price_charged_currency'])
}, self.request.invoice.get('snapshots', []))
return datas
except Exception:
error_message = _('Unable to get snapshot cost')
exceptions.handle(self.request, error_message)
return []
def get_image_cost_data(self):
try:
datas = map(lambda x: {
"id": x['id'],
"name": x['name'],
'size': x['space_allocation_gb'],
"usage": timesince(
dateutil.parser.isoparse(x['start_date']),
dateutil.parser.isoparse(x['adjusted_end_date'])
),
"cost": Money(amount=x['price_charged'], currency=x['price_charged_currency'])
}, self.request.invoice.get('images', []))
return datas
except Exception:
error_message = _('Unable to get images cost')
exceptions.handle(self.request, error_message)
return []
class FinishInvoice(views.APIView):
invoice_uc = InvoiceUseCase()
def get(self, request, *args, **kwargs):
self.invoice_uc.finish_invoice(request, kwargs['id'])
next_url = request.GET.get('next', reverse('horizon:admin:projects_invoice:index'))
return HttpResponseRedirect(next_url)
class RollbackToUnpaidInvoice(views.APIView):
invoice_uc = InvoiceUseCase()
def get(self, request, *args, **kwargs):
self.invoice_uc.rollback_to_unpaid_invoice(request, kwargs['id'])
next_url = request.GET.get('next', reverse('horizon:admin:projects_invoice:index'))
return HttpResponseRedirect(next_url)

0
yuyu/cases/__init__.py Normal file
View file

View file

@ -0,0 +1,19 @@
from openstack_dashboard.dashboards.yuyu.core import yuyu_client
class AdminOverviewUseCase:
def total_resource(self, request):
response = yuyu_client.get(request, f"admin_overview/total_resource/")
return response.json()
def active_resource(self, request):
response = yuyu_client.get(request, f"admin_overview/active_resource/")
return response.json()
def price_total_resource(self, request):
response = yuyu_client.get(request, f"admin_overview/price_total_resource/")
return response.json()
def price_active_resource(self, request):
response = yuyu_client.get(request, f"admin_overview/price_active_resource/")
return response.json()

View file

@ -0,0 +1,38 @@
from openstack_dashboard import api
from openstack_dashboard.dashboards.yuyu.cases.pricing_use_case import PricingUseCase
class FlavorPriceUseCase(PricingUseCase):
pricing_name = "flavor"
def list(self, request):
data = list(super().list(request))
for d in data:
try:
flavor = api.nova.flavor_get(request, d['flavor_id'])
d["name"] = flavor.name
except Exception:
d["name"] = 'Invalid Flavor'
return data
def get(self, request, id):
data = super().get(request, id)
try:
flavor = api.nova.flavor_get(request, d['flavor_id'])
data["name"] = flavor.name
except Exception:
data["name"] = 'Invalid Flavor'
return data
def has_missing_price(self, request):
server_data_prices = list(map(lambda d: d['flavor_id'], super().list(request)))
flavor_list = api.nova.flavor_list(request)
for f in flavor_list:
if f.id not in server_data_prices:
return True
return False

View file

@ -0,0 +1,5 @@
from openstack_dashboard.dashboards.yuyu.cases.pricing_use_case import PricingUseCase
class FloatingIpPriceUseCase(PricingUseCase):
pricing_name = "floating_ip"

View file

@ -0,0 +1,5 @@
from openstack_dashboard.dashboards.yuyu.cases.pricing_use_case import PricingUseCase
class ImagePriceUseCase(PricingUseCase):
pricing_name = "image"

View file

@ -0,0 +1,122 @@
import dateutil.parser
import requests
from djmoney.money import Money
from openstack_dashboard import api
from openstack_dashboard.dashboards.yuyu.core import yuyu_client
from openstack_dashboard.dashboards.yuyu.core.utils.invoice_utils import state_to_text
class InvoiceUseCase:
def get_simple_list(self, request, tenant_id=None):
if not tenant_id:
tenant_id = request.user.project_id
response = yuyu_client.get(request, f"invoice/simple_list/?tenant_id={tenant_id}")
data = response.json()
print(data)
for d in data:
zero_money = Money(amount=0, currency=d['subtotal_currency'])
d['start_date'] = dateutil.parser.isoparse(d['start_date'])
d['subtotal_money'] = Money(amount=d['subtotal'], currency=d['subtotal_currency'])
d['total_money'] = Money(amount=d['total'], currency=d['total_currency']) if d['total'] else zero_money
d['state_text'] = state_to_text(d['state'])
return data
def get_invoice(self, request, id, tenant_id=None):
if not tenant_id:
tenant_id = request.user.project_id
response = yuyu_client.get(request, f"invoice/{id}/?tenant_id={tenant_id}")
data = response.json()
data['subtotal_money'] = Money(amount=data['subtotal'], currency=data['subtotal_currency'])
zero_money = Money(amount=0, currency=data['subtotal_currency'])
data['tax_money'] = Money(amount=data['tax'], currency=data['tax_currency']) if data['tax'] else zero_money
data['total_money'] = Money(amount=data['total'], currency=data['total_currency']) if data[
'total'] else zero_money
data['start_date'] = dateutil.parser.isoparse(data['start_date'])
data['state_text'] = state_to_text(data['state'])
return data
def enable_billing(self, request):
volume_type_list = api.cinder.volume_type_list(request)
volume_type_name_to_id = {
v.name: v.id
for v in volume_type_list
}
router_with_ext = filter(lambda r: bool(r.external_gateway_info), api.neutron.router_list(request))
instances = api.nova.server_list(request, search_opts={'all_tenants': True})[0]
volumes = api.cinder.volume_list(request, search_opts={"all_tenants": 1})
floating_ips = api.neutron.tenant_floating_ip_list(request, all_tenants=True)
snapshots = api.cinder.volume_snapshot_list(request, search_opts={'all_tenants': True})
images = api.glance.image_list_detailed(request, filters={'is_public': None})[0]
# Note!: When initializing router, we don't know when external network was added to router
payload = {
"instances": list(map(lambda s: {
"tenant_id": s.tenant_id,
"instance_id": s.id,
"name": s.name,
"flavor_id": s.flavor['id'],
"start_date": s.created
}, instances)),
"volumes": list(map(lambda v: {
"tenant_id": v.tenant_id,
"volume_id": v.id,
"volume_name": v.name,
"volume_type_id": volume_type_name_to_id[v.volume_type],
"space_allocation_gb": v.size,
"start_date": v.created_at
}, volumes)),
"floating_ips": list(map(lambda f: {
"tenant_id": f.tenant_id,
"fip_id": f.id,
"ip": f.ip,
"start_date": f.created_at
}, floating_ips)),
"routers": list(map(lambda r: {
"tenant_id": r.tenant_id,
"router_id": r.id,
"name": r.name,
"start_date": r.created_at
}, router_with_ext)),
"snapshots": list(map(lambda s: {
"tenant_id": s.project_id,
"snapshot_id": s.id,
"name": s.name,
"space_allocation_gb": s.size,
"start_date": s.created_at
}, snapshots)),
"images": list(map(lambda i: {
"tenant_id": i.owner,
"image_id": i.id,
"name": i.name,
"space_allocation_gb": i.size / 1024 / 1024 / 1024,
"start_date": i.created_at
}, images))
}
response = yuyu_client.post(request, f"invoice/enable_billing/", payload)
if response.status_code == 200:
return True
raise Exception('Unable to enable billing')
def disable_billing(self, request):
yuyu_client.post(request, f"invoice/disable_billing/", {})
def reset_billing(self, request):
yuyu_client.post(request, f"invoice/reset_billing/", {})
def finish_invoice(self, request, id):
response = yuyu_client.get(request, f"invoice/{id}/finish/")
data = response.json()
return data
def rollback_to_unpaid_invoice(self, request, id):
response = yuyu_client.get(request, f"invoice/{id}/rollback_to_unpaid/")
data = response.json()
return data

View file

@ -0,0 +1,41 @@
from django.conf import settings
from djmoney.money import Money
from openstack_dashboard.dashboards.yuyu.core import yuyu_client
class PricingUseCase:
pricing_name = ""
def clean_price_response(self, response_data):
response_data["hourly_price"] = Money(amount=response_data['hourly_price'],
currency=response_data['hourly_price_currency'])
if response_data['monthly_price'] and response_data['monthly_price_currency']:
response_data["monthly_price"] = Money(amount=response_data['monthly_price'],
currency=response_data['monthly_price_currency'])
else:
response_data["monthly_price"] = Money(currency=settings.DEFAULT_CURRENCY)
return response_data
def create(self, request, payload):
return yuyu_client.post(request, f"price/{self.pricing_name}/", payload).json()
def update(self, request, id, payload):
return yuyu_client.patch(request, f"price/{self.pricing_name}/{id}/", payload).json()
def delete(self, request, id):
return yuyu_client.delete(request, f"price/{self.pricing_name}/{id}/")
def list(self, request):
response = yuyu_client.get(request, f"price/{self.pricing_name}/")
data = list(map(lambda f: self.clean_price_response(f), response.json()))
return data
def get(self, request, id):
data = yuyu_client.get(request, f"price/{self.pricing_name}/{id}/").json()
data = self.clean_price_response(data)
return data
def has_missing_price(self, request):
return len(self.list(request)) == 0

View file

@ -0,0 +1,19 @@
from openstack_dashboard.dashboards.yuyu.core import yuyu_client
class ProjectOverviewUseCase:
def total_resource(self, request):
response = yuyu_client.get(request, f"project_overview/total_resource/?tenant_id={request.user.tenant_id}")
return response.json()
def active_resource(self, request):
response = yuyu_client.get(request, f"project_overview/active_resource/?tenant_id={request.user.tenant_id}")
return response.json()
def price_total_resource(self, request):
response = yuyu_client.get(request, f"project_overview/price_total_resource/?tenant_id={request.user.tenant_id}")
return response.json()
def price_active_resource(self, request):
response = yuyu_client.get(request, f"project_overview/price_active_resource/?tenant_id={request.user.tenant_id}")
return response.json()

View file

@ -0,0 +1,5 @@
from openstack_dashboard.dashboards.yuyu.cases.pricing_use_case import PricingUseCase
class RouterPriceUseCase(PricingUseCase):
pricing_name = "router"

View file

@ -0,0 +1,11 @@
from openstack_dashboard.dashboards.yuyu.core import yuyu_client
class SettingUseCase:
def get_settings(self, request):
return yuyu_client.get(request, "settings/").json()
def set_setting(self, request, key, value):
return yuyu_client.patch(request, f"settings/{key}/", {
"value": value
}).json()

View file

@ -0,0 +1,5 @@
from openstack_dashboard.dashboards.yuyu.cases.pricing_use_case import PricingUseCase
class SnapshotPriceUseCase(PricingUseCase):
pricing_name = "snapshot"

View file

@ -0,0 +1,29 @@
from openstack_dashboard import api
from openstack_dashboard.dashboards.yuyu.cases.pricing_use_case import PricingUseCase
class VolumePriceUseCase(PricingUseCase):
pricing_name = "volume"
def list(self, request):
data = list(super().list(request))
for d in data:
d["name"] = api.cinder.volume_type_get(request, d['volume_type_id']).name
return data
def get(self, request, id):
data = super().get(request, id)
data["name"] = api.cinder.volume_type_get(request, data['volume_type_id']).name
return data
def has_missing_price(self, request):
server_data_prices = list(map(lambda d: d['volume_type_id'], super().list(request)))
volume_types = api.cinder.volume_type_list(request)
for f in volume_types:
if f.id not in server_data_prices:
return True
return False

View file

@ -0,0 +1,52 @@
from django.utils.translation import ugettext_lazy as _
from djmoney.forms import MoneyField
from openstack_dashboard.dashboards.yuyu.cases.pricing_use_case import PricingUseCase
from horizon import exceptions
from horizon import forms
from horizon import messages
class BasePriceForm(forms.SelfHandlingForm):
hourly_price = MoneyField(label=_("Hourly Price"))
monthly_price = MoneyField(label=_("Monthly Price"), required=False)
USE_CASE: PricingUseCase = None
NAME = ""
def __init__(self, request, *args, **kwargs):
super().__init__(request, *args, **kwargs)
self.model_id = kwargs.get('initial', {}).get('model_id', None)
def to_payload(self, data):
payload = {
"hourly_price": data['hourly_price'].amount,
"hourly_price_currency": data['hourly_price'].currency.code
}
if data['monthly_price']:
payload['monthly_price'] = data['monthly_price'].amount
payload['monthly_price_currency'] = data['monthly_price'].currency.code
return payload
def handle(self, request, data):
try:
if self.model_id:
result = self.USE_CASE.update(
request=request,
id=self.model_id,
payload=self.to_payload(data)
)
messages.success(request, _(f"Successfully update {self.NAME}"))
else:
result = self.USE_CASE.create(
request=request,
payload=self.to_payload(data)
)
messages.success(request, _(f"Successfully create {self.NAME}"))
return result
except Exception:
mode_str = "update" if self.model_id else "create"
exceptions.handle(request, _(f'Unable to {mode_str} {self.NAME}.'))

View file

@ -0,0 +1,60 @@
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _, ungettext_lazy
from horizon import tables, exceptions
class BaseCreatePrice(tables.LinkAction):
classes = ("ajax-modal",)
icon = "link"
single_data = False
def allowed(self, request, datum):
if self.single_data and len(self.table.data) >= 1:
self.classes = [c for c in self.classes] + ['hidden']
return True
class BaseEditPrice(tables.LinkAction):
classes = ("ajax-modal",)
icon = "pencil"
def get_link_url(self, datum=None):
instance_id = self.table.get_object_id(datum)
return reverse(self.url, args=[instance_id])
class BaseDeletePrice(tables.DeleteAction):
use_case = None
single_action_label = None
plural_action_label = None
def action_present(self, count):
return ungettext_lazy(
"Delete " + self.single_action_label,
"Delete " + self.plural_action_label,
count
)
def action_past(self, count):
return ungettext_lazy(
"Deleted " + self.single_action_label,
"Deleted " + self.plural_action_label,
count
)
def delete(self, request, obj_id):
try:
self.use_case.delete(request, obj_id)
except Exception as e:
print("Exception", e)
exceptions.handle(request, e)
class BasePriceTable(tables.DataTable):
hourly_price = tables.Column('hourly_price', verbose_name=_('Hourly Price'))
monthly_price = tables.Column('monthly_price', verbose_name=_('Monthly Price'))
def get_object_id(self, datum):
return datum['id']

View file

@ -0,0 +1,24 @@
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import tables
from openstack_dashboard.dashboards.yuyu.cases.pricing_use_case import PricingUseCase
class BasePriceIndexView(tables.DataTableView):
USE_CASE: PricingUseCase = None
def has_more_data(self, table):
return self._has_more
def get_data(self):
try:
datas = self.USE_CASE.list(self.request)
self._has_more = False # TODO: Pagination
return datas
except Exception:
self._has_more = False
error_message = _('Unable to get data')
exceptions.handle(self.request, error_message)
return []

View file

View file

@ -0,0 +1,110 @@
from django.utils.translation import ugettext_lazy as _
from horizon import tables
class InstanceCostTable(tables.DataTable):
name = tables.WrappingColumn('name', verbose_name=_('Name'))
flavor = tables.Column('flavor', verbose_name=_('Flavor'))
usage = tables.Column('usage', verbose_name=_('Usage'))
cost = tables.Column('cost', verbose_name=_('Cost'))
def get_object_id(self, datum):
return datum['id']
def get_object_display(self, datum):
return datum['name']
class Meta(object):
name = "instance_cost"
hidden_title = False
verbose_name = _("Instance")
class VolumeCostTable(tables.DataTable):
name = tables.WrappingColumn('name', verbose_name=_('Name'))
type = tables.Column('type', verbose_name=_('Type'))
size = tables.Column('size', verbose_name=_('Size'))
usage = tables.Column('usage', verbose_name=_('Usage'))
cost = tables.Column('cost', verbose_name=_('Cost'))
def get_object_id(self, datum):
return datum['id']
def get_object_display(self, datum):
return datum['name']
class Meta(object):
name = "volume_cost"
hidden_title = False
verbose_name = _("Volume")
class FloatingIpCostTable(tables.DataTable):
name = tables.WrappingColumn('name', verbose_name=_('Name'))
usage = tables.Column('usage', verbose_name=_('Usage'))
cost = tables.Column('cost', verbose_name=_('Cost'))
def get_object_id(self, datum):
return datum['id']
def get_object_display(self, datum):
return datum['name']
class Meta(object):
name = "floating_ip_cost"
hidden_title = False
verbose_name = _("Floating IP")
class RouterCostTable(tables.DataTable):
name = tables.WrappingColumn('name', verbose_name=_('Name'))
usage = tables.Column('usage', verbose_name=_('Usage'))
cost = tables.Column('cost', verbose_name=_('Cost'))
def get_object_id(self, datum):
return datum['id']
def get_object_display(self, datum):
return datum['name']
class Meta(object):
name = "router_cost"
hidden_title = False
verbose_name = _("Router")
class SnapshotCostTable(tables.DataTable):
name = tables.WrappingColumn('name', verbose_name=_('Name'))
size = tables.Column('size', verbose_name=_('Size'))
usage = tables.Column('usage', verbose_name=_('Usage'))
cost = tables.Column('cost', verbose_name=_('Cost'))
def get_object_id(self, datum):
return datum['id']
def get_object_display(self, datum):
return datum['name']
class Meta(object):
name = "snapshot_cost"
hidden_title = False
verbose_name = _("Snapshot")
class ImageCostTable(tables.DataTable):
name = tables.WrappingColumn('name', verbose_name=_('Name'))
size = tables.Column('size', verbose_name=_('Size'))
usage = tables.Column('usage', verbose_name=_('Usage'))
cost = tables.Column('cost', verbose_name=_('Cost'))
def get_object_id(self, datum):
return datum['id']
def get_object_display(self, datum):
return datum['name']
class Meta(object):
name = "image_cost"
hidden_title = False
verbose_name = _("Image")

View file

View file

@ -0,0 +1,7 @@
def state_to_text(state):
if state == 1:
return 'In Progress'
if state == 2:
return 'Unpaid'
if state == 100:
return 'Finished'

View file

@ -0,0 +1,28 @@
from openstack_dashboard.dashboards.yuyu.cases.flavor_price_use_case import FlavorPriceUseCase
from openstack_dashboard.dashboards.yuyu.cases.floating_ip_price_use_case import FloatingIpPriceUseCase
from openstack_dashboard.dashboards.yuyu.cases.image_price_use_case import ImagePriceUseCase
from openstack_dashboard.dashboards.yuyu.cases.router_price_use_case import RouterPriceUseCase
from openstack_dashboard.dashboards.yuyu.cases.volume_price_use_case import VolumePriceUseCase
def has_missing_price(request):
flavor_price_uc = FlavorPriceUseCase()
volume_price_uc = VolumePriceUseCase()
fip_price_uc = FloatingIpPriceUseCase()
router_price_uc = RouterPriceUseCase()
snapshot_price_uc = RouterPriceUseCase()
image_price_uc = ImagePriceUseCase()
context = {
'flavor': flavor_price_uc.has_missing_price(request),
'volume': volume_price_uc.has_missing_price(request),
'fip': fip_price_uc.has_missing_price(request),
'router': router_price_uc.has_missing_price(request),
'snapshot': snapshot_price_uc.has_missing_price(request),
'image': image_price_uc.has_missing_price(request),
}
context['has_missing'] = context['flavor'] or context['volume'] or context['fip'] or context['router'] or context[
'snapshot'] or context['image']
return context

26
yuyu/core/yuyu_client.py Normal file
View file

@ -0,0 +1,26 @@
import requests
from django.conf import settings
def _get_header(request):
return {}
def _yuyu_url(request, path):
return settings.YUYU_URL + "/api/" + path
def get(request, path):
return requests.get(_yuyu_url(request, path), headers=_get_header(request))
def post(request, path, payload):
return requests.post(_yuyu_url(request, path), headers=_get_header(request), json=payload)
def patch(request, path, payload):
return requests.patch(_yuyu_url(request, path), headers=_get_header(request), json=payload)
def delete(request, path):
return requests.delete(_yuyu_url(request, path), headers=_get_header(request))

View file

@ -0,0 +1,4 @@
ADD_INSTALLED_APPS = [
'djmoney',
'openstack_dashboard.dashboards.yuyu',
]

View file

@ -0,0 +1,8 @@
from django.utils.translation import ugettext_lazy as _
# The slug of the panel group to be added to HORIZON_CONFIG. Required.
PANEL_GROUP = 'billing'
# The display name of the PANEL_GROUP. Required.
PANEL_GROUP_NAME = _('Billing')
# The slug of the dashboard the PANEL_GROUP associated with. Required.
PANEL_GROUP_DASHBOARD = 'admin'

View file

@ -0,0 +1,9 @@
# The slug of the panel to be added to HORIZON_CONFIG. Required.
PANEL = 'billing_overview'
# The slug of the dashboard the PANEL associated with. Required.
PANEL_DASHBOARD = 'admin'
# The slug of the panel group the PANEL is associated with.
PANEL_GROUP = 'billing'
# Python panel class of the PANEL to be added.
ADD_PANEL = 'openstack_dashboard.dashboards.yuyu.admin.billing_overview.panel.BillingOverview'

View file

@ -0,0 +1,9 @@
# The slug of the panel to be added to HORIZON_CONFIG. Required.
PANEL = 'price_configuration'
# The slug of the dashboard the PANEL associated with. Required.
PANEL_DASHBOARD = 'admin'
# The slug of the panel group the PANEL is associated with.
PANEL_GROUP = 'billing'
# Python panel class of the PANEL to be added.
ADD_PANEL = 'openstack_dashboard.dashboards.yuyu.admin.price_configuration.panel.PriceConfiguration'

View file

@ -0,0 +1,9 @@
# The slug of the panel to be added to HORIZON_CONFIG. Required.
PANEL = 'billing_setting'
# The slug of the dashboard the PANEL associated with. Required.
PANEL_DASHBOARD = 'admin'
# The slug of the panel group the PANEL is associated with.
PANEL_GROUP = 'billing'
# Python panel class of the PANEL to be added.
ADD_PANEL = 'openstack_dashboard.dashboards.yuyu.admin.billing_setting.panel.BillingSetting'

View file

@ -0,0 +1,9 @@
# The slug of the panel to be added to HORIZON_CONFIG. Required.
PANEL = 'projects_invoice'
# The slug of the dashboard the PANEL associated with. Required.
PANEL_DASHBOARD = 'admin'
# The slug of the panel group the PANEL is associated with.
PANEL_GROUP = 'billing'
# Python panel class of the PANEL to be added.
ADD_PANEL = 'openstack_dashboard.dashboards.yuyu.admin.projects_invoice.panel.ProjectsInvoice'

View file

@ -0,0 +1,8 @@
from django.utils.translation import ugettext_lazy as _
# The slug of the panel group to be added to HORIZON_CONFIG. Required.
PANEL_GROUP = 'billing'
# The display name of the PANEL_GROUP. Required.
PANEL_GROUP_NAME = _('Billing')
# The slug of the dashboard the PANEL_GROUP associated with. Required.
PANEL_GROUP_DASHBOARD = 'project'

View file

@ -0,0 +1,9 @@
# The slug of the panel to be added to HORIZON_CONFIG. Required.
PANEL = 'project_billing_overview'
# The slug of the dashboard the PANEL associated with. Required.
PANEL_DASHBOARD = 'project'
# The slug of the panel group the PANEL is associated with.
PANEL_GROUP = 'billing'
# Python panel class of the PANEL to be added.
ADD_PANEL = 'openstack_dashboard.dashboards.yuyu.project.project_billing_overview.panel.ProjectBillingOverview'

View file

@ -0,0 +1,9 @@
# The slug of the panel to be added to HORIZON_CONFIG. Required.
PANEL = 'usage_cost'
# The slug of the dashboard the PANEL associated with. Required.
PANEL_DASHBOARD = 'project'
# The slug of the panel group the PANEL is associated with.
PANEL_GROUP = 'billing'
# Python panel class of the PANEL to be added.
ADD_PANEL = 'openstack_dashboard.dashboards.yuyu.project.usage_cost.panel.UsageCost'

View file

@ -0,0 +1,9 @@
# The slug of the panel to be added to HORIZON_CONFIG. Required.
PANEL = 'invoice'
# The slug of the dashboard the PANEL associated with. Required.
PANEL_DASHBOARD = 'project'
# The slug of the panel group the PANEL is associated with.
PANEL_GROUP = 'billing'
# Python panel class of the PANEL to be added.
ADD_PANEL = 'openstack_dashboard.dashboards.yuyu.project.invoice.panel.Invoice'

View file

View file

@ -0,0 +1,20 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.utils.translation import ugettext_lazy as _
import horizon
class Invoice(horizon.Panel):
name = _("Invoices")
slug = "invoice"

View file

@ -0,0 +1,31 @@
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from horizon import tables
class DetailAction(tables.LinkAction):
name = "detail"
verbose_name = "Detail"
def get_link_url(self, datum=None, *args, **kwargs):
print(datum, args, kwargs)
return reverse("horizon:project:invoice:download_pdf", kwargs={"id": datum['id']})
class InvoiceTable(tables.DataTable):
date = tables.WrappingColumn('date', verbose_name=_('Date'))
state = tables.WrappingColumn('state', verbose_name=_('State'))
total = tables.Column('total', verbose_name=_('Total'))
def get_object_id(self, datum):
return datum['id']
def get_object_display(self, datum):
return datum['date']
class Meta(object):
name = "invoice_table"
hidden_title = True
verbose_name = _("Invoice")
row_actions = (DetailAction, )

View file

@ -0,0 +1,116 @@
{% extends 'base.html' %}
{% block title %}{{ page_title }}{% endblock %}
{% block main %}
<button onclick="javascript:downloadPdf();" class="btn btn-default">Download PDF</button>
<br/>
<br/>
<div id="invoice">
<div>
<dl>
<dt>Invoice Month</dt>
<dd>
<h3>{{ invoice.start_date|date:"M Y" }}</h3>
</dd>
</dl>
<h5>Invoice State: {{ invoice.state_text }}</h5>
</div>
<table class="table table-striped">
<thead>
<tr>
<th>Component</th>
<th>Total Cost</th>
</tr>
</thead>
<tbody>
<tr>
<td>Instance</td>
<td>{{ instance_cost }}</td>
</tr>
<tr>
<td>Volume</td>
<td>{{ volume_cost }}</td>
</tr>
<tr>
<td>Floating IP</td>
<td>{{ fip_cost }}</td>
</tr>
<tr>
<td>Router</td>
<td>{{ router_cost }}</td>
</tr>
<tr>
<td>Snapshot</td>
<td>{{ snapshot_cost }}</td>
</tr>
<tr>
<td>Image</td>
<td>{{ image_cost }}</td>
</tr>
<tr>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td><b>Subtotal:</b> {{ invoice.subtotal_money }}</td>
</tr>
{% if invoice.state != 1 %}
<tr>
<td></td>
<td><b>Tax:</b> {{ invoice.tax_money }}</td>
</tr>
<tr>
<td></td>
<td><b>Total:</b> {{ invoice.total_money }}</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
{% endblock %}
{% block js %}
{{ block.super }}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/1.3.2/jspdf.min.js"></script>
<script type="text/javascript">
function downloadPdf() {
var pdf = new jsPDF('p', 'pt', 'letter');
// source can be HTML-formatted string, or a reference
// to an actual DOM element from which the text will be scraped.
source = $('#invoice')[0];
// we support special element handlers. Register them with jQuery-style
// ID selector for either ID or node name. ("#iAmID", "div", "span" etc.)
// There is no support for any other type of selectors
// (class, of compound) at this time.
specialElementHandlers = {
// element with id of "bypass" - jQuery style selector
'#bypassme': function (element, renderer) {
// true = "handled elsewhere, bypass text extraction"
return true
}
};
margins = {
top: 80,
bottom: 60,
left: 40,
width: 522
};
// all coords and widths are in jsPDF instance's declared units
// 'inches' in this case
pdf.fromHTML(
source, // HTML string or DOM elem ref.
margins.left, // x coord
margins.top, { // y coord
'width': margins.width, // max width of content on PDF
'elementHandlers': specialElementHandlers
},
function (dispose) {
// dispose: object with X, Y of the last line add to the PDF
// this allow the insertion of new lines after html
pdf.save('invoice.pdf');
}, margins);
}
</script>
{% endblock %}

View file

@ -0,0 +1,17 @@
{% load horizon %}
{% jstemplate %}
{% extends 'base.html' %}
{% load i18n %}
{% block title %}
{% trans "Price_Volume_Panel" %}
{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Price_Volume_Panel") %}
{% endblock page_header %}
{% block main %}
{% endblock %}
{% endjstemplate %}

View file

@ -0,0 +1,9 @@
{% extends 'base.html' %}
{% block title %}{{ page_title }}{% endblock %}
{% block main %}
{{ table.render }}
{% endblock %}
{% block js %}
{{ block.super }}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/1.3.2/jspdf.min.js"></script>
{% endblock %}

View file

@ -0,0 +1,21 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.conf.urls import url
from openstack_dashboard.dashboards.yuyu.project.invoice import views
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^invoice/pdf/(?P<id>[^/]+)/$', views.InvoiceView.as_view(), name='download_pdf'),
]

View file

@ -0,0 +1,82 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import datetime
import dateutil.parser
from dateutil.tz import tzutc
from django.template.defaultfilters import date
from django.utils.timesince import timesince
from django.utils import formats
from django.utils.translation import ugettext_lazy as _
from djmoney.money import Money
from horizon import exceptions
from horizon import tables
from horizon import views
from openstack_dashboard import api
from openstack_dashboard.dashboards.yuyu.cases.invoice_use_case import InvoiceUseCase
from .tables import InvoiceTable
from ...core.utils.invoice_utils import state_to_text
class IndexView(tables.DataTableView):
table_class = InvoiceTable
page_title = _("Invoice")
template_name = "project/invoice/invoice_table.html"
invoice_uc = InvoiceUseCase()
def get_data(self):
try:
data = []
for d in self.invoice_uc.get_simple_list(self.request):
data.append({
'id': d['id'],
'date': formats.date_format(d['start_date'], 'M Y'),
'state': state_to_text(d['state']),
'total': d['total_money'] or d['subtotal_money']
})
return list(data)
except Exception:
error_message = _('Unable to get invoice')
exceptions.handle(self.request, error_message)
return []
class InvoiceView(views.APIView):
page_title = _("Invoice")
template_name = "project/invoice/download_pdf.html"
invoice_uc = InvoiceUseCase()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
invoice = self.invoice_uc.get_invoice(self.request, self.kwargs['id'])
context['invoice'] = invoice
context['instance_cost'] = self.get_sum_price(invoice, 'instances')
context['volume_cost'] = self.get_sum_price(invoice, 'volumes')
context['fip_cost'] = self.get_sum_price(invoice, 'floating_ips')
context['router_cost'] = self.get_sum_price(invoice, 'routers')
context['snapshot_cost'] = self.get_sum_price(invoice, 'snapshots')
context['image_cost'] = self.get_sum_price(invoice, 'images')
return context
def get_sum_price(self, invoice, key):
instance_prices = map(
lambda x: Money(amount=x['price_charged'], currency=x['price_charged_currency']),
invoice.get(key, [])
)
return sum(instance_prices)

View file

@ -0,0 +1,20 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.utils.translation import ugettext_lazy as _
import horizon
class ProjectBillingOverview(horizon.Panel):
name = _("Billing Overview")
slug = "project_billing_overview"

View file

@ -0,0 +1,132 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Billing Setting" %}{% endblock %}
{% block main %}
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">This Month Total Resource Allocation</h3>
</div>
<div class="panel-body">
<canvas id="totalResourceChart" height="300"></canvas>
</div>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Active Resource Allocation</h3>
</div>
<div class="panel-body">
<canvas id="activeResourceChart" height="300"></canvas>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">This Month All Resource Price Charged</h3>
</div>
<div class="panel-body">
<canvas id="totalResourcePriceChart" height="300"></canvas>
</div>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Active Resource Price Charged</h3>
</div>
<div class="panel-body">
<canvas id="activeResourcePriceChart" height="300"></canvas>
</div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.0/chart.min.js"
integrity="sha512-TW5s0IT/IppJtu76UbysrBH9Hy/5X41OTAbQuffZFU6lQ1rdcLHzpU5BzVvr/YFykoiMYZVWlr/PX1mDcfM9Qg=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script type="text/javascript">
function showBarChart(id, title, data) {
const ctx = document.getElementById(id).getContext('2d');
const myChart = new Chart(ctx, {
type: 'bar',
data: {
labels: data.label,
datasets: [{
label: title,
data: data.data,
backgroundColor: [
'rgba(255, 99, 132, 0.2)',
'rgba(54, 162, 235, 0.2)',
'rgba(255, 206, 86, 0.2)',
'rgba(75, 192, 192, 0.2)',
'rgba(153, 102, 255, 0.2)',
'rgba(255, 159, 64, 0.2)'
],
borderColor: [
'rgba(255, 99, 132, 1)',
'rgba(54, 162, 235, 1)',
'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)',
'rgba(153, 102, 255, 1)',
'rgba(255, 159, 64, 1)'
],
borderWidth: 1
}]
},
options: {
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
}
}
});
}
function showPieChart(id, title, data) {
const ctx = document.getElementById(id).getContext('2d');
const myChart = new Chart(ctx, {
type: 'pie',
data: {
labels: data.label,
datasets: [{
label: title,
data: data.data,
backgroundColor: [
'rgba(255, 99, 132, 0.2)',
'rgba(54, 162, 235, 0.2)',
'rgba(255, 206, 86, 0.2)',
'rgba(75, 192, 192, 0.2)',
'rgba(153, 102, 255, 0.2)',
'rgba(255, 159, 64, 0.2)'
],
borderColor: [
'rgba(255, 99, 132, 1)',
'rgba(54, 162, 235, 1)',
'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)',
'rgba(153, 102, 255, 1)',
'rgba(255, 159, 64, 1)'
],
borderWidth: 1
}]
},
options: {
maintainAspectRatio: false,
}
});
}
showBarChart('totalResourceChart', 'This Month Total Resource Allocation', JSON.parse('{{ total_resource_json | safe }}'));
showPieChart('activeResourceChart', 'Active Resource Allocation', JSON.parse('{{ active_resource_json | safe }}'));
showBarChart('totalResourcePriceChart', 'This Month All Resource Price Charged', JSON.parse('{{ price_total_resource_json | safe }}'));
showPieChart('activeResourcePriceChart', 'Active Resource Price Charged', JSON.parse('{{ price_active_resource_json | safe }}'));
</script>
{% endblock %}

View file

@ -0,0 +1,19 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
]

View file

@ -0,0 +1,34 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
from django import shortcuts
from django.utils.translation import ugettext_lazy as _
from horizon import views
from openstack_dashboard.dashboards.yuyu.cases.project_overview_use_case import ProjectOverviewUseCase
class IndexView(views.APIView):
page_title = _("Billing Overview")
template_name = "project/project_billing_overview/index.html"
overview_uc = ProjectOverviewUseCase()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['total_resource_json'] = json.dumps(self.overview_uc.total_resource(self.request))
context['active_resource_json'] = json.dumps(self.overview_uc.active_resource(self.request))
context['price_total_resource_json'] = json.dumps(self.overview_uc.price_total_resource(self.request))
context['price_active_resource_json'] = json.dumps(self.overview_uc.price_active_resource(self.request))
return context

View file

View file

@ -0,0 +1,20 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.utils.translation import ugettext_lazy as _
import horizon
class UsageCost(horizon.Panel):
name = _("Usage Cost")
slug = "usage_cost"

View file

@ -0,0 +1,60 @@
{% extends 'base.html' %}
{% block title %}{{ page_title }}{% endblock %}
{% block main %}
{% if invoice %}
<div>
<dl class="dl-horizontal">
<dt>Invoice Month</dt>
<dd>
<select id="invoice_select" onchange="onInvoiceChange(this.value)">
{% for i in invoice_list %}
<option value="{{ i.id }}" {% if i.id == invoice.id %}
selected {% endif %}>{{ i.start_date|date:"M Y" }}</option>
{% endfor %}
</select>
</dd>
<dt>Invoice State</dt>
<dd>{{ invoice.state_text }}</dd>
<dt>Subtotal</dt>
<dd>{{ invoice.subtotal_money }}</dd>
{% if invoice.state != 1 %}
<dt>Tax</dt>
<dd>{{ invoice.tax_money }}</dd>
<dt>Total</dt>
<dd>{{ invoice.total_money }}</dd>
{% endif %}
</dl>
</div>
<div id="instance-cost">
{{ instance_cost_table.render }}
</div>
<div id="volume-cost">
{{ volume_cost_table.render }}
</div>
<div id="floating-ip-cost">
{{ floating_ip_cost_table.render }}
</div>
<div id="router-cost">
{{ router_cost_table.render }}
</div>
<div id="snapshot-cost">
{{ snapshot_cost_table.render }}
</div>
<div id="image-cost">
{{ image_cost_table.render }}
</div>
{% else %}
<h1>Billing not enabled or you don't have any usage yet</h1> <br/>
{% endif %}
{% endblock %}
{% block js %}
{{ block.super }}
<script type="text/javascript">
function onInvoiceChange(val) {
var search = "?invoice_id=" + val;
window.location.href = window.location.protocol + "//" + window.location.host + window.location.pathname + search;
}
</script>
{% endblock %}

View file

@ -0,0 +1,17 @@
{% load horizon %}
{% jstemplate %}
{% extends 'base.html' %}
{% load i18n %}
{% block title %}
{% trans "Price_Volume_Panel" %}
{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Price_Volume_Panel") %}
{% endblock page_header %}
{% block main %}
{% endblock %}
{% endjstemplate %}

View file

@ -0,0 +1,19 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from horizon.test import helpers as test
class Price_Volume_PanelTests(test.TestCase):
# Unit tests for price_volume_panel.
def test_me(self):
self.assertTrue(1 + 1 == 2)

View file

@ -0,0 +1,20 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.conf.urls import url
from openstack_dashboard.dashboards.yuyu.project.usage_cost import views
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
]

View file

@ -0,0 +1,169 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import dateutil.parser
from django.utils.timesince import timesince
from django.utils.translation import ugettext_lazy as _
from djmoney.money import Money
from horizon import exceptions
from horizon import tables
from openstack_dashboard import api
from openstack_dashboard.dashboards.yuyu.cases.invoice_use_case import InvoiceUseCase
from openstack_dashboard.dashboards.yuyu.core.usage_cost.tables import InstanceCostTable, VolumeCostTable, \
FloatingIpCostTable, RouterCostTable, SnapshotCostTable, ImageCostTable
class IndexView(tables.MultiTableView):
table_classes = (
InstanceCostTable, VolumeCostTable, FloatingIpCostTable, RouterCostTable, SnapshotCostTable, ImageCostTable)
page_title = _("Usage Cost")
template_name = "project/usage_cost/cost_tables.html"
invoice_uc = InvoiceUseCase()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def get(self, request, *args, **kwargs):
request.invoice_list = self.invoice_uc.get_simple_list(self.request)
request.has_invoice = len(request.invoice_list) != 0
invoice_id = self.request.GET.get('invoice_id', None)
if not invoice_id and request.has_invoice:
invoice_id = request.invoice_list[0]['id']
request.invoice = self.invoice_uc.get_invoice(self.request, invoice_id) if request.has_invoice else {}
return super(IndexView, self).get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['invoice_list'] = self.request.invoice_list
context['invoice'] = self.request.invoice
return context
def get_instance_cost_data(self):
try:
datas = map(lambda x: {
"id": x['id'],
"name": x['name'],
"flavor": api.nova.flavor_get(self.request, x['flavor_id']).name,
"usage": timesince(
dateutil.parser.isoparse(x['start_date']),
dateutil.parser.isoparse(x['adjusted_end_date'])
),
"cost": Money(amount=x['price_charged'], currency=x['price_charged_currency'])
}, self.request.invoice.get('instances', []))
return datas
except Exception:
error_message = _('Unable to get instance cost')
exceptions.handle(self.request, error_message)
return []
def get_volume_cost_data(self):
try:
datas = map(lambda x: {
"id": x['id'],
"name": x['volume_name'],
"usage": timesince(
dateutil.parser.isoparse(x['start_date']),
dateutil.parser.isoparse(x['adjusted_end_date'])
),
'type': api.cinder.volume_type_get(self.request, x['volume_type_id']).name,
'size': x['space_allocation_gb'],
"cost": Money(amount=x['price_charged'], currency=x['price_charged_currency'])
}, self.request.invoice.get('volumes', []))
return datas
except Exception:
error_message = _('Unable to get volume cost')
exceptions.handle(self.request, error_message)
return []
def get_floating_ip_cost_data(self):
try:
datas = map(lambda x: {
"id": x['id'],
"name": x['ip'],
"usage": timesince(
dateutil.parser.isoparse(x['start_date']),
dateutil.parser.isoparse(x['adjusted_end_date'])
),
"cost": Money(amount=x['price_charged'], currency=x['price_charged_currency'])
}, self.request.invoice.get('floating_ips', []))
return datas
except Exception:
error_message = _('Unable to get floating ip cost')
exceptions.handle(self.request, error_message)
return []
def get_router_cost_data(self):
try:
datas = map(lambda x: {
"id": x['id'],
"name": x['name'],
"usage": timesince(
dateutil.parser.isoparse(x['start_date']),
dateutil.parser.isoparse(x['adjusted_end_date'])
),
"cost": Money(amount=x['price_charged'], currency=x['price_charged_currency'])
}, self.request.invoice.get('routers', []))
return datas
except Exception:
error_message = _('Unable to get router cost')
exceptions.handle(self.request, error_message)
return []
def get_snapshot_cost_data(self):
try:
datas = map(lambda x: {
"id": x['id'],
"name": x['name'],
'size': x['space_allocation_gb'],
"usage": timesince(
dateutil.parser.isoparse(x['start_date']),
dateutil.parser.isoparse(x['adjusted_end_date'])
),
"cost": Money(amount=x['price_charged'], currency=x['price_charged_currency'])
}, self.request.invoice.get('snapshots', []))
return datas
except Exception:
error_message = _('Unable to get snapshot cost')
exceptions.handle(self.request, error_message)
return []
def get_image_cost_data(self):
try:
datas = map(lambda x: {
"id": x['id'],
"name": x['name'],
'size': x['space_allocation_gb'],
"usage": timesince(
dateutil.parser.isoparse(x['start_date']),
dateutil.parser.isoparse(x['adjusted_end_date'])
),
"cost": Money(amount=x['price_charged'], currency=x['price_charged_currency'])
}, self.request.invoice.get('images', []))
return datas
except Exception:
error_message = _('Unable to get images cost')
exceptions.handle(self.request, error_message)
return []