diff --git a/requirements.txt b/requirements.txt index 24e2a94..2a8a0a8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ pbr>=1.8 Django>=1.8,<1.9 # BSD django_compressor>=1.4 # MIT django_openstack_auth>=2.0.0 # Apache-2.0 +httplib2>=0.7.5 # MIT python-keystoneclient>=1.6.0,!=1.8.0,!=2.1.0 # Apache-2.0 pytz>=2013.6 # MIT diff --git a/run_tests.sh b/run_tests.sh index 4a0a1b9..f5907ec 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -343,7 +343,7 @@ function run_tests { fi if [ $with_selenium -eq 0 -a $integration -eq 0 ]; then - testopts="$testopts --exclude=watcher_dashboard/test/integration_tests/" + testopts="$testopts --exclude=watcher_dashboard/test/integration_tests/ " fi if [ $selenium_headless -eq 1 ]; then diff --git a/tox.ini b/tox.ini index be72d5d..69c1179 100644 --- a/tox.ini +++ b/tox.ini @@ -22,7 +22,7 @@ deps = -r{toxinidir}/requirements.txt commands = python manage.py test --settings=watcher_dashboard.test.settings \ --exclude-dir=watcher_dashboard/test/integration_tests \ - watcher_dashboard.test + watcher_dashboard [testenv:pep8] commands = flake8 diff --git a/watcher_dashboard/api/watcher.py b/watcher_dashboard/api/watcher.py index 96721fd..9dc81a0 100644 --- a/watcher_dashboard/api/watcher.py +++ b/watcher_dashboard/api/watcher.py @@ -46,7 +46,7 @@ def watcherclient(request, password=None): return client -class Audit(base.APIResourceWrapper): +class Audit(base.APIDictWrapper): _attrs = ('uuid', 'created_at', 'modified_at', 'deleted_at', 'deadline', 'state', 'type', 'audit_template_uuid', 'audit_template_name') @@ -62,8 +62,8 @@ class Audit(base.APIResourceWrapper): :param request: request object :type request: django.http.HttpRequest - :param audit_template: audit audit_template - :type audit_template: string + :param audit_template_uuid: related audit template UUID + :type audit_template_uuid: string :param type: audit type :type type: string @@ -72,12 +72,11 @@ class Audit(base.APIResourceWrapper): :type deadline: string :return: the created Audit object - :rtype: watcher_dashboard.api.watcher.Audit + :rtype: :py:class:`~.Audit` """ - audit = watcherclient(request).audit.create( + return watcherclient(request).audit.create( audit_template_uuid=audit_template_uuid, type=type, deadline=deadline) - return cls(audit, request=request) @classmethod def list(cls, request, audit_template_filter): @@ -90,11 +89,10 @@ class Audit(base.APIResourceWrapper): :type audit_template_filter: string :return: list of audits, or an empty list if there are none - :rtype: list of watcher_dashboard.api.watcher.Audit + :rtype: list of :py:class:`~.Audit` """ - audits = watcherclient(request).audit.list( + return watcherclient(request).audit.list( audit_template=audit_template_filter) - return [cls(audit, request=request) for audit in audits] @classmethod @errors_utils.handle_errors(_("Unable to retrieve audit")) @@ -109,10 +107,9 @@ class Audit(base.APIResourceWrapper): :return: matching audit, or None if no audit matches the ID - :rtype: watcher_dashboard.api.watcher.Audit + :rtype: :py:class:`~.Audit` """ - audit = watcherclient(request).audit.get(audit_id=audit_id) - return cls(audit, request=request) + return watcherclient(request).audit.get(audit_id=audit_id) @classmethod def delete(cls, request, audit_id): @@ -134,14 +131,15 @@ class Audit(base.APIResourceWrapper): class AuditTemplate(base.APIDictWrapper): _attrs = ('uuid', 'created_at', 'updated_at', 'deleted_at', 'description', 'host_aggregate', 'name', - 'extra', 'goal') + 'extra', 'goal_uuid' 'strategy_uuid') def __init__(self, apiresource, request=None): super(AuditTemplate, self).__init__(apiresource) self._request = request @classmethod - def create(cls, request, name, goal, description, host_aggregate): + def create(cls, request, name, goal_uuid, strategy_uuid, + description, host_aggregate): """Create an audit template in Watcher :param request: request object @@ -150,25 +148,29 @@ class AuditTemplate(base.APIDictWrapper): :param name: Name for this audit template :type name: string - :param goal: Goal Type associated to this audit template - :type goal: string + :param goal_uuid: Goal UUID associated to this audit template + :type goal_uuid: string + + :param strategy_uuid: Strategy UUID associated to this audit template + :type strategy_uuid: string :param description: Descrition of the audit template :type description: string - :param host_aggregate: Name or ID of the host aggregate targeted\ - by this audit template + :param host_aggregate: Name or UUID of the host aggregate targeted + by this audit template :type host_aggregate: string - :param audit_template: audit audit_template + :param audit_template: audit template :type audit_template: string :return: the created Audit Template object - :rtype: watcher_dashboard.api.watcher.AuditTemplate + :rtype: :py:class:`~.AuditTemplate` """ audit_template = watcherclient(request).audit_template.create( name=name, - goal=goal, + goal_uuid=goal_uuid, + strategy_uuid=strategy_uuid, description=description, host_aggregate=host_aggregate ) @@ -189,7 +191,7 @@ class AuditTemplate(base.APIDictWrapper): :type parameters: dict :return: the updated Audit Template object - :rtype: watcher_dashboard.api.watcher.AuditTemplate + :rtype: :py:class:`~.AuditTemplate` """ parameter_list = [{ 'name': str(name), @@ -200,22 +202,18 @@ class AuditTemplate(base.APIDictWrapper): return audit_template @classmethod - def list(cls, request, filter): + def list(cls, request, **filters): """Return a list of audit templates in Watcher :param request: request object :type request: django.http.HttpRequest - :param filter: audit template filter - :type filter: string + :param filters: key/value kwargs used as filters :return: list of audit templates, or an empty list if there are none - :rtype: list of watcher_dashboard.api.watcher.AuditTemplate + :rtype: list of :py:class:`~.AuditTemplate` """ - - audit_templates = watcherclient(request).audit_template.list( - name=filter) - return audit_templates + return watcherclient(request).audit_template.list(**filters) @classmethod @errors_utils.handle_errors(_("Unable to retrieve audit template")) @@ -230,31 +228,10 @@ class AuditTemplate(base.APIDictWrapper): :return: matching audit template, or None if no audit template matches the ID - :rtype: watcher_dashboard.api.watcher.AuditTemplate + :rtype: :py:class:`~.AuditTemplate` """ - audit_template = watcherclient(request).audit_template.get( + return watcherclient(request).audit_template.get( audit_template_id=audit_template_id) - # return cls(audit, request=request) - return audit_template - - @classmethod - @errors_utils.handle_errors(_("Unable to retrieve audit template goal")) - def get_goals(cls, request): - """Return the audit template goal that matches the ID - - :param request: request object - :type request: django.http.HttpRequest - - :param audit_template_id: id of audit template to be retrieved - :type audit_template_id: int - - :return: matching audit template, or None if no audit template matches - the ID - :rtype: watcher_dashboard.api.watcher.AuditTemplate - """ - - goals = watcherclient(request).goal.list() - return map(lambda goal: goal.name, goals) @classmethod def delete(cls, request, audit_template_id): @@ -274,7 +251,7 @@ class AuditTemplate(base.APIDictWrapper): return self.uuid -class ActionPlan(base.APIResourceWrapper): +class ActionPlan(base.APIDictWrapper): _attrs = ('uuid', 'created_at', 'updated_at', 'deleted_at', 'audit_uuid', 'state') @@ -293,12 +270,10 @@ class ActionPlan(base.APIResourceWrapper): :type audit_filter: string :return: list of action plans, or an empty list if there are none - :rtype: list of watcher_dashboard.api.watcher.ActionPlan + :rtype: list of :py:class:`~.ActionPlan` """ - action_plans = watcherclient(request).action_plan.list( + return watcherclient(request).action_plan.list( audit=audit_filter) - return [cls(action_plan, request=request) - for action_plan in action_plans] @classmethod @errors_utils.handle_errors(_("Unable to retrieve action plan")) @@ -313,11 +288,10 @@ class ActionPlan(base.APIResourceWrapper): :return: matching action plan, or None if no action plan matches the ID - :rtype: watcher_dashboard.api.watcher.ActionPlan + :rtype: :py:class:`~.ActionPlan` """ - action_plan = watcherclient(request).action_plan.get( + return watcherclient(request).action_plan.get( action_plan_id=action_plan_id) - return cls(action_plan, request=request) @classmethod def delete(cls, request, action_plan_id): @@ -351,7 +325,7 @@ class ActionPlan(base.APIResourceWrapper): return self.uuid -class Action(base.APIResourceWrapper): +class Action(base.APIDictWrapper): _attrs = ('uuid', 'created_at', 'updated_at', 'deleted_at', 'next_uuid', 'description', 'state', 'action_plan_uuid', 'action_type', 'applies_to', 'src', 'dst', 'parameter') @@ -371,13 +345,11 @@ class Action(base.APIResourceWrapper): :type action_plan_filter: string :return: list of actions, or an empty list if there are none - :rtype: list of watcher_dashboard.api.watcher.Action + :rtype: list of :py:class:`~.Action` """ - actions = watcherclient(request).action.list( + return watcherclient(request).action.list( action_plan=action_plan_filter, detail=True) - return [cls(action, request=request) - for action in actions] @classmethod @errors_utils.handle_errors(_("Unable to retrieve action")) @@ -392,11 +364,9 @@ class Action(base.APIResourceWrapper): :return: matching action, or None if no action matches the ID - :rtype: watcher_dashboard.api.watcher.Action + :rtype: :py:class:`~.Action` """ - action = watcherclient(request).action.get( - action_id=action_id) - return cls(action, request=request) + return watcherclient(request).action.get(action_id=action_id) @classmethod def delete(cls, request, action_id): @@ -428,3 +398,94 @@ class Action(base.APIResourceWrapper): @property def id(self): return self.uuid + + +class Goal(base.APIDictWrapper): + """Goal resource.""" + + _attrs = ('uuid', 'name', 'display_name', 'created_at', + 'updated_at', 'deleted_at') + + def __init__(self, apiresource, request=None): + super(Goal, self).__init__(apiresource) + self._request = request + + @classmethod + def list(cls, request, **filters): + """Return a list of goals in Watcher + + :param request: request object + :type request: django.http.HttpRequest + + :return: list of goals, or an empty list if there are none + :rtype: list of :py:class:`~.Goal` instance + """ + return watcherclient(request).goal.list(detail=True, **filters) + + @classmethod + @errors_utils.handle_errors(_("Unable to retrieve goal")) + def get(cls, request, goal_uuid): + """Return the goal that matches the ID + + :param request: request object + :type request: django.http.HttpRequest + + :param goal_uuid: uuid of goal to be retrieved + :type goal_uuid: int + + :return: matching goal, or None if no goal matches the UUID + :rtype: :py:class:`~.Goal` instance + """ + return watcherclient(request).goal.get(goal_uuid) + + @property + def id(self): + return self.uuid + + +class Strategy(base.APIDictWrapper): + """Strategy resource.""" + + _attrs = ('uuid', 'name', 'display_name', 'goal_uuid', 'created_at', + 'updated_at', 'deleted_at') + + def __init__(self, apiresource, request=None): + super(Strategy, self).__init__(apiresource) + self._request = request + + @classmethod + def list(cls, request, **filters): + """Return a list of strategies in Watcher + + :param request: request object + :type request: django.http.HttpRequest + + :param goal_uuid: goal uuid filter + :type goal_uuid: string + + :return: list of strategies, or an empty list if there are none + :rtype: list of :py:class:`~.Strategy` instances + """ + goal_uuid = filters.get('goal_uuid', None) + return watcherclient(request).strategy.list( + goal_uuid=goal_uuid, detail=True) + + @classmethod + @errors_utils.handle_errors(_("Unable to retrieve strategy")) + def get(cls, request, strategy_uuid): + """Return the strategy that matches the UUID + + :param request: request object + :type request: django.http.HttpRequest + + :param strategy_uuid: uuid of strategy to be retrieved + :type strategy_uuid: str + + :return: matching strategy, or None if no strategy matches the UUID + :rtype: :py:class:`~.Strategy` instance + """ + return watcherclient(request).strategy.get(strategy_uuid) + + @property + def id(self): + return self.uuid diff --git a/watcher_dashboard/content/action_plans/tables.py b/watcher_dashboard/content/action_plans/tables.py index fc82213..3790f52 100644 --- a/watcher_dashboard/content/action_plans/tables.py +++ b/watcher_dashboard/content/action_plans/tables.py @@ -32,7 +32,7 @@ LOG = logging.getLogger(__name__) ACTION_PLAN_STATE_DISPLAY_CHOICES = ( ("NO STATE", pgettext_lazy("State of an action plan", u"No State")), ("ONGOING", pgettext_lazy("State of an action plan", u"On Going")), - ("SUCCESS", pgettext_lazy("State of an action plan", u"Sucess")), + ("SUCCEEDED", pgettext_lazy("State of an action plan", u"Succeeded")), ("SUBMITTED", pgettext_lazy("State of an action plan", u"Submitted")), ("FAILED", pgettext_lazy("State of an action plan", u"Failed")), ("DELETED", pgettext_lazy("State of an action plan", u"Deleted")), @@ -124,31 +124,10 @@ class UpdateRow(horizon.tables.Row): return action_plan -# class CancelActionPlan(horizon.tables.DeleteAction): -# verbose_name = _(u"Cancel ActionPlans") -# icon = "trash" - -# @staticmethod -# def action_present(count): -# return ungettext_lazy( -# u"Cancel ActionPlan", -# u"Cancel ActionPlans", -# count -# ) - -# @staticmethod -# def action_past(count): -# return ungettext_lazy( -# u"Canceled ActionPlan", -# u"canceled ActionPlans", -# count -# ) - - class ActionPlansTable(horizon.tables.DataTable): name = horizon.tables.Column( - 'id', - verbose_name=_("ID"), + 'uuid', + verbose_name=_("UUID"), link="horizon:admin:action_plans:detail") audit = horizon.tables.Column( 'audit_uuid', @@ -165,6 +144,9 @@ class ActionPlansTable(horizon.tables.DataTable): status=True, status_choices=ACTION_PLAN_STATE_DISPLAY_CHOICES) + def get_object_id(self, datum): + return datum.uuid + class Meta(object): name = "action_plans" verbose_name = _("ActionPlans") diff --git a/watcher_dashboard/content/action_plans/urls.py b/watcher_dashboard/content/action_plans/urls.py index 27ce1e1..8954598 100644 --- a/watcher_dashboard/content/action_plans/urls.py +++ b/watcher_dashboard/content/action_plans/urls.py @@ -19,12 +19,11 @@ from django.conf import urls from watcher_dashboard.content.action_plans import views -urlpatterns = urls.patterns( - 'watcher_dashboard.content.action_plans.views', +urlpatterns = [ urls.url(r'^$', views.IndexView.as_view(), name='index'), - urls.url(r'^(?P[^/]+)/detail$', + urls.url(r'^(?P[^/]+)/detail$', views.DetailView.as_view(), name='detail'), urls.url(r'^archive/$', views.ArchiveView.as_view(), name='archive'), -) +] diff --git a/watcher_dashboard/content/action_plans/views.py b/watcher_dashboard/content/action_plans/views.py index 5b20aaa..7fe8bf2 100644 --- a/watcher_dashboard/content/action_plans/views.py +++ b/watcher_dashboard/content/action_plans/views.py @@ -85,13 +85,14 @@ class DetailView(horizon.tables.MultiTableView): @memoized.memoized_method def _get_data(self): - action_plan_id = None + action_plan_uuid = None try: - action_plan_id = self.kwargs['action_plan_id'] - action_plan = watcher.ActionPlan.get(self.request, action_plan_id) + action_plan_uuid = self.kwargs['action_plan_uuid'] + action_plan = watcher.ActionPlan.get( + self.request, action_plan_uuid) except Exception: msg = _('Unable to retrieve details for action_plan "%s".') \ - % action_plan_id + % action_plan_uuid horizon.exceptions.handle( self.request, msg, redirect=self.redirect_url) diff --git a/watcher_dashboard/content/actions/tables.py b/watcher_dashboard/content/actions/tables.py index 1449430..28167f4 100644 --- a/watcher_dashboard/content/actions/tables.py +++ b/watcher_dashboard/content/actions/tables.py @@ -32,7 +32,7 @@ LOG = logging.getLogger(__name__) ACTION_STATE_DISPLAY_CHOICES = ( ("NO STATE", pgettext_lazy("Power state of an Instance", u"No State")), ("ONGOING", pgettext_lazy("Power state of an Instance", u"On Going")), - ("SUCCESS", pgettext_lazy("Power state of an Instance", u"Success")), + ("SUCCEEDED", pgettext_lazy("Power state of an Instance", u"Succeeded")), ("CANCELLED", pgettext_lazy("Power state of an Instance", u"Cancelled")), ("FAILED", pgettext_lazy("Power state of an Instance", u"Failed")), ("DELETED", pgettext_lazy("Power state of an Instance", u"Deleted")), @@ -62,8 +62,8 @@ class ActionsFilterAction(horizon.tables.FilterAction): class ActionsTable(horizon.tables.DataTable): name = horizon.tables.Column( - 'id', - verbose_name=_("ID")) + 'uuid', + verbose_name=_("UUID")) action_type = horizon.tables.Column( 'action_type', verbose_name=_('Type'), @@ -81,6 +81,9 @@ class ActionsTable(horizon.tables.DataTable): verbose_name=_('Next Action')) ajax = True + def get_object_id(self, datum): + return datum.uuid + class Meta(object): name = "wactions" verbose_name = _("Actions") diff --git a/watcher_dashboard/content/actions/urls.py b/watcher_dashboard/content/actions/urls.py index c3bc5a8..a0defb5 100644 --- a/watcher_dashboard/content/actions/urls.py +++ b/watcher_dashboard/content/actions/urls.py @@ -19,10 +19,9 @@ from django.conf import urls from watcher_dashboard.content.actions import views -urlpatterns = urls.patterns( - 'watcher_dashboard.content.actions.views', +urlpatterns = [ urls.url(r'^$', views.IndexView.as_view(), name='index'), - urls.url(r'^(?P[^/]+)/$', + urls.url(r'^(?P[^/]+)/$', views.DetailView.as_view(), name='details'), -) +] diff --git a/watcher_dashboard/content/actions/views.py b/watcher_dashboard/content/actions/views.py index 3399c7f..bea8c9f 100644 --- a/watcher_dashboard/content/actions/views.py +++ b/watcher_dashboard/content/actions/views.py @@ -70,13 +70,13 @@ class DetailView(horizon.tabs.TabbedTableView): @memoized.memoized_method def _get_data(self): - action_plan_id = None + action_plan_uuid = None try: - action_plan_id = self.kwargs['action_plan_id'] - action = watcher.Action.get(self.request, action_plan_id) + action_plan_uuid = self.kwargs['action_plan_uuid'] + action = watcher.Action.get(self.request, action_plan_uuid) except Exception: msg = _('Unable to retrieve details for action "%s".') \ - % action_plan_id + % action_plan_uuid horizon.exceptions.handle( self.request, msg, redirect=self.redirect_url) diff --git a/watcher_dashboard/content/audit_templates/forms.py b/watcher_dashboard/content/audit_templates/forms.py index 5079ac4..3afcdeb 100644 --- a/watcher_dashboard/content/audit_templates/forms.py +++ b/watcher_dashboard/content/audit_templates/forms.py @@ -34,23 +34,29 @@ class CreateForm(forms.SelfHandlingForm): name = forms.CharField(max_length=255, label=_("Name")) description = forms.CharField(max_length=255, label=_("Description"), required=False) - goal = forms.ChoiceField(label=_('Goal'), - required=True, - ) + goal_uuid = forms.ChoiceField(label=_('Goal'), required=True) + strategy_uuid = forms.ChoiceField(label=_('Strategy'), required=False) failure_url = 'horizon:admin:audit_templates:index' def __init__(self, request, *args, **kwargs): super(CreateForm, self).__init__(request, *args, **kwargs) goals = self._get_goal_list(request) + strategies = self._get_strategy_list(request, goals) + if goals: - self.fields['goal'].choices = goals + self.fields['goal_uuid'].choices = goals else: - del self.fields['goal'] + del self.fields['goal_uuid'] + + if strategies: + self.fields['strategy_uuid'].choices = strategies + else: + del self.fields['strategy_uuid'] def _get_goal_list(self, request): try: - goals = watcher.AuditTemplate.get_goals(self.request) + goals = watcher.Goal.list(self.request) except Exception as exc: msg = _('Failed to get goals list.') LOG.info(msg) @@ -59,7 +65,7 @@ class CreateForm(forms.SelfHandlingForm): goals = [] choices = [ - (goal, goal) + (goal.uuid, goal.display_name) for goal in goals ] @@ -67,16 +73,41 @@ class CreateForm(forms.SelfHandlingForm): choices.insert(0, ("", _("Select Goal"))) return choices + def _get_strategy_list(self, request, goals): + try: + strategies = watcher.Strategy.list(self.request) + except Exception as exc: + msg = _('Failed to get the list of available strategies.') + LOG.info(msg) + messages.warning(request, msg) + messages.warning(request, exc) + strategies = [] + + _goals = {} + for goal in goals: + _goals[goal[0]] = goal[1] + + choices = [ + (strategy.uuid, strategy.display_name + + ' (GOAL: ' + _goals[strategy.goal_uuid] + ')') + for strategy in strategies + ] + + if choices: + choices.insert(0, ("", _("Select Strategy"))) + return choices + def handle(self, request, data): try: params = {'name': data['name']} - params['goal'] = data['goal'] params['description'] = data['description'] + params['goal_uuid'] = data['goal_uuid'] + params['strategy_uuid'] = data['strategy_uuid'] or None params['host_aggregate'] = None - audit_temp = watcher.AuditTemplate.create(request, **params) + audit_tpl = watcher.AuditTemplate.create(request, **params) message = _('Audit Template was successfully created.') messages.success(request, message) - return audit_temp + return audit_tpl except Exception as exc: msg = _('Failed to create audit template"%s".') % data['name'] LOG.info(exc) diff --git a/watcher_dashboard/content/audit_templates/tables.py b/watcher_dashboard/content/audit_templates/tables.py index 8abf02f..3b609a2 100644 --- a/watcher_dashboard/content/audit_templates/tables.py +++ b/watcher_dashboard/content/audit_templates/tables.py @@ -16,7 +16,6 @@ from __future__ import unicode_literals -from django.utils.translation import pgettext_lazy from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ungettext_lazy import horizon.exceptions @@ -26,25 +25,6 @@ import horizon.tables from watcher_dashboard.api import watcher -AUDIT_TEMPLATE_GOAL_DISPLAY_CHOICES = ( - ("BASIC_CONSOLIDATION", pgettext_lazy( - "Goal of an Audit", - "Consolidate Servers")), - ("MINIMIZE_ENERGY_CONSUMPTION", pgettext_lazy( - "Goal of an Audit", - "Minimize Energy")), - ("BALANCE_LOAD", pgettext_lazy( - "Goal of an Audit", - "Load Balancing")), - ("MINIMIZE_LICENSING_COST", pgettext_lazy( - "Goal of an Audit", - "Minimize Licensing Cost")), - ("PREPARED_PLAN_OPERATION", pgettext_lazy( - "Goal of an Audit", - "Prepared Plan Operation")), -) - - class CreateAuditTemplates(horizon.tables.LinkAction): name = "create" verbose_name = _("Create Template") @@ -55,7 +35,9 @@ class CreateAuditTemplates(horizon.tables.LinkAction): class AuditTemplatesFilterAction(horizon.tables.FilterAction): filter_type = "server" filter_choices = ( - ('name', _("Template Name ="), True), + ('name', _("Name ="), True), + ('goal_uuid', _("Goal ="), True), + ('strategy_uuid', _("Strategy ="), True), ) @@ -65,8 +47,6 @@ class LaunchAudit(horizon.tables.BatchAction): data_type_singular = _("Launch Audit") data_type_plural = _("Launch Audits") success_url = "horizon:admin:audits:index" - # icon = "cloud-upload" - # policy_rules = (("compute", "compute:create"),) @staticmethod def action_present(count): @@ -120,10 +100,14 @@ class AuditTemplatesTable(horizon.tables.DataTable): verbose_name=_("Name"), link="horizon:admin:audit_templates:detail") goal = horizon.tables.Column( - 'goal', + 'goal_uuid', verbose_name=_('Goal'), status=True, - status_choices=AUDIT_TEMPLATE_GOAL_DISPLAY_CHOICES + ) + strategy = horizon.tables.Column( + 'strategy_uuid', + verbose_name=_('Strategy'), + status=True, ) def get_object_id(self, datum): @@ -136,10 +120,7 @@ class AuditTemplatesTable(horizon.tables.DataTable): CreateAuditTemplates, DeleteAuditTemplates, AuditTemplatesFilterAction, - # LaunchAuditTemplates, ) row_actions = ( LaunchAudit, - # CreateAuditTemplates, - # DeleteAuditTemplates, ) diff --git a/watcher_dashboard/content/audit_templates/tests.py b/watcher_dashboard/content/audit_templates/tests.py index e765141..0577773 100644 --- a/watcher_dashboard/content/audit_templates/tests.py +++ b/watcher_dashboard/content/audit_templates/tests.py @@ -34,20 +34,17 @@ DETAILS_VIEW = 'horizon:admin:audit_templates:detail' class AuditTemplatesTest(test.BaseAdminViewTests): - goal_list = [ - 'BASIC_CONSOLIDATION', - 'MINIMIZE_ENERGY_CONSUMPTION', - 'BALANCE_LOAD', - 'MINIMIZE_LICENSING_COST', - 'PREPARED_PLAN_OPERATION', - ] + def setUp(self): + super(AuditTemplatesTest, self).setUp() + self.goal_list = self.goals.list() + self.strategy_list = self.strategies.list() @test.create_stubs({api.watcher.AuditTemplate: ('list',)}) def test_index(self): - search_opts = None + search_opts = {} api.watcher.AuditTemplate.list( IsA(http.HttpRequest), - filter=search_opts).MultipleTimes().AndReturn( + **search_opts).MultipleTimes().AndReturn( self.audit_templates.list()) self.mox.ReplayAll() @@ -68,34 +65,45 @@ class AuditTemplatesTest(test.BaseAdminViewTests): resp = self.client.get(INDEX_URL) self.assertMessageCount(resp, error=1, warning=0) - @test.create_stubs({api.watcher.AuditTemplate: ('get_goals',)}) + @test.create_stubs({api.watcher.Strategy: ('list',)}) + @test.create_stubs({api.watcher.Goal: ('list',)}) def test_create_get(self): - api.watcher.AuditTemplate.get_goals( + api.watcher.Goal.list( IsA(http.HttpRequest)).AndReturn(self.goal_list) + api.watcher.Strategy.list( + IsA(http.HttpRequest)).AndReturn(self.strategy_list) self.mox.ReplayAll() res = self.client.get(CREATE_URL) self.assertTemplateUsed(res, 'infra_optim/audit_templates/create.html') - @test.create_stubs({api.watcher.AuditTemplate: ('create', - 'get_goals')}) + @test.create_stubs({api.watcher.Strategy: ('list',)}) + @test.create_stubs({api.watcher.Goal: ('list',)}) + @test.create_stubs({api.watcher.AuditTemplate: ('create',)}) def test_create_post(self): at = self.audit_templates.first() - api.watcher.AuditTemplate.get_goals( + params = { + 'name': at.name, + 'goal_uuid': at.goal_uuid, + 'strategy_uuid': at.strategy_uuid, + 'description': at.description, + 'host_aggregate': at.host_aggregate, + } + api.watcher.Goal.list( IsA(http.HttpRequest)).AndReturn(self.goal_list) - params = {'name': at.name, - 'goal': at.goal, - 'description': at.description, - 'host_aggregate': at.host_aggregate, - } + api.watcher.Strategy.list( + IsA(http.HttpRequest)).AndReturn(self.strategy_list) + api.watcher.AuditTemplate.create( IsA(http.HttpRequest), **params).AndReturn(at) self.mox.ReplayAll() - form_data = {'name': at.name, - 'goal': at.goal, - 'description': at.description, - 'host_aggregate': at.host_aggregate, - } + form_data = { + 'name': at.name, + 'goal_uuid': at.goal_uuid, + 'strategy_uuid': at.strategy_uuid, + 'description': at.description, + 'host_aggregate': at.host_aggregate, + } res = self.client.post(CREATE_URL, form_data) self.assertNoFormErrors(res) self.assertRedirectsNoFollow(res, INDEX_URL) @@ -130,14 +138,13 @@ class AuditTemplatesTest(test.BaseAdminViewTests): @test.create_stubs({api.watcher.AuditTemplate: ('delete', 'list')}) def test_delete(self): - search_opts = None + search_opts = {} at_list = self.audit_templates.list() at = self.audit_templates.first() at_id = at.uuid api.watcher.AuditTemplate.list( IsA(http.HttpRequest), - filter=search_opts).MultipleTimes().AndReturn( - at_list) + **search_opts).MultipleTimes().AndReturn(at_list) api.watcher.AuditTemplate.delete(IsA(http.HttpRequest), at_id) self.mox.ReplayAll() diff --git a/watcher_dashboard/content/audit_templates/urls.py b/watcher_dashboard/content/audit_templates/urls.py index 99e8316..d9ca9ef 100644 --- a/watcher_dashboard/content/audit_templates/urls.py +++ b/watcher_dashboard/content/audit_templates/urls.py @@ -19,11 +19,10 @@ from django.conf import urls from watcher_dashboard.content.audit_templates import views -urlpatterns = urls.patterns( - 'watcher_dashboard.content.audit_templates.views', +urlpatterns = [ urls.url(r'^$', views.IndexView.as_view(), name='index'), urls.url(r'^create/$', views.CreateView.as_view(), name='create'), - urls.url(r'^(?P[^/]+)/detail$', + urls.url(r'^(?P[^/]+)/detail$', views.DetailView.as_view(), name='detail'), -) +] diff --git a/watcher_dashboard/content/audit_templates/views.py b/watcher_dashboard/content/audit_templates/views.py index d430372..e8bcde3 100644 --- a/watcher_dashboard/content/audit_templates/views.py +++ b/watcher_dashboard/content/audit_templates/views.py @@ -44,8 +44,8 @@ class IndexView(horizon.tables.DataTableView): audit_templates = [] search_opts = self.get_filters() try: - audit_templates = watcher.AuditTemplate.list(self.request, - filter=search_opts) + audit_templates = watcher.AuditTemplate.list( + self.request, **search_opts) except Exception: horizon.exceptions.handle( self.request, @@ -56,15 +56,15 @@ class IndexView(horizon.tables.DataTableView): return len(self.get_data()) def get_filters(self): - filter = None + filters = {} filter_action = self.table._meta._filter_action if filter_action: filter_field = self.table.get_filter_field() if filter_action.is_api_filter(filter_field): filter_string = self.table.get_filter_string() if filter_field and filter_string: - filter = filter_string - return filter + filters[filter_field] = filter_string + return filters class CreateView(forms.ModalFormView): @@ -85,15 +85,15 @@ class DetailView(horizon.tabs.TabbedTableView): page_title = _("Audit Template Details: {{ audit_template.name }}") def _get_data(self): - audit_template_id = None + audit_template_uuid = None try: LOG.info(self.kwargs) - audit_template_id = self.kwargs['audit_template_id'] - audit_template = watcher.AuditTemplate.get(self.request, - audit_template_id) + audit_template_uuid = self.kwargs['audit_template_uuid'] + audit_template = watcher.AuditTemplate.get( + self.request, audit_template_uuid) except Exception: msg = _('Unable to retrieve details for audit template "%s".') \ - % audit_template_id + % audit_template_uuid horizon.exceptions.handle( self.request, msg, redirect=self.redirect_url) diff --git a/watcher_dashboard/content/audits/forms.py b/watcher_dashboard/content/audits/forms.py index be73552..67cf698 100644 --- a/watcher_dashboard/content/audits/forms.py +++ b/watcher_dashboard/content/audits/forms.py @@ -45,7 +45,7 @@ class CreateForm(forms.SelfHandlingForm): def _get_audit_template_list(self, request): try: - audit_templates = watcher.AuditTemplate.list(self.request, None) + audit_templates = watcher.AuditTemplate.list(self.request) except Exception: msg = _('Failed to get audit template list.') LOG.info(msg) diff --git a/watcher_dashboard/content/audits/tables.py b/watcher_dashboard/content/audits/tables.py index d409e8f..ff9d645 100644 --- a/watcher_dashboard/content/audits/tables.py +++ b/watcher_dashboard/content/audits/tables.py @@ -14,6 +14,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import logging + from django.core import urlresolvers from django import shortcuts from django.template.defaultfilters import title # noqa @@ -23,15 +25,15 @@ import horizon.exceptions import horizon.messages import horizon.tables from horizon.utils import filters + from watcher_dashboard.api import watcher -import logging LOG = logging.getLogger(__name__) AUDIT_STATE_DISPLAY_CHOICES = ( ("NO STATE", pgettext_lazy("State of an audit", u"No State")), ("ONGOING", pgettext_lazy("State of an audit", u"On Going")), - ("SUCCESS", pgettext_lazy("State of an audit", u"Sucess")), + ("SUCCEEDED", pgettext_lazy("State of an audit", u"Succeeeded")), ("SUBMITTED", pgettext_lazy("State of an audit", u"Submitted")), ("FAILED", pgettext_lazy("State of an audit", u"Failed")), ("DELETED", pgettext_lazy("State of an audit", u"Deleted")), @@ -55,19 +57,10 @@ class CreateAudit(horizon.tables.LinkAction): # policy_rules = (("compute", "compute:create"),) -# class ArchiveAudits(horizon.tables.LinkAction): -# name = "archive_audits" -# verbose_name = _("Archive Audits") -# url = "horizon:project:instances:launch" -# classes = ("ajax-modal", "btn-launch") -# icon = "folder-open" - class GoToActionPlan(horizon.tables.Action): name = "go_to_action_plan" verbose_name = _("Go to Action Plan") url = "horizon:admin:action_plans:detail" - # classes = ("ajax-modal", "btn-launch") - # icon = "send" def allowed(self, request, audit): return ((audit is None) or @@ -117,8 +110,8 @@ class GoToAuditTemplate(horizon.tables.Action): class AuditsTable(horizon.tables.DataTable): name = horizon.tables.Column( - 'id', - verbose_name=_("ID"), + 'uuid', + verbose_name=_("UUID"), link="horizon:admin:audits:detail") audit_template = horizon.tables.Column( 'audit_template_name', @@ -130,6 +123,9 @@ class AuditsTable(horizon.tables.DataTable): status=True, status_choices=AUDIT_STATE_DISPLAY_CHOICES) + def get_object_id(self, datum): + return datum.uuid + class Meta(object): name = "audits" verbose_name = _("Audits") diff --git a/watcher_dashboard/content/audits/urls.py b/watcher_dashboard/content/audits/urls.py index 7c96a7e..dffc126 100644 --- a/watcher_dashboard/content/audits/urls.py +++ b/watcher_dashboard/content/audits/urls.py @@ -19,12 +19,11 @@ from django.conf import urls from watcher_dashboard.content.audits import views -urlpatterns = urls.patterns( - 'watcher_dashboard.audits.views', +urlpatterns = [ urls.url(r'^$', views.IndexView.as_view(), name='index'), urls.url(r'^create/$', views.CreateView.as_view(), name='create'), - urls.url(r'^(?P[^/]+)/detail$', + urls.url(r'^(?P[^/]+)/detail$', views.DetailView.as_view(), name='detail'), -) +] diff --git a/watcher_dashboard/content/audits/views.py b/watcher_dashboard/content/audits/views.py index a12868b..098f703 100644 --- a/watcher_dashboard/content/audits/views.py +++ b/watcher_dashboard/content/audits/views.py @@ -91,13 +91,13 @@ class DetailView(horizon.tabs.TabbedTableView): @memoized.memoized_method def _get_data(self): - audit_id = None + audit_uuid = None try: - audit_id = self.kwargs['audit_id'] - audit = watcher.Audit.get(self.request, audit_id) + audit_uuid = self.kwargs['audit_uuid'] + audit = watcher.Audit.get(self.request, audit_uuid) except Exception: msg = _('Unable to retrieve details for audit "%s".') \ - % audit_id + % audit_uuid horizon.exceptions.handle( self.request, msg, redirect=self.redirect_url) diff --git a/watcher_dashboard/content/goals/__init__.py b/watcher_dashboard/content/goals/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/watcher_dashboard/content/goals/forms.py b/watcher_dashboard/content/goals/forms.py new file mode 100644 index 0000000..e69de29 diff --git a/watcher_dashboard/content/goals/panel.py b/watcher_dashboard/content/goals/panel.py new file mode 100644 index 0000000..f304c83 --- /dev/null +++ b/watcher_dashboard/content/goals/panel.py @@ -0,0 +1,23 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2016 b<>com +# +# 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 Goals(horizon.Panel): + name = _("Goals") + slug = "goals" diff --git a/watcher_dashboard/content/goals/tables.py b/watcher_dashboard/content/goals/tables.py new file mode 100644 index 0000000..47c30ed --- /dev/null +++ b/watcher_dashboard/content/goals/tables.py @@ -0,0 +1,38 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2016 b<>com +# +# 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.template.defaultfilters import title # noqa +from django.utils.translation import ugettext_lazy as _ +import horizon.exceptions +import horizon.messages +import horizon.tables + + +class GoalsTable(horizon.tables.DataTable): + uuid = horizon.tables.Column( + 'uuid', + verbose_name=_("UUID"), + link="horizon:admin:goals:detail") + display_name = horizon.tables.Column( + 'display_name', + verbose_name=_('Name')) + + def get_object_id(self, datum): + return datum.uuid + + class Meta(object): + name = "goals" + verbose_name = _("Goals") diff --git a/watcher_dashboard/content/goals/tabs.py b/watcher_dashboard/content/goals/tabs.py new file mode 100644 index 0000000..4225dc1 --- /dev/null +++ b/watcher_dashboard/content/goals/tabs.py @@ -0,0 +1,33 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2016 b<>com +# +# 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 _ +from horizon import tabs + + +class OverviewTab(tabs.Tab): + name = _("Overview") + slug = "overview" + template_name = "infra_optim/goals/_detail_overview.html" + + def get_context_data(self, request): + return {"goal": self.tab_group.kwargs['goal']} + + +class GoalDetailTabs(tabs.TabGroup): + slug = "goal_details" + tabs = (OverviewTab,) + sticky = True diff --git a/watcher_dashboard/content/goals/tests.py b/watcher_dashboard/content/goals/tests.py new file mode 100644 index 0000000..443a2a5 --- /dev/null +++ b/watcher_dashboard/content/goals/tests.py @@ -0,0 +1,82 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2016 b<>com +# +# 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 import urlresolvers +from django import http +from mox3.mox import IsA # noqa + +from watcher_dashboard import api +from watcher_dashboard.test import helpers as test + +LOG = logging.getLogger(__name__) + +INDEX_URL = urlresolvers.reverse('horizon:admin:goals:index') +DETAILS_VIEW = 'horizon:admin:goals:detail' + + +class GoalsTest(test.BaseAdminViewTests): + + @test.create_stubs({api.watcher.Goal: ('list',)}) + def test_index(self): + search_opts = {} + api.watcher.Goal.list( + IsA(http.HttpRequest), **search_opts + ).MultipleTimes().AndReturn(self.goals.list()) + self.mox.ReplayAll() + + res = self.client.get(INDEX_URL) + self.assertTemplateUsed(res, 'infra_optim/goals/index.html') + goals = res.context['goals_table'].data + self.assertItemsEqual(goals, self.goals.list()) + + @test.create_stubs({api.watcher.Goal: ('list',)}) + def test_goal_list_unavailable(self): + search_opts = {} + api.watcher.Goal.list( + IsA(http.HttpRequest), **search_opts + ).MultipleTimes().AndRaise(self.exceptions.watcher) + self.mox.ReplayAll() + + resp = self.client.get(INDEX_URL) + self.assertMessageCount(resp, error=1, warning=0) + + @test.create_stubs({api.watcher.Goal: ('get',)}) + def test_details(self): + goal = self.goals.first() + goal_id = goal.uuid + api.watcher.Goal.get( + IsA(http.HttpRequest), goal_id).MultipleTimes().AndReturn(goal) + self.mox.ReplayAll() + DETAILS_URL = urlresolvers.reverse(DETAILS_VIEW, args=[goal_id]) + res = self.client.get(DETAILS_URL) + self.assertTemplateUsed(res, 'infra_optim/goals/details.html') + goals = res.context['goal'] + self.assertItemsEqual([goals], [goal]) + + @test.create_stubs({api.watcher.Goal: ('get',)}) + def test_details_exception(self): + at = self.goals.first() + at_id = at.uuid + api.watcher.Goal.get(IsA(http.HttpRequest), at_id) \ + .AndRaise(self.exceptions.watcher) + + self.mox.ReplayAll() + + DETAILS_URL = urlresolvers.reverse(DETAILS_VIEW, args=[at_id]) + res = self.client.get(DETAILS_URL) + self.assertRedirectsNoFollow(res, INDEX_URL) diff --git a/watcher_dashboard/content/goals/urls.py b/watcher_dashboard/content/goals/urls.py new file mode 100644 index 0000000..be8d6a3 --- /dev/null +++ b/watcher_dashboard/content/goals/urls.py @@ -0,0 +1,27 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2016 b<>com +# +# 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 import urls + +from watcher_dashboard.content.goals import views + + +urlpatterns = [ + urls.url(r'^$', + views.IndexView.as_view(), name='index'), + urls.url(r'^(?P[^/]+)/detail$', + views.DetailView.as_view(), name='detail'), +] diff --git a/watcher_dashboard/content/goals/views.py b/watcher_dashboard/content/goals/views.py new file mode 100644 index 0000000..77d891e --- /dev/null +++ b/watcher_dashboard/content/goals/views.py @@ -0,0 +1,95 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2016 b<>com +# +# 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.exceptions +import horizon.tables +import horizon.tabs +from horizon.utils import memoized +import horizon.workflows + +from watcher_dashboard.api import watcher +from watcher_dashboard.content.goals import tables +from watcher_dashboard.content.goals import tabs as wtabs + + +class IndexView(horizon.tables.DataTableView): + table_class = tables.GoalsTable + template_name = 'infra_optim/goals/index.html' + + def get_context_data(self, **kwargs): + context = super(IndexView, self).get_context_data(**kwargs) + context['goals_count'] = self.get_goals_count() + return context + + def get_data(self): + goals = [] + search_opts = self.get_filters() + try: + goals = watcher.Goal.list(self.request, **search_opts) + except Exception: + horizon.exceptions.handle( + self.request, + _("Unable to retrieve goal information.")) + return goals + + def get_goals_count(self): + return len(self.get_data()) + + def get_filters(self): + filters = {} + filter_action = self.table._meta._filter_action + if filter_action: + filter_field = self.table.get_filter_field() + if filter_action.is_api_filter(filter_field): + filter_string = self.table.get_filter_string() + if filter_field and filter_string: + filters[filter_field] = filter_string + return filters + + +class DetailView(horizon.tabs.TabbedTableView): + tab_group_class = wtabs.GoalDetailTabs + template_name = 'infra_optim/goals/details.html' + redirect_url = 'horizon:admin:goals:index' + page_title = _("Goal Details: {{ goal.name }}") + + @memoized.memoized_method + def _get_data(self): + goal_uuid = None + try: + goal_uuid = self.kwargs['goal_uuid'] + goal = watcher.Goal.get(self.request, goal_uuid) + except Exception: + msg = _('Unable to retrieve details for goal "%s".') \ + % goal_uuid + horizon.exceptions.handle( + self.request, msg, + redirect=self.redirect_url) + return goal + + def get_context_data(self, **kwargs): + context = super(DetailView, self).get_context_data(**kwargs) + goal = self._get_data() + context["goal"] = goal + return context + + def get_tabs(self, request, *args, **kwargs): + goal = self._get_data() + # ports = self._get_ports() + return self.tab_group_class(request, goal=goal, + # ports=ports, + **kwargs) diff --git a/watcher_dashboard/content/strategies/__init__.py b/watcher_dashboard/content/strategies/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/watcher_dashboard/content/strategies/forms.py b/watcher_dashboard/content/strategies/forms.py new file mode 100644 index 0000000..e69de29 diff --git a/watcher_dashboard/content/strategies/panel.py b/watcher_dashboard/content/strategies/panel.py new file mode 100644 index 0000000..2c433a4 --- /dev/null +++ b/watcher_dashboard/content/strategies/panel.py @@ -0,0 +1,23 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2016 b<>com +# +# 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 Strategies(horizon.Panel): + name = _("Strategies") + slug = "strategies" diff --git a/watcher_dashboard/content/strategies/tables.py b/watcher_dashboard/content/strategies/tables.py new file mode 100644 index 0000000..2e97b56 --- /dev/null +++ b/watcher_dashboard/content/strategies/tables.py @@ -0,0 +1,44 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2016 b<>com +# +# 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.template.defaultfilters import title # noqa +from django.utils.translation import ugettext_lazy as _ +import horizon.exceptions +import horizon.messages +import horizon.tables +from horizon.utils import filters + + +class StrategiesTable(horizon.tables.DataTable): + uuid = horizon.tables.Column( + 'uuid', + verbose_name=_("UUID"), + link="horizon:admin:strategies:detail") + display_name = horizon.tables.Column( + 'display_name', + verbose_name=_('Name'), + filters=(title, filters.replace_underscores)) + goal_uuid = horizon.tables.Column( + 'goal_uuid', + verbose_name=_("Goal UUID"), + ) + + def get_object_id(self, datum): + return datum.uuid + + class Meta(object): + name = "strategies" + verbose_name = _("Strategies") diff --git a/watcher_dashboard/content/strategies/tabs.py b/watcher_dashboard/content/strategies/tabs.py new file mode 100644 index 0000000..12fd166 --- /dev/null +++ b/watcher_dashboard/content/strategies/tabs.py @@ -0,0 +1,33 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2016 b<>com +# +# 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 _ +from horizon import tabs + + +class OverviewTab(tabs.Tab): + name = _("Overview") + slug = "overview" + template_name = "infra_optim/strategies/_detail_overview.html" + + def get_context_data(self, request): + return {"strategy": self.tab_group.kwargs['strategy']} + + +class StrategyDetailTabs(tabs.TabGroup): + slug = "strategy_details" + tabs = (OverviewTab,) + sticky = True diff --git a/watcher_dashboard/content/strategies/tests.py b/watcher_dashboard/content/strategies/tests.py new file mode 100644 index 0000000..2fd8d62 --- /dev/null +++ b/watcher_dashboard/content/strategies/tests.py @@ -0,0 +1,93 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2016 b<>com +# +# 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 import urlresolvers +from django import http +from mox3.mox import IsA # noqa + +from watcher_dashboard import api +from watcher_dashboard.test import helpers as test + +LOG = logging.getLogger(__name__) + +INDEX_URL = urlresolvers.reverse( + 'horizon:admin:strategies:index') +DETAILS_VIEW = 'horizon:admin:strategies:detail' + + +class StrategiesTest(test.BaseAdminViewTests): + + goal_list = [ + 'BASIC_CONSOLIDATION', + 'MINIMIZE_ENERGY_CONSUMPTION', + 'BALANCE_LOAD', + 'MINIMIZE_LICENSING_COST', + 'PREPARED_PLAN_OPERATION', + ] + + @test.create_stubs({api.watcher.Strategy: ('list',)}) + def test_index(self): + search_opts = {} + api.watcher.Strategy.list( + IsA(http.HttpRequest), **search_opts + ).MultipleTimes().AndReturn(self.strategies.list()) + self.mox.ReplayAll() + + res = self.client.get(INDEX_URL) + self.assertTemplateUsed(res, 'infra_optim/strategies/index.html') + strategies = res.context['strategies_table'].data + self.assertItemsEqual(strategies, self.strategies.list()) + + @test.create_stubs({api.watcher.Strategy: ('list',)}) + def test_strategy_list_unavailable(self): + search_opts = {} + api.watcher.Strategy.list( + IsA(http.HttpRequest), **search_opts).MultipleTimes().AndRaise( + self.exceptions.watcher) + self.mox.ReplayAll() + + resp = self.client.get(INDEX_URL) + self.assertMessageCount(resp, error=1, warning=0) + + @test.create_stubs({api.watcher.Strategy: ('get',)}) + def test_details(self): + at = self.strategies.first() + at_id = at.uuid + api.watcher.Strategy.get( + IsA(http.HttpRequest), at_id).\ + MultipleTimes().AndReturn(at) + self.mox.ReplayAll() + DETAILS_URL = urlresolvers.reverse(DETAILS_VIEW, args=[at_id]) + res = self.client.get(DETAILS_URL) + self.assertTemplateUsed(res, + 'infra_optim/strategies/details.html') + strategies = res.context['strategy'] + self.assertItemsEqual([strategies], [at]) + + @test.create_stubs({api.watcher.Strategy: ('get',)}) + def test_details_exception(self): + at = self.strategies.first() + at_id = at.uuid + api.watcher.Strategy.get(IsA(http.HttpRequest), at_id) \ + .AndRaise(self.exceptions.watcher) + + self.mox.ReplayAll() + + DETAILS_URL = urlresolvers.reverse(DETAILS_VIEW, args=[at_id]) + res = self.client.get(DETAILS_URL) + self.assertRedirectsNoFollow(res, INDEX_URL) diff --git a/watcher_dashboard/content/strategies/urls.py b/watcher_dashboard/content/strategies/urls.py new file mode 100644 index 0000000..2ed7279 --- /dev/null +++ b/watcher_dashboard/content/strategies/urls.py @@ -0,0 +1,27 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2016 b<>com +# +# 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 import urls + +from watcher_dashboard.content.strategies import views + + +urlpatterns = [ + urls.url(r'^$', + views.IndexView.as_view(), name='index'), + urls.url(r'^(?P[^/]+)/detail$', + views.DetailView.as_view(), name='detail'), +] diff --git a/watcher_dashboard/content/strategies/views.py b/watcher_dashboard/content/strategies/views.py new file mode 100644 index 0000000..cb3df1f --- /dev/null +++ b/watcher_dashboard/content/strategies/views.py @@ -0,0 +1,95 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2016 b<>com +# +# 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.exceptions +import horizon.tables +import horizon.tabs +from horizon.utils import memoized +import horizon.workflows + +from watcher_dashboard.api import watcher +from watcher_dashboard.content.strategies import tables +from watcher_dashboard.content.strategies import tabs as wtabs + + +class IndexView(horizon.tables.DataTableView): + table_class = tables.StrategiesTable + template_name = 'infra_optim/strategies/index.html' + + def get_context_data(self, **kwargs): + context = super(IndexView, self).get_context_data(**kwargs) + context['strategies_count'] = self.get_strategies_count() + return context + + def get_data(self): + strategies = [] + search_opts = self.get_filters() + try: + strategies = watcher.Strategy.list(self.request, **search_opts) + except Exception: + horizon.exceptions.handle( + self.request, + _("Unable to retrieve strategy information.")) + return strategies + + def get_strategies_count(self): + return len(self.get_data()) + + def get_filters(self): + filters = {} + filter_action = self.table._meta._filter_action + if filter_action: + filter_field = self.table.get_filter_field() + if filter_action.is_api_filter(filter_field): + filter_string = self.table.get_filter_string() + if filter_field and filter_string: + filters[filter_field] = filter_string + return filters + + +class DetailView(horizon.tabs.TabbedTableView): + tab_group_class = wtabs.StrategyDetailTabs + template_name = 'infra_optim/strategies/details.html' + redirect_url = 'horizon:admin:strategies:index' + page_title = _("Strategy Details: {{ strategy.name }}") + + @memoized.memoized_method + def _get_data(self): + strategy_uuid = None + try: + strategy_uuid = self.kwargs['strategy_uuid'] + strategy = watcher.Strategy.get(self.request, strategy_uuid) + except Exception: + msg = _('Unable to retrieve details for strategy "%s".') \ + % strategy_uuid + horizon.exceptions.handle( + self.request, msg, + redirect=self.redirect_url) + return strategy + + def get_context_data(self, **kwargs): + context = super(DetailView, self).get_context_data(**kwargs) + strategy = self._get_data() + context["strategy"] = strategy + return context + + def get_tabs(self, request, *args, **kwargs): + strategy = self._get_data() + # ports = self._get_ports() + return self.tab_group_class(request, strategy=strategy, + # ports=ports, + **kwargs) diff --git a/watcher_dashboard/enabled/_31000_goals_panel.py b/watcher_dashboard/enabled/_31000_goals_panel.py new file mode 100644 index 0000000..8f9d467 --- /dev/null +++ b/watcher_dashboard/enabled/_31000_goals_panel.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. + +# The slug of the panel to be added to HORIZON_CONFIG. Required. +PANEL = 'goals' +# 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 = 'watcher' + +# Python panel class of the PANEL to be added. +ADD_PANEL = 'watcher_dashboard.content.goals.panel.Goals' diff --git a/watcher_dashboard/enabled/_31010_strategies_panel.py b/watcher_dashboard/enabled/_31010_strategies_panel.py new file mode 100644 index 0000000..051bdd8 --- /dev/null +++ b/watcher_dashboard/enabled/_31010_strategies_panel.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. + +# The slug of the panel to be added to HORIZON_CONFIG. Required. +PANEL = 'strategies' +# 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 = 'watcher' + +# Python panel class of the PANEL to be added. +ADD_PANEL = 'watcher_dashboard.content.strategies.panel.Strategies' diff --git a/watcher_dashboard/enabled/_31000_watcher_panelgroup.py b/watcher_dashboard/enabled/_31020_watcher_panelgroup.py similarity index 100% rename from watcher_dashboard/enabled/_31000_watcher_panelgroup.py rename to watcher_dashboard/enabled/_31020_watcher_panelgroup.py diff --git a/watcher_dashboard/enabled/_31010_audit_templates_panel.py b/watcher_dashboard/enabled/_31030_audit_templates_panel.py similarity index 90% rename from watcher_dashboard/enabled/_31010_audit_templates_panel.py rename to watcher_dashboard/enabled/_31030_audit_templates_panel.py index a01a187..1019191 100644 --- a/watcher_dashboard/enabled/_31010_audit_templates_panel.py +++ b/watcher_dashboard/enabled/_31030_audit_templates_panel.py @@ -16,8 +16,6 @@ PANEL = 'audit_templates' PANEL_DASHBOARD = 'admin' # The slug of the panel group the PANEL is associated with. PANEL_GROUP = 'watcher' -# If set, it will update the default panel of the PANEL_DASHBOARD. -DEFAULT_PANEL = 'audit_templates' # Python panel class of the PANEL to be added. ADD_PANEL = 'watcher_dashboard.content.audit_templates.panel.AuditTemplates' diff --git a/watcher_dashboard/enabled/_31020_audits_panel.py b/watcher_dashboard/enabled/_31040_audits_panel.py similarity index 90% rename from watcher_dashboard/enabled/_31020_audits_panel.py rename to watcher_dashboard/enabled/_31040_audits_panel.py index e97c44f..bdcc578 100644 --- a/watcher_dashboard/enabled/_31020_audits_panel.py +++ b/watcher_dashboard/enabled/_31040_audits_panel.py @@ -16,8 +16,6 @@ PANEL = 'audits' PANEL_DASHBOARD = 'admin' # The slug of the panel group the PANEL is associated with. PANEL_GROUP = 'watcher' -# If set, it will update the default panel of the PANEL_DASHBOARD. -DEFAULT_PANEL = 'audits' # Python panel class of the PANEL to be added. ADD_PANEL = 'watcher_dashboard.content.audits.panel.Audits' diff --git a/watcher_dashboard/enabled/_31030_action_plans_panel.py b/watcher_dashboard/enabled/_31050_action_plans_panel.py similarity index 90% rename from watcher_dashboard/enabled/_31030_action_plans_panel.py rename to watcher_dashboard/enabled/_31050_action_plans_panel.py index bcf73e6..ccc5c4f 100644 --- a/watcher_dashboard/enabled/_31030_action_plans_panel.py +++ b/watcher_dashboard/enabled/_31050_action_plans_panel.py @@ -16,8 +16,6 @@ PANEL = 'action_plans' PANEL_DASHBOARD = 'admin' # The slug of the panel group the PANEL is associated with. PANEL_GROUP = 'watcher' -# If set, it will update the default panel of the PANEL_DASHBOARD. -DEFAULT_PANEL = 'action_plans' # Python panel class of the PANEL to be added. ADD_PANEL = 'watcher_dashboard.content.action_plans.panel.ActionPlans' diff --git a/watcher_dashboard/enabled/_31040_actions_panel.py b/watcher_dashboard/enabled/_31060_actions_panel.py similarity index 90% rename from watcher_dashboard/enabled/_31040_actions_panel.py rename to watcher_dashboard/enabled/_31060_actions_panel.py index 420cb1c..8e3cc7e 100644 --- a/watcher_dashboard/enabled/_31040_actions_panel.py +++ b/watcher_dashboard/enabled/_31060_actions_panel.py @@ -16,8 +16,6 @@ PANEL = 'actions' PANEL_DASHBOARD = 'admin' # The slug of the panel group the PANEL is associated with. PANEL_GROUP = 'watcher' -# If set, it will update the default panel of the PANEL_DASHBOARD. -DEFAULT_PANEL = 'actions' # Python panel class of the PANEL to be added. ADD_PANEL = 'watcher_dashboard.content.actions.panel.Actions' diff --git a/watcher_dashboard/static/infra_optim/scss/infra_optim.scss b/watcher_dashboard/static/infra_optim/scss/infra_optim.scss index ca82f2e..f167f21 100644 --- a/watcher_dashboard/static/infra_optim/scss/infra_optim.scss +++ b/watcher_dashboard/static/infra_optim/scss/infra_optim.scss @@ -1,3 +1,3 @@ -/* Additional CSS for infra_optim. */ -@import "/dashboard/scss/variables"; -@import "/bootstrap/scss/bootstrap/variables"; +// /* Additional CSS for infra_optim. */ +// @import "/dashboard/scss/variables"; +// @import "/bootstrap/scss/bootstrap/variables"; diff --git a/watcher_dashboard/templates/infra_optim/actions/details.html b/watcher_dashboard/templates/infra_optim/actions/details.html index 840a475..aa89b63 100644 --- a/watcher_dashboard/templates/infra_optim/actions/details.html +++ b/watcher_dashboard/templates/infra_optim/actions/details.html @@ -11,7 +11,7 @@

