Add seed code for ansible-role-ntp
[ta/ansible-role-ntp.git] / 0001-initial.patch
1 diff --git a/handlers/main.yml b/handlers/main.yml
2 index d18df56..6b58a86 100644
3 --- a/handlers/main.yml
4 +++ b/handlers/main.yml
5 @@ -1,3 +1,14 @@
6  ---
7 +- name: add ntp keys
8 +  ntp:
9 +    auth_type: "{{ time.auth_type }}"
10 +    ntpservers: "{{ ntp_config_server }}"
11 +    hosts: "{{ hosts }}"
12 +    filepath: "{{ time.serverkeys_path }}"
13 +
14 +- name: create redundant fallback ntp servers
15 +  fallback_ntp_servers:
16 +    hosts: "{{ hosts }}"
17 +
18  - name: restart ntp
19    service: name={{ ntp_service_name }} state=restarted
20 diff --git a/library/fallback_ntp_servers.py b/library/fallback_ntp_servers.py
21 new file mode 100644
22 index 0000000..7d7907e
23 --- /dev/null
24 +++ b/library/fallback_ntp_servers.py
25 @@ -0,0 +1,73 @@
26 +#!/bin/python
27 +# Copyright (C) 2017 Nokia
28 +# All rights reserved
29 +
30 +from ansible.module_utils.basic import AnsibleModule
31 +import socket
32 +
33 +DOCUMENTATION = '''
34 +module: fallback_ntp_servers
35 +short_description: adding peers and fallback option
36 +description: Adding peers and an optional fallback option to the controllers.
37 +author: nokia.com
38 +options:
39 +    option-name: hosts
40 +    description: a list of controller hostnames
41 +    type: list
42 +'''
43 +
44 +def get_hostname():
45 +    return socket.gethostname().split('.')[0]
46 +
47 +def get_controllers(hosts):
48 +    controllers = []
49 +    for item in hosts:
50 +        if "controller" in hosts[item]['service_profiles']:
51 +            controllers.append(item)
52 +    return controllers
53 +
54 +def is_installation_host(hosts):
55 +    hostname = get_hostname()
56 +    try:
57 +        if hosts[hostname]['installation_host']:
58 +            return True
59 +    except KeyError:
60 +        return False
61 +    return False
62 +
63 +def add_peers(hosts):
64 +    hosts.remove(get_hostname())
65 +    with open("/etc/ntp.conf") as cnf:
66 +        content = cnf.readlines()
67 +    for index, line in enumerate(content):
68 +        if "server" in line:
69 +            for peer in hosts:
70 +                content.insert(index, "peer  %s\n" % peer)
71 +            break
72 +    with open("/etc/ntp.conf", "w") as cnf:
73 +        for line in content:
74 +            cnf.write(line)
75 +
76 +def add_fallback(hosts):
77 +    if is_installation_host(hosts):
78 +        with open("/etc/ntp.conf") as cnf:
79 +            content = cnf.readlines()
80 +        for index, line in enumerate(content):
81 +            if "peer" in line:
82 +                content.insert(index, "fudge  127.127.1.0  stratum  10\n")
83 +                break
84 +        with open("/etc/ntp.conf", "w") as cnf:
85 +            for line in content:
86 +                cnf.write(line)
87 +
88 +def main():
89 +    module_args = dict(hosts=dict(type='dict', required=True))
90 +    module = AnsibleModule(argument_spec=module_args)
91 +    controllers = get_controllers(module.params['hosts'])
92 +    if get_hostname() in controllers:
93 +        add_peers(controllers)
94 +        add_fallback(module.params['hosts'])
95 +    module.exit_json(msg="configured")
96 +
97 +if __name__ == "__main__":
98 +    main()
99 diff --git a/library/ntp.py b/library/ntp.py
100 new file mode 100644
101 index 0000000..cef833e
102 --- /dev/null
103 +++ b/library/ntp.py
104 @@ -0,0 +1,476 @@
105 +#!/bin/python
106 +# Copyright (C) 2018 Nokia
107 +# All rights reserved
108 +
109 +import re
110 +import os
111 +from subprocess import check_output, CalledProcessError
112 +from ansible.module_utils.basic import AnsibleModule
113 +import socket
114 +import yaml
115 +import random
116 +import string
117 +import requests
118 +from urlparse import urlparse
119 +
120 +DOCUMENTATION = '''
121 +module: ntp
122 +short_description: configuring authentication
123 +description: Configuring the authentication of NTP servers.
124 +author: nokia.com
125 +options:
126 +    option-name: auth_type
127 +        description: the authentication type used by the server
128 +        type: string
129 +        choices: crypto, symmetric, none
130 +        default: crypto
131 +    option-name: hosts
132 +        description: a list of the controllers, in order to decide if the script is executed on controller or compute host
133 +        type: list
134 +    option-name: ntpservers
135 +        description: a list the NTP servers
136 +        type: list
137 +    option-name: filepath
138 +        description: the url of the required keys
139 +        type: string
140 +        default: empty string
141 +'''
142 +
143 +def get_hostname():
144 +    return socket.gethostname().split('.')[0]
145 +
146 +def get_controllers(hosts):
147 +    controllers = []
148 +    for item in hosts:
149 +        if "controller" in hosts[item]['service_profiles']:
150 +            controllers.append(item)
151 +    return controllers
152 +
153 +crypto_keys_dir = "/etc/ntp/crypto"
154 +crypto_parameter_file = crypto_keys_dir + "/params"
155 +symmetric_key_file = "/etc/ntp/keys"
156 +
157 +class KeyAuthDisabled(Exception):
158 +    pass
159 +
160 +class SymmetricKeyNotFound(Exception):
161 +    pass
162 +
163 +class NtpCryptoKeyHandler(object):
164 +    supported_types = {"iff": "ntpkey_iffkey_", "mv": "ntpkey_mvta_", "mvta": "ntpkey_mvta_", "gq": "ntpkey_gqkey_"}
165 +
166 +    def del_key(self, server, type):
167 +        filename = "%s/%s" % (crypto_keys_dir, self._get_filename(type, server))
168 +        os.remove(filename)
169 +        with open("/etc/ntp.conf") as cnf:
170 +            content = cnf.readlines()
171 +        regex = re.compile("\Aserver\s+%s\s+autokey" % server)
172 +        for index, line in enumerate(content):
173 +            if re.match(regex, line) is not None:
174 +                content.pop(index)
175 +                content.insert(index, "server %s\n" % server)
176 +        with open("/etc/ntp.conf", "w") as cnf:
177 +            for line in content:
178 +                cnf.write(line)
179 +
180 +    def add_key(self, servers):
181 +        self._remove_symmetric_keys()
182 +        self._enable_crypto_auth()
183 +        self._copy_keys(servers)
184 +        self._remove_old_client_keys(servers)
185 +        self._create_client_key()
186 +        self.set_key_permissions()
187 +
188 +    def update_client_certificate(self):
189 +        passwd = self._create_client_password()
190 +        os.system("cd %s; ntp-keygen -q %s" % (crypto_keys_dir, passwd))
191 +
192 +    def _enable_crypto_auth(self):
193 +        self._create_client_password()
194 +        with open("/etc/ntp.conf") as cnf:
195 +            content = cnf.readlines()
196 +        includefile_is_correct = False
197 +        keysdir_is_correct = False
198 +        for index, line in enumerate(content):
199 +            if line.startswith("crypto"):
200 +                content.pop(index)
201 +            elif line.startswith("includefile"):
202 +                self.replace_line_in_ntpconf(line, content, index, "includefile", crypto_parameter_file)
203 +                includefile_is_correct = True
204 +            elif line.startswith("keysdir"):
205 +                self.replace_line_in_ntpconf(line, content, index, "keysdir", crypto_keys_dir)
206 +                keysdir_is_correct = True
207 +        if not includefile_is_correct:
208 +            content.append("includefile %s\n" % crypto_parameter_file)
209 +        if not keysdir_is_correct:
210 +            content.append("keysdir %s\n" % crypto_keys_dir)
211 +        with open("/etc/ntp.conf", "w") as cnf:
212 +            for line in content:
213 +                cnf.write(line)
214 +
215 +    def replace_line_in_ntpconf(self, line, filecontent, index, linestarting, lineparameter):
216 +        if line.split()[1] != lineparameter:
217 +            filecontent.pop(index)
218 +            filecontent.insert(index, "%s %s\n" % (linestarting, lineparameter))
219 +
220 +    def _create_client_password(self):
221 +        if not os.path.exists(crypto_parameter_file):
222 +            os.mknod(crypto_parameter_file)
223 +        with open(crypto_parameter_file) as param:
224 +            content = param.readlines()
225 +        for line in content:
226 +            match = re.match(re.compile("\Acrypto\s+pw\s+"), line)
227 +            if match is not None:
228 +                return line.split(None, 2)[2]
229 +        randstr = ''.join([random.choice(string.ascii_letters + string.digits) for n in xrange(32)])
230 +        content.append("crypto pw %s\n" % randstr)
231 +        with open(crypto_parameter_file, "w") as param:
232 +            for line in content:
233 +                param.write(line)
234 +        return randstr
235 +
236 +    def _remove_symmetric_keys(self):
237 +        with open(symmetric_key_file, "w") as obj:
238 +            obj.truncate()
239 +
240 +    def _remove_old_client_keys(self, servers):
241 +        present_files = os.listdir(crypto_keys_dir)
242 +        possible_needed_files = [os.path.basename(crypto_parameter_file), os.path.basename(symmetric_key_file)]
243 +        for srv in servers:
244 +            possible_needed_files = possible_needed_files + self._get_all_supported_filenames(srv['server'])
245 +        for f in possible_needed_files:
246 +            try:
247 +                present_files.remove(f)
248 +            except ValueError:
249 +                pass
250 +        for f in present_files:
251 +            os.remove("%s/%s" % (crypto_keys_dir, f))
252 +
253 +    def _create_client_key(self):
254 +        clientpassword = self._create_client_password()
255 +        os.system("cd %s; ntp-keygen -H -c RSA-SHA1 -p %s" % (crypto_keys_dir, clientpassword))
256 +
257 +    def _copy_keys(self, servers):
258 +        print "copying keys"
259 +        for srv in servers:
260 +            self._remove_old_versions_of_key(str(srv["server"]))
261 +            filename = self._get_filename(str(srv["key"]["type"]), str(srv["server"]))
262 +            if not os.path.exists(filename):
263 +                os.mknod(filename)
264 +            with open("%s/%s" % (crypto_keys_dir, filename), "w") as keyfile:
265 +                for key in srv["key"]["keys"]:
266 +                    keyfile.write("-----BEGIN ENCRYPTED PRIVATE KEY-----\n")
267 +                    keyfile.write("%s\n" % key)
268 +                    keyfile.write("-----END ENCRYPTED PRIVATE KEY-----\n")
269 +
270 +    def _get_filename(self, type, server):
271 +        filename = "%s%s" % (NtpCryptoKeyHandler.supported_types[type], server)
272 +        return filename
273 +
274 +    def _get_all_supported_filenames(self, server):
275 +        filenames = []
276 +        for type in NtpCryptoKeyHandler.supported_types:
277 +            filenames.append(self._get_filename(type, server))
278 +        return filenames
279 +
280 +    def _remove_old_versions_of_key(self, server):
281 +        possible_filenames = self._get_all_supported_filenames(server)
282 +        for key in possible_filenames:
283 +            try:
284 +                os.remove("%s/%s" % (crypto_keys_dir, key))
285 +            except OSError:
286 +                pass
287 +
288 +    def set_key_permissions(self):
289 +        dircontent = os.listdir(crypto_keys_dir)
290 +        for f in dircontent:
291 +            os.chmod("%s/%s" % (crypto_keys_dir, f), 0600)
292 +
293 +
294 +class NtpSymmetricKeyHandler(object):
295 +
296 +    def add_key(self, key, server):
297 +        keys_file = symmetric_key_file
298 +        if not self._is_symmetric_key_auth_enabled():
299 +            self.enable_symmetric_key_auth(keys_file)
300 +        else:
301 +            keys_file = self._get_symmetric_key_file()
302 +        try:
303 +            key_id = self._get_symmetric_key_id(key, keys_file)
304 +        except SymmetricKeyNotFound:
305 +            key_id = self._find_highest_id(keys_file) + 1
306 +            with open(keys_file, "a") as keys:
307 +                keys.write("# %s\n" % server)
308 +                keys.write("%s  M  %s\n" % (key_id, key))
309 +        self._add_trustedkey(key_id)
310 +        self._add_controlkey(key_id)
311 +        self._add_requestkey(key_id)
312 +
313 +    def _enable_key_in_ntpconf(self, old, key_id):
314 +        is_replaced = False
315 +        key_id = str(key_id)
316 +        with open("/etc/ntp.conf", "r") as file:
317 +            buff = file.readlines()
318 +            for index, line in enumerate(buff):
319 +                if (line.startswith(old)) and (key_id not in line):
320 +                    buff[index] = line.rstrip('\n') + "  " + str(key_id) + "\n"
321 +                    is_replaced = True
322 +                    break
323 +                elif (line.startswith(old)) and (key_id in line):
324 +                    is_replaced = True
325 +                    break
326 +        if is_replaced:
327 +            with open("/etc/ntp.conf", "w") as file:
328 +                for line in buff:
329 +                    file.write(line)
330 +        else:
331 +            with open("/etc/ntp.conf", "a") as file:
332 +                file.write("%s  %s\n" % (old, str(key_id)))
333 +
334 +    def _add_trustedkey(self, key_id):
335 +        self._enable_key_in_ntpconf("trustedkey", str(key_id))
336 +
337 +    def _add_controlkey(self, key_id):
338 +        self._enable_key_in_ntpconf("controlkey", str(key_id))
339 +
340 +    def _add_requestkey(self, key_id):
341 +        self._enable_key_in_ntpconf("requestkey", str(key_id))
342 +
343 +    def _find_highest_id(self, keys_file):
344 +        ids = []
345 +        with open(keys_file) as keys:
346 +            for o in keys.readlines():
347 +                id = re.findall("^[0-9]+", o)
348 +                if len(id) > 0:
349 +                    ids.append(int(id[0]))
350 +        ids.sort()
351 +        if len(ids) != 0:
352 +            return ids[-1]
353 +        else:
354 +            return 0
355 +
356 +    def _is_symmetric_key_auth_enabled(self):
357 +        with open("/etc/ntp.conf") as cnf:
358 +            for line in cnf.read().split('\n'):
359 +                if line.startswith("keys"):
360 +                    return True
361 +        return False
362 +
363 +    def _get_symmetric_key_file(self):
364 +        with open("/etc/ntp.conf") as cnf:
365 +            for line in cnf.read().split('\n'):
366 +                if "keys" in line:
367 +                    return line.split()[1]
368 +        raise KeyAuthDisabled()
369 +
370 +    def _get_symmetric_key_id(self, key, keysfile=symmetric_key_file):
371 +        with open(keysfile) as keys:
372 +            for line in keys.read().split('\n'):
373 +                if key in line:
374 +                    return line.split()[0]
375 +        raise SymmetricKeyNotFound()
376 +
377 +    def enable_symmetric_key_auth(self, keys_loc=symmetric_key_file):
378 +        try:
379 +            self._get_symmetric_key_file()
380 +        except KeyAuthDisabled:
381 +            with open("/etc/ntp.conf", "a") as cnf:
382 +                cnf.write("keys %s\n" % keys_loc)
383 +
384 +
385 +class NtpServerHandler(object):
386 +
387 +    def __init__(self, auth_type):
388 +        if auth_type == "symmetric":
389 +            self.keyhandler = NtpSymmetricKeyHandler()
390 +        else:
391 +            self.keyhandler = NtpCryptoKeyHandler()
392 +        self.auth_type = auth_type
393 +
394 +    def delete_other_keys(self):
395 +        if os.path.exists(crypto_keys_dir):
396 +            dircontent = os.listdir(crypto_keys_dir)
397 +            for f in dircontent:
398 +                os.remove("%s/%s" % (crypto_keys_dir, f))
399 +
400 +    def delete_server(self, server):
401 +        with open("/etc/ntp.conf") as conf:
402 +            contents = conf.readlines()
403 +        for index, line in enumerate(contents):
404 +            if server in line:
405 +                contents.pop(index)
406 +                break
407 +        with open("/etc/ntp.conf", "w") as conf:
408 +            for line in contents:
409 +                conf.write(line)
410 +        self._restart_ntpd()
411 +
412 +    def add_server(self, servers):
413 +        self.delete_other_keys()
414 +        for srv in servers:
415 +            if self.auth_type != "none":
416 +                if self.auth_type == "symmetric":
417 +                    self.keyhandler.add_key(srv['key'], srv['server'])
418 +                else:
419 +                    self.keyhandler.add_key(servers)
420 +            self._insert_server_to_config(srv['server'], self.auth_type, srv['key'])
421 +
422 +    def _restart_ntpd(self):
423 +        os.system("systemctl restart ntpd")
424 +
425 +    def _insert_server_to_config(self, server, auth_type, key=None):
426 +        if auth_type == "symmetric":
427 +            keyfile = self.keyhandler._get_symmetric_key_file()
428 +            id = self.keyhandler._get_symmetric_key_id(key, keyfile)
429 +            serverline = "server  " + server + "  key  " + str(id) + '\n'
430 +        elif auth_type == "crypto":
431 +            serverline = "server  " + server + "  autokey\n"
432 +        else:
433 +            serverline = "server  " + server + '\n'
434 +        with open("/etc/ntp.conf") as cnf:
435 +            contents = cnf.readlines()
436 +        server_was_found = False
437 +        for index, line in enumerate(contents):
438 +            if (server in line) and (line.startswith("server")):
439 +                contents[index] = serverline
440 +                server_was_found = True
441 +                break
442 +        if not server_was_found:
443 +            index = 0
444 +            for line in contents:
445 +                if line.startswith("server"):
446 +                    break
447 +                index += 1
448 +            contents.insert(index, serverline)
449 +        with open("/etc/ntp.conf", "w") as cnf:
450 +            for line in contents:
451 +                cnf.write(line)
452 +
453 +    def get_ntp_status(self):
454 +        try:
455 +            print check_output(["systemctl", "status", "ntpd"])
456 +        except CalledProcessError as e:
457 +            print e.output
458 +        try:
459 +            print check_output(["ntpstat", "-u"])
460 +        except CalledProcessError as e:
461 +            print e.output
462 +        try:
463 +            print check_output(["ntpq", "-c", "as"])
464 +        except CalledProcessError as e:
465 +            print e.output
466 +
467 +def find_old_crypto_keys(remaining_servers):
468 +    files = os.listdir(crypto_keys_dir)
469 +    found_keys = []
470 +    for f in files:
471 +        for s in remaining_servers:
472 +            if 'ntpkey_' in f and s in f:
473 +                with open("%s/%s" % (crypto_keys_dir, f)) as keyfcnt:
474 +                    content = keyfcnt.readlines()
475 +                    regex = re.compile("-----BEGIN ENCRYPTED PRIVATE KEY-----\n|-----END ENCRYPTED PRIVATE KEY-----\n")
476 +                    keycont = ''.join(content)
477 +                    raw_keys = re.split(regex, keycont)
478 +                    raw_keys = filter(None, raw_keys)
479 +                    if f == 'ntpkey_iffkey_%s' % s:
480 +                        type = 'iff'
481 +                    elif f == 'ntpkey_gqkey_%s' % s:
482 +                        type = 'gq'
483 +                    elif f == 'ntpkey_mvta_%s' % s:
484 +                        type = 'mv'
485 +                    else:
486 +                        raise Exception("Something is wrong with the filename %s/%s" % (crypto_keys_dir, f))
487 +                    found_keys.append({'server': s, 'key': {'type': type, 'keys': raw_keys}})
488 +    return found_keys
489 +
490 +
491 +
492 +def find_old_symmetric_keys(remaining_servers):
493 +    keyfile = NtpSymmetricKeyHandler()._get_symmetric_key_file()
494 +    retlist = []
495 +    with open(keyfile) as kf:
496 +        content = kf.readlines()
497 +    for srv in remaining_servers:
498 +        if "# %s" % srv in content:
499 +            try:
500 +                index = content.index("# %s" % srv)
501 +                key = content[index + 1].split()[2]
502 +                retlist.append({"server": srv, "key": key})
503 +            except ValueError:
504 +                pass
505 +    return retlist
506 +
507 +
508 +def get_ntp_servers(url, ntpservers, auth_type):
509 +    file_reachable = True
510 +    servers = []
511 +    if url.startswith("file://"):
512 +        path = url.lstrip("file://")
513 +        try:
514 +            with open(path) as f:
515 +                f_content = f.read()
516 +        except IOError:
517 +            file_reachable = False
518 +    else:
519 +        try:
520 +            r = requests.get(url)
521 +            if r.status_code != 200:
522 +                raise requests.exceptions.ConnectionError()
523 +            f_content = r.content
524 +        except requests.exceptions.ConnectionError:
525 +            file_reachable = False
526 +    if file_reachable:
527 +        yaml_content = yaml.load(f_content)
528 +        for item in yaml_content:
529 +            srv = item.keys()[0]
530 +            if srv in ntpservers:
531 +                element = {"server": srv, "key": item[srv]}
532 +                servers.append(element)
533 +    found_servers = [item['server'] for item in servers]
534 +    if len(found_servers) != len(ntpservers):
535 +        remaining_servers = [item for item in ntpservers if item not in found_servers]
536 +        if auth_type == "crypto":
537 +            leftover_servers = find_old_crypto_keys(remaining_servers)
538 +        elif auth_type == "symmetric":
539 +            leftover_servers = find_old_symmetric_keys(remaining_servers)
540 +        else:
541 +            raise Exception("Unknown authentication type for NTP!")
542 +        servers = servers + leftover_servers
543 +        if len(servers) != len(ntpservers):
544 +            raise Exception("Something must be messed up in your config. The NTP servers provided by your key file and by your configuration doesn't match!")
545 +    return servers
546 +
547 +
548 +def remove(url):
549 +    o = urlparse(url)
550 +    if o.scheme == "file":
551 +        os.remove(o.path)
552 +
553 +
554 +
555 +def main():
556 +    module_args = dict(auth_type=dict(type='str', required=True),
557 +                       hosts=dict(type='dict', required=True),
558 +                       ntpservers=dict(type='list', required=True),
559 +                       filepath=dict(type='str', required=True))
560 +    module = AnsibleModule(argument_spec=module_args)
561 +    controllers = get_controllers(module.params['hosts'])
562 +    hostname = get_hostname()
563 +    if hostname not in controllers:
564 +        remove(module.params['filepath'])
565 +        module.exit_json(msg="nothing to do; the script is not executed on a controller host")
566 +        return 0
567 +    if module.params['auth_type'] == "symmetric" or module.params['auth_type'] == "crypto":
568 +        servers = get_ntp_servers(module.params['filepath'], module.params['ntpservers'], module.params['auth_type'])
569 +        handler = NtpServerHandler(module.params['auth_type'])
570 +        handler.add_server(servers)
571 +    elif module.params['auth_type'] == "none":
572 +        pass
573 +    else:
574 +        raise Exception("Invalid authentication type: %s" % module.params['auth_type'])
575 +    remove(module.params['filepath'].rstrip("file://"))
576 +    module.exit_json(msg="all done")
577 +
578 +if __name__ == "__main__":
579 +    main()
580 +
581 diff --git a/tasks/main.yml b/tasks/main.yml
582 index 5c80a4a..cc4f4b6 100644
583 --- a/tasks/main.yml
584 +++ b/tasks/main.yml
585 @@ -1,4 +1,6 @@
586  ---
587 +- shell: file=/etc/sysconfig/ntpdate; sed -i 's/SYNC_HWCLOCK=no/SYNC_HWCLOCK=yes/g' ${file};grep "SYNC_HWCLOCK=yes" ${file} || echo "SYNC_HWCLOCK=yes" >> ${file}
588 +
589  - name: Add the OS specific variables
590    include_vars: '{{ ansible_os_family }}.yml'
591    tags: [ 'configuration', 'package', 'service', 'ntp' ]
592 @@ -16,9 +18,20 @@
593  - name: Copy the ntp.conf template file
594    template: src=ntp.conf.j2 dest=/etc/ntp.conf
595    notify:
596 +  - add ntp keys
597 +  - create redundant fallback ntp servers
598    - restart ntp
599    tags: [ 'configuration', 'package', 'ntp' ]
600  
601 +- name: Clear step-tickers
602 +  lineinfile:
603 +    dest: /etc/ntp/step-tickers
604 +    regexp: ".*"
605 +    state: absent
606 +
607 +- name: enable ntpdate at boot time
608 +  shell: chkconfig ntpdate on
609 +
610  - name: Start/stop ntp service
611    service: name={{ ntp_service_name }} state={{ ntp_service_state }} enabled={{ ntp_service_enabled }} pattern='/ntpd'
612    tags: [ 'service', 'ntp' ]