From 5b64dcd17f96e8e6c54b1e6296e0cf13012b93fa Mon Sep 17 00:00:00 2001 From: Davanum Srinivas Date: Sun, 8 May 2016 22:10:46 -0400 Subject: [PATCH] [WIP] cobbling together a command line - NOT READY FOR REVIEW Change-Id: I9caefe557ac827ca7a3b8f9a1693d623cf369080 --- kolla_kubernetes/cli/__init__.py | 0 kolla_kubernetes/cli/service.py | 46 ++++ kolla_kubernetes/cmd/__init__.py | 0 kolla_kubernetes/cmd/shell.py | 160 +++++++++++++ kolla_kubernetes/common/__init__.py | 0 kolla_kubernetes/common/file_utils.py | 94 ++++++++ kolla_kubernetes/common/jinja_utils.py | 102 ++++++++ kolla_kubernetes/common/type_utils.py | 19 ++ kolla_kubernetes/common/utils.py | 46 ++++ kolla_kubernetes/config.py | 56 +++++ kolla_kubernetes/exception.py | 31 +++ kolla_kubernetes/service.py | 309 +++++++++++++++++++++++++ kolla_kubernetes/service_definition.py | 131 +++++++++++ setup.cfg | 12 +- 14 files changed, 1005 insertions(+), 1 deletion(-) create mode 100644 kolla_kubernetes/cli/__init__.py create mode 100644 kolla_kubernetes/cli/service.py create mode 100644 kolla_kubernetes/cmd/__init__.py create mode 100644 kolla_kubernetes/cmd/shell.py create mode 100644 kolla_kubernetes/common/__init__.py create mode 100644 kolla_kubernetes/common/file_utils.py create mode 100644 kolla_kubernetes/common/jinja_utils.py create mode 100644 kolla_kubernetes/common/type_utils.py create mode 100644 kolla_kubernetes/common/utils.py create mode 100644 kolla_kubernetes/config.py create mode 100644 kolla_kubernetes/exception.py create mode 100644 kolla_kubernetes/service.py create mode 100644 kolla_kubernetes/service_definition.py diff --git a/kolla_kubernetes/cli/__init__.py b/kolla_kubernetes/cli/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/kolla_kubernetes/cli/service.py b/kolla_kubernetes/cli/service.py new file mode 100644 index 000000000..369e6d159 --- /dev/null +++ b/kolla_kubernetes/cli/service.py @@ -0,0 +1,46 @@ +# 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 cliff import command +from oslo_config import cfg +from oslo_log import log + + +from kolla_kubernetes import service + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + + +class Run(command.Command): + """Run a service.""" + + def get_parser(self, prog_name): + parser = super(Run, self).get_parser(prog_name) + parser.add_argument('service') + return parser + + def take_action(self, parsed_args): + service.run_service(parsed_args.service, + CONF.service_dir) + + +class Kill(command.Command): + """Kill a service.""" + + def get_parser(self, prog_name): + parser = super(Kill, self).get_parser(prog_name) + parser.add_argument('service') + return parser + + def take_action(self, parsed_args): + service.kill_service(parsed_args.service) diff --git a/kolla_kubernetes/cmd/__init__.py b/kolla_kubernetes/cmd/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/kolla_kubernetes/cmd/shell.py b/kolla_kubernetes/cmd/shell.py new file mode 100644 index 000000000..3fed00b60 --- /dev/null +++ b/kolla_kubernetes/cmd/shell.py @@ -0,0 +1,160 @@ +# 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 os.path +import shlex +import sys + +from cliff import app +from cliff import commandmanager +from cliff import interactive +from oslo_config import cfg +from oslo_log import log + +from kolla_kubernetes.common import file_utils +from kolla_kubernetes.common import utils + +PROJECT = 'kolla_kubernetes' +VERSION = '1.0' + +CONF = cfg.CONF +CONF.import_group('kolla', 'kolla_kubernetes.config') +CONF.import_group('kolla-kubernetes', 'kolla_kubernetes.config') + +log.register_options(CONF) +log.set_defaults( + default_log_levels='requests.packages.urllib3.connectionpool=WARNING') + +cli_opts = [ + cfg.StrOpt('service-dir', + default=utils.env( + 'KM_SERVICE_DIR', default=os.path.join( + file_utils.find_base_dir(), 'services')), + help='Directory with services, (Env: KM_SERVICE_DIR)'), +] +CONF.register_cli_opts(cli_opts) + + +class KollaKubernetesInteractiveApp(interactive.InteractiveApp): + def do_run(self, arg): + self.default(arg) + + def do_help(self, arg): + line_parts = shlex.split(arg) + try: + self.command_manager.find_command(line_parts) + return self.default(self.parsed('help ' + arg)) + except ValueError: + # There is a builtin cmd2 command + pass + return interactive.InteractiveApp.do_help(self, arg) + + +class KollaKubernetesShell(app.App): + def __init__(self): + super(KollaKubernetesShell, self).__init__( + description='Kolla-kubernetes command-line interface', + version=VERSION, + command_manager=commandmanager.CommandManager( + 'kolla_kubernetes.cli'), + deferred_help=True, + interactive_app_factory=KollaKubernetesInteractiveApp + ) + + def configure_logging(self): + return + + def initialize_app(self, argv): + self.options.service_dir = CONF.service_dir + + def print_help(self): + outputs = [] + max_len = 0 + self.stdout.write('\nCommands :\n') + + for name, ep in sorted(self.command_manager): + factory = ep.load() + cmd = factory(self, None) + one_liner = cmd.get_description().split('\n')[0] + outputs.append((name, one_liner)) + max_len = max(len(name), max_len) + + for name, one_liner in outputs: + self.stdout.write(' %s %s\n' % (name.ljust(max_len), one_liner)) + + +def _separate_args(argv): + conf_opts = _config_opts_map() + config_args = [] + command_args = argv[:] + while command_args: + nargs = conf_opts.get(command_args[0]) + if nargs: + config_args.extend(command_args[:nargs]) + command_args = command_args[nargs:] + else: + break + return config_args, command_args + + +def _config_opts_map(): + opts = {'--help': 1, '-h': 1, '--config-dir': 2, '--config-file': 2, + '--version': 1} + for opt in CONF._all_cli_opts(): + if opt[1]: + arg = '%s-%s' % (opt[1].name, opt[0].name) + else: + arg = opt[0].name + + if isinstance(opt[0], cfg.BoolOpt): + nargs = 1 + opts['--no%s' % arg] = 1 + else: + nargs = 2 + opts['--%s' % arg] = nargs + + if opt[0].short: + opts['-%s' % opt[0].short] = nargs + + for dep_opt in opt[0].deprecated_opts: + if getattr(dep_opt, 'group'): + opts['--%s-%s' % (dep_opt.group, dep_opt.name)] = nargs + else: + opts['--%s' % dep_opt.name] = nargs + + return opts + + +def main(argv=sys.argv[1:]): + config_args, command_args = _separate_args(argv) + + need_help = (['help'] == command_args or '-h' in config_args or + '--help' in config_args) + if need_help: + CONF([], project=PROJECT, version=VERSION) + CONF.print_help() + return KollaKubernetesShell().print_help() + + CONF(config_args, project=PROJECT, version=VERSION) + log.setup(CONF, PROJECT, VERSION) + + if '-d' in config_args or '--debug' in config_args: + command_args.insert(0, '--debug') + CONF.log_opt_values( + log.getLogger(PROJECT), log.INFO) + + return KollaKubernetesShell().run(command_args) + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) diff --git a/kolla_kubernetes/common/__init__.py b/kolla_kubernetes/common/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/kolla_kubernetes/common/file_utils.py b/kolla_kubernetes/common/file_utils.py new file mode 100644 index 000000000..e6c09f973 --- /dev/null +++ b/kolla_kubernetes/common/file_utils.py @@ -0,0 +1,94 @@ +# 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 errno +import logging +import os +import platform +import sys + +from oslo_utils import importutils + +from kolla_kubernetes import exception + + +LOG = logging.getLogger(__name__) + + +def find_os_type(): + return platform.linux_distribution()[0] + + +def mkdir_p(path): + try: + os.makedirs(path) + except OSError as exc: # Python >2.5 + if exc.errno == errno.EEXIST and os.path.isdir(path): + pass + else: + raise + + +def get_src_dir(): + kolla_kubernetes = importutils.import_module('kolla_kubernetes') + mod_path = os.path.abspath(kolla_kubernetes.__file__) + # remove the file and module to get to the base. + return os.path.dirname(os.path.dirname(mod_path)) + + +def find_base_dir(): + script_path = os.path.dirname(os.path.realpath(sys.argv[0])) + base_script_path = os.path.basename(script_path) + if base_script_path == 'kolla-kubernetes': + return script_path + if base_script_path == 'kolla_kubernetes': + return os.path.join(script_path, '..') + if base_script_path == 'cmd': + return os.path.join(script_path, '..', '..') + if base_script_path == 'subunit': + return get_src_dir() + if base_script_path == 'bin': + if find_os_type() in ['Ubuntu', 'debian']: + base_dir = '/usr/local/share/kolla-kubernetes' + else: + base_dir = '/usr/share/kolla-kubernetes' + + if os.path.exists(base_dir): + return base_dir + else: + return get_src_dir() + raise exception.KollaDirNotFoundException( + 'Unable to detect kolla-kubernetes directory' + ) + + +def find_config_file(filename): + filepath = os.path.join('/etc/kolla-kubernetes', filename) + if os.access(filepath, os.R_OK): + config_file = filepath + else: + config_file = os.path.join(find_base_dir(), + 'etc', filename) + return config_file + + +POSSIBLE_PATHS = {'/usr/share/kolla-kubernetes', + get_src_dir(), + find_base_dir()} + + +def find_file(filename): + for path in POSSIBLE_PATHS: + file_path = os.path.join(path, filename) + if os.path.exists(file_path): + return file_path + raise exception.KollaNotFoundException(filename, entity='file') diff --git a/kolla_kubernetes/common/jinja_utils.py b/kolla_kubernetes/common/jinja_utils.py new file mode 100644 index 000000000..671c4ad5d --- /dev/null +++ b/kolla_kubernetes/common/jinja_utils.py @@ -0,0 +1,102 @@ +# 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 collections +import logging +import os + +import jinja2 +from jinja2 import meta +import six +import yaml + +from kolla_kubernetes.common import type_utils + +LOG = logging.getLogger(__name__) + + +# Customize PyYAML library to return the OrderedDict. That is needed, because +# when iterating on dict, we reuse its previous values when processing the +# next values and the order has to be preserved. + +def ordered_dict_constructor(loader, node): + """OrderedDict constructor for PyYAML.""" + return collections.OrderedDict(loader.construct_pairs(node)) + + +def ordered_dict_representer(dumper, data): + """Representer for PyYAML which is able to work with OrderedDict.""" + return dumper.represent_dict(data.items()) + + +yaml.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, + ordered_dict_constructor) +yaml.add_representer(collections.OrderedDict, ordered_dict_representer) + + +def jinja_render(fullpath, global_config, extra=None): + variables = global_config + if extra: + variables.update(extra) + + myenv = jinja2.Environment( + loader=jinja2.FileSystemLoader( + os.path.dirname(fullpath))) + myenv.filters['bool'] = type_utils.str_to_bool + return myenv.get_template(os.path.basename(fullpath)).render(variables) + + +def jinja_render_str(content, global_config, name='dafault_name', extra=None): + variables = global_config + if extra: + variables.update(extra) + + myenv = jinja2.Environment(loader=jinja2.DictLoader({name: content})) + myenv.filters['bool'] = type_utils.str_to_bool + return myenv.get_template(name).render(variables) + + +def jinja_find_required_variables(fullpath): + myenv = jinja2.Environment(loader=jinja2.FileSystemLoader( + os.path.dirname(fullpath))) + myenv.filters['bool'] = type_utils.str_to_bool + template_source = myenv.loader.get_source(myenv, + os.path.basename(fullpath))[0] + parsed_content = myenv.parse(template_source) + return meta.find_undeclared_variables(parsed_content) + + +def dict_jinja_render(raw_dict, jvars): + """Renders dict with jinja2 using provided variables and itself. + + By using itself, we mean reusing the previous values from dict for the + potential render of the next value in dict. + """ + for key, value in raw_dict.items(): + if isinstance(value, six.string_types): + value = jinja_render_str(value, jvars) + elif isinstance(value, dict): + value = dict_jinja_render(value, jvars) + jvars[key] = value + + +def yaml_jinja_render(filename, jvars): + """Parses YAML file and templates it with jinja2. + + 1. YAML file is rendered by jinja2 based on the provided variables. + 2. Rendered file is parsed. + 3. The every element dictionary being a result of parsing is rendered again + with itself. + """ + with open(filename, 'r') as yaml_file: + raw_dict = yaml.load(yaml_file) + dict_jinja_render(raw_dict, jvars) diff --git a/kolla_kubernetes/common/type_utils.py b/kolla_kubernetes/common/type_utils.py new file mode 100644 index 000000000..d4cbef9c9 --- /dev/null +++ b/kolla_kubernetes/common/type_utils.py @@ -0,0 +1,19 @@ +# 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. + + +def str_to_bool(text): + if not text: + return False + if text.lower() in ['true', 'yes']: + return True + return False diff --git a/kolla_kubernetes/common/utils.py b/kolla_kubernetes/common/utils.py new file mode 100644 index 000000000..69f808e2c --- /dev/null +++ b/kolla_kubernetes/common/utils.py @@ -0,0 +1,46 @@ +# 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 collections +import os +from six.moves.urllib import parse + + +def env(*args, **kwargs): + for arg in args: + value = os.environ.get(arg) + if value: + return value + return kwargs.get('default', '') + + +def dict_update(d, u): + """Recursively update 'd' with 'u' and return the result.""" + + if not isinstance(u, collections.Mapping): + return u + + for k, v in u.items(): + if isinstance(v, collections.Mapping): + d[k] = dict_update(d.get(k, {}), v) + else: + d[k] = u[k] + return d + + +def get_query_string(search_opts): + if search_opts: + qparams = sorted(search_opts.items(), key=lambda x: x[0]) + query_string = "?%s" % parse.urlencode(qparams, doseq=True) + else: + query_string = "" + return query_string diff --git a/kolla_kubernetes/config.py b/kolla_kubernetes/config.py new file mode 100644 index 000000000..7e11dd8f4 --- /dev/null +++ b/kolla_kubernetes/config.py @@ -0,0 +1,56 @@ +# 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 oslo_config import cfg + +from kolla_kubernetes.common import utils + +CONF = cfg.CONF + +kolla_opts = [ + cfg.StrOpt('namespace', + default='kollaglue', + help='The Docker namespace name'), + cfg.StrOpt('tag', + default='1.0.0', + help='The Docker tag'), + cfg.StrOpt('base', + default='centos', + help='The base distro which was used to build images'), + cfg.StrOpt('base-tag', + default='latest', + help='The base distro image tag'), + cfg.StrOpt('install-type', + default='binary', + help='The method of the OpenStack install'), + cfg.StrOpt('deployment-id', + default=utils.env('USER', default='default'), + help='Uniq name for deployment'), + cfg.StrOpt('profile', + default='default', + help='Build profile which was used to build images') +] +kolla_opt_group = cfg.OptGroup(name='kolla', + title='Options for Kolla Docker images') +CONF.register_group(kolla_opt_group) +CONF.register_cli_opts(kolla_opts, kolla_opt_group) +CONF.register_opts(kolla_opts, kolla_opt_group) + +kubernetes_opts = [ + cfg.StrOpt('host', default='localhost:8080'), + cfg.StrOpt('kubectl_path'), + cfg.StrOpt('yml_dir_path', ) +] +kubernetes_opt_group = cfg.OptGroup(name='kolla-kubernetes') +CONF.register_group(kubernetes_opt_group) +CONF.register_cli_opts(kubernetes_opts, kubernetes_opt_group) +CONF.register_opts(kubernetes_opts, kubernetes_opt_group) diff --git a/kolla_kubernetes/exception.py b/kolla_kubernetes/exception.py new file mode 100644 index 000000000..85b9f0eea --- /dev/null +++ b/kolla_kubernetes/exception.py @@ -0,0 +1,31 @@ +# 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. + + +class KollaException(Exception): + pass + + +class KollaDirNotFoundException(KollaException): + pass + + +class KollaNotFoundException(KollaException): + def __init__(self, message, entity='file'): + super(KollaNotFoundException, self).__init__( + 'The %s "%s" was not found' % (entity, message)) + + +class KollaNotSupportedException(KollaNotFoundException): + def __init__(self, operation='update', entity='kubernetes'): + super(KollaNotFoundException, self).__init__( + 'Operation "%s" is not supported by "%s"' % (operation, entity)) diff --git a/kolla_kubernetes/service.py b/kolla_kubernetes/service.py new file mode 100644 index 000000000..fceed7f3e --- /dev/null +++ b/kolla_kubernetes/service.py @@ -0,0 +1,309 @@ +# 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 functools +import os.path +import subprocess +import time + +from oslo_config import cfg +from oslo_log import log as logging +import yaml + +from kolla_kubernetes.common import file_utils +from kolla_kubernetes.common import jinja_utils +from kolla_kubernetes import service_definition + +LOG = logging.getLogger() +CONF = cfg.CONF +CONF.import_group('kolla', 'kolla_kubernetes.config') +CONF.import_group('kolla-kubernetes', 'kolla_kubernetes.config') + + +def execute_if_enabled(f): + """Decorator for executing methods only if runner is enabled.""" + @functools.wraps(f) + def wrapper(self, *args, **kwargs): + if not self._enabled: + return + return f(self, *args, **kwargs) + return wrapper + + +class File(object): + def __init__(self, conf, name, service_name): + self._conf = conf + self._name = name + self._service_name = service_name + self.base_dir = os.path.abspath(file_utils.find_base_dir()) + + # def merge_ini_files(self, source_files): + # config_p = configparser.ConfigParser() + # for src_file in source_files: + # if not src_file.startswith('/'): + # src_file = os.path.join(self.base_dir, src_file) + # if not os.path.exists(src_file): + # LOG.warning('path missing %s' % src_file) + # continue + # config_p.read(src_file) + # merged_f = cStringIO() + # config_p.write(merged_f) + # return merged_f.getvalue() + # + # def write_to_zookeeper(self, zk, base_node): + # dest_node = os.path.join(base_node, self._service_name, + # 'files', self._name) + # zk.ensure_path(dest_node) + # if isinstance(self._conf['source'], list): + # content = self.merge_ini_files(self._conf['source']) + # else: + # src_file = self._conf['source'] + # if not src_file.startswith('/'): + # src_file = file_utils.find_file(src_file) + # with open(src_file) as fp: + # content = fp.read() + # zk.set(dest_node, content.encode('utf-8')) + + +class Command(object): + def __init__(self, conf, name, service_name): + self._conf = conf + self._name = name + self._service_name = service_name + + # def write_to_zookeeper(self, zk, base_node): + # for fn in self._conf.get('files', []): + # fo = File(self._conf['files'][fn], fn, self._service_name) + # fo.write_to_zookeeper(zk, base_node) + + +class Runner(object): + def __init__(self, conf): + self._conf = conf + self.base_dir = os.path.abspath(file_utils.find_base_dir()) + self.type_name = None + self._enabled = self._conf.get('enabled', True) + if not self._enabled: + LOG.warn('Service %s disabled', self._conf['name']) + self.app_file = None + self.app_def = None + + def __new__(cls, conf): + """Create a new Runner of the appropriate class for its type.""" + # Call is already for a subclass, so pass it through + RunnerClass = cls + return super(Runner, cls).__new__(RunnerClass) + + @classmethod + def load_from_file(cls, service_file, variables): + return Runner(yaml.load( + jinja_utils.jinja_render(service_file, variables))) + + def _list_commands(self): + if 'service' in self._conf: + yield 'daemon', self._conf['service']['daemon'] + for key in self._conf.get('commands', []): + yield key, self._conf['commands'][key] + + # @execute_if_enabled + # def write_to_zookeeper(self, zk, base_node): + # for cmd_name, cmd_conf in self._list_commands(): + # cmd = Command(cmd_conf, cmd_name, self._conf['name']) + # # cmd.write_to_zookeeper(zk, base_node) + # + # dest_node = os.path.join(base_node, self._conf['name']) + # # zk.ensure_path(dest_node) + # # try: + # # zk.set(dest_node, json.dumps(self._conf).encode('utf-8')) + # # except Exception as te: + # # LOG.error('%s=%s -> %s' % (dest_node, self._conf, te)) + + # @classmethod + # def load_from_zk(cls, zk, service_name): + # variables = _load_variables_from_zk(zk) + # base_node = os.path.join('kolla', CONF.kolla.deployment_id) + # dest_node = os.path.join(base_node, "openstack", + # service_name.split('-')[0], service_name) + # try: + # conf_raw, _st = zk.get(dest_node) + # except Exception as te: + # LOG.error('%s -> %s' % (dest_node, te)) + # raise NameError(te) + # return Runner(yaml.load( + # jinja_utils.jinja_render_str(conf_raw.decode('utf-8'), + # variables))) + + +class JvarsDict(dict): + """Dict which can contain the 'global_vars' which are always preserved. + + They cannot be be overriden by any update nor single item setting. + """ + + def __init__(self, *args, **kwargs): + super(JvarsDict, self).__init__(*args, **kwargs) + self.global_vars = {} + + def __setitem__(self, key, value, force=False): + if not force and key in self.global_vars: + return + return super(JvarsDict, self).__setitem__(key, value) + + def set_force(self, key, value): + """Sets the variable even if it will override a global variable.""" + return self.__setitem__(key, value, force=True) + + def update(self, other_dict, force=False): + if not force: + other_dict = {key: value for key, value in other_dict.items() + if key not in self.global_vars} + super(JvarsDict, self).update(other_dict) + + def set_global_vars(self, global_vars): + self.update(global_vars) + self.global_vars = global_vars + + +def _load_variables_from_file(service_dir, project_name): + config_dir = os.path.join(service_dir, '..', 'config') + jvars = JvarsDict() + with open(file_utils.find_config_file('globals.yml'), 'r') as gf: + jvars.set_global_vars(yaml.load(gf)) + with open(file_utils.find_config_file('passwords.yml'), 'r') as gf: + jvars.update(yaml.load(gf)) + # Apply the basic variables that aren't defined in any config file. + jvars.update({ + 'deployment_id': CONF.kolla.deployment_id, + 'node_config_directory': '', + 'timestamp': str(time.time()) + }) + # Get the exact marathon framework name. + # config.get_marathon_framework(jvars) + # all.yml file uses some its variables to template itself by jinja2, + # so its raw content is used to template the file + all_yml_name = os.path.join(config_dir, 'all.yml') + jinja_utils.yaml_jinja_render(all_yml_name, jvars) + # Apply the dynamic deployment variables. + # config.apply_deployment_vars(jvars) + + proj_yml_name = os.path.join(config_dir, project_name, + 'defaults', 'main.yml') + if os.path.exists(proj_yml_name): + jinja_utils.yaml_jinja_render(proj_yml_name, jvars) + else: + LOG.warning('Path missing %s' % proj_yml_name) + return jvars + + +def _build_runner(service_name, service_dir, variables=None): + # config_dir = os.path.join(service_dir, '..', 'config') + # base_node = os.path.join('kolla', CONF.kolla.deployment_id) + filename = service_definition.find_service_file(service_name, + service_dir) + proj_name = filename.split('/')[-2] + + # is this a snapshot or from original src? + variables = _load_variables_from_file(service_dir, proj_name) + + # 1. validate the definition with the given variables + service_definition.validate(service_name, service_dir, variables) + # runner = Runner.load_from_file(filename, variables) + # with zk_utils.connection() as zk: + # # 2. write variables to zk (globally) + # config.write_variables_zookeeper(zk, variables) + # # 3. write common config and start script + # config.write_common_config_to_zookeeper(config_dir, zk, variables) + # + # # 4. write files/config to zk + # runner.write_to_zookeeper(zk, base_node) + + +# def _load_variables_from_zk(zk): +# path = os.path.join('/kolla', CONF.kolla.deployment_id, 'variables') +# variables = {} +# try: +# var_names = zk.get_children(path) +# except Exception: +# var_names = [] +# for var in var_names: +# value, _stat = zk.get(os.path.join(path, var)) +# variables[var] = value.decode('utf-8') +# # Add deployment_id +# variables.update({'deployment_id': CONF.kolla.deployment_id}) +# # override node_config_directory to empty +# variables.update({'node_config_directory': ''}) +# return variables + + +# Public API below +################## + +def run_service(service_name, service_dir, variables=None): + # generate zk variables + if service_name == 'nova-compute': + service_list = ['nova-compute', 'nova-libvirt', 'openvswitch-vswitchd', + 'neutron-openvswitch-agent', 'openvswitch-db'] + elif service_name == 'network-node': + service_list = ['neutron-openvswitch-agent', 'neutron-dhcp-agent', + 'neutron-metadata-agent', 'openvswitch-vswitchd', + 'openvswitch-db neutron-l3-agent'] + # TODO(dims): load this service _list from config + elif service_name == 'all': + service_list = ['keystone-init', 'keystone-api', 'keystone-db-sync', + 'glance-init', 'mariadb', 'rabbitmq', + 'glance-registry', + 'glance-api', 'nova-init', 'nova-api', + 'nova-scheduler', + 'nova-conductor', 'nova-consoleauth', 'neutron-init', + 'neutron-server', 'horizon', 'nova-compute', + 'nova-libvirt', 'openvswitch-vswitchd', + 'neutron-openvswitch-agent', 'openvswitch-db', + 'neutron-dhcp-agent', 'neutron-metadata-agent', + 'openvswitch-db neutron-l3-agent'] + elif service_name == 'zookeeper': + service_list = [] + else: + service_list = [service_name] + for service in service_list: + _build_runner(service, service_dir, variables=variables) + _deploy_instance(service_name) + + +def kill_service(service_name): + # if service_name == "all": + # with zk_utils.connection() as zk: + # status_node = os.path.join('kolla', CONF.kolla.deployment_id, + # 'status') + # zk.delete(status_node, recursive=True) + _delete_instance(service_name) + + +def _deploy_instance(service_name): + server = "--server=" + CONF.kubernetes.host + if service_name == 'all': + service_path = CONF.kubernetes.yml_dir_path + else: + service_path = CONF.kubernetes.yml_dir_path + service_name + ".yml" + cmd = [CONF.kubernetes.kubectl_path, server, "create", "-f", service_path] + # print cmd + subprocess.call(cmd) + + +def _delete_instance(service_name): + server = "--server=" + CONF.kubernetes.host + if service_name == 'all': + service_path = CONF.kubernetes.yml_dir_path + else: + service_path = CONF.kubernetes.yml_dir_path + service_name + ".yml" + cmd = [CONF.kubernetes.kubectl_path, server, "delete", "-f", service_path] + # print cmd + subprocess.call(cmd) diff --git a/kolla_kubernetes/service_definition.py b/kolla_kubernetes/service_definition.py new file mode 100644 index 000000000..923b36f8c --- /dev/null +++ b/kolla_kubernetes/service_definition.py @@ -0,0 +1,131 @@ +# 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 os.path +import socket +import yaml + +import jinja2 +from oslo_log import log + +from kolla_kubernetes.common import jinja_utils +from kolla_kubernetes import exception + +CNF_FIELDS = ('source', 'dest', 'owner', 'perm') +CMD_FIELDS = ('run_once', 'dependencies', 'command', 'env', + 'delay', 'retries', 'files') +DEP_FIELDS = ('path', 'scope') +SCOPE_OPTS = ('global', 'local') +LOG = log.getLogger() + + +def find_service_file(service_name, service_dir): + # let's be flexible with the input, to make life easy + # for users. + if not os.path.exists(service_dir): + raise exception.KollaNotFoundException(service_dir, + entity='service directory') + + short_name = service_name.split('/')[-1].replace('_ansible_tasks', '-init') + for root, dirs, names in os.walk(service_dir): + for name in names: + if short_name in name: + return os.path.join(root, name) + + raise exception.KollaNotFoundException(service_name, + entity='service definition') + + +def inspect(service_name, service_dir): + filename = find_service_file(service_name, service_dir) + try: + required_variables = set.union( + jinja_utils.jinja_find_required_variables(filename)) + except jinja2.exceptions.TemplateNotFound: + raise exception.KollaNotFoundException(filename, + entity='service definition') + return dict(required_variables=list(required_variables)) + + +def validate(service_name, service_dir, variables=None, deps=None): + if variables is None: + variables = {} + if deps is None: + deps = {} + + filename = find_service_file(service_name, service_dir) + try: + cnf = yaml.load(jinja_utils.jinja_render(filename, variables)) + except jinja2.exceptions.TemplateNotFound: + raise exception.KollaNotFoundException(filename, + entity='service definition') + + def get_commands(): + for cmd in cnf.get('commands', {}): + yield cmd, cnf['commands'][cmd] + if 'service' in cnf: + yield 'daemon', cnf['service']['daemon'] + + LOG.debug('%s: file found at %s' % (cnf['name'], filename)) + for cmd, cmd_info in get_commands(): + _validate_command(filename, cmd, cmd_info, deps, + cnf['name'], service_dir) + return deps + + +def _validate_config(filename, conf, service_dir): + for file in conf: + for key in conf[file]: + assert key in CNF_FIELDS, '%s: %s not in %s' % (filename, + key, CNF_FIELDS) + srcs = conf[file]['source'] + if isinstance(srcs, str): + srcs = [srcs] + for src in srcs: + file_path = os.path.join(service_dir, '..', src) + if not file_path.startswith('/etc'): + assert os.path.exists(file_path), '%s missing' % file_path + + +def _validate_command(filename, cmd, cmd_info, deps, + service_name, service_dir): + for key in cmd_info: + assert key in CMD_FIELDS, '%s not in %s' % (key, CMD_FIELDS) + + _, group, role = service_name.split('/') + regs = ['%s/%s' % (role, cmd), + '%s/%s/%s' % (socket.gethostname(), role, cmd)] + reqs = cmd_info.get('dependencies', []) + for reg in regs: + if reg not in deps: + deps[reg] = {'waiters': {}} + deps[reg]['registered_by'] = cmd + deps[reg]['name'] = cmd + deps[reg]['run_by'] = filename + for req in reqs: + for key in req: + assert key in DEP_FIELDS, '%s: %s not in %s' % (filename, + key, DEP_FIELDS) + scope = req.get('scope', 'global') + assert scope in SCOPE_OPTS, '%s: %s not in %s' % (filename, + scope, SCOPE_OPTS) + req_path = req['path'] + if scope == 'local': + req_path = os.path.join(socket.gethostname(), req_path) + if req_path not in deps: + deps[req_path] = {'waiters': {}} + for reg in regs: + deps[req_path]['waiters'][cmd] = reg + if 'files' in cmd_info: + _validate_config(filename, cmd_info['files'], service_dir) + LOG.debug('%s: command "%s" validated' % (service_name, cmd)) diff --git a/setup.cfg b/setup.cfg index 4d74ff6c9..64c5a4c12 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,7 +21,17 @@ classifier = [files] packages = - kolla-kubernetes + kolla_kubernetes +data_files = + share/kolla-kubernetes/services = services/* + +[entry_points] +console_scripts = + kolla-kubernetes = kolla_kubernetes.cmd.shell:main + +kolla_kubernetes.cli = + run = kolla_kubernetes.cli.service:Run + kill = kolla_kubernetes.cli.service:Kill [build_sphinx] source-dir = doc/source