Add mapper CLI utility

A busctl like CLI that removes the foreknowledge requirement of a
service name by leveraging the object mapper.

Initial applets are call and wait:
 call: Invoke a method
 wait: Wait for a list of objects to appear on the bus

Change-Id: Ic52f09a634949efe8d5c78e51ba965f2e7150018
Signed-off-by: Brad Bishop <bradleyb@fuzziesquirrel.com>
diff --git a/mapper b/mapper
new file mode 100644
index 0000000..be64aac
--- /dev/null
+++ b/mapper
@@ -0,0 +1,23 @@
+#!/usr/bin/env python
+
+# Contributors Listed Below - COPYRIGHT 2016
+# [+] International Business Machines Corp.
+#
+#
+# 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.
+
+import sys
+import obmc.mapper.cli
+
+if __name__ == '__main__':
+    sys.exit(obmc.mapper.cli.mapper_main())
diff --git a/obmc/mapper/cli.py b/obmc/mapper/cli.py
new file mode 100644
index 0000000..7f96316
--- /dev/null
+++ b/obmc/mapper/cli.py
@@ -0,0 +1,125 @@
+# Contributors Listed Below - COPYRIGHT 2016
+# [+] International Business Machines Corp.
+#
+#
+# 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.
+
+import sys
+import dbus
+import dbus.mainloop.glib
+import gobject
+import obmc.dbuslib.enums
+import obmc.mapper
+import obmc.mapper.utils
+import optparse
+
+
+def add_systemd_path_option(parser):
+    parser.add_option(
+        '-s', '--systemd', action='store_true', default=False,
+        help='interpret-dash-delimited-path-arguments-as-paths')
+
+
+def systemd_to_dbus(item):
+    if not item.startswith('/'):
+        item = '/%s' % item.replace('-', '/')
+    return item
+
+
+class CallApp(object):
+    usage = 'OBJECTPATH INTERFACE METHOD ARGUMENTS...'
+    description = 'Invoke a DBus method on the named DBus object.'
+
+    def setup(self, parser, command):
+        add_systemd_path_option(parser)
+
+    def main(self, parser):
+        args = parser.largs
+        try:
+            path, interface, method, parameters = \
+                args[0], args[1], args[2], args[3:]
+        except IndexError:
+            parser.error('Not enough arguments')
+
+        bus = dbus.SystemBus()
+        mapper = obmc.mapper.Mapper(bus)
+        if parser.values.systemd:
+            path = systemd_to_dbus(path)
+
+        try:
+            service_info = mapper.get_object(path)
+        except dbus.exceptions.DBusException, e:
+            if e.get_dbus_name() != obmc.mapper.MAPPER_NOT_FOUND:
+                raise
+            parser.error('\'%s\' was not found' % path)
+
+        obj = bus.get_object(list(service_info)[0], path, introspect=False)
+        iface = dbus.Interface(obj, interface)
+        func = getattr(iface, method)
+        try:
+            return func(*parameters)
+        except dbus.exceptions.DBusException, e:
+            if e.get_dbus_name() != obmc.dbuslib.enums.DBUS_UNKNOWN_METHOD:
+                raise
+            parser.error(
+                '\'%s.%s\' is not a valid method for \'%s\''
+                % (interface, method, path))
+
+
+class WaitApp(object):
+    usage = 'OBJECTPATH...'
+    description = 'Wait for one or more DBus ' \
+        'object(s) to appear on the system bus.'
+
+    def setup(self, parser, command):
+        add_systemd_path_option(parser)
+
+    def main(self, parser):
+        if not parser.largs:
+            parser.error('Specify one or more object paths')
+
+        dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
+        loop = gobject.MainLoop()
+        bus = dbus.SystemBus()
+        if parser.values.systemd:
+            waitlist = [systemd_to_dbus(x) for x in parser.largs]
+        else:
+            waitlist = parser.largs
+
+        waiter = obmc.mapper.utils.Wait(bus, waitlist, callback=loop.quit)
+        loop.run()
+
+
+def mapper_main():
+    all_commands = []
+    usage = '''%prog [options] SUBCOMMAND\n\nSUBCOMMANDS:\n'''
+    for k, v in sys.modules[__name__].__dict__.iteritems():
+        if k.endswith('App'):
+            all_commands.append(k.replace('App', '').lower())
+            usage += '    %s - %s\n' % (all_commands[-1], v.description)
+
+    parser = optparse.OptionParser(usage=usage)
+    commands = list(set(sys.argv[1:]).intersection(all_commands))
+    if len(commands) != 1:
+        parser.error('Specify a single sub-command')
+
+    classname = '%sApp' % commands[0].capitalize()
+    cls = getattr(sys.modules[__name__], classname)
+    usage = getattr(cls, 'usage')
+
+    parser.set_usage('%%prog %s [options] %s' % (commands[0], usage))
+    inst = cls()
+    inst.setup(parser, commands[0])
+    opts, args = parser.parse_args()
+    parser.largs = parser.largs[1:]
+    return inst.main(parser)
diff --git a/setup.py b/setup.py
index 9967f98..ca87b38 100644
--- a/setup.py
+++ b/setup.py
@@ -2,5 +2,5 @@
 setup(name='phosphor-mapper',
       version='1.0',
       packages=['obmc.mapper'],
-      scripts=['phosphor-mapper']
+      scripts=['phosphor-mapper', 'mapper']
       )