From 7ea11eada552627b5385a6a347f23fccee484e67 Mon Sep 17 00:00:00 2001 From: spisarski Date: Mon, 22 Jan 2018 19:27:31 -0700 Subject: Added password support for SSH and Ansible Additional protections when initializing network resources Enhanced playbook runner variable support Change-Id: Id897c4ece1de706afbc52d0a034ca3bfb68fdce3 Signed-off-by: spisarski --- requirements.txt | 2 +- snaps/openstack/create_instance.py | 40 +++++++++++++++------ snaps/openstack/create_network.py | 13 ++++--- snaps/openstack/create_router.py | 10 ++++-- snaps/openstack/utils/launch_utils.py | 2 +- snaps/openstack/utils/nova_utils.py | 7 ++-- snaps/playbook_runner.py | 45 ++++++++++++++++++------ snaps/provisioning/ansible_utils.py | 46 ++++++++++++++++++------- snaps/provisioning/tests/ansible_utils_tests.py | 15 ++++---- 9 files changed, 127 insertions(+), 53 deletions(-) diff --git a/requirements.txt b/requirements.txt index 137159f..f992fb4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ # The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. -python-novaclient>=9.0.0 # Apache-2.0 +python-novaclient>=9.0.0,<=10 # Apache-2.0 python-neutronclient>=6.3.0 # Apache-2.0 python-keystoneclient>=3.8.0 # Apache-2.0 python-glanceclient>=2.8.0 # Apache-2.0 diff --git a/snaps/openstack/create_instance.py b/snaps/openstack/create_instance.py index d91e360..631ac6b 100644 --- a/snaps/openstack/create_instance.py +++ b/snaps/openstack/create_instance.py @@ -380,7 +380,7 @@ class OpenStackVmInstance(OpenStackComputeObject): logger.error('Cannot add floating IP [%s]', bre) raise except Exception as e: - logger.debug( + logger.warn( 'Retry adding floating IP to instance. Last attempt ' 'failed with - %s', e) time.sleep(poll_interval) @@ -509,8 +509,9 @@ class OpenStackVmInstance(OpenStackComputeObject): """ return ansible_utils.apply_playbook( pb_file_loc, [self.get_floating_ip(fip_name=fip_name).ip], - self.get_image_user(), self.keypair_settings.private_filepath, - variables, self._os_creds.proxy_settings) + self.get_image_user(), + ssh_priv_key_file_path=self.keypair_settings.private_filepath, + variables=variables, proxy_setting=self._os_creds.proxy_settings) def get_image_user(self): """ @@ -619,7 +620,8 @@ class OpenStackVmInstance(OpenStackComputeObject): status) return status == expected_status_code - def vm_ssh_active(self, block=False, poll_interval=POLL_INTERVAL): + def vm_ssh_active(self, user_override=None, password=None, block=False, + timeout=None, poll_interval=POLL_INTERVAL): """ Returns true when the VM can be accessed via SSH :param block: When true, thread will block until active or timeout @@ -630,7 +632,8 @@ class OpenStackVmInstance(OpenStackComputeObject): # sleep and wait for VM status change logger.info('Checking if VM is active') - timeout = self.instance_settings.ssh_connect_timeout + if not timeout: + timeout = self.instance_settings.ssh_connect_timeout if self.vm_active(block=True): if block: @@ -639,7 +642,8 @@ class OpenStackVmInstance(OpenStackComputeObject): start = time.time() - timeout while timeout > time.time() - start: - status = self.__ssh_active() + status = self.__ssh_active( + user_override=user_override, password=password) if status: logger.info('SSH is active for VM instance') return True @@ -653,13 +657,14 @@ class OpenStackVmInstance(OpenStackComputeObject): logger.error('Timeout attempting to connect with VM via SSH') return False - def __ssh_active(self): + def __ssh_active(self, user_override=None, password=None): """ Returns True when can create a SSH session else False :return: T/F """ if len(self.__floating_ip_dict) > 0: - ssh = self.ssh_client() + ssh = self.ssh_client( + user_override=user_override, password=password) if ssh: ssh.close() return True @@ -727,19 +732,32 @@ class OpenStackVmInstance(OpenStackComputeObject): else: return self.__get_first_provisioning_floating_ip() - def ssh_client(self, fip_name=None): + def ssh_client(self, fip_name=None, user_override=None, password=None): """ Returns an SSH client using the name or the first known floating IP if exists, else None :param fip_name: the name of the floating IP to return + :param user_override: the username to use instead of the default + :param password: the password to use instead of the private key :return: the SSH client or None """ fip = self.get_floating_ip(fip_name) + + ansible_user = self.get_image_user() + if user_override: + ansible_user = user_override + + if password: + private_key = None + else: + private_key = self.keypair_settings.private_filepath + if fip: return ansible_utils.ssh_client( self.__get_first_provisioning_floating_ip().ip, - self.get_image_user(), - self.keypair_settings.private_filepath, + ansible_user, + private_key_filepath=private_key, + password=password, proxy_settings=self._os_creds.proxy_settings) else: FloatingIPAllocationError( diff --git a/snaps/openstack/create_network.py b/snaps/openstack/create_network.py index c9c58e8..984eedd 100644 --- a/snaps/openstack/create_network.py +++ b/snaps/openstack/create_network.py @@ -15,7 +15,7 @@ import logging import enum -from neutronclient.common.exceptions import NetworkNotFoundClient +from neutronclient.common.exceptions import NetworkNotFoundClient, Unauthorized from snaps.config.network import NetworkConfig, SubnetConfig, PortConfig from snaps.openstack.openstack_creator import OpenStackNetworkObject @@ -51,9 +51,14 @@ class OpenStackNetwork(OpenStackNetworkObject): """ super(self.__class__, self).initialize() - self.__network = neutron_utils.get_network( - self._neutron, network_settings=self.network_settings, - project_id=self.network_settings.get_project_id(self._os_creds)) + try: + self.__network = neutron_utils.get_network( + self._neutron, network_settings=self.network_settings, + project_id=self.network_settings.get_project_id( + self._os_creds)) + except Unauthorized as e: + logger.warn('Unable to lookup network with name %s - %s', + self.network_settings.name, e) return self.__network diff --git a/snaps/openstack/create_router.py b/snaps/openstack/create_router.py index 4f95c3b..c9ccdd6 100644 --- a/snaps/openstack/create_router.py +++ b/snaps/openstack/create_router.py @@ -14,7 +14,7 @@ # limitations under the License. import logging -from neutronclient.common.exceptions import NotFound +from neutronclient.common.exceptions import NotFound, Unauthorized from snaps.config.router import RouterConfig from snaps.openstack.openstack_creator import OpenStackNetworkObject @@ -61,8 +61,12 @@ class OpenStackRouter(OpenStackNetworkObject): """ super(self.__class__, self).initialize() - self.__router = neutron_utils.get_router( - self._neutron, router_settings=self.router_settings) + try: + self.__router = neutron_utils.get_router( + self._neutron, router_settings=self.router_settings) + except Unauthorized as e: + logger.warn('Unable to lookup router with name %s - %s', + self.router_settings.name, e) if self.__router: for internal_subnet_name in self.router_settings.internal_subnets: diff --git a/snaps/openstack/utils/launch_utils.py b/snaps/openstack/utils/launch_utils.py index e10cf48..05d4cb5 100644 --- a/snaps/openstack/utils/launch_utils.py +++ b/snaps/openstack/utils/launch_utils.py @@ -462,7 +462,7 @@ def __apply_ansible_playbook(ansible_config, os_creds, vm_dict, image_dict, retval = ansible_utils.apply_playbook( ansible_config['playbook_location'], floating_ips, remote_user, - private_key_filepath, + ssh_priv_key_file_path=private_key_filepath, variables=variables, proxy_setting=proxy_settings) if retval != 0: diff --git a/snaps/openstack/utils/nova_utils.py b/snaps/openstack/utils/nova_utils.py index e15484c..279e2ec 100644 --- a/snaps/openstack/utils/nova_utils.py +++ b/snaps/openstack/utils/nova_utils.py @@ -69,8 +69,11 @@ def create_server(nova, neutron, glance, instance_config, image_config, ports = list() for port_setting in instance_config.port_settings: - ports.append(neutron_utils.get_port( - neutron, port_settings=port_setting)) + port = neutron_utils.get_port(neutron, port_settings=port_setting) + if port: + ports.append(port) + else: + raise Exception('Cannot find port named - ' + port_setting.name) nics = [] for port in ports: kv = dict() diff --git a/snaps/playbook_runner.py b/snaps/playbook_runner.py index 87321f5..03b7006 100644 --- a/snaps/playbook_runner.py +++ b/snaps/playbook_runner.py @@ -15,9 +15,13 @@ import argparse import ast import logging +import os import re +import yaml +from jinja2 import Environment, FileSystemLoader + from snaps.openstack.os_credentials import ProxySettings from snaps.provisioning import ansible_utils @@ -41,20 +45,32 @@ def main(parsed_args): ssh_proxy_cmd=parsed_args.ssh_proxy_cmd) # Ensure can get an SSH client - ssh = ansible_utils.ssh_client(parsed_args.ip_addr, parsed_args.host_user, - parsed_args.priv_key, proxy_settings) + ssh = ansible_utils.ssh_client( + parsed_args.ip_addr, parsed_args.host_user, + private_key_filepath=parsed_args.priv_key, + proxy_settings=proxy_settings) if ssh: ssh.close() - vars = dict() - if args.vars: - vars = ast.literal_eval(args.vars) - if not isinstance(vars, dict): - vars = dict() + env = Environment(loader=FileSystemLoader( + searchpath=os.path.dirname(parsed_args.env_file))) + template = env.get_template(os.path.basename(parsed_args.env_file)) + + env_dict = dict() + if parsed_args.vars: + env_dict = ast.literal_eval(parsed_args.vars) + + output = template.render(**env_dict) + + variables = yaml.load(output) + + if not variables.get('env_file'): + variables['env_file'] = parsed_args.env_file retval = ansible_utils.apply_playbook( parsed_args.playbook, [parsed_args.ip_addr], parsed_args.host_user, - parsed_args.priv_key, variables=vars, + ssh_priv_key_file_path=parsed_args.priv_key, + password=parsed_args.password, variables=variables, proxy_setting=proxy_settings) exit(retval) @@ -63,19 +79,26 @@ if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('-a', '--ip-addr', dest='ip_addr', required=True, help='The Host IP Address') - parser.add_argument('-k', '--priv-key', dest='priv_key', required=True, - help='The location of the private key file') parser.add_argument('-u', '--host-user', dest='host_user', required=True, help='Host user account') + parser.add_argument('-k', '--priv-key', dest='priv_key', required=False, + help='The location of the private key file') + parser.add_argument('-pw', '--password', dest='password', required=False, + help='The host-user password') parser.add_argument('-b', '--playbook', dest='playbook', required=True, help='Playbook Location') parser.add_argument('-p', '--http-proxy', dest='http_proxy', required=False, help=':') parser.add_argument('-s', '--ssh-proxy-cmd', dest='ssh_proxy_cmd', required=False) + parser.add_argument('-e', '--env-file', dest='env_file', + help='Yaml file containing playbook substitution vals', + required=False) parser.add_argument('-v', '--vars', dest='vars', + help='String renditon of a dict to pass into ' + 'playbook for additional subtitution values not ' + 'found in env_file', required=False) args = parser.parse_args() main(args) - diff --git a/snaps/provisioning/ansible_utils.py b/snaps/provisioning/ansible_utils.py index 63f26e1..83fe449 100644 --- a/snaps/provisioning/ansible_utils.py +++ b/snaps/provisioning/ansible_utils.py @@ -32,8 +32,9 @@ __author__ = 'spisarski' logger = logging.getLogger('ansible_utils') -def apply_playbook(playbook_path, hosts_inv, host_user, ssh_priv_key_file_path, - variables=None, proxy_setting=None): +def apply_playbook(playbook_path, hosts_inv, host_user, + ssh_priv_key_file_path=None, password=None, variables=None, + proxy_setting=None): """ Executes an Ansible playbook to the given host :param playbook_path: the (relative) path to the Ansible playbook @@ -41,7 +42,10 @@ def apply_playbook(playbook_path, hosts_inv, host_user, ssh_priv_key_file_path, Ansible playbook :param host_user: A user for the host instances (must be a password-less sudo user if playbook has "sudo: yes" - :param ssh_priv_key_file_path: the file location of the ssh key + :param ssh_priv_key_file_path: the file location of the ssh key. Required + if password is None + :param password: the file location of the ssh key. Required if + ssh_priv_key_file_path is None :param variables: a dictionary containing any substitution variables needed by the Jinga 2 templates :param proxy_setting: instance of os_credentials.ProxySettings class @@ -50,10 +54,20 @@ def apply_playbook(playbook_path, hosts_inv, host_user, ssh_priv_key_file_path, if not os.path.isfile(playbook_path): raise AnsibleException('Requested playbook not found - ' + playbook_path) - pk_file_path = os.path.expanduser(ssh_priv_key_file_path) - if not os.path.isfile(pk_file_path): - raise AnsibleException('Requested private SSH key not found - ' + - pk_file_path) + pk_file_path = None + if ssh_priv_key_file_path: + pk_file_path = os.path.expanduser(ssh_priv_key_file_path) + if not password: + if not os.path.isfile(pk_file_path): + raise AnsibleException('Requested private SSH key not found - ' + + pk_file_path) + + if not ssh_priv_key_file_path and not password: + raise AnsibleException('Invalid credentials, no priv key or password') + + passwords = None + if password: + passwords = {'conn_pass': password, 'become_pass': password} import ansible.constants ansible.constants.HOST_KEY_CHECKING = False @@ -93,18 +107,20 @@ def apply_playbook(playbook_path, hosts_inv, host_user, ssh_priv_key_file_path, variable_manager=variable_manager, loader=loader, options=ansible_opts, - passwords=None) + passwords=passwords) logger.debug('Executing Ansible Playbook - ' + playbook_path) return executor.run() -def ssh_client(ip, user, private_key_filepath, proxy_settings=None): +def ssh_client(ip, user, private_key_filepath=None, password=None, + proxy_settings=None): """ Retrieves and attemts an SSH connection :param ip: the IP of the host to connect :param user: the user with which to connect - :param private_key_filepath: the path to the private key file + :param private_key_filepath: when None, password is required + :param password: when None, private_key_filepath is required :param proxy_settings: instance of os_credentials.ProxySettings class (optional) :return: the SSH client if can connect else false @@ -120,9 +136,13 @@ def ssh_client(ip, user, private_key_filepath, proxy_settings=None): proxy_cmd_str = proxy_cmd_str.replace("%p", '22') proxy_cmd = paramiko.ProxyCommand(proxy_cmd_str) - pk_abs_path = os.path.expanduser(private_key_filepath) - ssh.connect(ip, username=user, key_filename=pk_abs_path, - sock=proxy_cmd) + pk_abs_path = None + if not password and private_key_filepath: + pk_abs_path = os.path.expanduser(private_key_filepath) + + ssh.connect( + ip, username=user, key_filename=pk_abs_path, password=password, + sock=proxy_cmd) return ssh except Exception as e: logger.warning('Unable to connect via SSH with message - ' + str(e)) diff --git a/snaps/provisioning/tests/ansible_utils_tests.py b/snaps/provisioning/tests/ansible_utils_tests.py index 7600002..851dd64 100644 --- a/snaps/provisioning/tests/ansible_utils_tests.py +++ b/snaps/provisioning/tests/ansible_utils_tests.py @@ -266,8 +266,9 @@ class AnsibleProvisioningTests(OSIntegrationTestCase): retval = self.inst_creator.apply_ansible_playbook(relative_pb_path) self.assertEqual(0, retval) - ssh = ansible_utils.ssh_client(ip, user, priv_key, - self.os_creds.proxy_settings) + ssh = ansible_utils.ssh_client( + ip, user, private_key_filepath=priv_key, + proxy_settings=self.os_creds.proxy_settings) self.assertIsNotNone(ssh) scp = None try: @@ -329,13 +330,13 @@ class AnsibleProvisioningTests(OSIntegrationTestCase): relative_pb_path = pkg_resources.resource_filename( 'snaps.provisioning.tests.playbooks', 'template_playbook.yml') - retval = self.inst_creator.apply_ansible_playbook(relative_pb_path, - variables={ - 'name': 'Foo'}) + retval = self.inst_creator.apply_ansible_playbook( + relative_pb_path, variables={'name': 'Foo'}) self.assertEqual(0, retval) - ssh = ansible_utils.ssh_client(ip, user, priv_key, - self.os_creds.proxy_settings) + ssh = ansible_utils.ssh_client( + ip, user, private_key_filepath=priv_key, + proxy_settings=self.os_creds.proxy_settings) self.assertIsNotNone(ssh) scp = None -- cgit 1.2.3-korg