Introduced policies to resolve nodes by its role

NullResolver the fake resolver
PatternBasedRoleResolver allows to use pattern in name of role

implements blueprint: task-based-deployment-astute

Change-Id: I5bfb135fe95ed8faee6df81e31748e0143c568e6
This commit is contained in:
Bulat Gaifullin 2015-12-10 15:57:10 +03:00
parent f2aec0a6c7
commit eea95621e0
3 changed files with 274 additions and 0 deletions

View File

@ -440,4 +440,9 @@ OPENSTACK_CONFIG_TYPES = Enum(
'node',
)
NODE_RESOLVE_POLICY = Enum(
"all",
"any"
)
OVERRIDE_CONFIG_BASE_PATH = '/etc/hiera/override/configuration/'

View File

@ -0,0 +1,102 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Mirantis, Inc.
#
# 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.
import mock
import six
from nailgun import consts
from nailgun.test.base import BaseUnitTest
from nailgun.utils import role_resolver
class TestNameMatchPolicy(BaseUnitTest):
def test_exact_match(self):
match_policy = role_resolver.NameMatchPolicy.create("controller")
self.assertIsInstance(match_policy, role_resolver.ExactMatch)
self.assertTrue(match_policy.match("controller"))
self.assertFalse(match_policy.match("controller1"))
def test_pattern_match(self):
match_policy = role_resolver.NameMatchPolicy.create("/controller/")
self.assertIsInstance(match_policy, role_resolver.PatternMatch)
self.assertTrue(match_policy.match("controller"))
self.assertTrue(match_policy.match("controller1"))
class TestPatternBasedRoleResolver(BaseUnitTest):
@classmethod
def setUpClass(cls):
cls.roles_of_nodes = [
["primary-controller"],
["cinder"],
["controller", "compute"],
["controller", "cinder"],
["compute"],
]
cls.nodes = [
mock.MagicMock(uid=str(i))
for i in six.moves.range(len(cls.roles_of_nodes))
]
def setUp(self):
objs_mock = mock.patch('nailgun.utils.role_resolver.objects').start()
objs_mock.Node.all_roles.side_effect = self.roles_of_nodes
self.addCleanup(objs_mock.stop)
def test_resolve_by_pattern(self):
resolver = role_resolver.RoleResolver(self.nodes)
self.assertItemsEqual(
["0", "2", "3"],
resolver.resolve(["/.*controller/"])
)
self.assertItemsEqual(
["2", "3"],
resolver.resolve(["controller"])
)
self.assertItemsEqual(
["1", "2", "3", "4"],
resolver.resolve(["/c.+/"])
)
def test_resolve_all(self):
resolver = role_resolver.RoleResolver(self.nodes)
self.assertItemsEqual(
(x.uid for x in self.nodes),
resolver.resolve("*")
)
def test_resolve_master(self):
resolver = role_resolver.RoleResolver(self.nodes)
self.assertEqual(
[consts.MASTER_ROLE],
resolver.resolve(consts.MASTER_ROLE)
)
def test_resolve_any(self):
resolver = role_resolver.RoleResolver(self.nodes)
all_nodes = resolver.resolve("*", consts.NODE_RESOLVE_POLICY.all)
any_node = resolver.resolve("*", consts.NODE_RESOLVE_POLICY.any)
self.assertEqual(1, len(any_node))
self.assertIn(any_node[0], all_nodes)
class TestNullResolver(BaseUnitTest):
def test_resolve(self):
node_ids = ['1', '2', '3']
self.assertIs(
node_ids,
role_resolver.NullResolver(node_ids).resolve("controller")
)

View File

@ -0,0 +1,167 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Mirantis, Inc.
#
# 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.
import abc
from collections import defaultdict
import re
import six
from nailgun import consts
from nailgun.logger import logger
from nailgun import objects
@six.add_metaclass(abc.ABCMeta)
class NameMatchPolicy(object):
@abc.abstractmethod
def match(self, name):
"""Tests that name is acceptable.
:param name: the name to test
:type name: str
:returns: True if yes otherwise False
"""
@staticmethod
def create(pattern):
"""Makes name match policy.
the string wrapped with '/' treats as pattern
'/abc/' - pattern
'abc' - the string for exact match
:param pattern: the pattern to match
:return: the NameMatchPolicy instance
"""
if pattern.startswith("/") and pattern.endswith("/"):
return PatternMatch(pattern[1:-1])
return ExactMatch(pattern)
class ExactMatch(NameMatchPolicy):
"""Tests that name exact match to argument."""
def __init__(self, name):
"""Initializes.
:param name: the name to match
"""
self.name = name
def match(self, name):
return self.name == name
class PatternMatch(NameMatchPolicy):
"""Tests that pattern matches to argument."""
def __init__(self, patten):
self.pattern = re.compile(patten)
def match(self, name):
return self.pattern.match(name)
@six.add_metaclass(abc.ABCMeta)
class BaseRoleResolver(object):
"""Helper class to find nodes by role."""
@abc.abstractmethod
def resolve(self, roles, policy=None):
"""Resolve roles to IDs of nodes.
:param roles: the required roles
:type roles: list|str
:param policy: the policy to filter the list of resolved nodes
can be any|all
any means need to return any node from resolved
all means need to return all resolved nodes
:type policy: str
:return: the list of nodes
"""
class NullResolver(BaseRoleResolver):
"""The implementation of RoleResolver
that returns only specified IDs.
"""
def __init__(self, nodes_ids):
self.nodes_ids = nodes_ids
def resolve(self, roles, policy=None):
return self.nodes_ids
class RoleResolver(BaseRoleResolver):
"""The general role resolver.
Allows to use patterns in name of role
"""
# the mapping roles, those are resolved to known list of IDs
# master is used to run tasks on master node
SPECIAL_ROLES = {
consts.MASTER_ROLE: [consts.MASTER_ROLE]
}
def __init__(self, nodes):
"""Initializes.
:param nodes: the sequence of node objects
"""
self.__mapping = defaultdict(set)
for node in nodes:
for r in objects.Node.all_roles(node):
self.__mapping[r].add(node.uid)
def resolve(self, roles, policy=None):
if isinstance(roles, six.string_types) and roles in self.SPECIAL_ROLES:
result = self.SPECIAL_ROLES[roles]
elif roles == consts.ALL_ROLES:
result = list(set(
uid for nodes in six.itervalues(self.__mapping)
for uid in nodes
))
elif isinstance(roles, (list, tuple)):
result = set()
for role in roles:
pattern = NameMatchPolicy.create(role)
for node_role, nodes_ids in six.iteritems(self.__mapping):
if pattern.match(node_role):
result.update(nodes_ids)
result = list(result)
else:
# TODO(fix using wrong format for roles in tasks.yaml)
# After it will be allowed to raise exception here
logger.warn(
'Wrong roles format, `roles` should be a list or "*": %s',
roles
)
return []
# in some cases need only one any node from pool
# for example if need only one any controller.
# to distribute load select first node from pool
if result and policy == consts.NODE_RESOLVE_POLICY.any:
result = result[0:1]
logger.debug(
"Role '%s' and policy '%s' was resolved to: %s",
roles, policy, result
)
return result