Adds EDP support in the UI for job execution

First pass at job execution in the UI.  Job binaries
tab has been added, still needs work to handle internal
and external sources as well as overall refinement of controls.
Now includes job_binaries as a form rather than a workflow and
forces a unique name for binary-internal uploads.

Implements: blueprint edp-savanna-dashboard-ui

Change-Id: Id32f7c5d5458918bd832070c53271c042d886f86
This commit is contained in:
Chad Roberts 2013-09-03 10:52:23 -04:00 committed by Nikita Konovalov
parent 87e1e220f1
commit d2060199b2
45 changed files with 1281 additions and 51 deletions

View File

@ -78,6 +78,7 @@ class ResourceManager(object):
resp = self.api.client.put(url, json.dumps(data))
if resp.status_code != 202:
self._raise_api_exception(resp)
return get_json(resp)
def _list(self, url, response_key):
resp = self.api.client.get(url)

View File

@ -22,6 +22,9 @@ from savannadashboard.api import clusters
from savannadashboard.api import data_sources
from savannadashboard.api import httpclient
from savannadashboard.api import images
from savannadashboard.api import job_binaries
from savannadashboard.api import job_binaries_internal
from savannadashboard.api import job_executions
from savannadashboard.api import job_origins
from savannadashboard.api import jobs
from savannadashboard.api import node_group_templates
@ -77,3 +80,7 @@ class Client(object):
self.jobs = jobs.JobManager(self)
self.job_origins = job_origins.JobOriginManager(self)
self.data_sources = data_sources.DataSourceManager(self)
self.job_executions = job_executions.JobExecutionManager(self)
self.job_binaries = job_binaries.JobBinaryManager(self)
self.job_binaries_internal =\
job_binaries_internal.JobBinaryInternalManager(self)

View File

@ -0,0 +1,47 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2013 Red Hat Inc.
#
# 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 savannadashboard.api import base
class JobBinary(base.Resource):
resource_name = 'Job Binary'
defaults = {}
class JobBinaryManager(base.ResourceManager):
resource_class = JobBinary
def create(self, name, url, description, extra):
data = {
"name": name,
"url": url,
"description": description,
"extra": extra
}
return self._create('/job-binaries', data)
def list(self):
return self._list('/job-binaries', 'binaries')
def get(self, job_binary_id):
return self._get('/job-binaries/%s' % job_binary_id,
'resource')
def delete(self, job_binary_id):
self._delete('/job-binaries/%s' % job_binary_id)

View File

@ -0,0 +1,42 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2013 Red Hat Inc.
#
# 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 savannadashboard.api import base
class JobBinaryInternal(base.Resource):
resource_name = 'Job Binary Internal'
defaults = {}
class JobBinaryInternalManager(base.ResourceManager):
resource_class = JobBinaryInternal
def create(self, name, data):
resp = self.api.client.put('/job-binary-internals/%s' % name, data)
if resp.status_code != 202:
self._raise_api_exception(resp)
return resp.json()
def list(self):
return self._list('/job-binary-internals', 'binaries')
def get(self, job_binary_id):
return self._get('/job-binary-internals/%s' % job_binary_id,
'resource')
def delete(self, job_binary_id):
self._delete('/job-binary-internals/%s' % job_binary_id)

View File

@ -0,0 +1,36 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2013 Red Hat Inc.
#
# 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 savannadashboard.api import base
class JobExecution(base.Resource):
resource_name = 'JobExecution'
class JobExecutionManager(base.ResourceManager):
resource_class = JobExecution
def list(self):
return self._list('/job-executions', 'job_executions')
def get(self, obj_id):
return self._get('/job-executions/%s' % obj_id,
'resource')
def delete(self, obj_id):
self._delete('/job-executions/%s' % obj_id)

View File

@ -26,17 +26,13 @@ class JobOrigin(base.Resource):
class JobOriginManager(base.ResourceManager):
resource_class = JobOrigin
def create(self, name, storage_type, username, password,
location, description):
def create(self, name, mains, libs, description):
data = {
'credentials': {'user': username,
'password': password
},
'name': name,
'description': description,
'storage_type': storage_type,
'url': location
'mains': mains,
'libs': libs # TODO(croberts)fix when api is ready
}
self._create('/job-origins', data)

View File

@ -48,3 +48,14 @@ class JobManager(base.ResourceManager):
def delete(self, job_id):
self._delete('/jobs/%s' % job_id)
def launch(self, job_id, cluster_id, input_id, output_id, configs):
url = "/jobs/%s/execute" % job_id
data = {
"input_id": input_id,
"output_id": output_id,
"cluster_id": cluster_id,
"job_configs": configs
}
return self._create(url, data)

