commit 8fbcba397625b052676badfe984e9d0b53ac580d Author: Setyo Nugroho Date: Fri May 13 14:01:58 2022 +0700 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c65b72e --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..eec2f53 --- /dev/null +++ b/README.md @@ -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. diff --git a/remove_yuyu.sh b/remove_yuyu.sh new file mode 100755 index 0000000..d11b001 --- /dev/null +++ b/remove_yuyu.sh @@ -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" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..bdbe5de --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +django-money==2.0.1 +python-dateutil==2.8.2 \ No newline at end of file diff --git a/setup_yuyu.sh b/setup_yuyu.sh new file mode 100755 index 0000000..22a57e8 --- /dev/null +++ b/setup_yuyu.sh @@ -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" \ No newline at end of file diff --git a/yuyu/__init__.py b/yuyu/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yuyu/admin/billing_overview/__init__.py b/yuyu/admin/billing_overview/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yuyu/admin/billing_overview/panel.py b/yuyu/admin/billing_overview/panel.py new file mode 100644 index 0000000..591dc52 --- /dev/null +++ b/yuyu/admin/billing_overview/panel.py @@ -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" diff --git a/yuyu/admin/billing_overview/templates/billing_overview/index.html b/yuyu/admin/billing_overview/templates/billing_overview/index.html new file mode 100644 index 0000000..f92a722 --- /dev/null +++ b/yuyu/admin/billing_overview/templates/billing_overview/index.html @@ -0,0 +1,132 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Billing Setting" %}{% endblock %} + +{% block main %} +
+
+
+
+

This Month Total Resource Allocation

+
+
+ +
+
+
+
+
+
+

Active Resource Allocation

+
+
+ +
+
+
+
+
+
+
+
+

This Month All Resource Price Charged

+
+
+ +
+
+
+
+
+
+

Active Resource Price Charged

+
+
+ +
+
+
+
+ + +{% endblock %} diff --git a/yuyu/admin/billing_overview/urls.py b/yuyu/admin/billing_overview/urls.py new file mode 100644 index 0000000..205e24a --- /dev/null +++ b/yuyu/admin/billing_overview/urls.py @@ -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'), +] diff --git a/yuyu/admin/billing_overview/views.py b/yuyu/admin/billing_overview/views.py new file mode 100644 index 0000000..87ae1b7 --- /dev/null +++ b/yuyu/admin/billing_overview/views.py @@ -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 diff --git a/yuyu/admin/billing_setting/__init__.py b/yuyu/admin/billing_setting/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yuyu/admin/billing_setting/panel.py b/yuyu/admin/billing_setting/panel.py new file mode 100644 index 0000000..f126d90 --- /dev/null +++ b/yuyu/admin/billing_setting/panel.py @@ -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" diff --git a/yuyu/admin/billing_setting/templates/billing_setting/index.html b/yuyu/admin/billing_setting/templates/billing_setting/index.html new file mode 100644 index 0000000..f680740 --- /dev/null +++ b/yuyu/admin/billing_setting/templates/billing_setting/index.html @@ -0,0 +1,22 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Billing Setting" %}{% endblock %} + +{% block main %} + {% include "admin/price_configuration/missing_prices.html" %} + +
+
+ {% if setting.billing_enabled %} +

Billing Enabled


+ Disable Billing + {% else %} +

Billing Disabled


+

Please make sure all price is already configured before enable billing

