From bd80ed9a1690accd8d5dcb964ce387a27f6b014b Mon Sep 17 00:00:00 2001 From: Sam Thursfield Date: Mon, 17 Nov 2014 12:12:25 +0000 Subject: Avoid scanning SSH host key of UPSTREAM_TROVE unless necessary I've seen trove-setup.service fail in the case that the Trove is behind a proxy that blocks SSH traffic due to `ssh-keyscan` returning an error. We only need to scan the SSH host key of UPSTREAM_TROVE if we're going to be using SSH to acess it, so we can work around this issue. This pulls in the 'sshknownhosts' Ansible module from commit 80f31fa8a5d3751b4e3e19123164c3ea97f262dd of . The module seems small enough that we don't need to mess with a submodules to include it. --- ansible/library/sshknownhosts | 228 +++++++++++++++++++++ .../roles/trove-setup/tasks/known-hosts-setup.yml | 19 +- 2 files changed, 240 insertions(+), 7 deletions(-) create mode 100644 ansible/library/sshknownhosts diff --git a/ansible/library/sshknownhosts b/ansible/library/sshknownhosts new file mode 100644 index 0000000..b3f5cef --- /dev/null +++ b/ansible/library/sshknownhosts @@ -0,0 +1,228 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + + +import re +import os +import string + +DOCUMENTATION = """ +--- +module: sshknownhosts +short_description: Maintain the ssh_known_hosts file by adding/ + removing/ updating public keys. +description: + - This module will scan a host for its ssh key and add it to the ssh + known hosts file. Typically this file is located at + /etc/ssh/ssh_known_hosts or ~user/.ssh/known_hosts. + - If the public key is already present in the known hosts file and + it does not match the current value, it is updated. Otherwise + the hosts file is untouched. + - This is an alternative to copying a file to each host using the + copy command. + +options: + host: + required: true + aliases: [name] + description: + - the hostname to scan. Use a fully-qualified domain name if + possible. If used with state=absent, this specifies the + hostname (not the alias) of the key to remove from the dest file. + dest: + required: false + default: /etc/ssh/ssh_known_hosts + description: + - Full path of the file to modify. This will be created if it + does not exist. + state: + required: false + choices: [present, absent] + default: "present" + description: + - Whether the host should be there or not. + enctype: + required: false + choices: [ecdsa, rsa, dsa] + default: "rsa" + description: + - The type of public key to scan for. + port: + required: false + default: "22" + description: + - sshd server port, if it runs on a non-standard port. + keyscan: + required: false + default: "ssh-keyscan" + description: + - The full path to the program to run to do the scan. If not + specified, the module will run the ssh-keyscan program from + the path. + aliases: + required: false + default: "" + description: + - One or more aliases for this host in a comma-separated list. These + are other names a host is known by. +""" + +EXAMPLES = r""" +Examples: + + - name: Add localhost to ssh_known_hosts file + action: sshknownhosts host=localhost state=present + + - name: Add several hosts to ssh_known_hosts file + action: sshknownhosts host={{ item }} state=present port=2222 + with_items: + - host1.example.com + - host2.example.com + - host3.example.com + + - name: a long example + action: sshknownhosts host=abc.example.com dest=/usr/local/etc/ssh_known_hosts keyscan=/usr/local/bin/ssh-keyscan enctype=dsa port=2222 + + - name: for one user id + action: sshknownhosts host=mypc dest=~myself/.ssh/knownhosts + + - name: aliases example + action: sshknownhosts host=myserver aliases=mygitserver state=present +""" + + +# read a text file. +# return the lines as an array. if the file is not found, return an +# empty array +def read_known_hosts(dest): + if os.path.exists(dest): + f = open(dest, 'rb') + lines = f.readlines() + f.close() + else: + lines = [] + return lines + + +# locate a host in the known hosts array. +# return the position if found, or -1 if not found +def find_host(lines, host): + # look for the hostname at the beginning of a line, followed by a + # space character or a comma + mre = re.compile(r"^" + host + '[ ,]') + + found = -1 + for lineno, cur_line in enumerate(lines): + if mre.search(cur_line): + found = lineno + return found + + +# write the new/changed file. this is the only place where system +# changes are performed +def write_known_hosts(module, dest, lines): + if not module.check_mode: + of = open(dest, 'wb') + of.writelines(lines) + of.close + + +# scan the remote host. +# return an array: [rc, str] +# rc = return code: 0 = success, -1 = error +# str = public key or error message +def scan_key(module, host, keyscan, enctype, port): + cmd = keyscan + ' -p ' + port + ' -t ' + enctype + ' ' + host + (rc, out, err) = module.run_command(cmd) + + # look for a non-blank string + mre = re.compile(r"^\s*$") + if not mre.search(out): + return [0, out] + else: + return [-1, err.strip()] + + +# add aliases into knownhosts line. +# key = the line returned by keyscan +# aliases = the list of comma-separated aliases, or '' +def add_aliases(key, aliases): + if aliases == '': + return key + parts = string.split(key, ' ', 1) # split at first space (into 2 parts) + return parts[0] + ',' + aliases + ' ' + parts[1] + + +def present(module, dest, host, keyscan, enctype, port, aliases): + changed = False + msg = "" + lines = read_known_hosts(dest) + found = find_host(lines, host) + rc, key = scan_key(module, host, keyscan, enctype, port) + + if rc == 0: + key = add_aliases(key, aliases) + if found != -1: + if key != lines[found]: + # replace + del lines[found] + lines.append(key) + write_known_hosts(module, dest, lines) + changed = True + else: + # add + lines.append(key) + write_known_hosts(module, dest, lines) + changed = True + else: + # error: return the error message to the user + msg = key + module.exit_json(changed=changed, msg=msg) + + +def absent(module, dest, host): + changed = False + msg = "" + lines = read_known_hosts(dest) + found = find_host(lines, host) + + if found != -1: + del lines[found] + write_known_hosts(module, dest, lines) + changed = True + module.exit_json(changed=changed, msg=msg) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + host=dict(required=True, aliases=['name']), + dest=dict(default='/etc/ssh/ssh_known_hosts'), + enctype=dict(default='rsa', choices=['ecdsa', 'rsa', 'dsa']), + keyscan=dict(default='ssh-keyscan'), + port=dict(default='22'), + state=dict(default='present', choices=['absent', 'present']), + aliases=dict(default=''), + ), + supports_check_mode=True + ) + + host = module.params['host'] + keyscan = module.params['keyscan'] + port = module.params['port'] + enctype = module.params['enctype'] + dest = os.path.expanduser(module.params['dest']) + aliases = module.params['aliases'] + + if 'host' not in module.params: + module.fail_json(msg='host= is required') + + if module.params['state'] == 'present': + present(module, dest, host, keyscan, enctype, port, aliases) + else: + absent(module, dest, host) + +# this is magic, see lib/ansible/module_common.py +#<> + +main() diff --git a/ansible/roles/trove-setup/tasks/known-hosts-setup.yml b/ansible/roles/trove-setup/tasks/known-hosts-setup.yml index a8d18f7..b3e268a 100644 --- a/ansible/roles/trove-setup/tasks/known-hosts-setup.yml +++ b/ansible/roles/trove-setup/tasks/known-hosts-setup.yml @@ -1,10 +1,15 @@ # Depends on: # - check.yml +# Uses sshknownhosts module from: +# https://github.com/bfmartin/ansible-sshknownhosts --- -# If UPSTREAM_TROVE is not defined, the jinja2 'default' filter will -# replace it with an empty string. ssh-keyscan skips empty hostnames, -# so it will run only with 'localhost' -- name: Add localhost and UPSTREAM_TROVE to /etc/ssh/ssh_known_hosts - shell: | - ssh-keyscan localhost {{ UPSTREAM_TROVE|default('')|quote }} > /etc/ssh/ssh_known_hosts - creates=/etc/ssh/ssh_known_hosts +- name: Add localhost to /etc/ssh/ssh_known_hosts + sshknownhosts: host=localhost state=present + +# We avoid trying to scan UPSTREAM_TROVE's host keys unless the user explicitly +# configured Lorry Controller to access it over SSH. It's possible that the +# Trove is behind a proxy that blocks SSH traffic, and if this step fails then +# the whole trove-setup.service will fail. +- name: Add {{ UPSTREAM_TROVE }} to /etc/ssh/ssh_known_hosts + sshknownhosts: host={{ UPSTREAM_TROVE }} state=present + when: UPSTREAM_TROVE_PROTOCOL == ssh -- cgit v1.2.1