From cc2ed9fa5f181e07efc4f59224d915b77e0da6bf Mon Sep 17 00:00:00 2001
From: Clint Byrum <clint@fewbar.com>
Date: Thu, 17 Aug 2017 13:45:11 -0700
Subject: [PATCH] Add known_hosts generation for multiple nodes

Things will need to be done a bit differently for localhost-ssh, so this
is just for the case of multiple nodes needing to be able to SSH to
eachother.

Change-Id: I941cf3de7691ee1b5277ca50c7bb9daa5b9a0732
---
 playbooks/base/pre.yaml                       |  1 +
 .../library/generate_all_known_hosts.py       | 87 +++++++++++++++++++
 roles/multi-node-known-hosts/tasks/main.yaml  |  3 +
 .../tasks/setup-multinode-known-hosts.yaml    | 10 +++
 4 files changed, 101 insertions(+)
 create mode 100644 roles/multi-node-known-hosts/library/generate_all_known_hosts.py
 create mode 100644 roles/multi-node-known-hosts/tasks/main.yaml
 create mode 100644 roles/multi-node-known-hosts/tasks/setup-multinode-known-hosts.yaml

diff --git a/playbooks/base/pre.yaml b/playbooks/base/pre.yaml
index 320c179df..d0b31210a 100644
--- a/playbooks/base/pre.yaml
+++ b/playbooks/base/pre.yaml
@@ -1,6 +1,7 @@
 - hosts: all
   roles:
     - add-build-sshkey
+    - multi-node-known-hosts
     - prepare-workspace
     - role: validate-host
       # TODO(mordred) When we have site-local variables, these should go there
diff --git a/roles/multi-node-known-hosts/library/generate_all_known_hosts.py b/roles/multi-node-known-hosts/library/generate_all_known_hosts.py
new file mode 100644
index 000000000..98106925f
--- /dev/null
+++ b/roles/multi-node-known-hosts/library/generate_all_known_hosts.py
@@ -0,0 +1,87 @@
+#!/usr/bin/python
+
+# Copyright (c) 2017 Red Hat
+#
+# This module is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This software is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this software.  If not, see <http://www.gnu.org/licenses/>.
+
+
+def main():
+    module = AnsibleModule(
+        argument_spec=dict(
+            hostvars=dict(required=True, type='dict'),
+            hosts=dict(required=False, type='list'),
+        )
+    )
+
+    hostvars = module.params['hostvars']
+    hosts = module.params['hosts']
+    if not hosts:
+        hosts = hostvars.keys()
+
+    known_hosts = set()
+
+    for host in hosts:
+        this = hostvars[host]
+
+        public_keys = [x for x in this.keys() if
+                       x.startswith('ansible_ssh_host_key')]
+
+        for iface in this.get('ansible_interfaces', []):
+            iface_key = 'ansible_{}'.format(iface.replace('-', '_'))
+            ipv4 = this[iface_key].get('ipv4')
+            if not isinstance(ipv4, list):
+                ipv4 = [ipv4]
+            ipv6 = this[iface_key].get('ipv6')
+            if not isinstance(ipv6, list):
+                ipv6 = [ipv6]
+            addresses = [x['address'] for x in ipv4 if x and not
+                         x['address'].startswith('127.')]
+            addresses += [y['address'] for y in ipv6 if y and not
+                          y['scope'] == 'host']
+            addresses += [this['ansible_hostname']]
+            for addr in addresses:
+                for key in public_keys:
+                    if key.endswith('rsa_public'):
+                        key_type = 'ssh-rsa'
+                    elif key.endswith('ecdsa_public'):
+                        # XXX This will not work with > 256 bit ecdsa keys
+                        # until https://github.com/ansible/ansible/issues/28325
+                        # is fixed. We're using the proposed scheme in case it
+                        # does merge as-is, but it may need to be updated if
+                        # the patch is changed as well.
+                        key_type = this.get('{}_type'.format(key),
+                                            'ecdsa-sha2-nistp256')
+                    elif key.endswith('ed25519_public'):
+                        key_type = 'ssh-ed25519'
+                    else:
+                        continue
+                    known_hosts.add(
+                        '{addr} {key_type} {key}'.format(addr=addr,
+                                                         key_type=key_type,
+                                                         key=this[key]))
+
+    ret = {
+        'ansible_facts': {
+            'all_known_hosts': [dict(name=x.split()[0], key=x) for x in
+                                sorted(known_hosts)]
+        }
+    }
+
+    module.exit_json(changed=False, _zuul_nolog_return=True, **ret)
+
+from ansible.module_utils.basic import *  # noqa
+from ansible.module_utils.basic import AnsibleModule
+
+if __name__ == '__main__':
+    main()
diff --git a/roles/multi-node-known-hosts/tasks/main.yaml b/roles/multi-node-known-hosts/tasks/main.yaml
new file mode 100644
index 000000000..18763df7c
--- /dev/null
+++ b/roles/multi-node-known-hosts/tasks/main.yaml
@@ -0,0 +1,3 @@
+- name: Setup known_hosts for cross-node SSH'ing when we have more than 1 host
+  include: setup-multinode-known-hosts.yaml
+  when: hostvars|length > 1
diff --git a/roles/multi-node-known-hosts/tasks/setup-multinode-known-hosts.yaml b/roles/multi-node-known-hosts/tasks/setup-multinode-known-hosts.yaml
new file mode 100644
index 000000000..ad41f6230
--- /dev/null
+++ b/roles/multi-node-known-hosts/tasks/setup-multinode-known-hosts.yaml
@@ -0,0 +1,10 @@
+- name: Get known_hosts facts
+  generate_all_known_hosts:
+    hostvars: "{{ hostvars }}"
+
+- name: add known_host record for every public key of every other ip, hostname
+  known_hosts:
+    name: "{{ item.name }}"
+    key: "{{ item.key }}"
+  with_items:
+    "{{ all_known_hosts }}"