264 lines
10 KiB
Python
264 lines
10 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 os
|
|
import re
|
|
import time
|
|
import logging
|
|
from muranodashboard.dynamic_ui import metadata
|
|
from .helpers import decamelize, get_yaql_expr, create_yaql_context
|
|
|
|
|
|
try:
|
|
from collections import OrderedDict
|
|
except ImportError: # python2.6
|
|
from ordereddict import OrderedDict
|
|
import yaql
|
|
import yaml
|
|
from yaml.scanner import ScannerError
|
|
from django.utils.translation import ugettext_lazy as _
|
|
import copy
|
|
from muranodashboard.environments.consts import CACHE_REFRESH_SECONDS_INTERVAL
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class Service(object):
|
|
"""Class for keeping service persistent data, the most important are two:
|
|
``self.forms`` list of service's steps (as Django form classes) and
|
|
``self.cleaned_data`` dictionary of data from service validated steps.
|
|
|
|
Attribute ``self.cleaned_data`` is needed for, e.g. ServiceA.Step2, be
|
|
able to reference data at ServiceA.Step1 while actual form instance
|
|
representing Step1 is already gone.
|
|
|
|
Because the need to store this data per-user, sessions must be employed
|
|
(actually, they are not the _only_ way of doing this, but the most simple
|
|
one), and because every Django session backend uses pickle serialization,
|
|
__getstate__/__setstate__ methods for custom pickle serialization must be
|
|
implemented.
|
|
"""
|
|
NON_SERIALIZABLE_ATTRS = ('forms', 'context')
|
|
|
|
def __init__(self, forms=None, **kwargs):
|
|
self.context = create_yaql_context()
|
|
self.cleaned_data = {}
|
|
self.forms = []
|
|
self._forms = []
|
|
for key, value in kwargs.iteritems():
|
|
setattr(self, key, value)
|
|
|
|
if forms:
|
|
for data in forms:
|
|
name, field_specs, validators = self.extract_form_data(data)
|
|
self._add_form(name, field_specs, validators)
|
|
|
|
# for pickling/unpickling
|
|
self._forms.append((name, field_specs, validators))
|
|
|
|
def __getstate__(self):
|
|
log.debug("Pickling service '{service.type}'".format(
|
|
service=self))
|
|
dct = dict((k, v) for (k, v) in self.__dict__.iteritems()
|
|
if not k in self.NON_SERIALIZABLE_ATTRS)
|
|
return dct
|
|
|
|
def __setstate__(self, d):
|
|
log.debug("Unpickling service '{type}'".format(**d))
|
|
for k, v in d.iteritems():
|
|
setattr(self, k, v)
|
|
# dealing with the attributes which cannot be serialized (see
|
|
# http://tinyurl.com/kxx3tam on pickle restrictions )
|
|
# yaql context is not serializable because it contains lambda functions
|
|
self.context = create_yaql_context()
|
|
# form classes are not serializable 'cause they are defined dynamically
|
|
self.forms = []
|
|
for name, field_specs, validators in d.get('_forms', []):
|
|
self._add_form(name, field_specs, validators)
|
|
|
|
def _add_form(self, _name, _specs, _validators):
|
|
import muranodashboard.dynamic_ui.forms as forms
|
|
|
|
class Form(forms.ServiceConfigurationForm):
|
|
__metaclass__ = forms.DynamicFormMetaclass
|
|
|
|
service = self
|
|
name = _name
|
|
field_specs = _specs
|
|
validators = _validators
|
|
|
|
self.forms.append(Form)
|
|
|
|
@staticmethod
|
|
def extract_form_data(form_data):
|
|
form_name = form_data.keys()[0]
|
|
form_data = form_data[form_name]
|
|
return form_name, form_data['fields'], form_data.get('validators', [])
|
|
|
|
def get_data(self, form_name, expr, data=None):
|
|
"""First try to get value from cleaned data, if none
|
|
found, use raw data."""
|
|
if data:
|
|
self.update_cleaned_data(data, form_name=form_name)
|
|
expr = get_yaql_expr(expr)
|
|
data = self.cleaned_data
|
|
value = data and yaql.parse(expr).evaluate(data, self.context)
|
|
return data != {}, value
|
|
|
|
def update_cleaned_data(self, data, form=None, form_name=None):
|
|
form_name = form_name or form.__class__.__name__
|
|
if data:
|
|
self.cleaned_data[form_name] = data
|
|
return self.cleaned_data
|
|
|
|
|
|
def import_service(services, full_service_name, service_file):
|
|
try:
|
|
with open(service_file) as stream:
|
|
yaml_desc = yaml.load(stream)
|
|
except (OSError, ScannerError) as e:
|
|
log.warn("Failed to import service definition from {0},"
|
|
" reason: {1!s}".format(service_file, e))
|
|
else:
|
|
service = dict((decamelize(k), v) for (k, v) in yaml_desc.iteritems())
|
|
services[full_service_name] = Service(**service)
|
|
log.info("Added service '{0}' from '{1}'".format(
|
|
services[full_service_name].name, service_file))
|
|
|
|
|
|
def import_all_services(request):
|
|
"""Tries to import all metadata from repository, this includes calculating
|
|
hash-sum of local metadata package, making HTTP-request and unpacking
|
|
received package into cache directory. Calling this function several
|
|
times for each form in dynamicUI is inevitable, so to avoid significant
|
|
delays all metadata-related stuff is actually performed no more often than
|
|
each CACHE_REFRESH_SECONDS_INTERVAL.
|
|
|
|
Expected contents of metadata package is:
|
|
- <full_service_name1>/<form_definitionA>.yaml
|
|
- <full_service_name2>/<form_definitionB>.yaml
|
|
...
|
|
If there is no YAMLs with form definitions inside <full_service_nameN>
|
|
dir, then <full_service_nameN> won't be shown in Create Service first step.
|
|
"""
|
|
last_check_time = request.session.get('last_check_time', 0)
|
|
if time.time() - last_check_time > CACHE_REFRESH_SECONDS_INTERVAL:
|
|
request.session['last_check_time'] = time.time()
|
|
directory, modified = metadata.get_ui_metadata(request)
|
|
session_is_empty = not request.session.get('services', {})
|
|
# check directory here in case metadata service is not available
|
|
# and None is returned as directory value.
|
|
# TODO: it is better to use redirect for that purpose (if possible)
|
|
if directory is not None and (modified or session_is_empty):
|
|
request.session['services'] = {}
|
|
for full_service_name in os.listdir(directory):
|
|
final_dir = os.path.join(directory, full_service_name)
|
|
if os.path.isdir(final_dir) and len(os.listdir(final_dir)):
|
|
filename = os.listdir(final_dir)[0]
|
|
if filename.endswith('.yaml'):
|
|
import_service(request.session['services'],
|
|
full_service_name,
|
|
os.path.join(final_dir, filename))
|
|
|
|
|
|
def iterate_over_services(request):
|
|
import_all_services(request)
|
|
services = request.session.get('services', {})
|
|
for service in sorted(services.values(), key=lambda v: v.name):
|
|
yield service.type, service
|
|
|
|
|
|
def make_forms_getter(initial_forms=lambda request: copy.copy([])):
|
|
def _get_forms(request):
|
|
_forms = initial_forms(request)
|
|
for srv_type, service in iterate_over_services(request):
|
|
for step, form in enumerate(service.forms):
|
|
_forms.append(('{0}-{1}'.format(srv_type, step), form))
|
|
return _forms
|
|
return _get_forms
|
|
|
|
|
|
def service_type_from_id(service_id):
|
|
match = re.match('(.*)-[0-9]+', service_id)
|
|
if match:
|
|
return match.group(1)
|
|
else: # if no number suffix found, it was service_type itself passed in
|
|
return service_id
|
|
|
|
|
|
def with_service(request, service_id, getter, default):
|
|
service_type = service_type_from_id(service_id)
|
|
for srv_type, service in iterate_over_services(request):
|
|
if srv_type == service_type:
|
|
return getter(service)
|
|
return default
|
|
|
|
|
|
def get_service_name(request, service_id):
|
|
return with_service(request, service_id, lambda service: service.name, '')
|
|
|
|
|
|
def get_service_field_descriptions(request, service_id, index):
|
|
def get_descriptions(service):
|
|
form_cls = service.forms[index]
|
|
descriptions = []
|
|
for field in form_cls.base_fields.itervalues():
|
|
title = field.description_title
|
|
description = field.description
|
|
if description:
|
|
descriptions.append((title, description))
|
|
return descriptions
|
|
return with_service(request, service_id, get_descriptions, [])
|
|
|
|
|
|
def get_service_type(wizard):
|
|
cleaned_data = wizard.get_cleaned_data_for_step('service_choice') \
|
|
or {'service': 'none'}
|
|
return cleaned_data.get('service')
|
|
|
|
|
|
def get_service_choices(request, filter_func=None):
|
|
filter_func = filter_func or (lambda srv: True, None)
|
|
filtered, not_filtered = [], []
|
|
for srv_type, service in iterate_over_services(request):
|
|
has_filtered, message = filter_func(service, request)
|
|
if has_filtered:
|
|
filtered.append((srv_type, service.name))
|
|
else:
|
|
not_filtered.append((service.name, message))
|
|
return filtered, not_filtered
|
|
|
|
get_forms = make_forms_getter()
|
|
|
|
|
|
def get_service_checkers(request):
|
|
def make_comparator(srv_id):
|
|
def compare(wizard):
|
|
return service_type_from_id(srv_id) == get_service_type(wizard)
|
|
return compare
|
|
|
|
return [(srv_id, make_comparator(srv_id)) for srv_id, form
|
|
in get_forms(request)]
|
|
|
|
|
|
def get_service_descriptions(request):
|
|
descriptions = []
|
|
for srv_type, service in iterate_over_services(request):
|
|
description = getattr(service, 'description', _("<b>Default service \
|
|
description</b>. If you want to see here something meaningful, please \
|
|
provide `description' field in service markup."))
|
|
descriptions.append((srv_type, description))
|
|
return descriptions
|