{% trans "Audit Info" %}

-
{% trans "ID" %}
+
{% trans "UUID" %}
{{ action.uuid|default:"—" }}
{% trans "Type" %}
{{ action.type|default:"—" }}
diff --git a/watcher_dashboard/templates/infra_optim/audit_templates/_create.html b/watcher_dashboard/templates/infra_optim/audit_templates/_create.html index b6ec4d2..44e6afb 100644 --- a/watcher_dashboard/templates/infra_optim/audit_templates/_create.html +++ b/watcher_dashboard/templates/infra_optim/audit_templates/_create.html @@ -4,4 +4,7 @@ {% block modal-body-right %}

{% trans "Description:" %}

{% trans "Creates an audit template with specified parameters." %}

+

+ {% trans "Define the optimization goal to achieve, among those which are available." %} + {% trans "Optionaly, you can select the strategy used to achieve your goal. If not set, a strategy will be automatically selected among those which can be used for your goal" %}

{% endblock %} \ No newline at end of file diff --git a/watcher_dashboard/templates/infra_optim/audit_templates/details.html b/watcher_dashboard/templates/infra_optim/audit_templates/details.html index 193f748..3651a42 100644 --- a/watcher_dashboard/templates/infra_optim/audit_templates/details.html +++ b/watcher_dashboard/templates/infra_optim/audit_templates/details.html @@ -1,11 +1,11 @@ {% extends 'infra_optim/base.html' %} {% load i18n %} {% block title %}{% trans 'Audit Templates: ' %}{{ audit_template.name }}{% endblock %} - + {% block page_header %} {% include 'horizon/common/_page_header.html' with title=_('Audit Templates: ')|add:audit_template.name %} {% endblock page_header %} - + {% block main %}
@@ -13,10 +13,12 @@
{% trans "Name" %}
{{ audit_template.name|default:"—" }}
-
{% trans "Id" %}
+
{% trans "UUID" %}
{{ audit_template.uuid|default:"—" }}
-
{% trans "Goal" %}
-
{{ audit_template.goal|default:"—" }}
+
{% trans "Goal UUID" %}
+
{{ audit_template.goal_uuid|default:"—" }}
+
{% trans "Strategy UUID" %}
+
{{ audit_template.strategy_uuid|default:"—" }}
{% trans "Created At" %}
{{ audit_template.created_at|default:"—" }}
{% trans "Update At" %}
@@ -31,5 +33,5 @@ {{ table.render }}
- + {% endblock %} diff --git a/watcher_dashboard/templates/infra_optim/audits/details.html b/watcher_dashboard/templates/infra_optim/audits/details.html index ac213c3..421b8f5 100644 --- a/watcher_dashboard/templates/infra_optim/audits/details.html +++ b/watcher_dashboard/templates/infra_optim/audits/details.html @@ -1,17 +1,17 @@ {% extends 'infra_optim/base.html' %} {% load i18n %} {% block title %}{% trans 'Audits: ' %}{{ audit.uuid }}{% endblock %} - + {% block page_header %} {% include 'horizon/common/_page_header.html' with title=_('Audits: ')|add:audit.uuid %} {% endblock page_header %} - + {% block main %}

