diff --git a/freezer_ui/api/__init__.py b/freezer_ui/api/__init__.py index d95ce28..e69de29 100644 --- a/freezer_ui/api/__init__.py +++ b/freezer_ui/api/__init__.py @@ -1 +0,0 @@ -__author__ = 'jonas' diff --git a/freezer_ui/api/api.py b/freezer_ui/api/api.py index 4047e5a..1638fbb 100644 --- a/freezer_ui/api/api.py +++ b/freezer_ui/api/api.py @@ -90,6 +90,18 @@ class ActionJob(object): self.backup_name = backup_name +class Session(object): + def __init__(self, session_id, description, status, jobs, + start_datetime, interval, end_datetime): + self.session_id = session_id + self.description = description + self.status = status + self.jobs = jobs + self.start_datetime = start_datetime + self.interval = interval + self.end_datetime = end_datetime + + @memoized def get_service_url(request): """ Get Freezer API url from keystone catalog. @@ -263,3 +275,74 @@ def client_list(request): clients = _freezerclient(request).registration.list() clients = [Client(client) for client in clients] return clients + + +def add_job_to_session(request, session_id, job_id): + """This function will add a job to a session and the API will handle the + copy of job information to the session """ + return _freezerclient(request).sessions.add_job(session_id, job_id) + + +def remove_job_from_session(request, session_id, job_id): + """Remove a job from a session will delete the job information but not the + job itself """ + return _freezerclient(request).sessions.remove_job(session_id, job_id) + + +def session_create(request, context): + """A session is a group of jobs who share the same scheduling time. """ + session = create_dict_action(**context) + session['description'] = session.pop('description', None) + schedule = { + 'end_datetime': session.pop('end_datetime', None), + 'interval': session.pop('interval', None), + 'start_datetime': session.pop('start_datetime', None), + } + session['schedule'] = schedule + return _freezerclient(request).sessions.create(session) + + +def session_update(request, context): + """Update session information """ + session = create_dict_action(**context) + session_id = session.pop('session_id', None) + session['description'] = session.pop('description', None) + schedule = { + 'end_datetime': session.pop('end_datetime', None), + 'interval': session.pop('interval', None), + 'start_datetime': session.pop('start_datetime', None), + } + session['schedule'] = schedule + return _freezerclient(request).sessions.update(session_id, session) + + +def session_delete(request, session_id): + """Delete session from API """ + return _freezerclient(request).sessions.delete(session_id) + + +def session_list(request): + """List all sessions """ + sessions = _freezerclient(request).sessions.list_all() + sessions = [Session(s['session_id'], + s['description'], + s['status'], + s['jobs'], + s['schedule']['start_datetime'], + s['schedule']['interval'], + s['schedule']['end_datetime']) + for s in sessions] + return sessions + + +def session_get(request, session_id): + """Get a single session """ + session = _freezerclient(request).sessions.get(session_id) + session = Session(session['session_id'], + session['description'], + session['status'], + session['jobs'], + session['schedule']['start_datetime'], + session['schedule']['interval'], + session['schedule']['end_datetime']) + return session diff --git a/freezer_ui/dashboard.py b/freezer_ui/dashboard.py index d0604e2..b6206ff 100644 --- a/freezer_ui/dashboard.py +++ b/freezer_ui/dashboard.py @@ -18,7 +18,7 @@ import horizon class FreezerDR(horizon.PanelGroup): slug = "freezerdr" name = _("Backup and Restore") - panels = ('jobs',) + panels = ('jobs', 'sessions') class Freezer(horizon.Dashboard): diff --git a/freezer_ui/jobs/tables.py b/freezer_ui/jobs/tables.py index dd3a27d..9a73b96 100644 --- a/freezer_ui/jobs/tables.py +++ b/freezer_ui/jobs/tables.py @@ -47,6 +47,20 @@ def format_last_backup(last_backup): 'en="true"> {}'.format(colour, icon, text)) +class AttachJobToSession(tables.LinkAction): + name = "attach_job_to_session" + verbose_name = _("Attach To Session") + classes = ("ajax-modal") + url = "horizon:freezer_ui:sessions:attach" + + def allowed(self, request, instance): + return True + + def get_link_url(self, datum): + return reverse("horizon:freezer_ui:sessions:attach", + kwargs={'job_id': datum.job_id}) + + class Restore(tables.Action): name = "restore" verbose_name = _("Restore") @@ -149,6 +163,7 @@ class JobsTable(tables.DataTable): multi_select = False row_actions = (CreateAction, EditJob, + AttachJobToSession, CloneJob, DeleteJob,) diff --git a/freezer_ui/jobs/templates/jobs/snapshot.html b/freezer_ui/jobs/templates/jobs/snapshot.html deleted file mode 100644 index 36c0a71..0000000 --- a/freezer_ui/jobs/templates/jobs/snapshot.html +++ /dev/null @@ -1,7 +0,0 @@ -{% load i18n horizon humanize %} - -{% block help_message %} -{% endblock %} - - - \ No newline at end of file diff --git a/freezer_ui/sessions/__init__.py b/freezer_ui/sessions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/freezer_ui/sessions/browsers.py b/freezer_ui/sessions/browsers.py new file mode 100644 index 0000000..a775455 --- /dev/null +++ b/freezer_ui/sessions/browsers.py @@ -0,0 +1,27 @@ +# Copyright 2012 Nebula, Inc. +# Copyright 2015 Hewlett-Packard +# +# 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 browsers +from horizon_web_ui.freezer_ui.sessions import tables + + +class SessionBrowser(browsers.ResourceBrowser): + name = "session_configuration" + verbose_name = _("Session Configuration") + navigation_table_class = tables.SessionsTable + content_table_class = tables.JobsTable + navigable_item_name = _("Sessions") + navigation_kwarg_name = "session_id" diff --git a/freezer_ui/sessions/panel.py b/freezer_ui/sessions/panel.py new file mode 100644 index 0000000..cc28800 --- /dev/null +++ b/freezer_ui/sessions/panel.py @@ -0,0 +1,24 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.utils.translation import ugettext_lazy as _ + +import horizon +from horizon_web_ui.freezer_ui import dashboard + + +class SessionsPanel(horizon.Panel): + name = _("Sessions") + slug = "sessions" + + +dashboard.Freezer.register(SessionsPanel) diff --git a/freezer_ui/sessions/tables.py b/freezer_ui/sessions/tables.py new file mode 100644 index 0000000..c454169 --- /dev/null +++ b/freezer_ui/sessions/tables.py @@ -0,0 +1,154 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import datetime +from django import shortcuts +from django.utils import safestring +from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ungettext_lazy + +from horizon import messages +from horizon import tables +from horizon.utils.urlresolvers import reverse + +import horizon_web_ui.freezer_ui.api.api as freezer_api +from horizon_web_ui.freezer_ui.django_utils import timestamp_to_string + + +def get_link(session): + return reverse('horizon:freezer_ui:sessions:index', + kwargs={'session_id': session.session_id}) + + +class CreateJob(tables.LinkAction): + name = "create_session" + verbose_name = _("Create Session") + url = "horizon:freezer_ui:sessions:create" + classes = ("ajax-modal",) + icon = "plus" + + +class DeleteSession(tables.DeleteAction): + name = "delete" + classes = ("btn-danger",) + icon = "remove" + help_text = _("Delete sessions is not recoverable.") + + @staticmethod + def action_present(count): + return ungettext_lazy( + u"Delete Session", + u"Delete Sessions", + count + ) + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Deleted Session", + u"Deleted Sessions", + count + ) + + def delete(self, request, session_id): + return freezer_api.session_delete(request, session_id) + + +class EditSession(tables.LinkAction): + name = "edit_session" + verbose_name = _("Edit Session") + classes = ("ajax-modal",) + icon = "pencil" + + def get_link_url(self, datum=None): + return reverse("horizon:freezer_ui:sessions:edit", + kwargs={'session_id': datum.session_id}) + + +class DeleteMultipleActions(DeleteSession): + name = "delete_multiple_actions" + + +class DeleteJobFromSession(tables.DeleteAction): + name = "delete_job_from_session" + classes = ("btn-danger",) + icon = "remove" + help_text = _("Delete jobs is not recoverable.") + + @staticmethod + def action_present(count): + return ungettext_lazy( + u"Delete Job", + u"Delete Jobs", + count + ) + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Deleted Job", + u"Deleted Jobs", + count + ) + + def delete(self, request, session): + job_id, session_id = session.split('===') + return freezer_api.remove_job_from_session( + request, + session_id, + job_id) + + +class JobsTable(tables.DataTable): + client_id = tables.Column( + 'client_id', + verbose_name=_("Client ID")) + + status = tables.Column( + 'status', + verbose_name=_("Status")) + + def get_object_id(self, job): + # this is used to pass to values as an url + # TODO: look for a way to improve this + ids = '{0}==={1}'.format(job.job_id, job.session_id) + return ids + + class Meta(object): + name = "jobs" + verbose_name = _("Jobs") + table_actions = () + row_actions = (DeleteJobFromSession,) + footer = False + multi_select = True + + +class SessionsTable(tables.DataTable): + description = tables.Column('description', + link=get_link, + verbose_name=_("Session")) + + status = tables.Column('status', + verbose_name=_("Status")) + + def get_object_id(self, session): + return session.session_id + + class Meta(object): + name = "sessions" + verbose_name = _("Sessions") + table_actions = (CreateJob, + DeleteMultipleActions) + row_actions = (EditSession, + DeleteSession,) + footer = False + multi_select = True diff --git a/freezer_ui/sessions/templates/sessions/browser.html b/freezer_ui/sessions/templates/sessions/browser.html new file mode 100644 index 0000000..d1e7818 --- /dev/null +++ b/freezer_ui/sessions/templates/sessions/browser.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Session Configurations" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Session Configurations") %} +{% endblock page_header %} + +{% block main %} + {{ session_configuration_browser.render }} +{% endblock %} diff --git a/freezer_ui/sessions/urls.py b/freezer_ui/sessions/urls.py new file mode 100644 index 0000000..11adc48 --- /dev/null +++ b/freezer_ui/sessions/urls.py @@ -0,0 +1,38 @@ +# 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 patterns +from django.conf.urls import url + +from horizon_web_ui.freezer_ui.sessions import views + + +urlpatterns = patterns( + '', + + url(r'^(?P[^/]+)?$', + views.SessionsView.as_view(), + name='index'), + + url(r'^attach_to_session/(?P[^/]+)?$', + views.AttachToSessionWorkflow.as_view(), + name='attach'), + + url(r'^create/$', + views.CreateSessionWorkflow.as_view(), + name='create'), + + url(r'^edit/(?P[^/]+)?$', + views.CreateSessionWorkflow.as_view(), + name='edit'), + +) diff --git a/freezer_ui/sessions/views.py b/freezer_ui/sessions/views.py new file mode 100644 index 0000000..145cd81 --- /dev/null +++ b/freezer_ui/sessions/views.py @@ -0,0 +1,114 @@ +# 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.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import browsers +from horizon import exceptions +from horizon import workflows + +import horizon_web_ui.freezer_ui.sessions.browsers as project_browsers +from horizon_web_ui.freezer_ui.sessions.workflows import attach +from horizon_web_ui.freezer_ui.sessions.workflows import create_session +import horizon_web_ui.freezer_ui.api.api as freezer_api +from horizon_web_ui.freezer_ui.utils import SessionJob + + +class AttachToSessionWorkflow(workflows.WorkflowView): + workflow_class = attach.AttachJobToSession + + def get_object(self, *args, **kwargs): + job_id = self.kwargs['job_id'] + try: + return freezer_api.job_get(self.request, job_id) + except Exception: + redirect = reverse("horizon:freezer_ui:jobs:index") + msg = _('Unable to retrieve details.') + exceptions.handle(self.request, msg, redirect=redirect) + + def is_update(self): + return 'job_id' in self.kwargs and \ + bool(self.kwargs['job_id']) + + def get_initial(self): + initial = super(AttachToSessionWorkflow, self).get_initial() + job = self.get_object()[0] + initial.update({'job_id': job.id}) + return initial + + +class SessionsView(browsers.ResourceBrowserView): + browser_class = project_browsers.SessionBrowser + template_name = "freezer_ui/sessions/browser.html" + + def get_sessions_data(self): + sessions = [] + try: + sessions = freezer_api.session_list(self.request) + except Exception: + msg = _('Unable to retrieve sessions list.') + exceptions.handle(self.request, msg) + return sessions + + def get_jobs_data(self): + jobs = [] + session = None + try: + if self.kwargs['session_id']: + session = freezer_api.session_get( + self.request, + self.kwargs['session_id']) + + try: + jobs = [SessionJob(k, + self.kwargs['session_id'], + v['client_id'], + v['status']) + for k, v in session.jobs.iteritems()] + except AttributeError: + pass + except Exception: + msg = _('Unable to retrieve session information.') + exceptions.handle(self.request, msg) + return jobs + + +class CreateSessionWorkflow(workflows.WorkflowView): + workflow_class = create_session.CreateSession + + def get_object(self, *args, **kwargs): + session_id = self.kwargs['session_id'] + try: + return freezer_api.session_get(self.request, session_id) + except Exception: + redirect = reverse("horizon:freezer_ui:sessions:index") + msg = _('Unable to retrieve session.') + exceptions.handle(self.request, msg, redirect=redirect) + + def get_initial(self): + initial = super(CreateSessionWorkflow, self).get_initial() + if self.is_update(): + session = self.get_object() + initial.update({ + 'description': session.description, + 'session_id': session.session_id, + 'start_datetime': session.start_datetime, + 'interval': session.interval, + 'end_datetime': session.end_datetime + }) + return initial + + def is_update(self): + return 'session_id' in self.kwargs and \ + bool(self.kwargs['session_id']) diff --git a/freezer_ui/sessions/workflows/__init__.py b/freezer_ui/sessions/workflows/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/freezer_ui/sessions/workflows/attach.py b/freezer_ui/sessions/workflows/attach.py new file mode 100644 index 0000000..9fdc581 --- /dev/null +++ b/freezer_ui/sessions/workflows/attach.py @@ -0,0 +1,72 @@ +# 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 exceptions +from horizon import forms +from horizon import workflows + +import horizon_web_ui.freezer_ui.api.api as freezer_api + + +class SessionConfigurationAction(workflows.Action): + session_id = forms.ChoiceField( + help_text=_("Set a session to attach this job"), + label=_("Session Name"), + required=True) + + job_id = forms.CharField( + widget=forms.HiddenInput(), + required=True) + + def populate_session_id_choices(self, request, context): + sessions = [] + try: + sessions = freezer_api.session_list(request) + except Exception: + exceptions.handle(request, _('Error getting session list')) + + sessions = [(s.session_id, s.description) for s in sessions] + sessions.insert(0, ('', _('Select A Session'))) + return sessions + + class Meta: + name = _("Sessions") + slug = "sessions" + + +class SessionConfiguration(workflows.Step): + action_class = SessionConfigurationAction + contributes = ('session_id', + 'job_id') + + +class AttachJobToSession(workflows.Workflow): + slug = "attach_job" + name = _("Attach To Session") + finalize_button_name = _("Attach") + success_message = _('Job saved successfully.') + failure_message = _('Unable to attach to session.') + success_url = "horizon:freezer_ui:jobs:index" + default_steps = (SessionConfiguration,) + + def handle(self, request, context): + try: + freezer_api.add_job_to_session( + request, + context['session_id'], + context['job_id']) + return True + except Exception: + exceptions.handle(request) + return False diff --git a/freezer_ui/sessions/workflows/create_session.py b/freezer_ui/sessions/workflows/create_session.py new file mode 100644 index 0000000..3ac20f6 --- /dev/null +++ b/freezer_ui/sessions/workflows/create_session.py @@ -0,0 +1,123 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import datetime +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import forms +from horizon import workflows + +import horizon_web_ui.freezer_ui.api.api as freezer_api + + +class SessionConfigurationAction(workflows.Action): + description = forms.CharField( + label=_("Session Name"), + help_text=_("Define a name for this session"), + required=True) + + session_id = forms.CharField( + widget=forms.HiddenInput(), + required=False) + + class Meta: + name = _("Session Information") + slug = "sessions" + + +class SessionConfiguration(workflows.Step): + action_class = SessionConfigurationAction + contributes = ('description', + 'session_id') + + +class SchedulingConfigurationAction(workflows.Action): + start_datetime = forms.CharField( + label=_("Start Date and Time"), + required=False, + help_text=_("")) + + interval = forms.CharField( + label=_("Interval"), + required=False, + help_text=_("")) + + end_datetime = forms.CharField( + label=_("End Date and Time"), + required=False, + help_text=_("")) + + def __init__(self, request, context, *args, **kwargs): + self.request = request + self.context = context + super(SchedulingConfigurationAction, self).__init__( + request, context, *args, **kwargs) + + def clean(self): + cleaned_data = super(SchedulingConfigurationAction, self).clean() + self._check_start_datetime(cleaned_data) + self._check_end_datetime(cleaned_data) + return cleaned_data + + def _validate_iso_format(self, start_date): + try: + return datetime.datetime.strptime( + start_date, "%Y-%m-%dT%H:%M:%S") + except ValueError: + return False + + def _check_start_datetime(self, cleaned_data): + if cleaned_data.get('start_datetime') and not \ + self._validate_iso_format(cleaned_data.get('start_datetime')): + msg = _("Start date time is not in ISO format.") + self._errors['start_datetime'] = self.error_class([msg]) + + def _check_end_datetime(self, cleaned_data): + if cleaned_data.get('end_datetime') and not \ + self._validate_iso_format(cleaned_data.get('end_datetime')): + msg = _("End date time is not in ISO format.") + self._errors['end_datetime'] = self.error_class([msg]) + + class Meta(object): + name = _("Scheduling") + slug = "scheduling" + help_text_template = "freezer_ui/jobs" \ + "/_scheduling.html" + + +class SchedulingConfiguration(workflows.Step): + action_class = SchedulingConfigurationAction + contributes = ('start_datetime', + 'interval', + 'end_datetime') + + +class CreateSession(workflows.Workflow): + slug = "create_session" + name = _("Create Session") + finalize_button_name = _("Create") + success_message = _('Session created successfully.') + failure_message = _('Unable to create session.') + success_url = "horizon:freezer_ui:sessions:index" + default_steps = (SessionConfiguration, + SchedulingConfiguration) + + def handle(self, request, context): + try: + if context['session_id'] == '': + return freezer_api.session_create(request, context) + else: + return freezer_api.session_update(request, context) + except Exception: + exceptions.handle(request) + return False diff --git a/freezer_ui/utils.py b/freezer_ui/utils.py index 745ad88..396515e 100644 --- a/freezer_ui/utils.py +++ b/freezer_ui/utils.py @@ -17,3 +17,11 @@ def create_dict_action(**kwargs): None values """ return {k: v for k, v in kwargs.items() if v} + + +class SessionJob(object): + def __init__(self, job_id, session_id, client_id, status): + self.job_id = job_id + self.session_id = session_id + self.client_id = client_id + self.status = status