Introduce --inventory-base-uri option

The --inventory-base-uri will get prepended to the --nodes-uri and
--classes-uri options, unless they are absolute paths.

--nodes-uri and --classes-uri now default to 'nodes' and 'classes'
respectively.

Signed-off-by: martin f. krafft <madduck@madduck.net>
diff --git a/README b/README
index 228486f..07b0fd9 100644
--- a/README
+++ b/README
@@ -12,7 +12,7 @@
 inventory of nodes to be managed, completely separately from the configuration
 of the automation tool. Usually, the external node classifier completely
 replaces the tool-specific inventory (such as site.pp for Puppet,
-ext_pillar/ext_nodes for Salt, or /etc/ansible/hosts).
+ext_pillar/master_tops for Salt, or /etc/ansible/hosts).
 
 reclass allows you to define your nodes through class inheritance, while
 always able to override details of classes further up the tree. Think of
@@ -215,6 +215,14 @@
 For information on how to use reclass directly, invoke reclass.py with --help
 and study the output.
 
+The three options --inventory-base-uri, --nodes-uri, and --classes-uri
+together specify the location of the inventory. If the base URI is specified,
+then it is prepended to the other two URIs, unless they are absolute URIs. If
+these two URIs are not specified, they default to 'nodes' and 'classes'.
+Therefore, if your inventory is in '/etc/reclass/nodes' and
+'/etc/reclass/classes', all you need to specify is the base URI as
+'/etc/reclass'.
+
 More commonly, however, use of reclass will happen indirectly, and through
 so-called adapters, e.g. /…/reclass/adapters/salt. The job of an adapter is to
 translate between different invocation paradigms, provide a sane set of
@@ -234,6 +242,7 @@
   storage_type: yaml_fs
   pretty_print: True
   output: json
+  inventory_base_uri: /etc/reclass
   nodes_uri: ../nodes
 
 reclass first looks in the current directory for the file called
diff --git a/config.py b/config.py
index 1a3c603..e78c552 100644
--- a/config.py
+++ b/config.py
@@ -20,11 +20,14 @@
     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('-b', '--inventory-base-uri', dest='inventory_base_uri',
+                             default=defaults.get('inventory_base_uri', None),
+                             help='the base URI to append to nodes and classes [%default]'),
     options_group.add_option('-u', '--nodes-uri', dest='nodes_uri',
-                             default=defaults.get('nodes_uri', None),
+                             default=defaults.get('nodes_uri', './nodes'),
                              help='the URI to the nodes storage [%default]'),
     options_group.add_option('-c', '--classes-uri', dest='classes_uri',
-                             default=defaults.get('classes_uri', None),
+                             default=defaults.get('classes_uri', './classes'),
                              help='the URI to the classes storage [%default]')
     options_group.add_option('-o', '--output', dest='output',
                              default=defaults.get('output', 'yaml'),
@@ -63,10 +66,10 @@
         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
+    elif options.inventory_base_uri is None and options.nodes_uri is None:
+        usage_error('Must specify --inventory-base-uri or --nodes-uri')
+    elif options.inventory_base_uri is None and options.classes_uri is None:
+        usage_error('Must specify --inventory-base-uri or --classes-uri')
 
     return options
 
@@ -82,3 +85,19 @@
         config_data.update(read_config_file(config_file))
     parser = _make_parser(name, version, description, config_data)
     return _parse_and_check_options(parser)
+
+def path_mangler(inventory_base_uri, nodes_uri, classes_uri):
+
+    if inventory_base_uri is not None:
+        # if inventory_base is given, default to subdirectories
+        nodes_uri = nodes_uri or 'nodes'
+        classes_uri = classes_uri or 'classes'
+        # and prepend the inventory_base_uri
+        def _path_mangler_inner(path):
+            ret = os.path.join(inventory_base_uri, path)
+            ret = os.path.expanduser(ret)
+            return os.path.abspath(ret)
+
+        return map(_path_mangler_inner, (nodes_uri, classes_uri))
+
+    return nodes_uri, classes_uri
diff --git a/reclass.py.in b/reclass.py.in
index 9471646..dbb916f 100644
--- a/reclass.py.in
+++ b/reclass.py.in
@@ -24,10 +24,7 @@
 
 def get_data(storage_type, nodes_uri, classes_uri, applications_postfix, 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)),
-                            applications_postfix
-                           )
+    storage = storage_class(nodes_uri, classes_uri, applications_postfix)
     if node is False:
         ret = storage.inventory()
     else:
@@ -55,9 +52,11 @@
             break
     try:
         options = get_options(config_file)
-        data = get_data(options.storage_type, options.nodes_uri,
-                        options.classes_uri, options.applications_postfix,
-                        options.node)
+        nodes_uri, classes_uri = config.path_mangler(options.inventory_base_uri,
+                                                     options.nodes_uri,
+                                                     options.classes_uri)
+        data = get_data(options.storage_type, nodes_uri, classes_uri,
+                        options.applications_postfix, options.node)
         print output(data, options.output, options.pretty_print)
         sys.exit(posix.EX_OK)