diff --git a/lib/ansible/modules/network/ios/ios_ntp.py b/lib/ansible/modules/network/ios/ios_ntp.py new file mode 100644 index 00000000000..5de3f0ef1ed --- /dev/null +++ b/lib/ansible/modules/network/ios/ios_ntp.py @@ -0,0 +1,309 @@ +#!/usr/bin/python +# Copyright: Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'network'} + +DOCUMENTATION = ''' +--- +module: ios_ntp +extends_documentation_fragment: ios +version_added: "2.8" +short_description: Manages core NTP configuration. +description: + - Manages core NTP configuration. +author: + - Federico Olivieri (@Federico87) +options: + server: + description: + - Network address of NTP server. + source_int: + description: + - Source interface for NTP packets. + acl: + description: + - ACL for peer/server access restricition. + logging: + description: + - Enable NTP logs. Data type boolean. + type: bool + default: False + auth: + description: + - Enable NTP authentication. Data type boolean. + type: bool + default: False + auth_key: + description: + - md5 NTP authentication key of tye 7. + key_id: + description: + - auth_key id. Datat type string + state: + description: + - Manage the state of the resource. + default: present + choices: ['present', 'absent'] +''' + +EXAMPLES = ''' +# Set new NTP server and source interface +- ios_ntp: + server: 10.0.255.10 + source_int: Loopback0 + logging: false + state: present + +# Remove NTP ACL and logging +- ios_ntp: + acl: NTP_ACL + logging: true + state: absent + +# Set NTP authentication +- ios_ntp: + key_id: 10 + auth_key: 15435A030726242723273C21181319000A + auth: true + state: present + +# Set new NTP configuration +- ios_ntp: + server: 10.0.255.10 + source_int: Loopback0 + acl: NTP_ACL + logging: true + key_id: 10 + auth_key: 15435A030726242723273C21181319000A + auth: true + state: present +''' + +RETURN = ''' +commands: + description: command sent to the device + returned: always + type: list + sample: ["no ntp server 10.0.255.10", "no ntp source Loopback0"] +''' +import re + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.ios.ios import get_config, load_config +from ansible.module_utils.network.ios.ios import ios_argument_spec, check_args + + +def parse_server(line, dest): + if dest == 'server': + match = re.search(r'(ntp server )(\d+\.\d+\.\d+\.\d+)', line, re.M) + if match: + server = match.group(2) + return server + + +def parse_source_int(line, dest): + if dest == 'source': + match = re.search(r'(ntp source )(\S+)', line, re.M) + if match: + source = match.group(2) + return source + + +def parse_acl(line, dest): + if dest == 'access-group': + match = re.search(r'ntp access-group (?:peer|serve)(?:\s+)(\S+)', line, re.M) + if match: + acl = match.group(1) + return acl + + +def parse_logging(line, dest): + if dest == 'logging': + logging = dest + return logging + + +def parse_auth_key(line, dest): + if dest == 'authentication-key': + match = re.search(r'(ntp authentication-key \d+ md5 )(\w+)', line, re.M) + if match: + auth_key = match.group(2) + return auth_key + + +def parse_key_id(line, dest): + if dest == 'trusted-key': + match = re.search(r'(ntp trusted-key )(\d+)', line, re.M) + if match: + auth_key = match.group(2) + return auth_key + + +def parse_auth(dest): + if dest == 'authenticate': + return dest + + +def map_config_to_obj(module): + + obj_dict = {} + obj = [] + server_list = [] + + config = get_config(module, flags=['| include ntp']) + + for line in config.splitlines(): + match = re.search(r'ntp (\S+)', line, re.M) + if match: + dest = match.group(1) + + server = parse_server(line, dest) + source_int = parse_source_int(line, dest) + acl = parse_acl(line, dest) + logging = parse_logging(line, dest) + auth = parse_auth(dest) + auth_key = parse_auth_key(line, dest) + key_id = parse_key_id(line, dest) + + if server: + server_list.append(server) + if source_int: + obj_dict['source_int'] = source_int + if acl: + obj_dict['acl'] = acl + if logging: + obj_dict['logging'] = True + if auth: + obj_dict['auth'] = True + if auth_key: + obj_dict['auth_key'] = auth_key + if key_id: + obj_dict['key_id'] = key_id + + obj_dict['server'] = server_list + obj.append(obj_dict) + + return obj + + +def map_params_to_obj(module): + obj = [] + obj.append({ + 'state': module.params['state'], + 'server': module.params['server'], + 'source_int': module.params['source_int'], + 'logging': module.params['logging'], + 'acl': module.params['acl'], + 'auth': module.params['auth'], + 'auth_key': module.params['auth_key'], + 'key_id': module.params['key_id'] + }) + + return obj + + +def map_obj_to_commands(want, have, module): + + commands = list() + + server_have = have[0].get('server', None) + source_int_have = have[0].get('source_int', None) + acl_have = have[0].get('acl', None) + logging_have = have[0].get('logging', None) + auth_have = have[0].get('auth', None) + auth_key_have = have[0].get('auth_key', None) + key_id_have = have[0].get('key_id', None) + + for w in want: + server = w['server'] + source_int = w['source_int'] + acl = w['acl'] + logging = w['logging'] + state = w['state'] + auth = w['auth'] + auth_key = w['auth_key'] + key_id = w['key_id'] + + if state == 'absent': + if server_have and server in server_have: + commands.append('no ntp server {0}'.format(server)) + if source_int and source_int_have: + commands.append('no ntp source {0}'.format(source_int)) + if acl and acl_have: + commands.append('no ntp access-group peer {0}'.format(acl)) + if logging is True and logging_have: + commands.append('no ntp logging') + if auth is True and auth_have: + commands.append('no ntp authenticate') + if key_id and key_id_have: + commands.append('no ntp trusted-key {0}'.format(key_id)) + if auth_key and auth_key_have: + if key_id and key_id_have: + commands.append('no ntp authentication-key {0} md5 {1} 7'.format(key_id, auth_key)) + + elif state == 'present': + if server is not None and server not in server_have: + commands.append('ntp server {0}'.format(server)) + if source_int is not None and source_int != source_int_have: + commands.append('ntp source {0}'.format(source_int)) + if acl is not None and acl != acl_have: + commands.append('ntp access-group peer {0}'.format(acl)) + if logging is not None and logging != logging_have and logging is not False: + commands.append('ntp logging') + if auth is not None and auth != auth_have and auth is not False: + commands.append('ntp authenticate') + if key_id is not None and key_id != key_id_have: + commands.append('ntp trusted-key {0}'.format(key_id)) + if auth_key is not None and auth_key != auth_key_have: + if key_id is not None: + commands.append('ntp authentication-key {0} md5 {1} 7'.format(key_id, auth_key)) + + return commands + + +def main(): + + argument_spec = dict( + server=dict(), + source_int=dict(), + acl=dict(), + logging=dict(type='bool', default=False), + auth=dict(type='bool', default=False), + auth_key=dict(), + key_id=dict(), + state=dict(choices=['absent', 'present'], default='present') + ) + + argument_spec.update(ios_argument_spec) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True + ) + + result = {'changed': False} + + warnings = list() + check_args(module, warnings) + if warnings: + result['warnings'] = warnings + + want = map_params_to_obj(module) + have = map_config_to_obj(module) + + commands = map_obj_to_commands(want, have, module) + result['commands'] = commands + + if commands: + if not module.check_mode: + load_config(module, commands) + result['changed'] = True + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/ios_ntp/defaults/main.yaml b/test/integration/targets/ios_ntp/defaults/main.yaml new file mode 100644 index 00000000000..5f709c5aac1 --- /dev/null +++ b/test/integration/targets/ios_ntp/defaults/main.yaml @@ -0,0 +1,2 @@ +--- +testcase: "*" diff --git a/test/integration/targets/ios_ntp/meta/main.yml b/test/integration/targets/ios_ntp/meta/main.yml new file mode 100644 index 00000000000..159cea8d383 --- /dev/null +++ b/test/integration/targets/ios_ntp/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - prepare_ios_tests diff --git a/test/integration/targets/ios_ntp/tasks/cli.yaml b/test/integration/targets/ios_ntp/tasks/cli.yaml new file mode 100644 index 00000000000..303af407622 --- /dev/null +++ b/test/integration/targets/ios_ntp/tasks/cli.yaml @@ -0,0 +1,22 @@ +--- +- name: collect all cli test cases + find: + paths: "{{ role_path }}/tests/cli" + patterns: "{{ testcase }}.yaml" + register: test_cases + delegate_to: localhost + +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" + +- name: run test cases (connection=network_cli) + include: "{{ test_case_to_run }}" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run + +- name: run test case (connection=local) + include: "{{ test_case_to_run }} ansible_connection=local" + with_first_found: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/ios_ntp/tasks/main.yaml b/test/integration/targets/ios_ntp/tasks/main.yaml new file mode 100644 index 00000000000..415c99d8b12 --- /dev/null +++ b/test/integration/targets/ios_ntp/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: cli.yaml, tags: ['cli'] } diff --git a/test/integration/targets/ios_ntp/tests/cli/ntp_configuration.yaml b/test/integration/targets/ios_ntp/tests/cli/ntp_configuration.yaml new file mode 100644 index 00000000000..a3e586b86aa --- /dev/null +++ b/test/integration/targets/ios_ntp/tests/cli/ntp_configuration.yaml @@ -0,0 +1,75 @@ +--- +- name: remove NTP (if set) + ios_ntp: + server: 10.75.32.5 + source_int: Loopback0 + acl: NTP_ACL + logging: true + key_id: 10 + auth_key: 15435A030726242723273C21181319000A + auth: true + state: absent + ignore_errors: true + +- block: + + - name: configure NTP + ios_ntp: &config + server: 10.75.32.5 + source_int: Loopback0 + state: present + + - assert: &true + that: + - "result.changed == true" + + - name: idempotence check + ios_ntp: *config + + - assert: &false + that: + - "result.changed == false" + + - name: configure NTP + ios_ntp: &config1 + acl: NTP_ACL + logging: true + state: present + + - assert: *true + + - name: idempotence check + ios_ntp: *config1 + + - assert: *false + + - name: configure NTP with diffferen values + ios_ntp: &config2 + key_id: 10 + auth_key: 15435A030726242723273C21181319000A + auth: true + state: present + + - assert: *true + + - name: idempotence check + ios_ntp: *config2 + + - assert: *false + + - name: remove part of config + ios_ntp: &config3 + acl: NTP_ACL + logging: true + state: absent + + - assert: *true + + - name: idempotence check + ios_ntp: *config3 + + - assert: *false + + always: + - name: Remove ntp config + ios_ntp: *remove diff --git a/test/units/modules/network/ios/fixtures/ios_ntp_config.cfg b/test/units/modules/network/ios/fixtures/ios_ntp_config.cfg new file mode 100644 index 00000000000..ccd8558ab3e --- /dev/null +++ b/test/units/modules/network/ios/fixtures/ios_ntp_config.cfg @@ -0,0 +1,7 @@ +ntp logging +ntp authentication-key 10 md5 15435A030726242723273C21181319000A 7 +ntp authenticate +ntp trusted-key 10 +ntp source Loopback0 +ntp access-group peer NTP_ACL +ntp server 10.75.32.5 diff --git a/test/units/modules/network/ios/test_ios_ntp.py b/test/units/modules/network/ios/test_ios_ntp.py new file mode 100644 index 00000000000..0d106a75c0b --- /dev/null +++ b/test/units/modules/network/ios/test_ios_ntp.py @@ -0,0 +1,99 @@ +# This file is part of Ansible +# +# Ansible 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. +# +# Ansible 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 Ansible. If not, see . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from units.compat.mock import patch +from ansible.modules.network.ios import ios_ntp +from units.modules.utils import set_module_args +from .ios_module import TestIosModule, load_fixture + + +class TestIosNtpModule(TestIosModule): + + module = ios_ntp + + def setUp(self): + super(TestIosNtpModule, self).setUp() + + self.mock_get_config = patch('ansible.modules.network.ios.ios_ntp.get_config') + self.get_config = self.mock_get_config.start() + + self.mock_load_config = patch('ansible.modules.network.ios.ios_ntp.load_config') + self.load_config = self.mock_load_config.start() + + def tearDown(self): + super(TestIosNtpModule, self).tearDown() + self.mock_get_config.stop() + self.mock_load_config.stop() + + def load_fixtures(self, commands=None): + self.get_config.return_value = load_fixture('ios_ntp_config.cfg').strip() + self.load_config.return_value = dict(diff=None, session='session') + + def test_ios_ntp_idempotent(self): + set_module_args(dict( + server='10.75.32.5', + source_int='Loopback0', + acl='NTP_ACL', + logging=True, + auth=True, + auth_key='15435A030726242723273C21181319000A', + key_id='10', + state='present' + )) + commands = [] + self.execute_module(changed=False, commands=commands) + + def test_ios_ntp_config(self): + set_module_args(dict( + server='10.75.33.5', + source_int='Vlan2', + acl='NTP_ACL', + logging=True, + auth=True, + auth_key='15435A030726242723273C21181319000A', + key_id='10', + state='present' + )) + commands = [ + 'ntp server 10.75.33.5', + 'ntp source Vlan2' + ] + self.execute_module(changed=True, commands=commands) + + def test_ios_ntp_remove(self): + set_module_args(dict( + server='10.75.32.5', + source_int='Loopback0', + acl='NTP_ACL', + logging=True, + auth=True, + auth_key='15435A030726242723273C21181319000A', + key_id='10', + state='absent' + )) + commands = [ + 'no ntp server 10.75.32.5', + 'no ntp source Loopback0', + 'no ntp access-group peer NTP_ACL', + 'no ntp logging', + 'no ntp authenticate', + 'no ntp trusted-key 10', + 'no ntp authentication-key 10 md5 15435A030726242723273C21181319000A 7' + ] + self.execute_module(changed=True, commands=commands)