# 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. """Provides means to wrap dicts coming from DB layer in objects. The Conductor can fetch only values represented by JSON. That limitation comes from Oslo RPC implementation. This module provides means to wrap a fetched value, always dictionary, into an immutable Resource object. A descendant of Resource class might provide back references to parent objects and helper methods. """ import datetime import six from sahara.conductor import objects from sahara import exceptions as ex from sahara.i18n import _ from sahara.swift import swift_helper from sahara.utils import types def wrap(resource_class): """A decorator wraps dict returned by a given function into a Resource.""" def decorator(func): def handle(*args, **kwargs): ret = func(*args, **kwargs) if isinstance(ret, list): return [resource_class(el) for el in ret] elif ret: return resource_class(ret) else: return None return handle return decorator class Resource(types.FrozenDict): """Represents dictionary as an immutable object. Enhancing it with back references and helper methods. For instance, the following dictionary: {'first': {'a': 1, 'b': 2}, 'second': [1,2,3]} after wrapping with Resource will look like an object, let it be 'res' with the following fields: res.first res.second 'res.first' will in turn be wrapped into Resource with two fields: res.first.a == 1 res.first.b == 2 'res.second', which is a list, will be transformed into a tuple for immutability: res.second == (1,2,3) Additional helper methods could be specified in descendant classes. '_children' specifies children of that specific Resource in the following format: {refname: (child_class, backref_name)} Back reference is a reference to parent object which is injected into a Resource during wrapping. """ _resource_name = 'resource' _children = {} _filter_fields = [] _sanitize_fields = {} def __init__(self, dct): super(Resource, self).__setattr__('_initial_dict', dct) newdct = dict() for refname, entity in six.iteritems(dct): newdct[refname] = self._wrap_entity(refname, entity) super(Resource, self).__init__(newdct) def to_dict(self): """Return dictionary representing the Resource for REST API. On the way filter out fields which shouldn't be exposed. """ return self._to_dict(None) def to_wrapped_dict(self): return {self._resource_name: self.to_dict()} # Construction def _wrap_entity(self, refname, entity): if isinstance(entity, Resource): # that is a back reference return entity elif isinstance(entity, list): return self._wrap_list(refname, entity) elif isinstance(entity, dict): return self._wrap_dict(refname, entity) elif self._is_passthrough_type(entity): return entity else: raise TypeError(_("Unsupported type: %s") % type(entity).__name__) def _wrap_list(self, refname, lst): newlst = [self._wrap_entity(refname, entity) for entity in lst] return types.FrozenList(newlst) def _wrap_dict(self, refname, dct): if refname in self._children: dct = dict(dct) child_class = self._children[refname][0] backref_name = self._children[refname][1] if backref_name: dct[backref_name] = self return child_class(dct) else: return Resource(dct) def _is_passthrough_type(self, entity): return (entity is None or isinstance(entity, (six.integer_types, float, datetime.datetime, six.string_types))) # Conversion to dict def _to_dict(self, backref): dct = dict() for refname, entity in six.iteritems(self): if refname != backref and refname not in self._filter_fields: childs_backref = None if refname in self._children: childs_backref = self._children[refname][1] dct[refname] = self._entity_to_dict(entity, childs_backref) sanitize = self._sanitize_fields.get(refname) if sanitize is not None: dct[refname] = sanitize(self, dct[refname]) return dct def _entity_to_dict(self, entity, childs_backref): if isinstance(entity, Resource): return entity._to_dict(childs_backref) elif isinstance(entity, list): return self._list_to_dict(entity, childs_backref) elif entity is not None: return entity def _list_to_dict(self, lst, childs_backref): return [self._entity_to_dict(entity, childs_backref) for entity in lst] def __getattr__(self, item): return self[item] def __setattr__(self, *args): raise ex.FrozenClassError(self) class NodeGroupTemplateResource(Resource, objects.NodeGroupTemplate): _resource_name = 'node_group_template' class InstanceResource(Resource, objects.Instance): _filter_fields = ['tenant_id', 'node_group_id', "volumes"] @property def cluster_id(self): return self.node_group.cluster_id @property def cluster(self): return self.node_group.cluster class NodeGroupResource(Resource, objects.NodeGroup): _children = { 'instances': (InstanceResource, 'node_group'), 'node_group_template': (NodeGroupTemplateResource, None) } _filter_fields = ['tenant_id', 'cluster_id', 'cluster_template_id', 'image_username', 'open_ports'] class ClusterTemplateResource(Resource, objects.ClusterTemplate): _resource_name = 'cluster_template' _children = { 'node_groups': (NodeGroupResource, 'cluster_template') } class ClusterResource(Resource, objects.Cluster): def sanitize_cluster_configs(self, cluster_configs): if 'proxy_configs' in cluster_configs: del cluster_configs['proxy_configs'] return cluster_configs _resource_name = 'cluster' _children = { 'node_groups': (NodeGroupResource, 'cluster'), 'cluster_template': (ClusterTemplateResource, None) } _filter_fields = ['management_private_key', 'extra', 'rollback_info', 'sahara_info'] _sanitize_fields = {'cluster_configs': sanitize_cluster_configs} # EDP Resources class DataSource(Resource, objects.DataSource): _resource_name = "data_source" _filter_fields = ['credentials'] class JobExecution(Resource, objects.JobExecution): def sanitize_job_configs(self, job_configs): if 'configs' in job_configs: configs = job_configs['configs'] if swift_helper.HADOOP_SWIFT_USERNAME in configs: configs[swift_helper.HADOOP_SWIFT_USERNAME] = "" if swift_helper.HADOOP_SWIFT_PASSWORD in configs: configs[swift_helper.HADOOP_SWIFT_PASSWORD] = "" if 'trusts' in job_configs: del job_configs['trusts'] if 'proxy_configs' in job_configs: del job_configs['proxy_configs'] return job_configs def sanitize_info(self, info): if 'actions' in info: for d in info['actions']: if 'conf' in d: del d['conf'] return info _resource_name = "job_execution" _filter_fields = ['extra'] _sanitize_fields = {'job_configs': sanitize_job_configs, 'info': sanitize_info} # TODO(egafford): Sanitize interface ("secret" bool field on job args?) class JobBinary(Resource, objects.JobBinary): _resource_name = "job_binary" _filter_fields = ['extra'] class JobBinaryInternal(Resource, objects.JobBinaryInternal): _resource_name = "job_binary_internal" class Job(Resource, objects.Job): _resource_name = "job" _children = { 'mains': (JobBinary, None), 'libs': (JobBinary, None) }