+ + Enable + Reset Billing Data + {% endif %} +
+
+{% endblock %} diff --git a/yuyu/admin/billing_setting/urls.py b/yuyu/admin/billing_setting/urls.py new file mode 100644 index 0000000..af25ef3 --- /dev/null +++ b/yuyu/admin/billing_setting/urls.py @@ -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'), +] + diff --git a/yuyu/admin/billing_setting/views.py b/yuyu/admin/billing_setting/views.py new file mode 100644 index 0000000..797cd4f --- /dev/null +++ b/yuyu/admin/billing_setting/views.py @@ -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") diff --git a/yuyu/admin/price_configuration/__init__.py b/yuyu/admin/price_configuration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yuyu/admin/price_configuration/forms.py b/yuyu/admin/price_configuration/forms.py new file mode 100644 index 0000000..c62a68c --- /dev/null +++ b/yuyu/admin/price_configuration/forms.py @@ -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" diff --git a/yuyu/admin/price_configuration/panel.py b/yuyu/admin/price_configuration/panel.py new file mode 100644 index 0000000..2f570c5 --- /dev/null +++ b/yuyu/admin/price_configuration/panel.py @@ -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" diff --git a/yuyu/admin/price_configuration/tables.py b/yuyu/admin/price_configuration/tables.py new file mode 100644 index 0000000..523228d --- /dev/null +++ b/yuyu/admin/price_configuration/tables.py @@ -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" + ) + ) diff --git a/yuyu/admin/price_configuration/tabs.py b/yuyu/admin/price_configuration/tabs.py new file mode 100644 index 0000000..9d87bc9 --- /dev/null +++ b/yuyu/admin/price_configuration/tabs.py @@ -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 diff --git a/yuyu/admin/price_configuration/templates/price_configuration/_create_flavor.html b/yuyu/admin/price_configuration/templates/price_configuration/_create_flavor.html new file mode 100644 index 0000000..1a4ed2f --- /dev/null +++ b/yuyu/admin/price_configuration/templates/price_configuration/_create_flavor.html @@ -0,0 +1,8 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans 'Create/update a flavor price.' %}

+{% endblock %} + diff --git a/yuyu/admin/price_configuration/templates/price_configuration/_create_floating_ip.html b/yuyu/admin/price_configuration/templates/price_configuration/_create_floating_ip.html new file mode 100644 index 0000000..b50152e --- /dev/null +++ b/yuyu/admin/price_configuration/templates/price_configuration/_create_floating_ip.html @@ -0,0 +1,8 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans 'Create/update a floating IP price for each allocation.' %}

+{% endblock %} + diff --git a/yuyu/admin/price_configuration/templates/price_configuration/_create_image.html b/yuyu/admin/price_configuration/templates/price_configuration/_create_image.html new file mode 100644 index 0000000..2d0267a --- /dev/null +++ b/yuyu/admin/price_configuration/templates/price_configuration/_create_image.html @@ -0,0 +1,8 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans 'Create/update a image price for each 1GB of space.' %}

+{% endblock %} + diff --git a/yuyu/admin/price_configuration/templates/price_configuration/_create_router.html b/yuyu/admin/price_configuration/templates/price_configuration/_create_router.html new file mode 100644 index 0000000..ad0909b --- /dev/null +++ b/yuyu/admin/price_configuration/templates/price_configuration/_create_router.html @@ -0,0 +1,8 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans 'Create/update a router price for each allocation that have external network.' %}

+{% endblock %} + diff --git a/yuyu/admin/price_configuration/templates/price_configuration/_create_snapshot.html b/yuyu/admin/price_configuration/templates/price_configuration/_create_snapshot.html new file mode 100644 index 0000000..75c6509 --- /dev/null +++ b/yuyu/admin/price_configuration/templates/price_configuration/_create_snapshot.html @@ -0,0 +1,8 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans 'Create/update a snapshot price for each 1GB of space.' %}

+{% endblock %} + diff --git a/yuyu/admin/price_configuration/templates/price_configuration/_create_volume.html b/yuyu/admin/price_configuration/templates/price_configuration/_create_volume.html new file mode 100644 index 0000000..2c02866 --- /dev/null +++ b/yuyu/admin/price_configuration/templates/price_configuration/_create_volume.html @@ -0,0 +1,8 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans 'Create/update a volume price for each 1GB of space.' %}