View File

@ -31,8 +31,10 @@ class SavannaDashboard(horizon.Dashboard):
panels = ('clusters',
'cluster_templates',
'nodegroup_templates',
'job_executions',
'jobs',
'job_origins',
'job_binaries',
'data_sources',
'image_registry',
'plugins')

View File

@ -39,9 +39,6 @@ class DataSourcesView(tables.DataTableView):
data_sources = savanna.data_sources.list()
return data_sources
class FakeDataSource(object):
pass
class CreateDataSourceView(workflows.WorkflowView):
workflow_class = create_flow.CreateDataSource

View File

@ -62,7 +62,6 @@ class GeneralConfigAction(workflows.Action):
class GeneralConfig(workflows.Step):
action_class = GeneralConfigAction
contributes = ("hidden_configure_field", )
def contribute(self, data, context):
for k, v in data.items():

View File

@ -0,0 +1,154 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2013 Red Hat Inc.
#
# 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.forms.util import flatatt
from django.forms import widgets
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _
from horizon import forms
from horizon import messages
import savannadashboard.api.base as api_base
from savannadashboard.api import client as savannaclient
import uuid
LOG = logging.getLogger(__name__)
class LabeledInput(widgets.Input):
def render(self, name, values, attrs=None):
final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
output = "<span id='%s'>%s</span>%s" %\
("id_%s_label" % name,
"savanna-db://",
('<input%s />' % flatatt(final_attrs)))
return mark_safe(output)
class JobBinaryCreateForm(forms.SelfHandlingForm):
job_binary_name = forms.CharField(label=_("Name"),
required=True)
job_binary_type = forms.ChoiceField(label=_("Type"),
required=True)
job_binary_url = forms.CharField(label=_("URL"),
required=False,
widget=LabeledInput())
job_binary_savanna_internal = forms.ChoiceField(label=_("Savanna binary"),
required=False)
job_binary_file = forms.FileField(label=_("Upload File"),
required=False)
job_binary_username = forms.CharField(label=_("Username"),
required=False)
job_binary_password = forms.CharField(label=_("Password"),
required=False,
widget=forms.PasswordInput(
attrs={'autocomplete': 'off'}))
job_binary_description = forms.CharField(label=_("Description"),
required=False)
def __init__(self, request, *args, **kwargs):
super(JobBinaryCreateForm, self).__init__(request, *args, **kwargs)
self.fields["job_binary_type"].choices =\
[("savanna-db", "Savanna internal database"),
("swift-internal", "Swift internal"),
("swift-external", "Swift external")]
self.fields["job_binary_savanna_internal"].choices =\
self.populate_job_binary_savanna_internal_choices(request)
def populate_job_binary_savanna_internal_choices(self, request):
savanna = savannaclient.Client(request)
job_binaries = savanna.job_binaries_internal.list()
choices = [(job_binary.id, job_binary.name)
for job_binary in job_binaries]
choices.insert(0, ('', 'Upload a new file'))
return choices
def handle(self, request, context):
try:
savanna = savannaclient.Client(request)
extra = {}
bin_url = "%s://%s" % (context["job_binary_type"],
context["job_binary_url"])
if(context["job_binary_type"] == "savanna-db"):
bin_url = self.handle_savanna(request, context)
if(context["job_binary_type"] == "swift-internal"):
extra = self.handle_swift_internal(request, context)
savanna.job_binaries.create(
context["job_binary_name"],
bin_url,
context["job_binary_description"],
extra)
messages.success(request, "Successfully created job binary")
return True
except api_base.APIException as e:
messages.error(request, str(e))
return False
except Exception as e:
messages.error(request, str(e))
return True
class Meta:
name = _("Create Job Binary")
help_text_template = \
("job_binaries/_create_job_binary_help.html")
def handle_savanna(self, request, context):
savanna = savannaclient.Client(request)
bin_id = context["job_binary_savanna_internal"]
if(bin_id == ""):
result = savanna.job_binaries_internal.create(
self.get_unique_binary_name(
request, request.FILES["job_binary_file"].name),
request.FILES["job_binary_file"].read())
bin_id = result["resource"]["id"]
return "savanna-db://%s" % bin_id
def handle_swift_internal(self, request, context):
username = context["job_binary_username"]
password = context["job_binary_password"]
extra = {
"user": username,
"password": password
}
return extra
def get_unique_binary_name(self, request, base_name):
savanna = savannaclient.Client(request)
internals = savanna.job_binaries_internal.list()
names = [internal.name for internal in internals]
if base_name in names:
return "%s_%s" % (base_name, uuid.uuid1())
return base_name

