Quick-n-dirty addition of node environments

Signed-off-by: martin f. krafft <madduck@madduck.net>
diff --git a/doc/source/operations.rst b/doc/source/operations.rst
index 98adf57..42e8a86 100644
--- a/doc/source/operations.rst
+++ b/doc/source/operations.rst
@@ -38,6 +38,8 @@
                 parameters:
                   ssh.server:
                     permit_root_login: no
+environment  only relevant for nodes, this allows to specify an "environment"
+             into which the node definition is supposed to be place.
 ============ ================================================================
 
 Nodes may be defined in subdirectories. However, node names (filename) must be
diff --git a/doc/source/salt.rst b/doc/source/salt.rst
index 25bd53f..f0cc733 100644
--- a/doc/source/salt.rst
+++ b/doc/source/salt.rst
@@ -163,6 +163,7 @@
 classes           (none) [#nodegroups]_
 applications      states
 parameters        pillar
+environment       environment
 ================= ================
 
 .. [#nodegroups] See `Salt issue #5787`_ for steps into the direction of letting
diff --git a/doc/source/todo.rst b/doc/source/todo.rst
index 8f87c26..38df768 100644
--- a/doc/source/todo.rst
+++ b/doc/source/todo.rst
@@ -41,16 +41,6 @@
 Ideally, |reclass| could unify the interface so that even templates can be
 shared between the various CMS.
 
-Node environments
------------------
-At least Salt and Puppet support the notion of "environments", but the Salt
-adapter just puts everything into the "base" environment at the moment.
-
-Part of the reason that multiple environments aren't (yet) supported is
-because I don't see the use-case (anymore) with |reclass|. If you still see
-a use-case, then please help me understand it and let's figure out a good way
-to introduce this concept into |reclass|.
-
 Membership information
 ----------------------
 It would be nice if |reclass| could provide e.g. the Nagios master node with
diff --git a/reclass/__init__.py b/reclass/__init__.py
index 5c68e12..7cd6c30 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):
+def get_storage(storage_type, nodes_uri, classes_uri, **kwargs):
     storage_class = StorageBackendLoader(storage_type).load()
-    return MemcacheProxy(storage_class(nodes_uri, classes_uri))
+    return MemcacheProxy(storage_class(nodes_uri, classes_uri, **kwargs))
 
 
 def output(data, fmt, pretty_print=False):
diff --git a/reclass/adapters/salt.py b/reclass/adapters/salt.py
index eec2ac3..6483268 100755
--- a/reclass/adapters/salt.py
+++ b/reclass/adapters/salt.py
@@ -27,7 +27,8 @@
 
     nodes_uri, classes_uri = path_mangler(inventory_base_uri,
                                           nodes_uri, classes_uri)
-    storage = get_storage(storage_type, nodes_uri, classes_uri)
+    storage = get_storage(storage_type, nodes_uri, classes_uri,
+                          default_environment='base')
     reclass = Core(storage, class_mappings)
 
     data = reclass.nodeinfo(minion_id)
@@ -35,6 +36,7 @@
     params['__reclass__'] = {}
     params['__reclass__']['applications'] = data['applications']
     params['__reclass__']['classes'] = data['classes']
+    params['__reclass__']['environment'] = data['environment']
     return params
 
 
@@ -43,11 +45,10 @@
         classes_uri=OPT_CLASSES_URI,
         class_mappings=None):
 
-    env = 'base'
-    # TODO: node environments
     nodes_uri, classes_uri = path_mangler(inventory_base_uri,
                                           nodes_uri, classes_uri)
-    storage = get_storage(storage_type, nodes_uri, classes_uri)
+    storage = get_storage(storage_type, nodes_uri, classes_uri,
+                          default_environment='base')
     reclass = Core(storage, class_mappings)
 
     # if the minion_id is not None, then return just the applications for the
@@ -56,15 +57,19 @@
     if minion_id is not None:
         data = reclass.nodeinfo(minion_id)
         applications = data.get('applications', [])
+        env = data['environment']
         return {env: applications}
 
     else:
         data = reclass.inventory()
         nodes = {}
         for node_id, node_data in data['nodes'].iteritems():
-            nodes[node_id] = node_data['applications']
+            env = node_data['environment']
+            if env not in nodes:
+                nodes[env] = {}
+            nodes[env][node_id] = node_data['applications']
 
-        return {env: nodes}
+        return nodes
 
 
 def cli():
diff --git a/reclass/cli.py b/reclass/cli.py
index a07404a..5666e16 100644
--- a/reclass/cli.py
+++ b/reclass/cli.py
@@ -27,7 +27,7 @@
                               defaults=defaults)
 
         storage = get_storage(options.storage_type, options.nodes_uri,
-                              options.classes_uri)
+                              options.classes_uri, default_environment='base')
         class_mappings = defaults.get('class_mappings')
         reclass = Core(storage, class_mappings)
 
diff --git a/reclass/core.py b/reclass/core.py
index 07b2c3a..f65fb82 100644
--- a/reclass/core.py
+++ b/reclass/core.py
@@ -116,6 +116,7 @@
     def _nodeinfo_as_dict(self, nodename, entity):
         ret = {'__reclass__' : {'node': entity.name, 'name': nodename,
                                 'uri': entity.uri,
+                                'environment': entity.environment,
                                 'timestamp': Core._get_timestamp()
                                },
               }