+{% endblock %} + diff --git a/yuyu/admin/price_configuration/templates/price_configuration/create_flavor.html b/yuyu/admin/price_configuration/templates/price_configuration/create_flavor.html new file mode 100644 index 0000000..10c2dc4 --- /dev/null +++ b/yuyu/admin/price_configuration/templates/price_configuration/create_flavor.html @@ -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 %} diff --git a/yuyu/admin/price_configuration/templates/price_configuration/create_floating_ip.html b/yuyu/admin/price_configuration/templates/price_configuration/create_floating_ip.html new file mode 100644 index 0000000..0b11fc2 --- /dev/null +++ b/yuyu/admin/price_configuration/templates/price_configuration/create_floating_ip.html @@ -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 %} diff --git a/yuyu/admin/price_configuration/templates/price_configuration/create_image.html b/yuyu/admin/price_configuration/templates/price_configuration/create_image.html new file mode 100644 index 0000000..f3a7516 --- /dev/null +++ b/yuyu/admin/price_configuration/templates/price_configuration/create_image.html @@ -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 %} diff --git a/yuyu/admin/price_configuration/templates/price_configuration/create_router.html b/yuyu/admin/price_configuration/templates/price_configuration/create_router.html new file mode 100644 index 0000000..48ac3c0 --- /dev/null +++ b/yuyu/admin/price_configuration/templates/price_configuration/create_router.html @@ -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 %} diff --git a/yuyu/admin/price_configuration/templates/price_configuration/create_snapshot.html b/yuyu/admin/price_configuration/templates/price_configuration/create_snapshot.html new file mode 100644 index 0000000..f9844b9 --- /dev/null +++ b/yuyu/admin/price_configuration/templates/price_configuration/create_snapshot.html @@ -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 %} diff --git a/yuyu/admin/price_configuration/templates/price_configuration/create_volume.html b/yuyu/admin/price_configuration/templates/price_configuration/create_volume.html new file mode 100644 index 0000000..f7dbafa --- /dev/null +++ b/yuyu/admin/price_configuration/templates/price_configuration/create_volume.html @@ -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 %} diff --git a/yuyu/admin/price_configuration/templates/price_configuration/index.html b/yuyu/admin/price_configuration/templates/price_configuration/index.html new file mode 100644 index 0000000..e10e9a5 --- /dev/null +++ b/yuyu/admin/price_configuration/templates/price_configuration/index.html @@ -0,0 +1,13 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Price Configuration" %}{% endblock %} + +{% block main %} + {% include "admin/price_configuration/missing_prices.html" %} + +
+
+ {{ tab_group.render }} +
+
+{% endblock %} diff --git a/yuyu/admin/price_configuration/templates/price_configuration/missing_prices.html b/yuyu/admin/price_configuration/templates/price_configuration/missing_prices.html new file mode 100644 index 0000000..cfd7344 --- /dev/null +++ b/yuyu/admin/price_configuration/templates/price_configuration/missing_prices.html @@ -0,0 +1,70 @@ +{% if missing_price.flavor %} +
+
+ +
+
+{% endif %} + + +{% if missing_price.volume %} +
+
+ +
+
+{% endif %} + +{% if missing_price.fip %} +
+
+ +
+
+{% endif %} + +{% if missing_price.router %} +
+
+ +
+
+{% endif %} + +{% if missing_price.snapshot %} +
+
+ +
+
+{% endif %} + +{% if missing_price.image %} +
+
+ +
+
+{% endif %} + + + + diff --git a/yuyu/admin/price_configuration/urls.py b/yuyu/admin/price_configuration/urls.py new file mode 100644 index 0000000..b610602 --- /dev/null +++ b/yuyu/admin/price_configuration/urls.py @@ -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[^/]+)/$', 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[^/]+)/$', 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[^/]+)/$', 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[^/]+)/$', 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[^/]+)/$', 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[^/]+)/$', views.ImagePriceUpdateFormView.as_view(), + name='image_price_update'), +] diff --git a/yuyu/admin/price_configuration/views.py b/yuyu/admin/price_configuration/views.py new file mode 100644 index 0000000..c790299 --- /dev/null +++ b/yuyu/admin/price_configuration/views.py @@ -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'] + } diff --git a/yuyu/admin/projects_invoice/__init__.py b/yuyu/admin/projects_invoice/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yuyu/admin/projects_invoice/panel.py b/yuyu/admin/projects_invoice/panel.py new file mode 100644 index 0000000..8970fb7 --- /dev/null +++ b/yuyu/admin/projects_invoice/panel.py @@ -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" diff --git a/yuyu/admin/projects_invoice/tables.py b/yuyu/admin/projects_invoice/tables.py new file mode 100644 index 0000000..26c99e1 --- /dev/null +++ b/yuyu/admin/projects_invoice/tables.py @@ -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) diff --git a/yuyu/admin/projects_invoice/templates/projects_invoice/cost_tables.html b/yuyu/admin/projects_invoice/templates/projects_invoice/cost_tables.html new file mode 100644 index 0000000..6b40a64 --- /dev/null +++ b/yuyu/admin/projects_invoice/templates/projects_invoice/cost_tables.html @@ -0,0 +1,66 @@ +{% extends 'base.html' %} +{% block title %}{{ page_title }}{% endblock %} +{% block main %} + {% if invoice %} + {% if invoice.state == 2 %} + Set + to Finished + {% endif %} + + {% if invoice.state == 100 %} + Rollback + to Unpaid + {% endif %} +
+
+
+
+
Invoice Month
+
{{ invoice.start_date|date:"M Y" }}
+
Invoice State
+
{{ invoice.state_text }}
+
Subtotal
+
{{ invoice.subtotal_money }}
+ {% if invoice.state != 1 %} +
Tax
+
{{ invoice.tax_money }}
+
Total
+
{{ invoice.total_money }}
+ {% endif %} +
+
+ +
+ {{ instance_cost_table.render }} +
+ +
+ {{ volume_cost_table.render }} +
+
+ {{ floating_ip_cost_table.render }} +
+
+ {{ router_cost_table.render }} +
+
+ {{ snapshot_cost_table.render }} +
+
+ {{ image_cost_table.render }} +
+ {% else %} +

