From 7bd0c7566c9d3da0fc1f3e016395a17797bf7931 Mon Sep 17 00:00:00 2001 From: Davanum Srinivas Date: Sun, 8 May 2016 22:10:46 -0400 Subject: [PATCH] Implement a command line to start/stop keystone services Picked up code from old repositories. Main change is in service.py. related blueprint kolla-kubernetes implements blueprint kolla-kubernetes-cli Change-Id: I9caefe557ac827ca7a3b8f9a1693d623cf369080 --- kolla_kubernetes/cli/__init__.py | 0 kolla_kubernetes/cli/service.py | 48 +++++ kolla_kubernetes/cmd/__init__.py | 0 kolla_kubernetes/cmd/shell.py | 160 ++++++++++++++++ kolla_kubernetes/common/__init__.py | 0 kolla_kubernetes/common/file_utils.py | 109 +++++++++++ kolla_kubernetes/common/jinja_utils.py | 102 ++++++++++ kolla_kubernetes/common/type_utils.py | 19 ++ kolla_kubernetes/common/utils.py | 46 +++++ kolla_kubernetes/config.py | 55 ++++++ kolla_kubernetes/exception.py | 31 ++++ kolla_kubernetes/service.py | 174 ++++++++++++++++++ kolla_kubernetes/service_definition.py | 151 +++++++++++++++ kolla_kubernetes/tests/base.py | 30 ++- kolla_kubernetes/tests/common/__init__.py | 0 .../tests/common/test_file_utils.py | 21 +++ .../tests/common/test_jinja_utils.py | 29 +++ .../tests/common/test_type_utils.py | 35 ++++ kolla_kubernetes/tests/common/test_utils.py | 41 +++++ .../tests/test_kolla_kubernetes.py | 28 --- kubernetes/keystone/keystone-services.yaml | 20 -- requirements.txt | 7 + .../keystone/keystone-pod.yml.j2 | 9 +- .../keystone/keystone-service-admin.yml.j2 | 9 + .../keystone/keystone-service-public.yml.j2 | 9 + setup.cfg | 12 +- 26 files changed, 1085 insertions(+), 60 deletions(-) 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 create mode 100644 kolla_kubernetes/tests/common/__init__.py create mode 100644 kolla_kubernetes/tests/common/test_file_utils.py create mode 100644 kolla_kubernetes/tests/common/test_jinja_utils.py create mode 100644 kolla_kubernetes/tests/common/test_type_utils.py create mode 100644 kolla_kubernetes/tests/common/test_utils.py delete mode 100644 kolla_kubernetes/tests/test_kolla_kubernetes.py delete mode 100644 kubernetes/keystone/keystone-services.yaml rename kubernetes/keystone/keystone-pod.yaml => services/keystone/keystone-pod.yml.j2 (59%) create mode 100644 services/keystone/keystone-service-admin.yml.j2 create mode 100644 services/keystone/keystone-service-public.yml.j2 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..990622a43 --- /dev/null +++ b/kolla_kubernetes/cli/service.py @@ -0,0 +1,48 @@ +# 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.common import file_utils +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, + file_utils.get_services_dir(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, + file_utils.get_services_dir(CONF.service_dir)) 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..eebb7140c --- /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..ec5a9641b --- /dev/null +++ b/kolla_kubernetes/common/file_utils.py @@ -0,0 +1,109 @@ +# 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_services_dir(base_dir): + if os.path.exists(os.path.join(base_dir, 'services')): + return os.path.join(base_dir, 'services') + elif os.path.exists(os.path.join(get_src_dir(), 'services')): + return os.path.join(get_src_dir(), 'services') + raise exception.KollaDirNotFoundException( + 'Unable to detect kolla-kubernetes directory' + ) + + +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 get_shared_directory(): + if os.path.exists('/usr/local/share/kolla'): + return '/usr/local/share/kolla' + elif os.path.exists('/usr/share/kolla'): + return '/usr/share/kolla' + return None + + +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 os.path.exists('/usr/local/share/kolla'): + return '/usr/local/share/kolla' + elif os.path.exists('/usr/share/kolla'): + return '/usr/share/kolla' + 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..b0e4d8d36 --- /dev/null +++ b/kolla_kubernetes/config.py @@ -0,0 +1,55 @@ +# 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='http://localhost:8080'), + cfg.StrOpt('kubectl_path', default='kubectl') +] +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..7b256459e --- /dev/null +++ b/kolla_kubernetes/service.py @@ -0,0 +1,174 @@ +# 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 datetime +import functools +import os.path +import subprocess +import tempfile +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 + + +class Command(object): + def __init__(self, conf, name, service_name): + self._conf = conf + self._name = name + self._service_name = service_name + + +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): + jvars = JvarsDict() + f = file_utils.find_config_file('globals.yml') + if os.path.exists(f): + with open(f, 'r') as gf: + jvars.set_global_vars(yaml.load(gf)) + f = file_utils.find_config_file('passwords.yml') + if os.path.exists(f): + with open(f, '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()) + }) + + dir = file_utils.get_shared_directory() + if dir and os.path.exists(os.path.join(dir, 'ansible/group_vars/all.yml')): + all_yml_name = os.path.join(dir, 'ansible/group_vars/all.yml') + jinja_utils.yaml_jinja_render(all_yml_name, jvars) + + proj_yml_name = os.path.join(dir, 'ansible/roles', + project_name, 'defaults', 'main.yml') + if dir and 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): + ts = time.time() + ts = datetime.datetime.fromtimestamp(ts).strftime('%Y-%m-%d_%H-%M-%S_') + temp_dir = tempfile.mkdtemp(prefix='kolla-' + ts) + working_dir = os.path.join(temp_dir, 'kubernetes') + os.makedirs(working_dir) + + for filename in service_definition.find_service_files(service_name, + service_dir): + proj_filename = filename.split('/')[-1].replace('.j2', '') + proj_name = filename.split('/')[-2] + LOG.debug( + 'proj_filename : %s proj_name: %s' % (proj_filename, proj_name)) + + # 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) + + content = yaml.load( + jinja_utils.jinja_render(filename, variables)) + with open(os.path.join(working_dir, proj_filename), 'w') as f: + LOG.debug('_build_runner : service file : %s' % + os.path.join(working_dir, proj_filename)) + f.write(yaml.dump(content, default_flow_style=False)) + + return working_dir + + +def run_service(service_name, service_dir, variables=None): + directory = _build_runner(service_name, service_dir, variables=variables) + _deploy_instance(directory, service_name) + + +def kill_service(service_name, service_dir, variables=None): + directory = _build_runner(service_name, service_dir, variables=variables) + _delete_instance(directory, service_name) + + +def _deploy_instance(directory, service_name): + server = "--server=" + CONF.kolla_kubernetes.host + cmd = [CONF.kolla_kubernetes.kubectl_path, server, "create", "-f", + directory] + LOG.info('Command : %r' % cmd) + subprocess.call(cmd) + + +def _delete_instance(directory, service_name): + server = "--server=" + CONF.kolla_kubernetes.host + cmd = [CONF.kolla_kubernetes.kubectl_path, server, "delete", "-f", + directory] + LOG.info('Command : %r' % cmd) + subprocess.call(cmd) diff --git a/kolla_kubernetes/service_definition.py b/kolla_kubernetes/service_definition.py new file mode 100644 index 000000000..1810f8995 --- /dev/null +++ b/kolla_kubernetes/service_definition.py @@ -0,0 +1,151 @@ +# 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 find_service_files(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') + files = [] + for root, dirs, names in os.walk(service_dir): + for name in names: + if short_name in name: + files.append(os.path.join(root, name)) + + if not files: + raise exception.KollaNotFoundException(service_name, + entity='service definition') + return files + + +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['metadata']['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/kolla_kubernetes/tests/base.py b/kolla_kubernetes/tests/base.py index 1c30cdb56..70fcdaeea 100644 --- a/kolla_kubernetes/tests/base.py +++ b/kolla_kubernetes/tests/base.py @@ -1,8 +1,3 @@ -# -*- coding: utf-8 -*- - -# Copyright 2010-2011 OpenStack Foundation -# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. -# # 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 @@ -15,9 +10,32 @@ # License for the specific language governing permissions and limitations # under the License. +import contextlib +import os.path +import sys + +from oslo_config import cfg from oslotest import base +import six +import testscenarios -class TestCase(base.BaseTestCase): +# Python 3, thank you for dropping contextlib.nested +if six.PY3: + @contextlib.contextmanager + def nested(*contexts): + with contextlib.ExitStack() as stack: + yield [stack.enter_context(c) for c in contexts] +else: + nested = contextlib.nested + +class BaseTestCase(testscenarios.WithScenarios, + base.BaseTestCase): """Test case base class for all unit tests.""" + + def setUp(self): + super(BaseTestCase, self).setUp() + self.addCleanup(cfg.CONF.reset) + mod_dir = os.path.dirname(sys.modules[__name__].__file__) + self.project_dir = os.path.abspath(os.path.join(mod_dir, '..', '..')) diff --git a/kolla_kubernetes/tests/common/__init__.py b/kolla_kubernetes/tests/common/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/kolla_kubernetes/tests/common/test_file_utils.py b/kolla_kubernetes/tests/common/test_file_utils.py new file mode 100644 index 000000000..8910a8f6b --- /dev/null +++ b/kolla_kubernetes/tests/common/test_file_utils.py @@ -0,0 +1,21 @@ +# 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 kolla_kubernetes.common import file_utils +from kolla_kubernetes.tests import base + + +class FindBaseDirTest(base.BaseTestCase): + + def test_when_is_a_test(self): + tdir = file_utils.find_base_dir() + self.assertEqual(self.project_dir, tdir) diff --git a/kolla_kubernetes/tests/common/test_jinja_utils.py b/kolla_kubernetes/tests/common/test_jinja_utils.py new file mode 100644 index 000000000..fbfd105fc --- /dev/null +++ b/kolla_kubernetes/tests/common/test_jinja_utils.py @@ -0,0 +1,29 @@ +# 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 + +from kolla_kubernetes.common import jinja_utils +from kolla_kubernetes.tests import base + + +class TestJinjaUtils(base.BaseTestCase): + + def test_dict_jinja_render(self): + raw_dict = collections.OrderedDict([ + ('first_key', '{{ test_var }}_test',), + ('second_key', '{{ first_key }}_test'), + ]) + jvars = {'test_var': 'test'} + jinja_utils.dict_jinja_render(raw_dict, jvars) + self.assertEqual(jvars['first_key'], 'test_test') + self.assertEqual(jvars['second_key'], 'test_test_test') diff --git a/kolla_kubernetes/tests/common/test_type_utils.py b/kolla_kubernetes/tests/common/test_type_utils.py new file mode 100644 index 000000000..90a01718d --- /dev/null +++ b/kolla_kubernetes/tests/common/test_type_utils.py @@ -0,0 +1,35 @@ +# 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 kolla_kubernetes.common import type_utils +from kolla_kubernetes.tests import base + + +class StrToBoolTest(base.BaseTestCase): + + scenarios = [ + ('none', dict(text=None, expect=False)), + ('empty', dict(text='', expect=False)), + ('junk', dict(text='unlikely', expect=False)), + ('no', dict(text='no', expect=False)), + ('yes', dict(text='yes', expect=True)), + ('0', dict(text='0', expect=False)), + ('1', dict(text='1', expect=False)), + ('True', dict(text='True', expect=True)), + ('False', dict(text='False', expect=False)), + ('true', dict(text='true', expect=True)), + ('false', dict(text='false', expect=False)), + ('shouty', dict(text='TRUE', expect=True)), + ] + + def test_str_to_bool(self): + self.assertEqual(self.expect, type_utils.str_to_bool(self.text)) diff --git a/kolla_kubernetes/tests/common/test_utils.py b/kolla_kubernetes/tests/common/test_utils.py new file mode 100644 index 000000000..77fd5ffa4 --- /dev/null +++ b/kolla_kubernetes/tests/common/test_utils.py @@ -0,0 +1,41 @@ +# 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 kolla_kubernetes.common import utils +from kolla_kubernetes.tests import base + + +class TestDictUpdate(base.BaseTestCase): + + def test_flat_no_overwrites(self): + a = {'a': 'foo', 'b': 'no'} + b = {'c': 'foo', 'd': 'no'} + expect = {'a': 'foo', 'c': 'foo', 'b': 'no', 'd': 'no'} + self.assertEqual(expect, utils.dict_update(a, b)) + + def test_flat_with_overwrites(self): + a = {'a': 'foo', 'b': 'no'} + b = {'c': 'foo', 'b': 'yes'} + expect = {'a': 'foo', 'c': 'foo', 'b': 'yes'} + self.assertEqual(expect, utils.dict_update(a, b)) + + def test_nested_no_overwrites(self): + a = {'a': 'foo', 'b': {'bb': 'no'}} + b = {'c': 'foo'} + expect = {'a': 'foo', 'c': 'foo', 'b': {'bb': 'no'}} + self.assertEqual(expect, utils.dict_update(a, b)) + + def test_nested_with_overwrites(self): + a = {'a': 'foo', 'b': {'bb': 'no'}} + b = {'c': 'foo', 'b': {'bb': 'yes'}} + expect = {'a': 'foo', 'c': 'foo', 'b': {'bb': 'yes'}} + self.assertEqual(expect, utils.dict_update(a, b)) diff --git a/kolla_kubernetes/tests/test_kolla_kubernetes.py b/kolla_kubernetes/tests/test_kolla_kubernetes.py deleted file mode 100644 index 5f5ab6965..000000000 --- a/kolla_kubernetes/tests/test_kolla_kubernetes.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- - -# 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. - -""" -test_kolla-kubernetes ----------------------------------- - -Tests for `kolla-kubernetes` module. -""" - -from kolla_kubernetes.tests import base - - -class TestKollaKubernetes(base.TestCase): - - def test_something(self): - pass diff --git a/kubernetes/keystone/keystone-services.yaml b/kubernetes/keystone/keystone-services.yaml deleted file mode 100644 index 0b61e42bc..000000000 --- a/kubernetes/keystone/keystone-services.yaml +++ /dev/null @@ -1,20 +0,0 @@ -apiVersion: v1 -kind: Service -spec: - ports: - - port: 35357 - selector: - name: keystone-admin -metadata: - name: keystone-admin - ---- -apiVersion: v1 -kind: Service -spec: - ports: - - port: 5000 - selector: - name: keystone-public -metadata: - name: keystone-public \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 30806d5ac..5250e5449 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,10 @@ # process, which may cause wedges in the gate later. pbr>=1.6 +cliff!=1.16.0,!=1.17.0,>=1.15.0 # Apache-2.0 +oslo.config>=3.9.0 # Apache-2.0 +oslo.utils>=3.5.0 # Apache-2.0 +oslo.log>=1.14.0 # Apache-2.0 +six>=1.9.0 # MIT +Jinja2>=2.8 # BSD License (3 clause) +PyYAML>=3.1.0 # MIT diff --git a/kubernetes/keystone/keystone-pod.yaml b/services/keystone/keystone-pod.yml.j2 similarity index 59% rename from kubernetes/keystone/keystone-pod.yaml rename to services/keystone/keystone-pod.yml.j2 index f6433e4d9..31206c652 100644 --- a/kubernetes/keystone/keystone-pod.yaml +++ b/services/keystone/keystone-pod.yml.j2 @@ -3,18 +3,17 @@ kind: Pod spec: hostNetwork: True containers: - #TODO: Use a jinja2 template for image - - image: kollaglue/centos-binary-keystone:2.0.0 + - image: "{{ keystone_image_full }}" name: keystone volumeMounts: - - mountPath: "/var/lib/kolla/config_files" + - mountPath: {{ container_config_directory }} name: keystone-config env: - name: KOLLA_CONFIG_STRATEGY - value: COPY_ALWAYS + value: {{ config_strategy }} volumes: - name: keystone-config hostPath: path: "/etc/kolla/keystone" metadata: - name: keystone + name: keystone \ No newline at end of file diff --git a/services/keystone/keystone-service-admin.yml.j2 b/services/keystone/keystone-service-admin.yml.j2 new file mode 100644 index 000000000..d8c9ccfa7 --- /dev/null +++ b/services/keystone/keystone-service-admin.yml.j2 @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Service +spec: + ports: + - port: {{ keystone_admin_port }} + selector: + name: keystone-admin +metadata: + name: keystone-admin \ No newline at end of file diff --git a/services/keystone/keystone-service-public.yml.j2 b/services/keystone/keystone-service-public.yml.j2 new file mode 100644 index 000000000..50cfabc94 --- /dev/null +++ b/services/keystone/keystone-service-public.yml.j2 @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Service +spec: + ports: + - port: {{ keystone_public_port }} + selector: + name: keystone-public +metadata: + name: keystone-public \ No newline at end of file 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