summaryrefslogtreecommitdiff
path: root/ansible/library/sshknownhosts
blob: b3f5cef27e74a226ba417dbfeb1ece8c237823d5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
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
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>

main()