Billing not enabled or you don't have any usage yet


+ {% endif %} +{% endblock %} +{% block js %} + {{ block.super }} + +{% endblock %} \ No newline at end of file diff --git a/yuyu/admin/projects_invoice/templates/projects_invoice/download_pdf.html b/yuyu/admin/projects_invoice/templates/projects_invoice/download_pdf.html new file mode 100644 index 0000000..d39dabb --- /dev/null +++ b/yuyu/admin/projects_invoice/templates/projects_invoice/download_pdf.html @@ -0,0 +1,123 @@ +{% extends 'base.html' %} +{% block title %}{{ page_title }}{% endblock %} +{% block main %} + + {% if invoice.state == 2 %} + Set to Finished + {% endif %} + + {% if invoice.state == 100 %} + Rollback to Unpaid + {% endif %} +
+
+ +
+
+
+
Invoice Month
+
+

{{ invoice.start_date|date:"M Y" }}

+
+
+
Invoice State: {{ invoice.state_text }}
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% if invoice.state != 1 %} + + + + + + + + + {% endif %} + +
ComponentTotal Cost
Instance{{ instance_cost }}
Volume{{ volume_cost }}
Floating IP{{ fip_cost }}
Router{{ router_cost }}
Snapshot{{ snapshot_cost }}
Image{{ image_cost }}
Subtotal: {{ invoice.subtotal_money }}
Tax: {{ invoice.tax_money }}
Total: {{ invoice.total_money }}
+
+{% endblock %} +{% block js %} + {{ block.super }} + + +{% endblock %} \ No newline at end of file diff --git a/yuyu/admin/projects_invoice/templates/projects_invoice/index.html b/yuyu/admin/projects_invoice/templates/projects_invoice/index.html new file mode 100644 index 0000000..aca8d1b --- /dev/null +++ b/yuyu/admin/projects_invoice/templates/projects_invoice/index.html @@ -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 %} diff --git a/yuyu/admin/projects_invoice/templates/projects_invoice/invoice_table.html b/yuyu/admin/projects_invoice/templates/projects_invoice/invoice_table.html new file mode 100644 index 0000000..963f07d --- /dev/null +++ b/yuyu/admin/projects_invoice/templates/projects_invoice/invoice_table.html @@ -0,0 +1,31 @@ +{% extends 'base.html' %} +{% block title %}{{ page_title }}{% endblock %} +{% block main %} +
+
+
Project:
+
+ +
+
+
+ {{ table.render }} +{% endblock %} +{% block js %} + {{ block.super }} + +{% endblock %} \ No newline at end of file diff --git a/yuyu/admin/projects_invoice/urls.py b/yuyu/admin/projects_invoice/urls.py new file mode 100644 index 0000000..4737770 --- /dev/null +++ b/yuyu/admin/projects_invoice/urls.py @@ -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[^/]+)/(?P[^/]+)/$', views.InvoiceView.as_view(), name='download_pdf'), + url(r'^invoice/usage/(?P[^/]+)/(?P[^/]+)/$', views.UsageCostView.as_view(), name='usage_cost'), + url(r'^invoice/finish/(?P[^/]+)/$', views.FinishInvoice.as_view(), name='finish_invoice'), + url(r'^invoice/rollback_to_unpaid/(?P[^/]+)/$', views.RollbackToUnpaidInvoice.as_view(), + name='rollback_to_unpaid'), +] diff --git a/yuyu/admin/projects_invoice/views.py b/yuyu/admin/projects_invoice/views.py new file mode 100644 index 0000000..a5ef938 --- /dev/null +++ b/yuyu/admin/projects_invoice/views.py @@ -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) diff --git a/yuyu/cases/__init__.py b/yuyu/cases/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yuyu/cases/admin_overview_use_case.py b/yuyu/cases/admin_overview_use_case.py new file mode 100644 index 0000000..285f3a6 --- /dev/null +++ b/yuyu/cases/admin_overview_use_case.py @@ -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() diff --git a/yuyu/cases/flavor_price_use_case.py b/yuyu/cases/flavor_price_use_case.py new file mode 100644 index 0000000..f0755ad --- /dev/null +++ b/yuyu/cases/flavor_price_use_case.py @@ -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 \ No newline at end of file diff --git a/yuyu/cases/floating_ip_price_use_case.py b/yuyu/cases/floating_ip_price_use_case.py new file mode 100644 index 0000000..a75f2bb --- /dev/null +++ b/yuyu/cases/floating_ip_price_use_case.py @@ -0,0 +1,5 @@ +from openstack_dashboard.dashboards.yuyu.cases.pricing_use_case import PricingUseCase + + +class FloatingIpPriceUseCase(PricingUseCase): + pricing_name = "floating_ip" diff --git a/yuyu/cases/image_price_use_case.py b/yuyu/cases/image_price_use_case.py new file mode 100644 index 0000000..28a867e --- /dev/null +++ b/yuyu/cases/image_price_use_case.py @@ -0,0 +1,5 @@ +from openstack_dashboard.dashboards.yuyu.cases.pricing_use_case import PricingUseCase + + +class ImagePriceUseCase(PricingUseCase): + pricing_name = "image" diff --git a/yuyu/cases/invoice_use_case.py b/yuyu/cases/invoice_use_case.py new file mode 100644 index 0000000..6847485 --- /dev/null +++ b/yuyu/cases/invoice_use_case.py @@ -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 \ No newline at end of file diff --git a/yuyu/cases/pricing_use_case.py b/yuyu/cases/pricing_use_case.py new file mode 100644 index 0000000..78254ac --- /dev/null +++ b/yuyu/cases/pricing_use_case.py @@ -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 \ No newline at end of file diff --git a/yuyu/cases/project_overview_use_case.py b/yuyu/cases/project_overview_use_case.py new file mode 100644 index 0000000..8f4d312 --- /dev/null +++ b/yuyu/cases/project_overview_use_case.py @@ -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() diff --git a/yuyu/cases/router_price_use_case.py b/yuyu/cases/router_price_use_case.py new file mode 100644 index 0000000..f52a37f --- /dev/null +++ b/yuyu/cases/router_price_use_case.py @@ -0,0 +1,5 @@ +from openstack_dashboard.dashboards.yuyu.cases.pricing_use_case import PricingUseCase + + +class RouterPriceUseCase(PricingUseCase): + pricing_name = "router" diff --git a/yuyu/cases/setting_use_case.py b/yuyu/cases/setting_use_case.py new file mode 100644 index 0000000..d218980 --- /dev/null +++ b/yuyu/cases/setting_use_case.py @@ -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() diff --git a/yuyu/cases/snapshot_price_use_case.py b/yuyu/cases/snapshot_price_use_case.py new file mode 100644 index 0000000..6282f93 --- /dev/null +++ b/yuyu/cases/snapshot_price_use_case.py @@ -0,0 +1,5 @@ +from openstack_dashboard.dashboards.yuyu.cases.pricing_use_case import PricingUseCase + + +class SnapshotPriceUseCase(PricingUseCase): + pricing_name = "snapshot" diff --git a/yuyu/cases/volume_price_use_case.py b/yuyu/cases/volume_price_use_case.py new file mode 100644 index 0000000..5322ee0 --- /dev/null +++ b/yuyu/cases/volume_price_use_case.py @@ -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 diff --git a/yuyu/core/pricing_admin/forms.py b/yuyu/core/pricing_admin/forms.py new file mode 100644 index 0000000..3bcd796 --- /dev/null +++ b/yuyu/core/pricing_admin/forms.py @@ -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}.')) diff --git a/yuyu/core/pricing_admin/tables.py b/yuyu/core/pricing_admin/tables.py new file mode 100644 index 0000000..15ed7ae --- /dev/null +++ b/yuyu/core/pricing_admin/tables.py @@ -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'] diff --git a/yuyu/core/pricing_admin/views.py b/yuyu/core/pricing_admin/views.py new file mode 100644 index 0000000..b95154e --- /dev/null +++ b/yuyu/core/pricing_admin/views.py @@ -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 [] diff --git a/yuyu/core/usage_cost/__init__.py b/yuyu/core/usage_cost/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yuyu/core/usage_cost/tables.py b/yuyu/core/usage_cost/tables.py new file mode 100644 index 0000000..0c8e005 --- /dev/null +++ b/yuyu/core/usage_cost/tables.py @@ -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") diff --git a/yuyu/core/utils/__init__.py b/yuyu/core/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yuyu/core/utils/invoice_utils.py b/yuyu/core/utils/invoice_utils.py new file mode 100644 index 0000000..d633539 --- /dev/null +++ b/yuyu/core/utils/invoice_utils.py @@ -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' diff --git a/yuyu/core/utils/price_checker.py b/yuyu/core/utils/price_checker.py new file mode 100644 index 0000000..51b46f0 --- /dev/null +++ b/yuyu/core/utils/price_checker.py @@ -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 diff --git a/yuyu/core/yuyu_client.py b/yuyu/core/yuyu_client.py new file mode 100644 index 0000000..2bc5a0a --- /dev/null +++ b/yuyu/core/yuyu_client.py @@ -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)) diff --git a/yuyu/local/enabled/_6100_yuyu.py b/yuyu/local/enabled/_6100_yuyu.py new file mode 100644 index 0000000..f64b53b --- /dev/null +++ b/yuyu/local/enabled/_6100_yuyu.py @@ -0,0 +1,4 @@ +ADD_INSTALLED_APPS = [ + 'djmoney', + 'openstack_dashboard.dashboards.yuyu', +] \ No newline at end of file diff --git a/yuyu/local/enabled/_6101_admin_billing_panel_group.py b/yuyu/local/enabled/_6101_admin_billing_panel_group.py new file mode 100644 index 0000000..0d0edef --- /dev/null +++ b/yuyu/local/enabled/_6101_admin_billing_panel_group.py @@ -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' diff --git a/yuyu/local/enabled/_6102_admin_billing_overview.py b/yuyu/local/enabled/_6102_admin_billing_overview.py new file mode 100644 index 0000000..fa6f0e7 --- /dev/null +++ b/yuyu/local/enabled/_6102_admin_billing_overview.py @@ -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' diff --git a/yuyu/local/enabled/_6103_admin_billing_price_configuration.py b/yuyu/local/enabled/_6103_admin_billing_price_configuration.py new file mode 100644 index 0000000..8e41401 --- /dev/null +++ b/yuyu/local/enabled/_6103_admin_billing_price_configuration.py @@ -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' diff --git a/yuyu/local/enabled/_6104_admin_billing_setting.py b/yuyu/local/enabled/_6104_admin_billing_setting.py new file mode 100644 index 0000000..17481c4 --- /dev/null +++ b/yuyu/local/enabled/_6104_admin_billing_setting.py @@ -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' diff --git a/yuyu/local/enabled/_6105_admin_billing_projects_invoice.py b/yuyu/local/enabled/_6105_admin_billing_projects_invoice.py new file mode 100644 index 0000000..c8a93ce --- /dev/null +++ b/yuyu/local/enabled/_6105_admin_billing_projects_invoice.py @@ -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' diff --git a/yuyu/local/enabled/_6111_project_billing_panel_group.py b/yuyu/local/enabled/_6111_project_billing_panel_group.py new file mode 100644 index 0000000..1ee1d64 --- /dev/null +++ b/yuyu/local/enabled/_6111_project_billing_panel_group.py @@ -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' diff --git a/yuyu/local/enabled/_6112_project_billing_overview.py b/yuyu/local/enabled/_6112_project_billing_overview.py new file mode 100644 index 0000000..85f0e94 --- /dev/null +++ b/yuyu/local/enabled/_6112_project_billing_overview.py @@ -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' diff --git a/yuyu/local/enabled/_6113_project_billing_usage_cost.py b/yuyu/local/enabled/_6113_project_billing_usage_cost.py new file mode 100644 index 0000000..03bc234 --- /dev/null +++ b/yuyu/local/enabled/_6113_project_billing_usage_cost.py @@ -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' diff --git a/yuyu/local/enabled/_6114_project_billing_invoice.py b/yuyu/local/enabled/_6114_project_billing_invoice.py new file mode 100644 index 0000000..3c9b9a7 --- /dev/null +++ b/yuyu/local/enabled/_6114_project_billing_invoice.py @@ -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' diff --git a/yuyu/project/invoice/__init__.py b/yuyu/project/invoice/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yuyu/project/invoice/panel.py b/yuyu/project/invoice/panel.py new file mode 100644 index 0000000..90f2605 --- /dev/null +++ b/yuyu/project/invoice/panel.py @@ -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" diff --git a/yuyu/project/invoice/tables.py b/yuyu/project/invoice/tables.py new file mode 100644 index 0000000..18f0f39 --- /dev/null +++ b/yuyu/project/invoice/tables.py @@ -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, ) diff --git a/yuyu/project/invoice/templates/invoice/download_pdf.html b/yuyu/project/invoice/templates/invoice/download_pdf.html new file mode 100644 index 0000000..ee6ee2e --- /dev/null +++ b/yuyu/project/invoice/templates/invoice/download_pdf.html @@ -0,0 +1,116 @@ +{% extends 'base.html' %} +{% block title %}{{ page_title }}{% endblock %} +{% block main %} + +
+
+ +
+
+
+
Invoice Month
+
+

