basic $[] functionality
diff --git a/reclass/__init__.py b/reclass/__init__.py
index c86b880..eeb8f7f 100644
--- a/reclass/__init__.py
+++ b/reclass/__init__.py
@@ -11,9 +11,9 @@
from storage.loader import StorageBackendLoader
from storage.memcache_proxy import MemcacheProxy
-def get_storage(storage_type, nodes_uri, classes_uri, **kwargs):
+def get_storage(storage_type, nodes_uri, classes_uri, exports_uri, **kwargs):
storage_class = StorageBackendLoader(storage_type).load()
- return MemcacheProxy(storage_class(nodes_uri, classes_uri, **kwargs))
+ return MemcacheProxy(storage_class(nodes_uri, classes_uri, exports_uri, **kwargs))
def output(data, fmt, pretty_print=False, no_refs=False):
diff --git a/reclass/cli.py b/reclass/cli.py
index 1f7213d..17467cd 100644
--- a/reclass/cli.py
+++ b/reclass/cli.py
@@ -28,7 +28,8 @@
defaults=defaults)
storage = get_storage(options.storage_type, options.nodes_uri,
- options.classes_uri, default_environment='base')
+ options.classes_uri, options.exports_uri,
+ default_environment='base')
class_mappings = defaults.get('class_mappings')
reclass = Core(storage, class_mappings)
diff --git a/reclass/config.py b/reclass/config.py
index 3d218d8..52fbe4c 100644
--- a/reclass/config.py
+++ b/reclass/config.py
@@ -28,7 +28,10 @@
help='the URI to the nodes storage [%default]'),
ret.add_option('-c', '--classes-uri', dest='classes_uri',
default=defaults.get('classes_uri', OPT_CLASSES_URI),
- help='the URI to the classes storage [%default]')
+ help='the URI to the classes storage [%default]'),
+ ret.add_option('-e', '--exports-uri', dest='exports_uri',
+ default=defaults.get('exports_uri', OPT_EXPORTS_URI),
+ help='the URI to the exports file [%default]')
return ret
@@ -127,11 +130,13 @@
parser.error('Must specify --inventory-base-uri or --nodes-uri')
elif options.inventory_base_uri is None and options.classes_uri is None:
parser.error('Must specify --inventory-base-uri or --classes-uri')
+ elif options.inventory_base_uri is None and options.exports_uri is None:
+ parser.error('Must specify --inventory-base-uri or --exports-uri')
return parser, option_checker
-def path_mangler(inventory_base_uri, nodes_uri, classes_uri):
+def path_mangler(inventory_base_uri, nodes_uri, classes_uri, exports_uri):
if inventory_base_uri is None:
# if inventory_base is not given, default to current directory
@@ -139,20 +144,21 @@
nodes_uri = nodes_uri or 'nodes'
classes_uri = classes_uri or 'classes'
+ exports_uri = exports_uri or 'exports'
def _path_mangler_inner(path):
ret = os.path.join(inventory_base_uri, path)
ret = os.path.expanduser(ret)
return os.path.abspath(ret)
- n, c = map(_path_mangler_inner, (nodes_uri, classes_uri))
- if n == c:
- raise errors.DuplicateUriError(n, c)
+ n, c, e = map(_path_mangler_inner, (nodes_uri, classes_uri, exports_uri))
+ if n == c or n == e or c == e:
+ raise errors.DuplicateUriError(n, c, e)
common = os.path.commonprefix((n, c))
if common == n or common == c:
raise errors.UriOverlapError(n, c)
- return n, c
+ return n, c, e
def get_options(name, version, description,
@@ -178,9 +184,9 @@
options, args = parser.parse_args()
checker(options, args)
- options.nodes_uri, options.classes_uri = \
+ options.nodes_uri, options.classes_uri, options.exports_uri = \
path_mangler(options.inventory_base_uri, options.nodes_uri,
- options.classes_uri)
+ options.classes_uri, options.exports_uri)
return options
diff --git a/reclass/core.py b/reclass/core.py
index b9da0d2..b3620b6 100644
--- a/reclass/core.py
+++ b/reclass/core.py
@@ -7,6 +7,7 @@
# Released under the terms of the Artistic Licence 2.0
#
+import copy
import time
#import types
import re
@@ -14,6 +15,8 @@
import fnmatch
import shlex
import string
+import yaml
+from reclass.output.yaml_outputter import ExplicitDumper
from reclass.datatypes import Entity, Classes, Parameters
from reclass.errors import MappingFormatError, ClassNotFound
from reclass.defaults import AUTOMATIC_RECLASS_PARAMETERS
@@ -111,20 +114,25 @@
merge_base.merge(entity)
return merge_base
- def _nodeinfo(self, nodename):
+ def _get_automatic_parameters(self, nodename):
+ if AUTOMATIC_RECLASS_PARAMETERS:
+ return Parameters({ '_reclass_': { 'name': { 'full': nodename, 'short': string.split(nodename, '.')[0] } } })
+ else:
+ return Parameters()
+
+
+ def _nodeinfo(self, nodename, exports):
node_entity = self._storage.get_node(nodename)
base_entity = Entity(name='base')
base_entity.merge(self._get_class_mappings_entity(node_entity.name))
base_entity.merge(self._get_input_data_entity())
- if AUTOMATIC_RECLASS_PARAMETERS:
- params = { '_reclass_': { 'name': { 'full': nodename, 'short': string.split(nodename, '.')[0] } } }
- base_entity.merge_parameters(params)
+ base_entity.merge_parameters(self._get_automatic_parameters(nodename))
seen = {}
merge_base = self._recurse_entity(base_entity, seen=seen,
nodename=base_entity.name)
ret = self._recurse_entity(node_entity, merge_base, seen=seen,
nodename=node_entity.name)
- ret.interpolate()
+ ret.interpolate(nodename, exports)
return ret
def _nodeinfo_as_dict(self, nodename, entity):
@@ -137,13 +145,28 @@
ret.update(entity.as_dict())
return ret
+ def _update_exports(self, old, new):
+ old_yaml = yaml.dump(old.as_dict(), default_flow_style=True, Dumper=ExplicitDumper)
+ new_yaml = yaml.dump(new.as_dict(), default_flow_style=True, Dumper=ExplicitDumper)
+ if old_yaml != new_yaml:
+ self._storage.put_exports(new)
+
def nodeinfo(self, nodename):
- return self._nodeinfo_as_dict(nodename, self._nodeinfo(nodename))
+ original_exports = Parameters(self._storage.get_exports())
+ exports = copy.deepcopy(original_exports)
+ original_exports.render_simple()
+ ret = self._nodeinfo_as_dict(nodename, self._nodeinfo(nodename, exports))
+ self._update_exports(original_exports, exports)
+ return ret
def inventory(self):
+ original_exports = Parameters(self._storage.get_exports())
+ exports = copy.deepcopy(original_exports)
+ original_exports.render_simple()
entities = {}
for n in self._storage.enumerate_nodes():
- entities[n] = self._nodeinfo(n)
+ entities[n] = self._nodeinfo(n, exports)
+ self._update_exports(original_exports, exports)
nodes = {}
applications = {}
diff --git a/reclass/datatypes/entity.py b/reclass/datatypes/entity.py
index c6c3b28..41c9db6 100644
--- a/reclass/datatypes/entity.py
+++ b/reclass/datatypes/entity.py
@@ -75,9 +75,11 @@
def merge_parameters(self, params):
self._parameters.merge(params)
- def interpolate(self):
+ def interpolate(self, nodename, exports):
self._exports.interpolate_from_external(self._parameters)
- self._parameters.interpolate(exports={ self._name: self._exports.as_dict() })
+ exports.merge({ nodename: self._exports.as_dict() })
+ exports.render_simple()
+ self._parameters.interpolate(exports=exports.as_dict())
def __eq__(self, other):
return isinstance(other, type(self)) \
@@ -92,7 +94,7 @@
return not self.__eq__(other)
def __repr__(self):
- return "%s(%r, %r, %r, uri=%r, name=%r)" % (self.__class__.__name__,
+ return "%s(%r, %r, %r, %r, uri=%r, name=%r)" % (self.__class__.__name__,
self.classes,
self.applications,
self.parameters,
diff --git a/reclass/defaults.py b/reclass/defaults.py
index 557d511..24801ef 100644
--- a/reclass/defaults.py
+++ b/reclass/defaults.py
@@ -14,6 +14,7 @@
OPT_INVENTORY_BASE_URI = os.path.join('/etc', RECLASS_NAME)
OPT_NODES_URI = 'nodes'
OPT_CLASSES_URI = 'classes'
+OPT_EXPORTS_URI = 'exports'
OPT_PRETTY_PRINT = True
OPT_NO_REFS = False
OPT_OUTPUT = 'yaml'
@@ -37,3 +38,4 @@
MERGE_ALLOW_DICT_OVER_SCALAR = False
AUTOMATIC_RECLASS_PARAMETERS = True
+AUTOMATIC_EXPORT_PARAMETERS = True
diff --git a/reclass/storage/__init__.py b/reclass/storage/__init__.py
index 8ae2408..001fdce 100644
--- a/reclass/storage/__init__.py
+++ b/reclass/storage/__init__.py
@@ -22,6 +22,14 @@
msg = "Storage class '{0}' does not implement class entity retrieval."
raise NotImplementedError(msg.format(self.name))
+ def get_exports(self):
+ msg = "Storage class '{0}' does not implement get_exports."
+ raise NotImplementedError(msg.format(self.name))
+
+ def put_exports(self, new):
+ msg = "Storage class '{0}' does not implement put_exports."
+ raise NotImplementedError(msg.format(self.name))
+
def enumerate_nodes(self):
msg = "Storage class '{0}' does not implement node enumeration."
raise NotImplementedError(msg.format(self.name))
diff --git a/reclass/storage/memcache_proxy.py b/reclass/storage/memcache_proxy.py
index 7d9ab5e..545c68f 100644
--- a/reclass/storage/memcache_proxy.py
+++ b/reclass/storage/memcache_proxy.py
@@ -14,7 +14,7 @@
class MemcacheProxy(NodeStorageBase):
def __init__(self, real_storage, cache_classes=True, cache_nodes=True,
- cache_nodelist=True):
+ cache_nodelist=True, cache_exports=True):
name = '{0}({1})'.format(STORAGE_NAME, real_storage.name)
super(MemcacheProxy, self).__init__(name)
self._real_storage = real_storage
@@ -27,6 +27,9 @@
self._cache_nodelist = cache_nodelist
if cache_nodelist:
self._nodelist_cache = None
+ self._cache_exports = cache_exports
+ if cache_exports:
+ self._exports_cache = None
name = property(lambda self: self._real_storage.name)
@@ -41,6 +44,12 @@
return ret
+ @staticmethod
+ def _cache(cache, getter):
+ if cache is None:
+ cache = getter()
+ return cache
+
def get_node(self, name):
if not self._cache_nodes:
return self._real_storage.get_node(name)
@@ -55,6 +64,14 @@
return MemcacheProxy._cache_proxy(name, self._classes_cache,
self._real_storage.get_class)
+ def get_exports(self):
+ if not self._cache_exports:
+ return self._real_storage.get_exports()
+ return MemcacheProxy._cache(self._exports_cache, self._real_storage.get_exports)
+
+ def put_exports(self, new):
+ self._real_storage.put_exports(new)
+
def enumerate_nodes(self):
if not self._cache_nodelist:
return self._real_storage.enumerate_nodes()
diff --git a/reclass/storage/yaml_fs/__init__.py b/reclass/storage/yaml_fs/__init__.py
index 5a13050..50fdc2d 100644
--- a/reclass/storage/yaml_fs/__init__.py
+++ b/reclass/storage/yaml_fs/__init__.py
@@ -8,6 +8,8 @@
#
import os, sys
import fnmatch
+import yaml
+from reclass.output.yaml_outputter import ExplicitDumper
from reclass.storage import NodeStorageBase
from yamlfile import YamlFile
from directory import Directory
@@ -23,7 +25,7 @@
class ExternalNodeStorage(NodeStorageBase):
- def __init__(self, nodes_uri, classes_uri, default_environment=None):
+ def __init__(self, nodes_uri, classes_uri, exports_uri, default_environment=None):
super(ExternalNodeStorage, self).__init__(STORAGE_NAME)
def name_mangler(relpath, name):
@@ -47,11 +49,12 @@
return relpath, '.'.join(parts)
self._classes_uri = classes_uri
self._classes = self._enumerate_inventory(classes_uri, name_mangler)
-
+ self._exports_uri = exports_uri
self._default_environment = default_environment
nodes_uri = property(lambda self: self._nodes_uri)
classes_uri = property(lambda self: self._classes_uri)
+ exports_uri = property(lambda self: self._exports_uri)
def _enumerate_inventory(self, basedir, name_mangler):
ret = {}
@@ -96,5 +99,17 @@
entity = YamlFile(path).get_entity(name)
return entity
+ def get_exports(self):
+ vvv('GET EXPORTS')
+ path = os.path.join(self.exports_uri, 'exports{0}'.format(FILE_EXTENSION))
+ entity = YamlFile(path).get_data()
+ return entity
+
+ def put_exports(self, new):
+ vvv('PUT EXPORTS')
+ path = os.path.join(self.exports_uri, 'exports{0}'.format(FILE_EXTENSION))
+ with open(path, 'w') as yaml_file:
+ yaml.dump(new.as_dict(), yaml_file, default_flow_style=False, Dumper=ExplicitDumper)
+
def enumerate_nodes(self):
return self._nodes.keys()
diff --git a/reclass/storage/yaml_fs/yamlfile.py b/reclass/storage/yaml_fs/yamlfile.py
index 03a5c11..dcd6df4 100644
--- a/reclass/storage/yaml_fs/yamlfile.py
+++ b/reclass/storage/yaml_fs/yamlfile.py
@@ -31,6 +31,9 @@
self._data = data
fp.close()
+ def get_data(self):
+ return self._data
+
def get_entity(self, name=None, default_environment=None):
classes = self._data.get('classes')
if classes is None:
@@ -52,11 +55,11 @@
exports = {}
exports = datatypes.Parameters(exports)
- env = self._data.get('environment', default_environment)
-
if name is None:
name = self._path
+ env = self._data.get('environment', default_environment)
+
return datatypes.Entity(classes, applications, parameters, exports,
name=name, environment=env,
uri='yaml_fs://{0}'.format(self._path))
diff --git a/reclass/values/expitem.py b/reclass/values/expitem.py
index fddf0da..22a473b 100644
--- a/reclass/values/expitem.py
+++ b/reclass/values/expitem.py
@@ -4,9 +4,12 @@
# This file is part of reclass
#
+import copy
+
from item import Item
from reclass.utils.dictpath import DictPath
from reclass.errors import UndefinedVariableError
+from reclass.defaults import AUTOMATIC_EXPORT_PARAMETERS
class ExpItem(Item):
@@ -41,7 +44,10 @@
exp_path.drop_first()
for node, items in exports.iteritems():
if exp_path.exists_in(items):
- result.append(self._resolve(exp_path, exp_key, items))
+ value = copy.deepcopy(self._resolve(exp_path, exp_key, items))
+ if isinstance(value, dict) and AUTOMATIC_EXPORT_PARAMETERS:
+ value['_node_'] = node
+ result.append(value)
return result
def __repr__(self):
diff --git a/reclass/values/item.py b/reclass/values/item.py
index 035f3fb..9ee301e 100644
--- a/reclass/values/item.py
+++ b/reclass/values/item.py
@@ -23,3 +23,15 @@
def is_complex():
return (self.has_references | self.has_exports)
+
+ def contents(self):
+ msg = "Item class {0} does not implement contents()"
+ raise NotImplementedError(msg.format(self.__class__.__name__))
+
+ def merge_over(self, item, options):
+ msg = "Item class {0} does not implement merge_over()"
+ raise NotImplementedError(msg.format(self.__class__.__name__))
+
+ def render(self, context, exports):
+ msg = "Item class {0} does not implement render()"
+ raise NotImplementedError(msg.format(self.__class__.__name__))