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]