{{ invoice.start_date|date:"M Y" }}

+
+
+
Invoice State: {{ invoice.state_text }}
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% if invoice.state != 1 %} + + + + + + + + + {% endif %} + +
ComponentTotal Cost
Instance{{ instance_cost }}
Volume{{ volume_cost }}
Floating IP{{ fip_cost }}
Router{{ router_cost }}
Snapshot{{ snapshot_cost }}
Image{{ image_cost }}
Subtotal: {{ invoice.subtotal_money }}
Tax: {{ invoice.tax_money }}
Total: {{ invoice.total_money }}
+
+{% endblock %} +{% block js %} + {{ block.super }} + + +{% endblock %} \ No newline at end of file diff --git a/yuyu/project/invoice/templates/invoice/index.html b/yuyu/project/invoice/templates/invoice/index.html new file mode 100644 index 0000000..aca8d1b --- /dev/null +++ b/yuyu/project/invoice/templates/invoice/index.html @@ -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 %} diff --git a/yuyu/project/invoice/templates/invoice/invoice_table.html b/yuyu/project/invoice/templates/invoice/invoice_table.html new file mode 100644 index 0000000..e9f941f --- /dev/null +++ b/yuyu/project/invoice/templates/invoice/invoice_table.html @@ -0,0 +1,9 @@ +{% extends 'base.html' %} +{% block title %}{{ page_title }}{% endblock %} +{% block main %} + {{ table.render }} +{% endblock %} +{% block js %} + {{ block.super }} + +{% endblock %} \ No newline at end of file diff --git a/yuyu/project/invoice/urls.py b/yuyu/project/invoice/urls.py new file mode 100644 index 0000000..ae2dfdf --- /dev/null +++ b/yuyu/project/invoice/urls.py @@ -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[^/]+)/$', views.InvoiceView.as_view(), name='download_pdf'), +] diff --git a/yuyu/project/invoice/views.py b/yuyu/project/invoice/views.py new file mode 100644 index 0000000..425b92c --- /dev/null +++ b/yuyu/project/invoice/views.py @@ -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) \ No newline at end of file diff --git a/yuyu/project/project_billing_overview/__init__.py b/yuyu/project/project_billing_overview/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yuyu/project/project_billing_overview/panel.py b/yuyu/project/project_billing_overview/panel.py new file mode 100644 index 0000000..652e874 --- /dev/null +++ b/yuyu/project/project_billing_overview/panel.py @@ -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" diff --git a/yuyu/project/project_billing_overview/templates/project_billing_overview/index.html b/yuyu/project/project_billing_overview/templates/project_billing_overview/index.html new file mode 100644 index 0000000..f92a722 --- /dev/null +++ b/yuyu/project/project_billing_overview/templates/project_billing_overview/index.html @@ -0,0 +1,132 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Billing Setting" %}{% endblock %} + +{% block main %} +
+
+
+
+

