From 3f029a7bd465f377eec716d06918f6e0a92398ea Mon Sep 17 00:00:00 2001 From: Steve Noyes Date: Tue, 20 Sep 2016 15:26:49 +0200 Subject: [PATCH] add ability to cli/api to add/clear ssh keys in password file - add new password setkey cli command - add new api password_set_sshkey - add ability to param checker to not display bad param - add new change_password util for kolla_actions to call - update kolla_actions to handle ssh keys - add utest for setting/clearing ssh keys Change-Id: I1fedb85d21cd04c222f7250bdda66ad42a9ddca3 Jira-Issue: OPENSTACK-1071 --- kollacli/api/password.py | 18 +++++++++++++ kollacli/commands/password.py | 40 +++++++++++++++++++++++++++ kollacli/common/passwords.py | 10 +++++++ kollacli/common/utils.py | 51 ++++++++++++++++++++++++++++++++--- setup.cfg | 1 + tests/password.py | 41 ++++++++++++++++++++++++++++ tools/kolla_actions.py | 32 +++++++++++++--------- 7 files changed, 177 insertions(+), 16 deletions(-) diff --git a/kollacli/api/password.py b/kollacli/api/password.py index bf2bf5b..e7f520f 100644 --- a/kollacli/api/password.py +++ b/kollacli/api/password.py @@ -16,6 +16,7 @@ import kollacli.i18n as u from kollacli.common.passwords import clear_password from kollacli.common.passwords import get_password_names from kollacli.common.passwords import set_password +from kollacli.common.passwords import set_password_sshkey from kollacli.common.utils import check_arg @@ -31,8 +32,25 @@ class PasswordApi(object): :type value: string """ check_arg(name, u._('Password name'), str) + check_arg(value, u._('Password value'), str, display_param=False) set_password(name, value) + def password_set_sshkey(self, name, private_key, public_key): + # type: (str, str, str) -> None + """Set password to an ssh key + + :param name: name of the password + :type name: string + :param private_key: ssh private key + :type value: string + :param public_key: ssh public key + :type value: string + """ + check_arg(name, u._('Password name'), str) + check_arg(private_key, u._('Private key'), str, display_param=False) + check_arg(public_key, u._('Public key'), str, display_param=False) + set_password_sshkey(name, private_key, public_key) + def password_clear(self, name): # type: (str) -> None """Clear password diff --git a/kollacli/commands/password.py b/kollacli/commands/password.py index f099df5..a2cc652 100644 --- a/kollacli/commands/password.py +++ b/kollacli/commands/password.py @@ -13,6 +13,7 @@ # under the License. import argparse import getpass +import os import traceback import kollacli.i18n as u @@ -54,6 +55,45 @@ class PasswordSet(Command): raise Exception(traceback.format_exc()) +class PasswordSetKey(Command): + "Password Set SSH Key" + + def get_parser(self, prog_name): + parser = super(PasswordSetKey, self).get_parser(prog_name) + parser.add_argument('passwordname', metavar='', + help=u._('Password name')) + parser.add_argument('privatekeypath', metavar='', + help=u._('Path to private key file')) + parser.add_argument('publickeypath', metavar='', + help=u._('Path to public key file')) + return parser + + def take_action(self, parsed_args): + try: + password_name = parsed_args.passwordname.strip() + private_keypath = parsed_args.privatekeypath.strip() + private_keypath = os.path.abspath(private_keypath) + public_keypath = parsed_args.publickeypath.strip() + public_keypath = os.path.abspath(public_keypath) + + if not os.path.isfile(private_keypath): + raise(CommandError(u._('Private key file not found: {path}') + .format(path=private_keypath))) + if not os.path.isfile(public_keypath): + raise(CommandError(u._('Public key file not found: {path}') + .format(path=public_keypath))) + + with open(private_keypath, 'r') as f: + private_key = f.read() + with open(public_keypath, 'r') as f: + public_key = f.read() + CLIENT.password_set_sshkey(password_name, private_key.strip(), + public_key.strip()) + + except Exception: + raise Exception(traceback.format_exc()) + + class PasswordClear(Command): "Password Clear" diff --git a/kollacli/common/passwords.py b/kollacli/common/passwords.py index 2088bf2..92e0d79 100644 --- a/kollacli/common/passwords.py +++ b/kollacli/common/passwords.py @@ -35,6 +35,16 @@ def set_password(pwd_key, pwd_value): .format(error=err_msg, message=output)) +def set_password_sshkey(pwd_key, private_key, public_key): + cmd = '%s -k %s -r "%s" -u "%s"' % (_get_cmd_prefix(), pwd_key, + private_key, public_key) + err_msg, output = utils.run_cmd(cmd, print_output=False) + if err_msg: + raise FailedOperation( + u._('Password ssh key set failed. {error} {message}') + .format(error=err_msg, message=output)) + + def clear_password(pwd_key): """clear a password diff --git a/kollacli/common/utils.py b/kollacli/common/utils.py index bc442fc..4c157ab 100644 --- a/kollacli/common/utils.py +++ b/kollacli/common/utils.py @@ -21,6 +21,7 @@ import six import subprocess # nosec import sys import time +import yaml import kollacli.i18n as u @@ -196,6 +197,42 @@ def run_cmd(cmd, print_output=True): return err, output +def change_password(file_path, pname, pvalue=None, public_key=None, + private_key=None, clear=False): + """change password in passwords.yml file + + file_path: path to passwords file + pname: name of password + pvalue: value of password when not ssh key + public_key: public ssh key + private_key: private ssh key + clear: flag to remove 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. + + The passwords file contains both key-value pairs and key-dictionary + pairs. + """ + read_data = sync_read_file(file_path) + file_pwds = yaml.safe_load(read_data) + if clear: + # clear + if pname in file_pwds: + del file_pwds[pname] + else: + # edit + if pvalue: + file_pwds[pname] = pvalue + elif private_key: + file_pwds[pname] = {'private_key': private_key, + 'public_key': public_key} + write_data = yaml.safe_dump(file_pwds, default_flow_style=False) + sync_write_file(file_path, write_data) + + def change_property(file_path, property_dict, clear=False): """change property with a file @@ -375,7 +412,8 @@ def convert_list_to_string(alist): return '[' + ','.join(alist) + ']' -def check_arg(param, param_name, expected_type, none_ok=False, empty_ok=False): +def check_arg(param, param_name, expected_type, none_ok=False, empty_ok=False, + display_param=True): if param is None: if none_ok: return @@ -395,9 +433,14 @@ def check_arg(param, param_name, expected_type, none_ok=False, empty_ok=False): if not isinstance(param, expected_type): # wrong type - raise InvalidArgument(u._('{name} ({param}) is not a {type}') - .format(name=param_name, param=param, - type=expected_type)) + if display_param: + raise InvalidArgument(u._('{name} ({param}) is not a {type}') + .format(name=param_name, param=param, + type=expected_type)) + else: + raise InvalidArgument(u._('{name} is not a {type}') + .format(name=param_name, + type=expected_type)) class Lock(object): diff --git a/setup.cfg b/setup.cfg index 44fce64..26c828d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -48,6 +48,7 @@ kolla.cli = password_clear = kollacli.commands.password:PasswordClear password_list = kollacli.commands.password:PasswordList password_set = kollacli.commands.password:PasswordSet + password_setkey = kollacli.commands.password:PasswordSetKey property_clear = kollacli.commands.property:PropertyClear property_list = kollacli.commands.property:PropertyList property_set = kollacli.commands.property:PropertySet diff --git a/tests/password.py b/tests/password.py index 4f67024..9fb392c 100644 --- a/tests/password.py +++ b/tests/password.py @@ -16,8 +16,40 @@ from common import KollaCliTest import os import unittest +from kollacli.api import client from kollacli.common.utils import get_kolla_etc +CLIENT = client.ClientApi() + +PUBLIC_KEY = ( + 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCqusDp5jpkbng3sRue8gZV6/PCQp9' + 'ogUd5/OZ3sh9VgdaigHoYUfXZElTZLlkL71tD9WZJr69PDwmG/nE4quba8rLcDY2wC0' + 'qjq+r06ExhlRu4ivy7OxT29s8FSe8Uht9Pz8ahnXxddLF55yTbC81XrSXDBFc6Nnogz' + '+g6GgXVKtwTkm5g3K+qix5zVECu8zzawBR/s+v0dkDxKwSY8XOG6JZlMUndaDaikZZi' + 'qp8KAOJpajM77aCfDkY3VZGFBCJiEGLVDhFrtXuBI9I0YzX4j9pZZWpSzkM/FwlPjDR' + 'SW1C9MAAFLoEQTN4j1Z5hkDNXDsr49wJBi+jjQ0FPMMvfJktrznRuO2fUa9W2iilOrv' + '1PyrknssmW1iYXiWJ5Bq8A9sKE1r7Nbdjhcjskp77X57tNjtarRUcj3FqGjC8pv+k92' + '9Y+FvkXbjpBsHpdMFh8BlM+EnwnsjkiQpmjLv8bpeeQooLyQQmZn94zY73bbGrsjzXe' + 'OhOTDnKAS14hxCnBlEbudHB4erp/5Nj+A8UVAT0KXPM+mkDrum/dsvV0wnvBicAVt/a' + 'tmkwDKJqXDmj4elNe8/jTXSYHpTDo29xtcGpka9AtWarmnt8QkRuieD1xSXsEUQswjq' + 'aQD2ikitKt/hEyCmT+7fy4yYKK35kukUj5qV85A8O/hOYf5vFjtRw==') + +PRIVATE_KEY = ( + '-----BEGIN RSA PRIVATE KEY-----\n' + 'MIIJKAIBAAKCAgEAqrrA6eY6ZG54N7EbnvIGVevzwkKfaIFHefzmd7IfVYHWooB6\n' + 'GFH12RJU2S5ZC+9bQ/VmSa+vTw8Jhv5xOKrm2vKy3A2NsAtKo6vq9OhMYZUbuIr8\n' + 'uzsU9vbPBUnvFIbfT8/GoZ18XXSxeeck2wvNV60lwwRXOjZ6IPoOhoF1SrcE5JuY\n' + 'Nyvqosec1RArvM82sAUf7Pr9HZA8SsEmPFzhuiWZTFJ3Wg2opGWYqqfCgDiaWozO\n' + '+2gnw5GN1WRhQQiYhBi1Q4Ra7V7gSPSNGM1+I/aWWVqUs5DPxcJT4w0UltQvTAAB\n' + 'S6BEEzeI9WeYZAzVw7K+PcCQYvo40NBTzDL3yZLa850bjtn1GvVtoopTq79T8q5J\n' + '7LJltYmF4lieQavAPbChNa+zW3Y4XI7JKe+1+e7TY7Wq0VHI9xahowvKb/pPdvWP\n' + 'hb5F246QbB6XTBYfAZTPhJ8J7I5IkKZoy7/G6XnkKKC8kEJmZ/eM2O922xq7I813\n' + 'joTkw5ygEteIcQpwZRG7nRweHq6f+TY/gPFFQE9ClzzPppA67pv3bL1dMJ7wYnAF\n' + '69VedCYMSoYIHpcN80w9it/6Cfm8niAy3v9e0icSVEsvkzcV6eFjLggY1DQ9WBPN\n' + 'MR4LKGNDuxEWeZAQi+A6Ejclx1KKBhL/E4SNj3ev4/5glaMjzSIUpA4415o=\n' + '-----END RSA PRIVATE KEY-----' + ) + class TestFunctional(KollaCliTest): @@ -66,6 +98,15 @@ class TestFunctional(KollaCliTest): '(%s/%s) not in output: %s' % (key, value, msg)) + # test setting/clearing an ssh key + key = 'TeStKeY' + 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 ' + diff --git a/tools/kolla_actions.py b/tools/kolla_actions.py index b0803f0..0135a13 100755 --- a/tools/kolla_actions.py +++ b/tools/kolla_actions.py @@ -18,7 +18,7 @@ import signal import sys import yaml -from kollacli.common.utils import change_property +from kollacli.common.utils import change_password def _get_empty_keys(path): @@ -64,17 +64,21 @@ def _password_cmd(argv): """password command args for password command: - -p path # path to passwords.yaml - -k key # key of password - -v value # value of password - -c # flag to clear the password - -l # print to stdout a csv string of the existing keys - -e # get keys of passwords with empty values + -p path # path to passwords.yaml + -k key # key of password + -v value # value of password (if not ssh keys) + -r private key value # ssh private key + -u public key value # ssh public key + -c # flag to clear the password + -l # print to stdout a csv string of the existing keys + -e # get keys of passwords with empty values """ - opts, _ = getopt.getopt(argv[2:], 'p:k:v:cle') + opts, _ = getopt.getopt(argv[2:], 'p:k:v:r:u:cle') path = '' pwd_key = '' pwd_value = '' + pwd_ssh_private = '' + pwd_ssh_public = '' clear_flag = False list_flag = False empty_flag = False @@ -85,6 +89,10 @@ def _password_cmd(argv): pwd_key = arg elif opt == '-v': pwd_value = arg + elif opt == '-r': + pwd_ssh_private = arg.replace('"', '') + elif opt == '-u': + pwd_ssh_public = arg.replace('"', '') elif opt == '-c': clear_flag = True elif opt == '-l': @@ -98,10 +106,10 @@ def _password_cmd(argv): # get empty passwords _get_empty_keys(path) else: - # edit a password - property_dict = {} - property_dict[pwd_key] = pwd_value - change_property(path, property_dict, clear_flag) + # edit/clear a password + change_password(path, pwd_key, pvalue=pwd_value, + private_key=pwd_ssh_private, + public_key=pwd_ssh_public, clear=clear_flag) def _job_cmd(argv):