zaqar/zaqar/storage/redis/pools.py

266 lines
9.4 KiB
Python

# Copyright (c) 2017 ZTE Corporation.
#
# 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.
"""pools: an implementation of the pool management storage
controller for redis.
Schema:
'n': name :: six.text_type
'u': uri :: six.text_type
'w': weight :: int
'o': options :: dict
"""
import functools
import msgpack
from oslo_log import log as logging
import redis
from zaqar.common import utils as common_utils
from zaqar.storage import base
from zaqar.storage import errors
from zaqar.storage.redis import utils
LOG = logging.getLogger(__name__)
class PoolsController(base.PoolsBase):
"""Implements Pools resource operations using Redis.
* All pool (Redis sorted set):
Set of all pool_ids, ordered by name. Used to delete the all
records of table pools.
Key: pools
+--------+-----------------------------+
| Id | Value |
+========+=============================+
| name | <pool> |
+--------+-----------------------------+
* Flavor Index (Redis sorted set):
Set of all pool_ids for the given flavor, ordered by name.
Key: <flavor>.pools
+--------+-----------------------------+
| Id | Value |
+========+=============================+
| name | <pool> |
+--------+-----------------------------+
* Pools Information (Redis hash):
Key: <pool>.pools
+----------------------+---------+
| Name | Field |
+======================+=========+
| pool | pl |
+----------------------+---------+
| uri | u |
+----------------------+---------+
| weight | w |
+----------------------+---------+
| options | o |
+----------------------+---------+
| flavor | f |
+----------------------+---------+
"""
def __init__(self, *args, **kwargs):
super(PoolsController, self).__init__(*args, **kwargs)
self._client = self.driver.connection
self.flavor_ctl = self.driver.flavors_controller
self._packer = msgpack.Packer(encoding='utf-8',
use_bin_type=True).pack
self._unpacker = functools.partial(msgpack.unpackb, encoding='utf-8')
@utils.raises_conn_error
@utils.retries_on_connection_error
def _list(self, marker=None, limit=10, detailed=False):
client = self._client
set_key = utils.pools_set_key()
marker_key = utils.pools_name_hash_key(marker)
rank = client.zrank(set_key, marker_key)
start = rank + 1 if rank is not None else 0
cursor = (f for f in client.zrange(set_key, start,
start + limit - 1))
marker_next = {}
def normalizer(pools):
marker_next['next'] = pools['pl']
return self._normalize(pools, detailed=detailed)
yield utils.PoolsListCursor(self._client, cursor, normalizer)
yield marker_next and marker_next['next']
@utils.raises_conn_error
@utils.retries_on_connection_error
def _get(self, name, detailed=False):
pool_key = utils.pools_name_hash_key(name)
pool = self._client.hgetall(pool_key)
if pool is None or len(pool) == 0:
raise errors.PoolDoesNotExist(name)
return self._normalize(pool, detailed)
@utils.raises_conn_error
@utils.retries_on_connection_error
def _get_pools_by_flavor(self, flavor=None, detailed=False):
cursor = None
if flavor is None or flavor.get('name') is None:
set_key = utils.pools_set_key()
cursor = (pl for pl in self._client.zrange(set_key, 0, -1))
elif flavor.get('name') is not None:
subset_key = utils.pools_subset_key(flavor['name'])
cursor = (pl for pl in self._client.zrange(subset_key, 0, -1))
if cursor is None:
return []
normalizer = functools.partial(self._normalize, detailed=detailed)
return utils.PoolsListCursor(self._client, cursor, normalizer)
@utils.raises_conn_error
@utils.retries_on_connection_error
def _create(self, name, weight, uri, group=None, flavor=None,
options=None):
if group is not None:
raise errors.PoolRedisNotSupportGroup
flavor = flavor if flavor is not None else None
options = {} if options is None else options
pool_key = utils.pools_name_hash_key(name)
subset_key = utils.pools_subset_key(flavor)
set_key = utils.pools_set_key()
if self._exists(name):
self._update(name, weight=weight, uri=uri,
flavor=flavor, options=options)
return
pool = {
'pl': name,
'u': uri,
'w': weight,
'o': self._packer(options),
'f': flavor
}
# Pipeline ensures atomic inserts.
with self._client.pipeline() as pipe:
pipe.zadd(set_key, 1, pool_key)
if flavor is not None:
pipe.zadd(subset_key, 1, pool_key)
pipe.hmset(pool_key, pool)
pipe.execute()
@utils.raises_conn_error
@utils.retries_on_connection_error
def _exists(self, name):
pool_key = utils.pools_name_hash_key(name)
return self._client.exists(pool_key)
@utils.raises_conn_error
@utils.retries_on_connection_error
def _update(self, name, **kwargs):
names = ('uri', 'weight', 'flavor', 'options')
fields = common_utils.fields(kwargs, names,
pred=lambda x: x is not None,
key_transform=lambda x: x[0])
assert fields, ('`weight`, `uri`, `flavor`, '
'or `options` not found in kwargs')
if 'o' in fields:
new_options = fields.get('o', None)
fields['o'] = self._packer(new_options)
pool_key = utils.pools_name_hash_key(name)
# (gengchc2): Pipeline ensures atomic inserts.
with self._client.pipeline() as pipe:
# (gengchc2): If flavor is changed, we need to change.pool key
# in pools subset.
if 'f' in fields:
flavor_old = self._get(name).get('flavor')
flavor_new = fields['f']
if flavor_old != flavor_new:
if flavor_new is not None:
new_subset_key = utils.pools_subset_key(flavor_new)
pipe.zadd(new_subset_key, 1, pool_key)
# (gengchc2) remove pool from flavor_old.pools subset
if flavor_old is not None:
old_subset_key = utils.pools_subset_key(flavor_old)
pipe.zrem(old_subset_key, pool_key)
pipe.hmset(pool_key, fields)
pipe.execute()
@utils.raises_conn_error
@utils.retries_on_connection_error
def _delete(self, name):
try:
pool = self.get(name)
flavor = pool.get("flavor", None)
# NOTE(gengchc2): If this is the only pool in the
# flavor and it's being used by a flavor, don't allow
# it to be deleted.
if flavor is not None:
flavor1 = {}
flavor1['name'] = flavor
pools_in_flavor = list(self.get_pools_by_flavor(
flavor=flavor1))
if self.flavor_ctl.exists(flavor)\
and len(pools_in_flavor) == 1:
raise errors.PoolInUseByFlavor(name, flavor)
pool_key = utils.pools_name_hash_key(name)
subset_key = utils.pools_subset_key(flavor)
set_key = utils.pools_set_key()
with self._client.pipeline() as pipe:
if flavor is not None:
pipe.zrem(subset_key, pool_key)
pipe.zrem(set_key, pool_key)
pipe.delete(pool_key)
pipe.execute()
except errors.PoolDoesNotExist:
pass
@utils.raises_conn_error
@utils.retries_on_connection_error
def _drop_all(self):
poolsobj_key = self._client.keys(pattern='*pools')
if len(poolsobj_key) == 0:
return
with self._client.pipeline() as pipe:
for key in poolsobj_key:
pipe.delete(key)
try:
pipe.execute()
except redis.exceptions.ResponseError:
return False
def _normalize(self, pool, detailed=False):
ret = {
'name': pool['pl'],
'uri': pool['u'],
'weight': int(pool['w']),
'flavor': pool['f']
}
if detailed:
ret['options'] = self._unpacker(pool['o'])
return ret