diff --git a/iotronic_ui/api/iotronic.py b/iotronic_ui/api/iotronic.py index 9f69941..cf2c825 100644 --- a/iotronic_ui/api/iotronic.py +++ b/iotronic_ui/api/iotronic.py @@ -80,10 +80,9 @@ def board_delete(request, board_id): def plugin_list(request, detail=None, project=None, with_public=False, all_plugins=False): """List plugins.""" - plugin = iotronicclient(request).plugin() - plugins = plugin.list(detail, project, - with_public=with_public, - all_plugins=all_plugins) + plugins = iotronicclient(request).plugin.list(detail, project, \ + with_public=with_public, \ + all_plugins=all_plugins) return plugins @@ -119,30 +118,115 @@ def plugin_delete(request, plugin_id): # PLUGIN MANAGEMENT (Board Side) def plugin_inject(request, board_id, plugin_id, onboot): """Inject plugin on board(s).""" - plugin_injection = iotronicclient(request).plugin_injection() - plugin = plugin_injection.plugin_inject(board_id, plugin_id, onboot) + plugin = iotronicclient(request).plugin_injection. \ + plugin_inject(board_id, \ + plugin_id, \ + onboot) return plugin def plugin_action(request, board_id, plugin_id, action, params={}): """Start/Stop/Call actions on board(s).""" - plugin_injection = iotronicclient(request).plugin_injection() - plugin = plugin_injection.plugin_action(board_id, - plugin_id, - action, - params) + plugin = iotronicclient(request).plugin_injection. \ + plugin_action(board_id, + plugin_id, + action, + params) return plugin def plugin_remove(request, board_id, plugin_id): """Inject plugin on board(s).""" - plugin_injection = iotronicclient(request).plugin_injection() - plugin = plugin_injection.plugin_remove(board_id, plugin_id) + plugin = iotronicclient(request).plugin_injection. \ + plugin_remove(board_id, plugin_id) return plugin def plugins_on_board(request, board_id): """Plugins on board.""" - plugin_injection = iotronicclient(request).plugin_injection() - plugins = plugin_injection.plugins_on_board(board_id) - return plugins + plugins = iotronicclient(request).plugin_injection. \ + plugins_on_board(board_id) + + detailed_plugins = [] + # fields = {"name", "public", "callable"} + fields = {"name"} + for plugin in plugins: + details = iotronicclient(request).plugin.get(plugin.plugin, fields) + detailed_plugins.append({"name": details._info["name"], + "id": plugin.plugin}) + + return detailed_plugins + + +# SERVICE MANAGEMENT +def service_list(request, detail=None): + """List services.""" + services = iotronicclient(request).service.list(detail) + return services + + +def service_get(request, service_id, fields): + """Get service info.""" + service = iotronicclient(request).service.get(service_id, fields) + return service + + +def service_create(request, name, port, protocol): + """Create service.""" + params = {"name": name, + "port": port, + "protocol": protocol} + service = iotronicclient(request).service.create(**params) + return service + + +def service_update(request, service_id, patch): + """Update service.""" + service = iotronicclient(request).service.update(service_id, patch) + return service + + +def service_delete(request, service_id): + """Delete service.""" + service = iotronicclient(request).service.delete(service_id) + return service + + +def services_on_board(request, board_id, detail=False): + """List services on board.""" + services = iotronicclient(request).exposed_service \ + .services_on_board(board_id) + + if detail: + detailed_services = [] + fields = {"name", "port", "protocol"} + + for service in services: + details = iotronicclient(request). \ + service.get(service._info["service"], fields) + + detailed_services.append({"name": details._info["name"], + "public_port": + service._info["public_port"], + "port": details._info["port"], + "protocol": details._info["protocol"]}) + + return detailed_services + + else: + return services + + +def service_action(request, board_id, service_id, action): + """Action on service.""" + service_action = iotronicclient(request).exposed_service. \ + service_action(board_id, service_id, action) + return service_action + + +def restore_services(request, board_id): + """Restore services.""" + service_restore = iotronicclient(request).exposed_service. \ + restore_services(board_id) + return service_restore + diff --git a/iotronic_ui/enabled/_6030_iot_services_panel.py b/iotronic_ui/enabled/_6030_iot_services_panel.py new file mode 100644 index 0000000..4a07dc3 --- /dev/null +++ b/iotronic_ui/enabled/_6030_iot_services_panel.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. + +# The slug of the panel to be added to HORIZON_CONFIG. Required. +PANEL = 'services' +# The slug of the dashboard the PANEL associated with. Required. +PANEL_DASHBOARD = 'iot' +# The slug of the panel group the PANEL is associated with. +PANEL_GROUP = 'iot' +# If set, it will update the default panel of the PANEL_DASHBOARD. +DEFAULT_PANEL = '' + +# Python panel class of the PANEL to be added. +ADD_PANEL = 'iotronic_ui.iot.services.panel.Services' diff --git a/iotronic_ui/iot/boards/tables.py b/iotronic_ui/iot/boards/tables.py index c2a4bf9..c944caf 100644 --- a/iotronic_ui/iot/boards/tables.py +++ b/iotronic_ui/iot/boards/tables.py @@ -12,6 +12,7 @@ import logging +from django import template from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ungettext_lazy @@ -45,6 +46,24 @@ class EditBoardLink(tables.LinkAction): """ +class RestoreServices(tables.BatchAction): + name = "restoreservices" + + @staticmethod + def action_present(count): + return u"Restore Services" + + @staticmethod + def action_past(count): + return u"Restore Services" + + def allowed(self, request, board=None): + return True + + def action(self, request, board_id): + api.iotronic.restore_services(request, board_id) + + class RemovePluginsLink(tables.LinkAction): name = "removeplugins" verbose_name = _("Remove Plugin(s)") @@ -98,6 +117,13 @@ class BoardFilterAction(tables.FilterAction): return [board for board in boards if q in board.name.lower()] +def show_services(board_info): + template_name = 'iot/boards/_cell_services.html' + context = board_info._info + # LOG.debug("CONTEXT: %s", context) + return template.loader.render_to_string(template_name, + context) + class BoardsTable(tables.DataTable): name = tables.WrappingColumn('name', link="horizon:iot:boards:detail", @@ -107,7 +133,8 @@ class BoardsTable(tables.DataTable): uuid = tables.Column('uuid', verbose_name=_('Board ID')) # code = tables.Column('code', verbose_name=_('Code')) status = tables.Column('status', verbose_name=_('Status')) - location = tables.Column('location', verbose_name=_('Geo')) + # location = tables.Column('location', verbose_name=_('Geo')) + services = tables.Column(show_services, verbose_name=_('Services')) # extra = tables.Column('extra', verbose_name=_('Extra')) # Overriding get_object_id method because in IoT service the "id" is @@ -118,6 +145,7 @@ class BoardsTable(tables.DataTable): class Meta(object): name = "boards" verbose_name = _("boards") - row_actions = (EditBoardLink, RemovePluginsLink, DeleteBoardsAction) - table_actions = (BoardFilterAction, CreateBoardLink, - DeleteBoardsAction) + row_actions = (EditBoardLink, RestoreServices, + RemovePluginsLink, DeleteBoardsAction) + table_actions = (BoardFilterAction, CreateBoardLink, + RestoreServices, DeleteBoardsAction) diff --git a/iotronic_ui/iot/boards/tabs.py b/iotronic_ui/iot/boards/tabs.py index f99d752..07a5528 100644 --- a/iotronic_ui/iot/boards/tabs.py +++ b/iotronic_ui/iot/boards/tabs.py @@ -32,11 +32,15 @@ class OverviewTab(tabs.Tab): template_name = ("iot/boards/_detail_overview.html") def get_context_data(self, request): - coordinates = self.tab_group.kwargs['board'].__dict__["location"][0] - # LOG.debug('IOT INFO: %s', coordinates) + + coordinates = self.tab_group.kwargs['board'].__dict__['location'][0] + services = self.tab_group.kwargs['board']._info['services'] + plugins = self.tab_group.kwargs['board']._info['plugins'] return {"board": self.tab_group.kwargs['board'], "coordinates": coordinates, + "services": services, + "plugins": plugins, "is_superuser": request.user.is_superuser} diff --git a/iotronic_ui/iot/boards/templates/boards/_cell_services.html b/iotronic_ui/iot/boards/templates/boards/_cell_services.html new file mode 100644 index 0000000..65b1370 --- /dev/null +++ b/iotronic_ui/iot/boards/templates/boards/_cell_services.html @@ -0,0 +1,8 @@ +{% load i18n %} +{% if services %} + {% for service in services %} +
{{ service.name }} [{{ service.protocol }}] {{ service.port }} --> {{ service.public_port }}
+ {% endfor %} +{% else %} +
--
+{% endif %} diff --git a/iotronic_ui/iot/boards/templates/boards/_detail_overview.html b/iotronic_ui/iot/boards/templates/boards/_detail_overview.html index d396f88..60afed2 100644 --- a/iotronic_ui/iot/boards/templates/boards/_detail_overview.html +++ b/iotronic_ui/iot/boards/templates/boards/_detail_overview.html @@ -22,6 +22,22 @@
{{ board.mobile }}
{% trans "Extra" %}
{{ board.extra }}
+
{% trans "Services" %}
+ {% if services %} + {% for service in services %} +
{{ service.name }} [{{ service.protocol }}] {{ service.port }} --> {{ service.public_port }}
+ {% endfor %} + {% else %} +
--
+ {% endif %} +
{% trans "Plugins" %}
+ {% if plugins %} + {% for plugin in plugins %} +
{{ plugin.name }}
+ {% endfor %} + {% else %} +
--
+ {% endif %} diff --git a/iotronic_ui/iot/boards/views.py b/iotronic_ui/iot/boards/views.py index 961f222..df8e3af 100644 --- a/iotronic_ui/iot/boards/views.py +++ b/iotronic_ui/iot/boards/views.py @@ -84,6 +84,11 @@ class IndexView(tables.DataTableView): exceptions.handle(self.request, _('Unable to retrieve user boards list.')) + for board in boards: + board_services = iotronic.services_on_board(self.request, board.uuid, True) + + # board.__dict__.update(dict(services=board_services)) + board._info.update(dict(services=board_services)) return boards @@ -116,7 +121,7 @@ class UpdateView(forms.ModalFormView): except Exception: redirect = reverse("horizon:iot:boards:index") exceptions.handle(self.request, - _('Unable to update board.'), + _('Unable to get board information.'), redirect=redirect) def get_context_data(self, **kwargs): @@ -127,8 +132,6 @@ class UpdateView(forms.ModalFormView): def get_initial(self): board = self.get_object() - - # LOG.debug("MELO BOARD INFO: %s", board) location = board.location[0] return {'uuid': board.uuid, @@ -159,7 +162,7 @@ class RemovePluginsView(forms.ModalFormView): except Exception: redirect = reverse("horizon:iot:boards:index") exceptions.handle(self.request, - _('Unable to remove plugin.'), + _('Unable to get board information.'), redirect=redirect) def get_context_data(self, **kwargs): @@ -301,7 +304,18 @@ class DetailView(tabs.TabView): def get_data(self): board_id = self.kwargs['board_id'] try: + + board_services = [] + board_plugins = [] + board = iotronic.board_get(self.request, board_id, None) + board_services = iotronic.services_on_board(self.request, board_id, True) + board._info.update(dict(services=board_services)) + + board_plugins = iotronic.plugins_on_board(self.request, board_id) + board._info.update(dict(plugins=board_plugins)) + # LOG.debug("BOARD: %s\n\n%s", board, board._info) + except Exception: msg = ('Unable to retrieve board %s information') % {'name': board.name} diff --git a/iotronic_ui/iot/dashboard.py b/iotronic_ui/iot/dashboard.py index a84e97e..3660d9b 100644 --- a/iotronic_ui/iot/dashboard.py +++ b/iotronic_ui/iot/dashboard.py @@ -16,9 +16,9 @@ import horizon class Iot(horizon.Dashboard): - name = _("Iot") + name = _("IoT") slug = "iot" - panels = ('boards', 'plugins') # Add your panels here. + panels = ('boards', 'plugins', 'services') # Add your panels here. # Specify the slug of the dashboard's default panel. default_panel = 'boards' diff --git a/iotronic_ui/iot/plugins/views.py b/iotronic_ui/iot/plugins/views.py index 0a1dc6c..1cb3f13 100644 --- a/iotronic_ui/iot/plugins/views.py +++ b/iotronic_ui/iot/plugins/views.py @@ -123,7 +123,7 @@ class InjectView(forms.ModalFormView): except Exception: redirect = reverse("horizon:iot:plugins:index") exceptions.handle(self.request, - _('Unable to inject plugin.'), + _('Unable to get plugin information.'), redirect=redirect) def get_context_data(self, **kwargs): @@ -167,7 +167,7 @@ class StartView(forms.ModalFormView): except Exception: redirect = reverse("horizon:iot:plugins:index") exceptions.handle(self.request, - _('Unable to start plugin.'), + _('Unable to get plugin information.'), redirect=redirect) def get_context_data(self, **kwargs): @@ -211,7 +211,7 @@ class StopView(forms.ModalFormView): except Exception: redirect = reverse("horizon:iot:plugins:index") exceptions.handle(self.request, - _('Unable to stop plugin.'), + _('Unable to get plugin information.'), redirect=redirect) def get_context_data(self, **kwargs): @@ -255,7 +255,7 @@ class CallView(forms.ModalFormView): except Exception: redirect = reverse("horizon:iot:plugins:index") exceptions.handle(self.request, - _('Unable to call plugin.'), + _('Unable to get plugin information.'), redirect=redirect) def get_context_data(self, **kwargs): @@ -299,7 +299,7 @@ class RemoveView(forms.ModalFormView): except Exception: redirect = reverse("horizon:iot:plugins:index") exceptions.handle(self.request, - _('Unable to remove plugin.'), + _('Unable to get plugin information.'), redirect=redirect) def get_context_data(self, **kwargs): @@ -342,7 +342,7 @@ class UpdateView(forms.ModalFormView): except Exception: redirect = reverse("horizon:iot:plugins:index") exceptions.handle(self.request, - _('Unable to update plugin.'), + _('Unable to get plugin information.'), redirect=redirect) def get_context_data(self, **kwargs): diff --git a/iotronic_ui/iot/services/__init__.py b/iotronic_ui/iot/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/iotronic_ui/iot/services/forms.py b/iotronic_ui/iot/services/forms.py new file mode 100644 index 0000000..c8297c3 --- /dev/null +++ b/iotronic_ui/iot/services/forms.py @@ -0,0 +1,201 @@ +# 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 logging + +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import forms +from horizon import messages + +from openstack_dashboard.api import iotronic +from openstack_dashboard import policy + +LOG = logging.getLogger(__name__) + + +class CreateServiceForm(forms.SelfHandlingForm): + name = forms.CharField(label=_("Service Name")) + port = forms.IntegerField( + label=_("Port"), + help_text=_("Service port") + ) + + protocol = forms.ChoiceField( + label=_("Protocol"), + choices=[('TCP', _('TCP')), ('UDP', _('UDP'))], + widget=forms.Select( + attrs={'class': 'switchable', 'data-slug': 'slug-protocol'}, + ) + ) + + def handle(self, request, data): + try: + # LOG.error("DATA: %s", data) + service = iotronic.service_create(request, data["name"], + data["port"], data["protocol"]) + messages.success(request, _("Service created successfully.")) + + return service + except Exception: + exceptions.handle(request, _('Unable to create service.')) + + +class UpdateBoardForm(forms.SelfHandlingForm): + uuid = forms.CharField(label=_("Service ID"), widget=forms.HiddenInput) + name = forms.CharField(label=_("Service Name")) + port = forms.IntegerField(label=_("Port")) + protocol = forms.ChoiceField( + label=_("Protocol"), + choices=[('TCP', _('TCP')), ('UDP', _('UDP'))], + widget=forms.Select( + attrs={'class': 'switchable', 'data-slug': 'slug-protocol'}, + ) + ) + + def __init__(self, *args, **kwargs): + + super(UpdateBoardForm, self).__init__(*args, **kwargs) + + # Admin + if policy.check((("iot", "iot:update_services"),), self.request): + # LOG.debug("MELO ADMIN") + pass + + # Manager or Admin of the iot project + elif (policy.check((("iot", "iot_manager"),), self.request) or + policy.check((("iot", "iot_admin"),), self.request)): + # LOG.debug("MELO NO-edit IOT ADMIN") + pass + + # Other users + else: + if self.request.user.id != kwargs["initial"]["owner"]: + # LOG.debug("MELO IMMUTABLE FIELDS") + self.fields["name"].widget.attrs = {'readonly': 'readonly'} + self.fields["port"].widget.attrs = {'readonly': 'readonly'} + self.fields["protocol"].widget.attrs = {'readonly': 'readonly'} + + def handle(self, request, data): + try: + iotronic.service_update(request, data["uuid"], + {"name": data["name"], + "port": data["port"], + "protocol": data["protocol"]}) + + messages.success(request, _("Service updated successfully.")) + return True + except Exception: + exceptions.handle(request, _('Unable to update service.')) + + +class ServiceActionForm(forms.SelfHandlingForm): + + uuid = forms.CharField(label=_("Plugin ID"), widget=forms.HiddenInput) + + name = forms.CharField( + label=_('Service Name'), + widget=forms.TextInput(attrs={'readonly': 'readonly'}) + ) + + board_list = forms.MultipleChoiceField( + label=_("Boards List"), + widget=forms.SelectMultiple( + attrs={'class': 'switchable', 'data-slug': 'slug-select-boards'}), + help_text=_("Select boards in this pool") + ) + + action = forms.ChoiceField( + label=_("Action"), + choices=[('ServiceEnable', _('Enable')), ('ServiceDisable', _('Disable')), ('ServiceRestore', _('Restore'))], + widget=forms.Select( + attrs={'class': 'switchable', 'data-slug': 'slug-action'}, + ) + ) + + def __init__(self, *args, **kwargs): + + super(ServiceActionForm, self).__init__(*args, **kwargs) + # input=kwargs.get('initial',{}) + + boardslist_length = len(kwargs["initial"]["board_list"]) + + self.fields["board_list"].choices = kwargs["initial"]["board_list"] + # self.fields["board_list"].max_length = boardslist_length + + def handle(self, request, data): + + counter = 0 + + for board in data["board_list"]: + for key, value in self.fields["board_list"].choices: + if key == board: + + try: + action = None + action = iotronic.service_action(request, key, + data["uuid"], + data["action"]) + + message_text = "Action executed successfully on " \ + "board " + str(value) + "." + messages.success(request, _(message_text)) + + if counter != len(data["board_list"]) - 1: + counter += 1 + else: + return action + except Exception: + message_text = "Unable to execute action on board " \ + + str(value) + "." + exceptions.handle(request, _(message_text)) + + break + + +class RemoveServicesForm(forms.SelfHandlingForm): + + uuid = forms.CharField(label=_("Service ID"), widget=forms.HiddenInput) + + name = forms.CharField( + label=_('Service Name'), + widget=forms.TextInput(attrs={'readonly': 'readonly'}) + ) + port = forms.IntegerField( + label=_("Port"), + widget=forms.TextInput(attrs={'readonly': 'readonly'}) + ) + + protocol = forms.ChoiceField( + label=_("Protocol"), + choices=[('TCP', _('TCP')), ('UDP', _('UDP'))], + widget=forms.TextInput(attrs={'readonly': 'readonly'}) + ) + + def __init__(self, *args, **kwargs): + + super(RemoveServicesForm, self).__init__(*args, **kwargs) + # input=kwargs.get('initial',{}) + + + def handle(self, request, data): + + try: + message_text = "Service "+str(data["name"])+" deleted successfully." + + iotronic.service_delete(request, data["uuid"]) + messages.success(request, _(message_text)) + return True + except Exception: + message_text = "Unable to delete service "+str(data["name"])+"." + exceptions.handle(request, _(message_text)) diff --git a/iotronic_ui/iot/services/panel.py b/iotronic_ui/iot/services/panel.py new file mode 100644 index 0000000..aa531a0 --- /dev/null +++ b/iotronic_ui/iot/services/panel.py @@ -0,0 +1,25 @@ +# 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 + +# from openstack_dashboard.api import keystone + + +class Services(horizon.Panel): + name = _("Services") + slug = "services" + permissions = ('openstack.services.iot', ) + # policy_rules = (("iot", "iot:list_all_services"),) + diff --git a/iotronic_ui/iot/services/tables.py b/iotronic_ui/iot/services/tables.py new file mode 100644 index 0000000..d4efc26 --- /dev/null +++ b/iotronic_ui/iot/services/tables.py @@ -0,0 +1,116 @@ +# 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 logging + +from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ungettext_lazy + +from horizon import tables + +from openstack_dashboard import api + +LOG = logging.getLogger(__name__) + + + +class CreateServiceLink(tables.LinkAction): + name = "create" + verbose_name = _("Create Service") + url = "horizon:iot:services:create" + classes = ("ajax-modal",) + icon = "plus" + # policy_rules = (("iot", "iot:create_service"),) + + +class EditBoardLink(tables.LinkAction): + name = "edit" + verbose_name = _("Edit") + url = "horizon:iot:services:update" + classes = ("ajax-modal",) + icon = "pencil" + # policy_rules = (("iot", "iot:update_service"),) + + +""" +class RemoveServicesLink(tables.LinkAction): + name = "remove" + verbose_name = _("Remove Service(s)") + url = "horizon:iot:services:remove" + classes = ("ajax-modal",) + icon = "plus" + # policy_rules = (("iot", "iot:delete_service"),) +""" + + +class ActionServiceLink(tables.LinkAction): + name = "action" + verbose_name = _("Service Action") + url = "horizon:iot:services:action" + classes = ("ajax-modal",) + # icon = "plus" + # policy_rules = (("iot", "iot:service_action"),) + + +class DeleteServicesAction(tables.DeleteAction): + @staticmethod + def action_present(count): + return ungettext_lazy( + u"Delete Service", + u"Delete Services", + count + ) + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Deleted Service", + u"Deleted Services", + count + ) + # policy_rules = (("iot", "iot:delete_service"),) + + def delete(self, request, service_id): + api.iotronic.service_delete(request, service_id) + + +class ServiceFilterAction(tables.FilterAction): + + def filter(self, table, services, filter_string): + # Naive case-insensitive search. + q = filter_string.lower() + return [service for service in services + if q in service.name.lower()] + + +class ServicesTable(tables.DataTable): + name = tables.WrappingColumn('name', link="horizon:iot:services:detail", + verbose_name=_('Service Name')) + protocol = tables.Column('protocol', verbose_name=_('Protocol')) + port = tables.Column('port', verbose_name=_('Port')) + + # Overriding get_object_id method because in IoT service the "id" is + # identified by the field UUID + def get_object_id(self, datum): + return datum.uuid + + class Meta(object): + name = "services" + verbose_name = _("services") + row_actions = (EditBoardLink, ActionServiceLink, + DeleteServicesAction) + table_actions = (ServiceFilterAction, CreateServiceLink, + DeleteServicesAction) + + # row_actions = (EditBoardLink, RemovePluginsLink, DeleteBoardsAction) + # table_actions = (BoardFilterAction, CreateBoardLink, + # DeleteBoardsAction) diff --git a/iotronic_ui/iot/services/tabs.py b/iotronic_ui/iot/services/tabs.py new file mode 100644 index 0000000..5bbf0a5 --- /dev/null +++ b/iotronic_ui/iot/services/tabs.py @@ -0,0 +1,40 @@ +# 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 logging + +# from django.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import tabs + +LOG = logging.getLogger(__name__) + + +class OverviewTab(tabs.Tab): + name = _("Overview") + slug = "overview" + template_name = ("iot/services/_detail_overview.html") + + def get_context_data(self, request): + # coordinates = self.tab_group.kwargs['board'].__dict__["location"][0] + # LOG.debug('IOT INFO: %s', coordinates) + + return {"service": self.tab_group.kwargs['service'], + "is_superuser": request.user.is_superuser} + + +class ServiceDetailTabs(tabs.TabGroup): + slug = "service_details" + # tabs = (OverviewTab, LogTab, ConsoleTab, AuditTab) + tabs = (OverviewTab,) + sticky = True diff --git a/iotronic_ui/iot/services/templates/services/_action.html b/iotronic_ui/iot/services/templates/services/_action.html new file mode 100644 index 0000000..37fb87d --- /dev/null +++ b/iotronic_ui/iot/services/templates/services/_action.html @@ -0,0 +1,7 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Execute action on board(s)." %}

