From 09b10046475ca327464bfc62d02ae1f92453b8b3 Mon Sep 17 00:00:00 2001 From: tengqm Date: Fri, 26 Dec 2014 13:52:17 +0800 Subject: [PATCH 01/59] Initial version Initial version that documents our vocabulary. --- doc/source/glossary.rst | 133 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 doc/source/glossary.rst diff --git a/doc/source/glossary.rst b/doc/source/glossary.rst new file mode 100644 index 000000000..f2f9737c8 --- /dev/null +++ b/doc/source/glossary.rst @@ -0,0 +1,133 @@ +.. + 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. + +========== + Glossary +========== + +.. glossary:: + :sorted: + + Action + An action is an operation that can be performed on a :term:`Cluster`, a + :term:`Node, a :term:`Policy`, etc. Different types of objects support + different set of actions. An action is executed by a :term:`Worker` thread + when the action becomes ready. Most Senlin APIs create actions in database + for worker threads to execute asynchronously. An action, when executed, + will check and enforce :term:`Policy` associated with the cluster. An + action can be triggered via :term:`Webhook`. + + API server + HTTP REST API service for Senlin. + + Cluster + A cluster is a group of homogeneous objects (i.e. :term:`Node`s). A + cluster consists of 0 or more nodes and it can be associated with 0 or + more :term:`Policy` objects. It is associated with a :term:`Profile Type` + when created. + + Dependency + The :term:`Action` objects are stored into database for execution. These + actions may have dependencies among them. + + Driver + A driver is a Senlin internal module that enables Senlin :term:`Engine` to + interact with other :term:`OpenStack` services. The interactions here are + usually used to create, destroy, update the objects exposed by those + services. + + Environment + Used to specify user provided :term:`Plugin` that implement a + :term:`Profile Type` or a :term:`Policy Type'. User can provide plugins + that override the default plugins by customizing an environment. + + Event + An event is a record left in Senlin database when something matters to + users happened. A event can be of different criticality levels. + + Index + An integer property of a :term:`Node` when it is a member of a + :term:`Cluster`. Each node has an auto-generated index value that is + unique in the cluster. + + Nested Cluster + A cluster that serves a member of another :term:`Cluster`. + + Node + A node is an object that belongs to at most one :term:`Cluster`. A node + can become an 'orphaned node' when it is not a member of any clusters. + All nodes in a cluster must be of the same :term:`Profile Type` of the + owning cluster. In general, a node represents a physical object exposed + by other OpenStack services. A node has a unique :term:`Index` value + scoped to the cluster that owns it. + + Permission + A string dictating which user (role or group) has what permissions on a + given object (i.e. :term:`Cluster`, :term:`Node`, :term:`Profile` and + :term:`Policy` etc.) + + Plugin + A plugin is an implementation of a :term:`Policy Type` or :term:`Profile + Type` that can be dynamically loaded and registered to Senlin engine. + Senlin engine comes with a set of builtin plugins. Users can add their own + plugins by customizing the :term:`Environment` configuration. + + Policy + A policy is a set of rules that can be checked and/or enforced when an + :term:`Action` is performed on a :term:`Cluster`. A policy is an instance + of a particular :term:`Policy Type`. Users can specify the enforcement + level when creating a policy object. Such a policy object can be attached + to and detached from a cluster. + + Policy Type + A policy type is an abstraction of :term:`Policy` objects. The + implementation of a policy type specifies when the policy should be + checked and/or enforce, what profile types are supported, what operations + are to be done before, during and after each :term:`Action`. All policy + types are provided as Senlin plugins. + + Profile + A profile is a mould used for creating objects (i.e. :term:`Node`). A + profile is an instance of a :term:`Profile Type` with all required + information specified. Each profile has an unique ID. As a guideline, a + profile cannot be updated once created. To change a profile, you have to + create a new instance. + + Profile Type + A profile type is an abstraction of objects that are backed by some + :term:`Driver`s. The implementation of a profile type calls the driver(s) + to create objects that are managed by Senlin. The implementation also + serves a factory that can 'produce' objects given a profile. All profile + types are provided as Senlin plugins. + + Role + A role is a string property that can be assigned to a :term:`Node`. + Nodes in the same cluster may assume a role for certain reason such as + application configuration. The default role for a node is empty. + + OpenStack + Open source software for building private and public clouds. + + Webhook + A webhook is an encoded URI (Universion Resource Identifier) that + encapsulates a tuple (user, object, action), where the user is a Keystone + entity that initiates an action and the object is a specific + :term:`Cluster`, a :term:`Node` or a :term:`Policy` etc. The action item + specifies an :term:`Action` to be triggered. Such a Webhook is the only + thing one needs to know to trigger an action on an object in Senlin. + + Worker + A worker is the thread created and managed by Senlin engine to execute + an :term:`Action` that becomes ready. When the current action completes + (with a success or failure), a worker will check the database to find + another action for execution. From ae923454aca31ca07bb4f31c687420d16df32f43 Mon Sep 17 00:00:00 2001 From: tengqm Date: Fri, 26 Dec 2014 14:36:03 +0800 Subject: [PATCH 02/59] Renamed some exceptions --- senlin/common/exception.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/senlin/common/exception.py b/senlin/common/exception.py index 34834a215..7b93d071b 100644 --- a/senlin/common/exception.py +++ b/senlin/common/exception.py @@ -211,7 +211,11 @@ class ClusterExists(SenlinException): msg_fmt = _("The Cluster (%(cluster_name)s) already exists.") -class ClusterValidationFailed(SenlinException): +class ProfileValidationFailed(SenlinException): + msg_fmt = _("%(message)s") + + +class PolicyValidationFailed(SenlinException): msg_fmt = _("%(message)s") From fbe83d97a42d4201f78f458d0be81dd7d4a891b9 Mon Sep 17 00:00:00 2001 From: tengqm Date: Fri, 26 Dec 2014 15:15:33 +0800 Subject: [PATCH 03/59] Added some optimization to YAML loader --- senlin/engine/parser.py | 87 ++++++++++++++++++++++++----------------- 1 file changed, 52 insertions(+), 35 deletions(-) diff --git a/senlin/engine/parser.py b/senlin/engine/parser.py index 65ef9e8e8..fe38a914c 100644 --- a/senlin/engine/parser.py +++ b/senlin/engine/parser.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import json import six import yaml @@ -19,55 +20,71 @@ from senlin.openstack.common import log as logging _LE = i18n._LE LOG = logging.getLogger(__name__) - # Try LibYAML if available -try: - Loader = yaml.CLoader - Dumper = yaml.CDumper -except ImportError as err: - Loader = yaml.Loader - Dumper = yaml.Dumper +if hasattr(yaml, 'CSafeLoader'): + YamlLoader = yaml.CSafeLoader +else: + YamlLoader = yaml.SafeLoader + +if hasattr(yaml, 'CSafeDumper'): + YamlDumper = yaml.CSafeDumper +else: + YamlDumper = yaml.SafeDumper -def parse_profile(profile): +def _construct_yaml_str(self, node): + # Override the default string handling function + # to always return unicode objects + return self.construct_scalar(node) + +YamlLoader.add_constructor(u'tag:yaml.org,2002:str', _construct_yaml_str) + +# Unquoted dates in YAML files get loaded as objects of type datetime.date +# which may cause problems in API layer. Therefore, make unicode string +# out of timestamps until openstack.common.jsonutils can handle dates. +YamlLoader.add_constructor(u'tag:yaml.org,2002:timestamp', _construct_yaml_str) + + +def simple_parse(in_str): + try: + out_dict = json.loads(in_str) + except ValueError: + try: + out_dict = yaml.load(in_str, Loader=YamlLoader) + except yaml.YAMLError as yea: + yea = six.text_type(yea) + msg = _('Error parsing input: %s') % yea + raise ValueError(msg) + else: + if out_dict is None: + out_dict = {} + + if not isinstance(out_dict, dict): + msg = _('The input is not a JSON object or YAML mapping.') + raise ValueError(msg) + + return out_dict + + +def parse_profile(profile_str): ''' Parse and validate the specified string as a profile. ''' - if not isinstance(profile, six.string_types): - # TODO(Qiming): Throw exception - return None + data = simple_parse(profile_str) - data = {} - try: - data = yaml.load(profile, Loader=Loader) - except Exception as ex: - # TODO(Qiming): Throw exception - LOG.error(_LE('Failed parsing given data as YAML: %s'), - six.text_type(ex)) - return None - - # TODO(Qiming): Construct a profile object based on the type specified + # TODO(Qiming): + # Construct a profile object based on the type specified return data -def parse_policy(policy): +def parse_policy(policy_str): ''' Parse and validate the specified string as a policy. ''' - if not isinstance(policy, six.string_types): - # TODO(Qiming): Throw exception - return None + data = simple_parse(policy_str) - data = {} - try: - data = yaml.load(policy, Loader=Loader) - except Exception as ex: - # TODO(Qiming): Throw exception - LOG.error(_LE('Failed parsing given data as YAML: %s'), - six.text_type(ex)) - return None - - # TODO(Qiming): Construct a policy object based on the type specified + # TODO(Qiming): + # Construct a policy object based on the type specified return data From a8099a5739c63a0267b09400b421db9d3f630dc3 Mon Sep 17 00:00:00 2001 From: tengqm Date: Fri, 26 Dec 2014 19:59:41 +0800 Subject: [PATCH 04/59] Remove this file for refactoring --- senlin/engine/profile_mgr.py | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 senlin/engine/profile_mgr.py diff --git a/senlin/engine/profile_mgr.py b/senlin/engine/profile_mgr.py deleted file mode 100644 index 5a9532b4b..000000000 --- a/senlin/engine/profile_mgr.py +++ /dev/null @@ -1,30 +0,0 @@ -# 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 uuid - - -class ProfileRegistry(object): - ''' - A registry for profile types. - ''' - def __init__(self, **extra_paths): - self._registry = {'profiles': {}} - - -class ProfileManager(object): - ''' - A ProfileManager manages the profile intance database. - ''' - - def __init__(self, **extra_paths): - pass From 6729a3d64de27eefba6c3b3f2427e5dcde4d43a2 Mon Sep 17 00:00:00 2001 From: tengqm Date: Sat, 27 Dec 2014 18:22:41 +0800 Subject: [PATCH 05/59] Replaced README.rst with README.md --- README.rst | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 README.rst diff --git a/README.rst b/README.rst deleted file mode 100644 index 3ba6852d8..000000000 --- a/README.rst +++ /dev/null @@ -1,22 +0,0 @@ -====== -SENLIN -====== - -Senlin is a service to orchestrate multiple composite cloud applications using -templates. - -Getting Started ---------------- - -If you'd like to run from the master branch, you can clone the git repo: - - git clone git@github.com:openstack/senlin.git - - -* Wiki: http://wiki.openstack.org/Senlin -* Developer docs: http://docs.openstack.org/developer/senlin - - -Python client -------------- -https://github.com/openstack/python-senlinclient From aa473eb910baf8fa8bd01853f22548a28437c55d Mon Sep 17 00:00:00 2001 From: tengqm Date: Sat, 27 Dec 2014 18:30:20 +0800 Subject: [PATCH 06/59] Initial version A registry is responsible for managing a type of plugins, e.g. profile plugins or policy plugins. In the global environment (global_env), there will be two registries each responsible for profiles and policies management. --- senlin/engine/registry.py | 167 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 senlin/engine/registry.py diff --git a/senlin/engine/registry.py b/senlin/engine/registry.py new file mode 100644 index 000000000..a489c51f3 --- /dev/null +++ b/senlin/engine/registry.py @@ -0,0 +1,167 @@ +# 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 itertools +import six + +from senlin.common.i18n import _LI +from senlin.common.i18n import _LW +from senlin.engine import environment +from senlin.openstack.common import log + +LOG = log.getLogger(__name__) + + +class PluginInfo(object): + ''' + Base mapping of plugin type to implementation. + ''' + def __new__(cls, registry, name, plugin, **kwargs): + ''' + Create a new PluginInfo of the appropriate class. + Placeholder for class hierarchy extensibility + ''' + return super(PluginInfo, cls).__new__(cls) + + def __init__(self, registry, name, plugin): + self.registry = registry + self.name = name + self.plugin = plugin + self.user_provided = True + + def __eq__(self, other): + if other is None: + return False + return (self.name == other.name and + self.plugin == other.plugin and + self.user_provided == other.user_provided) + + def __ne__(self, other): + return not self.__eq__(other) + + def __lt__(self, other): + if self.user_provided != other.user_provided: + # user provided ones must be sorted above system ones. + return self.user_provided > other.user_provided_ + if len(self.name) != len(other.name): + # more specific (longer) name must be sorted above system ones. + return len(self.name) > len(other.name) + return self.name < other.name + + def __gt__(self, other): + return other.__lt__(self) + + def __str__(self): + return '[Plugin](User:%s) %s -> %s' % (self.user_provided, + self.name, str(self.plugin)) + + +class Registry(object): + ''' + A registry for managing profile or policy classes. + ''' + + def __init__(self, registry_name, is_global): + self._registry = {registry_name: {}} + self.is_global = is_global + global_registry = environment.global_env().registry + self.global_registry = None if is_global else global_registry + + def _register_info(self, path, info): + ''' + place the new info in the correct location in the registry. + + :param path: a list of keys ['profiles', 'my_stack', 'os.heat.stack'], + or ['policies', 'my_policy', 'ScalingPolicy'] + :param info: reference to a PluginInfo data structure, deregister a + PluginInfo if specified as None. + ''' + descriptive_path = '/'.join(path) + name = path[-1] + # create the structure if needed + registry = self._registry + for key in path[:-1]: + if key not in registry: + registry[key] = {} + registry = registry[key] + + if info is None: + # delete this entry. + LOG.warn(_LW('Removing %(item)s from %(path)s'), { + 'item': name, 'path': descriptive_path}) + registry.pop(name, None) + return + + if name in registry and isinstance(registry[name], PluginInfo): + if registry[name] == info: + return + details = { + 'path': descriptive_path, + 'old': str(registry[name].value), + 'new': str(info.value) + } + LOG.warn(_LW('Changing %(path)s from %(old)s to %(new)s'), details) + else: + LOG.info(_LI('Registering %(path)s -> %(value)s'), { + 'path': descriptive_path, 'value': str(info.value)}) + + info.user_provided = self.user_env + registry[name] = info + + def register_plugin(self, name, plugin): + pi = PluginInfo(self, [name], plugin) + self._register_info([name], pi) + + def _load_registry(self, path, registry): + for k, v in iter(registry.items()): + path = path + [k] + if v is None: + self._register_info(path, None) + elif isinstance(v, dict): + self._load_registry(path, v) + else: + info = PluginInfo(self, path, v) + self._register_info(path, info) + + def load(self, json_snippet): + self._load_registry([], json_snippet) + + def iterable_by(self, name): + plugin = self._registry.get(name) + if plugin: + yield plugin + + def get_plugin(self, name): + giter = [] + if self.user_env: + giter = self.global_registry.iterable_by(name) + + matches = itertools.chain(self.iterable_by(name), giter) + info = sorted(matches) + return info.plugin if info else None + + def as_dict(self): + """Return profiles in a dict format.""" + def _as_dict(level): + tmp = {} + for k, v in iter(level.items()): + if isinstance(v, dict): + tmp[k] = _as_dict(v) + elif v.user_provided: + tmp[k] = v.value + return tmp + + return _as_dict(self._registry) + + def get_types(self): + '''Return a list of valid profile types.''' + return [name for name in six.iteritems(self._registry)] From d5fc91b0919277fbf5045f397b6aa543ce355d64 Mon Sep 17 00:00:00 2001 From: tengqm Date: Sat, 27 Dec 2014 18:36:49 +0800 Subject: [PATCH 07/59] Initial version An environment encapsulates the runtime settings to a Selin cluster. Currently, the only settings stored there is the plugins that implement policies or profiles. --- senlin/engine/environment.py | 203 +++++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 senlin/engine/environment.py diff --git a/senlin/engine/environment.py b/senlin/engine/environment.py new file mode 100644 index 000000000..69bda003a --- /dev/null +++ b/senlin/engine/environment.py @@ -0,0 +1,203 @@ +# 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 glob +import os.path +from stevedore import extension + +from oslo.config import cfg + +from senlin.common import exception +from senlin.common.i18n import _ +from senlin.common.i18n import _LE +from senlin.common.i18n import _LI +from senlin.engine import clients +from senlin.engine import parser +from senlin.engine import registry +from senlin.openstack.common import log + + +LOG = log.getLogger(__name__) + +_environment = None + + +def global_env(): + if _environment is None: + initialize() + return _environment + + +class Environment(object): + ''' + An object that contains all profiles, policies and customizations. + ''' + SECTIONS = ( + PARAMETERS, CUSTOM_PROFILES, CUSTOM_POLICIES, + ) = ( + 'parameters', 'custom_profiles', 'custom_policies', + ) + + def __init__(self, env=None, is_global=False): + ''' + Create an Environment from a dict. + + :param env: the json environment + :param is_global: boolean indicating if this is a user created one. + ''' + self.params = {} + self.profile_registry = registry.Registry('profiles', is_global) + self.policy_registry = registry.Registry('policies', is_global) + + if env is None: + env = {} + else: + # Merge user specified keys with current environment + self.params = env.get(self.PARAMETERS, {}) + custom_profiles = env.get(self.CUSTOM_PROFILES, {}) + custom_policies = env.get(self.CUSTOM_POLICIES, {}) + self.profile_registry.load(custom_profiles) + self.policy_registry.load(custom_policies) + + def parse(self, env_str): + ''' + Parse a string format environment file into a dictionary. + ''' + if env_str is None: + return {} + + env = parser.simple_parse(env_str) + + # Check unknown sections + for sect in env: + if sect not in self.SECTIONS: + msg = _('environment has unknown section "%s"') % sect + raise ValueError(msg) + + # Fill in default values for missing sections + for sect in self.SECTIONS: + if sect not in env: + env[sect] = {} + + return env + + def load(self, env_dict): + ''' + Load environment from the given dictionary. + ''' + self.params.update(env_dict.get(self.PARAMETERS, {})) + self.profile_registry.load(env_dict.get(self.CUSTOM_PROFILES, {})) + self.policy_registry.load(env_dict.get(self.CUSTOM_POLICIES, {})) + + def _check_profile_type_name(self, name): + if name == "" or name is None: + msg = _('Profile type name not specified') + raise exception.ProfileValidationFailed(message=msg) + elif not isinstance(name, six.string_types): + msg = _('Profile type name is not a string') + raise exception.ProfileValidationFailed(message=msg) + + def register_profile(self, name, plugin): + self._check_profile_type_name(name) + self.profile_registry.register_plugin(name, plugin) + + def get_profile(self, name): + self._check_profile_type_name(name) + plugin = self.profile_registry.get_plugin(name) + if plugin is None: + msg = _("Unknown profile type : %s") % name + raise exception.ProfileValidationFailed(message=msg) + return plugin + + def get_profile_types(self): + return self.profile_registry.get_types() + + def _check_policy_type_name(self, name): + if name == "" or name is None: + msg = _('Policy type name not specified') + raise exception.PolicyValidationFailed(message=msg) + elif not isinstance(name, six.string_types): + msg = _('Policy type name is not a string') + raise exception.PolicyValidationFailed(message=msg) + + def register_policy(self, name, plugin): + self._check_policy_type_name(name) + self.policy_registry.register_plugin(name, plugin) + + def get_policy(self, name): + self._check_policy_type_name(name) + plugin = self.policy_registry.get_plugin(name) + if plugin is None: + msg = _("Unknown policy type : %s") % name + raise exception.PolicyValidationFailed(message=msg) + return plugin + + def get_policy_types(self): + return self.policy_registry.get_types() + + def read_global_environment(self): + ''' + Read and parse global enviroment files. + ''' + cfg.CONF.import_opt('environment_dir', 'senlin.common.config') + env_dir = cfg.CONF.environment_dir + + try: + files = glob.glob(os.path.join(env_dir, '*')) + except OSError as ex: + LOG.error(_LE('Failed to read %s'), env_dir) + LOG.exception(ex) + return + + for fname in files: + try: + with open(fname) as f: + LOG.info(_LI('Loading environment from %s'), fname) + self.load(self.parse(f.read())) + except ValueError as vex: + LOG.error(_LE('Failed to parse %s'), fname) + LOG.exception(vex) + except IOError as ioex: + LOG.error(_LE('Failed to read %s'), fname) + LOG.exception(ioex) + + +def initialize(): + + global _environment + + def _get_mapping(namespace): + mgr = extension.ExtensionManager( + namespace=namespace, + invoke_on_load=False, + verify_requirements=True) + return [[name, mgr[name].plugin] for name in mgr.names()] + + if _environment is not None: + return + + # TODO(Qiming): Check when to initialize clients if needed + clients.initialise() + + env = Environment(is_global=True) + + # Register global plugins when initialized + entries = _get_mapping('senlin.profiles') + for name, plugin in entries: + env.register_profile(name, plugin) + + entries = _get_mapping('senlin.policies') + for name, plugin in entries: + env.register_policy(name, plugin) + + env.read_global_environment() + _environment = env From db29298dad36415a8d20da43ad4d6ce6cd56f3de Mon Sep 17 00:00:00 2001 From: tengqm Date: Sat, 27 Dec 2014 20:08:52 +0800 Subject: [PATCH 08/59] Added support to '!include' mechanism The '!include' idiom is used to include an external YAML file into current one. Currently, it supports protocol like 'file', 'http' and 'https'(not tested). --- senlin/engine/parser.py | 58 ++++++++++++++++++++++++++++++++--------- 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/senlin/engine/parser.py b/senlin/engine/parser.py index fe38a914c..dd826f748 100644 --- a/senlin/engine/parser.py +++ b/senlin/engine/parser.py @@ -11,7 +11,10 @@ # under the License. import json +import os +import requests import six +from six.moves import urllib import yaml from senlin.common import i18n @@ -22,27 +25,56 @@ LOG = logging.getLogger(__name__) # Try LibYAML if available if hasattr(yaml, 'CSafeLoader'): - YamlLoader = yaml.CSafeLoader + Loader = yaml.CSafeLoader else: - YamlLoader = yaml.SafeLoader + Loader = yaml.SafeLoader if hasattr(yaml, 'CSafeDumper'): - YamlDumper = yaml.CSafeDumper + Dumper = yaml.CSafeDumper else: - YamlDumper = yaml.SafeDumper + Dumper = yaml.SafeDumper -def _construct_yaml_str(self, node): - # Override the default string handling function - # to always return unicode objects - return self.construct_scalar(node) +class YamlLoader(Loader): + def __init__(self, stream): + self._curdir = os.path.split(stream.name)[0] + super(YamlLoader, self).__init__(stream) -YamlLoader.add_constructor(u'tag:yaml.org,2002:str', _construct_yaml_str) + def include(self, node): + url = self.construct_scalar(node) + components = urllib.parse.urlparse(url) -# Unquoted dates in YAML files get loaded as objects of type datetime.date -# which may cause problems in API layer. Therefore, make unicode string -# out of timestamps until openstack.common.jsonutils can handle dates. -YamlLoader.add_constructor(u'tag:yaml.org,2002:timestamp', _construct_yaml_str) + if components.scheme == '': + try: + url = os.path.join(self._curdir, url) + with open(url, 'r') as f: + return yaml.load(f, Loader) + except Exception as ex: + raise Exception('Failed loading file %s: %s' % (url, + six.text_type(ex))) + try: + resp = requests.get(url, stream=True) + resp.raise_for_status() + reader = resp.iter_content(chunk_size=1024) + result = '' + for chunk in reader: + result += chunk + return yaml.load(result, Loader) + except Exception as ex: + raise Exception('Failed retrieving file %s: %s' % (url, + six.text_type(ex))) + + def process_unicode(self, node): + # Override the default string handling function to always return + # unicode objects + return self.construct_scalar(node) + + +YamlLoader.add_constructor('!include', YamlLoader.include) +YamlLoader.add_constructor(u'tag:yaml.org,2002:str', + YamlLoader.process_unicode) +YamlLoader.add_constructor(u'tag:yaml.org,2002:timestamp', + YamlLoader.process_unicode) def simple_parse(in_str): From dd43a6b195156385dc026c8d9f2554740fffe513 Mon Sep 17 00:00:00 2001 From: tengqm Date: Mon, 29 Dec 2014 14:45:27 +0800 Subject: [PATCH 09/59] WIP change to stack profile example. Senlin parser will support '!include' constructor for users to compose a larger YAML file from different ones. --- .../profiles/heat_stack_random_string.yaml | 18 +++++++++--------- examples/profiles/node_stack.yaml | 12 ++++++++++++ 2 files changed, 21 insertions(+), 9 deletions(-) create mode 100644 examples/profiles/node_stack.yaml diff --git a/examples/profiles/heat_stack_random_string.yaml b/examples/profiles/heat_stack_random_string.yaml index 0358e211c..86f6f4c67 100644 --- a/examples/profiles/heat_stack_random_string.yaml +++ b/examples/profiles/heat_stack_random_string.yaml @@ -1,12 +1,12 @@ +senlin_profile_version: 2014-10-16 type: os.heat.stack spec: template: - heat_template_version: 2014-10-16 - resources: - random: - type: OS::Heat::RandomString - properties: - length: 64 - outputs: - result: - value: {get_attr: [random, value]} + !include node_stack.yaml + context: default + parameters: + len: 16 + outputs: + name: node-%index% + timeoout: 60 + enable_rollback: True diff --git a/examples/profiles/node_stack.yaml b/examples/profiles/node_stack.yaml new file mode 100644 index 000000000..e099bf3f8 --- /dev/null +++ b/examples/profiles/node_stack.yaml @@ -0,0 +1,12 @@ +heat_template_version: 2014-10-16 +parameters: + len: + type: integer +resources: + random: + type: OS::Heat::RandomString + properties: + length: 64 +outputs: + result: + value: {get_attr: [random, value]} From 5504c35e9b2cc46530f6a2a5db180f82e28a95f4 Mon Sep 17 00:00:00 2001 From: tengqm Date: Mon, 29 Dec 2014 14:54:44 +0800 Subject: [PATCH 10/59] Revised --- TODO | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/TODO b/TODO index d9a0d488c..7b5a8c033 100644 --- a/TODO +++ b/TODO @@ -13,7 +13,11 @@ DB ENGINE ------ - cleanse scheduler module - - complete parser logic + - complete parser logic, construct profile/policy objects there? + +DRIVER +------ + - complete Heat stack driver [Qiming] Middle Priority @@ -21,15 +25,19 @@ Middle Priority DB -- + - Add test cases for policy_delete with 'force' set to True -- Add test cases for policy_delete with 'force' set to True +ENGINE +------ + - Design and implement dynamical plugin loading mechanism that allows + loading plugins from any paths OSLO ---- -- Migrate to oslo.log -- Migrate to oslo.context + - Migrate to oslo.log + - Migrate to oslo.context Low Priority @@ -37,6 +45,8 @@ Low Priority TEST ---- - -- Add test case in db cluster to test that cluster-policy association is - deleted when we delete a cluster + - Add test case in db cluster to test that cluster-policy association is + deleted when we delete a cluster + - Add test case to engine/parser + - Add test case to engine/registry + - Add test case to engine/environment From 88ad5355b03f816adfd9d093aded3960307bc02f Mon Sep 17 00:00:00 2001 From: tengqm Date: Mon, 29 Dec 2014 15:05:07 +0800 Subject: [PATCH 11/59] Initial version --- Changelog | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Changelog diff --git a/Changelog b/Changelog new file mode 100644 index 000000000..24bfe380e --- /dev/null +++ b/Changelog @@ -0,0 +1,2 @@ +2014-12-29 tengqm + * TODO: Added some test cases jobs. From 86a807815d98814f054bf553293e3b84a3c69ccd Mon Sep 17 00:00:00 2001 From: tengqm Date: Mon, 29 Dec 2014 16:59:17 +0800 Subject: [PATCH 12/59] Fixed comment error. A comment error was preventing python to parse this file. --- senlin/engine/scheduler.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/senlin/engine/scheduler.py b/senlin/engine/scheduler.py index 651f87677..2dbba90e8 100644 --- a/senlin/engine/scheduler.py +++ b/senlin/engine/scheduler.py @@ -528,19 +528,17 @@ class PollingTaskGroup(object): def runAction(action): -""" + ''' Start a thread to run action until finished -""" - # TODO - # Query lock for this action - + ''' + # TODO(Yanyan): Query lock for this action # call action.execute with args in subthread pass + def wait(handle): -""" + ''' Wait an action to finish -""" - # TODO - # Make the subthread join the main thread + ''' + # TODO(Yanyan): Make the subthread join the main thread pass From 3846c37f510530682e29356f21edb99510da1b82 Mon Sep 17 00:00:00 2001 From: tengqm Date: Mon, 29 Dec 2014 17:15:49 +0800 Subject: [PATCH 13/59] Fixed cluster db model (important!!!) This patch adds a new 'node_count' integer field to the cluster table. The newly added field will be used for checking cluster status from DB layer. This patch also changes the 'profile_id' field to be 'profile_type', this is because nodes in a cluster may have different versions of the same profile type while the cluster should only care about the profile type instead of a specific version. Another fix is to the syntax error found in API. --- senlin/db/sqlalchemy/api.py | 2 +- .../sqlalchemy/migrate_repo/versions/001_first_version.py | 4 ++-- senlin/db/sqlalchemy/models.py | 6 +++--- senlin/tests/db/test_cluster_api.py | 3 ++- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/senlin/db/sqlalchemy/api.py b/senlin/db/sqlalchemy/api.py index 965d79fb7..4c8f30113 100644 --- a/senlin/db/sqlalchemy/api.py +++ b/senlin/db/sqlalchemy/api.py @@ -761,7 +761,7 @@ def action_mark_succeeded(context, action_id): action.status = ACTION_SUCCEEDED - for a in action.depended_by + for a in action.depended_by: action_del_depends_on(context, a, action_id) action.depended_by = [] diff --git a/senlin/db/sqlalchemy/migrate_repo/versions/001_first_version.py b/senlin/db/sqlalchemy/migrate_repo/versions/001_first_version.py index cea378795..0c37917de 100644 --- a/senlin/db/sqlalchemy/migrate_repo/versions/001_first_version.py +++ b/senlin/db/sqlalchemy/migrate_repo/versions/001_first_version.py @@ -39,8 +39,7 @@ def upgrade(migrate_engine): nullable=False), sqlalchemy.Column('name', sqlalchemy.String(255), nullable=False), - sqlalchemy.Column('profile_id', sqlalchemy.String(36), - sqlalchemy.ForeignKey('profile.id'), + sqlalchemy.Column('profile_type', sqlalchemy.String(255), nullable=False), sqlalchemy.Column('user', sqlalchemy.String(36)), sqlalchemy.Column('project', sqlalchemy.String(36)), @@ -49,6 +48,7 @@ def upgrade(migrate_engine): sqlalchemy.Column('created_time', sqlalchemy.DateTime), sqlalchemy.Column('updated_time', sqlalchemy.DateTime), sqlalchemy.Column('deleted_time', sqlalchemy.DateTime), + sqlalchemy.Column('node_count', sqlalchemy.Integer), sqlalchemy.Column('next_index', sqlalchemy.Integer), sqlalchemy.Column('timeout', sqlalchemy.Integer), sqlalchemy.Column('status', sqlalchemy.String(255)), diff --git a/senlin/db/sqlalchemy/models.py b/senlin/db/sqlalchemy/models.py index 54aa59cdc..3a28eca75 100644 --- a/senlin/db/sqlalchemy/models.py +++ b/senlin/db/sqlalchemy/models.py @@ -88,9 +88,8 @@ class Cluster(BASE, SenlinBase, SoftDelete): id = sqlalchemy.Column('id', sqlalchemy.String(36), primary_key=True, default=lambda: str(uuid.uuid4())) name = sqlalchemy.Column('name', sqlalchemy.String(255)) - profile_id = sqlalchemy.Column(sqlalchemy.String(36), - sqlalchemy.ForeignKey('profile.id'), - nullable=False) + profile_type = sqlalchemy.Column(sqlalchemy.String(255), + nullable=False) user = sqlalchemy.Column(sqlalchemy.String(36)) project = sqlalchemy.Column(sqlalchemy.String(36)) domain = sqlalchemy.Column(sqlalchemy.String(36)) @@ -98,6 +97,7 @@ class Cluster(BASE, SenlinBase, SoftDelete): created_time = sqlalchemy.Column(sqlalchemy.DateTime) updated_time = sqlalchemy.Column(sqlalchemy.DateTime) deleted_time = sqlalchemy.Column(sqlalchemy.DateTime) + node_count = sqlalchemy.Column(sqlalchemy.Integer) next_index = sqlalchemy.Column(sqlalchemy.Integer) timeout = sqlalchemy.Column(sqlalchemy.Integer) status = sqlalchemy.Column(sqlalchemy.String(255)) diff --git a/senlin/tests/db/test_cluster_api.py b/senlin/tests/db/test_cluster_api.py index 3154428b1..ed1db2d65 100644 --- a/senlin/tests/db/test_cluster_api.py +++ b/senlin/tests/db/test_cluster_api.py @@ -31,11 +31,12 @@ class DBAPIClusterTest(base.SenlinTestCase): cluster = shared.create_cluster(self.ctx, self.profile) self.assertIsNotNone(cluster.id) self.assertEqual('db_test_cluster_name', cluster.name) - self.assertEqual(self.profile.id, cluster.profile_id) + self.assertEqual(self.profile.type, cluster.profile_type) self.assertEqual(self.ctx.username, cluster.user) self.assertEqual(self.ctx.tenant_id, cluster.project) self.assertEqual('unknown', cluster.domain) self.assertIsNone(cluster.parent) + self.assertEqual(0, cluster.node_count) self.assertEqual(0, cluster.next_index) self.assertEqual('60', cluster.timeout) self.assertEqual('INIT', cluster.status) From 0fa2d87ba2572853c98261592cf7eb37c88e81f1 Mon Sep 17 00:00:00 2001 From: tengqm Date: Mon, 29 Dec 2014 17:19:30 +0800 Subject: [PATCH 14/59] Fixed error when parsing a stream from memory Previous version mistakenly treats all streams as file objects, but in real life usage, that might not be true. --- senlin/engine/parser.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/senlin/engine/parser.py b/senlin/engine/parser.py index dd826f748..d847f85b8 100644 --- a/senlin/engine/parser.py +++ b/senlin/engine/parser.py @@ -37,7 +37,10 @@ else: class YamlLoader(Loader): def __init__(self, stream): - self._curdir = os.path.split(stream.name)[0] + if isinstance(stream, file): + self._curdir = os.path.split(stream.name)[0] + else: + self._curdir = './' super(YamlLoader, self).__init__(stream) def include(self, node): From a1112b3d4c2f79740022fcb35ce506d48489ec4b Mon Sep 17 00:00:00 2001 From: tengqm Date: Mon, 29 Dec 2014 17:29:48 +0800 Subject: [PATCH 15/59] Revert profile reference revision I think I made a mistake. A cluster, when created, must refer to an existing version of a specific profile, i.e. a profile.id. This is required because later on a user may directly request a resize operation on the cluster. Such a resize request won't carry any profile id information, so it must come from the cluster itself. In addition to this, the profile implies a type info already. In certain cases, it can be treated as the 'desired version' of a profile type, for 'convergence'. --- .../db/sqlalchemy/migrate_repo/versions/001_first_version.py | 3 ++- senlin/db/sqlalchemy/models.py | 5 +++-- senlin/tests/db/shared.py | 1 + senlin/tests/db/test_cluster_api.py | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/senlin/db/sqlalchemy/migrate_repo/versions/001_first_version.py b/senlin/db/sqlalchemy/migrate_repo/versions/001_first_version.py index 0c37917de..4f7d13000 100644 --- a/senlin/db/sqlalchemy/migrate_repo/versions/001_first_version.py +++ b/senlin/db/sqlalchemy/migrate_repo/versions/001_first_version.py @@ -39,7 +39,8 @@ def upgrade(migrate_engine): nullable=False), sqlalchemy.Column('name', sqlalchemy.String(255), nullable=False), - sqlalchemy.Column('profile_type', sqlalchemy.String(255), + sqlalchemy.Column('profile_id', sqlalchemy.String(36), + sqlalchemy.ForeignKey('profile.id'), nullable=False), sqlalchemy.Column('user', sqlalchemy.String(36)), sqlalchemy.Column('project', sqlalchemy.String(36)), diff --git a/senlin/db/sqlalchemy/models.py b/senlin/db/sqlalchemy/models.py index 3a28eca75..11155222e 100644 --- a/senlin/db/sqlalchemy/models.py +++ b/senlin/db/sqlalchemy/models.py @@ -88,8 +88,9 @@ class Cluster(BASE, SenlinBase, SoftDelete): id = sqlalchemy.Column('id', sqlalchemy.String(36), primary_key=True, default=lambda: str(uuid.uuid4())) name = sqlalchemy.Column('name', sqlalchemy.String(255)) - profile_type = sqlalchemy.Column(sqlalchemy.String(255), - nullable=False) + profile_id = sqlalchemy.Column(sqlalchemy.String(36), + sqlalchemy.ForeignKey('profile.id'), + nullable=False) user = sqlalchemy.Column(sqlalchemy.String(36)) project = sqlalchemy.Column(sqlalchemy.String(36)) domain = sqlalchemy.Column(sqlalchemy.String(36)) diff --git a/senlin/tests/db/shared.py b/senlin/tests/db/shared.py index 30ea1f405..db0a06b75 100644 --- a/senlin/tests/db/shared.py +++ b/senlin/tests/db/shared.py @@ -67,6 +67,7 @@ def create_cluster(ctx, profile, **kwargs): 'project': ctx.tenant_id, 'domain': 'unknown', 'parent': None, + 'node_count': 0, 'next_index': 0, 'timeout': '60', 'status': 'INIT', diff --git a/senlin/tests/db/test_cluster_api.py b/senlin/tests/db/test_cluster_api.py index ed1db2d65..abe69b448 100644 --- a/senlin/tests/db/test_cluster_api.py +++ b/senlin/tests/db/test_cluster_api.py @@ -31,7 +31,7 @@ class DBAPIClusterTest(base.SenlinTestCase): cluster = shared.create_cluster(self.ctx, self.profile) self.assertIsNotNone(cluster.id) self.assertEqual('db_test_cluster_name', cluster.name) - self.assertEqual(self.profile.type, cluster.profile_type) + self.assertEqual(self.profile.id, cluster.profile_id) self.assertEqual(self.ctx.username, cluster.user) self.assertEqual(self.ctx.tenant_id, cluster.project) self.assertEqual('unknown', cluster.domain) From 5584b749fc60ab8f723ddc76d9698fd2fea025a2 Mon Sep 17 00:00:00 2001 From: tengqm Date: Mon, 29 Dec 2014 17:34:57 +0800 Subject: [PATCH 16/59] Revised --- Changelog | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Changelog b/Changelog index 24bfe380e..f9215a34b 100644 --- a/Changelog +++ b/Changelog @@ -1,2 +1,7 @@ +2014-12-29 tengqm + * db/sqlalchemy/models.py: added 'node_count' to cluster class. + * db/sqlalchemy/migrate_repo/versions/001_first_version.py: + added 'node_count' to cluster class. + 2014-12-29 tengqm * TODO: Added some test cases jobs. From af4c7b6993be7f526904921f9239b5f7ddcf5054 Mon Sep 17 00:00:00 2001 From: tengqm Date: Tue, 30 Dec 2014 10:43:23 +0800 Subject: [PATCH 17/59] Remove action_update interface Remove direct update logic from db to avoid out-of-band action status changes. --- senlin/db/api.py | 4 ---- senlin/db/sqlalchemy/api.py | 11 ----------- 2 files changed, 15 deletions(-) diff --git a/senlin/db/api.py b/senlin/db/api.py index d3041115e..a29d9e069 100644 --- a/senlin/db/api.py +++ b/senlin/db/api.py @@ -279,10 +279,6 @@ def action_start_work_on(context, action_id, owner): return IMPL.action_start_work_on(context, action_id, owner) -def action_update(context, action_id, values): - return IMPL.action_update(context, action_id, values) - - def db_sync(engine, version=None): """Migrate the database to `version` or the most recent version.""" return IMPL.db_sync(engine, version=version) diff --git a/senlin/db/sqlalchemy/api.py b/senlin/db/sqlalchemy/api.py index 4c8f30113..b09833b8c 100644 --- a/senlin/db/sqlalchemy/api.py +++ b/senlin/db/sqlalchemy/api.py @@ -793,17 +793,6 @@ def action_start_work_on(context, action_id, owner): return action -def action_update(context, action_id, values): - #TODO(liuh):Need check if 'status' is being updated? - action = model_query(context, models.Action).get(action_id) - if not action: - raise exception.NotFound( - _('Action with id "%s" not found') % action_id) - - action.update(values) - action.save(_session(context)) - return action - # Utils def db_sync(engine, version=None): """Migrate the database to `version` or the most recent version.""" From 20993eafb06aee6765d23aa66f16e9deeefca932 Mon Sep 17 00:00:00 2001 From: tengqm Date: Tue, 30 Dec 2014 10:47:29 +0800 Subject: [PATCH 18/59] Revised --- Changelog | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Changelog b/Changelog index f9215a34b..f574fccd2 100644 --- a/Changelog +++ b/Changelog @@ -1,3 +1,7 @@ +2014-12-30 tengqm + * db/api.py: remove action_update interface function to allow + stricter checking of action status changes. + 2014-12-29 tengqm * db/sqlalchemy/models.py: added 'node_count' to cluster class. * db/sqlalchemy/migrate_repo/versions/001_first_version.py: From c54f39a9c21a51f2d64f781390dcefd34a3349a2 Mon Sep 17 00:00:00 2001 From: tengqm Date: Tue, 30 Dec 2014 11:50:54 +0800 Subject: [PATCH 19/59] Add 'ActionNotSupport' exception --- senlin/common/exception.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/senlin/common/exception.py b/senlin/common/exception.py index 7b93d071b..5d6bcef69 100644 --- a/senlin/common/exception.py +++ b/senlin/common/exception.py @@ -270,6 +270,10 @@ class RequestLimitExceeded(SenlinException): msg_fmt = _('Request limit exceeded: %(message)s') +class ActionNotSupported(SenlinException): + msg_fmt = _('Action "%(action)s" not supported by %(object)s') + + class ActionInProgress(SenlinException): msg_fmt = _("Cluster %(cluster_name)s already has an action (%(action)s) " "in progress.") From 8d58688638245e0ce8c31db29f244397e4a69c14 Mon Sep 17 00:00:00 2001 From: tengqm Date: Tue, 30 Dec 2014 12:08:56 +0800 Subject: [PATCH 20/59] WIP version for Yanyan to continue work on --- senlin/engine/cluster.py | 59 ++++++++++++++---------- senlin/engine/node.py | 47 +++++++++++++++++--- senlin/engine/service.py | 89 +++++++++++++++++-------------------- senlin/engine/thread_mgr.py | 84 +++++++++++++++++++++------------- 4 files changed, 167 insertions(+), 112 deletions(-) diff --git a/senlin/engine/cluster.py b/senlin/engine/cluster.py index 732573e8c..d0fb3635d 100644 --- a/senlin/engine/cluster.py +++ b/senlin/engine/cluster.py @@ -35,7 +35,7 @@ class Cluster(object): 'INIT', 'ACTIVE', 'ERROR', 'DELETED', 'UPDATING', ) - def __init__(self, name, profile, size=0, **kwargs): + def __init__(self, name, profile_id, size=0, **kwargs): ''' Intialize a cluster object. The cluster defaults to have 0 nodes with no profile assigned. @@ -43,23 +43,26 @@ class Cluster(object): self.name = name self.profile_id = profile_id - self.user = kwargs.get('user') - self.project = kwargs.get('project') - self.domain = kwargs.get('domain') - + self.user = kwargs.get('user', '') + self.project = kwargs.get('project', '') + self.domain = kwargs.get('domain', '') self.parent = kwargs.get('parent') self.created_time = None self.updated_time = None self.deleted_time = None + # node_count is only the 'current count', not the desired count + # (i.e. size) + self.desired_size = size + self.node_count = 0 self.next_index = 0 - self.timeout = 0 + self.timeout = kwargs.get('timeout', 0) - self.status = '' - self.status_reason = '' + self.status = self.INIT + self.status_reason = 'Initializing' self.data = {} - self.tags = {} + self.tags = kwargs.get('tags', {}) # persist object into database very early because: # 1. object creation may be a time consuming task @@ -68,12 +71,21 @@ class Cluster(object): db_api.create_cluster(self) # rt is a dict for runtime data - self.rt = dict(size=size, - nodes={}, + self.rt = dict(nodes={}, policies={}) def _set_status(self, context, status): - pass + now = datetime.datetime.utcnow() + if status == self.ACTIVE: + if self.status == self.INIT: + kwargs['created_time'] = now + else: + kwargs['updated_time'] = now + elif status == self.DELETED: + kwargs['deleted_time'] = now + + kwargs['status'] = status + db_api.cluster_update(self.context, {'status': status}) #event.info(context, self.id, status, # log status to log file # generate event record @@ -109,7 +121,7 @@ class Cluster(object): node_list = self.get_nodes() for n in node_list: - node = nodes.Node(None, profile_id, cluster_id) + node = nodes.Node(None, profile.id, cluster_id) action = actions.NodeAction(context, node, 'UPDATE', **kwargs) # start a thread asynchronously @@ -190,7 +202,7 @@ class Cluster(object): message = _('No cluster exists with id "%s"') % str(cluster_id) raise exception.NotFound(message) - return cls._from_db(context, cluster) + return cls.from_db(context, cluster) @classmethod def load_all(cls, context, limit=None, marker=None, sort_keys=None, @@ -200,29 +212,28 @@ class Cluster(object): sort_dir, filters, tenant_safe, show_deleted, show_nested) or [] for cluster in clusters: - yield cls._from_db(context, cluster) + yield cls.from_db(context, cluster) @classmethod - def _from_db(cls, context, cluster): - # TODO: calculate current size based on nodes - size = self.size - return cls(context, cluster.name, cluster.profile, size, + def from_db(cls, context, cluster): + # TODO(Qiming): calculate current size based on nodes + return cls(context, cluster.name, cluster.profile, id=cluster.id, status=cluster.status, - status_reason=cluster_status_reason, + status_reason=cluster.status_reason, parent=cluster.parent, project=cluster.project, created_time=cluster.created_time, updated_time=cluster.updated_time, deleted_time=cluster.deleted_time, - domain = cluster.domain, - timeout = cluster.timeout, + domain=cluster.domain, + timeout=cluster.timeout, user=cluster.user) def to_dict(self): info = { rpc_api.CLUSTER_NAME: self.name, rpc_api.CLUSTER_PROFILE: self.profile, - rpc_api.CLUSTER_SIZE: self.size, + rpc_api.CLUSTER_SIZE: self.node_count, rpc_api.CLUSTER_UUID: self.id, rpc_api.CLUSTER_PARENT: self.parent, rpc_api.CLUSTER_DOMAIN: self.domain, @@ -235,5 +246,5 @@ class Cluster(object): rpc_api.CLUSTER_STATUS_REASON: self.status_reason, rpc_api.CLUSTER_TIMEOUT: self.timeout, } - + return info diff --git a/senlin/engine/node.py b/senlin/engine/node.py index 97ec0dcc3..b297ad727 100644 --- a/senlin/engine/node.py +++ b/senlin/engine/node.py @@ -26,20 +26,21 @@ class Node(object): ''' statuses = ( - ACTIVE, ERROR, DELETED, UPDATING, + INIT, ACTIVE, ERROR, DELETED, UPDATING, ) = ( - 'ACTIVE', 'ERROR', 'DELETED', 'UPDATING', + 'INITIALIZING', 'ACTIVE', 'ERROR', 'DELETED', 'UPDATING', ) def __init__(self, name, profile_id, cluster_id=None, **kwargs): + self.id = None if name: self.name = name else: # TODO # Using self.physical_resource_name() to generate a unique name self.name = 'node-name-tmp' - self.physical_id = None - self.cluster_id = cluster_id + self.physical_id = '' + self.cluster_id = cluster_id or '' self.profile_id = profile_id if cluster_id is None: self.index = -1 @@ -51,11 +52,41 @@ class Node(object): self.updated_time = None self.deleted_time = None - self.status = self.ACTIVE - self.status_reason = 'Initialized' + self.status = self.INIT + self.status_reason = 'Initializing' self.data = {} self.tags = {} - # TODO: store this to database + self.store() + + def store(self): + ''' + Store the node record into database table. + + The invocation of DB API could be a node_create or a node_update, + depending on whether node has an ID assigned. + ''' + + values = { + 'name': self.name, + 'physical_id': self.physical_id, + 'cluster_id': self.cluster_id, + 'profile_id': self.profile_id, + 'index': self.index, + 'role': self.role, + 'created_time': self.created_time, + 'updated_time': self.updated_time, + 'deleted_time': self.deleted_time, + 'status': self.status, + 'status_reason': self.status_reason, + 'data': self.data, + 'tags': self.tags, + } + + if self.id: + db_api.node_update(self.context, self.id, values) + else: + node = db_api.node_create(self.context, values) + self.id = node.id def create(self, name, profile_id, cluster_id=None, **kwargs): # TODO: invoke profile to create new object and get the physical id @@ -65,6 +96,8 @@ class Node(object): def delete(self): node = db_api.get_node(self.id) + physical_id = node.physical_id + # TODO: invoke profile to delete this object # TODO: check if actions are working on it and can be canceled diff --git a/senlin/engine/service.py b/senlin/engine/service.py index 565cb5a54..806e7c7b3 100644 --- a/senlin/engine/service.py +++ b/senlin/engine/service.py @@ -58,7 +58,7 @@ class EngineListener(service.Service): def __init__(self, host, engine_id, thread_group_mgr): super(EngineListener, self).__init__() - self.thread_group_mgr = thread_group_mgr + self.TG = thread_group_mgr self.engine_id = engine_id self.host = host @@ -78,10 +78,10 @@ class EngineListener(service.Service): def stop_cluster(self, ctxt, cluster_id): '''Stop any active threads on a cluster.''' - self.thread_group_mgr.stop(cluster_id) + self.TG.stop(cluster_id) def send(self, ctxt, cluster_id, message): - self.thread_group_mgr.send(cluster_id, message) + self.TG.send(cluster_id, message) @profiler.trace_cls("rpc") @@ -100,20 +100,21 @@ class EngineService(service.Service): def __init__(self, host, topic, manager=None): super(EngineService, self).__init__() + # TODO(Qiming): call environment.initialize() when environment + # is ready self.host = host self.topic = topic # The following are initialized here, but assigned in start() which # happens after the fork when spawning multiple worker processes self.engine_id = None - self.thread_group_mgr = None + self.TG= None self.target = None def start(self): self.engine_id = senlin_lock.BaseLock.generate_engine_id() - self.thread_group_mgr = ThreadGroupManager() - self.listener = EngineListener(self.host, self.engine_id, - self.thread_group_mgr) + self.TG = ThreadGroupManager() + self.listener = EngineListener(self.host, self.engine_id, self.TG) LOG.debug("Starting listener for engine %s" % self.engine_id) self.listener.start() @@ -135,14 +136,14 @@ class EngineService(service.Service): pass # Wait for all active threads to be finished - for cluster_id in self.thread_group_mgr.groups.keys(): + for cluster_id in self.TG.groups.keys(): # Ignore dummy service task if cluster_id == cfg.CONF.periodic_interval: continue LOG.info(_LI("Waiting cluster %s processing to be finished"), cluster_id) # Stop threads gracefully - self.thread_group_mgr.stop(cluster_id, True) + self.TG.stop(cluster_id, True) LOG.info(_LI("cluster %s processing was finished"), cluster_id) # Terminate the engine process @@ -245,39 +246,29 @@ class EngineService(service.Service): return {'clusters': clusters_info} @request_context - def create_cluster(self, cnxt, cluster_name, size, profile, - owner_id=None, nested_depth=0, user_creds_id=None, - cluster_user_project_id=None): - """ + def create_cluster(self, cntxt, name, profile_id, size, args): + ''' Handle request to perform a create action on a cluster - :param cnxt: RPC context. - :param cluster_name: Name of the cluster you want to create. - :param size: Size of cluster you want to create. - :param profile: Profile used to create cluster nodes. - :param owner_id: parent cluster ID for nested clusters, only - expected when called from another senlin-engine - (not a user option) - :param nested_depth: the nested depth for nested clusters, only - expected when called from another senlin-engine - :param user_creds_id: the parent user_creds record for nested clusters - :param cluster_user_project_id: the parent cluster_user_project_id for - nested clusters - """ - LOG.info(_LI('Creating cluster %s'), cluster_name) + :param cntxt: RPC context. + :param name: Name of the cluster to created. + :param profile_id: Profile used to create cluster nodes. + :param size: Desired size of cluster to be created. + :param args: A dictionary of other parameters + ''' + LOG.info(_LI('Creating cluster %s'), name) - # TODO: construct real kwargs based on input for cluster creating - kwargs = {} - kwargs['owner_id'] = owner_id - kwargs['nested_depth'] = nested_depth - kwargs['user_creds_id'] = user_creds_id - kwargs['cluster_user_project_id'] = cluster_user_project_id + kwargs = { + 'parent': args.get('parent', ''), + 'user': cntxt.get('username', ''), + 'project': cntxt.get('tenant_id', ''), + 'timeout': args.get('timeout', 0), + 'tags': args.get('tags', {}), + } - cluster = clusters.Cluster(cluster_name, size, profile, **kwargs) - action = actions.ClusterAction(cnxt, cluster, 'CREATE', **kwargs) - - self.thread_group_mgr.start_with_lock(cnxt, cluster, 'cluster', - self.engine_id, action.execute) + cluster = clusters.Cluster(name, profile_id, size, **kwargs) + action = actions.Action(cnxt, cluster, 'CREATE', **kwargs) + self.TG.start_action_woker(action, self.engine_id) return cluster.id @@ -304,12 +295,12 @@ class EngineService(service.Service): msg = _('Updating a cluster which has been deleted') raise exception.NotSupported(feature=msg) - kwargs = {} - kwargs['profile'] = profile - action = actions.ClusterAction(cnxt, cluster, 'UPDATE', **kwargs) + kwargs = { + 'profile_id': profile_id + } - self.thread_group_mgr.start_with_lock(cnxt, cluster, 'cluster', - self.engine_id, action.execute) + action = actions.Action(cnxt, cluster, 'UPDATE', **kwargs) + self.TG.start_action_worker(action, self.engine_id) return cluster.id @@ -332,9 +323,9 @@ class EngineService(service.Service): # Successfully acquired lock if acquire_result is None: - self.thread_group_mgr.stop_timers(cluster.id) + self.TG.stop_timers(cluster.id) action = actions.ClusterAction(cnxt, cluster, 'DELETE') - self.thread_group_mgr.start_with_acquired_lock(cluster, lock, + self.TG.start_with_acquired_lock(cluster, lock, action.execute) return @@ -343,7 +334,7 @@ class EngineService(service.Service): # give threads which are almost complete an opportunity to # finish naturally before force stopping them eventlet.sleep(0.2) - self.thread_group_mgr.stop(cluster.id) + self.TG.stop(cluster.id) # Another active engine has the lock elif senlin_lock.ClusterLock.engine_alive(cnxt, acquire_result): stop_result = self._remote_call( @@ -363,7 +354,7 @@ class EngineService(service.Service): cluster = clusters.Cluster.load(cnxt, cluster=db_cluster) action = actions.ClusterAction(cnxt, cluster, 'DELETE') - self.thread_group_mgr.start_with_lock(cnxt, cluster, 'cluster', + self.TG.start_with_lock(cnxt, cluster, 'cluster', self.engine_id, action.execute) return None @@ -380,7 +371,7 @@ class EngineService(service.Service): cluster = clusters.Cluster.load(cnxt, cluster=db_cluster) action = actions.ClusterAction(cnxt, cluster, 'SUSPEND') - self.thread_group_mgr.start_with_lock(cnxt, cluster, 'cluster', + self.TG.start_with_lock(cnxt, cluster, 'cluster', self.engine_id, action.execute) @request_context @@ -394,7 +385,7 @@ class EngineService(service.Service): cluster = clusters.Cluster.load(cnxt, cluster=db_cluster) action = actions.ClusterAction(cnxt, cluster, 'RESUME') - self.thread_group_mgr.start_with_lock(cnxt, cluster, 'cluster', + self.TG.start_with_lock(cnxt, cluster, 'cluster', self.engine_id, action.execute) def _remote_call(self, cnxt, lock_engine_id, call, *args, **kwargs): diff --git a/senlin/engine/thread_mgr.py b/senlin/engine/thread_mgr.py index ac3f4f536..30428f021 100644 --- a/senlin/engine/thread_mgr.py +++ b/senlin/engine/thread_mgr.py @@ -1,15 +1,14 @@ -# -# 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 +# 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. +# 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 collections import os @@ -40,42 +39,26 @@ class ThreadGroupManager(object): # Create dummy service task, because when there is nothing queued # on self.tg the process exits self.add_timer(cfg.CONF.periodic_interval, self._service_task) - + def _service_task(self): - """ + ''' This is a dummy task which gets queued on the service.Service threadgroup. Without this service.Service sees nothing running i.e has nothing to wait() on, so the process exits.. This could also be used to trigger periodic non-cluster-specific housekeeping tasks - """ + ''' + # TODO(Yanyan): have this task call dbapi purge events pass - - def _serialize_profile_info(self): - prof = profiler.get() - trace_info = None - if prof: - trace_info = { - "hmac_key": prof.hmac_key, - "base_id": prof.get_base_id(), - "parent_id": prof.get_id() - } - return trace_info - - def _start_with_trace(self, trace, func, *args, **kwargs): - if trace: - profiler.init(**trace) - return func(*args, **kwargs) - + def start(self, target_id, func, *args, **kwargs): """ Run the given method in a sub-thread. """ if target_id not in self.groups: self.groups[target_id] = threadgroup.ThreadGroup() - return self.groups[target_id].add_thread(self._start_with_trace, - self._serialize_profile_info(), - func, *args, **kwargs) + + return self.groups[target_id].add_thread(func, *args, **kwargs) def start_with_lock(self, cnxt, target, target_type, engine_id, func, *args, **kwargs): @@ -99,6 +82,7 @@ class ThreadGroupManager(object): lock = senlin_lock.ClusterLock(cnxt, target, engine_id) elif target_type == 'node': lock = senlin_lock.NodeLock(cnxt, target, engine_id) + with lock.thread_lock(target.id): th = self.start_with_acquired_lock(target, lock, func, *args, **kwargs) @@ -168,9 +152,45 @@ class ThreadGroupManager(object): for th in threads: th.link(mark_done, th) + while not all(links_done.values()): eventlet.sleep() def send(self, target_id, message): for event in self.events.pop(target_id, []): event.send(message) + + def action_proc(self, action, worker_id): + ''' + Thread procedure. + ''' + status = action.get_status() + while status in (action.INIT, action.WAITING): + # TODO(Qiming): Handle 'start_time' field of an action + yield + status = action.get_status() + + # Exit quickly if action has been taken care of or marked + # completed or cancelled by other activities + if status != action.READY: + return + + done = False + while not done: + # Take over the action + action.set_status(action.RUNNING) + + result = action.execute() + + if result == action.OK: + action.set_status(action.SUCCEEDED) + done = True + elif result == action.ERROR: + action.set_status(action.FAILED) + done = True + elif result == action.RETRY: + continue + + + def start_action_worker(self, action, engine_id): + self.start(self.action_proc, engine_id) From 5f37f1fba84ca43392a0c4d8de6ba2b04584330f Mon Sep 17 00:00:00 2001 From: tengqm Date: Wed, 31 Dec 2014 11:46:41 +0800 Subject: [PATCH 21/59] Rename base.py to policy.py --- senlin/policies/{base.py => policy.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename senlin/policies/{base.py => policy.py} (98%) diff --git a/senlin/policies/base.py b/senlin/policies/policy.py similarity index 98% rename from senlin/policies/base.py rename to senlin/policies/policy.py index e818f39a8..acdbb8424 100644 --- a/senlin/policies/base.py +++ b/senlin/policies/policy.py @@ -11,7 +11,7 @@ # under the License. -class PolicyBase(object): +class Policy(object): ''' Base class for policies. ''' From b41506824be9459a2fd63ac07f8c5b34fef1bc16 Mon Sep 17 00:00:00 2001 From: Hang Liu Date: Wed, 31 Dec 2014 12:11:59 +0800 Subject: [PATCH 22/59] add action API revision task --- TODO | 1 + 1 file changed, 1 insertion(+) diff --git a/TODO b/TODO index 7b5a8c033..b21f12c65 100644 --- a/TODO +++ b/TODO @@ -5,6 +5,7 @@ High Priority DB -- - Add action APIs + - Revise action add/remove dependencies APIs - Make sure cluster-policy association is deleted when a cluster is deleted - Add field size to cluster table - Modify node_set_status to check/update cluster status From f630d5a15516c2804a5d1cfad3d7359d4b069270 Mon Sep 17 00:00:00 2001 From: tengqm Date: Wed, 31 Dec 2014 12:48:55 +0800 Subject: [PATCH 23/59] Cleansed the class implementation Major revisions: - Removed 'suspend' and 'resume' support, at least for now. - Revised logics used for action creation and worker thread notification. - Other minor stuffs --- senlin/engine/service.py | 122 +++++++++++++++------------------------ 1 file changed, 46 insertions(+), 76 deletions(-) diff --git a/senlin/engine/service.py b/senlin/engine/service.py index 806e7c7b3..e93bdef4b 100644 --- a/senlin/engine/service.py +++ b/senlin/engine/service.py @@ -27,7 +27,7 @@ from senlin.db import api as db_api from senlin.engine import action as actions from senlin.engine import cluster as clusters from senlin.engine import senlin_lock -from senlin.engine.thread_mgr import ThreadGroupManager +from senlin.engine import thread_mgr from senlin.openstack.common import log as logging from senlin.openstack.common import service from senlin.openstack.common import uuidutils @@ -108,12 +108,12 @@ class EngineService(service.Service): # The following are initialized here, but assigned in start() which # happens after the fork when spawning multiple worker processes self.engine_id = None - self.TG= None + self.TG = None self.target = None def start(self): self.engine_id = senlin_lock.BaseLock.generate_engine_id() - self.TG = ThreadGroupManager() + self.TG = thread_mgr.ThreadGroupManager() self.listener = EngineListener(self.host, self.engine_id, self.TG) LOG.debug("Starting listener for engine %s" % self.engine_id) self.listener.start() @@ -151,39 +151,39 @@ class EngineService(service.Service): super(EngineService, self).stop() @request_context - def identify_cluster(self, cnxt, cluster_name): + def identify_cluster(self, context, cluster_name): """ The identify_cluster method returns the cluster id for a single, live cluster given the cluster name. - :param cnxt: RPC context. + :param context: RPC context. :param cluster_name: Name or ID of the cluster to look up. """ if uuidutils.is_uuid_like(cluster_name): - db_cluster = db_api.cluster_get(cnxt, cluster_name, + db_cluster = db_api.cluster_get(context, cluster_name, show_deleted=True) # may be the name is in uuid format, so if get by id returns None, # we should get the info by name again if not db_cluster: - db_cluster = db_api.cluster_get_by_name(cnxt, cluster_name) + db_cluster = db_api.cluster_get_by_name(context, cluster_name) else: - db_cluster = db_api.cluster_get_by_name(cnxt, cluster_name) + db_cluster = db_api.cluster_get_by_name(context, cluster_name) if db_cluster: - cluster = clusters.Cluster.load(cnxt, cluster=db_cluster) + cluster = clusters.Cluster.load(context, cluster=db_cluster) return dict(cluster.id) else: raise exception.ClusterNotFound(cluster_name=cluster_name) - def _get_cluster(self, cnxt, cluster_identity, show_deleted=False): + def _get_cluster(self, context, cluster_identity, show_deleted=False): """ Get Cluster record in DB based on cluster id """ # Currently, cluster_identity is cluster id OR cluster name - # TODO: use full cluster identity as inpurt, e.g. + # TODO(Yanyan): use full cluster identity as input, e.g. # *cluster_name/cluster_id* - cluster_id = self.identify_cluster(cnxt, cluster_identity) + cluster_id = self.identify_cluster(context, cluster_identity) - db_cluster = db_api.cluster_get(cnxt, cluster_id, + db_cluster = db_api.cluster_get(context, cluster_id, show_deleted=show_deleted, eager_load=True) @@ -193,20 +193,21 @@ class EngineService(service.Service): return db_cluster @request_context - def show_cluster(self, cnxt, cluster_identity): + def show_cluster(self, context, cluster_identity): """ Return detailed information about one or all clusters. - :param cnxt: RPC context. + :param context: RPC context. :param cluster_identity: Name of the cluster you want to show, or None to show all """ if cluster_identity is not None: - db_cluster = self._get_cluster(cnxt, cluster_identity, + db_cluster = self._get_cluster(context, cluster_identity, show_deleted=True) - cluster_list = clusters.Cluster.load(cnxt, cluster=db_cluster) + cluster_list = clusters.Cluster.load(context, cluster=db_cluster) else: - cluster_list = clusters.Cluster.load_all(cnxt, show_deleted=True) + cluster_list = clusters.Cluster.load_all(context, + show_deleted=True) # Format clusters info clusters_info = [] @@ -216,13 +217,13 @@ class EngineService(service.Service): return {'clusters': clusters_info} @request_context - def list_clusters(self, cnxt, limit=None, marker=None, sort_keys=None, + def list_clusters(self, context, limit=None, marker=None, sort_keys=None, sort_dir=None, filters=None, tenant_safe=True, show_deleted=False, show_nested=False): """ The list_clusters method returns attributes of all clusters. - :param cnxt: RPC context + :param context: RPC context :param limit: the number of clusters to list (integer or string) :param marker: the ID of the last item in the previous page :param sort_keys: an array of fields used to sort the list @@ -233,7 +234,7 @@ class EngineService(service.Service): :param show_nested: if true, show nested clusters :returns: a list of formatted clusters """ - cluster_list = clusters.Cluster.load_all(cnxt, limit, marker, + cluster_list = clusters.Cluster.load_all(context, limit, marker, sort_keys, sort_dir, filters, tenant_safe, show_deleted, show_nested) @@ -246,7 +247,7 @@ class EngineService(service.Service): return {'clusters': clusters_info} @request_context - def create_cluster(self, cntxt, name, profile_id, size, args): + def create_cluster(self, context, name, profile_id, size, args): ''' Handle request to perform a create action on a cluster @@ -260,33 +261,33 @@ class EngineService(service.Service): kwargs = { 'parent': args.get('parent', ''), - 'user': cntxt.get('username', ''), - 'project': cntxt.get('tenant_id', ''), + 'user': context.get('username', ''), + 'project': context.get('tenant_id', ''), 'timeout': args.get('timeout', 0), 'tags': args.get('tags', {}), } cluster = clusters.Cluster(name, profile_id, size, **kwargs) - action = actions.Action(cnxt, cluster, 'CREATE', **kwargs) + action = actions.Action(context, cluster, 'CLUSTER_CREATE', **kwargs) self.TG.start_action_woker(action, self.engine_id) return cluster.id @request_context - def update_cluster(self, cnxt, cluster_identity, profile): + def update_cluster(self, context, cluster_identity, profile_id): """ Handle request to perform a update action on a cluster - :param cnxt: RPC context. + :param context: RPC context. :param cluster_identity: Name of the cluster you want to create. :param size: Size of cluster you want to create. :param profile: Profile used to create cluster nodes. """ # Get the database representation of the existing cluster - db_cluster = self._get_cluster(cnxt, cluster_identity) + db_cluster = self._get_cluster(context, cluster_identity) LOG.info(_LI('Updating cluster %s'), db_cluster.name) - cluster = clusters.Cluster.load(cnxt, cluster=db_cluster) + cluster = clusters.Cluster.load(context, cluster=db_cluster) if cluster.status == cluster.ERROR: msg = _('Updating a cluster when it is errored') raise exception.NotSupported(feature=msg) @@ -299,34 +300,33 @@ class EngineService(service.Service): 'profile_id': profile_id } - action = actions.Action(cnxt, cluster, 'UPDATE', **kwargs) + action = actions.Action(context, cluster, 'CLUSTER_UPDATE', **kwargs) self.TG.start_action_worker(action, self.engine_id) return cluster.id @request_context - def delete_cluster(self, cnxt, cluster_identity): + def delete_cluster(self, context, cluster_identity): """ Handle request to perform a delete action on a cluster - :param cnxt: RPC context. + :param context: RPC context. :param cluster_identity: Name or ID of the cluster you want to delete. """ - db_cluster = self._get_cluster(cnxt, cluster_identity) + db_cluster = self._get_cluster(context, cluster_identity) LOG.info(_LI('Deleting cluster %s'), db_cluster.name) # This is an operation on a cluster, so we try to acquire ClusterLock - cluster = clusters.Cluster.load(cnxt, cluster=db_cluster) - lock = senlin_lock.ClusterLock(cnxt, cluster, self.engine_id) + cluster = clusters.Cluster.load(context, cluster=db_cluster) + lock = senlin_lock.ClusterLock(context, cluster, self.engine_id) with lock.try_thread_lock(cluster.id) as acquire_result: # Successfully acquired lock if acquire_result is None: self.TG.stop_timers(cluster.id) - action = actions.ClusterAction(cnxt, cluster, 'DELETE') - self.TG.start_with_acquired_lock(cluster, lock, - action.execute) + action = actions.Action(context, cluster, 'CLUSTER_DELETE') + self.TG.start_action_worker(action, self.engine_id) return # Current engine has the lock @@ -336,9 +336,9 @@ class EngineService(service.Service): eventlet.sleep(0.2) self.TG.stop(cluster.id) # Another active engine has the lock - elif senlin_lock.ClusterLock.engine_alive(cnxt, acquire_result): + elif senlin_lock.ClusterLock.engine_alive(context, acquire_result): stop_result = self._remote_call( - cnxt, acquire_result, self.listener.STOP_CLUSTER, + context, acquire_result, self.listener.STOP_CLUSTER, cluster_id=cluster.id) if stop_result is None: LOG.debug("Successfully stopped remote task on engine %s" @@ -350,49 +350,19 @@ class EngineService(service.Service): # There may be additional nodes that we don't know about # if an update was in-progress when the cluster was stopped, so # reload the cluster from the database. - db_cluster = self._get_cluster(cnxt, cluster_identity) - cluster = clusters.Cluster.load(cnxt, cluster=db_cluster) - action = actions.ClusterAction(cnxt, cluster, 'DELETE') + db_cluster = self._get_cluster(context, cluster_identity) + cluster = clusters.Cluster.load(context, cluster=db_cluster) + action = actions.Action(context, cluster, 'CLUSTER_DELETE') - self.TG.start_with_lock(cnxt, cluster, 'cluster', - self.engine_id, action.execute) + self.TB.start_action_worker(action, self.engine_id) return None - @request_context - def cluster_suspend(self, cnxt, cluster_identity): - ''' - Handle request to perform suspend action on a cluster - ''' - - db_cluster = self._get_cluster(cnxt, cluster_identity) - LOG.debug("suspending cluster %s" % db_cluster.name) - - cluster = clusters.Cluster.load(cnxt, cluster=db_cluster) - action = actions.ClusterAction(cnxt, cluster, 'SUSPEND') - - self.TG.start_with_lock(cnxt, cluster, 'cluster', - self.engine_id, action.execute) - - @request_context - def cluster_resume(self, cnxt, cluster_identity): - ''' - Handle request to perform a resume action on a cluster - ''' - db_cluster = self._get_cluster(cnxt, cluster_identity) - LOG.debug("resuming cluster %s" % db_cluster.name) - - cluster = clusters.Cluster.load(cnxt, cluster=db_cluster) - action = actions.ClusterAction(cnxt, cluster, 'RESUME') - - self.TG.start_with_lock(cnxt, cluster, 'cluster', - self.engine_id, action.execute) - - def _remote_call(self, cnxt, lock_engine_id, call, *args, **kwargs): + def _remote_call(self, context, lock_engine_id, call, *args, **kwargs): self.cctxt = self._client.prepare( version='1.0', topic=lock_engine_id) try: - self.cctxt.call(cnxt, call, *args, **kwargs) + self.cctxt.call(context, call, *args, **kwargs) except messaging.MessagingTimeout: return False From 1d2047ec959ef689cfa4adbc0de5ec657f387624 Mon Sep 17 00:00:00 2001 From: tengqm Date: Wed, 31 Dec 2014 13:17:46 +0800 Subject: [PATCH 24/59] Added create_action function Not a significant patch, only testing if my new account works. --- senlin/tests/db/shared.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/senlin/tests/db/shared.py b/senlin/tests/db/shared.py index db0a06b75..0687d3357 100644 --- a/senlin/tests/db/shared.py +++ b/senlin/tests/db/shared.py @@ -115,3 +115,21 @@ def create_event(ctx, **kwargs): } values.update(kwargs) return db_api.event_create(ctx, values) + + +def create_action(ctx, **kwargs): + values = { + 'context': kwargs.get('context'), + 'description': 'Action description', + 'target': kwargs.get('target'), + 'action': kwargs.get('action'), + 'cause': 'Reason for action', + 'owner': kwarge.get('owner'), + 'interval': -1, + 'inputs': {'key': 'value'}, + 'outputs': {'result': 'value'} + 'depends_on': [], + 'depended_on': [] + } + values.update(kwargs) + return db_api.action_create(ctx, values) From b4cc7026891f8bb40aaf2b11af181af6a59ff683 Mon Sep 17 00:00:00 2001 From: tengqm Date: Wed, 31 Dec 2014 15:00:32 +0800 Subject: [PATCH 25/59] Rename 'node_count' to 'size' in cluster table 'node_count' is a wierd property name. Using 'size' seems a better choice. --- senlin/db/sqlalchemy/migrate_repo/versions/001_first_version.py | 2 +- senlin/db/sqlalchemy/models.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/senlin/db/sqlalchemy/migrate_repo/versions/001_first_version.py b/senlin/db/sqlalchemy/migrate_repo/versions/001_first_version.py index 4f7d13000..4344f8ba5 100644 --- a/senlin/db/sqlalchemy/migrate_repo/versions/001_first_version.py +++ b/senlin/db/sqlalchemy/migrate_repo/versions/001_first_version.py @@ -49,7 +49,7 @@ def upgrade(migrate_engine): sqlalchemy.Column('created_time', sqlalchemy.DateTime), sqlalchemy.Column('updated_time', sqlalchemy.DateTime), sqlalchemy.Column('deleted_time', sqlalchemy.DateTime), - sqlalchemy.Column('node_count', sqlalchemy.Integer), + sqlalchemy.Column('size', sqlalchemy.Integer), sqlalchemy.Column('next_index', sqlalchemy.Integer), sqlalchemy.Column('timeout', sqlalchemy.Integer), sqlalchemy.Column('status', sqlalchemy.String(255)), diff --git a/senlin/db/sqlalchemy/models.py b/senlin/db/sqlalchemy/models.py index 11155222e..d84c5a895 100644 --- a/senlin/db/sqlalchemy/models.py +++ b/senlin/db/sqlalchemy/models.py @@ -98,7 +98,7 @@ class Cluster(BASE, SenlinBase, SoftDelete): created_time = sqlalchemy.Column(sqlalchemy.DateTime) updated_time = sqlalchemy.Column(sqlalchemy.DateTime) deleted_time = sqlalchemy.Column(sqlalchemy.DateTime) - node_count = sqlalchemy.Column(sqlalchemy.Integer) + size = sqlalchemy.Column(sqlalchemy.Integer) next_index = sqlalchemy.Column(sqlalchemy.Integer) timeout = sqlalchemy.Column(sqlalchemy.Integer) status = sqlalchemy.Column(sqlalchemy.String(255)) From d81f2d6bc286d87b6d50f99b5fa3ef5782582c80 Mon Sep 17 00:00:00 2001 From: tengqm Date: Wed, 31 Dec 2014 17:27:05 +0800 Subject: [PATCH 26/59] Mass rewriting -- fixing holes --- senlin/engine/cluster.py | 268 ++++++++++++++++++++++----------------- 1 file changed, 152 insertions(+), 116 deletions(-) diff --git a/senlin/engine/cluster.py b/senlin/engine/cluster.py index d0fb3635d..add82d3b1 100644 --- a/senlin/engine/cluster.py +++ b/senlin/engine/cluster.py @@ -10,13 +10,14 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid -from datetime import datetime +import datetime +from senlin.common import exception +from senlin.common.i18n import _ +from senlin.common.i18n import _LW from senlin.db import api as db_api -from senlin.engine import action as actions +from senlin.engine import event as events from senlin.engine import node as nodes -from senlin.engine import scheduler from senlin.rpc import api as rpc_api @@ -40,98 +41,163 @@ class Cluster(object): Intialize a cluster object. The cluster defaults to have 0 nodes with no profile assigned. ''' + self.context = kwargs.get('context', {}) + self.id = kwargs.get('id', None) self.name = name self.profile_id = profile_id + # Initialize the fields using kwargs passed in self.user = kwargs.get('user', '') self.project = kwargs.get('project', '') self.domain = kwargs.get('domain', '') - self.parent = kwargs.get('parent') + self.parent = kwargs.get('parent', '') - self.created_time = None - self.updated_time = None - self.deleted_time = None + self.created_time = kwargs.get('created_time', None) + self.updated_time = kwargs.get('updated_time', None) + self.deleted_time = kwargs.get('deleted_time', None) - # node_count is only the 'current count', not the desired count - # (i.e. size) - self.desired_size = size - self.node_count = 0 - self.next_index = 0 + # size is only the 'desired capacity', which many not be the real + # size of the cluster at a moment. + self.size = size + self.next_index = kwargs.get('next_index', 0) self.timeout = kwargs.get('timeout', 0) - self.status = self.INIT - self.status_reason = 'Initializing' - self.data = {} + self.status = kwargs.get('status', self.INIT) + self.status_reason = kwargs.get('status_reason', 'Initializing') + self.data = kwargs.get('data', {}) self.tags = kwargs.get('tags', {}) - # persist object into database very early because: - # 1. object creation may be a time consuming task - # 2. user may want to cancel the action when cluster creation - # is still in progress - db_api.create_cluster(self) - # rt is a dict for runtime data - self.rt = dict(nodes={}, - policies={}) + self.rt = dict(nodes={}, policies={}) - def _set_status(self, context, status): + @classmethod + def from_db_record(cls, context, record): + ''' + Construct a cluster object from database record. + :param context: the context used for DB operations; + :param record: a DB cluster object that will receive all fields; + ''' + kwargs = { + 'id': record.id, + 'user': record.user, + 'project': record.project, + 'domain': record.domain, + 'parent': record.parent, + 'created_time': record.created_time, + 'updated_time': record.updated_time, + 'deleted_time': record.deleted_time, + 'next_index': record.next_index, + 'timeout': record.timeout, + 'status': record.status, + 'status_reason': record.status_reason, + 'data': record.data, + 'tags': record.tags, + } + return cls(context, record.name, record.profile_id, record.size, + **kwargs) + + @classmethod + def load(cls, context, cluster_id, show_deleted=False): + ''' + Retrieve a cluster from database. + ''' + cluster = db_api.cluster_get(context, cluster_id, + show_deleted=show_deleted) + + if cluster is None: + msg = _('No cluster with id "%s" exists') % cluster_id + raise exception.NotFound(msg) + + return cls._from_db_record(context, cluster) + + @classmethod + def load_all(cls, context, limit=None, sort_keys=None, marker=None, + sort_dir=None, filters=None, tenant_safe=True, + show_deleted=False, show_nested=False): + ''' + Retrieve all clusters from database. + ''' + records = db_api.cluster_get_all(context, limit, sort_keys, marker, + sort_dir, filters, tenant_safe, + show_deleted, show_nested) + + for record in records: + yield cls._from_db_record(context, record) + + def store(self): + ''' + Store the cluster in database and return its ID. + If the ID already exists, we do an update. + ''' + values = { + 'name': self.name, + 'profile_id': self.profile_id, + 'user': self.user, + 'project': self.project, + 'domain': self.domain, + 'parent': self.parent, + 'created_time': self.created_time, + 'updated_time': self.updated_time, + 'deleted_time': self.deleted_time, + 'size': self.size, + 'next_index': self.next_index, + 'timeout': self.timeout, + 'status': self.status, + 'status_reason': self.status_reason, + 'tags': self.tags, + 'data': self.data, + } + + if self.id: + db_api.cluster_update(self.context, self.id, values) + # TODO(Qiming): create event/log + else: + cluster = db_api.cluster_create(self.context, values) + # TODO(Qiming): create event/log + self.id = cluster.id + + return self.id + + def _set_status(self, status): + ''' + Set status of the cluster. + ''' + values = {} now = datetime.datetime.utcnow() if status == self.ACTIVE: if self.status == self.INIT: - kwargs['created_time'] = now + values['created_time'] = now else: - kwargs['updated_time'] = now + values['updated_time'] = now elif status == self.DELETED: - kwargs['deleted_time'] = now + values['deleted_time'] = now - kwargs['status'] = status - db_api.cluster_update(self.context, {'status': status}) - #event.info(context, self.id, status, + values['status'] = status + db_api.cluster_update(self.context, self.id, values) # log status to log file # generate event record - def do_create(self, context, **kwargs): + def do_create(self, **kwargs): ''' - A routine to be called from an action by a thread. + A routine to be called from an action. ''' - for m in range[self.size]: - node = nodes.Node(None, profile_id, cluster_id) - action = actions.NodeAction(context, node, 'CREATE', **kwargs) - # start a thread asynchnously - handle = scheduler.runAction(action) - # add subthread to the waiting list of main thread - scheduler.wait(handle) - self._set_status(self.ACTIVE) - def do_delete(self, **kwargs): + def do_delete(self, context, **kwargs): self.status = self.DELETED - def do_update(self, **kwargs): - # Profile type checking is done here because the do_update logic can + def do_update(self, context, **kwargs): + # Profile type checking is done here because the do_update logic can # be triggered from API or Webhook - # TODO: check if profile is of the same type - profile = kwargs.get('profile') - if self.profile == profile: - event.warning(_LW('Cluster refuses to update to the same profile' - '(%s)' % (profile))) - return self.FAILED - - self._set_status(self.UPDATING) - - node_list = self.get_nodes() - for n in node_list: - node = nodes.Node(None, profile.id, cluster_id) - action = actions.NodeAction(context, node, 'UPDATE', **kwargs) - - # start a thread asynchronously - handle = scheduler.runAction(action) - scheduler.wait(handle) - - self._set_status(self.ACTIVE) + # TODO(Qiming): check if profile is of the same type + profile_id = kwargs.get('profile_id') + if self.profile_id == profile_id: + events.warning(_LW('Cluster refuses to update to the same profile' + '(%s)' % (profile_id))) + return False def get_next_index(self): - # TODO: Get next_index from db and increment it in db + # TODO(Qiming): Get next_index from db and increment it in db curr = self._next_index self._next_index = self._next_index + 1 return curr @@ -139,96 +205,66 @@ class Cluster(object): def get_nodes(self): # This method will return each node with their associated profiles. # Members may have different versions of the same profile type. - return {} + return self.rt.nodes def get_policies(self): # policies are stored in database when policy association is created # this method retrieves the attached policies from database - return {} + return self.rt.policies def add_nodes(self, node_ids): pass def del_nodes(self, node_ids): - for node in node_ids: - res = Node.destroy(node) - return True + ''' + Remove nodes from current cluster. + ''' + deleted = [] + for node_id in node_ids: + node = db_api.node_get(node_id) + if node.leave(self): + deleted.append(node_id) + return deleted def attach_policy(self, policy_id): ''' Attach specified policy instance to this cluster. ''' - # TODO: check conflicts with existing policies + # TODO(Qiming): check conflicts with existing policies self.policies.append(policy_id) def detach_policy(self, policy_id): - # TODO: check if actions of specified policies are ongoing + # TODO(Qiming): check if actions of specified policies are ongoing self.policies.remove(policy_id) @classmethod def create(cls, name, size=0, profile=None, **kwargs): cluster = cls(name, size, profile, kwargs) cluster.do_create() - # TODO: store this to database - # TODO: log events? + # TODO(Qiming): store this to database + # log events? return cluster.id @classmethod def delete(cls, cluster_id): cluster = db_api.get_cluster(cluster_id) + if not cluster: + message = _('No cluster exists with id "%s"') % str(cluster_id) + raise exception.NotFound(message) - # TODO: check if actions are working on and can be canceled - # TODO: destroy nodes + # TODO(Qiming): check if actions are working on and can be canceled + # destroy nodes db_api.delete_cluster(cluster_id) return True @classmethod def update(cls, cluster_id, profile): - cluster = db_api.get_cluster(cluster_id) + # cluster = db_api.get_cluster(cluster_id) + # TODO(Qiming): Implement this - # TODO: Implement this - return True - @classmethod - def load(cls, context, cluster_id=None, cluster=None, show_deleted=True): - '''Retrieve a Cluster from the database.''' - if cluster is None: - cluster = db_api.cluster_get(context, cluster_id, - show_deleted=show_deleted) - - if cluster is None: - message = _('No cluster exists with id "%s"') % str(cluster_id) - raise exception.NotFound(message) - - return cls.from_db(context, cluster) - - @classmethod - def load_all(cls, context, limit=None, marker=None, sort_keys=None, - sort_dir=None, filters=None, tenant_safe=True, - show_deleted=False, show_nested=False): - clusters = db_api.cluster_get_all(context, limit, sort_keys, marker, - sort_dir, filters, tenant_safe, - show_deleted, show_nested) or [] - for cluster in clusters: - yield cls.from_db(context, cluster) - - @classmethod - def from_db(cls, context, cluster): - # TODO(Qiming): calculate current size based on nodes - return cls(context, cluster.name, cluster.profile, - id=cluster.id, status=cluster.status, - status_reason=cluster.status_reason, - parent=cluster.parent, - project=cluster.project, - created_time=cluster.created_time, - updated_time=cluster.updated_time, - deleted_time=cluster.deleted_time, - domain=cluster.domain, - timeout=cluster.timeout, - user=cluster.user) - def to_dict(self): info = { rpc_api.CLUSTER_NAME: self.name, From 6ebeb98d2f5e09ef1e5a756a0bc64444f44c1957 Mon Sep 17 00:00:00 2001 From: tengqm Date: Wed, 31 Dec 2014 17:38:06 +0800 Subject: [PATCH 27/59] Add DB load/store logic to Node class --- senlin/engine/node.py | 117 +++++++++++++++++++++++++++++++----------- 1 file changed, 87 insertions(+), 30 deletions(-) diff --git a/senlin/engine/node.py b/senlin/engine/node.py index b297ad727..fdfb10394 100644 --- a/senlin/engine/node.py +++ b/senlin/engine/node.py @@ -10,10 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid import datetime +from senlin.common import exception from senlin.db import api as db_api +from senlin.engine import environment class Node(object): @@ -32,31 +33,27 @@ class Node(object): ) def __init__(self, name, profile_id, cluster_id=None, **kwargs): - self.id = None + self.id = kwargs.get('id', None) if name: self.name = name else: - # TODO - # Using self.physical_resource_name() to generate a unique name + # TODO(Qiming): Use self.physical_resource_name() to + # generate a unique name self.name = 'node-name-tmp' - self.physical_id = '' - self.cluster_id = cluster_id or '' + + self.physical_id = kwargs.get('physical_id', '') self.profile_id = profile_id - if cluster_id is None: - self.index = -1 - else: - self.index = db_api.get_next_index(cluster_id) - self.role = '' + self.cluster_id = cluster_id or '' + self.index = kwargs.get('index', -1) + self.role = kwargs.get('role', '') + self.created_time = kwargs.get('created_time', None) + self.updated_time = kwargs.get('updated_time', None) + self.deleted_time = kwargs.get('deleted_time', None) - self.created_time = None - self.updated_time = None - self.deleted_time = None - - self.status = self.INIT - self.status_reason = 'Initializing' - self.data = {} - self.tags = {} - self.store() + self.status = kwargs.get('status', self.INIT) + self.status_reason = kwargs.get('status_reason', 'Initializing') + self.data = kwargs.get('data', {}) + self.tags = kwargs.get('tags', {}) def store(self): ''' @@ -84,34 +81,94 @@ class Node(object): if self.id: db_api.node_update(self.context, self.id, values) + # TODO(Qiming): create event/log else: node = db_api.node_create(self.context, values) + # TODO(Qiming): create event/log self.id = node.id + @classmethod + def from_db_record(cls, context, record): + ''' + Construct a node object from database record. + :param context: the context used for DB operations; + :param record: a DB node object that will receive all fields; + ''' + kwargs = { + 'id': record.id, + 'physical_id': record.physical_id, + 'index': record.index, + 'role': record.role, + 'created_time': record.created_time, + 'updated_time': record.updated_time, + 'deleted_time': record.deleted_time, + 'status': record.status, + 'status_reason': record.status_reason, + 'data': record.data, + 'tags': record.tags, + } + return cls(context, record.name, record.profile_id, record.size, + **kwargs) + + @classmethod + def load(cls, context, node_id): + ''' + Retrieve a node from database. + ''' + node = db_api.node_get(context, node_id) + + if node is None: + msg = _('No node with id "%s" exists') % node_id + raise exception.NotFound(msg) + + return cls._from_db_record(context, node) + + @classmethod + def load_all(cls, context, cluster_id): + ''' + Retrieve all nodes of from database. + ''' + records = db_api.node_get_all_by_cluster(context, cluster_id) + + for record in records: + yield cls._from_db_record(context, record) + def create(self, name, profile_id, cluster_id=None, **kwargs): - # TODO: invoke profile to create new object and get the physical id - # TODO: log events? + # TODO(Qiming): invoke profile to create new object and get the + # physical id + # TODO(Qiming): log events? self.created_time = datetime.datetime.utcnnow() + profile = db_api.get_profile(profile_id) + profile_cls = environment.global_env().get_profile(profile.type) + node = profile_cls.create_object(self.id, profile_id) + return node.id def delete(self): - node = db_api.get_node(self.id) - physical_id = node.physical_id + # node = db_api.node_get(self.id) + # physical_id = node.physical_id - # TODO: invoke profile to delete this object - # TODO: check if actions are working on it and can be canceled + # TODO(Qiming): invoke profile to delete this object + # TODO(Qiming): check if actions are working on it and can be canceled db_api.delete_node(self.id) return True + def join(self, cluster): + return True + + def leave(self, cluster): + return True + def update(self, new_profile_id): - old_profile = db_api.get_profile(self.profile_id) new_profile = db_api.get_profile(new_profile_id) - # TODO: check if profile type matches - new_profile_type = new_profile.type_name + if self.profile_id == new_profile.id: + return True - profile_cls = profile_registry.get_class(type_name) + new_type = new_profile.type_name + + profile_cls = environment.global_env().get_profile(new_type) profile_cls.update_object(self.id, new_profile) self.profile_id = new_profile From 328bd73d90ddfcade2d1be10a06d23e4cbb4f56a Mon Sep 17 00:00:00 2001 From: tengqm Date: Wed, 31 Dec 2014 17:39:13 +0800 Subject: [PATCH 28/59] Rework scheduler interface A scheduler only cares about worker threads. It has no idea of actions. NOTE: If a scheduler know and imports action, and an action needs to know the existence of scheduler thus imports it, there will be circular references which is not allowed in Python. --- senlin/engine/scheduler.py | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/senlin/engine/scheduler.py b/senlin/engine/scheduler.py index 2dbba90e8..c66211dc8 100644 --- a/senlin/engine/scheduler.py +++ b/senlin/engine/scheduler.py @@ -23,7 +23,6 @@ import six from senlin.common.i18n import _ from senlin.common.i18n import _LI -from senlin.engine import action as actions from senlin.openstack.common import log as logging LOG = logging.getLogger(__name__) @@ -526,19 +525,7 @@ class PollingTaskGroup(object): for r in runners: r.cancel() - -def runAction(action): - ''' - Start a thread to run action until finished - ''' - # TODO(Yanyan): Query lock for this action - # call action.execute with args in subthread - pass - - -def wait(handle): - ''' - Wait an action to finish - ''' - # TODO(Yanyan): Make the subthread join the main thread +def notify(): + # TODO(Yanyan): Check if workers are available to pick actions to + # execute pass From 4ab3e21bc1764bbf57215890e89941903c7f7029 Mon Sep 17 00:00:00 2001 From: tengqm Date: Wed, 31 Dec 2014 17:42:15 +0800 Subject: [PATCH 29/59] Make service engine do explict store() This is not clean, but currently I don't have a better idea of this. --- senlin/engine/service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/senlin/engine/service.py b/senlin/engine/service.py index e93bdef4b..9f0e26655 100644 --- a/senlin/engine/service.py +++ b/senlin/engine/service.py @@ -268,6 +268,7 @@ class EngineService(service.Service): } cluster = clusters.Cluster(name, profile_id, size, **kwargs) + cluster.store() action = actions.Action(context, cluster, 'CLUSTER_CREATE', **kwargs) self.TG.start_action_woker(action, self.engine_id) From 27a0cf7556d3abe3cffeab35902bc51875d3e030 Mon Sep 17 00:00:00 2001 From: tengqm Date: Wed, 31 Dec 2014 17:43:18 +0800 Subject: [PATCH 30/59] A bunch of changes to Action implementation. - No target object is passed as positional argument; - Added CustomAction as a placeholder for future extension; - Reworked intialization logic; - Renamed default action sets to make them more explicit; - Added some pseudo logics to some methods, to be completed yet. --- senlin/engine/action.py | 258 ++++++++++++++++++++++++++++++---------- 1 file changed, 192 insertions(+), 66 deletions(-) diff --git a/senlin/engine/action.py b/senlin/engine/action.py index cb6369b06..51f8a2cea 100644 --- a/senlin/engine/action.py +++ b/senlin/engine/action.py @@ -10,9 +10,16 @@ # License for the specific language governing permissions and limitations # under the License. +import copy +import datetime + from oslo.config import cfg +from senlin.common import exception from senlin.db import api as db_api +from senlin.engine import cluster as clusters +from senlin.engine import node as nodes +from senlin.engine import scheduler class Action(object): @@ -20,9 +27,9 @@ class Action(object): An action can be performed on a cluster or a node of a cluster. ''' RETURNS = ( - OK, FAILED, RETRY, + RES_OK, RES_ERROR, RES_RETRY, ) = ( - 'OK', 'FAILED', 'RETRY', + 'OK', 'ERROR', 'RETRY', ) # Action status definitions: @@ -41,57 +48,72 @@ class Action(object): 'SUCCEEDED', 'FAILED', 'CANCELLED', ) - def __init__(self, context): - self.id = None + def __new__(cls, context, action, **kwargs): + if (cls != Action): + return super(Action, cls).__new__(cls) + + target_type = action.split('_')[0] + if target_type == 'CLUSTER': + ActionClass = ClusterAction + elif target_type == 'NODE': + ActionClass = NodeAction + elif target_type == 'POLICY': + ActionClass = PolicyAction + else: + ActionClass = CustomAction + + return super(Action, cls).__new__(ActionClass) + + def __init__(self, context, action, **kwargs): # context will be persisted into database so that any worker thread # can pick the action up and execute it on behalf of the initiator - self.context = context + self.context = copy.deepcopy(context) - self.description = '' + self.description = kwargs.get('description', '') # Target is the ID of a cluster, a node, a profile - self.target = '' + self.target = kwargs.get('target', None) + if self.target is None: + raise exception.ActionMissingTarget(action=action) - # An action - self.action = '' + self.action = action # Why this action is fired, it can be a UUID of another action - self.cause = '' + self.cause = kwargs.get('cause', '') # Owner can be an UUID format ID for the worker that is currently # working on the action. It also serves as a lock. - self.owner = '' + self.owner = kwargs.get('owner', None) # An action may need to be executed repeatitively, interval is the # time in seconds between two consequtive execution. # A value of -1 indicates that this action is only to be executed once - self.interval = -1 + self.interval = kwargs.get('interval', -1) # Start time can be an absolute time or a time relative to another # action. E.g. # - '2014-12-18 08:41:39.908569' # - 'AFTER: 57292917-af90-4c45-9457-34777d939d4d' - # - 'WHEN: 0265f93b-b1d7-421f-b5ad-cb83de2f559d' - self.start_time = '' - self.end_time = '' + # - 'WHEN: 0265f93b-b1d7-421f-b5ad-cb83de2f559d' + self.start_time = kwargs.get('start_time', None) + self.end_time = kwargs.get('end_time', None) # Timeout is a placeholder in case some actions may linger too long - self.timeout = cfg.CONF.default_action_timeout + self.timeout = kwargs.get('timeout', cfg.CONF.default_action_timeout) # Return code, useful when action is not automatically deleted # after execution - self.status = '' - self.status_reason = '' + self.status = kwargs.get('status', self.INIT) + self.status_reason = kwargs.get('status_reason', '') - # All parameters are passed in using keyward arguments which is - # a list stored as JSON in DB - self.inputs = {} - self.outputs = {} + # All parameters are passed in using keyword arguments which is + # a dictionary stored as JSON in DB + self.inputs = kwargs.get('inputs', {}) + self.outputs = kwargs.get('outputs', {}) # Dependency with other actions - self.depends_on = [] - self.depended_by = [] - + self.depends_on = kwargs.get('depends_on', []) + self.depended_by = kwargs.get('depended_by', []) def execute(self, **kwargs): return NotImplemented @@ -100,43 +122,101 @@ class Action(object): return NotImplemented def store(self): - #db_api.action_update(self.id) + #db_api.action_update(self.id) return + def set_status(self, status): + ''' + Set action status. + This is not merely about a db record update. + ''' + if status == self.SUCCEEDED: + db_api.action_mark_succeeded(self.context, self.id) + elif status == self.FAILED: + db_api.action_mark_failed(self.context, self.id) + elif status == self.CANCELLED: + db_api.action_mark_cancelled(self.context, self.id) + + self.status = status + + def get_status(self): + action = db_api.action_get(self.context, self.id) + self.status = action.status + return action.status + class ClusterAction(Action): ''' An action performed on a cluster. ''' ACTIONS = ( - CREATE, DELETE, ADD_NODE, DEL_NODE, UPDATE, - ATTACH_POLICY, DETACH_POLICY, + CLUSTER_CREATE, CLUSTER_DELETE, CLUSTER_UPDATE, + CLUSTER_ADD_NODES, CLUSTER_DEL_NODES, CLUSTER_RESIZE, + CLUSTER_ATTACH_POLICY, CLUSTER_DETACH_POLICY, ) = ( - 'CREATE', 'DELETE', 'ADD_NODE', 'DEL_NODE', 'UPDATE', - 'ATTACH_POLICY', 'DETACH_POLICY', + 'CLUSTER_CREATE', 'CLUSTER_DELETE', 'CLUSTER_UPDATE', + 'CLUSTER_ADD_NODES', 'CLUSTER_DEL_NODES', 'CLUSTER_RESIZE', + 'CLUSTER_ATTACH_POLICY', 'CLUSTER_DETACH_POLICY', ) - def __init__(self, context, cluster): - super(ClusterAction, self).__init__(context) - self.target = cluster - - def execute(self, action, **kwargs): + def __init__(self, context, action, **kwargs): + super(ClusterAction, self).__init__(context, action, **kwargs) if action not in self.ACTIONS: - return self.FAILED + raise exception.ActionNotSupported( + action=action, object=_('cluster %s') % self.target) - if action == self.CREATE: - # TODO: + def execute(self, **kwargs): + ''' + Execute the action. + In theory, the action encapsulates all information needed for + execution. 'kwargs' may specify additional parameters. + :param kwargs: additional parameters that may override the default + properties stored in the action record. + ''' + cluster = db_api.cluster_get(self.context, self.target) + if not cluster: + return self.RES_ERROR + + if self.action == self.CLUSTER_CREATE: + # TODO(Qiming): # We should query the lock of cluster here and wrap # cluster.do_create, and then let threadgroupmanager # to start a thread for this progress. - cluster.do_create(kwargs) - else: - return self.FAILED + cluster.do_create() - return self.OK + for m in range(cluster.size): + name = 'node-%003s' % m + node = nodes.Node(name, cluster.profile_id, cluster.id) + node.store() + kwargs = { + 'target': node.id, + } + + action = NodeAction(context, 'NODE_CREATE', **kwargs) + action.set_status(self.READY) + + scheduler.notify() + + elif self.action == self.CLUSTER_UPDATE: + # TODO(Yanyan): grab lock + cluster._set_status(self.UPDATING) + node_list = cluster.get_nodes() + for node_id in node_list: + node = db_api.node_get(node_id) + action = actions.Action(context, node, 'NODE_UPDATE', **kwargs) + + # start a thread asynchronously + handle = scheduler.runAction(action) + scheduler.wait(handle) + # TODO(Yanyan): release lock + cluster._set_status(self.ACTIVE) + + return self.RES_ERROR + + return self.RES_OK def cancel(self): - return self.FAILED + return self.RES_OK class NodeAction(Action): @@ -144,24 +224,44 @@ class NodeAction(Action): An action performed on a cluster member. ''' ACTIONS = ( - CREATE, DELETE, UPDATE, JOIN, LEAVE, + NODE_CREATE, NODE_DELETE, NODE_UPDATE, + NODE_JOIN_CLUSTER, NODE_LEAVE_CLUSTER, ) = ( - 'CREATE', 'DELETE', 'UPDATE', 'JOIN', 'LEAVE', + 'NODE_CREATE', 'NODE_DELETE', 'NODE_UPDATE', + 'NODE_JOIN_CLUSTER', 'NODE_LEAVE_CLUSTER', ) - def __init__(self, context, node): - super(NodeAction, self).__init__(context) + def __init__(self, context, action, **kwargs): + super(NodeAction, self).__init__(context, action, **kwargs) - # get cluster of this node + if action not in self.ACTIONS: + return self.RES_ERROR + + # get cluster of this node # get policies associated with the cluster - def execute(self, action, **kwargs): - if action not in self.ACTIONS: - return self.FAILED - return self.OK + def execute(self, **kwargs): + if self.action == self.NODE_CREATE: + profile_id = kwargs.get('profile_id') + name = kwargs.get('name') + profile = db_api.profile_get(self.context, profile_id) + node = profile.create_object(name, profile_id) + if not node: + return self.RES_ERROR + elif self.action == self.NODE_DELETE: + node_id = self.target + profile.delete_object(node_id) + elif self.action == self.NODE_UPDATE: + node_id = self.target + profile_id = kwargs.get('profile_id') + profile.update_object(node_id, profile_id) + else: + return self.RES_ERROR + + return self.RES_OK def cancel(self): - return self.OK + return self.RES_OK class PolicyAction(Action): @@ -173,18 +273,26 @@ class PolicyAction(Action): ''' ACTIONS = ( - ENABLE, DISABLE, UPDATE, + POLICY_ENABLE, POLICY_DISABLE, POLICY_UPDATE, ) = ( - 'ENABLE', 'DISABLE', 'UPDATE', + 'POLICY_ENABLE', 'POLICY_DISABLE', 'POLICY_UPDATE', ) - def __init__(self, context, cluster_id, policy_id): - super(PolicyAction, self).__init__(context) + def __init__(self, context, action, **kwargs): + super(PolicyAction, self).__init__(context, action, **kwargs) + self.cluster_id = kwargs.get('cluster_id', None) + if self.cluster_id is None: + raise exception.ActionMissingTarget(action) + + self.policy_id = kwargs.get('policy_id', None) + if self.policy_id is None: + raise exception.ActionMissingPolicy(action) + # get policy associaton using the cluster id and policy id - def execute(self, action, **kwargs): - if action not in self.ACTIONS: - return self.FAILED + def execute(self, **kwargs): + if self.action not in self.ACTIONS: + return self.RES_ERROR self.store(start_time=datetime.datetime.utcnow(), status=self.RUNNING) @@ -193,23 +301,41 @@ class PolicyAction(Action): policy_id = kwargs.get('policy_id') # an ENABLE/DISABLE action only changes the database table - if action == self.ENABLE: + if self.action == self.POLICY_ENABLE: db_api.cluster_enable_policy(cluster_id, policy_id) - elif action == self.DISABLE: + elif self.action == self.POLICY_DISABLE: db_api.cluster_disable_policy(cluster_id, policy_id) - else: # action == self.UPDATE: + else: # self.action == self.UPDATE: # There is not direct way to update a policy because the policy # might be shared with another cluster, instead, we clone a new # policy and replace the cluster-policy entry. pass # TODO(Qiming): Add DB API complete this. - + self.store(end_time=datetime.datetime.utcnow(), status=self.SUCCEEDED) - return self.OK + + return self.RES_OK def cancel(self): self.store(end_time=datetime.datetime.utcnow(), status=self.CANCELLED) - return self.OK + return self.RES_OK + + +class CustomAction(Action): + ACTIONS = ( + ACTION_EXECUTE, + ) = ( + 'ACTION_EXECUTE', + ) + + def __init__(self, context, action, **kwargs): + super(CustomAction, self).__init__(context, action, **kwargs) + + def execute(self, **kwargs): + return self.RES_OK + + def cancel(self): + return self.RES_OK From 7047ca0bf0e1f1a3bd0a703d3a2cd28df9467e31 Mon Sep 17 00:00:00 2001 From: tengqm Date: Wed, 31 Dec 2014 17:51:06 +0800 Subject: [PATCH 31/59] Added two new types of exceptions --- senlin/common/exception.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/senlin/common/exception.py b/senlin/common/exception.py index 5d6bcef69..ec6a08b61 100644 --- a/senlin/common/exception.py +++ b/senlin/common/exception.py @@ -270,6 +270,14 @@ class RequestLimitExceeded(SenlinException): msg_fmt = _('Request limit exceeded: %(message)s') +class ActionMissingTarget(SenlinException): + msg_fmt = _('Action "%(action)s" must have target specified') + + +class ActionMissingPolicy(SenlinException): + msg_fmt = _('Action "%(action)s" must have policy specified') + + class ActionNotSupported(SenlinException): msg_fmt = _('Action "%(action)s" not supported by %(object)s') From c05c2fcefdb5ed052664b3b95770f65b6b215b61 Mon Sep 17 00:00:00 2001 From: tengqm Date: Wed, 31 Dec 2014 17:51:56 +0800 Subject: [PATCH 32/59] Added two comments as TODOs It can be safely ignored if the logic is implemented somewhere else. --- senlin/db/sqlalchemy/api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/senlin/db/sqlalchemy/api.py b/senlin/db/sqlalchemy/api.py index b09833b8c..5be61fc42 100644 --- a/senlin/db/sqlalchemy/api.py +++ b/senlin/db/sqlalchemy/api.py @@ -713,6 +713,7 @@ def action_add_depends_on(context, action_id, *actions): _('Action with id "%s" not found') % action_id) action.depends_on = list(set(actions).union(set(action.depends_on))) + # TODO(liuh): Set status to WAITING if 'depends_on' is not empty action.save(_session(context)) return action @@ -724,6 +725,7 @@ def action_del_depends_on(context, action_id, *actions): _('Action with id "%s" not found') % action_id) action.depends_on = list(set(action.depends_on).different(set(actions))) + # TODO(liuh): Set status to READY if 'depends_on' is empty action.save(_session(context)) return action From 05367f9ada5f4dcd822c85f877f43cfc41fcc050 Mon Sep 17 00:00:00 2001 From: tengqm Date: Wed, 31 Dec 2014 22:06:00 +0800 Subject: [PATCH 33/59] Switch to oslo.context --- openstack-common.conf | 1 - requirements.txt | 1 + senlin/common/context.py | 2 +- senlin/openstack/common/context.py | 126 ----------------------------- 4 files changed, 2 insertions(+), 128 deletions(-) delete mode 100644 senlin/openstack/common/context.py diff --git a/openstack-common.conf b/openstack-common.conf index f8b52912c..19b91edc1 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -1,7 +1,6 @@ [DEFAULT] # The list of modules to copy from oslo-incubator -module=context module=eventlet_backdoor module=local module=log diff --git a/requirements.txt b/requirements.txt index 83acea091..b98f8ec0c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,6 +14,7 @@ kombu>=2.5.0 lxml>=2.3 netaddr>=0.7.12 oslo.config>=1.4.0 # Apache-2.0 +oslo.context>=0.1.0 # Apache-2.0 oslo.db>=1.1.0 # Apache-2.0 oslo.i18n>=1.0.0 # Apache-2.0 oslo.messaging>=1.4.0,!=1.5.0 diff --git a/senlin/common/context.py b/senlin/common/context.py index d7ca9fbcb..1583c2246 100644 --- a/senlin/common/context.py +++ b/senlin/common/context.py @@ -12,12 +12,12 @@ from oslo.middleware import request_id as oslo_request_id from oslo.utils import importutils +from oslo_context import context from senlin.common import exception from senlin.common import policy from senlin.common import wsgi from senlin.db import api as db_api -from senlin.openstack.common import context class RequestContext(context.RequestContext): diff --git a/senlin/openstack/common/context.py b/senlin/openstack/common/context.py deleted file mode 100644 index b612db714..000000000 --- a/senlin/openstack/common/context.py +++ /dev/null @@ -1,126 +0,0 @@ -# Copyright 2011 OpenStack Foundation. -# 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. - -""" -Simple class that stores security context information in the web request. - -Projects should subclass this class if they wish to enhance the request -context or provide additional information in their specific WSGI pipeline. -""" - -import itertools -import uuid - - -def generate_request_id(): - return b'req-' + str(uuid.uuid4()).encode('ascii') - - -class RequestContext(object): - - """Helper class to represent useful information about a request context. - - Stores information about the security context under which the user - accesses the system, as well as additional request information. - """ - - user_idt_format = '{user} {tenant} {domain} {user_domain} {p_domain}' - - def __init__(self, auth_token=None, user=None, tenant=None, domain=None, - user_domain=None, project_domain=None, is_admin=False, - read_only=False, show_deleted=False, request_id=None, - instance_uuid=None): - self.auth_token = auth_token - self.user = user - self.tenant = tenant - self.domain = domain - self.user_domain = user_domain - self.project_domain = project_domain - self.is_admin = is_admin - self.read_only = read_only - self.show_deleted = show_deleted - self.instance_uuid = instance_uuid - if not request_id: - request_id = generate_request_id() - self.request_id = request_id - - def to_dict(self): - user_idt = ( - self.user_idt_format.format(user=self.user or '-', - tenant=self.tenant or '-', - domain=self.domain or '-', - user_domain=self.user_domain or '-', - p_domain=self.project_domain or '-')) - - return {'user': self.user, - 'tenant': self.tenant, - 'domain': self.domain, - 'user_domain': self.user_domain, - 'project_domain': self.project_domain, - 'is_admin': self.is_admin, - 'read_only': self.read_only, - 'show_deleted': self.show_deleted, - 'auth_token': self.auth_token, - 'request_id': self.request_id, - 'instance_uuid': self.instance_uuid, - 'user_identity': user_idt} - - @classmethod - def from_dict(cls, ctx): - return cls( - auth_token=ctx.get("auth_token"), - user=ctx.get("user"), - tenant=ctx.get("tenant"), - domain=ctx.get("domain"), - user_domain=ctx.get("user_domain"), - project_domain=ctx.get("project_domain"), - is_admin=ctx.get("is_admin", False), - read_only=ctx.get("read_only", False), - show_deleted=ctx.get("show_deleted", False), - request_id=ctx.get("request_id"), - instance_uuid=ctx.get("instance_uuid")) - - -def get_admin_context(show_deleted=False): - context = RequestContext(None, - tenant=None, - is_admin=True, - show_deleted=show_deleted) - return context - - -def get_context_from_function_and_args(function, args, kwargs): - """Find an arg of type RequestContext and return it. - - This is useful in a couple of decorators where we don't - know much about the function we're wrapping. - """ - - for arg in itertools.chain(kwargs.values(), args): - if isinstance(arg, RequestContext): - return arg - - return None - - -def is_user_context(context): - """Indicates if the request context is a normal user.""" - if not context: - return False - if context.is_admin: - return False - if not context.user_id or not context.project_id: - return False - return True From 384cf67330840027b19c2d6f578cfbdce68d1c7a Mon Sep 17 00:00:00 2001 From: tengqm Date: Thu, 1 Jan 2015 09:27:59 +0800 Subject: [PATCH 34/59] Fixed typo errors --- senlin/engine/node.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/senlin/engine/node.py b/senlin/engine/node.py index fdfb10394..52692e406 100644 --- a/senlin/engine/node.py +++ b/senlin/engine/node.py @@ -87,12 +87,14 @@ class Node(object): # TODO(Qiming): create event/log self.id = node.id + return self.id + @classmethod def from_db_record(cls, context, record): ''' Construct a node object from database record. :param context: the context used for DB operations; - :param record: a DB node object that will receive all fields; + :param record: a DB node object that contains all fields; ''' kwargs = { 'id': record.id, @@ -121,7 +123,7 @@ class Node(object): msg = _('No node with id "%s" exists') % node_id raise exception.NotFound(msg) - return cls._from_db_record(context, node) + return cls.from_db_record(context, node) @classmethod def load_all(cls, context, cluster_id): @@ -131,7 +133,7 @@ class Node(object): records = db_api.node_get_all_by_cluster(context, cluster_id) for record in records: - yield cls._from_db_record(context, record) + yield cls.from_db_record(context, record) def create(self, name, profile_id, cluster_id=None, **kwargs): # TODO(Qiming): invoke profile to create new object and get the From f8e19680cc4c245306b9c2dc10ed8dd4fb6ed796 Mon Sep 17 00:00:00 2001 From: tengqm Date: Thu, 1 Jan 2015 10:40:43 +0800 Subject: [PATCH 35/59] Added some required methods Logics for cluster actions and node actions are partially added. --- senlin/engine/action.py | 301 +++++++++++++++++++++++++++++++--------- 1 file changed, 239 insertions(+), 62 deletions(-) diff --git a/senlin/engine/action.py b/senlin/engine/action.py index 51f8a2cea..6e6cced6d 100644 --- a/senlin/engine/action.py +++ b/senlin/engine/action.py @@ -17,7 +17,7 @@ from oslo.config import cfg from senlin.common import exception from senlin.db import api as db_api -from senlin.engine import cluster as clusters +from senlin.engine import environment from senlin.engine import node as nodes from senlin.engine import scheduler @@ -67,6 +67,10 @@ class Action(object): def __init__(self, context, action, **kwargs): # context will be persisted into database so that any worker thread # can pick the action up and execute it on behalf of the initiator + if action not in self.ACTIONS: + raise exception.ActionNotSupported( + action=action, object=_('target %s') % self.target) + self.context = copy.deepcopy(context) self.description = kwargs.get('description', '') @@ -115,16 +119,88 @@ class Action(object): self.depends_on = kwargs.get('depends_on', []) self.depended_by = kwargs.get('depended_by', []) + def store(self): + ''' + Store the action record into database table. + ''' + values = { + 'name': self.name, + 'context': self.context, + 'target': self.target, + 'action': self.action, + 'cause': self.cause, + 'owner': self.owner, + 'interval': self.interval, + 'start_time': self.start_time, + 'end_time': self.end_time, + 'timeout': self.timeout, + 'status': self.status, + 'status_reason': self.status_reason, + 'inputs': self.inputs, + 'outputs': self.outputs, + 'depends_on': self.depends_on, + 'depended_by': self.depended_by, + 'deleted_time': self.deleted_time, + } + + action = db_api.action_create(self.context, self.id, values) + self.id = action.id + return self.id + + @classmethod + def from_db_record(cls, context, record): + ''' + Construct a action object from database record. + :param context: the context used for DB operations; + :param record: a DB action object that contains all fields. + ''' + kwargs = { + 'id': record.id, + 'name': record.name, + 'context': record.context, + 'target': record.target, + 'cause': record.cause, + 'owner': record.owner, + 'interval': record.interval, + 'start_time': record.start_time, + 'end_time': record.end_time, + 'timeout': record.timeout, + 'status': record.status, + 'status_reason': record.status_reason, + 'inputs': record.inputs, + 'outputs': record.outputs, + 'depends_on': record.depends_on, + 'depended_by': record.depended_by, + 'deleted_time': record.deleted_time, + } + + return cls(context, record.action, **kwargs) + + @classmethod + def load(cls, context, action_id): + ''' + Retrieve an action from database. + ''' + action = db_api.action_get(context, action_id) + if action is None: + msg = _('No action with id "%s" exists') % action_id + raise exception.NotFound(msg) + + return cls.from_db_record(context, action) + def execute(self, **kwargs): + ''' + Execute the action. + In theory, the action encapsulates all information needed for + execution. 'kwargs' may specify additional parameters. + :param kwargs: additional parameters that may override the default + properties stored in the action record. + ''' return NotImplemented def cancel(self): return NotImplemented - def store(self): - #db_api.action_update(self.id) - return - def set_status(self, status): ''' Set action status. @@ -161,57 +237,102 @@ class ClusterAction(Action): def __init__(self, context, action, **kwargs): super(ClusterAction, self).__init__(context, action, **kwargs) - if action not in self.ACTIONS: - raise exception.ActionNotSupported( - action=action, object=_('cluster %s') % self.target) + + def do_cluster_create(self, cluster): + # TODO(Yanyan): Check if cluster lock is needed + res = cluster.do_create() + if res is False: + return self.RES_ERROR + + for m in range(cluster.size): + name = 'node-%003d' % m + node = nodes.Node(name, cluster.profile_id, cluster.id) + node.store() + kwargs = { + 'name': 'node-create-%003d' % m, + 'context': self.context, + 'target': node.id, + 'cause': 'Cluster creation', + } + + action = Action(self.context, 'NODE_CREATE', **kwargs) + action.set_status(self.READY) + + scheduler.notify() + + def do_update(self, cluster): + # TODO(Yanyan): Check if cluster lock is needed + cluster.set_status(self.UPDATING) + node_list = cluster.get_nodes() + for node_id in node_list: + kwargs = { + 'name': 'node-update-%s' % node_id, + 'context': self.context, + 'target': node_id, + 'cause': 'Cluster update', + } + action = Action(self.context, 'NODE_UPDATE', **kwargs) + action.set_status(self.READY) + + scheduler.notify() + # TODO(Yanyan): release lock + cluster.set_status(self.ACTIVE) + + return self.RES_OK + + def do_delete(self, cluster): + # TODO(Yanyan): Check if cluster lock is needed + node_list = cluster.get_nodes() + for node_id in node_list: + kwargs = { + 'name': 'node-delete-%s' % node_id, + 'context': self.context, + 'target': node_id, + 'cause': 'Cluster update', + } + action = Action(self.context, 'NODE_UPDATE', **kwargs) + action.set_status(self.READY) + + scheduler.notify() + + return self.RES_OK + + def do_add_nodes(self, cluster): + return self.RES_OK + + def do_del_nodes(self, cluster): + return self.RES_OK + + def do_resize(self, cluster): + return self.RES_OK + + def do_attach_policy(self, cluster): + return self.RES_OK + + def do_detach_policy(self, cluster): + return self.RES_OK def execute(self, **kwargs): - ''' - Execute the action. - In theory, the action encapsulates all information needed for - execution. 'kwargs' may specify additional parameters. - :param kwargs: additional parameters that may override the default - properties stored in the action record. - ''' cluster = db_api.cluster_get(self.context, self.target) if not cluster: return self.RES_ERROR if self.action == self.CLUSTER_CREATE: - # TODO(Qiming): - # We should query the lock of cluster here and wrap - # cluster.do_create, and then let threadgroupmanager - # to start a thread for this progress. - cluster.do_create() - - for m in range(cluster.size): - name = 'node-%003s' % m - node = nodes.Node(name, cluster.profile_id, cluster.id) - node.store() - kwargs = { - 'target': node.id, - } - - action = NodeAction(context, 'NODE_CREATE', **kwargs) - action.set_status(self.READY) - - scheduler.notify() - + return self.do_create(cluster) elif self.action == self.CLUSTER_UPDATE: - # TODO(Yanyan): grab lock - cluster._set_status(self.UPDATING) - node_list = cluster.get_nodes() - for node_id in node_list: - node = db_api.node_get(node_id) - action = actions.Action(context, node, 'NODE_UPDATE', **kwargs) - - # start a thread asynchronously - handle = scheduler.runAction(action) - scheduler.wait(handle) - # TODO(Yanyan): release lock - cluster._set_status(self.ACTIVE) - - return self.RES_ERROR + return self.do_update(cluster) + elif self.action == self.CLUSTER_DELETE: + return self.do_delete(cluster) + elif self.action == self.CLUSTER_ADD_NODES: + return self.do_add_nodes(cluster) + elif self.action == self.CLUSTER_DEL_NODES: + return self.do_del_nodes(cluster) + elif self.action == self.CLUSTER_RESIZE: + return self.do_resize(cluster) + elif self.action == self.CLUSTER_ATTACH_POLICY: + return self.do_attach_policy(cluster) + elif self.action == self.CLUSTER_DETACH_POLICY: + return self.do_detach_policy(cluster) return self.RES_OK @@ -234,27 +355,83 @@ class NodeAction(Action): def __init__(self, context, action, **kwargs): super(NodeAction, self).__init__(context, action, **kwargs) - if action not in self.ACTIONS: - return self.RES_ERROR - # get cluster of this node # get policies associated with the cluster + def _get_profile_cls(self, context, profile_id): + profile = db_api.profile_get(context, profile_id) + cls = environment.global_env().get_profile(profile.type) + if not cls: + raise exception.ProfileNotFound(profile=profile.name) + return cls + + def do_create(self, node): + cls = self._get_profile_cls(self.context, node.profile_id) + node = cls.create_object(node.name, node.profile_id) + if not node: + return self.RES_ERROR + + return self.RES_OK + + def do_update(self, node): + new_profile_id = self.inputs.get('new_profile', None) + if not new_profile_id: + raise exception.ProfileNotSpecified() + + if new_profile_id == node.profile_id: + return self.RES_OK + + if not node.physical_id: + return self.RES_ERROR + + cls = self._get_profile_cls(self.context, node.profile_id) + node = cls.update_object(node.physical_id, new_profile_id) + if not node: + return self.RES_ERROR + + return self.RES_OK + + def do_delete(self, node): + if not node.physical_id: + return self.RES_OK + + cls = self._get_profile_cls(self.context, node.profile_id) + node = cls.delete_object(node.physical_id) + if not node: + return self.RES_ERROR + + return self.RES_OK + + def do_join(self, node): + new_cluster_id = self.inputs.get('cluster_id', None) + if not new_cluster_id: + raise exception.ClusterNotSpecified() + + # TODO(Qiming): complete this + return self.RES_OK + + def do_leave(self, node): + # cluster_id = node.cluster_id + # TODO(Qiming): complete this + + return self.RES_OK + def execute(self, **kwargs): + node = db_api.node_get(self.context, self.target) + if not node: + msg = _('Node with id (%s) is not found') % self.target + raise exception.NotFound(msg) + if self.action == self.NODE_CREATE: - profile_id = kwargs.get('profile_id') - name = kwargs.get('name') - profile = db_api.profile_get(self.context, profile_id) - node = profile.create_object(name, profile_id) - if not node: - return self.RES_ERROR + return self.do_create(node) elif self.action == self.NODE_DELETE: - node_id = self.target - profile.delete_object(node_id) + return self.do_delete(node) elif self.action == self.NODE_UPDATE: - node_id = self.target - profile_id = kwargs.get('profile_id') - profile.update_object(node_id, profile_id) + return self.do_update(node) + elif self.action == self.NODE_JOIN_CLUSTER: + return self.do_join(node) + elif self.action == self.NODE_LEAVE_CLUSTER: + return self.do_leave(node) else: return self.RES_ERROR From e641f7a49883f625ff59cb17b1b40367d5669cbc Mon Sep 17 00:00:00 2001 From: tengqm Date: Thu, 1 Jan 2015 11:56:54 +0800 Subject: [PATCH 36/59] Initial version --- doc/source/architecture.rst | 77 +++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 doc/source/architecture.rst diff --git a/doc/source/architecture.rst b/doc/source/architecture.rst new file mode 100644 index 000000000..40ff6ddae --- /dev/null +++ b/doc/source/architecture.rst @@ -0,0 +1,77 @@ +.. + 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. + +Senlin Architecture +=================== + +Senlin is a service to create and manage clusters of homogeneous resources in +an OpenStack cloud. Senlin provides an OpenStack-native ReST API. + + +-------------------- +Detailed Description +-------------------- + +What is the purpose of the project and vision for it? + +*Senlin provides a clustering service for OpenStack that manages a collection +of nodes that are of the same type.* + +Describe the relevance of the project to other OpenStack projects and the +OpenStack mission to provide a ubiquitous cloud computing platform: + +*The Senlin service aggregates resources exposed by other components of +OpenStack into a cluster. Such a cluster can be associated with different +policies that can be checked/enforced at varying enforcement levels. Through +service APIs, a user can dynamically add nodes to and remove nodes from a +cluster, attach and detach policies, such as creation policy, deletion policy, +load-balancing policy, scaling policy, health checking policy etc. Through +integration with other OpenStack projects, users will be enabled to manage +deployments and orchestrations large scale resource pools much easier.* +*Currently no other clustering service exists for OpenStack. The developers +believe cloud developers have a strong desire to create and operate resource +clusters on OpenStack deployments. The Heat project provides a preliminary +support to resource groups but Heat developers have achieved a consensus that +such kind of a service should stand on its own feet.* + +--------------- +Senlin Services +--------------- + +The developers are focusing on creating an OpenStack style project using +OpenStack design tenets, implemented in Python. We have started with a close +interaction with Heat project. + +As the developers have only started development in December 2014, the +architecture is evolving rapidly. + +senlin +------ + +The senlin tool is a CLI which communicates with the senlin-api to manage +clusters, nodes, profiles, policies and events. End developers could also use +the Senlin REST API directly. + + +senlin-api +---------- + +The senlin-api component provides an OpenStack-native REST API that processes +API requests by sending them to the senlin-engine over RPC. + + +senlin-engine +------------- + +The senlin engine's main responsibility is to orchestrate the clusters, nodes, +profiles and policies. From c73d780e9daaaa9669f9197bf52f2c3a2def809b Mon Sep 17 00:00:00 2001 From: tengqm Date: Thu, 1 Jan 2015 14:25:13 +0800 Subject: [PATCH 37/59] Initial version --- doc/source/index.rst | 96 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 doc/source/index.rst diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 000000000..4aa3cf67e --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,96 @@ +.. + 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. + +============================================== +Welcome to the Senlin developer documentation! +============================================== + +Senlin is a service to create and manage :term:`cluster` of multiple cloud +resources. Senlin provides an OpenStack-native ReST API and a AWS +AutoScaling-compatible Query API is in plan. + +What is the purpose of the project and vision for it? +===================================================== + +* Senlin provides a clustering solution for :term:`OpenStack` cloud. A user + can create clusters of :term:`node` and associate :term:`policy` to such + a cluster. +* The software interacts with other components of OpenStack so that clusters + of resources exposed by those components can be created and operated. +* The software complements Heat project each other so Senlin can create and + manage clusters of Heat stacks while Heat can invoke Senlin APIs to + orchestrate collections of homogeneous resources. +* Senlin provides policies as plugins that can be used to specify how clusters + operate. Example policies include creation policy, placement policy, + deletion policy, load-balancing policy, scaling policy etc. +* Senlin can interact with all other OpenStack components via :term:`profile` + plugins. Each profile type implementation enable Senlin to create resources + provided by a corresponding OpenStack service. + +This documentation offers information on how Senlin works and how to +contribute to the project. + +Getting Started +=============== + +.. toctree:: + :maxdepth: 1 + + getting_started/index + policies/index + profiles/index + glossary + +Man Pages +========= + +.. toctree:: + :maxdepth: 2 + + man/index + +Developers Documentation +======================== +.. toctree:: + :maxdepth: 1 + + architecture + pluginguide + +API Documentation +======================== + +- `Senlin REST API Reference (OpenStack API Complete Reference - Clustering)`_ + + .. _`Senlin REST API Reference (OpenStack API Complete Reference - Clustering)`: http://api.openstack.org/api-ref-clustering-v1.html + +Operations Documentation +======================== +.. toctree:: + :maxdepth: 1 + + scale_deployment + +Code Documentation +================== +.. toctree:: + :maxdepth: 3 + + sourcecode/autoindex + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` From 2547595eb2a2675fb8a3693a4c8e44a1b7adea07 Mon Sep 17 00:00:00 2001 From: tengqm Date: Thu, 1 Jan 2015 23:19:21 +0800 Subject: [PATCH 38/59] Added node_migrate API The 'node_migrate' API is used to change the cluster_id of a node. The 'from_cluster' can be None, which means the node was an orphan node before joining a new cluster ('to_cluster'). The 'to_cluster' can be None, which means the node is leaving its current cluster ('from_cluster'). When both 'from_cluster' and 'to_cluster' are not None, a node is migrated from the former one to the later one. --- senlin/db/api.py | 5 +++ senlin/db/sqlalchemy/api.py | 69 +++++++++++++++++++++++-------------- 2 files changed, 48 insertions(+), 26 deletions(-) diff --git a/senlin/db/api.py b/senlin/db/api.py index a29d9e069..3e6632b94 100644 --- a/senlin/db/api.py +++ b/senlin/db/api.py @@ -113,6 +113,10 @@ def node_set_status(context, node_id, status): return IMPL.node_set_status(context, node_id, status) +def node_migrate(context, node_id, from_cluster, to_cluster): + return IMPL.node_migrate(context, node_id, from_cluster, to_cluster) + + # Locks def cluster_lock_create(cluster_id, worker_id): return IMPL.cluster_lock_create(cluster_id, worker_id) @@ -222,6 +226,7 @@ def event_get_all_by_cluster(context, cluster_id, limit=None, marker=None, sort_dir=sort_dir, filters=filters) + # Actions def action_create(context, values): return IMPL.action_create(context, values) diff --git a/senlin/db/sqlalchemy/api.py b/senlin/db/sqlalchemy/api.py index 5be61fc42..03a1fa09e 100644 --- a/senlin/db/sqlalchemy/api.py +++ b/senlin/db/sqlalchemy/api.py @@ -33,8 +33,9 @@ CONF = cfg.CONF CONF.import_opt('max_events_per_cluster', 'senlin.common.config') # Action status definitions: -# ACTION_INIT: Not ready to be executed because fields are being modified, -# or dependency with other actions are being analyzed. +# ACTION_INIT: Not ready to be executed because fields are being +# modified, or dependency with other actions are being +# analyzed. # ACTION_READY: Initialized and ready to be executed by a worker. # ACTION_RUNNING: Being executed by a worker thread. # ACTION_SUCCEEDED: Completed with success. @@ -136,7 +137,7 @@ def cluster_get_all_by_parent(context, parent): def cluster_get_by_name_and_parent(context, cluster_name, parent): query = soft_delete_aware_query(context, models.Cluster).\ - filter_by(tenant == context.tenant_id).\ + filter_by(tenant=context.tenant_id).\ filter_by(name=cluster_name).\ filter_by(parent=parent) return query.first() @@ -228,8 +229,8 @@ def cluster_update(context, cluster_id, values): if not cluster: raise exception.NotFound( - _('Attempt to update a cluster with id "%s" that does not' - ' exist') % cluster_id) + _('Attempt to update a cluster with id "%s" that does ' + ' exist failed') % cluster_id) cluster.update(values) cluster.save(_session(context)) @@ -239,8 +240,8 @@ def cluster_delete(context, cluster_id): cluster = cluster_get(context, cluster_id) if not cluster: raise exception.NotFound( - _('Attempt to delete a cluster with id "%s" that does not' - ' exist') % cluster_id) + _('Attempt to delete a cluster with id "%s" that does ' + 'not exist failed') % cluster_id) session = orm_session.Session.object_session(cluster) @@ -298,6 +299,21 @@ def node_get_by_physical_id(context, phy_id): return query.first() +def node_migrate(context, node_id, from_cluster, to_cluster): + query = model_query(context, models.Node) + node = query.get(node_id) + session = query.session + session.begin() + if from_cluster: + cluster1 = session.query(models.Cluster).get(from_cluster) + cluster1.size -= 1 + if to_cluster: + cluster2 = session.query(models.Cluster).get(to_cluster) + cluster2.size += 1 + node.cluster_id = to_cluster + session.commit() + + # Locks def cluster_lock_create(cluster_id, worker_id): session = get_session() @@ -394,8 +410,8 @@ def policy_update(context, policy_id, values): policy = policy_get(context, policy_id) if not policy: - msg = _('Attempt to update a policy with id: %(id)s that does not' - ' exist') % policy_id + msg = _('Attempt to update a policy with id: %(id)s that does not ' + 'exist failed') % policy_id raise exception.NotFound(msg) policy.update(values) @@ -407,8 +423,8 @@ def policy_delete(context, policy_id, force=False): policy = policy_get(context, policy_id) if not policy: - msg = _('Attempt to delete a policy with id "%s" that does not' - ' exist') % policy_id + msg = _('Attempt to delete a policy with id "%s" that does not ' + 'exist failed') % policy_id raise exception.NotFound(msg) session = orm_session.Session.object_session(policy) @@ -438,9 +454,9 @@ def cluster_detach_policy(context, cluster_id, policy_id): filter(cluster_id=cluster_id, policy_id=policy_id) if not binding: - msg = _('Failed detach policy "%(policy)s" from cluster ' - '"%(cluster)s"') % {'policy': policy_id, - 'cluster': cluster_id} + msg = _('Failed detaching policy "%(policy)s" from cluster ' + '"%(cluster)s"') % {'policy': policy_id, + 'cluster': cluster_id} raise exception.NotFound(msg) session = orm_session.Session.object_session(binding) @@ -454,8 +470,8 @@ def cluster_enable_policy(context, cluster_id, policy_id): if not binding: msg = _('Failed enabling policy "%(policy)s" on cluster ' - '"%(cluster)s"') % {'policy': policy_id, - 'cluster': cluster_id} + '"%(cluster)s"') % {'policy': policy_id, + 'cluster': cluster_id} raise exception.NotFound(msg) @@ -470,8 +486,8 @@ def cluster_disable_policy(context, cluster_id, policy_id): if not binding: msg = _('Failed disabling policy "%(policy)s" on cluster ' - '"%(cluster)s"') % {'policy': policy_id, - 'cluster': cluster_id} + '"%(cluster)s"') % {'policy': policy_id, + 'cluster': cluster_id} raise exception.NotFound(msg) binding.update(enabled=False) @@ -602,7 +618,7 @@ def _events_filter_and_page_query(context, query, limit=None, marker=None, def event_count_by_cluster(context, cid): count = model_query(context, models.Event).\ filter_by(obj_id=cid, obj_type='CLUSTER').count() - return count + return count def _events_by_cluster(context, cid): @@ -665,12 +681,13 @@ def purge_deleted(age, granularity='days'): # user_creds_del = user_creds.delete().where(user_creds.c.id == s[2]) # engine.execute(user_creds_del) + # Actions def action_create(context, values): action = models.Action() action.update(values) action.save(_session(context)) - return action + return action def action_get(context, action_id): @@ -683,7 +700,7 @@ def action_get(context, action_id): def action_get_1st_ready(context): query = model_query(context, models.Action).\ - filter_by(status == ACTION_READY) + filter_by(status=ACTION_READY) return query.first() @@ -693,8 +710,7 @@ def action_get_all_ready(context): def action_get_all_by_owner(context, owner_id): - query = model_query(context, models.Action).\ - filter_by(owner == owner_id) + query = model_query(context, models.Action).filter_by(owner=owner_id) return query.all() @@ -753,7 +769,8 @@ def action_del_depended_by(context, action_id, *actions): def action_mark_succeeded(context, action_id): - action = model_query(context, models.Action).get(action_id) + query = model_query(context, models.Action) + action = query.get(action_id) if not action: raise exception.NotFound( _('Action with id "%s" not found') % action_id) @@ -788,9 +805,9 @@ def action_start_work_on(context, action_id, owner): raise exception.NotFound( _('Action with id "%s" not found') % action_id) - action.owner = owner + action.owner = owner action.status = ACTION_RUNNING - action.status_reason = _('The action is being processing.') + action.status_reason = _('The action is being processed.') action.save(_session(context)) return action From 11c643d089e50d9e77e7b49dff4cd222a68e0a27 Mon Sep 17 00:00:00 2001 From: tengqm Date: Thu, 1 Jan 2015 23:57:29 +0800 Subject: [PATCH 39/59] Revised execute() logics Both ClusterAction and NodeAction were revised. --- senlin/engine/action.py | 103 ++++++++++------------------------------ 1 file changed, 24 insertions(+), 79 deletions(-) diff --git a/senlin/engine/action.py b/senlin/engine/action.py index 6e6cced6d..85cb9f433 100644 --- a/senlin/engine/action.py +++ b/senlin/engine/action.py @@ -259,6 +259,7 @@ class ClusterAction(Action): action.set_status(self.READY) scheduler.notify() + return self.RES_OK def do_update(self, cluster): # TODO(Yanyan): Check if cluster lock is needed @@ -313,28 +314,29 @@ class ClusterAction(Action): return self.RES_OK def execute(self, **kwargs): + res = self.RES_ERROR cluster = db_api.cluster_get(self.context, self.target) if not cluster: - return self.RES_ERROR + return res if self.action == self.CLUSTER_CREATE: - return self.do_create(cluster) + res = self.do_create(cluster) elif self.action == self.CLUSTER_UPDATE: - return self.do_update(cluster) + res = self.do_update(cluster) elif self.action == self.CLUSTER_DELETE: - return self.do_delete(cluster) + res = self.do_delete(cluster) elif self.action == self.CLUSTER_ADD_NODES: - return self.do_add_nodes(cluster) + res = self.do_add_nodes(cluster) elif self.action == self.CLUSTER_DEL_NODES: - return self.do_del_nodes(cluster) + res = self.do_del_nodes(cluster) elif self.action == self.CLUSTER_RESIZE: - return self.do_resize(cluster) + res = self.do_resize(cluster) elif self.action == self.CLUSTER_ATTACH_POLICY: - return self.do_attach_policy(cluster) + res = self.do_attach_policy(cluster) elif self.action == self.CLUSTER_DETACH_POLICY: - return self.do_detach_policy(cluster) + res = self.do_detach_policy(cluster) - return self.RES_OK + return res def cancel(self): return self.RES_OK @@ -355,85 +357,28 @@ class NodeAction(Action): def __init__(self, context, action, **kwargs): super(NodeAction, self).__init__(context, action, **kwargs) - # get cluster of this node - # get policies associated with the cluster - - def _get_profile_cls(self, context, profile_id): - profile = db_api.profile_get(context, profile_id) - cls = environment.global_env().get_profile(profile.type) - if not cls: - raise exception.ProfileNotFound(profile=profile.name) - return cls - - def do_create(self, node): - cls = self._get_profile_cls(self.context, node.profile_id) - node = cls.create_object(node.name, node.profile_id) - if not node: - return self.RES_ERROR - - return self.RES_OK - - def do_update(self, node): - new_profile_id = self.inputs.get('new_profile', None) - if not new_profile_id: - raise exception.ProfileNotSpecified() - - if new_profile_id == node.profile_id: - return self.RES_OK - - if not node.physical_id: - return self.RES_ERROR - - cls = self._get_profile_cls(self.context, node.profile_id) - node = cls.update_object(node.physical_id, new_profile_id) - if not node: - return self.RES_ERROR - - return self.RES_OK - - def do_delete(self, node): - if not node.physical_id: - return self.RES_OK - - cls = self._get_profile_cls(self.context, node.profile_id) - node = cls.delete_object(node.physical_id) - if not node: - return self.RES_ERROR - - return self.RES_OK - - def do_join(self, node): - new_cluster_id = self.inputs.get('cluster_id', None) - if not new_cluster_id: - raise exception.ClusterNotSpecified() - - # TODO(Qiming): complete this - return self.RES_OK - - def do_leave(self, node): - # cluster_id = node.cluster_id - # TODO(Qiming): complete this - - return self.RES_OK - def execute(self, **kwargs): - node = db_api.node_get(self.context, self.target) + res = self.RES_ERROR + node = nodes.load(self.context, self.target) if not node: msg = _('Node with id (%s) is not found') % self.target raise exception.NotFound(msg) + # TODO(Qiming): Add node status changes if self.action == self.NODE_CREATE: - return self.do_create(node) + res = node.do_create() elif self.action == self.NODE_DELETE: - return self.do_delete(node) + res = node.do_delete() elif self.action == self.NODE_UPDATE: - return self.do_update(node) + new_profile_id = self.inputs.get('new_profile') + res = node.do_update(new_profile_id) elif self.action == self.NODE_JOIN_CLUSTER: - return self.do_join(node) + new_cluster_id = self.inputs.get('cluster_id', None) + if not new_cluster_id: + raise exception.ClusterNotSpecified() + res = node.do_join(new_cluster_id) elif self.action == self.NODE_LEAVE_CLUSTER: - return self.do_leave(node) - else: - return self.RES_ERROR + res = node.do_leave() return self.RES_OK From 572a2cf44e63937d0b55f866b23d88f79401df88 Mon Sep 17 00:00:00 2001 From: tengqm Date: Fri, 2 Jan 2015 00:01:21 +0800 Subject: [PATCH 40/59] Fixed flake8 errors --- senlin/engine/action.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/senlin/engine/action.py b/senlin/engine/action.py index 85cb9f433..0e53b2dad 100644 --- a/senlin/engine/action.py +++ b/senlin/engine/action.py @@ -17,7 +17,6 @@ from oslo.config import cfg from senlin.common import exception from senlin.db import api as db_api -from senlin.engine import environment from senlin.engine import node as nodes from senlin.engine import scheduler @@ -314,10 +313,10 @@ class ClusterAction(Action): return self.RES_OK def execute(self, **kwargs): - res = self.RES_ERROR + res = False cluster = db_api.cluster_get(self.context, self.target) if not cluster: - return res + return self.RES_ERROR if self.action == self.CLUSTER_CREATE: res = self.do_create(cluster) @@ -336,7 +335,7 @@ class ClusterAction(Action): elif self.action == self.CLUSTER_DETACH_POLICY: res = self.do_detach_policy(cluster) - return res + return self.RES_OK if res else self.RES_ERROR def cancel(self): return self.RES_OK @@ -358,7 +357,7 @@ class NodeAction(Action): super(NodeAction, self).__init__(context, action, **kwargs) def execute(self, **kwargs): - res = self.RES_ERROR + res = False node = nodes.load(self.context, self.target) if not node: msg = _('Node with id (%s) is not found') % self.target @@ -380,7 +379,7 @@ class NodeAction(Action): elif self.action == self.NODE_LEAVE_CLUSTER: res = node.do_leave() - return self.RES_OK + return self.RES_OK if res else self.RES_ERROR def cancel(self): return self.RES_OK From 230026626432ef8605582d444db6b21462869007 Mon Sep 17 00:00:00 2001 From: tengqm Date: Fri, 2 Jan 2015 00:09:13 +0800 Subject: [PATCH 41/59] Rough implementation of Node class - Removed cluster_id from positional arguments of __init__ - Added runtime data (self.rt.profile) - Implemented do_create, do_delete, do_update, do_join, do_leave methods. --- senlin/engine/node.py | 111 ++++++++++++++++++++++++++---------------- 1 file changed, 70 insertions(+), 41 deletions(-) diff --git a/senlin/engine/node.py b/senlin/engine/node.py index 52692e406..b8c587ae3 100644 --- a/senlin/engine/node.py +++ b/senlin/engine/node.py @@ -14,7 +14,7 @@ import datetime from senlin.common import exception from senlin.db import api as db_api -from senlin.engine import environment +from senlin.profiles import base as profiles class Node(object): @@ -32,7 +32,8 @@ class Node(object): 'INITIALIZING', 'ACTIVE', 'ERROR', 'DELETED', 'UPDATING', ) - def __init__(self, name, profile_id, cluster_id=None, **kwargs): + def __init__(self, context, name, profile_id, **kwargs): + self.context = context self.id = kwargs.get('id', None) if name: self.name = name @@ -43,7 +44,7 @@ class Node(object): self.physical_id = kwargs.get('physical_id', '') self.profile_id = profile_id - self.cluster_id = cluster_id or '' + self.cluster_id = kwargs.get('cluster_id', '') self.index = kwargs.get('index', -1) self.role = kwargs.get('role', '') self.created_time = kwargs.get('created_time', None) @@ -55,6 +56,10 @@ class Node(object): self.data = kwargs.get('data', {}) self.tags = kwargs.get('tags', {}) + self.rt = { + 'profile': profiles.load(context, self.profile_id), + } + def store(self): ''' Store the node record into database table. @@ -109,21 +114,20 @@ class Node(object): 'data': record.data, 'tags': record.tags, } - return cls(context, record.name, record.profile_id, record.size, - **kwargs) + return cls(context, record.name, record.profile_id, **kwargs) @classmethod def load(cls, context, node_id): ''' Retrieve a node from database. ''' - node = db_api.node_get(context, node_id) + record = db_api.node_get(context, node_id) - if node is None: + if record is None: msg = _('No node with id "%s" exists') % node_id raise exception.NotFound(msg) - return cls.from_db_record(context, node) + return cls.from_db_record(context, record) @classmethod def load_all(cls, context, cluster_id): @@ -135,44 +139,69 @@ class Node(object): for record in records: yield cls.from_db_record(context, record) - def create(self, name, profile_id, cluster_id=None, **kwargs): - # TODO(Qiming): invoke profile to create new object and get the - # physical id + def do_create(self): # TODO(Qiming): log events? self.created_time = datetime.datetime.utcnnow() - profile = db_api.get_profile(profile_id) - profile_cls = environment.global_env().get_profile(profile.type) - node = profile_cls.create_object(self.id, profile_id) + res = profiles.create_object(self) + if res: + self.physical_id = res + return True + else: + return False - return node.id - - def delete(self): - # node = db_api.node_get(self.id) - # physical_id = node.physical_id - - # TODO(Qiming): invoke profile to delete this object - # TODO(Qiming): check if actions are working on it and can be canceled - - db_api.delete_node(self.id) - return True - - def join(self, cluster): - return True - - def leave(self, cluster): - return True - - def update(self, new_profile_id): - new_profile = db_api.get_profile(new_profile_id) - - if self.profile_id == new_profile.id: + def do_delete(self): + if not self.physical_id: return True - new_type = new_profile.type_name + # TODO(Qiming): check if actions are working on it and can be canceled + # TODO(Qiming): log events + res = profiles.delete_object(self) + if res: + db_api.delete_node(self.id) + return True + else: + return False - profile_cls = environment.global_env().get_profile(new_type) + def do_update(self, new_profile_id): + if not new_profile_id: + raise exception.ProfileNotSpecified() + + if new_profile_id == self.profile_id: + return True + + if not self.physical_id: + return False + + res = profiles.update_object(self, new_profile_id) + if res: + self.rt['profile'] = profiles.load(self.context, new_profile_id) + self.profile_id = new_profile_id + self.updated_time = datetime.datetime.utcnow() + db_api.node_update(self.context, self.id, + {'profile_id': self.profile_id, + 'updated_time': self.updated_time}) + + return res + + def do_join(self, cluster_id): + if self.cluster_id == cluster_id: + return True + + db_api.node_migrate(self.context, self.id, self.cluster_id, + cluster_id) + + self.updated_time = datetime.datetime.utcnow() + db_api.node_update(self.context, self.id, + {'updated_time': self.updated_time}) + return True + + def do_leave(self): + if self.cluster_id is None: + return True + + db_api.node_migrate(self.context, self.id, self.cluster_id, None) + self.updated_time = datetime.datetime.utcnow() + db_api.node_update(self.context, self.id, + {'updated_time': self.updated_time}) - profile_cls.update_object(self.id, new_profile) - self.profile_id = new_profile - self.updated_time = datetime.utcnow() return True From 3f7882df43f9fd236f680c9aa0ec4470ff17815e Mon Sep 17 00:00:00 2001 From: tengqm Date: Fri, 2 Jan 2015 00:13:31 +0800 Subject: [PATCH 42/59] Minor revision - Revised __init__ method to remove context from positional arguments; - Added runtime data including profile and nodes --- senlin/engine/cluster.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/senlin/engine/cluster.py b/senlin/engine/cluster.py index add82d3b1..e458bb6e9 100644 --- a/senlin/engine/cluster.py +++ b/senlin/engine/cluster.py @@ -18,6 +18,7 @@ from senlin.common.i18n import _LW from senlin.db import api as db_api from senlin.engine import event as events from senlin.engine import node as nodes +from senlin.profiles import base as profiles from senlin.rpc import api as rpc_api @@ -36,12 +37,12 @@ class Cluster(object): 'INIT', 'ACTIVE', 'ERROR', 'DELETED', 'UPDATING', ) - def __init__(self, name, profile_id, size=0, **kwargs): + def __init__(self, context, name, profile_id, size=0, **kwargs): ''' Intialize a cluster object. The cluster defaults to have 0 nodes with no profile assigned. ''' - self.context = kwargs.get('context', {}) + self.context = context self.id = kwargs.get('id', None) self.name = name self.profile_id = profile_id @@ -68,7 +69,12 @@ class Cluster(object): self.tags = kwargs.get('tags', {}) # rt is a dict for runtime data - self.rt = dict(nodes={}, policies={}) + # TODO(Qiming): nodes have to be reloaded when membership changes + self.rt = { + 'profile': profiles.load(context, self.profile_id), + 'nodes': nodes.load_all(context, cluster_id=self.id), + 'policies': {}, + } @classmethod def from_db_record(cls, context, record): @@ -108,7 +114,7 @@ class Cluster(object): msg = _('No cluster with id "%s" exists') % cluster_id raise exception.NotFound(msg) - return cls._from_db_record(context, cluster) + return cls.from_db_record(context, cluster) @classmethod def load_all(cls, context, limit=None, sort_keys=None, marker=None, @@ -122,7 +128,7 @@ class Cluster(object): show_deleted, show_nested) for record in records: - yield cls._from_db_record(context, record) + yield cls.from_db_record(context, record) def store(self): ''' @@ -182,6 +188,7 @@ class Cluster(object): A routine to be called from an action. ''' self._set_status(self.ACTIVE) + return True def do_delete(self, context, **kwargs): self.status = self.DELETED @@ -262,7 +269,6 @@ class Cluster(object): def update(cls, cluster_id, profile): # cluster = db_api.get_cluster(cluster_id) # TODO(Qiming): Implement this - return True def to_dict(self): From a862b30742d22464c0df2f8c365ee3b9d49975bf Mon Sep 17 00:00:00 2001 From: tengqm Date: Fri, 2 Jan 2015 00:14:54 +0800 Subject: [PATCH 43/59] Fixed global registry intialization --- senlin/engine/environment.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/senlin/engine/environment.py b/senlin/engine/environment.py index 69bda003a..34d0e724c 100644 --- a/senlin/engine/environment.py +++ b/senlin/engine/environment.py @@ -55,8 +55,14 @@ class Environment(object): :param is_global: boolean indicating if this is a user created one. ''' self.params = {} - self.profile_registry = registry.Registry('profiles', is_global) - self.policy_registry = registry.Registry('policies', is_global) + if is_global: + self.profile_registry = registry.Registry('profiles') + self.policy_registry = registry.Registry('policies') + else: + self.profile_registry = registry.Registry( + 'profiles', global_env.profile_registry) + self.policy_registry = registry.Registry( + 'policies', global_env.policy_registry) if env is None: env = {} From 2cc721f24ea4c2b70238551ca634bfa3ab94ec74 Mon Sep 17 00:00:00 2001 From: tengqm Date: Fri, 2 Jan 2015 00:16:35 +0800 Subject: [PATCH 44/59] Fixed flake8 errors --- senlin/engine/environment.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/senlin/engine/environment.py b/senlin/engine/environment.py index 34d0e724c..aa8885728 100644 --- a/senlin/engine/environment.py +++ b/senlin/engine/environment.py @@ -12,6 +12,7 @@ import glob import os.path +import six from stevedore import extension from oslo.config import cfg @@ -67,7 +68,7 @@ class Environment(object): if env is None: env = {} else: - # Merge user specified keys with current environment + # Merge user specified keys with current environment self.params = env.get(self.PARAMETERS, {}) custom_profiles = env.get(self.CUSTOM_PROFILES, {}) custom_policies = env.get(self.CUSTOM_POLICIES, {}) From 1f9bd50fdadbd1932c0769aa2be9abc60d0271b8 Mon Sep 17 00:00:00 2001 From: tengqm Date: Fri, 2 Jan 2015 00:17:20 +0800 Subject: [PATCH 45/59] Fixed initialization of global registries --- senlin/engine/registry.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/senlin/engine/registry.py b/senlin/engine/registry.py index a489c51f3..328f698a9 100644 --- a/senlin/engine/registry.py +++ b/senlin/engine/registry.py @@ -15,7 +15,6 @@ import six from senlin.common.i18n import _LI from senlin.common.i18n import _LW -from senlin.engine import environment from senlin.openstack.common import log LOG = log.getLogger(__name__) @@ -70,11 +69,10 @@ class Registry(object): A registry for managing profile or policy classes. ''' - def __init__(self, registry_name, is_global): + def __init__(self, registry_name, global_registry=None): self._registry = {registry_name: {}} - self.is_global = is_global - global_registry = environment.global_env().registry - self.global_registry = None if is_global else global_registry + self.is_global = True if global_registry else False + self.global_registry = global_registry def _register_info(self, path, info): ''' From edd5080efa47c23cfb2fc89d43008d275739b21e Mon Sep 17 00:00:00 2001 From: tengqm Date: Fri, 2 Jan 2015 00:20:44 +0800 Subject: [PATCH 46/59] Rewriting base Profile class - Revised __new__ method to allow for polymorphism; - Added DB 'serialization' supports; - Simplified object life cycle methods. --- senlin/profiles/base.py | 100 +++++++++++++++++++++++++++------------- 1 file changed, 67 insertions(+), 33 deletions(-) diff --git a/senlin/profiles/base.py b/senlin/profiles/base.py index f1e97087d..4fc466b8c 100644 --- a/senlin/profiles/base.py +++ b/senlin/profiles/base.py @@ -10,64 +10,99 @@ # License for the specific language governing permissions and limitations # under the License. -import collections -import uuid +from senlin.common import exception +from senlin.db import api as db_api +from senlin.engine import environment from senlin.openstack.common import log as logging LOG = logging.getLogger(__name__) -class ProfileBase(object): +class Profile(object): ''' Base class for profiles. ''' - def __new__(cls, profile, *args, **kwargs): + def __new__(cls, type_name, name, **kwargs): ''' Create a new profile of the appropriate class. ''' - global _profile_classes - if _profile_classes is None: - mgr = extension.ExtensionManager(name_space='senlin.profiles', - invoke_on_load=False, - verify_requirements=True) - _profile_classes = dict((tuple(name.split('.')), mgr[name].plugin) - for name in mgr.names()) - - if cls != ProfileBase: + if cls != Profile: ProfileClass = cls else: - ProfileClass = get_profile_class(profile) + ProfileClass = environment.global_env().get_profile(type_name) return super(Profile, cls).__new__(ProfileClass) - def __init__(self, name, type_name, **kwargs): + def __init__(self, type_name, name, **kwargs): ''' Initialize the profile with given parameters and a JSON object. ''' self.name = name - self.type_name = type_name - self.permission = '' - self.spec = kwargs.get('spec') - self.tags = {} + self.type = type_name + self.id = kwargs.get('id', None) + self.permission = kwargs.get('permission', '') + self.spec = kwargs.get('spec', {}) + self.tags = kwargs.get('tags', {}) + self.deleted_time = kwargs.get('deleted_time', None) @classmethod - def create_object(cls, name, type_name, **kwargs): - obj = cls(name, type_name, kwargs) - physical_id = obj.do_create() - return physical_id + def from_db_record(cls, context, record): + ''' + Construct a profile object from database record. + :param context: the context used for DB operations. + :param record: a DB Profle object that contains all required fields. + ''' + kwargs = { + 'id': record.id, + 'spec': record.spec, + 'permission': record.permission, + 'tags': record.tags, + 'deleted_time': record.deleted_time, + } + + return cls(record.type, record.name, **kwargs) @classmethod - def delete_object(cls, physical_id): - obj = db_api.load_member(physical_id=physical_id) - result = obj.do_delete() - return result + def load(cls, context, profile_id): + ''' + Retrieve a profile object from database. + ''' + profile = db_api.profile_get(context, profile_id) + if profile is None: + msg = _('No profile with id "%s" exists') % profile_id + raise exception.NotFound(msg) + + return cls.from_db_record(context, profile) + + def store(self): + ''' + Store the profile into database and return its ID. + ''' + values = { + 'name': self.name, + 'type': self.type, + 'spec': self.spec, + 'permission': self.permission, + 'tags': self.tags, + } + profile = db_api.profile_create(self.context, values) + return profile.id @classmethod - def update_object(cls, physical_id, new_profile): - obj = db_api.load_member(physical_id=physical_id) - result = obj.do_update() - return result + def create_object(cls, obj): + profile = cls.from_db(obj.context, obj.profile_id) + return profile.do_create(obj) + + @classmethod + def delete_object(cls, obj): + profile = cls.from_db(obj.context, obj.profile_id) + return profile.do_delete(obj) + + @classmethod + def update_object(cls, obj, new_profile_id): + profile = cls.from_db(obj.context, obj.profile_id) + return profile.do_update(obj, new_profile_id) def do_create(self): ''' @@ -99,5 +134,4 @@ class ProfileBase(object): @classmethod def from_dict(cls, **kwargs): - pb = cls(kwargs) - return pb + return cls(kwargs) From e86abb7cd2c1dcfec0543c96ce5c1fd49394cd4c Mon Sep 17 00:00:00 2001 From: tengqm Date: Fri, 2 Jan 2015 00:24:48 +0800 Subject: [PATCH 47/59] Added three new exception types --- senlin/common/exception.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/senlin/common/exception.py b/senlin/common/exception.py index ec6a08b61..ece9af68a 100644 --- a/senlin/common/exception.py +++ b/senlin/common/exception.py @@ -211,6 +211,18 @@ class ClusterExists(SenlinException): msg_fmt = _("The Cluster (%(cluster_name)s) already exists.") +class ClusterNotSpecified(SenlinException): + msg_fmt = _("The cluster was not specified.") + + +class ProfileNotFound(SenlinException): + msg_fmt = _("The profile (%(profile)s) could not be found.") + + +class ProfileNotSpecicified(SenlinException): + msg_fmt = _("Profile not specified.") + + class ProfileValidationFailed(SenlinException): msg_fmt = _("%(message)s") From 4cffbaa51d828ea87c2234978d63834942f48334 Mon Sep 17 00:00:00 2001 From: tengqm Date: Fri, 2 Jan 2015 00:25:46 +0800 Subject: [PATCH 48/59] Fixed typo errors --- senlin/common/exception.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/senlin/common/exception.py b/senlin/common/exception.py index ece9af68a..f8af0c405 100644 --- a/senlin/common/exception.py +++ b/senlin/common/exception.py @@ -219,7 +219,7 @@ class ProfileNotFound(SenlinException): msg_fmt = _("The profile (%(profile)s) could not be found.") -class ProfileNotSpecicified(SenlinException): +class ProfileNotSpecified(SenlinException): msg_fmt = _("Profile not specified.") From 10752d3782af125adccf3134dc9682415f059795 Mon Sep 17 00:00:00 2001 From: tengqm Date: Fri, 2 Jan 2015 00:26:44 +0800 Subject: [PATCH 49/59] Remove license text --- senlin/profiles/__init__.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/senlin/profiles/__init__.py b/senlin/profiles/__init__.py index c86d0f806..8b1378917 100644 --- a/senlin/profiles/__init__.py +++ b/senlin/profiles/__init__.py @@ -1,16 +1 @@ -# 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. - -from stevedore import extension - - From 27a872af3304027ac3d1c5f3ed19db169f7670a4 Mon Sep 17 00:00:00 2001 From: tengqm Date: Fri, 2 Jan 2015 09:24:05 +0800 Subject: [PATCH 50/59] Remove openstack.common.uuidutils --- openstack-common.conf | 1 - senlin/engine/service.py | 2 +- senlin/openstack/common/uuidutils.py | 37 ---------------------------- 3 files changed, 1 insertion(+), 39 deletions(-) delete mode 100644 senlin/openstack/common/uuidutils.py diff --git a/openstack-common.conf b/openstack-common.conf index 19b91edc1..a8b9f7fac 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -8,7 +8,6 @@ module=loopingcall module=policy module=service module=threadgroup -module=uuidutils module=middleware.request_id # The base module to hold the copy of openstack.common diff --git a/senlin/engine/service.py b/senlin/engine/service.py index 9f0e26655..45d9fefa9 100644 --- a/senlin/engine/service.py +++ b/senlin/engine/service.py @@ -16,6 +16,7 @@ import functools import eventlet from oslo.config import cfg from oslo import messaging +from oslo.utils import uuidutils from osprofiler import profiler from senlin.common import context @@ -30,7 +31,6 @@ from senlin.engine import senlin_lock from senlin.engine import thread_mgr from senlin.openstack.common import log as logging from senlin.openstack.common import service -from senlin.openstack.common import uuidutils LOG = logging.getLogger(__name__) diff --git a/senlin/openstack/common/uuidutils.py b/senlin/openstack/common/uuidutils.py deleted file mode 100644 index 234b880c9..000000000 --- a/senlin/openstack/common/uuidutils.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright (c) 2012 Intel Corporation. -# 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. - -""" -UUID related utilities and helper functions. -""" - -import uuid - - -def generate_uuid(): - return str(uuid.uuid4()) - - -def is_uuid_like(val): - """Returns validation of a value as a UUID. - - For our purposes, a UUID is a canonical form string: - aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa - - """ - try: - return str(uuid.UUID(val)) == val - except (TypeError, ValueError, AttributeError): - return False From 8157f4b5a09def57bc4076f84b6f4c48cb45c5c5 Mon Sep 17 00:00:00 2001 From: tengqm Date: Fri, 2 Jan 2015 10:48:36 +0800 Subject: [PATCH 51/59] Added DB serialization logics --- senlin/policies/policy.py | 105 +++++++++++++++++++++++++++++++++----- 1 file changed, 92 insertions(+), 13 deletions(-) diff --git a/senlin/policies/policy.py b/senlin/policies/policy.py index acdbb8424..93b78e523 100644 --- a/senlin/policies/policy.py +++ b/senlin/policies/policy.py @@ -10,34 +10,111 @@ # License for the specific language governing permissions and limitations # under the License. +from senlin.common import exception +from senlin.db import api as db_api +from senlin.engine import environment + class Policy(object): ''' Base class for policies. ''' - def __init__(self, name, type_name, **kwargs): + ENFORCEMENT_LEVELS = ( + CRITICAL, ERROR, WARNING, INFO, DEBUG, + ) = ( + 'CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG', + ) + + def __new__(cls, type_name, name, **kwargs): + ''' + Create a new policy of the appropriate class. + ''' + if cls != Policy: + PolicyClass = cls + else: + PolicyClass = environment.global_env().get_policy(type_name) + + return super(Policy, cls).__new__(PolicyClass) + + def __init__(self, type_name, name, **kwargs): + self.id = kwargs.get('id', None) self.name = name self.type = type_name - self.cooldown = 0 - self.level = DEBUG - self.spec = {} - self.data = {} - def pre_op(self, cluster_id, action, **args): + self.context = kwargs.get('context', None) + self.cooldown = kwargs.get('cooldown', 0) + self.level = kwargs.get('level', self.DEBUG) + self.spec = kwargs.get('spec', {}) + self.data = kwargs.get('data', {}) + + def store(self): + ''' + Store the policy object into database table. + This could be a policy_create or a policy_update DB API invocation, + depends on whether self.id is set. + ''' + values = { + 'name': self.name, + 'type': self.type, + 'cooldown': self.cooldown, + 'level': self.level, + 'spec': self.spec, + 'data': self.data, + } + + if self.id: + db_api.policy_update(self.context, self.id, values) + else: + policy = db_api.policy_create(self.context, values) + self.id = policy.id + return self.id + + @classmethod + def from_db_record(cls, context, record): + ''' + Construct a policy object from a database record. + :param context: + :param record: + ''' + kwargs = { + 'id': record.id, + 'name': record.name, + 'context': context, + 'type': record.type, + 'cooldown': record.cooldown, + 'level': record.level, + 'spec': record.spec, + 'data': record.data, + } + return cls(record.type, record.name, **kwargs) + + @classmethod + def load(cls, context, policy_id): + ''' + Retrieve and reconstruct a policy object from DB. + ''' + policy = db_api.policy_get(context, policy_id) + if policy is None: + msg = _('No policy with id "%s" exists') % policy_id + raise exception.NotFound(msg) + + return cls.from_db_record(context, policy) + + def pre_op(self, cluster_id, action, **kwargs): ''' Force all subclasses to implement an operation that will be invoked before an action. ''' return NotImplemented - def enforce(self, cluster_id, action, **args): + def enforce(self, cluster_id, action, **kwargs): ''' Force all subclasses to implement an operation that can be called during an action. ''' return NotImplemented - def post_op(self, cluster_id, action, **args): + def post_op(self, cluster_id, action, **kwargs): ''' Force all subclasses to implement an operation that will be performed after an action. @@ -46,16 +123,18 @@ class Policy(object): def to_dict(self): pb_dict = { + 'id': self.id, 'name': self.name, - 'type': self.type_name, - 'uuid': self.uuid, + 'type': self.type, 'spec': self.spec, 'level': self.level, 'cooldown': self.cooldown, + 'data': self.data, } return pb_dict @classmethod - def from_dict(self, **kwargs): - pb = PolicyBase(kwargs) - return pb + def from_dict(cls, **kwargs): + type_name = kwargs.get('type', '') + name = kwargs.get('name', '') + return cls(type_name, name, **kwargs) From acde2deeaf28dd15a991c5434831de7b31505798 Mon Sep 17 00:00:00 2001 From: tengqm Date: Fri, 2 Jan 2015 12:52:44 +0800 Subject: [PATCH 52/59] Revised cluster_attach_policy api --- senlin/db/api.py | 4 ++-- senlin/db/sqlalchemy/api.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/senlin/db/api.py b/senlin/db/api.py index 3e6632b94..1468d043b 100644 --- a/senlin/db/api.py +++ b/senlin/db/api.py @@ -164,8 +164,8 @@ def policy_delete(context, policy_id, force=False): # Cluster-Policy Associations -def cluster_attach_policy(context, values): - return IMPL.cluster_attach_policy(context, values) +def cluster_attach_policy(context, cluster_id, policy_id, values): + return IMPL.cluster_attach_policy(context, cluster_id, policy_id, values) def cluster_get_policies(context, cluster_id): diff --git a/senlin/db/sqlalchemy/api.py b/senlin/db/sqlalchemy/api.py index 03a1fa09e..15c5db1d3 100644 --- a/senlin/db/sqlalchemy/api.py +++ b/senlin/db/sqlalchemy/api.py @@ -436,8 +436,10 @@ def policy_delete(context, policy_id, force=False): # Cluster-Policy Associations -def cluster_attach_policy(context, values): +def cluster_attach_policy(context, cluster_id, policy_id, values): binding = models.ClusterPolicies() + binding.cluster_id = cluster_id + binding.policy_id = policy_id binding.update(values) binding.save(_session(context)) return binding From ca9cc2d8036d6bb209fc6e830ce84b3860617234 Mon Sep 17 00:00:00 2001 From: tengqm Date: Fri, 2 Jan 2015 16:34:23 +0800 Subject: [PATCH 53/59] Added implementation for attach_policy Also replaced CLUSTER_RESIZE with CLUSTER_SCALE_UP and CLUSTER_SCALE_DOWN. --- senlin/engine/action.py | 43 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/senlin/engine/action.py b/senlin/engine/action.py index 0e53b2dad..f1e965e0b 100644 --- a/senlin/engine/action.py +++ b/senlin/engine/action.py @@ -19,6 +19,7 @@ from senlin.common import exception from senlin.db import api as db_api from senlin.engine import node as nodes from senlin.engine import scheduler +from senlin.policies import policy as policies class Action(object): @@ -226,11 +227,13 @@ class ClusterAction(Action): ''' ACTIONS = ( CLUSTER_CREATE, CLUSTER_DELETE, CLUSTER_UPDATE, - CLUSTER_ADD_NODES, CLUSTER_DEL_NODES, CLUSTER_RESIZE, + CLUSTER_ADD_NODES, CLUSTER_DEL_NODES, + CLUSTER_SCALE_UP, CLUSTER_SCALE_DOWN, CLUSTER_ATTACH_POLICY, CLUSTER_DETACH_POLICY, ) = ( 'CLUSTER_CREATE', 'CLUSTER_DELETE', 'CLUSTER_UPDATE', - 'CLUSTER_ADD_NODES', 'CLUSTER_DEL_NODES', 'CLUSTER_RESIZE', + 'CLUSTER_ADD_NODES', 'CLUSTER_DEL_NODES', + 'CLUSTER_SCALE_UP', 'CLUSTER_SCALE_DOWN', 'CLUSTER_ATTACH_POLICY', 'CLUSTER_DETACH_POLICY', ) @@ -303,10 +306,38 @@ class ClusterAction(Action): def do_del_nodes(self, cluster): return self.RES_OK - def do_resize(self, cluster): + def do_scale_up(self, cluster): + return self.RES_OK + + def do_scale_down(self, cluster): return self.RES_OK def do_attach_policy(self, cluster): + policy_id = self.inputs.get('policy_id', None) + if policy_id is None: + raise exception.PolicyNotSpecified() + + policy = policies.load(self.context, policy_id) + # Check if policy has already been attached + all = db_api.cluster_get_policies(self.context, cluster.id) + for existing in all: + # Policy already attached + if existing.id == policy_id: + return self.RES_OK + + if existing.type == policy.type: + raise exception.PolicyExists(policy_type=policy.type) + + values = { + 'cooldown': self.inputs.get('cooldown', policy.cooldown), + 'level': self.inputs.get('level', policy.level), + 'enabled': self.inputs.get('enabled', True), + } + + db_api.cluster_attach_policy(self.context, cluster.id, policy_id, + values) + + cluster.rt.policies.append(policy) return self.RES_OK def do_detach_policy(self, cluster): @@ -328,8 +359,10 @@ class ClusterAction(Action): res = self.do_add_nodes(cluster) elif self.action == self.CLUSTER_DEL_NODES: res = self.do_del_nodes(cluster) - elif self.action == self.CLUSTER_RESIZE: - res = self.do_resize(cluster) + elif self.action == self.CLUSTER_SCALE_UP: + res = self.do_scale_up(cluster) + elif self.action == self.CLUSTER_SCALE_DOWN: + res = self.do_scale_down(cluster) elif self.action == self.CLUSTER_ATTACH_POLICY: res = self.do_attach_policy(cluster) elif self.action == self.CLUSTER_DETACH_POLICY: From 1c17b69682d4827e117b3645f1bc7a3dfc1f5593 Mon Sep 17 00:00:00 2001 From: tengqm Date: Fri, 2 Jan 2015 16:39:49 +0800 Subject: [PATCH 54/59] Rename policy.py back to base.py --- senlin/policies/{policy.py => base.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename senlin/policies/{policy.py => base.py} (100%) diff --git a/senlin/policies/policy.py b/senlin/policies/base.py similarity index 100% rename from senlin/policies/policy.py rename to senlin/policies/base.py From 7e598e6d04a9b68d4b45f45d4ad833adfa32ae35 Mon Sep 17 00:00:00 2001 From: tengqm Date: Fri, 2 Jan 2015 16:40:39 +0800 Subject: [PATCH 55/59] Rename policy.py to base.py --- senlin/engine/action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/senlin/engine/action.py b/senlin/engine/action.py index f1e965e0b..e89752f4f 100644 --- a/senlin/engine/action.py +++ b/senlin/engine/action.py @@ -19,7 +19,7 @@ from senlin.common import exception from senlin.db import api as db_api from senlin.engine import node as nodes from senlin.engine import scheduler -from senlin.policies import policy as policies +from senlin.policies import base as policies class Action(object): From 0911ccebb5d70e305bee12aa911da035c31ef09a Mon Sep 17 00:00:00 2001 From: tengqm Date: Fri, 2 Jan 2015 16:43:56 +0800 Subject: [PATCH 56/59] Revise to match base class --- senlin/policies/health_policy.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/senlin/policies/health_policy.py b/senlin/policies/health_policy.py index 3e66ef5b1..7988e8a5b 100644 --- a/senlin/policies/health_policy.py +++ b/senlin/policies/health_policy.py @@ -37,12 +37,12 @@ class HealthPolicy(base.PolicyBase): 'AWS.AutoScaling.LaunchConfiguration', ] - def __init__(self, name, type_name, **kwargs): - super(HealthPolicy, self).__init__(name, type_name, kwargs) + def __init__(self, type_name, name, **kwargs): + super(HealthPolicy, self).__init__(type_name, name, kwargs) - self.interval = kwargs.get('interval') - self.grace_period = kwargs.get('grace_period') - self.check_type = kwargs.get('check_type') + self.interval = self.spec.get('interval') + self.grace_period = self.spec.get('grace_period') + self.check_type = self.spec.get('check_type') def pre_op(self, cluster_id, action, **args): pass @@ -51,6 +51,6 @@ class HealthPolicy(base.PolicyBase): pass def post_op(self, cluster_id, action, **args): - # TODO: subscribe to vm-lifecycle-events for the specified VM - # or add vm to the list of VM status polling + # TODO(Qiming): subscribe to vm-lifecycle-events for the specified VM + # or add vm to the list of VM status polling pass From a68193102354af807285d0238354314c9aa67a25 Mon Sep 17 00:00:00 2001 From: tengqm Date: Fri, 2 Jan 2015 17:17:10 +0800 Subject: [PATCH 57/59] Move victim selection to enforce We don't block scaling down anyway. --- senlin/policies/deletion_policy.py | 64 ++++++++++++++++-------------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/senlin/policies/deletion_policy.py b/senlin/policies/deletion_policy.py index 13baec246..85f26b14d 100644 --- a/senlin/policies/deletion_policy.py +++ b/senlin/policies/deletion_policy.py @@ -10,11 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. +import random + from senlin.db import api as db_api from senlin.policies import base -class DeletionPolicy(base.PolicyBase): +class DeletionPolicy(base.Policy): ''' Policy for deleting member(s) from a cluster. ''' @@ -28,46 +30,48 @@ class DeletionPolicy(base.PolicyBase): ) TARGET = [ - ('BEFORE', 'CLUSTER', 'DELETE_MEMBER'), - ('AFTER', 'CLUSTER', 'DELETE_MEMBER'), + ('WHEN', 'CLUSTER_SCALE_DOWN'), + ('AFTER', 'CLUSTER_DEL_NODES'), + ('AFTER', 'CLUSTER_SCALE_DOWN'), ] PROFILE_TYPE = [ 'ANY' ] - def __init__(self, name, type_name, **kwargs): - super(DeletionPolicy, self).__init__(name, type_name, kwargs) + def __init__(self, type_name, name, **kwargs): + super(DeletionPolicy, self).__init__(type_name, name, **kwargs) - self.criteria = kwargs.get('criteria') - self.grace_period = kwargs.get('grace_period') - self.delete_desired_capacity = kwargs.get('reduce_desired_capacity') - - def _sort_members_by_creation_time(members): - # TODO: do sorting - return members + self.criteria = kwargs.get('criteria', '') + self.grace_period = kwargs.get('grace_period', 0) + self.reduce_desired_capacity = kwargs.get('reduce_desired_capacity', + False) + random.seed() def pre_op(self, cluster_id, action, **args): - # :cluster_id the cluster - # :action 'DEL_MEMBER' - # :args a list of candidate members - - # TODO: choose victims from the given cluster - members = db_api.get_members(cluster_id) - sorted = self._sort_members_by_creation_time(members) - if self.criteria == self.OLDEST_FIRST: - victim = sorted[0] - elif self.criteria ==self.YOUNGEST_FIRST: - victim = sorted[-1] - else: - rand = random(len(sorted)) - victim = sorted[rand] - - # TODO: return True/False - return victim + ''' + We don't block the deletion anyhow. + ''' + return True def enforce(self, cluster_id, action, **args): - pass + ''' + The enforcement of a deletion policy returns the chosen victims + that will be deleted. + ''' + nodes = db_api.node_get_all_by_cluster_id(cluster_id) + if self.criteria == self.RANDOM: + rand = random.randrange(len(nodes)) + return nodes[rand] + + sorted_list = sorted(nodes, key=lambda r: (r.created_time, r.name)) + if self.criteria == self.OLDEST_FIRST: + victim = sorted_list[0] + else: # self.criteria == self.YOUNGEST_FIRST: + victim = sorted_list[-1] + + return victim def post_op(self, cluster_id, action, **args): + # TODO(Qiming): process grace period here if needed pass From b4e19d8a9a1c50226ab417fee950550db5a063ca Mon Sep 17 00:00:00 2001 From: tengqm Date: Fri, 2 Jan 2015 17:32:33 +0800 Subject: [PATCH 58/59] Revised to use global constants --- senlin/common/senlin_consts.py | 33 ++++++++++++++++++++++++++++++ senlin/policies/deletion_policy.py | 7 ++++--- 2 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 senlin/common/senlin_consts.py diff --git a/senlin/common/senlin_consts.py b/senlin/common/senlin_consts.py new file mode 100644 index 000000000..64c9fe241 --- /dev/null +++ b/senlin/common/senlin_consts.py @@ -0,0 +1,33 @@ +# 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. + + +# ACTION PHRASES +CLUSTER_CREATE = 'CLUSTER_CREATE' +CLUSTER_DELETE = 'CLUSTER_DELETE' +CLUSTER_UPDATE = 'CLUSTER_UPDATE' +CLUSTER_ADD_NODES = 'CLUSTER_ADD_NODES' +CLUSTER_DEL_NODES = 'CLUSTER_DEL_NODES' +CLUSTER_SCALE_UP = 'CLUSTER_SCALE_UP' +CLUSTER_SCALE_DOWN = 'CLUSTER_SCALE_DOWN' +CLUSTER_ATTACH_POLICY = 'CLUSTER_ATTACH_POLICY' +CLUSTER_DETACH_POLICY = 'CLUSTER_DETACH_POLICY' + +NODE_CREATE = 'NODE_CREATE' +NODE_DELETE = 'NODE_DELETE' +NODE_UPDATE = 'NODE_UPDATE' +NODE_JOIN_CLUSTER = 'NODE_JOIN_CLUSTER' +NODE_LEAVE_CLUSTER = 'NODE_LEAVE_CLUSTER' + +POLICY_ENABLE = 'POLICY_ENABLE' +POLICY_DISABLE = 'POLICY_DISABLE' +POLICY_UPDATE = 'POLICY_UPDATE' diff --git a/senlin/policies/deletion_policy.py b/senlin/policies/deletion_policy.py index 85f26b14d..296ea18be 100644 --- a/senlin/policies/deletion_policy.py +++ b/senlin/policies/deletion_policy.py @@ -12,6 +12,7 @@ import random +from senlin.common import senlin_consts as consts from senlin.db import api as db_api from senlin.policies import base @@ -30,9 +31,9 @@ class DeletionPolicy(base.Policy): ) TARGET = [ - ('WHEN', 'CLUSTER_SCALE_DOWN'), - ('AFTER', 'CLUSTER_DEL_NODES'), - ('AFTER', 'CLUSTER_SCALE_DOWN'), + ('WHEN', consts.CLUSTER_SCALE_DOWN), + ('AFTER', consts.CLUSTER_DEL_NODES), + ('AFTER', consts.CLUSTER_SCALE_DOWN), ] PROFILE_TYPE = [ From 714cc8ad3e5d6486b64814590efeb5bc44f17daa Mon Sep 17 00:00:00 2001 From: tengqm Date: Fri, 2 Jan 2015 17:38:44 +0800 Subject: [PATCH 59/59] HealthPolicy skeleton for checking --- senlin/policies/health_policy.py | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/senlin/policies/health_policy.py b/senlin/policies/health_policy.py index 7988e8a5b..517b21672 100644 --- a/senlin/policies/health_policy.py +++ b/senlin/policies/health_policy.py @@ -10,14 +10,15 @@ # License for the specific language governing permissions and limitations # under the License. +from senlin.common import senlin_consts as consts from senlin.policies import base -class HealthPolicy(base.PolicyBase): +class HealthPolicy(base.Policy): ''' Policy for health checking for members of a cluster. ''' - + CHECK_TYPES = ( VM_LIFECYCLE_EVENTS, VM_STATUS_POLLING, @@ -29,7 +30,10 @@ class HealthPolicy(base.PolicyBase): ) TARGET = [ - ('AFTER', 'CLUSTER', 'ADD_MEMBER') + ('AFTER', consts.CLUSTER_SCALE_UP), + ('AFTER', consts.CLUSTER_ADD_NODES), + ('BEFORE', consts.CLUSTER_SCALE_DOWN), + ('BEFORE', consts.CLUSTER_DEL_NODES), ] PROFILE_TYPE = [ @@ -45,12 +49,23 @@ class HealthPolicy(base.PolicyBase): self.check_type = self.spec.get('check_type') def pre_op(self, cluster_id, action, **args): - pass + # Ignore actions that are not required to be processed at this stage + if action not in (consts.CLUSTER_SCALE_DOWN, + consts.CLUSTER_DEL_NODES): + return True + + # TODO(anyone): Unsubscribe nodes from backend health monitoring + # infrastructure + return True def enforce(self, cluster_id, action, **args): pass def post_op(self, cluster_id, action, **args): - # TODO(Qiming): subscribe to vm-lifecycle-events for the specified VM - # or add vm to the list of VM status polling - pass + # Ignore irrelevant action here + if action not in (consts.CLUSTER_SCALE_UP, consts.CLUSTER_ADD_NODES): + return True + + # TODO(anyone): subscribe to vm-lifecycle-events for the specified VM + # or add vm to the list of VM status polling + return True