# 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 urllib.parse as urlparse 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 gettext_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 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 reqs.items(): _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(str(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(str(redirect)) def _handle_exception(self, original_e): reason = '' if hasattr(original_e, 'details'): try: error = json.loads(original_e.details).get('error') if error: reason = error.get('message') except ValueError: raise original_e 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 reqs.items(): _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)