+{% endblock %} diff --git a/iotronic_ui/iot/services/templates/services/_create.html b/iotronic_ui/iot/services/templates/services/_create.html new file mode 100644 index 0000000..9564ea9 --- /dev/null +++ b/iotronic_ui/iot/services/templates/services/_create.html @@ -0,0 +1,8 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Add a new service." %}

+{% endblock %} + diff --git a/iotronic_ui/iot/services/templates/services/_detail_overview.html b/iotronic_ui/iot/services/templates/services/_detail_overview.html new file mode 100644 index 0000000..22eac74 --- /dev/null +++ b/iotronic_ui/iot/services/templates/services/_detail_overview.html @@ -0,0 +1,16 @@ +{% load i18n sizeformat %} + +
+
+
{% trans "Name" %}
+
{{ service.name }}
+
{% trans "ID" %}
+
{{ service.uuid }}
+
{% trans "Protocol" %}
+
{{ service.protocol }}
+
{% trans "Port" %}
+
{{ service.port }}
+
{% trans "Extra" %}
+
{{ service.extra }}
+
+
diff --git a/iotronic_ui/iot/services/templates/services/_remove.html b/iotronic_ui/iot/services/templates/services/_remove.html new file mode 100644 index 0000000..7a23241 --- /dev/null +++ b/iotronic_ui/iot/services/templates/services/_remove.html @@ -0,0 +1,8 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Remove service(s)." %}