{% trans "Audit Info" %}

-
{% trans "ID" %}
+
{% trans "UUID" %}
{{ audit.uuid|default:"—" }}
{% trans "Type" %}
{{ audit.type|default:"—" }}
@@ -34,5 +34,5 @@ {{ table.render }}
- + {% endblock %} diff --git a/watcher_dashboard/templates/infra_optim/goals/details.html b/watcher_dashboard/templates/infra_optim/goals/details.html new file mode 100644 index 0000000..8866d10 --- /dev/null +++ b/watcher_dashboard/templates/infra_optim/goals/details.html @@ -0,0 +1,31 @@ +{% extends 'infra_optim/base.html' %} +{% load i18n %} +{% block title %}{% trans 'Goals: ' %}{{ goal.display_name }}{% endblock %} + +{% block page_header %} + {% include 'horizon/common/_page_header.html' with title=_('Goals: ')|add:goal.display_name %} +{% endblock page_header %} + +{% block main %} +
+
+

{% trans "Goal Info" %}

+
+
{% trans "UUID" %}
+
{{ goal.uuid|default:"—" }}
+
{% trans "Name" %}
+
{{ goal.display_name|default:"—" }}
+
{% trans "Created At" %}
+
{{ goal.created_at|default:"—" }}
+
{% trans "Update At" %}
+
{{ goal.updated_at|default:"—" }}
+
+
+
+
+
+ {{ table.render }} +
+
+ +{% endblock %} diff --git a/watcher_dashboard/templates/infra_optim/goals/index.html b/watcher_dashboard/templates/infra_optim/goals/index.html new file mode 100644 index 0000000..f1bafbc --- /dev/null +++ b/watcher_dashboard/templates/infra_optim/goals/index.html @@ -0,0 +1,14 @@ +{% extends 'infra_optim/base.html' %} +{% load i18n %} +{% load url from future %} +{% block title %}{% trans 'Goals' %}{% endblock %} + +{% block page_header %} + {% include 'horizon/common/_page_header.html' with title=_('Goals') items_count=goal_count %} +{% endblock page_header %} + +{% block main %} +
+ {{ goals_table.render }} +
+{% endblock %} diff --git a/watcher_dashboard/templates/infra_optim/strategies/details.html b/watcher_dashboard/templates/infra_optim/strategies/details.html new file mode 100644 index 0000000..02c9199 --- /dev/null +++ b/watcher_dashboard/templates/infra_optim/strategies/details.html @@ -0,0 +1,34 @@ +{% extends 'infra_optim/base.html' %} +{% load i18n %} +{% block title %}{% trans 'Strategies: ' %}{{ strategy.display_name }}{% endblock %} + +{% block page_header %} + {% include 'horizon/common/_page_header.html' with title=_('Strategies: ')|add:strategy.display_name %} +{% endblock page_header %} + +{% block main %} +
+
+

