draft implementation of BackendDB API
authorEd Bartosh <eduard.bartosh@intel.com>
Sat, 18 May 2013 21:07:50 +0000 (00:07 +0300)
committerGerrit Code Review <gerrit2@otctools.jf.intel.com>
Mon, 27 May 2013 02:50:17 +0000 (19:50 -0700)
This is generic API, which allows to store repository data in Redis.
API utilizes Redis hashes:
    'repo:<name>' hash contains attributes of the repository and their values

It's also an attempt to build generic API to store any type of data in Redis.
The ideas is that any type of data can be stored in Redis as set of
<type>:<subtype1>:<subtype2>... records, which point to data in Redis
hash format.

For example:
  Git->OBS mappings can be stored as set of keys with this format:
  gitobs-map:<project>:<branch>
  and hashes of this format: {"OBS_project": "home:user:project",
                              "OBS_staging_project": "home:user:staging"}

Format of hashes is not strict, so it can be extended whenever
needed.

Format of keys is stricter, but also flexible in a sense that
it's easy to change it or even increase amount of levels(depth) of it.

Change-Id: I2eb5d92d0ff6f5536a230068783a6507dfd6134d
Signed-off-by: Ed Bartosh <eduard.bartosh@intel.com>
common/backenddb.py [new file with mode: 0644]
packaging/jenkins-scripts.spec
tests/test_backenddb.py [new file with mode: 0644]

diff --git a/common/backenddb.py b/common/backenddb.py
new file mode 100644 (file)
index 0000000..08b8feb
--- /dev/null
@@ -0,0 +1,129 @@
+#!/usr/bin/env python
+# vim: ai ts=4 sts=4 et sw=4
+
+"""
+Access to backend data stored in Redis
+
+Example of usage:
+ dbobj = BackendDB()
+ # Get repositories as a dict-like object
+ repos = dbobj.get_repos()
+ # Use it
+ print repo['Release'], repo['latest_snapshot']
+ # Change latest snapshot
+ repo['latest_snapshot'] = 'product.123456.7'
+ dbobj.set_repo('Project-Main', repo)
+"""
+
+import redis
+import yaml
+
+class BackendDBError(Exception):
+    """BackendDB custom exception."""
+    pass
+
+class Entity(object):
+    """
+    Generic dict-like access to any type of
+    entities: repos, mappings, etc.
+
+    Usage example:
+        bdb = BackendDB()
+        repos = bdb.get_repos() # get entitiy object
+        if 'Myrepo' in repos:
+            print repos['Myrepo'], repos['Myrepo']['latest_snapthot']
+            repos.delete('Myrepo')
+        else:
+            repos['Myrepo'] = {'name': 'Myreponame', 'latest_snapshot': '123.4'}
+    """
+
+    def __init__(self, dbobj, prefix):
+        self._db = dbobj
+        self._prefix = prefix
+
+    def keys(self):
+        """Get set of keys started with the self._prefix."""
+        return set([key.split(self._prefix)[1] \
+                        for key in self._db.keys("%s*" % self._prefix)])
+
+    def __contains__(self, key):
+        return self._db.exists('%s%s' % (self._prefix, key))
+
+    def __getitem__(self, key):
+        return self._db.hgetall("%s%s" % (self._prefix, key))
+               #self._db.get('%s%s' % (self._prefix, key))
+
+    def __setitem__(self, key, value):
+        self._db.hmset("%s%s" % (self._prefix, key), value)
+
+    def delete(self, key):
+        """Remove key starting with the prefix from the db."""
+        self._db.delete("%s%s" % (self._prefix, key))
+
+    def __iter__(self):
+        return iter(self.keys())
+
+    def __len__(self):
+        return len(self.keys())
+
+class BackendDB():
+    """
+    Access and maintain backend data stored in Redis.
+
+    Data structure:
+       'repo:<name>' hash contains attributes of the repository and their values
+
+    # Future plans: develop similar set of APIs for git->obs mappings and other
+                    backend-related data
+       for example, for git->obs mappings it would look like this:
+       'git_obs_mapping:<name>' hash contains attributes of the mapping
+                                and their values
+       read_gitobs_mappings - read git->obs mappings from the file
+       get_gitobs_mappings - get mapings as a dict-like object
+       the rest is the same as for repos as it's implemented in Entity API
+    """
+
+    def __init__(self, host = 'localhost', port = 6379):
+        try:
+            self._redis = redis.Redis(host=host, port=port)
+            # try to do a simple query to confirm connection
+            self._redis.exists('anything')
+        except redis.RedisError, ex:
+            raise BackendDBError('[Error] cannot connect to redis server: %s' \
+                                 % str(ex))
+
+    def get_repos(self):
+        """Return repos entity object."""
+        return Entity(self._redis, "repo:")
+
+    def read_repos(self, yamlobj):
+        """
+        Read repos from repos.yaml.
+        Args:
+            yamlobj (str or file object): Content of repos.yaml
+                                          or its file object
+
+        Raises: BackendDBError when can't load yaml
+
+        """
+        try:
+            repos = yaml.load(yamlobj)["Repositories"]
+        except (yaml.YAMLError, TypeError), err:
+            raise BackendDBError("Error loading yaml: %s" % err)
+
+        db_repos = self.get_repos()
+        db_list = db_repos.keys() # save set of repos before loading new repos
+        names = set([])
+        for repo in repos:
+            name = repo.pop('Name')
+            names.add(name)
+            # set latest_snapshot attribute as it doesn't exist in repos.yaml
+            repo['latest_snapshot'] = '%s-%s' % (repo['PartOf'],
+                                                 repo['Release'])
+            db_repos[name] = repo
+
+        # Cleanup entries, which are present in db_list,
+        # but don't present in names
+        for name in db_list.difference(names):
+            db_repos.delete(name)
+
index db598e6..609491b 100644 (file)
@@ -9,6 +9,7 @@ Source:         %{name}-%{version}.tar.gz
 Requires:       git-core
 Requires:       python-mysql
 Requires:       python-yaml
