murano-dashboard/muranodashboard/packages/views.py

617 lines
24 KiB
Python

# Copyright (c) 2013 Mirantis, 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 json
import sys
from django.core.files import storage
from django import http
from django.urls import reverse
from django.urls import reverse_lazy
from django.utils.translation import ugettext_lazy as _
# django.contrib.formtools migration to django 1.8
# https://docs.djangoproject.com/en/1.8/ref/contrib/formtools/
try:
from django.contrib.formtools.wizard import views as wizard_views
except ImportError:
from formtools.wizard import views as wizard_views
from horizon import exceptions
from horizon.forms import views
from horizon import messages
from horizon import tables as horizon_tables
from horizon.utils import functions as utils
from horizon import views as horizon_views
from muranoclient.common import exceptions as exc
from muranoclient.common import utils as muranoclient_utils
from openstack_dashboard.api import glance
from openstack_dashboard.api import keystone
from oslo_log import log as logging
import six
import six.moves.urllib.parse as urlparse
from muranodashboard import api
from muranodashboard.api import packages as pkg_api
from muranodashboard.catalog import views as catalog_views
from muranodashboard.common import utils as muranodashboard_utils
from muranodashboard.environments import consts
from muranodashboard.packages import consts as packages_consts
from muranodashboard.packages import forms
from muranodashboard.packages import tables
LOG = logging.getLogger(__name__)
FORMS = [('upload', forms.ImportPackageForm),
('modify', forms.UpdatePackageForm),
('add_category', forms.SelectCategories)]
BUNDLE_FORMS = [('upload', forms.ImportBundleForm), ]
def is_app(wizard):
"""Check if we're uploading an application
Return true if uploading package is an application.
In that case, category selection form need to be shown.
"""
step_data = wizard.storage.get_step_data('upload')
if step_data:
return step_data['package'].type == 'Application'
return False
def _ensure_images(name, package, request, step_data=None):
glance_client = glance.glanceclient(
request, version='2')
base_url = packages_consts.MURANO_REPO_URL
image_specs = package.images()
try:
imgs = muranoclient_utils.ensure_images(
glance_client=glance_client,
image_specs=image_specs,
base_url=base_url)
for img in imgs:
msg = _("Trying to add {0} image to glance. "
"Image will be ready for deployment after "
"successful upload").format(img['name'],)
messages.warning(request, msg)
log_msg = _("Trying to add {0}, {1} image to "
"glance. Image will be ready for "
"deployment after successful upload")\
.format(img['name'], img['id'],)
LOG.info(log_msg)
if step_data:
step_data['images'].append(img)
except Exception as e:
msg = _("Error {0} occurred while installing "
"images for {1}").format(e, name)
messages.error(request, msg)
LOG.exception(msg)
class PackageDefinitionsView(horizon_tables.DataTableView):
table_class = tables.PackageDefinitionsTable
template_name = 'packages/index.html'
page_title = _("Packages")
_more = False
_prev = False
def has_more_data(self, table):
return self._more
def has_prev_data(self, table):
return self._prev
def get_data(self):
sort_dir = self.request.GET.get('sort_dir', 'asc')
opts = {
'include_disabled': True,
'sort_dir': sort_dir,
}
marker = self.request.GET.get(
tables.PackageDefinitionsTable._meta.pagination_param, None)
opts = self.get_filters(opts)
packages = []
page_size = utils.get_page_size(self.request)
with api.handled_exceptions(self.request):
packages, extra = pkg_api.package_list(
self.request, marker=marker, filters=opts, paginate=True,
page_size=page_size)
if sort_dir == 'asc':
self._more = extra
else:
packages = list(reversed(packages))
self._prev = extra
if packages:
if sort_dir == 'asc':
backward_marker = packages[0].id
opts['sort_dir'] = 'desc'
else:
backward_marker = packages[-1].id
opts['sort_dir'] = 'asc'
__, extra = pkg_api.package_list(
self.request, filters=opts, paginate=True,
marker=backward_marker, page_size=0)
if sort_dir == 'asc':
self._prev = extra
else:
self._more = extra
# Add information about project tenant for admin user
if self.request.user.is_superuser:
tenants = []
try:
tenants, _more = keystone.tenant_list(self.request)
except Exception:
exceptions.handle(self.request,
_("Unable to retrieve project list."))
tenent_name_by_id = {tenant.id: tenant.name for tenant in tenants}
for i, p in enumerate(packages):
packages[i].tenant_name = tenent_name_by_id.get(p.owner_id)
else:
current_tenant = self.request.session['token'].tenant
for i, package in enumerate(packages):
if package.owner_id == current_tenant['id']:
packages[i].tenant_name = current_tenant['name']
else:
packages[i].tenant_name = _('UNKNOWN')
return packages
def get_context_data(self, **kwargs):
context = super(PackageDefinitionsView,
self).get_context_data(**kwargs)
context['tenant_id'] = self.request.session['token'].tenant['id']
return context
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 ImportBundleWizard(horizon_views.PageTitleMixin, views.ModalFormMixin,
wizard_views.SessionWizardView):
template_name = 'packages/import_bundle.html'
page_title = _("Import Bundle")
def get_context_data(self, **kwargs):
context = super(ImportBundleWizard, self).get_context_data(**kwargs)
repo_url = urlparse.urlparse(packages_consts.MURANO_REPO_URL)
context['murano_repo_url'] = "{}://{}".format(
repo_url.scheme, repo_url.netloc)
return context
def get_form_initial(self, step):
initial_dict = self.initial_dict.get(step, {})
if step == 'upload':
for name in ['url', 'name', 'import_type']:
if name in self.request.GET:
initial_dict[name] = self.request.GET[name]
return initial_dict
def process_step(self, form):
@catalog_views.update_latest_apps
def _update_latest_apps(request, app_id):
LOG.info('Adding {0} application to the'
' latest apps list'.format(app_id))
step_data = self.get_form_step_data(form)
if self.steps.current == 'upload':
import_type = form.cleaned_data['import_type']
data = {}
f = None
base_url = packages_consts.MURANO_REPO_URL
if import_type == 'by_url':
f = form.cleaned_data['url']
elif import_type == 'by_name':
f = muranoclient_utils.to_url(
form.cleaned_data['name'],
path='bundles/',
base_url=base_url,
extension='.bundle',
)
try:
bundle = muranoclient_utils.Bundle.from_file(f)
except Exception as e:
if '(404)' in e.message:
msg = _("Bundle creation failed."
"Reason: Can't find Bundle name from repository.")
else:
msg = _("Bundle creation failed."
"Reason: {0}").format(e)
LOG.exception(msg)
messages.error(self.request, msg)
raise exceptions.Http302(
reverse('horizon:app-catalog:packages:index'))
for package_spec in bundle.package_specs():
try:
package = muranoclient_utils.Package.from_location(
package_spec['Name'],
version=package_spec.get('Version'),
url=package_spec.get('Url'),
base_url=base_url,
path=None,
)
except Exception as e:
msg = _("Error {0} occurred while parsing package {1}")\
.format(e, package_spec.get('Name'))
messages.error(self.request, msg)
LOG.exception(msg)
continue
reqs = package.requirements(base_url=base_url)
for dep_name, dep_package in six.iteritems(reqs):
_ensure_images(dep_name, dep_package,
self.request)
try:
files = {dep_name: dep_package.file()}
package = api.muranoclient(
self.request).packages.create(data, files)
messages.success(
self.request,
_('Package {0} uploaded').format(dep_name)
)
_update_latest_apps(
request=self.request, app_id=package.id)
except exc.HTTPConflict:
msg = _("Package {0} already registered.").format(
dep_name)
messages.warning(self.request, msg)
LOG.exception(msg)
except exc.HTTPException as e:
reason = muranodashboard_utils.parse_api_error(
getattr(e, 'details', ''))
if not reason:
raise
msg = _("Package {0} upload failed. {1}").format(
dep_name, reason)
messages.warning(self.request, msg)
LOG.exception(msg)
except Exception as e:
msg = _("Importing package {0} failed. "
"Reason: {1}").format(dep_name, e)
messages.warning(self.request, msg)
LOG.exception(msg)
continue
return step_data
def done(self, form_list, **kwargs):
redirect = reverse('horizon:app-catalog:packages:index')
msg = _('Bundle successfully imported.')
LOG.info(msg)
messages.success(self.request, msg)
return http.HttpResponseRedirect(six.text_type(redirect))
class ImportPackageWizard(horizon_views.PageTitleMixin, views.ModalFormMixin,
wizard_views.SessionWizardView):
file_storage = storage.FileSystemStorage(location=consts.CACHE_DIR)
template_name = 'packages/upload.html'
condition_dict = {'add_category': is_app}
page_title = _("Import Package")
def get_form_initial(self, step):
initial_dict = self.initial_dict.get(step, {})
if step == 'upload':
for name in ['url', 'repo_name', 'repo_version', 'import_type']:
if name in self.request.GET:
initial_dict[name] = self.request.GET[name]
return initial_dict
def get_context_data(self, **kwargs):
context = super(ImportPackageWizard, self).get_context_data(**kwargs)
repo_url = urlparse.urlparse(packages_consts.MURANO_REPO_URL)
context['murano_repo_url'] = "{}://{}".format(
repo_url.scheme, repo_url.netloc)
return context
def done(self, form_list, **kwargs):
data = self.get_all_cleaned_data()
app_id = self.storage.get_step_data('upload')['package'].id
# Remove package file from result data
for key in ('package', 'import_type', 'url',
'repo_version', 'repo_name'):
del data[key]
dep_pkgs = self.storage.get_step_data('upload').get(
'dependencies', [])
installed_images = self.storage.get_step_data('upload').get(
'images', [])
redirect = reverse('horizon:app-catalog:packages:index')
dep_data = {'enabled': data['enabled'],
'is_public': data['is_public']}
murano_client = api.muranoclient(self.request)
for dep_pkg in dep_pkgs:
try:
murano_client.packages.update(dep_pkg.id, dep_data)
LOG.debug('Success update for package {0}.'.format(dep_pkg.id))
except Exception as e:
msg = _("Couldn't update package {0} parameters. Error: {1}")\
.format(dep_pkg.fully_qualified_name, e)
LOG.warning(msg)
messages.warning(self.request, msg)
# Images have been imported as private images during the 'upload' step
# If the package is public, make the required images public
if data['is_public']:
try:
glance_client = glance.glanceclient(self.request, '1')
except Exception:
glance_client = None
if glance_client:
for img in installed_images:
try:
glance_client.images.update(img['id'], is_public=True)
LOG.debug(
'Success update for image {0}'.format(img['id']))
except Exception as e:
msg = _("Error {0} occurred while setting image {1}, "
"{2} public").format(e, img['name'], img['id'])
messages.error(self.request, msg)
LOG.exception(msg)
elif len(installed_images):
msg = _("Couldn't initialise glance v1 client, "
"therefore could not make the following images "
"public: {0}").format(' '.join(
[img['name'] for img in installed_images]))
messages.warning(self.request, msg)
LOG.warning(msg)
try:
data['tags'] = [t.strip() for t in data['tags'].split(',')]
murano_client.packages.update(app_id, data)
except exc.HTTPForbidden:
msg = _("You are not allowed to change"
" this properties of the package")
LOG.exception(msg)
exceptions.handle(
self.request, msg,
redirect=reverse('horizon:app-catalog:packages:index'))
except (exc.HTTPException, Exception):
LOG.exception(_('Modifying package failed'))
exceptions.handle(self.request,
_('Unable to modify package'),
redirect=redirect)
else:
msg = _('Package parameters successfully updated.')
LOG.info(msg)
messages.success(self.request, msg)
return http.HttpResponseRedirect(six.text_type(redirect))
def _handle_exception(self, original_e):
exc_info = sys.exc_info()
reason = ''
if hasattr(original_e, 'details'):
try:
error = json.loads(original_e.details).get('error')
if error:
reason = error.get('message')
except ValueError:
# Let horizon operate with original exception
six.reraise(original_e.__class__,
original_e.__class__(original_e),
exc_info[2])
msg = _('Uploading package failed. {0}').format(reason)
LOG.exception(msg)
exceptions.handle(
self.request,
msg,
redirect=reverse('horizon:app-catalog:packages:index'))
def process_step(self, form):
@catalog_views.update_latest_apps
def _update_latest_apps(request, app_id):
LOG.info('Adding {0} application to the'
' latest apps list'.format(app_id))
step_data = self.get_form_step_data(form).copy()
if self.steps.current == 'upload':
import_type = form.cleaned_data['import_type']
data = {}
f = None
base_url = packages_consts.MURANO_REPO_URL
if import_type == 'upload':
pkg = form.cleaned_data['package']
f = pkg.file
elif import_type == 'by_url':
f = form.cleaned_data['url']
elif import_type == 'by_name':
name = form.cleaned_data['repo_name']
version = form.cleaned_data['repo_version']
f = muranoclient_utils.to_url(
name, version=version,
path='apps/',
extension='.zip',
base_url=base_url,
)
try:
package = muranoclient_utils.Package.from_file(f)
name = package.manifest['FullName']
except Exception as e:
if '(404)' in e.message:
msg = _("Package creation failed."
"Reason: Can't find Package name from repository.")
else:
msg = _("Package creation failed."
"Reason: {0}").format(e)
LOG.exception(msg)
messages.error(self.request, msg)
raise exceptions.Http302(
reverse('horizon:app-catalog:packages:index'))
reqs = package.requirements(base_url=base_url)
original_package = reqs.pop(name)
step_data['dependencies'] = []
step_data['images'] = []
for dep_name, dep_package in six.iteritems(reqs):
_ensure_images(dep_name, dep_package, self.request, step_data)
try:
files = {dep_name: dep_package.file()}
package = api.muranoclient(self.request).packages.create(
data, files)
messages.success(
self.request,
_('Package {0} uploaded').format(dep_name)
)
_update_latest_apps(
request=self.request, app_id=package.id)
step_data['dependencies'].append(package)
except exc.HTTPConflict:
msg = _("Package {0} already registered.").format(
dep_name)
messages.warning(self.request, msg)
LOG.exception(msg)
except Exception as e:
msg = _("Error {0} occurred while "
"installing package {1}").format(e, dep_name)
messages.error(self.request, msg)
LOG.exception(msg)
continue
# add main packages images
_ensure_images(name, original_package, self.request, step_data)
# import main package itself
try:
files = {name: original_package.file()}
package = api.muranoclient(self.request).packages.create(
data, files)
messages.success(self.request,
_('Package {0} uploaded').format(name))
_update_latest_apps(request=self.request, app_id=package.id)
step_data['package'] = package
except exc.HTTPConflict:
msg = _("Package with specified name already exists")
LOG.exception(msg)
exceptions.handle(
self.request,
msg,
redirect=reverse('horizon:app-catalog:packages:index'))
except exc.HTTPInternalServerError as e:
self._handle_exception(e)
except exc.HTTPException as e:
reason = muranodashboard_utils.parse_api_error(
getattr(e, 'details', ''))
if not reason:
raise
LOG.exception(reason)
exceptions.handle(
self.request,
reason,
redirect=reverse('horizon:app-catalog:packages:index'))
except Exception as original_e:
self._handle_exception(original_e)
return step_data
def get_form_kwargs(self, step=None):
kwargs = {}
if step == 'add_category':
kwargs.update({'request': self.request})
if step == 'modify':
package = self.storage.get_step_data('upload').get('package')
kwargs.update({'package': package, 'request': self.request})
return kwargs
class ModifyPackageView(views.ModalFormView):
form_class = forms.ModifyPackageForm
template_name = 'packages/modify_package.html'
success_url = reverse_lazy('horizon:app-catalog:packages:index')
failure_url = reverse_lazy('horizon:app-catalog:packages:index')
page_title = _("Modify Package")
def get_initial(self):
app_id = self.kwargs['app_id']
package = api.muranoclient(self.request).packages.get(app_id)
return {
'package': package,
'app_id': app_id,
}
def get_context_data(self, **kwargs):
context = super(ModifyPackageView, self).get_context_data(**kwargs)
context['app_id'] = self.kwargs['app_id']
context['type'] = self.get_form().initial['package'].type
return context
class DetailView(horizon_views.HorizonTemplateView):
template_name = 'packages/detail.html'
page_title = "{{ app.name }}"
def get_context_data(self, **kwargs):
context = super(DetailView, self).get_context_data(**kwargs)
app = self.get_data()
context["app"] = app
return context
def get_data(self):
app = None
try:
app_id = self.kwargs['app_id']
app = api.muranoclient(self.request).packages.get(app_id)
except Exception:
INDEX_URL = 'horizon:app-catalog:packages:index'
exceptions.handle(self.request,
_('Unable to retrieve package details.'),
redirect=reverse(INDEX_URL))
return app
def download_packge(request, app_name, app_id):
try:
body = api.muranoclient(request).packages.download(app_id)
content_type = 'application/octet-stream'
response = http.HttpResponse(body, content_type=content_type)
response['Content-Disposition'] = 'filename={name}.zip'.format(
name=app_name)
return response
except exc.HTTPException:
LOG.exception(_('Something went wrong during package downloading'))
redirect = reverse('horizon:app-catalog:packages:index')
exceptions.handle(request,
_('Unable to download package.'),
redirect=redirect)