This Month Total Resource Allocation

+
+
+ +
+
+
+
+
+
+

Active Resource Allocation

+
+
+ +
+
+
+
+
+
+
+
+

This Month All Resource Price Charged

+
+
+ +
+
+
+
+
+
+

Active Resource Price Charged

+
+
+ +
+
+
+
+ + +{% endblock %} diff --git a/yuyu/project/project_billing_overview/urls.py b/yuyu/project/project_billing_overview/urls.py new file mode 100644 index 0000000..205e24a --- /dev/null +++ b/yuyu/project/project_billing_overview/urls.py @@ -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'), +] diff --git a/yuyu/project/project_billing_overview/views.py b/yuyu/project/project_billing_overview/views.py new file mode 100644 index 0000000..922858b --- /dev/null +++ b/yuyu/project/project_billing_overview/views.py @@ -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 diff --git a/yuyu/project/usage_cost/__init__.py b/yuyu/project/usage_cost/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yuyu/project/usage_cost/panel.py b/yuyu/project/usage_cost/panel.py new file mode 100644 index 0000000..e967ef0 --- /dev/null +++ b/yuyu/project/usage_cost/panel.py @@ -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" diff --git a/yuyu/project/usage_cost/templates/usage_cost/cost_tables.html b/yuyu/project/usage_cost/templates/usage_cost/cost_tables.html new file mode 100644 index 0000000..1dd362d --- /dev/null +++ b/yuyu/project/usage_cost/templates/usage_cost/cost_tables.html @@ -0,0 +1,60 @@ +{% extends 'base.html' %} +{% block title %}{{ page_title }}{% endblock %} +{% block main %} + {% if invoice %} +
+
+
Invoice Month
+
+ +
+
Invoice State
+
{{ invoice.state_text }}
+
Subtotal
+
{{ invoice.subtotal_money }}
+ {% if invoice.state != 1 %} +
Tax
+
{{ invoice.tax_money }}
+
Total
+
{{ invoice.total_money }}
+ {% endif %} +
+
+ +
+ {{ instance_cost_table.render }} +
+ +
+ {{ volume_cost_table.render }} +
+
+ {{ floating_ip_cost_table.render }} +
+
+ {{ router_cost_table.render }} +
+
+ {{ snapshot_cost_table.render }} +
+
+ {{ image_cost_table.render }} +
+ {% else %} +

Billing not enabled or you don't have any usage yet


+ {% endif %} +{% endblock %} +{% block js %} + {{ block.super }} + +{% endblock %} \ No newline at end of file diff --git a/yuyu/project/usage_cost/templates/usage_cost/index.html b/yuyu/project/usage_cost/templates/usage_cost/index.html new file mode 100644 index 0000000..aca8d1b --- /dev/null +++ b/yuyu/project/usage_cost/templates/usage_cost/index.html @@ -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 %} diff --git a/yuyu/project/usage_cost/tests.py b/yuyu/project/usage_cost/tests.py new file mode 100644 index 0000000..a2f7429 --- /dev/null +++ b/yuyu/project/usage_cost/tests.py @@ -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) diff --git a/yuyu/project/usage_cost/urls.py b/yuyu/project/usage_cost/urls.py new file mode 100644 index 0000000..3892a28 --- /dev/null +++ b/yuyu/project/usage_cost/urls.py @@ -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'), +] diff --git a/yuyu/project/usage_cost/views.py b/yuyu/project/usage_cost/views.py new file mode 100644 index 0000000..6df96de --- /dev/null +++ b/yuyu/project/usage_cost/views.py @@ -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 []