View File

@ -0,0 +1,30 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2013 Red Hat Inc.
#
# 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 savannadashboard import dashboard
class JobBinariesPanel(horizon.Panel):
name = _("Job Binaries")
slug = 'job_binaries'
dashboard.SavannaDashboard.register(JobBinariesPanel)

View File

@ -0,0 +1,63 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2013 Red Hat Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
from django.utils.translation import ugettext_lazy as _
from horizon import tables
from savannadashboard.api import client as savannaclient
LOG = logging.getLogger(__name__)
class CreateJobBinary(tables.LinkAction):
name = "create job binary"
verbose_name = _("Create Job Binary")
url = "horizon:savanna:job_binaries:create-job-binary"
classes = ("btn-launch", "ajax-modal")
class DeleteJobBinary(tables.BatchAction):
name = "delete"
action_present = _("Delete")
action_past = _("Deleted")
data_type_singular = _("Job binary")
data_type_plural = _("Job binaries")
classes = ('btn-danger', 'btn-terminate')
def action(self, request, obj_id):
savanna = savannaclient.Client(request)
savanna.job_binaries.delete(obj_id)
class JobBinariesTable(tables.DataTable):
name = tables.Column("name",
verbose_name=_("Name"),
link=("horizon:savanna:job_binaries:details"))
type = tables.Column("url",
verbose_name=_("Url"))
description = tables.Column("description",
verbose_name=_("Description"))
class Meta:
name = "job_binaries"
verbose_name = _("Job Binaries")
table_actions = (CreateJobBinary,
DeleteJobBinary)
row_actions = (DeleteJobBinary,)

View File

@ -0,0 +1,44 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2013 Red Hat Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
from django.utils.translation import ugettext_lazy as _
from horizon import tabs
from savannadashboard.api import client as savannaclient
LOG = logging.getLogger(__name__)
class GeneralTab(tabs.Tab):
name = _("General Info")
slug = "job_binaries_details_tab"
template_name = ("job_binaries/_details.html")
def get_context_data(self, request):
job_binary_id = self.tab_group.kwargs['job_binary_id']
savanna = savannaclient.Client(request)
job_binary = savanna.job_binaries.get(job_binary_id)
return {"job_binary": job_binary}
class JobBinaryDetailsTabs(tabs.TabGroup):
slug = "job_binary_details"
tabs = (GeneralTab,)
sticky = True

View File

@ -0,0 +1,34 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2013 Red Hat Inc.
#
# 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.defaults import patterns
from django.conf.urls.defaults import url
import savannadashboard.job_binaries.views as views
urlpatterns = patterns('',
url(r'^$', views.JobBinariesView.as_view(),
name='index'),
url(r'^$', views.JobBinariesView.as_view(),
name='job-binaries'),
url(r'^create-job-binary$',
views.CreateJobBinaryView.as_view(),
name='create-job-binary'),
url(r'^(?P<job_binary_id>[^/]+)$',
views.JobBinaryDetailsView.as_view(),
name='details'))

View File

@ -0,0 +1,63 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2013 Red Hat Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
from django.core.urlresolvers import reverse_lazy
from horizon import forms
from horizon import tables
from horizon import tabs
from savannadashboard.api import client as savannaclient
import savannadashboard.job_binaries.forms as job_binary_forms
from savannadashboard.job_binaries.tables import JobBinariesTable
import savannadashboard.job_binaries.tabs as _tabs
LOG = logging.getLogger(__name__)
class JobBinariesView(tables.DataTableView):
table_class = JobBinariesTable
template_name = 'job_binaries/job_binaries.html'
def get_data(self):
savanna = savannaclient.Client(self.request)
job_binaries = savanna.job_binaries.list()
return job_binaries
class CreateJobBinaryView(forms.ModalFormView):
form_class = job_binary_forms.JobBinaryCreateForm
success_url = reverse_lazy('horizon:savanna:job_binaries:index')
classes = ("ajax-modal")
template_name = "job_binaries/create.html"
class JobBinaryDetailsView(tabs.TabView):
tab_group_class = _tabs.JobBinaryDetailsTabs
template_name = 'job_binaries/details.html'
def get_context_data(self, **kwargs):
context = super(JobBinaryDetailsView, self)\
.get_context_data(**kwargs)
return context
def get_data(self):
pass

View File

@ -0,0 +1,30 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2013 Red Hat Inc.
#
# 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 savannadashboard import dashboard
class JobExecutionsPanel(horizon.Panel):
name = _("Job Executions")
slug = 'job_executions'
dashboard.SavannaDashboard.register(JobExecutionsPanel)

