Added CLI interface to trsync

Added an interface based on cliff to call push/remove commands.

Change-Id: Ic308974da577386c975a96fb1edb259175601680
diff --git a/requirements.txt b/requirements.txt
index 59b1755..505fbb5 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,4 +2,5 @@
 # of appearance. Changing the order has an impact on the overall integration
 # process, which may cause wedges in the gate later.
 
-pbr>=1.6
\ No newline at end of file
+pbr>=1.6
+cliff>=1.4.5
diff --git a/setup.cfg b/setup.cfg
index bd94c2d..6bad8ba 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,6 @@
 [metadata]
 name = trsync
+version = 0.5
 summary = rsync wrapper that implements transactional synchronization with remote location
 description-file =
     README.rst
@@ -43,4 +44,17 @@
 [extract_messages]
 keywords = _ gettext ngettext l_ lazy_gettext
 mapping_file = babel.cfg
-output_file = trsync/locale/trsync.pot
\ No newline at end of file
+output_file = trsync/locale/trsync.pot
+
+[entry_points]
+console_scripts =
+    trsync=trsync.cmd.cli:main
+
+trsync =
+    push = trsync.cmd.cli:PushCmd
+    remove = trsync.cmd.cli:RemoveCmd
+
+[global]
+setup-hooks =
+    pbr.hooks.setup_hook
+    setup_hooks.setup_hook
diff --git a/setup.py b/setup.py
index 4b34a0a..056c16c 100644
--- a/setup.py
+++ b/setup.py
@@ -26,4 +26,4 @@
 
 setuptools.setup(
     setup_requires=['pbr'],
-    pbr=True)
\ No newline at end of file
+    pbr=True)
diff --git a/setup_hooks.py b/setup_hooks.py
new file mode 100644
index 0000000..94cc99e
--- /dev/null
+++ b/setup_hooks.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+
+#    Copyright 2015 Mirantis, Inc.
+#
+#    This program is free software; you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation; either version 2 of the License, or
+#    (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License along
+#    with this program; if not, write to the Free Software Foundation, Inc.,
+#    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+
+def setup_hook(config):
+    import pbr
+    import pbr.packaging
+
+    # this monkey patch is to avoid appending git version to version
+    pbr.packaging._get_version_from_git = lambda pre_version: pre_version
diff --git a/trsync/__init__.py b/trsync/__init__.py
index 4fe32df..d523281 100644
--- a/trsync/__init__.py
+++ b/trsync/__init__.py
@@ -14,6 +14,8 @@
 
 import pbr.version
 