{% trans "Strategy Info" %}

+
+
{% trans "UUID" %}
+
{{ strategy.uuid|default:"—" }}
+
{% trans "Name" %}
+
{{ strategy.display_name|default:"—" }}
+
{% trans "Goal UUID" %}
+ {% url 'horizon:admin:goals:detail' strategy.goal_uuid as goal_url %} +
{{ strategy.goal_uuid|default:"—" }}
+
{% trans "Created At" %}
+
{{ strategy.created_at|default:"—" }}
+
{% trans "Update At" %}
+
{{ strategy.updated_at|default:"—" }}
+
+
+
+
+
+ {{ table.render }} +
+
+ +{% endblock %} diff --git a/watcher_dashboard/templates/infra_optim/strategies/index.html b/watcher_dashboard/templates/infra_optim/strategies/index.html new file mode 100644 index 0000000..79a9124 --- /dev/null +++ b/watcher_dashboard/templates/infra_optim/strategies/index.html @@ -0,0 +1,14 @@ +{% extends 'infra_optim/base.html' %} +{% load i18n %} +{% load url from future %} +{% block title %}{% trans 'Strategies' %}{% endblock %} + +{% block page_header %} + {% include 'horizon/common/_page_header.html' with title=_('Strategies') items_count=strategy_count %} +{% endblock page_header %} + +{% block main %} +
+ {{ strategies_table.render }} +
+{% endblock %} diff --git a/watcher_dashboard/test/api_tests/watcher_tests.py b/watcher_dashboard/test/api_tests/watcher_tests.py index 32d3bcb..92cab12 100644 --- a/watcher_dashboard/test/api_tests/watcher_tests.py +++ b/watcher_dashboard/test/api_tests/watcher_tests.py @@ -21,37 +21,95 @@ from watcher_dashboard.test import helpers as test class WatcherAPITests(test.APITestCase): + def test_goal_list(self): + goals = {'goals': self.api_goals.list()} + watcherclient = self.stub_watcherclient() + + watcherclient.goal = self.mox.CreateMockAnything() + watcherclient.goal.list(detail=True).AndReturn(goals) + self.mox.ReplayAll() + + ret_val = api.watcher.Goal.list(self.request) + self.assertIsInstance(ret_val, dict) + self.assertIn('goals', ret_val) + for n in ret_val['goals']: + self.assertIsInstance(n, dict) + + def test_goal_get(self): + goal = self.api_goals.first() + goal_id = self.api_goals.first()['uuid'] + + watcherclient = self.stub_watcherclient() + watcherclient.goal = self.mox.CreateMockAnything() + watcherclient.goal.get(goal_id).AndReturn(goal) + self.mox.ReplayAll() + + ret_val = api.watcher.Goal.get(self.request, goal_id) + self.assertIsInstance(ret_val, dict) + + def test_strategy_list(self): + strategies = {'strategies': self.api_strategies.list()} + watcherclient = self.stub_watcherclient() + + watcherclient.strategy = self.mox.CreateMockAnything() + watcherclient.strategy.list( + goal_uuid=None, detail=True).AndReturn(strategies) + self.mox.ReplayAll() + + ret_val = api.watcher.Strategy.list(self.request) + self.assertIn('strategies', ret_val) + for n in ret_val['strategies']: + self.assertIsInstance(n, dict) + + def test_strategy_get(self): + strategy = self.api_strategies.first() + strategy_id = self.api_strategies.first()['uuid'] + + watcherclient = self.stub_watcherclient() + watcherclient.strategy = self.mox.CreateMockAnything() + watcherclient.strategy.get(strategy_id).AndReturn(strategy) + self.mox.ReplayAll() + + ret_val = api.watcher.Strategy.get(self.request, strategy_id) + self.assertIsInstance(ret_val, dict) + def test_audit_template_list(self): audit_templates = {'audit_templates': self.api_audit_templates.list()} watcherclient = self.stub_watcherclient() watcherclient.audit_template = self.mox.CreateMockAnything() - watcherclient.audit_template.list(name=None).AndReturn(audit_templates) + watcherclient.audit_template.list().AndReturn(audit_templates) self.mox.ReplayAll() - ret_val = api.watcher.AuditTemplate.list(self.request, filter=None) - for n in ret_val: - self.assertTrue(type(n), 'dict') + ret_val = api.watcher.AuditTemplate.list(self.request) + + self.assertIn('audit_templates', ret_val) + for n in ret_val['audit_templates']: + self.assertIsInstance(n, dict) def test_audit_template_list_with_filters(self): - search_opts = 'Audit Template 1' - audit_templates = self.api_audit_templates.filter(name=search_opts) + search_opts = {'name': 'Audit Template 1'} + audit_templates = { + 'audit_templates': self.api_audit_templates.filter(**search_opts)} watcherclient = self.stub_watcherclient() watcherclient.audit_template = self.mox.CreateMockAnything() - watcherclient.audit_template.list(name=search_opts)\ - .AndReturn(audit_templates) + watcherclient.audit_template.list( + **search_opts).AndReturn(audit_templates) self.mox.ReplayAll() - ret_val = api.watcher.AuditTemplate\ - .list(self.request, filter=search_opts) - for n in ret_val: - self.assertTrue(type(n), 'dict') + ret_val = api.watcher.AuditTemplate.list( + self.request, **search_opts) + + self.assertIn('audit_templates', ret_val) + for n in ret_val['audit_templates']: + self.assertIsInstance(n, dict) + self.assertEqual(ret_val, audit_templates) def test_audit_template_get(self): - audit_template = {'audit_template': self.api_audit_templates.first()} + audit_template = self.api_audit_templates.first() audit_template_id = self.api_audit_templates.first()['uuid'] watcherclient = self.stub_watcherclient() @@ -62,30 +120,33 @@ class WatcherAPITests(test.APITestCase): ret_val = api.watcher.AuditTemplate.get(self.request, audit_template_id) - self.assertTrue(type(ret_val), 'dict') + self.assertIsInstance(ret_val, dict) def test_audit_template_create(self): - audit_template = {'audit_template': self.api_audit_templates.first()} - name = self.api_audit_templates.first()['name'] - goal = self.api_audit_templates.first()['goal'] - description = self.api_audit_templates.first()['description'] - host_aggregate = self.api_audit_templates.first()['host_aggregate'] + audit_template = self.api_audit_templates.first() + name = audit_template['name'] + goal_uuid = audit_template['goal_uuid'] + strategy_uuid = audit_template['strategy_uuid'] + description = audit_template['description'] + host_aggregate = audit_template['host_aggregate'] watcherclient = self.stub_watcherclient() watcherclient.audit_template = self.mox.CreateMockAnything() watcherclient.audit_template.create( name=name, - goal=goal, + goal_uuid=goal_uuid, + strategy_uuid=strategy_uuid, description=description, host_aggregate=host_aggregate).AndReturn(audit_template) self.mox.ReplayAll() ret_val = api.watcher.AuditTemplate.create( - self.request, name, goal, description, host_aggregate) - self.assertTrue(type(ret_val), 'dict') + self.request, name, goal_uuid, strategy_uuid, + description, host_aggregate) + self.assertIsInstance(ret_val, dict) def test_audit_template_patch(self): - audit_template = {'audit_template': self.api_audit_templates.first()} + audit_template = self.api_audit_templates.first() audit_template_id = self.api_audit_templates.first()['uuid'] form_data = {'name': 'new Audit Template 1'} @@ -100,7 +161,7 @@ class WatcherAPITests(test.APITestCase): ret_val = api.watcher.AuditTemplate.patch( self.request, audit_template_id, form_data) - self.assertTrue(type(ret_val), 'dict') + self.assertIsInstance(ret_val, dict) def test_audit_template_delete(self): audit_template_list = self.api_audit_templates.list() @@ -127,26 +188,26 @@ class WatcherAPITests(test.APITestCase): self.mox.ReplayAll() ret_val = api.watcher.Audit.list( - self.request, - audit_template_filter=None) - for n in ret_val: - self.assertIsInstance(n, api.watcher.Audit) + self.request, audit_template_filter=None) + + self.assertIn('audits', ret_val) + for n in ret_val['audits']: + self.assertIsInstance(n, dict) def test_audit_get(self): - audit = {'audit': self.api_audits.first()} - audit_id = self.api_audits.first()['id'] + audit = self.api_audits.first() + audit_id = self.api_audits.first()['uuid'] watcherclient = self.stub_watcherclient() watcherclient.audit = self.mox.CreateMockAnything() - watcherclient.audit.get( - audit_id=audit_id).AndReturn(audit) + watcherclient.audit.get(audit_id=audit_id).AndReturn(audit) self.mox.ReplayAll() ret_val = api.watcher.Audit.get(self.request, audit_id) - self.assertIsInstance(ret_val, api.watcher.Audit) + self.assertIsInstance(ret_val, dict) def test_audit_create(self): - audit = {'audit': self.api_audits.first()} + audit = self.api_audits.first() audit_template_id = self.api_audit_templates.first()['uuid'] deadline = self.api_audits.first()['deadline'] @@ -163,10 +224,10 @@ class WatcherAPITests(test.APITestCase): ret_val = api.watcher.Audit.create( self.request, audit_template_uuid, _type, deadline) - self.assertIsInstance(ret_val, api.watcher.Audit) + self.assertIsInstance(ret_val, dict) def test_audit_delete(self): - audit_id = self.api_audits.first()['id'] + audit_id = self.api_audits.first()['uuid'] watcherclient = self.stub_watcherclient() watcherclient.audit = self.mox.CreateMockAnything() @@ -185,15 +246,15 @@ class WatcherAPITests(test.APITestCase): watcherclient.action_plan.list(audit=None).AndReturn(action_plans) self.mox.ReplayAll() - ret_val = api.watcher.ActionPlan.list( - self.request, - audit_filter=None) - for n in ret_val: - self.assertIsInstance(n, api.watcher.ActionPlan) + ret_val = api.watcher.ActionPlan.list(self.request, audit_filter=None) + + self.assertIn('action_plans', ret_val) + for n in ret_val['action_plans']: + self.assertIsInstance(n, dict) def test_action_plan_get(self): - action_plan = {'action_plan': self.api_action_plans.first()} - action_plan_id = self.api_action_plans.first()['id'] + action_plan = self.api_action_plans.first() + action_plan_id = self.api_action_plans.first()['uuid'] watcherclient = self.stub_watcherclient() watcherclient.action_plan = self.mox.CreateMockAnything() @@ -202,10 +263,10 @@ class WatcherAPITests(test.APITestCase): self.mox.ReplayAll() ret_val = api.watcher.ActionPlan.get(self.request, action_plan_id) - self.assertIsInstance(ret_val, api.watcher.ActionPlan) + self.assertIsInstance(ret_val, dict) def test_action_plan_start(self): - action_plan_id = self.api_action_plans.first()['id'] + action_plan_id = self.api_action_plans.first()['uuid'] patch = [] patch.append({'path': '/state', 'value': 'PENDING', 'op': 'replace'}) @@ -217,7 +278,7 @@ class WatcherAPITests(test.APITestCase): api.watcher.ActionPlan.start(self.request, action_plan_id) def test_action_plan_delete(self): - action_plan_id = self.api_action_plans.first()['id'] + action_plan_id = self.api_action_plans.first()['uuid'] watcherclient = self.stub_watcherclient() watcherclient.action_plan = self.mox.CreateMockAnything() @@ -237,7 +298,8 @@ class WatcherAPITests(test.APITestCase): self.mox.ReplayAll() ret_val = api.watcher.Action.list( - self.request, - action_plan_filter=None) - for n in ret_val: - self.assertIsInstance(n, api.watcher.Action) + self.request, action_plan_filter=None) + + self.assertIn('actions', ret_val) + for n in ret_val['actions']: + self.assertIsInstance(n, dict) diff --git a/watcher_dashboard/test/helpers.py b/watcher_dashboard/test/helpers.py index 367092b..6e4fe24 100644 --- a/watcher_dashboard/test/helpers.py +++ b/watcher_dashboard/test/helpers.py @@ -223,7 +223,7 @@ class TestCase(horizon_helpers.TestCase): Asserts that the given response issued a 302 redirect without processing the view which is redirected to. """ - assert (response.status_code / 100 == 3), \ + assert (300 <= response.status_code < 400), \ "The response did not return a redirect." self.assertEqual(response._headers.get('location', None), ('Location', settings.TESTSERVER + expected_url)) diff --git a/watcher_dashboard/test/integration_tests/pages/admin/optimization/auditspage.py b/watcher_dashboard/test/integration_tests/pages/admin/optimization/auditspage.py index 6cf7692..7268991 100644 --- a/watcher_dashboard/test/integration_tests/pages/admin/optimization/auditspage.py +++ b/watcher_dashboard/test/integration_tests/pages/admin/optimization/auditspage.py @@ -29,7 +29,7 @@ class AuditsTable(tables.TableRegion): launch_button.click() return forms.BaseFormRegion(self.driver, self.conf) - @tables.bind_row_action('go_to_action_plan', primary=True) + @tables.bind_row_action('go_to_action_plan') def go_to_action_plan(self, goto_button): goto_button.click() return forms.BaseFormRegion(self.driver, self.conf) diff --git a/watcher_dashboard/test/integration_tests/pages/admin/optimization/audittemplatespage.py b/watcher_dashboard/test/integration_tests/pages/admin/optimization/audittemplatespage.py index ee4a708..200f6c1 100644 --- a/watcher_dashboard/test/integration_tests/pages/admin/optimization/audittemplatespage.py +++ b/watcher_dashboard/test/integration_tests/pages/admin/optimization/audittemplatespage.py @@ -21,7 +21,8 @@ class AuditTemplatesTable(tables.TableRegion): name = 'audit_templates' - CREATE_AUDIT_TEMPLATE_FORM_FIELDS = ("name", "description", "goal") + CREATE_AUDIT_TEMPLATE_FORM_FIELDS = ("name", "description", + "goal_id", "strategy_id") @tables.bind_table_action('create') def create_audit_template(self, create_button): @@ -35,7 +36,7 @@ class AuditTemplatesTable(tables.TableRegion): delete_button.click() return forms.BaseFormRegion(self.driver, self.conf, None) - @tables.bind_row_action('launch_audit', primary=True) + @tables.bind_row_action('launch_audit') def launch_audit(self, launch_button, row): launch_button.click() return forms.BaseFormRegion(self.driver, self.conf) @@ -44,7 +45,7 @@ class AuditTemplatesTable(tables.TableRegion): class AudittemplatesPage(basepage.BaseNavigationPage): DEFAULT_DESCRIPTION = "Fake description from integration tests" - DEFAULT_GOAL = "BASIC_CONSOLIDATION" + DEFAULT_GOAL = "SERVER_CONSOLIDATION" AUDITS_PAGE_TITLE = "Audits - OpenStack Dashboard" @@ -86,11 +87,11 @@ class AudittemplatesPage(basepage.BaseNavigationPage): def create_audit_template(self, name, description=DEFAULT_DESCRIPTION, - goal=DEFAULT_GOAL): + goal_id=DEFAULT_GOAL): self.audittemplates_table.create_audit_template() self.audit_templates__action_create_form.name.text = name self.audit_templates__action_create_form.description.text = description - self.audit_templates__action_create_form.goal.value = goal + self.audit_templates__action_create_form.goal_id.value = goal_id self.audit_templates__action_create_form.submit() def is_audit_template_present(self, name): diff --git a/watcher_dashboard/test/integration_tests/tests/audit_template_panel_test.py b/watcher_dashboard/test/integration_tests/tests/audit_template_panel_test.py index 449a3fe..97ce352 100644 --- a/watcher_dashboard/test/integration_tests/tests/audit_template_panel_test.py +++ b/watcher_dashboard/test/integration_tests/tests/audit_template_panel_test.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import unittest import uuid from openstack_dashboard.test.integration_tests import helpers @@ -66,7 +65,6 @@ class AuditTemplatePanelTests(helpers.AdminTestCase): audit_template_page.show_audit_template_info( self.audit_template_name)) - @unittest.skip(reason="https://bugs.launchpad.net/horizon/+bug/1537526") def test_launch_audit(self): """Test the audit template panel "Launch Audit" row button diff --git a/watcher_dashboard/test/test_data/watcher_data.py b/watcher_dashboard/test/test_data/watcher_data.py index 4c9e818..f011e22 100644 --- a/watcher_dashboard/test/test_data/watcher_data.py +++ b/watcher_dashboard/test/test_data/watcher_data.py @@ -34,6 +34,50 @@ def data(TEST): "publicURL": "http://public.watcher2.example.com:9322"}]}, ) + TEST.goals = utils.TestDataContainer() + TEST.api_goals = utils.TestDataContainer() + goal_dict1 = { + 'uuid': 'gggggggg-1111-1111-1111-gggggggggggg', + 'name': 'MINIMIZE_LICENSING_COST', + 'display_name': 'Dummy', + } + goal_dict2 = { + 'uuid': 'gggggggg-2222-2222-2222-gggggggggggg', + 'name': 'SERVER_CONSOLIDATION', + 'display_name': 'Server consolidation', + } + TEST.api_goals.add(goal_dict1) + TEST.api_goals.add(goal_dict2) + _goal_dict1 = copy.deepcopy(goal_dict1) + _goal_dict2 = copy.deepcopy(goal_dict2) + + TEST.strategies = utils.TestDataContainer() + TEST.api_strategies = utils.TestDataContainer() + strategy_dict1 = { + 'uuid': 'ssssssss-1111-1111-1111-ssssssssssss', + 'name': 'minimize_licensing_cost1', + 'goal_uuid': 'gggggggg-1111-1111-1111-gggggggggggg', + 'display_name': 'Fake licensing cost strategy1', + } + strategy_dict2 = { + 'uuid': 'ssssssss-2222-2222-2222-ssssssssssss', + 'name': 'minimize_licensing_cost2', + 'goal_uuid': 'gggggggg-1111-1111-1111-gggggggggggg', + 'display_name': 'Fake licensing cost strategy2', + } + strategy_dict3 = { + 'uuid': 'ssssssss-3333-3333-3333-ssssssssssss', + 'name': 'sercon', + 'goal_uuid': 'gggggggg-2222-2222-2222-gggggggggggg', + 'display_name': 'Fake Sercon', + } + TEST.api_strategies.add(strategy_dict1) + TEST.api_strategies.add(strategy_dict2) + TEST.api_strategies.add(strategy_dict3) + _strategy_dict1 = copy.deepcopy(strategy_dict1) + _strategy_dict2 = copy.deepcopy(strategy_dict2) + _strategy_dict3 = copy.deepcopy(strategy_dict3) + TEST.audit_templates = utils.TestDataContainer() TEST.api_audit_templates = utils.TestDataContainer() audit_template_dict = { @@ -42,7 +86,8 @@ def data(TEST): 'description': 'Audit Template 1 description', 'host_aggregate': None, 'extra': {'automatic': False}, - 'goal': 'MINIMIZE_LICENSING_COST' + 'goal_uuid': 'gggggggg-1111-1111-1111-gggggggggggg', + 'strategy_uuid': 'ssssssss-1111-1111-1111-ssssssssssss', } audit_template_dict2 = { 'uuid': '11111111-2222-2222-2222-111111111111', @@ -50,7 +95,8 @@ def data(TEST): 'description': 'Audit Template 2 description', 'host_aggregate': None, 'extra': {'automatic': False}, - 'goal': 'MINIMIZE_LICENSING_COST' + 'goal_uuid': 'gggggggg-1111-1111-1111-gggggggggggg', + 'strategy_uuid': 'ssssssss-2222-2222-2222-ssssssssssss', } audit_template_dict3 = { 'uuid': '11111111-3333-3333-3333-111111111111', @@ -58,7 +104,8 @@ def data(TEST): 'description': 'Audit Template 3 description', 'host_aggregate': None, 'extra': {'automatic': False}, - 'goal': 'MINIMIZE_LICENSING_COST' + 'goal_uuid': 'gggggggg-2222-2222-2222-gggggggggggg', + 'strategy_uuid': None, } TEST.api_audit_templates.add(audit_template_dict) TEST.api_audit_templates.add(audit_template_dict2) @@ -67,13 +114,10 @@ def data(TEST): _audit_template_dict2 = copy.deepcopy(audit_template_dict2) _audit_template_dict3 = copy.deepcopy(audit_template_dict3) - TEST.goals = utils.TestDataContainer() - TEST.api_goals = utils.TestDataContainer() - TEST.audits = utils.TestDataContainer() TEST.api_audits = utils.TestDataContainer() audit_dict = { - 'id': '22222222-2222-2222-2222-222222222222', + 'uuid': '22222222-2222-2222-2222-222222222222', 'deadline': None, 'type': 'ONESHOT', 'audit_template_uuid': '11111111-1111-1111-1111-111111111111' @@ -84,7 +128,7 @@ def data(TEST): TEST.action_plans = utils.TestDataContainer() TEST.api_action_plans = utils.TestDataContainer() action_plan_dict = { - 'id': '33333333-3333-3333-3333-333333333333', + 'uuid': '33333333-3333-3333-3333-333333333333', 'state': 'RECOMMENDED', 'first_action_uuid': '44444444-4444-4444-4444-111111111111', 'audit_uuid': '22222222-2222-2222-2222-222222222222' @@ -95,7 +139,7 @@ def data(TEST): TEST.actions = utils.TestDataContainer() TEST.api_actions = utils.TestDataContainer() action_dict1 = { - 'id': '44444444-4444-4444-4444-111111111111', + 'uuid': '44444444-4444-4444-4444-111111111111', 'state': 'PENDING', 'next_uuid': '44444444-4444-4444-4444-222222222222', 'action_plan_uuid': '33333333-3333-3333-3333-333333333333' @@ -103,7 +147,7 @@ def data(TEST): TEST.api_actions.add(action_dict1) action_dict2 = { - 'id': '44444444-4444-4444-4444-222222222222', + 'uuid': '44444444-4444-4444-4444-222222222222', 'state': 'PENDING', 'next_uuid': None, 'action_plan_uuid': '33333333-3333-3333-3333-333333333333' @@ -121,11 +165,23 @@ def data(TEST): _audit_dict['action_plans'] = [action_plan] audit = watcher.Audit(_audit_dict) - # _audit_template_dict['audits'] = [audit] + goal1 = watcher.Goal(_goal_dict1) + goal2 = watcher.Goal(_goal_dict2) + + strategy1 = watcher.Strategy(_strategy_dict1) + strategy2 = watcher.Strategy(_strategy_dict2) + strategy3 = watcher.Strategy(_strategy_dict3) + audit_template1 = watcher.AuditTemplate(_audit_template_dict) audit_template2 = watcher.AuditTemplate(_audit_template_dict2) audit_template3 = watcher.AuditTemplate(_audit_template_dict3) + TEST.goals.add(goal1) + TEST.goals.add(goal2) + TEST.strategies.add(strategy1) + TEST.strategies.add(strategy2) + TEST.strategies.add(strategy3) + TEST.audit_templates.add(audit_template1) TEST.audit_templates.add(audit_template2) TEST.audit_templates.add(audit_template3) diff --git a/watcher_dashboard/test/urls.py b/watcher_dashboard/test/urls.py index 9bef20f..cf28475 100644 --- a/watcher_dashboard/test/urls.py +++ b/watcher_dashboard/test/urls.py @@ -14,7 +14,6 @@ from django.conf import urls import openstack_dashboard.urls -urlpatterns = urls.patterns( - '', +urlpatterns = [ urls.url(r'', urls.include(openstack_dashboard.urls)) -) +]