nova/nova/datastore.py

255 lines
8.0 KiB
Python

# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# 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:
MAKE Sure that ReDIS is running, and your flags are set properly,
before trying to run this.
"""
import logging
import redis
from nova import exception
from nova import flags
from nova import utils
FLAGS = flags.FLAGS
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')
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 ConnectionError(exception.Error):
pass
def absorb_connection_error(fn):
def _wrapper(*args, **kwargs):
try:
return fn(*args, **kwargs)
except redis.exceptions.ConnectionError, ce:
raise ConnectionError(str(ce))
return _wrapper
class BasicModel(object):
"""
All Redis-backed data derives from this class.
You MUST specify an identifier() property that returns a unique string
per instance.
You MUST have an initializer that takes a single argument that is a value
returned by identifier() to load a new class with.
You may want to specify a dictionary for default_state().
You may also specify override_type at the class left to use a key other
than __class__.__name__.
You override save and destroy calls to automatically build and destroy
associations.
"""
override_type = None
@absorb_connection_error
def __init__(self):
self.initial_state = {}
self.state = Redis.instance().hgetall(self.__redis_key)
if self.state:
self.initial_state = self.state
else:
self.state = self.default_state()
def default_state(self):
"""You probably want to define this in your subclass"""
return {}
@classmethod
def _redis_name(cls):
return self.override_type or cls.__name__
@classmethod
def lookup(cls, identifier):
rv = cls(identifier)
if rv.is_new_record():
return None
else:
return rv
@classmethod
@absorb_connection_error
def all(cls):
"""yields all objects in the store"""
redis_set = cls._redis_set_name(cls.__name__)
for identifier in Redis.instance().smembers(redis_set):
yield cls(identifier)
@classmethod
@absorb_connection_error
def associated_to(cls, foreign_type, foreign_id):
redis_set = cls._redis_association_name(foreign_type, foreign_id)
for identifier in Redis.instance().smembers(redis_set):
yield cls(identifier)
@classmethod
def _redis_set_name(cls, kls_name):
# stupidly pluralize (for compatiblity with previous codebase)
return kls_name.lower() + "s"
@classmethod
def _redis_association_name(cls, foreign_type, foreign_id):
return cls._redis_set_name("%s:%s:%s" %
(foreign_type, foreign_id, cls.__name__))
@property
def identifier(self):
"""You DEFINITELY want to define this in your subclass"""
raise NotImplementedError("Your subclass should define identifier")
@property
def __redis_key(self):
return '%s:%s' % (self.__class__.__name__.lower(), self.identifier)
def __repr__(self):
return "<%s:%s>" % (self.__class__.__name__, self.identifier)
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 update(self, update_dict):
return self.state.update(update_dict)
def setdefault(self, item, default):
return self.state.setdefault(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, models NEED all their properties.")
def is_new_record(self):
return self.initial_state == {}
@absorb_connection_error
def add_to_index(self):
set_name = self.__class__._redis_set_name(self.__class__.__name__)
Redis.instance().sadd(set_name, self.identifier)
@absorb_connection_error
def remove_from_index(self):
set_name = self.__class__._redis_set_name(self.__class__.__name__)
Redis.instance().srem(set_name, self.identifier)
@absorb_connection_error
def remove_from_index(self):
set_name = self.__class__._redis_set_name(self.__class__.__name__)
Redis.instance().srem(set_name, self.identifier)
@absorb_connection_error
def associate_with(self, foreign_type, foreign_id):
# note the extra 's' on the end is for plurality
# to match the old data without requiring a migration of any sort
self.add_associated_model_to_its_set(foreign_type, foreign_id)
redis_set = self.__class__._redis_association_name(foreign_type,
foreign_id)
Redis.instance().sadd(redis_set, self.identifier)
@absorb_connection_error
def unassociate_with(self, foreign_type, foreign_id):
redis_set = self.__class__._redis_association_name(foreign_type,
foreign_id)
Redis.instance().srem(redis_set, self.identifier)
def add_associated_model_to_its_set(self, my_type, my_id):
table = globals()
klsname = my_type.capitalize()
if table.has_key(klsname):
my_class = table[klsname]
my_inst = my_class(my_id)
my_inst.save()
else:
logging.warning("no model class for %s when building"
" association from %s",
klsname, self)
@absorb_connection_error
def save(self):
"""
update the directory with the state from this model
also add it to the index of items of the same type
then set the initial_state = state so new changes are tracked
"""
# TODO(ja): implement hmset in redis-py and use it
# instead of multiple calls to hset
if self.is_new_record():
self["create_time"] = utils.isotime()
for key, val in self.state.iteritems():
Redis.instance().hset(self.__redis_key, key, val)
self.add_to_index()
self.initial_state = self.state
return True
@absorb_connection_error
def destroy(self):
"""deletes all related records from datastore."""
logging.info("Destroying datamodel for %s %s",
self.__class__.__name__, self.identifier)
Redis.instance().delete(self.__redis_key)
self.remove_from_index()
return True