View File

@ -0,0 +1,62 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2013 Red Hat Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
from django.utils.translation import ugettext_lazy as _
from horizon import tables
from savannadashboard.api import client as savannaclient
LOG = logging.getLogger(__name__)
class DeleteJobExecution(tables.BatchAction):
name = "delete"
action_present = _("Delete")
action_past = _("Deleted")
data_type_singular = _("Job execution")
data_type_plural = _("Job executions")
classes = ('btn-danger', 'btn-terminate')
def action(self, request, obj_id):
savanna = savannaclient.Client(request)
savanna.job_executions.delete(obj_id)
class JobExecutionsTable(tables.DataTable):
class StatusColumn(tables.Column):
def get_data(self, datum):
return datum.info['status']
name = tables.Column("id",
verbose_name=_("ID"),
display_choices=(("id", "ID"), ("name", "Name")),
link=("horizon:savanna:job_executions:details"))
status = StatusColumn("info",
verbose_name=_("Status"))
def get_object_display(self, datum):
return datum.id
class Meta:
name = "job_executions"
verbose_name = _("Job Executions")
table_actions = [DeleteJobExecution]
row_actions = [DeleteJobExecution]

View File

@ -0,0 +1,44 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2013 Red Hat Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
from django.utils.translation import ugettext_lazy as _
from horizon import tabs
from savannadashboard.api import client as savannaclient
LOG = logging.getLogger(__name__)
class GeneralTab(tabs.Tab):
name = _("General Info")
slug = "job_execution_tab"
template_name = ("job_executions/_details.html")
def get_context_data(self, request):
job_execution_id = self.tab_group.kwargs['job_execution_id']
savanna = savannaclient.Client(request)
job_execution = savanna.job_executions.get(job_execution_id)
return {"job_execution": job_execution}
class JobExecutionDetailsTabs(tabs.TabGroup):
slug = "job_execution_details"
tabs = (GeneralTab,)
sticky = True

View File

@ -0,0 +1,31 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2013 Red Hat Inc.
#
# 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.defaults import patterns
from django.conf.urls.defaults import url
import savannadashboard.job_executions.views as views
urlpatterns = patterns('',
url(r'^$', views.JobExecutionsView.as_view(),
name='index'),
url(r'^$', views.JobExecutionsView.as_view(),
name='job-executions'),
url(r'^(?P<job_execution_id>[^/]+)$',
views.JobExecutionDetailsView.as_view(),
name='details'))

View File

@ -0,0 +1,51 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2013 Red Hat Inc.
#
# 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 horizon import tables
from horizon import tabs
from savannadashboard.api import client as savannaclient
from savannadashboard.job_executions.tables import JobExecutionsTable
import savannadashboard.job_executions.tabs as _tabs
LOG = logging.getLogger(__name__)
class JobExecutionsView(tables.DataTableView):
table_class = JobExecutionsTable
template_name = 'job_executions/job_executions.html'
def get_data(self):
savanna = savannaclient.Client(self.request)
jobs = savanna.job_executions.list()
return jobs
class JobExecutionDetailsView(tabs.TabView):
tab_group_class = _tabs.JobExecutionDetailsTabs
template_name = 'job_executions/details.html'
def get_context_data(self, **kwargs):
context = super(JobExecutionDetailsView, self)\
.get_context_data(**kwargs)
return context
def get_data(self):
pass

View File

@ -50,8 +50,6 @@ class JobOriginsTable(tables.DataTable):
name = tables.Column("name",
verbose_name=_("Name"),
link=("horizon:savanna:job_origins:details"))
type = tables.Column("type",
verbose_name=_("Type"))
description = tables.Column("description",
verbose_name=_("Description"))

View File