+Requires:       python-redis
 Requires:       createrepo
 Requires:       rpmlint >= 1.3
 Requires:       gbs-api
@@ -53,7 +54,7 @@ if [ $1 = 0 ]; then
     rm -rf $JENKINS_HOME/jenkins-scripts
   fi
 fi
+
 %files
 %defattr(-,jenkins,jenkins)
 %{_localstatedir}/lib/jenkins
diff --git a/tests/test_backenddb.py b/tests/test_backenddb.py
new file mode 100644 (file)
index 0000000..f659b50
--- /dev/null
@@ -0,0 +1,201 @@
+#!/usr/bin/python -tt
+# vim: ai ts=4 sts=4 et sw=4
+#
+# Copyright (c) 2012 Intel, 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; version 2 of the License
+#
+# 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., 59
+# Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""Unit tests for class BackednDB"""
+
+import unittest
+import re
+from mock import patch
+from StringIO import StringIO
+
+import redis
+
+from common.backenddb import BackendDB, BackendDBError
+
+REPOS = """
+Repositories:
+    -   Name: Repo1
+        Link: latest
+        PartOf: trunk
+        TopLevel: /srv/trunk
+        Location: /srv/trunk/repos/repo1
+        Project: Project:Repo1
+        ProjectConfig: yes
+        Target: standard
+        Release: "release"
+        SnapshotDir: /srv/snapshots/trunk
+        Architectures:
+            - ia32
+            - armv7l
+
+    -   Name: Repo2
+        Link: latest
+        PartOf: trunk
+        TopLevel: /srv/trunk
+        Location: /srv/trunk/repos/repo2
+        Project: Project:Repo2
+        Target: standard
+        DependsOn: Repo1
+        SnapshotDir: /srv/snapshots/trunk
+        Release: "release"
+        Architectures:
+            - ia32
+            - armv7l
+
+    -   Name: Repo3
+        Link: latest
+        PartOf: trunk
+        TopLevel: /srv/trunk
+        Location: /srv/trunk/repos/repo3
+        Project: Tizen:non-oss
+        Target: standard
+        DependsOn: Tizen-main
+        SnapshotDir: /srv/snapshots/trunk
+        Release: "tizen"
+        Architectures:
+            - ia32
+
+"""
+
+
+class RedisMock(object):
+    """Mock minimal Redis functionality, required for this testing."""
+
+    def __init__(self, host='localhost', port=6379):
+        self.host = host
+        self.prot = port
+        self._dict = {}
+        if port != 6379:
+            raise redis.RedisError("Connect error!")
+
+    def delete(self, key):
+        """Delete key from db."""
+        self._dict.pop(key)
+
+    def exists(self, key):
+        """Check if key exists in the db."""
+        return key in self._dict
+
+    def keys(self, pattern):
+        return [key for key in self._dict.keys() \
+                    if re.match("^%s" % pattern, str(key))]
+
+    def hmset(self, key, value):
+        """Set hash fields to values."""
+        if key not in self._dict:
+            self._dict[key] = {}
+        for akey, avalue in value.items():
+            self._dict[key][akey] = str(avalue)
+        return True
+
+    def hgetall(self, key):
+        """Get all the fields and values in a hash."""
+        return self._dict.get(key)
+
+
+@patch('redis.Redis', RedisMock) # pylint: disable=R0904
+class BackendDBTest(unittest.TestCase):
+    '''Tests for BackendDB functionality.'''
+
+    def test_connect_exception(self):
+        """Pass incorrect port to raise connect exception."""
+        self.assertRaises(BackendDBError, BackendDB, port=65535)
+
+    def test_read_repos(self):
+        """Read repos from the yaml string."""
+        bdb = BackendDB()
+        bdb.read_repos(REPOS)
+        repos = bdb.get_repos()
+        self.assertEqual(repos.keys(), set(['Repo1', 'Repo2', 'Repo3']))
+
+    def test_reading_repos_from_fileobj(self):
+        """Read repos from file object."""
+        bdb = BackendDB()
+        bdb.read_repos(StringIO(REPOS))
+        repos = bdb.get_repos()
+        self.assertEqual(repos.keys(), set(['Repo1', 'Repo2', 'Repo3']))
+
+    def test_read_exception(self):
+        """Raising exception by providing incorrect yaml to read_repos."""
+        bdb = BackendDB()
+        self.assertRaises(BackendDBError, bdb.read_repos, 'bla')
+
+    def test_get_repo(self):
+        """Getting repo data."""
+        bdb = BackendDB()
+        bdb.read_repos(REPOS)
+        data = {'ProjectConfig': 'True', 'Target': 'standard',
+                'latest_snapshot': 'trunk-release',
+                'Project': 'Project:Repo1',
+                'TopLevel': '/srv/trunk',
+                'SnapshotDir': '/srv/snapshots/trunk',
+                'Link': 'latest',
+                'Architectures': "['ia32', 'armv7l']",
+                'Release': 'release', 'PartOf': 'trunk',
+                'Location': '/srv/trunk/repos/repo1'}
+        repos = bdb.get_repos()
+        self.assertEqual(repos["Repo1"], data)
+
+    def test_set_repo(self):
+        """Setting repo data."""
+        data = {'field1': 'value1', 'field2': 'value2'}
+        bdb = BackendDB()
+        repos = bdb.get_repos()
+        repos['Test'] = data
+        self.assertEqual(repos['Test'], data)
+
+    def test_get_repos_list(self):
+        """Get list of repos."""
+        bdb = BackendDB()
+        bdb.read_repos(REPOS)
+        repos = bdb.get_repos()
+        self.assertEqual(repos.keys(), set(['Repo1', 'Repo2', 'Repo3']))
+
+    def test_resetting_repos(self):
+        """Set repos 2 times to check if old repos are removed."""
+        bdb = BackendDB()
+        bdb.read_repos(REPOS)
+        repos = bdb.get_repos()
+        repos.delete('Repo1')
+        repos['Test'] = {'key': 'value'}
+        self.assertEqual(repos.keys(), set(['Repo2', 'Repo3', 'Test']))
+        bdb.read_repos(REPOS)
+        self.assertEqual(repos.keys(), set(['Repo1', 'Repo2', 'Repo3']))
+
+    def test_repo_exists(self):
+        """Test if repo exists in the database."""
+        bdb = BackendDB()
+        bdb.read_repos(REPOS)
+        repos = bdb.get_repos()
+        self.assertTrue('Repo1' in repos)
+        self.assertFalse('bla' in repos)
+
+    def test_iteration(self):
+        """Test iteration over the repos."""
+        bdb = BackendDB()
+        bdb.read_repos(REPOS)
+        repos = bdb.get_repos()
+        self.assertEqual(sorted([repo for repo in repos]),
+                         ['Repo1', 'Repo2', 'Repo3'])
+
+    def test_len(self):
+        """Test getting length(amount of repos in db)."""
+        bdb = BackendDB()
+        bdb.read_repos(REPOS)
+        repos = bdb.get_repos()
+        self.assertEqual(len(repos), 3)