368 lines
11 KiB
Python
368 lines
11 KiB
Python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
|
|
# Copyright [2010] [Anso Labs, LLC]
|
|
#
|
|
# 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.
|
|
|
|
"""
|
|
Datastore:
|
|
|
|
Providers the Keeper class, a simple pseudo-dictionary that
|
|
persists on disk.
|
|
|
|
MAKE Sure that ReDIS is running, and your flags are set properly,
|
|
before trying to run this.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import sqlite3
|
|
|
|
from nova import vendor
|
|
import redis
|
|
|
|
from nova import flags
|
|
from nova import utils
|
|
|
|
|
|
FLAGS = flags.FLAGS
|
|
flags.DEFINE_string('datastore_path', utils.abspath('../keeper'),
|
|
'where keys are stored on disk')
|
|
flags.DEFINE_string('redis_host', '127.0.0.1',
|
|
'Host that redis is running on.')
|
|
flags.DEFINE_integer('redis_port', 6379,
|
|
'Port that redis is running on.')
|
|
flags.DEFINE_integer('redis_db', 0, 'Multiple DB keeps tests away')
|
|
flags.DEFINE_string('keeper_backend', 'redis',
|
|
'which backend to use for keeper')
|
|
|
|
|
|
class Redis(object):
|
|
def __init__(self):
|
|
if hasattr(self.__class__, '_instance'):
|
|
raise Exception('Attempted to instantiate singleton')
|
|
|
|
@classmethod
|
|
def instance(cls):
|
|
if not hasattr(cls, '_instance'):
|
|
inst = redis.Redis(host=FLAGS.redis_host, port=FLAGS.redis_port, db=FLAGS.redis_db)
|
|
cls._instance = inst
|
|
return cls._instance
|
|
|
|
|
|
class RedisModel(object):
|
|
""" Wrapper around redis-backed properties """
|
|
object_type = 'generic'
|
|
def __init__(self, object_id):
|
|
""" loads an object from the datastore if exists """
|
|
self.object_id = object_id
|
|
self.initial_state = {}
|
|
self.state = Redis.instance().hgetall(self.__redis_key)
|
|
if self.state:
|
|
self.initial_state = self.state
|
|
else:
|
|
self.set_default_state()
|
|
|
|
def set_default_state(self):
|
|
self.state = {'state' : 'pending'}
|
|
self.state[self.object_type+"_id"] = self.object_id
|
|
|
|
@property
|
|
def __redis_key(self):
|
|
""" Magic string for instance keys """
|
|
return '%s:%s' % (self.object_type, self.object_id)
|
|
|
|
def __repr__(self):
|
|
return "<%s:%s>" % (self.object_type, self.object_id)
|
|
|
|
def __str__(self):
|
|
return str(self.state)
|
|
|
|
def keys(self):
|
|
return self.state.keys()
|
|
|
|
def copy(self):
|
|
copyDict = {}
|
|
for item in self.keys():
|
|
copyDict[item] = self[item]
|
|
return copyDict
|
|
|
|
def get(self, item, default):
|
|
return self.state.get(item, default)
|
|
|
|
def __getitem__(self, item):
|
|
return self.state[item]
|
|
|
|
def __setitem__(self, item, val):
|
|
self.state[item] = val
|
|
return self.state[item]
|
|
|
|
def __delitem__(self, item):
|
|
""" We don't support this """
|
|
raise Exception("Silly monkey, we NEED all our properties.")
|
|
|
|
def save(self):
|
|
""" update the directory with the state from this instance """
|
|
# TODO(ja): implement hmset in redis-py and use it
|
|
# instead of multiple calls to hset
|
|
for key, val in self.state.iteritems():
|
|
# if (not self.initial_state.has_key(key)
|
|
# or self.initial_state[key] != val):
|
|
Redis.instance().hset(self.__redis_key, key, val)
|
|
if self.initial_state == {}:
|
|
self.first_save()
|
|
self.initial_state = self.state
|
|
return True
|
|
|
|
def first_save(self):
|
|
pass
|
|
|
|
def destroy(self):
|
|
""" deletes all related records from datastore.
|
|
does NOT do anything to running state.
|
|
"""
|
|
Redis.instance().delete(self.__redis_key)
|
|
return True
|
|
|
|
|
|
def slugify(key, prefix=None):
|
|
"""
|
|
Key has to be a valid filename. Slugify solves that.
|
|
"""
|
|
return "%s%s" % (prefix, key)
|
|
|
|
|
|
class SqliteKeeper(object):
|
|
""" Keeper implementation in SQLite, mostly for in-memory testing """
|
|
_conn = {} # class variable
|
|
|
|
def __init__(self, prefix):
|
|
self.prefix = prefix
|
|
|
|
@property
|
|
def conn(self):
|
|
if self.prefix not in self.__class__._conn:
|
|
logging.debug('no sqlite connection (%s), making new', self.prefix)
|
|
if FLAGS.datastore_path != ':memory:':
|
|
try:
|
|
os.mkdir(FLAGS.datastore_path)
|
|
except Exception:
|
|
pass
|
|
conn = sqlite3.connect(os.path.join(
|
|
FLAGS.datastore_path, '%s.sqlite' % self.prefix))
|
|
else:
|
|
conn = sqlite3.connect(':memory:')
|
|
|
|
c = conn.cursor()
|
|
try:
|
|
c.execute('''CREATE TABLE data (item text, value text)''')
|
|
conn.commit()
|
|
except Exception:
|
|
logging.exception('create table failed')
|
|
finally:
|
|
c.close()
|
|
|
|
self.__class__._conn[self.prefix] = conn
|
|
|
|
return self.__class__._conn[self.prefix]
|
|
|
|
def __delitem__(self, item):
|
|
#logging.debug('sqlite deleting %s', item)
|
|
c = self.conn.cursor()
|
|
try:
|
|
c.execute('DELETE FROM data WHERE item = ?', (item, ))
|
|
self.conn.commit()
|
|
except Exception:
|
|
logging.exception('delete failed: %s', item)
|
|
finally:
|
|
c.close()
|
|
|
|
def __getitem__(self, item):
|
|
#logging.debug('sqlite getting %s', item)
|
|
result = None
|
|
c = self.conn.cursor()
|
|
try:
|
|
c.execute('SELECT value FROM data WHERE item = ?', (item, ))
|
|
row = c.fetchone()
|
|
if row:
|
|
result = json.loads(row[0])
|
|
else:
|
|
result = None
|
|
except Exception:
|
|
logging.exception('select failed: %s', item)
|
|
finally:
|
|
c.close()
|
|
#logging.debug('sqlite got %s: %s', item, result)
|
|
return result
|
|
|
|
def __setitem__(self, item, value):
|
|
serialized_value = json.dumps(value)
|
|
insert = True
|
|
if self[item] is not None:
|
|
insert = False
|
|
#logging.debug('sqlite insert %s: %s', item, value)
|
|
c = self.conn.cursor()
|
|
try:
|
|
if insert:
|
|
c.execute('INSERT INTO data VALUES (?, ?)',
|
|
(item, serialized_value))
|
|
else:
|
|
c.execute('UPDATE data SET item=?, value=? WHERE item = ?',
|
|
(item, serialized_value, item))
|
|
|
|
self.conn.commit()
|
|
except Exception:
|
|
logging.exception('select failed: %s', item)
|
|
finally:
|
|
c.close()
|
|
|
|
def clear(self):
|
|
if self.prefix not in self.__class__._conn:
|
|
return
|
|
self.conn.close()
|
|
if FLAGS.datastore_path != ':memory:':
|
|
os.unlink(os.path.join(FLAGS.datastore_path, '%s.sqlite' % self.prefix))
|
|
del self.__class__._conn[self.prefix]
|
|
|
|
def clear_all(self):
|
|
for k, conn in self.__class__._conn.iteritems():
|
|
conn.close()
|
|
if FLAGS.datastore_path != ':memory:':
|
|
os.unlink(os.path.join(FLAGS.datastore_path,
|
|
'%s.sqlite' % self.prefix))
|
|
self.__class__._conn = {}
|
|
|
|
|
|
def set_add(self, item, value):
|
|
group = self[item]
|
|
if not group:
|
|
group = []
|
|
group.append(value)
|
|
self[item] = group
|
|
|
|
def set_is_member(self, item, value):
|
|
group = self[item]
|
|
if not group:
|
|
return False
|
|
return value in group
|
|
|
|
def set_remove(self, item, value):
|
|
group = self[item]
|
|
if not group:
|
|
group = []
|
|
group.remove(value)
|
|
self[item] = group
|
|
|
|
def set_fetch(self, item):
|
|
# TODO(termie): I don't really know what set_fetch is supposed to do
|
|
group = self[item]
|
|
if not group:
|
|
group = []
|
|
return iter(group)
|
|
|
|
class JsonKeeper(object):
|
|
"""
|
|
Simple dictionary class that persists using
|
|
JSON in files saved to disk.
|
|
"""
|
|
def __init__(self, prefix):
|
|
self.prefix = prefix
|
|
|
|
def __delitem__(self, item):
|
|
"""
|
|
Removing a key means deleting a file from disk.
|
|
"""
|
|
item = slugify(item, self.prefix)
|
|
path = "%s/%s" % (FLAGS.datastore_path, item)
|
|
if os.path.isfile(path):
|
|
os.remove(path)
|
|
|
|
def __getitem__(self, item):
|
|
"""
|
|
Fetch file contents and dejsonify them.
|
|
"""
|
|
item = slugify(item, self.prefix)
|
|
path = "%s/%s" % (FLAGS.datastore_path, item)
|
|
if os.path.isfile(path):
|
|
return json.load(open(path, 'r'))
|
|
return None
|
|
|
|
def __setitem__(self, item, value):
|
|
"""
|
|
JSON encode value and save to file.
|
|
"""
|
|
item = slugify(item, self.prefix)
|
|
path = "%s/%s" % (FLAGS.datastore_path, item)
|
|
with open(path, "w") as blobfile:
|
|
blobfile.write(json.dumps(value))
|
|
return value
|
|
|
|
|
|
class RedisKeeper(object):
|
|
"""
|
|
Simple dictionary class that persists using
|
|
ReDIS.
|
|
"""
|
|
def __init__(self, prefix="redis-"):
|
|
self.prefix = prefix
|
|
Redis.instance().ping()
|
|
|
|
def __setitem__(self, item, value):
|
|
"""
|
|
JSON encode value and save to file.
|
|
"""
|
|
item = slugify(item, self.prefix)
|
|
Redis.instance().set(item, json.dumps(value))
|
|
return value
|
|
|
|
def __getitem__(self, item):
|
|
item = slugify(item, self.prefix)
|
|
value = Redis.instance().get(item)
|
|
if value:
|
|
return json.loads(value)
|
|
|
|
def __delitem__(self, item):
|
|
item = slugify(item, self.prefix)
|
|
return Redis.instance().delete(item)
|
|
|
|
def clear(self):
|
|
raise NotImplementedError()
|
|
|
|
def clear_all(self):
|
|
raise NotImplementedError()
|
|
|
|
def set_add(self, item, value):
|
|
item = slugify(item, self.prefix)
|
|
return Redis.instance().sadd(item, json.dumps(value))
|
|
|
|
def set_is_member(self, item, value):
|
|
item = slugify(item, self.prefix)
|
|
return Redis.instance().sismember(item, json.dumps(value))
|
|
|
|
def set_remove(self, item, value):
|
|
item = slugify(item, self.prefix)
|
|
return Redis.instance().srem(item, json.dumps(value))
|
|
|
|
def set_fetch(self, item):
|
|
item = slugify(item, self.prefix)
|
|
for obj in Redis.instance().sinter([item]):
|
|
yield json.loads(obj)
|
|
|
|
|
|
def Keeper(prefix=''):
|
|
KEEPERS = {'redis': RedisKeeper,
|
|
'sqlite': SqliteKeeper}
|
|
return KEEPERS[FLAGS.keeper_backend](prefix)
|
|
|