@ -17,6 +17,8 @@
import logging
from django.forms.util import flatatt
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _
from horizon import forms
@ -24,40 +26,85 @@ from horizon import workflows
from savannadashboard.api import client as savannaclient
LOG = logging.getLogger(__name__)
class AddExtraSelectWidget(forms.Select):
def render(self, name, value, attrs=None, choices=()):
if value is None:
value = ''
final_attrs = self.build_attrs(attrs, name=name)
output = [u'<select%s>' % flatatt(final_attrs)]
options = self.render_options(choices, [value])
if options:
output.append(options)
output.append('</select>')
output.append("<input type='button' " +
"class='job_binary_add_button' value='Add' " +
"onclick='addExtraBinary(this);'></input>")
return mark_safe("".join(output))
class GeneralConfigAction(workflows.Action):
NUM_LOCATION_FIELDS = 10
FIELDS_BEFORE_MAIN = 2
local_binary_choices = []
extra_binary_count = forms.CharField(widget=forms.HiddenInput())
job_origin_name = forms.CharField(label=_("Name"),
required=True)
job_origin_credential_user = forms.CharField(label=_("Origin username"),
required=True)
job_origin_credential_pass = forms.CharField(
widget=forms.PasswordInput(attrs={'autocomplete': 'off'}),
label=_("Origin password"),
required=True)
job_storage_type = forms.ChoiceField(
label=_("Job Storage Type"),
required=True,
choices=[("internal", "Savanna DB"),
("swift", "Swift"),
("hdfs", "HDFS")],
widget=forms.Select(
attrs={"class": "job_storage_type_choice"}))
job_origin_location = forms.CharField(label=_("Job Storage Location"),
required=True)
job_origin_description = forms.CharField(label=_("Description"),
required=False,
widget=forms.Textarea)
def populate_job_origin_local_db_choices(self, request):
savanna = savannaclient.Client(request)
job_binaries = savanna.job_binaries.list()
choices = [(job_binary.id, job_binary.name)
for job_binary in job_binaries]
choices.insert(0, ('', 'NONE'))
return choices
def __init__(self, request, *args, **kwargs):
extra_fields = kwargs.pop('extra', 0)
super(GeneralConfigAction, self).__init__(request, *args, **kwargs)
self.fields['extra_binary_count'].initial = extra_fields
self.local_binary_choices =\
self.populate_job_origin_local_db_choices(request)
for i in range(self.NUM_LOCATION_FIELDS, 0, -1):
self.fields.insert(
self.FIELDS_BEFORE_MAIN, 'job_origin_main_%s' % i,
forms.ChoiceField(
label=_("Main Binary"),
required=False,
choices=self.local_binary_choices,
initial=(None, "None"),
widget=AddExtraSelectWidget(
attrs={"class": "job_origin_main"})))
for i in range(self.NUM_LOCATION_FIELDS, 0, -1):
self.fields.insert(
self.FIELDS_BEFORE_MAIN + self.NUM_LOCATION_FIELDS,
'job_origin_lib_%s' % i,
forms.ChoiceField(
label=_("Library"),
required=False,
choices=self.local_binary_choices,
initial=(None, "None"),
widget=AddExtraSelectWidget(
attrs={"class": "job_origin_lib"})))
self.fields["extra_locations"] = forms.CharField(
widget=forms.HiddenInput(),
initial=self.NUM_LOCATION_FIELDS)
class Meta:
name = _("Create Job Origin")
help_text_template = \
@ -66,7 +113,6 @@ class GeneralConfigAction(workflows.Action):
class GeneralConfig(workflows.Step):
action_class = GeneralConfigAction
contributes = ("hidden_configure_field", )
def contribute(self, data, context):
for k, v in data.items():
@ -86,11 +132,23 @@ class CreateJobOrigin(workflows.Workflow):
def handle(self, request, context):
savanna = savannaclient.Client(request)
main_locations = []
lib_locations = []
extra_count = 2
for i in range(1, extra_count + 1):
if(context["general_job_origin_main_%s" % i] != ""):
main_locations.append(
context["general_job_origin_main_%s" % i])
for i in range(1, extra_count + 1):
if(context["general_job_origin_lib_%s" % i] != ""):
lib_locations.append(
context["general_job_origin_lib_%s" % i])
savanna.job_origins.create(
context["general_job_origin_name"],
context["general_job_storage_type"],
context["general_job_origin_credential_user"],
context["general_job_origin_credential_pass"],
context["general_job_origin_location"],
main_locations,
lib_locations,
context["general_job_origin_description"])
return True

View File

