Import of working code base
Signed-off-by: martin f. krafft <madduck@madduck.net>
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..169396b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+*.pyc
+/reclass-config.yml
diff --git a/adapters/ansible b/adapters/ansible
new file mode 100755
index 0000000..bb35e3f
--- /dev/null
+++ b/adapters/ansible
@@ -0,0 +1,81 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# ansible-adapter — adapter between Ansible and reclass
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+import os, sys, posix, stat
+
+def usage_error(msg):
+ print >>sys.stderr, msg
+ sys.exit(posix.EX_USAGE)
+
+if len(sys.argv) == 1:
+ usage_error('Need to specify --list or --host.')
+
+ansible_dir = os.path.dirname(sys.argv[0])
+
+# In order to be able to use reclass as modules, manipulate the search path,
+# starting from the location of the adapter. Realpath will make sure that
+# symlinks are resolved.
+realpath = os.path.realpath(sys.argv[0] + '/../../')
+sys.path.insert(0, realpath)
+import reclass, config
+
+# The adapter resides in the Ansible directory, so let's look there for an
+# optional configuration file called reclass-config.yml.
+options = {'output':'json', 'pretty_print':True}
+config_path = os.path.join(ansible_dir, 'reclass-config.yml')
+if os.path.exists(config_path) and os.access(config_path, os.R_OK):
+ options.update(config.read_config_file(config_path))
+
+# Massage options into shape
+if 'storage_type' not in options:
+ options['storage_type'] = 'yaml_fs'
+
+if 'nodes_uri' not in options:
+ nodes_uri = os.path.join(ansible_dir, 'nodes')
+ if stat.S_ISDIR(os.stat(nodes_uri).st_mode):
+ options['nodes_uri'] = nodes_uri
+ else:
+ usage_error('nodes_uri not specified')
+
+if 'classes_uri' not in options:
+ classes_uri = os.path.join(ansible_dir, 'classes')
+ if not stat.S_ISDIR(os.stat(classes_uri).st_mode):
+ classes_uri = options['nodes_uri']
+ options['classes_uri'] = classes_uri
+
+# Invoke reclass according to what Ansible wants.
+# If the 'node' option is set, we want node information. If the option is
+# False instead, we print the inventory. Yeah for option abuse!
+if sys.argv[1] == '--list':
+ if len(sys.argv) > 2:
+ usage_error('Unknown arguments: ' + ' '.join(sys.argv[2:]))
+ options['node'] = False
+
+elif sys.argv[1] == '--host':
+ if len(sys.argv) < 3:
+ usage_error('Missing hostname.')
+ elif len(sys.argv) > 3:
+ usage_error('Unknown arguments: ' + ' '.join(sys.argv[3:]))
+ options['node'] = sys.argv[2]
+
+else:
+ usage_error('Unknown mode (--list or --host required).')
+
+data = reclass.get_data(options['storage_type'], options['nodes_uri'],
+ options['classes_uri'], options['node'])
+
+if options['node']:
+ # Massage and shift the data like Ansible wants it
+ data['parameters']['RECLASS'] = data['RECLASS']
+ for i in ('classes', 'applications'):
+ data['parameters']['RECLASS'][i] = data[i]
+ data = data['parameters']
+
+print reclass.output(data, options['output'], options['pretty_print'])
+
+sys.exit(posix.EX_OK)
diff --git a/config.py b/config.py
new file mode 100644
index 0000000..58c29c5
--- /dev/null
+++ b/config.py
@@ -0,0 +1,81 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+import yaml, os, optparse, posix, sys
+
+def _make_parser(name, version, description, defaults={}):
+ parser = optparse.OptionParser(version=version)
+ parser.prog = name
+ parser.version = version
+ parser.description = description
+ parser.usage = '%prog [options] ( --inventory | --nodeinfo <nodename> )'
+
+ options_group = optparse.OptionGroup(parser, 'Options',
+ 'Configure the way {0} works'.format(name))
+ options_group.add_option('-t', '--storage-type', dest='storage_type',
+ default=defaults.get('storage_type', 'yaml_fs'),
+ help='the type of storage backend to use [%default]')
+ options_group.add_option('-u', '--nodes-uri', dest='nodes_uri',
+ default=defaults.get('nodes_uri', None),
+ help='the URI to the nodes storage [%default]'),
+ options_group.add_option('-c', '--classes-uri', dest='classes_uri',
+ default=defaults.get('classes_uri', None),
+ help='the URI to the classes storage [%default]')
+ options_group.add_option('-o', '--output', dest='output',
+ default=defaults.get('output', 'yaml'),
+ help='output format (yaml or json) [%default]')
+ options_group.add_option('-p', '--pretty-print', dest='pretty_print',
+ default=defaults.get('pretty_print', False),
+ action="store_true",
+ help='try to make the output prettier [%default]')
+ parser.add_option_group(options_group)
+
+ run_modes = optparse.OptionGroup(parser, 'Modes',
+ 'Specify one of these to determine what to do.')
+ run_modes.add_option('-i', '--inventory', action='store_false', dest='node',
+ help='output the entire inventory')
+ run_modes.add_option('-n', '--nodeinfo', action='store', dest='node',
+ default=None,
+ help='output information for a specific node')
+ parser.add_option_group(run_modes)
+
+ return parser
+
+def _parse_and_check_options(parser):
+ options, args = parser.parse_args()
+
+ def usage_error(msg):
+ sys.stderr.write(msg + '\n\n')
+ parser.print_help(sys.stderr)
+ sys.exit(posix.EX_USAGE)
+
+ if len(args) > 0:
+ usage_error('No arguments allowed')
+ elif options.node is None:
+ usage_error('You need to either pass --inventory or --nodeinfo <nodename>')
+ elif options.output not in ('json', 'yaml'):
+ usage_error('Unknown output format: {0}'.format(options.output))
+ elif options.nodes_uri is None:
+ usage_error('Must specify at least --nodes-uri')
+
+ options.classes_uri = options.classes_uri or options.nodes_uri
+
+ return options
+
+def read_config_file(path):
+ if os.path.exists(path):
+ return yaml.safe_load(file(path))
+ else:
+ return {}
+
+def get_options(name, version, description, config_file=None):
+ config_data = {}
+ if config_file is not None:
+ config_data.update(read_config_file(config_file))
+ parser = _make_parser(name, version, description, config_data)
+ return _parse_and_check_options(parser)
diff --git a/datatypes/__init__.py b/datatypes/__init__.py
new file mode 100644
index 0000000..919ea86
--- /dev/null
+++ b/datatypes/__init__.py
@@ -0,0 +1,12 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+from applications import Applications
+from classes import Classes
+from entity import Entity
+from parameters import Parameters
diff --git a/datatypes/applications.py b/datatypes/applications.py
new file mode 100644
index 0000000..0a8f402
--- /dev/null
+++ b/datatypes/applications.py
@@ -0,0 +1,40 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+from mergers.list import SetExtend
+
+class ApplicationsMerger(SetExtend):
+
+ def __init__(self, negater='~'):
+ self._negater=negater
+
+ def _combine(self, first, second):
+ for i in second:
+ remove = False
+ if i.startswith('~'):
+ i = i[1:]
+ remove = True
+ if i not in first:
+ if not remove:
+ first.append(i)
+ elif remove:
+ first.remove(i)
+ return first
+
+
+class Applications(list):
+
+ def __init__(self, *args, **kwargs):
+ super(Applications, self).__init__(*args, **kwargs)
+
+ def merge(self, other, negater='~'):
+ merger = ApplicationsMerger(negater)
+ self[:] = merger.merge(self, other)
+
+ def __repr__(self):
+ return '<Applications {0}>'.format(super(Applications, self).__repr__())
diff --git a/datatypes/classes.py b/datatypes/classes.py
new file mode 100644
index 0000000..facaf93
--- /dev/null
+++ b/datatypes/classes.py
@@ -0,0 +1,21 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+from mergers.list import SetExtend
+
+class Classes(list):
+
+ def __init__(self, *args, **kwargs):
+ super(Classes, self).__init__(*args, **kwargs)
+
+ def merge(self, other):
+ merger = SetExtend()
+ self[:] = merger.merge(self, other)
+
+ def __repr__(self):
+ return '<Classes {0}>'.format(super(Classes, self).__repr__())
diff --git a/datatypes/entity.py b/datatypes/entity.py
new file mode 100644
index 0000000..e493c8b
--- /dev/null
+++ b/datatypes/entity.py
@@ -0,0 +1,32 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+from classes import Classes
+from parameters import Parameters
+from applications import Applications
+
+class Entity(object):
+
+ def __init__(self, classes=Classes(), parameters=Parameters(),
+ applications=Applications()):
+ self._applications = applications
+ self._classes = classes
+ self._parameters = parameters
+
+ applications = property(lambda self: self._applications)
+ classes = property(lambda self: self._classes)
+ parameters = property(lambda self: self._parameters)
+
+ def merge(self, other):
+ self.applications.merge(other.applications)
+ self.classes.merge(other.classes)
+ self.parameters.merge(other.parameters)
+
+ def __repr__(self):
+ return '<Entity classes:{0} parameters:{1} applications:{2}>'.format(
+ len(self.classes), len(self.parameters), len(self.applications))
diff --git a/datatypes/parameters.py b/datatypes/parameters.py
new file mode 100644
index 0000000..d5c25d6
--- /dev/null
+++ b/datatypes/parameters.py
@@ -0,0 +1,20 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+from mergers.dict import DictRecursivePolicyUpdate
+
+class Parameters(dict):
+
+ def __init__(self, *args, **kwargs):
+ super(Parameters, self).__init__(*args, **kwargs)
+
+ def merge(self, other, merger=DictRecursivePolicyUpdate()):
+ self.update(merger.merge(self, other))
+
+ def __repr__(self):
+ return '<Parameters {0}>'.format(super(Parameters, self).__repr__())
diff --git a/datatypes/tests/test_applications.py b/datatypes/tests/test_applications.py
new file mode 100644
index 0000000..b18c07e
--- /dev/null
+++ b/datatypes/tests/test_applications.py
@@ -0,0 +1,57 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+from datatypes import Applications
+
+class TestApplications:
+
+ def test_constructor0(self):
+ c = Applications()
+ assert len(c) == 0
+
+ def test_constructor1(self):
+ DATA = ['one', 'two', 'three', 'four']
+ c = Applications(DATA)
+ assert len(c) == len(DATA)
+ for i in range(0, len(c)):
+ assert DATA[i] == c[i]
+
+ def test_merge(self):
+ DATA0 = ['one', 'two', 'three', 'four']
+ DATA1 = ['one', 'three', 'five', 'seven']
+ c = Applications(DATA0)
+ c.merge(DATA1)
+ assert len(c) == 6
+ assert c[:4] == DATA0
+ assert c[4] == DATA1[2]
+ assert c[5] == DATA1[3]
+
+ def test_merge_negate(self):
+ negater = '~'
+ DATA0 = ['red', 'green', 'blue']
+ DATA1 = [negater + 'red', 'yellow', 'black']
+ c = Applications(DATA0)
+ c.merge(DATA1, negater)
+ assert len(c) == len(DATA0)-1 + len(DATA1)-1
+ assert c[0] == DATA0[1]
+ assert c[1] == DATA0[2]
+ assert c[2] == DATA1[1]
+ assert c[3] == DATA1[2]
+
+ def test_merge_negate_default(self):
+ c = Applications(['red'])
+ c.merge(['~red'])
+ assert len(c) == 0
+
+ def test_merge_negate_nonexistent(self):
+ c = Applications(['blue'])
+ c.merge(['~red'])
+ assert len(c) == 1
+ assert 'red' not in c
+ assert '~red' not in c
+ assert 'blue' in c
diff --git a/datatypes/tests/test_classes.py b/datatypes/tests/test_classes.py
new file mode 100644
index 0000000..deca77b
--- /dev/null
+++ b/datatypes/tests/test_classes.py
@@ -0,0 +1,32 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+from datatypes import Classes
+
+class TestClasses:
+
+ def test_constructor0(self):
+ c = Classes()
+ assert len(c) == 0
+
+ def test_constructor1(self):
+ DATA = ['one', 'two', 'three', 'four']
+ c = Classes(DATA)
+ assert len(c) == len(DATA)
+ for i in range(0, len(c)):
+ assert DATA[i] == c[i]
+
+ def test_merge(self):
+ DATA0 = ['one', 'two', 'three', 'four']
+ DATA1 = ['one', 'three', 'five', 'seven']
+ c = Classes(DATA0)
+ c.merge(DATA1)
+ assert len(c) == 6
+ assert c[:4] == DATA0
+ assert c[4] == DATA1[2]
+ assert c[5] == DATA1[3]
diff --git a/datatypes/tests/test_entity.py b/datatypes/tests/test_entity.py
new file mode 100644
index 0000000..1b1ac7f
--- /dev/null
+++ b/datatypes/tests/test_entity.py
@@ -0,0 +1,29 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+from datatypes import Entity, Classes, Parameters, Applications
+
+class TestEntity:
+
+ def test_constructor0(self):
+ e = Entity()
+ assert isinstance(e.classes, Classes)
+ assert len(e.classes) == 0
+ assert isinstance(e.parameters, Parameters)
+ assert len(e.parameters) == 0
+ assert isinstance(e.applications, Applications)
+ assert len(e.applications) == 0
+
+ def test_constructor1(self):
+ c = Classes(['one', 'two'])
+ p = Parameters({'blue':'white', 'black':'yellow'})
+ a = Applications(['three', 'four'])
+ e = Entity(c, p, a)
+ assert e.classes == c
+ assert e.parameters == p
+ assert e.applications == a
diff --git a/datatypes/tests/test_parameters.py b/datatypes/tests/test_parameters.py
new file mode 100644
index 0000000..e6efbb8
--- /dev/null
+++ b/datatypes/tests/test_parameters.py
@@ -0,0 +1,22 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+from datatypes import Parameters
+
+class TestParameters:
+
+ def test_constructor0(self):
+ c = Parameters()
+ assert len(c) == 0
+
+ def test_constructor1(self):
+ DATA = {'blue':'white', 'black':'yellow'}
+ c = Parameters(DATA)
+ assert len(c) == len(DATA)
+ for i in c.iterkeys():
+ assert DATA[i] == c[i]
diff --git a/mergers/__init__.py b/mergers/__init__.py
new file mode 100644
index 0000000..ada8bd8
--- /dev/null
+++ b/mergers/__init__.py
@@ -0,0 +1,9 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+
diff --git a/mergers/base.py b/mergers/base.py
new file mode 100644
index 0000000..b9a23dc
--- /dev/null
+++ b/mergers/base.py
@@ -0,0 +1,12 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+class BaseMerger(object):
+
+ def merge(self, first, second):
+ raise NotImplementedError
diff --git a/mergers/dict.py b/mergers/dict.py
new file mode 100644
index 0000000..77f7239
--- /dev/null
+++ b/mergers/dict.py
@@ -0,0 +1,10 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+from update import Update
+from recursive_extend import RecursiveExtend
diff --git a/mergers/dict/__init__.py b/mergers/dict/__init__.py
new file mode 100644
index 0000000..8346017
--- /dev/null
+++ b/mergers/dict/__init__.py
@@ -0,0 +1,11 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+from update import DictUpdate
+from recursive_update import DictRecursiveUpdate
+from recursive_policy_update import DictRecursivePolicyUpdate
diff --git a/mergers/dict/base.py b/mergers/dict/base.py
new file mode 100644
index 0000000..92fdd1b
--- /dev/null
+++ b/mergers/dict/base.py
@@ -0,0 +1,12 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+from mergers.base import BaseMerger
+
+class BaseDictMerger(BaseMerger):
+ pass
diff --git a/mergers/dict/recursive_policy_update.py b/mergers/dict/recursive_policy_update.py
new file mode 100644
index 0000000..f3bcb3e
--- /dev/null
+++ b/mergers/dict/recursive_policy_update.py
@@ -0,0 +1,32 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+from base import BaseDictMerger
+
+class DictRecursivePolicyUpdate(BaseDictMerger):
+
+ def __init__(self, policy=None):
+ super(DictRecursivePolicyUpdate, self).__init__()
+ if policy is None:
+ policy = {(dict,dict) : self.merge,
+ (list,list) : lambda x,y: x+y,
+ (dict,list) : lambda x,y: self.merge(x, dict(y)),
+ None : lambda x,y: y
+ }
+ self._policy = policy
+
+ def merge(self, first, second):
+ ret = first.copy()
+ for k,v in second.iteritems():
+ if k in ret:
+ pfn = self._policy.get((type(ret[k]), type(v)),
+ self._policy.get(None))
+ ret[k] = pfn(ret[k], v)
+ else:
+ ret[k] = v
+ return ret
diff --git a/mergers/dict/recursive_update.py b/mergers/dict/recursive_update.py
new file mode 100644
index 0000000..ba94c1f
--- /dev/null
+++ b/mergers/dict/recursive_update.py
@@ -0,0 +1,25 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+from base import BaseDictMerger
+
+class DictRecursiveUpdate(BaseDictMerger):
+
+ def merge(self, first, second):
+ ret = first.copy()
+ for k,v in second.iteritems():
+ if k in ret:
+ if isinstance(ret[k], dict):
+ if isinstance(v, (list, tuple)):
+ v = dict(v)
+ ret[k] = self.merge(ret[k], v)
+ else:
+ ret[k] = v
+ else:
+ ret[k] = v
+ return ret
diff --git a/mergers/dict/tests/test_recursive_policy_update.py b/mergers/dict/tests/test_recursive_policy_update.py
new file mode 100644
index 0000000..f44ada6
--- /dev/null
+++ b/mergers/dict/tests/test_recursive_policy_update.py
@@ -0,0 +1,26 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+from test_recursive_update import TestDictRecursiveUpdate
+from mergers.dict import DictRecursivePolicyUpdate
+
+class TestDictRecursivePolicyUpdate(TestDictRecursiveUpdate):
+
+ def setUp(self):
+ self.merger = DictRecursivePolicyUpdate()
+
+ def test_nested_lists_extend(self):
+ first = {'one': [1,2],
+ 'two': {'one': [1,2]}}
+ second = {'one': [3,4],
+ 'two': {'one': [3,4]}}
+ ret = self.merger.merge(first, second)
+ assert len(ret['one']) == 4
+ assert ret['one'][2] == 3
+ assert len(ret['two']['one']) == 4
+ assert ret['two']['one'][3] == 4
diff --git a/mergers/dict/tests/test_recursive_update.py b/mergers/dict/tests/test_recursive_update.py
new file mode 100644
index 0000000..c1e8501
--- /dev/null
+++ b/mergers/dict/tests/test_recursive_update.py
@@ -0,0 +1,45 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+from test_update import TestDictUpdate
+from mergers.dict import DictRecursiveUpdate
+
+class TestDictRecursiveUpdate(TestDictUpdate):
+
+ def setUp(self):
+ self.merger = DictRecursiveUpdate()
+
+ def test_simple_recursive_dict_update(self):
+ first = {'one':{1:1,2:3,3:2}}
+ second = {'one':{2:2,3:3,4:4}}
+ ret = self.merger.merge(first, second)
+ assert len(ret) == 1
+ for k,v in ret['one'].iteritems():
+ assert k == v
+
+ def test_complex_recursive_dict_update(self):
+ first = {'one': 1,
+ 'two': {'a':92,'b':94},
+ 'three': {'third':0.33,'two thirds':0.67},
+ 'four': {1:{1:1},2:{2:2},3:{3:4}}
+ }
+ second = {'five': 5,
+ 'one': 1,
+ 'two': {'b':93,'c':94},
+ 'four': {4:{4:4}, 3:{3:3}},
+ }
+ ret = self.merger.merge(first, second)
+ assert ret['one'] == 1
+ assert len(ret['two']) == 3
+ assert ret['two']['b'] == 93
+ assert len(ret['three']) == 2
+ assert len(ret['four']) == 4
+ for i in range(1,4):
+ assert len(ret['four'][i]) == 1
+ for k,v in ret['four'][i].iteritems():
+ assert k == v
diff --git a/mergers/dict/tests/test_update.py b/mergers/dict/tests/test_update.py
new file mode 100644
index 0000000..8e35c53
--- /dev/null
+++ b/mergers/dict/tests/test_update.py
@@ -0,0 +1,22 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+from mergers.dict import DictUpdate
+
+class TestDictUpdate:
+
+ def setUp(self):
+ self.merger = DictUpdate()
+
+ def test_dict_update(self):
+ first = {1:1,2:3,3:2}
+ second = {2:2,3:3,4:4}
+ ret = self.merger.merge(first, second)
+ assert len(ret) == 4
+ for k,v in ret.iteritems():
+ assert k == v
diff --git a/mergers/dict/update.py b/mergers/dict/update.py
new file mode 100644
index 0000000..dd834f0
--- /dev/null
+++ b/mergers/dict/update.py
@@ -0,0 +1,16 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+from base import BaseDictMerger
+
+class DictUpdate(BaseDictMerger):
+
+ def merge(self, first, second):
+ ret = first.copy()
+ ret.update(second)
+ return ret
diff --git a/mergers/list.py b/mergers/list.py
new file mode 100644
index 0000000..5885147
--- /dev/null
+++ b/mergers/list.py
@@ -0,0 +1,9 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+from list import Extend
diff --git a/mergers/list/__init__.py b/mergers/list/__init__.py
new file mode 100644
index 0000000..51ee818
--- /dev/null
+++ b/mergers/list/__init__.py
@@ -0,0 +1,10 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+from extend import ListExtend
+from set import SetExtend
diff --git a/mergers/list/base.py b/mergers/list/base.py
new file mode 100644
index 0000000..a7bbc17
--- /dev/null
+++ b/mergers/list/base.py
@@ -0,0 +1,16 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+from mergers.base import BaseMerger
+
+class BaseListMerger(BaseMerger):
+
+ def merge(self, first, second):
+ first = [first] if not isinstance(first, list) else first[:]
+ second = [second] if not isinstance(second, list) else second[:]
+ return self._combine(first, second)
diff --git a/mergers/list/extend.py b/mergers/list/extend.py
new file mode 100644
index 0000000..f4acc35
--- /dev/null
+++ b/mergers/list/extend.py
@@ -0,0 +1,18 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+from base import BaseListMerger
+
+class ListExtend(BaseListMerger):
+
+ def _combine(self, first, second):
+ if isinstance(second, list):
+ first.extend(second)
+ else:
+ first.append(second)
+ return first
diff --git a/mergers/list/set.py b/mergers/list/set.py
new file mode 100644
index 0000000..ddb0eb4
--- /dev/null
+++ b/mergers/list/set.py
@@ -0,0 +1,17 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+from extend import BaseListMerger
+
+class SetExtend(BaseListMerger):
+
+ def _combine(self, first, second):
+ for i in second:
+ if i not in first:
+ first.append(i)
+ return first
diff --git a/mergers/list/tests/test_extend.py b/mergers/list/tests/test_extend.py
new file mode 100644
index 0000000..bdc40ca
--- /dev/null
+++ b/mergers/list/tests/test_extend.py
@@ -0,0 +1,66 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+from mergers.list import ListExtend
+
+class TestListExtend:
+
+ def setUp(self):
+ self.merger = ListExtend()
+
+ def _test_merge(self, one, two, target):
+ res = self.merger.merge(one, two)
+ print res, target
+ return res == target
+
+ def test_merge_scalars(self):
+ assert self._test_merge(1, 2, [1,2])
+
+ def test_merge_tuples(self):
+ t1 = (1,2,3)
+ t2 = (6,5,4)
+ target = [t1, t2]
+ assert self._test_merge(t1, t2, target)
+
+ def test_merge_lists(self):
+ l1 = [1,2,3]
+ l2 = [6,5,4]
+ target = l1 + l2
+ assert self._test_merge(l1, l2, target)
+
+ def test_merge_scalar_tuple(self):
+ s = 'one'
+ t = (2,3)
+ target = [s, t]
+ assert self._test_merge(s, t, target)
+
+ def test_merge_scalar_list(self):
+ s = 'foo'
+ l = [1,2,3]
+ target = [s]
+ target.extend(l)
+ assert self._test_merge(s, l, target)
+
+ def test_merge_list_scalar(self):
+ l = [1,2,3]
+ s = 'bar'
+ target = l[:]
+ target.append(s)
+ assert self._test_merge(l, s, target)
+
+ def test_merge_duplicates_scalar(self):
+ s1 = 2
+ s2 = 2
+ target = [2,2]
+ assert self._test_merge(s1, s2, target)
+
+ def test_merge_duplicates_list(self):
+ l1 = [1,2,3]
+ l2 = [3,2,1]
+ target = l1 + l2
+ assert self._test_merge(l1, l2, target)
diff --git a/mergers/list/tests/test_set.py b/mergers/list/tests/test_set.py
new file mode 100644
index 0000000..d3e8183
--- /dev/null
+++ b/mergers/list/tests/test_set.py
@@ -0,0 +1,27 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+from test_extend import TestListExtend
+from mergers.list import SetExtend
+
+class TestSetExtend(TestListExtend):
+
+ def setUp(self):
+ self.merger = SetExtend()
+
+ def test_merge_duplicates_scalar(self):
+ s1 = 2
+ s2 = 2
+ target = [2]
+ assert self._test_merge(s1, s2, target)
+
+ def test_merge_duplicates_list(self):
+ l1 = [1,2,3]
+ l2 = [3,2,1]
+ target = l1
+ assert self._test_merge(l1, l2, target)
diff --git a/mergers/recursive_extend.py b/mergers/recursive_extend.py
new file mode 100644
index 0000000..f8487af
--- /dev/null
+++ b/mergers/recursive_extend.py
@@ -0,0 +1,14 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+from base import BaseMerger
+
+class RecursiveExtend(BaseMerger):
+
+ def merge(self, first, second):
+ for
diff --git a/output/__init__.py b/output/__init__.py
new file mode 100644
index 0000000..75f6f4a
--- /dev/null
+++ b/output/__init__.py
@@ -0,0 +1,33 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+class OutputterBase(object):
+
+ def __init__(self):
+ pass
+
+ def dump(self, data, pretty_print=False):
+ raise NotImplementedError, "dump() method not yet implemented"
+
+
+class OutputLoader(object):
+
+ def __init__(self, outputter):
+ self._name = outputter + '_outputter'
+ try:
+ self._module = __import__(self._name, globals(), locals(),
+ self._name)
+ except ImportError:
+ raise NotImplementedError
+
+ def load(self, attr='Outputter'):
+ klass = getattr(self._module, attr, None)
+ if klass is None:
+ raise AttributeError, \
+ 'Outputter class {0} does not export "{1}"'.format(self._name, klass)
+ return klass
diff --git a/output/json_outputter.py b/output/json_outputter.py
new file mode 100644
index 0000000..25fa8bc
--- /dev/null
+++ b/output/json_outputter.py
@@ -0,0 +1,17 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+from output import OutputterBase
+import json
+
+class Outputter(OutputterBase):
+
+ def dump(self, data, pretty_print=False):
+ separators = (',', ': ') if pretty_print else (',', ':')
+ indent = 2 if pretty_print else None
+ return json.dumps(data, indent=indent, separators=separators)
diff --git a/output/yaml_outputter.py b/output/yaml_outputter.py
new file mode 100644
index 0000000..493f5cb
--- /dev/null
+++ b/output/yaml_outputter.py
@@ -0,0 +1,15 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+from output import OutputterBase
+import yaml
+
+class Outputter(OutputterBase):
+
+ def dump(self, data, pretty_print=False):
+ return yaml.dump(data, default_flow_style=not pretty_print)
diff --git a/reclass.py b/reclass.py
new file mode 100755
index 0000000..f0c24ce
--- /dev/null
+++ b/reclass.py
@@ -0,0 +1,53 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# reclass — recursive external node classifier
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+__prog__ = 'reclass'
+__description__ = 'classify nodes based on an external data source'
+__version__ = '1.0'
+__author__ = 'martin f. krafft <madduck@madduck.net>'
+__copyright__ = 'Copyright © 2007–13 ' + __author__
+__licence__ = 'Artistic Licence 2.0'
+
+import sys, os, posix, time
+import config
+from output import OutputLoader
+from storage import StorageBackendLoader
+
+def get_options(config_file=None):
+ return config.get_options(__name__, __version__, __description__, config_file)
+
+def get_data(storage_type, nodes_uri, classes_uri, node):
+ storage_class = StorageBackendLoader(storage_type).load()
+ storage = storage_class(os.path.abspath(os.path.expanduser(nodes_uri)),
+ os.path.abspath(os.path.expanduser(classes_uri)))
+ if node is False:
+ ret = storage.inventory()
+ else:
+ ret = storage.nodeinfo(node)
+ ret['RECLASS']['timestamp'] = time.strftime('%c')
+
+ return ret
+
+def output(data, fmt, pretty_print=False):
+ output_class = OutputLoader(fmt).load()
+ outputter = output_class()
+ return outputter.dump(data, pretty_print=pretty_print)
+
+if __name__ == '__main__':
+ __name__ = __prog__
+ config_file = None
+ for d in (os.getcwd(), os.path.dirname(sys.argv[0])):
+ f = os.path.join(d, __name__ + '-config.yml')
+ if os.access(f, os.R_OK):
+ config_file = f
+ break
+ options = get_options(config_file)
+ data = get_data(options.storage_type, options.nodes_uri,
+ options.classes_uri, options.node)
+ print output(data, options.output, options.pretty_print)
+ sys.exit(posix.EX_OK)
diff --git a/run_tests.py b/run_tests.py
new file mode 100755
index 0000000..7658f91
--- /dev/null
+++ b/run_tests.py
@@ -0,0 +1,10 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+import nose
+nose.main()
diff --git a/storage/__init__.py b/storage/__init__.py
new file mode 100644
index 0000000..0613bfe
--- /dev/null
+++ b/storage/__init__.py
@@ -0,0 +1,52 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+class NodeStorageBase(object):
+
+ def __init__(self, nodes_uri, classes_uri):
+ self._nodes_uri = nodes_uri
+ self._classes_uri = classes_uri
+
+ nodes_uri = property(lambda self: self._nodes_uri)
+ classes_uri = property(lambda self: self._classes_uri)
+
+ def _read_entity(self, node, base_uri, seen={}):
+ raise NotImplementedError, "Storage class not implement node info retrieval"
+
+ def nodeinfo(self, node):
+ entity, uri = self._read_nodeinfo(node, self.nodes_uri, {})
+ return {'RECLASS' : {'node': node, 'node_uri': uri},
+ 'classes': list(entity.classes),
+ 'applications': list(entity.applications),
+ 'parameters': dict(entity.parameters)
+ }
+
+ def _list_inventory(self):
+ raise NotImplementedError, "Storage class does not implement inventory listing"
+
+ def inventory(self):
+ entity, applications, classes = self._list_inventory()
+ ret = classes
+ ret.update([(k + '_hosts',v) for k,v in applications.iteritems()])
+ return ret
+
+class StorageBackendLoader(object):
+
+ def __init__(self, storage_type):
+ self._name = storage_type
+ try:
+ self._module = __import__(storage_type, globals(), locals(), storage_type)
+ except ImportError:
+ raise NotImplementedError
+
+ def load(self, attr='ExternalNodeStorage'):
+ klass = getattr(self._module, attr, None)
+ if klass is None:
+ raise AttributeError, \
+ 'Storage backend class {0} does not export "{1}"'.format(self._name, klass)
+ return klass
diff --git a/storage/yaml_fs/__init__.py b/storage/yaml_fs/__init__.py
new file mode 100644
index 0000000..0eff9ed
--- /dev/null
+++ b/storage/yaml_fs/__init__.py
@@ -0,0 +1,59 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+import os
+from storage import NodeStorageBase
+from yamlfile import YamlFile
+from directory import Directory
+
+FILE_EXTENSION = '.yml'
+
+class ExternalNodeStorage(NodeStorageBase):
+
+ def __init__(self, nodes_uri, classes_uri):
+ super(ExternalNodeStorage, self).__init__(nodes_uri, classes_uri)
+
+ def _read_nodeinfo(self, name, base_uri, seen):
+ path = os.path.join(base_uri, name + FILE_EXTENSION)
+ entity = YamlFile(path).entity
+ seen[name] = True
+ for klass in entity.classes:
+ if klass not in seen:
+ ret = self._read_nodeinfo(klass, self.classes_uri, seen)[0]
+ ret.merge(entity)
+ entity = ret
+ return entity, path
+
+ def _list_inventory(self):
+ d = Directory(self.nodes_uri)
+
+ entities = {}
+
+ def register_fn(dirpath, filenames):
+ for f in filter(lambda f: f.endswith(FILE_EXTENSION), filenames):
+ name = f[:-len(FILE_EXTENSION)]
+ nodeinfo = self.nodeinfo(name)
+ entities[name] = nodeinfo
+
+ d.walk(register_fn)
+
+ applications = {}
+ classes = {}
+ for f, nodeinfo in entities.iteritems():
+ for a in nodeinfo['applications']:
+ if a in applications:
+ applications[a].append(f)
+ else:
+ applications[a] = [f]
+ for c in nodeinfo['classes']:
+ if c in classes:
+ classes[c].append(f)
+ else:
+ classes[c] = [f]
+
+ return entities, applications, classes
diff --git a/storage/yaml_fs/directory.py b/storage/yaml_fs/directory.py
new file mode 100644
index 0000000..c085731
--- /dev/null
+++ b/storage/yaml_fs/directory.py
@@ -0,0 +1,53 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+import os
+import sys
+
+SKIPDIRS = ( '.git' , '.svn' , 'CVS', 'SCCS', '.hg', '_darcs' )
+FILE_EXTENSION = '.yml'
+
+def vvv(msg):
+ #print >>sys.stderr, msg
+ pass
+
+class Directory(object):
+
+ def __init__(self, path, fileclass=None):
+ ''' Initialise a directory object '''
+ self._path = path
+ self._fileclass = fileclass
+ self._files = {}
+
+ def _register_files(self, dirpath, filenames):
+ for f in filter(lambda f: f.endswith(FILE_EXTENSION), filenames):
+ vvv('REGISTER {0}'.format(f))
+ f = os.path.join(dirpath, f)
+ ptr = None if not self._fileclass else self._fileclass(f)
+ self._files[f] = ptr
+
+ files = property(lambda self: self._files)
+
+ def walk(self, register_fn=None):
+ def _error(error):
+ raise Exception('{0}: {1} ({2})'.format(error.filename, error.strerror, error.errno))
+ if not callable(register_fn): register_fn = self._register_files
+ for dirpath, dirnames, filenames in os.walk(self._path,
+ topdown=True,
+ onerror=_error,
+ followlinks=True):
+ vvv('RECURSE {0}, {1} files, {2} subdirectories'.format(
+ dirpath.replace(os.getcwd(), '.'), len(filenames), len(dirnames)))
+ for d in SKIPDIRS:
+ if d in dirnames:
+ vvv(' SKIP subdirectory {0}'.format(d))
+ dirnames.remove(d)
+ register_fn(dirpath, filenames)
+
+ def __repr__(self):
+ return '<{0} {1}>'.format(self.__class__.__name__, self._path)
diff --git a/storage/yaml_fs/tests/classes/basenode.yml b/storage/yaml_fs/tests/classes/basenode.yml
new file mode 100644
index 0000000..d9d997a
--- /dev/null
+++ b/storage/yaml_fs/tests/classes/basenode.yml
@@ -0,0 +1,5 @@
+applications:
+- motd
+- firewall
+parameters:
+ realm: madduck.net
diff --git a/storage/yaml_fs/tests/classes/debiannode.yml b/storage/yaml_fs/tests/classes/debiannode.yml
new file mode 100644
index 0000000..43e35e4
--- /dev/null
+++ b/storage/yaml_fs/tests/classes/debiannode.yml
@@ -0,0 +1,4 @@
+classes:
+- basenode
+applications:
+- apt
diff --git a/storage/yaml_fs/tests/classes/debiannode@sid.yml b/storage/yaml_fs/tests/classes/debiannode@sid.yml
new file mode 100644
index 0000000..806612a
--- /dev/null
+++ b/storage/yaml_fs/tests/classes/debiannode@sid.yml
@@ -0,0 +1,4 @@
+classes:
+- debiannode
+parameters:
+ debian_codename: sid
diff --git a/storage/yaml_fs/tests/classes/debiannode@squeeze.yml b/storage/yaml_fs/tests/classes/debiannode@squeeze.yml
new file mode 100644
index 0000000..08075b5
--- /dev/null
+++ b/storage/yaml_fs/tests/classes/debiannode@squeeze.yml
@@ -0,0 +1,4 @@
+classes:
+- debiannode
+parameters:
+ debian_codename: squeeze
diff --git a/storage/yaml_fs/tests/classes/debiannode@wheezy.yml b/storage/yaml_fs/tests/classes/debiannode@wheezy.yml
new file mode 100644
index 0000000..5ced15a
--- /dev/null
+++ b/storage/yaml_fs/tests/classes/debiannode@wheezy.yml
@@ -0,0 +1,4 @@
+classes:
+- debiannode
+parameters:
+ debian_codename: wheezy
diff --git a/storage/yaml_fs/tests/classes/hosted@munich.yml b/storage/yaml_fs/tests/classes/hosted@munich.yml
new file mode 100644
index 0000000..1d14dad
--- /dev/null
+++ b/storage/yaml_fs/tests/classes/hosted@munich.yml
@@ -0,0 +1,4 @@
+parameters:
+ location: munich
+ apt:
+ mirror_base: uni-erlangen
diff --git a/storage/yaml_fs/tests/classes/hosted@zurich.yml b/storage/yaml_fs/tests/classes/hosted@zurich.yml
new file mode 100644
index 0000000..050af27
--- /dev/null
+++ b/storage/yaml_fs/tests/classes/hosted@zurich.yml
@@ -0,0 +1,4 @@
+parameters:
+ location: zurich
+ apt:
+ mirror_base: switch
diff --git a/storage/yaml_fs/tests/classes/mailserver.yml b/storage/yaml_fs/tests/classes/mailserver.yml
new file mode 100644
index 0000000..37d6673
--- /dev/null
+++ b/storage/yaml_fs/tests/classes/mailserver.yml
@@ -0,0 +1,5 @@
+applications:
+- postfix
+parameters:
+ firewall:
+ openport: 25/tcp
diff --git a/storage/yaml_fs/tests/classes/subdir/subclass.yml b/storage/yaml_fs/tests/classes/subdir/subclass.yml
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/storage/yaml_fs/tests/classes/subdir/subclass.yml
diff --git a/storage/yaml_fs/tests/classes/webserver.yml b/storage/yaml_fs/tests/classes/webserver.yml
new file mode 100644
index 0000000..5db36ff
--- /dev/null
+++ b/storage/yaml_fs/tests/classes/webserver.yml
@@ -0,0 +1,5 @@
+applications:
+- lighttpd
+parameters:
+ firewall:
+ openport: 80/tcp
diff --git a/storage/yaml_fs/tests/nodes/blue.yml b/storage/yaml_fs/tests/nodes/blue.yml
new file mode 100644
index 0000000..d7d06ee
--- /dev/null
+++ b/storage/yaml_fs/tests/nodes/blue.yml
@@ -0,0 +1,9 @@
+classes:
+- debiannode@squeeze
+- hosted@munich
+- mailserver
+- webserver
+parameters:
+ motd:
+ greeting: This node is $nodename
+ colour: blue
diff --git a/storage/yaml_fs/tests/nodes/green.yml b/storage/yaml_fs/tests/nodes/green.yml
new file mode 100644
index 0000000..1f3d6d6
--- /dev/null
+++ b/storage/yaml_fs/tests/nodes/green.yml
@@ -0,0 +1,10 @@
+classes:
+- debiannode@wheezy
+- hosted@zurich
+- mailserver
+applications:
+- ~firewall
+parameters:
+ motd:
+ greeting: This node is $nodename
+ colour: green
diff --git a/storage/yaml_fs/tests/nodes/red.yml b/storage/yaml_fs/tests/nodes/red.yml
new file mode 100644
index 0000000..5fe618d
--- /dev/null
+++ b/storage/yaml_fs/tests/nodes/red.yml
@@ -0,0 +1,8 @@
+classes:
+- debiannode@sid
+- hosted@zurich
+- webserver
+parameters:
+ motd:
+ greeting: This node is $nodename
+ colour: red
diff --git a/storage/yaml_fs/tests/test_directory.py b/storage/yaml_fs/tests/test_directory.py
new file mode 100644
index 0000000..4c2604a
--- /dev/null
+++ b/storage/yaml_fs/tests/test_directory.py
@@ -0,0 +1,30 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+import storage.yaml_fs.directory as directory
+import os, sys
+
+TESTDIR = os.path.join(sys.path[0], 'classes')
+FILECOUNT = 10
+
+class TestDirectory:
+
+ def setUp(self):
+ self._dir = directory.Directory(TESTDIR)
+
+ def test_walk_registry(self):
+ def count_fn(d, f):
+ count_fn.c += len(f)
+ count_fn.c = 0
+ self._dir.walk(register_fn=count_fn)
+ assert count_fn.c == FILECOUNT
+
+ def test_walk(self):
+ self._dir.walk()
+ assert len(self._dir.files) == FILECOUNT
+
diff --git a/storage/yaml_fs/tests/test_yaml_fs.py b/storage/yaml_fs/tests/test_yaml_fs.py
new file mode 100644
index 0000000..d2cdf29
--- /dev/null
+++ b/storage/yaml_fs/tests/test_yaml_fs.py
@@ -0,0 +1,64 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+from storage.yaml_fs import ExternalNodeStorage
+
+import os
+
+PWD = os.path.dirname(__file__)
+HOSTS = ['red', 'blue', 'green']
+MEMBERSHIPS = {'apt_hosts': HOSTS,
+ 'motd_hosts': HOSTS,
+ 'firewall_hosts': HOSTS[:2],
+ 'lighttpd_hosts': HOSTS[:2],
+ 'postfix_hosts': HOSTS[1:],
+ 'basenode': HOSTS,
+ 'debiannode': HOSTS,
+ 'debiannode@sid': HOSTS[0:1],
+ 'debiannode@wheezy': HOSTS[2:3],
+ 'debiannode@squeeze': HOSTS[1:2],
+ 'hosted@munich': HOSTS[1:2],
+ 'hosted@zurich': [HOSTS[0], HOSTS[2]],
+ 'mailserver': HOSTS[1:],
+ 'webserver': HOSTS[:2]
+ }
+
+class TestYamlFs:
+
+ def setUp(self):
+ self._storage = ExternalNodeStorage(os.path.join(PWD, 'nodes'),
+ os.path.join(PWD, 'classes'))
+ self._inventory = self._storage.inventory()
+
+ def test_inventory_setup(self):
+ assert isinstance(self._inventory, dict)
+ assert len(self._inventory) == len(MEMBERSHIPS)
+ for i in MEMBERSHIPS.iterkeys():
+ assert i in self._inventory
+
+ def test_inventory_memberships(self):
+ for app, members in self._inventory.iteritems():
+ for i in MEMBERSHIPS[app]:
+ print i
+ assert i in members
+ for app, members in MEMBERSHIPS.iteritems():
+ for i in self._inventory[app]:
+ print i
+ assert i in members
+
+ def test_host_meta(self):
+ for n in HOSTS:
+ node = self._storage.nodeinfo(n)
+ assert 'RECLASS' in node
+
+ def test_host_entity(self):
+ for n in HOSTS:
+ node = self._storage.nodeinfo(n)
+ assert 'applications' in node
+ assert 'classes' in node
+ assert 'parameters' in node
diff --git a/storage/yaml_fs/tests/test_yamlfile.py b/storage/yaml_fs/tests/test_yamlfile.py
new file mode 100644
index 0000000..d8302f1
--- /dev/null
+++ b/storage/yaml_fs/tests/test_yamlfile.py
@@ -0,0 +1,28 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+import storage.yaml_fs.yamlfile as yamlfile
+import os, sys
+
+TESTFILE = os.path.join(sys.path[0], 'nodes', 'blue.yml')
+
+class TestYamlFile:
+
+ def setUp(self):
+ self._file = yamlfile.YamlFile(TESTFILE)
+
+ def test_data(self):
+ e = self._file.entity
+ c = e.classes
+ assert len(c) == 4
+ assert hasattr(c, 'merge')
+ p = e.parameters
+ assert len(p) == 2
+ assert 'motd' in p
+ assert 'colour' in p
+ assert hasattr(p, 'merge')
diff --git a/storage/yaml_fs/yamlfile.py b/storage/yaml_fs/yamlfile.py
new file mode 100644
index 0000000..0d1175a
--- /dev/null
+++ b/storage/yaml_fs/yamlfile.py
@@ -0,0 +1,37 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+import datatypes
+import yaml
+
+class YamlFile(object):
+
+ def __init__(self, path):
+ ''' Initialise a yamlfile object '''
+ self._path = path
+ self._data = dict()
+ self._read()
+
+ def _read(self):
+ fp = file(self._path)
+ self._data = yaml.safe_load(fp)
+ self._name = self._data.get('name', self._path)
+ fp.close()
+
+ name = property(lambda self: self._name)
+
+ def _get_entity(self):
+ classes = datatypes.Classes(self._data.get('classes', []))
+ parameters = datatypes.Parameters(self._data.get('parameters', {}))
+ applications = datatypes.Applications(self._data.get('applications', []))
+ return datatypes.Entity(classes, parameters, applications)
+ entity = property(lambda self: self._get_entity())
+
+ def __repr__(self):
+ return '<{0} {1}, {2}>'.format(self.__class__.__name__, self._name,
+ self._data.keys())