-
-__version__ = pbr.version.VersionInfo(
-    'trsync').version_string()
\ No newline at end of file
+try:
+    __version__ = pbr.version.VersionInfo(
+        'trsync').version_string()
+except Exception as e:
+    __version__ = "0.0.0"
diff --git a/trsync/cmd/cli.py b/trsync/cmd/cli.py
new file mode 100644
index 0000000..ff5a81b
--- /dev/null
+++ b/trsync/cmd/cli.py
@@ -0,0 +1,164 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os
+import sys
+import logging
+from cliff.app import App
+from cliff.commandmanager import CommandManager
+from cliff.command import Command
+
+from trsync.objects.rsync_mirror import TRsync
+
+class PushCmd(Command):
+    log = logging.getLogger(__name__)
+
+    def get_description(self):
+        return "push SRC to several DST with snapshots"
+
+    def get_parser(self, prog_name):
+        parser = super(PushCmd, self).get_parser(prog_name)
+        parser.add_argument('source', help='Source path')
+        parser.add_argument('mirror_name', help='Mirror name')
+        parser.add_argument('-d', '--dest',
+                            nargs='+',
+                            required=True,
+                            help='Destination rsync url')
+        parser.add_argument('-t', '--timestamp',
+                            required=False,
+                            help='Specified timestamp will be used for snapshot.'
+                            'Format:yyyy-mm-dd-hhMMSS')
+        parser.add_argument('--snapshot-dir',
+                            required=False,
+                            default='snapshots',
+                            help='Directory name for snapshots. "snapshots" '
+                            'by default')
+        parser.add_argument('--init-directory-structure',
+                            action='store_true',
+                            required=False,
+                            default=False,
+                            help='It specified, all directories including'
+                            '"snapshots-dir" will be created on remote location')
+        parser.add_argument('--save-latest-days',
+                            required=False,
+                            default=61,
+                            help='Snapshots for specified number of days will be '
+                            'saved. All older will be removed. 61 by default. '
+                            '0 mean that old snapshots will not be deleted, '
+                            '"None" mean that all snapshots excluding latest '
+                            'will be deleted')
+        parser.add_argument('--latest-successful-postfix',
+                            required=False,
+                            default='latest',
+                            help='Postfix for symlink to latest successfully '
+                            'synced snapshot. Also used as --link-dest target. '
+                            '"latest" by default.')
+        parser.add_argument('-s', '--symlinks',
+                            nargs='+',
+                            required=False,
+                            default=[],
+                            help='Update additional symlinks relative destination')
+        parser.add_argument('--extra',
+                            required=False,
+                            default='',
+                            #action='store_const',
+                            help='String with additional rsync parameters. For '
+                            'example it may be "\--dry-run --any-rsync-option".'
+                            'Use "\\" to disable argparse to parse extra value.')
+
+        return parser
+
+    def take_action(self, parsed_args):
+        properties = vars(parsed_args)
+        source_dir = properties.pop('source', None)
+        mirror_name = properties.pop('mirror_name', None)
+        symlinks = properties.pop('symlinks', None)
+        servers = properties.pop('dest', None)
+        if properties['extra'].startswith('\\'):
+            properties['extra'] = properties['extra'][1:]
+        properties['rsync_extra_params'] = properties.pop('extra')
+        properties['save_latest_days'] = \
+            None if properties['save_latest_days'] == 'None' \
+                else int(properties['save_latest_days'])
+
+        failed = list()
+        for server in servers:
+            source_dir = os.path.realpath(source_dir)
+            if not source_dir.endswith('/'):
+                source_dir += '/'
+            remote = TRsync(server, **properties)
+            try:
+                remote.push(source_dir, mirror_name, symlinks=symlinks)
+            except Exception as e:
+                print e.message
+                failed.append(server)
+
+        if failed:
+            print "Failed to push to {}".format(str(failed))
+            sys.exit(1)
+            #self.app.stdout.write(parsed_args.arg + "\n")
+
+class RemoveCmd(Command):
+    log = logging.getLogger(__name__)
+
+    def get_description(self):
+        return "remove all specified paths from several DST recursively"
+
+    def get_parser(self, prog_name):
+        parser = super(RemoveCmd, self).get_parser(prog_name)
+
+        parser.add_argument('path',
+                            nargs='+',
+                            help='Path to remove')
+        parser.add_argument('-d', '--dest',
+                            nargs='+',
+                            required=True,
+                            help='Destination rsync url')
+        parser.add_argument('--extra',
+                            required=False,
+                            default='',
+                            help='String with additional rsync parameters. For '
+                            'example it may be "\--dry-run --any-rsync-option".'
+                            'Use "\\" to disable argparse to parse extra value.')
+        return parser
+
+    def take_action(self, parsed_args):
+        properties = vars(parsed_args)
+        servers = properties.pop('dest', None)
+        path = properties.pop('path', None)
+        if properties['extra'].startswith('\\'):
+            properties['extra'] = properties['extra'][1:]
+        properties['init_directory_structure'] = False
+        properties['rsync_extra_params'] = properties.pop('extra')# + ' --dry-run'
+
+        failed = list()
+        for server in servers:
+            remote = TRsync(server, **properties)
+            try:
+                print "Removing items {}".format(str(path))
+                remote.rm_all(path)
+            except Exception as e:
+                print e.message
+                failed.append(server)
+
+        if failed:
+            print "Failed to remove {}".format(str(failed))
+            sys.exit(1)
+
+class TRsyncApp(App):
+    log = logging.getLogger(__name__)
+
+    def __init__(self):
+        super(TRsyncApp, self).__init__(
+            description='TRsync',
+            version=trsync.__version__,
+            command_manager=CommandManager('trsync'),
+            deferred_help=True,
+            )
+
+def main(argv=sys.argv[1:]):
+    app = TRsyncApp()
+    return app.run(argv)
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv[1:]))