Implemented basic class for functional tests

Functional tests uses rsyncd instances and file system
PEP8 fixes

Related-Bug: #1570260
Partial-Bug: #1575759
Change-Id: Id6533aba293b4a50be04967b68fb1827dec8141a
diff --git a/trsync/tests/base.py b/trsync/tests/base.py
index 185fd6f..36f0adc 100644
--- a/trsync/tests/base.py
+++ b/trsync/tests/base.py
@@ -1,7 +1,6 @@
 # -*- coding: utf-8 -*-
 
-# Copyright 2010-2011 OpenStack Foundation
-# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
+# Copyright (c) 2015-2016, Mirantis, Inc.
 #
 # 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
@@ -20,4 +19,4 @@
 
 class TestCase(base.BaseTestCase):
 
-    """Test case base class for all unit tests."""
\ No newline at end of file
+    """Test case base class for all unit tests."""
diff --git a/trsync/tests/rsync_base.py b/trsync/tests/rsync_base.py
new file mode 100644
index 0000000..ea97660
--- /dev/null
+++ b/trsync/tests/rsync_base.py
@@ -0,0 +1,64 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2015-2016, Mirantis, Inc.
+#
+# 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 filecmp
+import os
+import pkgutil
+
+from trsync.tests import base
+from trsync.tests import rsync_remotes as remotes
+from trsync.utils import utils as utils
+
+
+logger = utils.logger.getChild('TestRsyncUrl')
+
+
+class TestRsyncBase(base.TestCase):
+
+    """Test case base class for all functional tests"""
+    rsyncd = utils.bunch()
+
+    @property
+    def testname(self):
+        return self.__module__ + "." + self.__class__.__name__ + \
+            "." + self._get_test_method().__name__
+
+    def getDataFile(self, name):
+        path, _ = os.path.split(name)
+        if not os.path.isdir(path):
+            os.makedirs(path)
+        with open(name, 'w') as outf:
+            outf.write('TEST DATA')
+        return name
+
+    def setUp(self):
+        super(TestRsyncBase, self).setUp()
+        self.rsyncd[self.testname] = list()
+        for importer, modname, ispkg in \
+                pkgutil.iter_modules(remotes.__path__, remotes.__name__ + '.'):
+            module = __import__(modname, fromlist='dummy')
+            self.rsyncd[self.testname].append(module.Instance(self.testname))
+
+    def tearDown(self):
+        super(TestRsyncBase, self).tearDown()
+        for module in self.rsyncd[self.testname]:
+            module.stop()
+
+    def assertDirsEqual(self, left, right):
+        diff = filecmp.dircmp(left, right)
+        self.assertListEqual(diff.diff_files, [])
+        self.assertListEqual(diff.left_only, [])
+        self.assertListEqual(diff.right_only, [])
diff --git a/trsync/tests/rsync_remotes/__init__.py b/trsync/tests/rsync_remotes/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/trsync/tests/rsync_remotes/__init__.py
diff --git a/trsync/tests/rsync_remotes/path.py b/trsync/tests/rsync_remotes/path.py
new file mode 100644
index 0000000..3807c67
--- /dev/null
+++ b/trsync/tests/rsync_remotes/path.py
@@ -0,0 +1,58 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2015-2016, Mirantis, Inc.
+#
+# 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 logging
+import os
+import shutil
+
+
+logging.basicConfig()
+log = logging.getLogger(__name__ + 'Instance')
+log.setLevel('DEBUG')
+
+
+class Instance(object):
+
+    """Provide an temporal rsync daemon on custom port"""
+
+    def __init__(self, name):
+        self._log = log.getChild(name)
+        self._name = name
+        self._root_dir = '/tmp/trsync_test/path'
+        self._data_dir = os.path.join(self._root_dir, name)
+
+        self._init_files()
+
+    def stop(self):
+        if os.path.isdir(self._data_dir):
+            self._log.debug('Removed directory "%s"', self._data_dir)
+            shutil.rmtree(self._data_dir)
+
+    @property
+    def url(self):
+        return self.path
+
+    @property
+    def path(self):
+        return self._data_dir
+
+    def _init_files(self):
+        if os.path.isdir(self._data_dir):
+            self._log.debug('Directory %s already exists. Removing...'
+                            '', self._data_dir)
+            shutil.rmtree(self._data_dir)
+        self._log.debug('Creating rsync local directory %s', self.path)
+        os.makedirs(self.path)
diff --git a/trsync/tests/rsync_remotes/rsync2.conf b/trsync/tests/rsync_remotes/rsync2.conf
new file mode 100644
index 0000000..a7a28a9
--- /dev/null
+++ b/trsync/tests/rsync_remotes/rsync2.conf
@@ -0,0 +1,53 @@
+# sample rsyncd.conf configuration file
+
+# GLOBAL OPTIONS
+
+#motd file=/etc/motd
+#log file=/var/log/rsyncd
+# for pid file, do not use /var/run/rsync.pid if
+# you are going to run rsync out of the init.d script.
+# The init.d script does its own pid file handling,
+# so omit the "pid file" line completely in that case.
+# pid file=/var/run/rsyncd.pid
+pid file = {{pid_file}}
+#syslog facility=daemon
+#socket options=
+port = {{port}}
+
+# MODULE OPTIONS
+
+#[ftp]
+[{{module}}]
+
+#	comment = public archive
+	comment = {{comment}}
+#	path = /var/www/pub
+	path = {{path}}
+	use chroot = no
+#	max connections=10
+#	lock file = /var/lock/rsyncd
+	lock file = /var/lock/{{name}}
+# the default for read only is yes...
+#	read only = yes
+	read only = no
+	list = yes
+#	uid = nobody
+#	gid = nogroup
+#	exclude = 
+#	exclude from = 
+#	include =
+#	include from =
+#	auth users = 
+#	secrets file = /etc/rsyncd.secrets
+	strict modes = yes
+#	hosts allow =
+	hosts allow = {{hosts_allow}}
+#	hosts deny =
+	ignore errors = no
+	ignore nonreadable = yes
+	transfer logging = no
+#	log format = %t: host %h (%a) %o %f (%l bytes). Total %b bytes.
+	timeout = 600
+#	refuse options = checksum dry-run
+	dont compress = *.gz *.tgz *.zip *.z *.rpm *.deb *.iso *.bz2 *.tbz
+    munge symlinks = no
diff --git a/trsync/tests/rsync_remotes/rsync2.py b/trsync/tests/rsync_remotes/rsync2.py
new file mode 100644
index 0000000..d6f6082
--- /dev/null
+++ b/trsync/tests/rsync_remotes/rsync2.py
@@ -0,0 +1,123 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2015-2016, Mirantis, Inc.
+#
+# 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 logging
+import os
+import shutil
+import signal
+import socket
+
+from jinja2 import Template
+from time import sleep
+
+from trsync.utils import shell as shell
+from trsync.utils.utils import bunch as bunch
+
+
+logging.basicConfig()
+log = logging.getLogger(__name__ + 'Instance')
+log.setLevel('DEBUG')
+
+
+class Instance(object):
+
+    """Provide an temporal rsync daemon on custom port"""
+
+    def __init__(self, name):
+        self._log = log.getChild(name)
+        self._name = name
+        self._root_dir = '/tmp/trsync_test/rsync2'
+        self._data_dir = os.path.join(self._root_dir, name)
+
+        self._cfg = bunch()
+        self._cfg.module = name
+        self._cfg.comment = 'rsyncd instance for {}'.format(self._name)
+        self._cfg.path = os.path.join(self._data_dir, self._name)
+        self._cfg.hosts_allow = 'localhost 127.0.0.1'
+        self._cfg.port = self._get_port()
+        self._cfg.pid_file = os.path.join(self._data_dir, self._name + '.pid')
+        self._cfg.config = os.path.join(self._data_dir, self._name + '.conf')
+
+        self._init_files()
+        self._run()
+
+    def _run(self):
+
+        sh = shell.Shell()
+        self._cmd = '/usr/bin/rsync --verbose --daemon --port {} --config {}'\
+            ''.format(self._cfg.port, self._cfg.config)
+        self._log.debug('Starting rsync daemon "{}"'.format(self._cmd))
+        sh.shell(self._cmd)
+        retry_time = 3.0
+        sleep_time = 0.0
+        while not os.path.isfile(self._cfg.pid_file):
+            sleep(0.2)
+            sleep_time += 0.2
+            if sleep_time >= retry_time:
+                raise RuntimeError('pid-file "{}" not found'
+                                   ''.format(self._cfg.pid_file))
+        self._pid = int(open(self._cfg.pid_file).read().strip())
+
+    def stop(self):
+        self._log.debug('Stoping rsync daemon "%s" (PID=%d)',
+                        self._cmd, self._pid)
+        os.kill(self._pid, signal.SIGTERM)
+        if os.path.isdir(self._data_dir):
+            self._log.debug('Removed directory "%s"', self._data_dir)
+            shutil.rmtree(self._data_dir)
+
+    @property
+    def url(self):
+        return 'rsync://localhost:{port}/{module}'.format(**self._cfg)
+
+    @property
+    def path(self):
+        return self._cfg.path
+
+    def _get_config(self):
+        tpl_path, _ = os.path.split(os.path.realpath(__file__))
+        template = Template(open(os.path.join(tpl_path, 'rsync2.conf')).read())
+        return template.render(**self._cfg)
+
+    def _get_port(self):
+        portrange = [
+            int(_) for _
+            in open('/proc/sys/net/ipv4/ip_local_port_range').read().split()
+        ]
+        self._log.debug('Portrange is %s' % portrange)
+        for port in xrange(*portrange):
+            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+            self._log.debug('Trying to use port %d...' % port)
+            result = sock.connect_ex(('127.0.0.1', port))
+            self._log.debug('Result is %d' % result)
+            if result != 0:
+                self._log.debug('Port %s assigned', port)
+                return port
+            else:
+                self._log.debug('Port %d in use', port)
+        else:
+            raise RuntimeError("Can't assign port number for rsyncd")
+
+    def _init_files(self):
+        if os.path.isdir(self._data_dir):
+            self._log.debug('Directory %s already exists. Removing...'
+                            '', self._data_dir)
+            shutil.rmtree(self._data_dir)
+        self._log.debug('Creating module directory %s', self.path)
+        os.makedirs(self.path)
+        self._log.debug('Creating config %s', self._cfg.config)
+        with open(self._cfg.config, 'w') as config_file:
+            config_file.write(self._get_config())