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
This commit is contained in:
Borne Mace 2018-05-10 16:50:48 -07:00
parent 502bb06d2f
commit e6459aa8f2
12 changed files with 376 additions and 64 deletions

View File

@ -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,

34
kolla_cli/api/config.py Normal file
View File

@ -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))

View File

@ -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())

View File

@ -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

View File

@ -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 <<EOF
database_password: foobar
nova_password: foobar
EOF
# If it's not there, clone the kolla-ansible repo to get its ansible directory
# and then copy it over
mkdir -p $KOLLA_HOME/git
@ -29,5 +23,6 @@ if [ ! -d $KOLLA_HOME/ansible ]; then
fi
# setup needed kolla-ansible files
cp $KOLLA_HOME/git/etc/kolla/passwords.yml $KOLLA_ETC/passwords.yml
mkdir -p $KOLLA_HOME/ansible/host_vars
touch $KOLLA_HOME/ansible/group_vars/__GLOBAL__

View File

@ -0,0 +1,131 @@
# Copyright(c) 2016, 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.api.properties
import unittest
from kolla_cli.api.client import ClientApi
from kolla_cli.tests.functional.common import KollaCliTest
CLIENT = ClientApi()
class TestFunctional(KollaCliTest):
def test_config_reset(self):
# test global property reset
# set a property and make sure it was set correctly
property_dict = {'test': 'test'}
CLIENT.property_set(property_dict)
fetched_properties = CLIENT.property_get()
fetched_dict = self._properties_to_dict(fetched_properties)
self.assertIs(self._in_dict(property_dict, fetched_dict), True,
'property set failed')
# now clear the config and make sure the property we just
# set is now gone
CLIENT.config_reset()
fetched_properties = CLIENT.property_get()
fetched_dict = self._properties_to_dict(fetched_properties)
self.assertIs(self._in_dict(property_dict, fetched_dict), False,
'global property reset config failed')
# test host property reset
host_list = ['test']
CLIENT.host_add(host_list)
CLIENT.property_set(property_dict,
kolla_cli.api.properties.HOST_TYPE)
fetched_properties = CLIENT.property_get(
kolla_cli.api.properties.HOST_TYPE, host_list)
fetched_dict = self._properties_to_dict(fetched_properties)
self.assertIs(self._in_dict(property_dict, fetched_dict), True,
'host property set failed')
CLIENT.config_reset()
# need to add back in the host 'test' or the property
# get call will fail after a reset
CLIENT.host_add(host_list)
fetched_properties = CLIENT.property_get(
kolla_cli.api.properties.HOST_TYPE, host_list)
fetched_dict = self._properties_to_dict(fetched_properties)
self.assertIs(self._in_dict(property_dict, fetched_dict), False,
'host property reset config failed')
# test group property reset
group_list = ['control']
CLIENT.property_set(property_dict,
kolla_cli.api.properties.GROUP_TYPE)
fetched_properties = CLIENT.property_get(
kolla_cli.api.properties.GROUP_TYPE, group_list)
fetched_dict = self._properties_to_dict(fetched_properties)
self.assertIs(self._in_dict(property_dict, fetched_dict), True,
'group property set failed')
CLIENT.config_reset()
fetched_properties = CLIENT.property_get(
kolla_cli.api.properties.GROUP_TYPE, group_list)
fetched_dict = self._properties_to_dict(fetched_properties)
self.assertIs(self._in_dict(property_dict, fetched_dict), False,
'group property reset config failed')
# test host reset
# add a host and make sure it was added correctly
host_list = ['test']
CLIENT.host_add(host_list)
fetched_hosts = CLIENT.host_get_all()
fetched_list = self._hosts_to_list(fetched_hosts)
self.assertIs(set(host_list).issubset(fetched_list), True,
'host set failed')
# now clear the config and make sure the host we just
# added is now gone
CLIENT.config_reset()
fetched_hosts = CLIENT.host_get_all()
fetched_list = self._hosts_to_list(fetched_hosts)
self.assertIs(set(host_list).issubset(fetched_list), False,
'inventory reset config failed')
# need to populate the password file or many other tests will fail
CLIENT.password_init()
@staticmethod
def _properties_to_dict(props):
property_dict = {}
for prop in props:
property_dict[prop.name] = prop.value
return property_dict
@staticmethod
def _hosts_to_list(hosts):
host_list = []
for host in hosts:
host_list.append(host.name)
return host_list
@staticmethod
def _in_dict(base, target):
base_keys = base.keys()
target_keys = target.keys()
if set(base_keys).issubset(target_keys) is False:
return False
for key in base.keys():
target_value = target.get(key, None)
if target_value != base[key]:
return False
return True
if __name__ == '__main__':
unittest.main()