diff --git a/reclass/datatypes/entity.py b/reclass/datatypes/entity.py
index 684705b..573a28c 100644
--- a/reclass/datatypes/entity.py
+++ b/reclass/datatypes/entity.py
@@ -17,7 +17,7 @@
     uri of the Entity that is being merged.
     '''
     def __init__(self, classes=None, applications=None, parameters=None,
-                 uri=None, name=None):
+                 uri=None, name=None, environment=None):
         if classes is None: classes = Classes()
         self._set_classes(classes)
         if applications is None: applications = Applications()
@@ -26,9 +26,11 @@
         self._set_parameters(parameters)
         self._uri = uri or ''
         self._name = name or ''
+        self._environment = environment or ''
 
     name = property(lambda s: s._name)
     uri = property(lambda s: s._uri)
+    environment = property(lambda s: s._environment)
     classes = property(lambda s: s._classes)
     applications = property(lambda s: s._applications)
     parameters = property(lambda s: s._parameters)
@@ -57,6 +59,7 @@
         self._parameters.merge(other._parameters)
         self._name = other.name
         self._uri = other.uri
+        self._environment = other.environment
 
     def interpolate(self):
         self._parameters.interpolate()
@@ -83,5 +86,6 @@
     def as_dict(self):
         return {'classes': self._classes.as_list(),
                 'applications': self._applications.as_list(),
-                'parameters': self._parameters.as_dict()
+                'parameters': self._parameters.as_dict(),
+                'environment': self._environment
                }
diff --git a/reclass/datatypes/tests/test_entity.py b/reclass/datatypes/tests/test_entity.py
index 0fd5687..17ec9e8 100644
--- a/reclass/datatypes/tests/test_entity.py
+++ b/reclass/datatypes/tests/test_entity.py
@@ -54,6 +54,11 @@
         e = Entity(*self._make_instances(**types), uri=uri)
         self.assertEqual(e.uri, uri)
 
+    def test_constructor_empty_env(self, **types):
+        env = 'not base'
+        e = Entity(*self._make_instances(**types), environment=env)
+        self.assertEqual(e.environment, env)
+
     def test_equal_empty(self, **types):
         instances = self._make_instances(**types)
         self.assertEqual(Entity(*instances), Entity(*instances))
@@ -127,13 +132,22 @@
         e1.merge(e2)
         self.assertEqual(e1.uri, newuri)
 
+    def test_merge_newenv(self, **types):
+        instances = self._make_instances(**types)
+        newenv = 'new env'
+        e1 = Entity(*instances, environment='env')
+        e2 = Entity(*instances, environment=newenv)
+        e1.merge(e2)
+        self.assertEqual(e1.environment, newenv)
+
     def test_as_dict(self, **types):
         instances = self._make_instances(**types)
-        entity = Entity(*instances, name='test')
+        entity = Entity(*instances, name='test', environment='test')
         comp = {}
         comp['classes'] = instances[0].as_list()
         comp['applications'] = instances[1].as_list()
         comp['parameters'] = instances[2].as_dict()
+        comp['environment'] = 'test'
         d = entity.as_dict()
         self.assertDictEqual(d, comp)
 
diff --git a/reclass/storage/yaml_fs/__init__.py b/reclass/storage/yaml_fs/__init__.py
index c163f15..6c917e0 100644
--- a/reclass/storage/yaml_fs/__init__.py
+++ b/reclass/storage/yaml_fs/__init__.py
@@ -23,7 +23,7 @@
 
 class ExternalNodeStorage(NodeStorageBase):
 
-    def __init__(self, nodes_uri, classes_uri):
+    def __init__(self, nodes_uri, classes_uri, default_environment=None):
         super(ExternalNodeStorage, self).__init__(STORAGE_NAME)
 
         def _handle_node_duplicates(name, uri1, uri2):
@@ -35,6 +35,8 @@
         self._classes_uri = classes_uri
         self._classes = self._enumerate_inventory(classes_uri)
 
+        self._default_environment = default_environment
+
     nodes_uri = property(lambda self: self._nodes_uri)
     classes_uri = property(lambda self: self._classes_uri)
 
@@ -62,7 +64,7 @@
             name = os.path.splitext(relpath)[0]
         except KeyError, e:
             raise reclass.errors.NodeNotFound(self.name, name, self.nodes_uri)
-        entity = YamlFile(path).get_entity(name)
+        entity = YamlFile(path).get_entity(name, self._default_environment)
         return entity
 
     def get_class(self, name, nodename=None):
@@ -71,7 +73,7 @@
             path = os.path.join(self.classes_uri, self._classes[name])
         except KeyError, e:
             raise reclass.errors.ClassNotFound(self.name, name, self.classes_uri)
-        entity = YamlFile(path).get_entity()
+        entity = YamlFile(path).get_entity(name)
         return entity
 
     def enumerate_nodes(self):
diff --git a/reclass/storage/yaml_fs/yamlfile.py b/reclass/storage/yaml_fs/yamlfile.py
index 2178e87..717a911 100644
--- a/reclass/storage/yaml_fs/yamlfile.py
+++ b/reclass/storage/yaml_fs/yamlfile.py
@@ -31,7 +31,7 @@
             self._data = data
         fp.close()
 
-    def get_entity(self, name=None):
+    def get_entity(self, name=None, default_environment=None):
         classes = self._data.get('classes')
         if classes is None:
             classes = []
@@ -47,11 +47,13 @@
             parameters = {}
         parameters = datatypes.Parameters(parameters)
 
+        env = self._data.get('environment', default_environment)
+
         if name is None:
             name = self._path
 
         return datatypes.Entity(classes, applications, parameters,
-                                name=name,
+                                name=name, environment=env,
                                 uri='yaml_fs://{0}'.format(self._path))
 
     def __repr__(self):