@ -17,6 +17,8 @@
import logging
from django.core import urlresolvers
from django.utils import http
from django.utils.translation import ugettext_lazy as _
from horizon import tables
@ -46,6 +48,23 @@ class DeleteJob(tables.BatchAction):
savanna.jobs.delete(obj_id)
class LaunchJob(tables.LinkAction):
name = "launch-job"
verbose_name = _("Launch Job")
action_present = _("Launch")
action_past = _("Launched")
data_type_singular = _("Job")
data_type_plural = _("Jobs")
url = "horizon:savanna:jobs:launch-job"
classes = ('ajax-modal', 'btn-launch')
def get_link_url(self, datum):
base_url = urlresolvers.reverse(self.url)
params = http.urlencode({"job_id": datum.id})
return "?".join([base_url, params])
class JobsTable(tables.DataTable):
name = tables.Column("name",
verbose_name=_("Name"),
@ -60,4 +79,4 @@ class JobsTable(tables.DataTable):
verbose_name = _("Jobs")
table_actions = (CreateJob,
DeleteJob)
row_actions = (DeleteJob,)
row_actions = (LaunchJob, DeleteJob,)

View File

@ -29,6 +29,9 @@ urlpatterns = patterns('',
url(r'^create-job$',
views.CreateJobView.as_view(),
name='create-job'),
url(r'^launch-job$',
views.LaunchJobView.as_view(),
name='launch-job'),
url(r'^(?P<job_id>[^/]+)$',
views.JobDetailsView.as_view(),
name='details'))

View File

@ -26,6 +26,7 @@ from savannadashboard.api import client as savannaclient
from savannadashboard.jobs.tables import JobsTable
import savannadashboard.jobs.tabs as _tabs
import savannadashboard.jobs.workflows.create as create_flow
import savannadashboard.jobs.workflows.launch as launch_flow
LOG = logging.getLogger(__name__)
@ -59,3 +60,16 @@ class JobDetailsView(tabs.TabView):
def get_data(self):
pass
class LaunchJobView(workflows.WorkflowView):
workflow_class = launch_flow.LaunchJob
success_url = \
"horizon:savanna:jobs"
classes = ("ajax-modal")
template_name = "jobs/launch.html"
def get_context_data(self, **kwargs):
context = super(LaunchJobView, self)\
.get_context_data(**kwargs)
return context

View File

@ -0,0 +1,154 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2013 Red Hat Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
from django.utils.translation import ugettext as _
from horizon import forms
from horizon import workflows
from savannadashboard.api import client as savannaclient
LOG = logging.getLogger(__name__)
class GeneralConfigAction(workflows.Action):
cluster = forms.ChoiceField(
label=_("Cluster"),
required=True,
initial=(None, "None"),
widget=forms.Select(attrs={"class": "cluster_choice"}))
job_input = forms.ChoiceField(
label=_("Input"),
required=True,
initial=(None, "None"),
widget=forms.Select(attrs={"class": "job_input_choice"}))
job_output = forms.ChoiceField(
label=_("Output"),
required=True,
initial=(None, "None"),
widget=forms.Select(attrs={"class": "job_output_choice"}))
def __init__(self, request, *args, **kwargs):
super(GeneralConfigAction, self).__init__(request, *args, **kwargs)
if request.REQUEST.get("job_id", None) is None:
self.fields["job"] = forms.ChoiceField(
label=_("Job"),
required=True)
self.fields["job"].choices = self.populate_job_choices(request)
else:
self.fields["job"] = forms.CharField(
widget=forms.HiddenInput(),
initial=request.REQUEST.get("job_id", None))
def populate_cluster_choices(self, request, context):
savanna = savannaclient.Client(request)
clusters = savanna.clusters.list()
choices = [(cluster.id, cluster.name)
for cluster in clusters]
return choices
def populate_job_input_choices(self, request, context):
return self.get_data_source_choices(request, context)
def populate_job_output_choices(self, request, context):
return self.get_data_source_choices(request, context)
def get_data_source_choices(self, request, context):
savanna = savannaclient.Client(request)
data_sources = savanna.data_sources.list()
choices = [(data_source.id, data_source.name)
for data_source in data_sources]
return choices
def populate_job_choices(self, request):
savanna = savannaclient.Client(request)
jobs = savanna.jobs.list()
choices = [(job.id, job.name)
for job in jobs]
return choices
class Meta:
name = _("Job")
help_text_template = \
("jobs/_launch_job_help.html")
class JobConfigAction(workflows.Action):
config = forms.CharField(
label=_("Job Config"),
widget=forms.Textarea())
def __init__(self, request, *args, **kwargs):
super(JobConfigAction, self).__init__(request, *args, **kwargs)
# TODO(croberts) pre-populate config with job-type
# appropriate config settings
class Meta:
name = _("Configure")
help_text_template = \
("jobs/_launch_job_configure_help.html")
class GeneralConfig(workflows.Step):
action_class = GeneralConfigAction
def contribute(self, data, context):
for k, v in data.items():
context["general_" + k] = v
return context
class JobConfig(workflows.Step):
action_class = JobConfigAction
def contribute(self, data, context):
for k, v in data.items():
context["job_" + k] = v
return context
class LaunchJob(workflows.Workflow):
slug = "launch_job"
name = _("Launch Job")
finalize_button_name = _("Launch")
success_message = _("Job launched")
failure_message = _("Could not launch job")
success_url = "horizon:savanna:jobs:index"
default_steps = (GeneralConfig, JobConfig)
def handle(self, request, context):
savanna = savannaclient.Client(request)
savanna.jobs.launch(
context["general_job"],
context["general_cluster"],
context["general_job_input"],
context["general_job_output"],
context["job_config"])
return True

View File

@ -8,7 +8,7 @@
{% block main %}
<div class="cluster_templates">
<div class="data_sources">
{{ data_sources_table.render }}
</div>

View File

@ -0,0 +1,21 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block form_id %}create-job-binary{% endblock %}
{% block form_action %}{% url horizon:savanna:job_binaries:create-job-binary %}{% endblock %}
{% block form_attrs %}enctype="multipart/form-data"{% endblock %}
{% block modal-header %}{% trans "Create Job Binary" %}{% endblock %}
{% block modal-body %}
<div class="left">
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
{% endblock %}
{% block modal-footer %}
<input class="btn btn-primary pull-right" id="upload_file_btn" type="submit" value="{% trans "Create" %}"/>
<a href="{% url horizon:savanna:job_binaries:index %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -0,0 +1,18 @@
{% load i18n sizeformat %}
<h3>{% trans "Job Binary Overview" %}</h3>
<div class="status row-fluid detail">
<dl>
<dt>{% trans "Name" %}</dt>
<dd>{{ job_binary.name }}</dd>
<dt>{% trans "ID" %}</dt>
<dd>{{ job_binary.id }}</dd>
<dt>{% trans "URL" %}</dt>
<dd>{{ job_binary.url }}</dd>
<dt>{% trans "Description" %}</dt>
<dd>{{ job_binary.description|default:"None" }}</dd>
<dt>{% trans "Tenant id" %}</dt>
<dd>{{ job_binary.tenant_id }}</dd>
<dt>{% trans "Create time" %}</dt>
<dd>{{ job_binary.created_at }}</dd>
</dl>
</div>

