From e6459aa8f2a5cdf0c8113fd9b737fb36e46d4f56 Mon Sep 17 00:00:00 2001 From: Borne Mace Date: Thu, 10 May 2018 16:50:48 -0700 Subject: [PATCH] Add support for config reset command. The config reset command wipes all properties, inventory data, default tls certificates and passwords. The password set command now only allows modification of existing passwords, and clear only removes the value for existing password keys. The ability to completely add new and remove passwords no longer exists as it was really only useful in very edge cases and did not play nicely with the concept of doing a config reset. Change-Id: I9d1868da1161ebaf64793ab6d0e42de74389feab --- kolla_cli/api/client.py | 2 + kolla_cli/api/config.py | 34 +++++ kolla_cli/commands/config.py | 32 +++++ kolla_cli/common/utils.py | 84 +++++++++-- .../tests/functional/functional_test_setup.sh | 7 +- kolla_cli/tests/functional/test_config.py | 131 ++++++++++++++++++ kolla_cli/tests/functional/test_password.py | 75 +++++----- requirements.txt | 1 - setup.cfg | 2 + test-requirements.txt | 1 + tools/kolla_actions.py | 63 ++++++++- tox.ini | 8 +- 12 files changed, 376 insertions(+), 64 deletions(-) create mode 100644 kolla_cli/api/config.py create mode 100644 kolla_cli/commands/config.py create mode 100644 kolla_cli/tests/functional/test_config.py diff --git a/kolla_cli/api/client.py b/kolla_cli/api/client.py index 98999cf..5cd6251 100644 --- a/kolla_cli/api/client.py +++ b/kolla_cli/api/client.py @@ -16,6 +16,7 @@ import logging import sys from kolla_cli.api.certificate import CertificateApi +from kolla_cli.api.config import ConfigApi from kolla_cli.api.control_plane import ControlPlaneApi from kolla_cli.api.group import GroupApi from kolla_cli.api.host import HostApi @@ -32,6 +33,7 @@ VERSION = '0.1' class ClientApi( CertificateApi, + ConfigApi, ControlPlaneApi, GroupApi, HostApi, diff --git a/kolla_cli/api/config.py b/kolla_cli/api/config.py new file mode 100644 index 0000000..9aa2451 --- /dev/null +++ b/kolla_cli/api/config.py @@ -0,0 +1,34 @@ +# Copyright(c) 2018, Oracle and/or its affiliates. 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. +import kolla_cli.i18n as u + +from kolla_cli.api.exceptions import FailedOperation +from kolla_cli.common import utils + + +class ConfigApi(object): + + @staticmethod + def config_reset(): + """Config Reset. + + Resets the kolla-ansible configuration to its release defaults. + """ + actions_path = utils.get_kolla_actions_path() + cmd = ('%s config_reset' % actions_path) + err_msg, output = utils.run_cmd(cmd, print_output=False) + if err_msg: + raise FailedOperation( + u._('Configuration reset failed. {error} {message}') + .format(error=err_msg, message=output)) diff --git a/kolla_cli/commands/config.py b/kolla_cli/commands/config.py new file mode 100644 index 0000000..c6abb6d --- /dev/null +++ b/kolla_cli/commands/config.py @@ -0,0 +1,32 @@ +# Copyright(c) 2018, Oracle and/or its affiliates. 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. +import logging +import traceback + +from cliff.command import Command + +from kolla_cli.api.client import ClientApi + +CLIENT = ClientApi() +LOG = logging.getLogger(__name__) + + +class ConfigReset(Command): + """Resets the kolla-ansible configuration to its release defaults.""" + + def take_action(self, parsed_args): + try: + CLIENT.config_reset() + except Exception: + raise Exception(traceback.format_exc()) diff --git a/kolla_cli/common/utils.py b/kolla_cli/common/utils.py index 046aab3..83fe0a0 100644 --- a/kolla_cli/common/utils.py +++ b/kolla_cli/common/utils.py @@ -29,6 +29,9 @@ from kolla_cli.api.exceptions import MissingArgument LOG = logging.getLogger(__name__) +private_key_string = 'private_key' +public_key_string = 'public_key' + def get_log_level(): evar = os.environ.get('KOLLA_LOG_LEVEL', 'info') @@ -151,7 +154,7 @@ def run_cmd(cmd, print_output=True): err = safe_decode(err) output = safe_decode(output) - if process.returncode != 0: + if process is not None and process.returncode != 0: err = (u._('Command failed. : {error}') .format(error=err)) if print_output: @@ -168,38 +171,97 @@ def change_password(file_path, pname, pvalue=None, public_key=None, pvalue: value of password when not ssh key public_key: public ssh key private_key: private ssh key - clear: flag to remove password + clear: flag to clear password - If clear, and password exists, remove it from the password file. - If clear, and password doesn't exists, nothing is done. - If not clear, and key is not found, the new password will be added. - If not clear, and key is found, edit password in place. + If key is not found, an error is returned. + If clear, and password exists, remove password. + If clear, and password is already empty, nothing is done. + If not clear, edit password in place. The passwords file contains both key-value pairs and key-dictionary - pairs. + pairs. Type is maintained so you cannot change a key-dictionary + password to a key-value password or the other way around. """ read_data = sync_read_file(file_path) file_pwds = yaml.safe_load(read_data) # if the password file is empty file_pwds will be None after safe_load if file_pwds is None: file_pwds = {} + + if pname not in file_pwds.keys(): + raise Exception( + u._('unable to update password as it does not exist: {pname}') + .format(pname=pname)) + + ssh_password_type = is_ssh_password(file_pwds[pname]) + if clear: # clear if pname in file_pwds: - del file_pwds[pname] + if ssh_password_type: + file_pwds[pname] = {private_key_string: None, + public_key_string: None} + else: + file_pwds[pname] = None else: # edit if private_key: - file_pwds[pname] = {'private_key': private_key, - 'public_key': public_key} + if not ssh_password_type: + raise Exception( + u._('unable to set non ssh type password to ssh value')) + file_pwds[pname] = {private_key_string: private_key, + public_key_string: public_key} else: + if ssh_password_type: + raise Exception( + u._('unable to set ssh password type to non ssh value')) if not pvalue: pvalue = None file_pwds[pname] = pvalue - write_data = yaml.safe_dump(file_pwds, default_flow_style=False) + + # dump Nones as empty strings instead of the value 'null' as this is how + # it looks when we read it. also, this will not work with safe_dump + yaml.add_representer(type(None), _empty_is_none) + write_data = yaml.dump(file_pwds, default_flow_style=False) sync_write_file(file_path, write_data) +def clear_all_passwords(): + """clear all passwords in passwords.yml file""" + password_path = os.path.join(get_kolla_etc(), 'passwords.yml') + read_data = sync_read_file(password_path) + file_pwds = yaml.safe_load(read_data) + # if the password file is empty file_pwds will be None after safe_load + if file_pwds is None: + file_pwds = {} + + keys = file_pwds.keys() + for key in keys: + if is_ssh_password(file_pwds[key]): + file_pwds[key] = {private_key_string: None, + public_key_string: None} + else: + file_pwds[key] = None + + yaml.add_representer(type(None), _empty_is_none) + write_data = yaml.dump(file_pwds, default_flow_style=False) + sync_write_file(password_path, write_data) + + +def _empty_is_none(self, _): + return self.represent_scalar('tag:yaml.org,2002:null', '') + + +def is_ssh_password(password): + if password is not None: + if isinstance(password, dict): + password_keys = password.keys() + if (private_key_string in password_keys and + public_key_string in password_keys): + return True + return False + + def change_property(file_path, property_dict, clear=False): """change property with a file diff --git a/kolla_cli/tests/functional/functional_test_setup.sh b/kolla_cli/tests/functional/functional_test_setup.sh index fd6da93..0b74896 100755 --- a/kolla_cli/tests/functional/functional_test_setup.sh +++ b/kolla_cli/tests/functional/functional_test_setup.sh @@ -14,12 +14,6 @@ touch $KOLLA_ETC/kolla-cli/ansible/inventory.json mkdir -p $KOLLA_HOME/kolla-cli touch $KOLLA_HOME/kolla-cli/ansible.lock -# setup kolla-ansible passwords file with just 2 passwords -cat > $KOLLA_ETC/passwords.yml <=1.9.2 -kolla-ansible Babel>=0.9.6 cliff>=1.13.0 # Apache-2.0 cliff-tablib<=1.1 diff --git a/setup.cfg b/setup.cfg index 80ae5f5..1d443eb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,6 +29,7 @@ console_scripts = kolla.cli = certificate_init = kolla_cli.commands.certificate:CertificateInit + config_reset = kolla_cli.commands.config:ConfigReset deploy = kolla_cli.commands.deploy:Deploy dump = kolla_cli.commands.support:Dump group_add = kolla_cli.commands.group:GroupAdd @@ -61,6 +62,7 @@ kolla.cli = setdeploy = kolla_cli.commands.deploy:Setdeploy upgrade = kolla_cli.commands.upgrade:Upgrade + [extract_messages] keywords = _ gettext ngettext l_ lazy_gettext mapping_file = babel.cfg diff --git a/test-requirements.txt b/test-requirements.txt index f7ea402..bf21a0c 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -10,6 +10,7 @@ discover fixtures>=0.3.14 mock>=1.0 mypy>=0.6; python_version>'2.7' +oslo.utils>=3.33.0 # Apache-2.0 os-testr>=1.0.0 # Apache-2.0 pexpect>=4.0.1 sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 diff --git a/tools/kolla_actions.py b/tools/kolla_actions.py index d65a7eb..ae4794c 100755 --- a/tools/kolla_actions.py +++ b/tools/kolla_actions.py @@ -20,10 +20,16 @@ import sys import yaml from kolla_cli.common.utils import change_password +from kolla_cli.common.utils import clear_all_passwords +from kolla_cli.common.utils import get_kolla_ansible_home +from kolla_cli.common.utils import get_kolla_cli_etc +from kolla_cli.common.utils import get_kolla_etc -def _init_keys(): +def _init_keys(path): cmd = 'kolla-genpwd' + if os.path.exists(path): + cmd = ' '.join((cmd, '-p', path)) (_, err) = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() @@ -117,7 +123,7 @@ def _password_cmd(argv): init_flag = True if init_flag: # init empty keys - _init_keys() + _init_keys(path) elif list_flag: # print the password keys _print_pwd_keys(path) @@ -154,6 +160,56 @@ def _job_cmd(argv): raise Exception('%s, pid %s' % (str(e), pid)) +def _config_reset_cmd(): + """config_reset command + + args for config_reset command + - none + """ + kolla_etc = get_kolla_etc() + kolla_home = get_kolla_ansible_home() + kollacli_etc = get_kolla_cli_etc() + + group_vars_path = os.path.join(kolla_home, 'ansible/group_vars') + host_vars_path = os.path.join(kolla_home, 'ansible/host_vars') + globals_path = os.path.join(group_vars_path, '__GLOBAL__') + inventory_path = os.path.join(kollacli_etc, 'ansible/inventory.json') + + # truncate global property and inventory files + with open(globals_path, 'w') as globals_file: + globals_file.truncate() + + with open(inventory_path, 'w') as inventory_file: + inventory_file.truncate() + + # clear all passwords + clear_all_passwords() + + # nuke all files under the kolla etc base, skipping everything + # in the kollacli directory and the passwords.yml file + for dir_path, dir_names, file_names in os.walk(kolla_etc, topdown=False): + if 'kollacli' not in dir_path: + for dir_name in dir_names: + if dir_name != 'kollacli': + os.rmdir(os.path.join(dir_path, dir_name)) + + for file_name in file_names: + if file_name != 'passwords.yml': + os.remove(os.path.join(dir_path, file_name)) + + # nuke all property files under the kolla-ansible base other than + # all.yml and the global property file which we truncate above + for dir_path, _, file_names in os.walk(group_vars_path): + for file_name in file_names: + if (file_name != '__GLOBAL__' and + file_name != 'all.yml'): + os.remove(os.path.join(dir_path, file_name)) + + for dir_path, _, file_names in os.walk(host_vars_path): + for file_name in file_names: + os.remove(os.path.join(dir_path, file_name)) + + def main(): """perform actions on behalf of kolla user @@ -163,6 +219,7 @@ def main(): Supported commands: - password - job + - config_reset """ if len(sys.argv) <= 1: raise Exception('Invalid number of parameters') @@ -172,6 +229,8 @@ def main(): _password_cmd(sys.argv) elif command == 'job': _job_cmd(sys.argv) + elif command == 'config_reset': + _config_reset_cmd() else: raise Exception('Invalid command %s' % command) diff --git a/tox.ini b/tox.ini index 3556493..776fd4b 100644 --- a/tox.ini +++ b/tox.ini @@ -6,6 +6,7 @@ envlist = pep8,mypy,functional,functional-py35 [testenv] usedevelop=True whitelist_externals = find + bash install_command = pip install -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt @@ -22,12 +23,13 @@ whitelist_externals = {toxinidir}/kolla_cli/tests/functional/functional_test_setup.sh setenv = OS_TEST_PATH = ./kolla_cli/tests/functional - KOLLA_ETC = /tmp/kollaclitest/etc/kolla/ - KOLLA_HOME = /tmp/kollaclitest/usr/share/kolla-ansible/ - KOLLA_TOOLS_DIR = {toxinidir}/tools/ + KOLLA_ETC = /tmp/kollaclitest/etc/kolla + KOLLA_HOME = /tmp/kollaclitest/usr/share/kolla-ansible + KOLLA_TOOLS_DIR = {toxinidir}/tools commands = {[testenv]commands} {toxinidir}/kolla_cli/tests/functional/functional_test_setup.sh + bash -c "pushd /tmp/kollaclitest/usr/share/kolla-ansible/git; python setup.py install; popd" ostestr {posargs} --serial [testenv:functional-py35]