View File

@ -12,12 +12,12 @@
# License for the specific language governing permissions and limitations
# under the License.
#
from kolla_cli.tests.functional.common import KollaCliTest
import os
import unittest
from kolla_cli.api import client
from kolla_cli.common.utils import get_kolla_etc
from kolla_cli.tests.functional.common import KollaCliTest
CLIENT = client.ClientApi()
@ -54,13 +54,6 @@ PRIVATE_KEY = (
class TestFunctional(KollaCliTest):
def test_password_set_clear(self):
# This test should leave the passwords.yml file unchanged
# after the test completes. The tox setup bash script sets
# one password - "database_password" to "foobar".
pwds_path = os.path.join(get_kolla_etc(), 'passwords.yml')
size_start = os.path.getsize(pwds_path)
# test list
msg = self.run_cli_cmd('password list')
key = 'database_password'
@ -69,26 +62,6 @@ class TestFunctional(KollaCliTest):
self.assertTrue(ok, 'list failed. Password (%s/%s) not in output: %s'
% (key, value, msg))
# test append
key = 'TeStKeY'
value = '-'
self.run_cli_cmd('password set %s --insecure %s' % (key, value))
msg = self.run_cli_cmd('password list')
ok = self._password_value_exists(key, value, msg)
self.assertTrue(ok, 'set new password failed. Password ' +
'(%s/%s) not in output: %s'
% (key, value, msg))
# test modify existing
key = 'TeStKeY'
value = '-'
self.run_cli_cmd('password set %s --insecure %s' % (key, value))
msg = self.run_cli_cmd('password list')
ok = self._password_value_exists(key, value, msg)
self.assertTrue(ok, 'set modify password failed. Password ' +
'(%s/%s) not in output: %s' %
(key, value, msg))
# test setting empty password
self.run_cli_cmd('password set %s --insecure' % key)
msg = self.run_cli_cmd('password list')
@ -106,28 +79,48 @@ class TestFunctional(KollaCliTest):
(key, msg))
# test clear
key = 'TeStKeY'
key = 'database_password'
value = '-'
self.run_cli_cmd('password clear %s' % key)
msg = self.run_cli_cmd('password list')
ok = self._password_value_exists(key, value, msg)
self.assertFalse(ok, 'clear password failed. Password ' +
'(%s/%s) not in output: %s' %
(key, value, msg))
self.assertTrue(ok, 'clear password failed. Password ' +
'(%s/%s) not in output: %s' %
(key, value, msg))
# test setting/clearing an ssh key
key = 'TeStKeY'
# test setting an ssh key
key = 'nova_ssh_key'
CLIENT.password_set_sshkey(key, PRIVATE_KEY, PUBLIC_KEY)
keynames = CLIENT.password_get_names()
self.assertIn(key, keynames, 'ssh key not in passwords')
CLIENT.password_clear(key)
keynames = CLIENT.password_get_names()
self.assertNotIn(key, keynames, 'ssh key not cleared from passwords')
# check that passwords.yml file size didn't change
size_end = os.path.getsize(pwds_path)
self.assertEqual(size_start, size_end, 'passwords.yml size changed ' +
'from %s to %s' % (size_start, size_end))
# test modify non-ssh password
key = 'database_password'
value = '-'
self.run_cli_cmd('password set %s --insecure %s' % (key, value))
msg = self.run_cli_cmd('password list')
ok = self._password_value_exists(key, value, msg)
self.assertTrue(ok, 'set modify password failed. Password ' +
'(%s/%s) not in output: %s' %
(key, value, msg))
# test to make sure that saves / loads aren't doing something
# bad to the password file size
CLIENT.password_clear(key)
# snapshot file size with key cleared
password_file_path = os.path.join(get_kolla_etc(), 'passwords.yml')
size_start = os.path.getsize(password_file_path)
# set and clear password
CLIENT.password_set(key, value)
CLIENT.password_clear(key)
size_end = os.path.getsize(password_file_path)
self.assertEqual(size_start, size_end, 'password file size changed ' +
'during set/clear (%s/%s)' % (size_start, size_end))
# make sure to end the test with the password init, as some other
# non-password related tests require that all passwords in the file
# be populated
CLIENT.password_init()
def _password_value_exists(self, key, value, cli_output):
"""Verify cli data against model data"""

View File

@ -1,5 +1,4 @@
ansible>=1.9.2
kolla-ansible
Babel>=0.9.6
cliff>=1.13.0 # Apache-2.0
cliff-tablib<=1.1

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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]