View File

@ -0,0 +1,11 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Create Job Binary" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Savanna - Create Job Binary") %}
{% endblock page_header %}
{% block main %}
{% include 'job_binaries/_create.html' %}
{% endblock %}

View File

@ -0,0 +1,15 @@
{% extends 'base.html' %}
{% load i18n sizeformat %}
{% block title %}{% trans "Job Binary Details" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Savanna - Job Binary Details") %}
{% endblock page_header %}
{% block main %}
<div class="row-fluid">
<div class="span12">
{{ tab_group.render }}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,84 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Savanna" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Savanna - Job Binaries") %}
{% endblock page_header %}
{% block main %}
<style type="text/css">
#id_job_binary_url {
width: 200px !important; }
</style>
<div class="job_binaries">
{{ job_binaries_table.render }}
</div>
<script type="text/javascript">
addHorizonLoadEvent(function () {
horizon.modals.addModalInitFunction(function (modal) {
hide_extra_fields();
function hide_extra_fields() {
$("[name=job_binary_username]").closest(".control-group").hide();
$("[name=job_binary_password]").closest(".control-group").hide();
$("[name=job_binary_file]").closest(".control-group").hide();
$("[name=job_binary_url]").closest(".control-group").hide();
$("[name=job_binary_savanna_internal]").closest(".control-group").hide();
}
$("#id_job_binary_type").change(function() {
var label = $("#id_job_binary_url_label");
var bin_file = $("[name=job_binary_file]").closest(".control-group");
var bin_choice = $("[name=job_binary_savanna_internal]").closest(".control-group");
var bin_url = $("[name=job_binary_url]").closest(".control-group");
var username = $("[name=job_binary_username]").closest(".control-group");
var password = $("[name=job_binary_password]").closest(".control-group");
switch($(this).val()) {
case "savanna-db":
label.html("savanna-db://");
username.hide();
password.hide();
bin_file.show();
bin_choice.show();
bin_url.hide();
break;
case "swift-external":
username.hide();
password.hide();
bin_file.hide();
bin_choice.hide();
bin_url.show()
label.html("swift-external://");
break;
case "swift-internal":
username.show();
password.show();
bin_file.hide();
bin_choice.hide();
bin_url.show();
label.html("swift-internal://");
break;
}
});
$("#id_job_binary_type").change();
$("[name=job_binary_savanna_internal]").change(function() {
var bin_file = $("[name=job_binary_file]").closest(".control-group");
var bin_choice = $("[name=job_binary_savanna_internal]").closest(".control-group");
var bin_url = $("[name=job_binary_url]").closest(".control-group");
switch($(this).val()) {
case "":
bin_file.show();
break;
default:
bin_file.hide();
break;
}
});
$("#job_binary_savanna_internal").change();
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,28 @@
{% load i18n sizeformat %}
<h3>{% trans "Job Execution Overview" %}</h3>
<div class="status row-fluid detail">
<dl>
<dt>{% trans "Status" %}</dt>
<dd>{{ job_execution.info.status }}</dd>
<dt>{% trans "Id" %}</dt>
<dd>{{ job_execution.id }}</dd>
<dt>{% trans "Job Id" %}</dt>
<dd>{{ job_execution.job_id }}</dd>
<dt>{% trans "Input Id" %}</dt>
<dd>{{ job_execution.input_id }}</dd>
<dt>{% trans "Output Id" %}</dt>
<dd>{{ job_execution.output_id }}</dd>
<dt>{% trans "Cluster Id" %}</dt>
<dd>{{ job_execution.cluster_id }}</dd>
<dt>{% trans "Last Updated" %}</dt>
<dd>{{ job_execution.updated_at }}</dd>
<dt>{% trans "Return Code" %}</dt>
<dd>{{ job_execution.return_code }}</dd>
<dt>{% trans "Oozie Job Id" %}</dt>
<dd>{{ job_execution.oozie_job_id }}</dd>
<dt>{% trans "Created" %}</dt>
<dd>{{ job_execution.created_at }}</dd>
<dt>{% trans "Tenant Id" %}</dt>
<dd>{{ job_execution.tenant_id }}</dd>
</dl>
</div>

View File

@ -0,0 +1,15 @@
{% extends 'base.html' %}
{% load i18n sizeformat %}
{% block title %}{% trans "Job Execution Details" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Savanna - Job Execution Details") %}
{% endblock page_header %}
{% block main %}
<div class="row-fluid">
<div class="span12">
{{ tab_group.render }}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,15 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Savanna" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Savanna - Job Executions") %}
{% endblock page_header %}
{% block main %}
<div class="job_executions">
{{ job_executions_table.render }}
</div>
{% endblock %}

View File

@ -6,14 +6,6 @@
<dd>{{ job_origin.name }}</dd>
<dt>{% trans "ID" %}</dt>
<dd>{{ job_origin.id }}</dd>
<dt>{% trans "URL" %}</dt>
<dd>{{ job_origin.url }}</dd>
<dt>{% trans "Storage type" %}</dt>
<dd>{{ job_origin.storage_type }}</dd>
<dt>{% trans "Username" %}</dt>
<dd>{{ job_origin.credentials.user|default:"None" }}</dd>
<dt>{% trans "Password" %}</dt>
<dd>{{ job_origin.credentials.password|default:"None" }}</dd>
<dt>{% trans "Description" %}</dt>
<dd>{{ job_origin.description|default:"None" }}</dd>
<dt>{% trans "Tenant id" %}</dt>

View File

@ -7,9 +7,39 @@
{% endblock page_header %}
{% block main %}
<div class="cluster_templates">
<style type="text/css">
.job_origin_main, .job_origin_lib {
width: 200px !important; }
.job_binary_add_button, .job_binary_remove_button {
width: 80px !important;
margin-left: 5px; }
</style>
<div class="job_origins">
{{ job_origins_table.render }}
</div>
<script type="text/javascript">
addHorizonLoadEvent(function () {
horizon.modals.addModalInitFunction(function (modal) {
hide_extra_fields();
function hide_extra_fields() {
for(i=2; i <= $("[name=extra_locations]").val(); i++) {
$("[name=job_origin_main_" + i + "]").closest(".control-group").hide();
$("[name=job_origin_lib_" + i + "]").closest(".control-group").hide();
}
}
});
});
addExtraBinary = function (where_from) {
var loc_type = where_from.previousSibling.name.contains("main") ? "main" : "lib";
for(i=2; i <= $("[name=extra_locations]").val(); i++) {
if (!$("[name=job_origin_" + loc_type + "_" + i + "]").closest(".control-group").is(":visible")) {
$("[name=job_origin_" + loc_type + "_" + i + "]").closest(".control-group").show();
break;
}
}
}
</script>
{% endblock %}

View File

@ -8,7 +8,7 @@
{% block main %}
<div class="cluster_templates">
<div class="jobs">
{{ jobs_table.render }}
</div>

View File

@ -0,0 +1,11 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Launch Job" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Savanna - Launch Job") %}
{% endblock page_header %}
{% block main %}
{% include 'horizon/common/_workflow.html' %}
{% endblock %}