--- /dev/null
+#!/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)
+
--- /dev/null
+#!/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)