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)