+{% endblock %} + diff --git a/iotronic_ui/iot/services/templates/services/_update.html b/iotronic_ui/iot/services/templates/services/_update.html new file mode 100644 index 0000000..d4eace0 --- /dev/null +++ b/iotronic_ui/iot/services/templates/services/_update.html @@ -0,0 +1,7 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Edit the service's details." %}

+{% endblock %} diff --git a/iotronic_ui/iot/services/templates/services/action.html b/iotronic_ui/iot/services/templates/services/action.html new file mode 100644 index 0000000..4cf08a8 --- /dev/null +++ b/iotronic_ui/iot/services/templates/services/action.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Execute Action" %}{% endblock %} + +{% block main %} + {% include 'iot/services/_action.html' %} +{% endblock %} diff --git a/iotronic_ui/iot/services/templates/services/create.html b/iotronic_ui/iot/services/templates/services/create.html new file mode 100644 index 0000000..5aca055 --- /dev/null +++ b/iotronic_ui/iot/services/templates/services/create.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Insert Service" %}{% endblock %} + +{% block main %} + {% include 'iot/services/_create.html' %} +{% endblock %} diff --git a/iotronic_ui/iot/services/templates/services/index.html b/iotronic_ui/iot/services/templates/services/index.html new file mode 100644 index 0000000..0e9a68a --- /dev/null +++ b/iotronic_ui/iot/services/templates/services/index.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Services" %}{% endblock %} + +{% block main %} + {{ table.render }} +{% endblock %} diff --git a/iotronic_ui/iot/services/templates/services/remove.html b/iotronic_ui/iot/services/templates/services/remove.html new file mode 100644 index 0000000..d1addca --- /dev/null +++ b/iotronic_ui/iot/services/templates/services/remove.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Remove service(s)." %}{% endblock %} + +{% block main %} + {% include 'iot/services/_remove.html' %} +{% endblock %} diff --git a/iotronic_ui/iot/services/templates/services/update.html b/iotronic_ui/iot/services/templates/services/update.html new file mode 100644 index 0000000..bb6378f --- /dev/null +++ b/iotronic_ui/iot/services/templates/services/update.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Update Service" %}{% endblock %} + +{% block main %} + {% include 'iot/services/_update.html' %} +{% endblock %} diff --git a/iotronic_ui/iot/services/tests.py b/iotronic_ui/iot/services/tests.py new file mode 100644 index 0000000..0ff0975 --- /dev/null +++ b/iotronic_ui/iot/services/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 ServicesTests(test.TestCase): + # Unit tests for boards. + def test_me(self): + self.assertTrue(1 + 1 == 2) diff --git a/iotronic_ui/iot/services/urls.py b/iotronic_ui/iot/services/urls.py new file mode 100644 index 0000000..b748942 --- /dev/null +++ b/iotronic_ui/iot/services/urls.py @@ -0,0 +1,29 @@ +# 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 iotronic_ui.iot.services import views + + +urlpatterns = [ + url(r'^$', views.IndexView.as_view(), name='index'), + url(r'^create/$', views.CreateView.as_view(), name='create'), + url(r'^(?P[^/]+)/update/$', views.UpdateView.as_view(), + name='update'), + url(r'^(?P[^/]+)/action/$', views.ActionView.as_view(), + name='action'), + url(r'^(?P[^/]+)/remove/$', + views.RemoveServicesView.as_view(), name='remove'), + url(r'^(?P[^/]+)/detail/$', views.ServiceDetailView.as_view(), + name='detail'), +] diff --git a/iotronic_ui/iot/services/views.py b/iotronic_ui/iot/services/views.py new file mode 100644 index 0000000..b17a0f3 --- /dev/null +++ b/iotronic_ui/iot/services/views.py @@ -0,0 +1,242 @@ +# 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 logging + +from django.core.urlresolvers import reverse +from django.core.urlresolvers import reverse_lazy +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import forms +# from horizon import messages +from horizon import tables +from horizon import tabs +from horizon.utils import memoized + +from openstack_dashboard.api import iotronic +from openstack_dashboard import policy + +from iotronic_ui.iot.services import forms as project_forms +from iotronic_ui.iot.services import tables as project_tables +from iotronic_ui.iot.services import tabs as project_tabs + + +LOG = logging.getLogger(__name__) + + +class IndexView(tables.DataTableView): + table_class = project_tables.ServicesTable + template_name = 'iot/services/index.html' + page_title = _("Services") + + def get_data(self): + services = [] + + # Admin + if policy.check((("iot", "iot:list_all_services"),), self.request): + try: + services = iotronic.service_list(self.request, None) + + except Exception: + exceptions.handle(self.request, + _('Unable to retrieve services list.')) + + # Admin_iot_project + elif policy.check((("iot", "iot:list_project_services"),), self.request): + try: + services = iotronic.service_list(self.request, None) + + except Exception: + exceptions.handle(self.request, + _('Unable to retrieve user services list.')) + + # Other users + else: + try: + services = iotronic.service_list(self.request, None) + + except Exception: + exceptions.handle(self.request, + _('Unable to retrieve user services list.')) + + return services + + +class CreateView(forms.ModalFormView): + template_name = 'iot/services/create.html' + modal_header = _("Create Service") + form_id = "create_service_form" + form_class = project_forms.CreateServiceForm + submit_label = _("Create Service") + submit_url = reverse_lazy("horizon:iot:services:create") + success_url = reverse_lazy('horizon:iot:services:index') + page_title = _("Create Service") + + +class UpdateView(forms.ModalFormView): + template_name = 'iot/services/update.html' + modal_header = _("Update Service") + form_id = "update_service_form" + form_class = project_forms.UpdateBoardForm + submit_label = _("Update Service") + submit_url = "horizon:iot:services:update" + success_url = reverse_lazy('horizon:iot:services:index') + page_title = _("Update Service") + + @memoized.memoized_method + def get_object(self): + try: + return iotronic.service_get(self.request, self.kwargs['service_id'], + None) + except Exception: + redirect = reverse("horizon:iot:services:index") + exceptions.handle(self.request, + _('Unable to get service information.'), + redirect=redirect) + + def get_context_data(self, **kwargs): + context = super(UpdateView, self).get_context_data(**kwargs) + args = (self.get_object().uuid,) + context['submit_url'] = reverse(self.submit_url, args=args) + return context + + def get_initial(self): + service = self.get_object() + + return {'uuid': service.uuid, + 'name': service.name, + 'port': service.port, + 'protocol': service.protocol} + + +class ActionView(forms.ModalFormView): + template_name = 'iot/services/action.html' + modal_header = _("Service Action") + form_id = "service_action_form" + form_class = project_forms.ServiceActionForm + submit_label = _("Service Action") + # submit_url = reverse_lazy("horizon:iot:services:action") + submit_url = "horizon:iot:services:action" + success_url = reverse_lazy('horizon:iot:services:index') + page_title = _("Service Action") + + @memoized.memoized_method + def get_object(self): + try: + return iotronic.service_get(self.request, self.kwargs['service_id'], + None) + except Exception: + redirect = reverse("horizon:iot:services:index") + exceptions.handle(self.request, + _('Unable to get service information.'), + redirect=redirect) + + def get_context_data(self, **kwargs): + context = super(ActionView, self).get_context_data(**kwargs) + args = (self.get_object().uuid,) + context['submit_url'] = reverse(self.submit_url, args=args) + return context + + def get_initial(self): + service = self.get_object() + + # Populate boards + boards = iotronic.board_list(self.request, "online", None, None) + boards.sort(key=lambda b: b.name) + + board_list = [] + for board in boards: + board_list.append((board.uuid, _(board.name))) + + return {'uuid': service.uuid, + 'name': service.name, + 'board_list': board_list} + + +class RemoveServicesView(forms.ModalFormView): + template_name = 'iot/services/remove.html' + modal_header = _("Remove Service") + form_id = "remove_service_form" + form_class = project_forms.RemoveServicesForm + submit_label = _("Remove Service") + # submit_url = reverse_lazy("horizon:iot:boards:removeplugins") + submit_url = "horizon:iot:services:remove" + success_url = reverse_lazy('horizon:iot:services:index') + page_title = _("Remove Service") + + @memoized.memoized_method + def get_object(self): + try: + return iotronic.service_get(self.request, self.kwargs['service_id'], + None) + except Exception: + redirect = reverse("horizon:iot:services:index") + exceptions.handle(self.request, + _('Unable to get service information.'), + redirect=redirect) + + def get_context_data(self, **kwargs): + context = super(RemoveServicesView, self).get_context_data(**kwargs) + args = (self.get_object().uuid,) + context['submit_url'] = reverse(self.submit_url, args=args) + return context + + def get_initial(self): + service = self.get_object() + + return {'uuid': service.uuid, + 'name': service.name, + 'port': service.port, + 'protocol': service.protocol} + + +class DetailView(tabs.TabView): + tab_group_class = project_tabs.ServiceDetailTabs + template_name = 'horizon/common/_detail.html' + page_title = "{{ service.name|default:service.uuid }}" + + def get_context_data(self, **kwargs): + context = super(DetailView, self).get_context_data(**kwargs) + service = self.get_data() + context["service"] = service + context["url"] = reverse(self.redirect_url) + context["actions"] = self._get_actions(service) + + return context + + def _get_actions(self, service): + table = project_tables.ServicesTable(self.request) + return table.render_row_actions(service) + + @memoized.memoized_method + def get_data(self): + service_id = self.kwargs['service_id'] + try: + service = iotronic.service_get(self.request, service_id, None) + except Exception: + msg = ('Unable to retrieve service %s information') % {'name': + service.name} + exceptions.handle(self.request, msg, ignore=True) + return service + + def get_tabs(self, request, *args, **kwargs): + service = self.get_data() + return self.tab_group_class(request, service=service, **kwargs) + + +class ServiceDetailView(DetailView): + redirect_url = 'horizon:iot:services:index' + + def _get_actions(self, service): + table = project_tables.ServicesTable(self.request) + return table.render_row_actions(service)