diff options
Diffstat (limited to 'plugins/modules')
| -rw-r--r-- | plugins/modules/__init__.py | 0 | ||||
| -rw-r--r-- | plugins/modules/vyos_banner.py | 178 | ||||
| -rw-r--r-- | plugins/modules/vyos_command.py | 222 | ||||
| -rw-r--r-- | plugins/modules/vyos_config.py | 344 | ||||
| -rw-r--r-- | plugins/modules/vyos_facts.py | 333 | ||||
| -rw-r--r-- | plugins/modules/vyos_interface.py | 438 | ||||
| -rw-r--r-- | plugins/modules/vyos_l3_interface.py | 285 | ||||
| -rw-r--r-- | plugins/modules/vyos_linkagg.py | 265 | ||||
| -rw-r--r-- | plugins/modules/vyos_lldp.py | 121 | ||||
| -rw-r--r-- | plugins/modules/vyos_lldp_interface.py | 228 | ||||
| -rw-r--r-- | plugins/modules/vyos_logging.py | 263 | ||||
| -rw-r--r-- | plugins/modules/vyos_ping.py | 246 | ||||
| -rw-r--r-- | plugins/modules/vyos_static_route.py | 265 | ||||
| -rw-r--r-- | plugins/modules/vyos_system.py | 211 | ||||
| -rw-r--r-- | plugins/modules/vyos_user.py | 340 | ||||
| -rw-r--r-- | plugins/modules/vyos_vlan.py | 332 | 
16 files changed, 4071 insertions, 0 deletions
| diff --git a/plugins/modules/__init__.py b/plugins/modules/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/modules/__init__.py diff --git a/plugins/modules/vyos_banner.py b/plugins/modules/vyos_banner.py new file mode 100644 index 00000000..06d5a305 --- /dev/null +++ b/plugins/modules/vyos_banner.py @@ -0,0 +1,178 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017, Ansible by Red Hat, inc +# +# This file is part of Ansible by Red Hat +# +# 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 <http://www.gnu.org/licenses/>. +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', +                    'status': ['preview'], +                    'supported_by': 'network'} + +DOCUMENTATION = """ +--- +module: vyos_banner +version_added: "2.4" +author: "Trishna Guha (@trishnaguha)" +short_description: Manage multiline banners on VyOS devices +description: +  - This will configure both pre-login and post-login banners on remote +    devices running VyOS. It allows playbooks to add or remote +    banner text from the active running configuration. +notes: +  - Tested against VYOS 1.1.7 +options: +  banner: +    description: +      - Specifies which banner that should be +        configured on the remote device. +    required: true +    choices: ['pre-login', 'post-login'] +  text: +    description: +      - The banner text that should be +        present in the remote device running configuration. This argument +        accepts a multiline string, with no empty lines. Requires I(state=present). +  state: +    description: +      - Specifies whether or not the configuration is present in the current +        devices active running configuration. +    default: present +    choices: ['present', 'absent'] +extends_documentation_fragment: vyos +""" + +EXAMPLES = """ +- name: configure the pre-login banner +  vyos_banner: +    banner: pre-login +    text: | +      this is my pre-login banner +      that contains a multiline +      string +    state: present +- name: remove the post-login banner +  vyos_banner: +    banner: post-login +    state: absent +""" + +RETURN = """ +commands: +  description: The list of configuration mode commands to send to the device +  returned: always +  type: list +  sample: +    - banner pre-login +    - this is my pre-login banner +    - that contains a multiline +    - string +""" + +import re + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_config, load_config +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import vyos_argument_spec + + +def spec_to_commands(updates, module): +    commands = list() +    want, have = updates +    state = module.params['state'] + +    if state == 'absent': +        if have.get('state') != 'absent' or (have.get('state') != 'absent' and +                                             'text' in have.keys() and have['text']): +            commands.append('delete system login banner %s' % module.params['banner']) + +    elif state == 'present': +        if want['text'] and want['text'].encode().decode('unicode_escape') != have.get('text'): +            banner_cmd = 'set system login banner %s ' % module.params['banner'] +            banner_cmd += want['text'].strip() +            commands.append(banner_cmd) + +    return commands + + +def config_to_dict(module): +    data = get_config(module) +    output = None +    obj = {'banner': module.params['banner'], 'state': 'absent'} + +    for line in data.split('\n'): +        if line.startswith('set system login banner %s' % obj['banner']): +            match = re.findall(r'%s (.*)' % obj['banner'], line, re.M) +            output = match +    if output: +        obj['text'] = output[0].encode().decode('unicode_escape') +        obj['state'] = 'present' + +    return obj + + +def map_params_to_obj(module): +    text = module.params['text'] +    if text: +        text = "%r" % (str(text).strip()) + +    return { +        'banner': module.params['banner'], +        'text': text, +        'state': module.params['state'] +    } + + +def main(): +    """ main entry point for module execution +    """ +    argument_spec = dict( +        banner=dict(required=True, choices=['pre-login', 'post-login']), +        text=dict(), +        state=dict(default='present', choices=['present', 'absent']) +    ) + +    argument_spec.update(vyos_argument_spec) + +    required_if = [('state', 'present', ('text',))] + +    module = AnsibleModule(argument_spec=argument_spec, +                           required_if=required_if, +                           supports_check_mode=True) + +    warnings = list() + +    result = {'changed': False} +    if warnings: +        result['warnings'] = warnings + +    want = map_params_to_obj(module) +    have = config_to_dict(module) + +    commands = spec_to_commands((want, have), module) +    result['commands'] = commands + +    if commands: +        commit = not module.check_mode +        load_config(module, commands, commit=commit) +        result['changed'] = True + +    module.exit_json(**result) + + +if __name__ == '__main__': +    main() diff --git a/plugins/modules/vyos_command.py b/plugins/modules/vyos_command.py new file mode 100644 index 00000000..16487e37 --- /dev/null +++ b/plugins/modules/vyos_command.py @@ -0,0 +1,222 @@ +#!/usr/bin/python +# +# 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 <http://www.gnu.org/licenses/>. +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', +                    'status': ['preview'], +                    'supported_by': 'network'} + + +DOCUMENTATION = """ +--- +module: vyos_command +version_added: "2.2" +author: "Nathaniel Case (@qalthos)" +short_description: Run one or more commands on VyOS devices +description: +  - The command module allows running one or more commands on remote +    devices running VyOS.  This module can also be introspected +    to validate key parameters before returning successfully.  If the +    conditional statements are not met in the wait period, the task +    fails. +  - Certain C(show) commands in VyOS produce many lines of output and +    use a custom pager that can cause this module to hang.  If the +    value of the environment variable C(ANSIBLE_VYOS_TERMINAL_LENGTH) +    is not set, the default number of 10000 is used. +extends_documentation_fragment: vyos +options: +  commands: +    description: +      - The ordered set of commands to execute on the remote device +        running VyOS.  The output from the command execution is +        returned to the playbook.  If the I(wait_for) argument is +        provided, the module is not returned until the condition is +        satisfied or the number of retries has been exceeded. +    required: true +  wait_for: +    description: +      - Specifies what to evaluate from the output of the command +        and what conditionals to apply.  This argument will cause +        the task to wait for a particular conditional to be true +        before moving forward.  If the conditional is not true +        by the configured I(retries), the task fails. See examples. +    aliases: ['waitfor'] +  match: +    description: +      - The I(match) argument is used in conjunction with the +        I(wait_for) argument to specify the match policy. Valid +        values are C(all) or C(any).  If the value is set to C(all) +        then all conditionals in the wait_for must be satisfied.  If +        the value is set to C(any) then only one of the values must be +        satisfied. +    default: all +    choices: ['any', 'all'] +  retries: +    description: +      - Specifies the number of retries a command should be tried +        before it is considered failed. The command is run on the +        target device every retry and evaluated against the I(wait_for) +        conditionals. +    default: 10 +  interval: +    description: +      - Configures the interval in seconds to wait between I(retries) +        of the command. If the command does not pass the specified +        conditions, the interval indicates how long to wait before +        trying the command again. +    default: 1 + +notes: +  - Tested against VYOS 1.1.7 +  - Running C(show system boot-messages all) will cause the module to hang since +    VyOS is using a custom pager setting to display the output of that command. +  - If a command sent to the device requires answering a prompt, it is possible +    to pass a dict containing I(command), I(answer) and I(prompt). See examples. +""" + +EXAMPLES = """ +tasks: +  - name: show configuration on ethernet devices eth0 and eth1 +    vyos_command: +      commands: +        - show interfaces ethernet {{ item }} +    with_items: +      - eth0 +      - eth1 + +  - name: run multiple commands and check if version output contains specific version string +    vyos_command: +      commands: +        - show version +        - show hardware cpu +      wait_for: +        - "result[0] contains 'VyOS 1.1.7'" + +  - name: run command that requires answering a prompt +    vyos_command: +      commands: +        - command: 'rollback 1' +          prompt: 'Proceed with reboot? [confirm][y]' +          answer: y +""" + +RETURN = """ +stdout: +  description: The set of responses from the commands +  returned: always apart from low level errors (such as action plugin) +  type: list +  sample: ['...', '...'] +stdout_lines: +  description: The value of stdout split into a list +  returned: always +  type: list +  sample: [['...', '...'], ['...'], ['...']] +failed_conditions: +  description: The list of conditionals that have failed +  returned: failed +  type: list +  sample: ['...', '...'] +warnings: +  description: The list of warnings (if any) generated by module based on arguments +  returned: always +  type: list +  sample: ['...', '...'] +""" +import time + +from ansible.module_utils._text import to_text +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.common.parsing import Conditional +from ansible.module_utils.network.common.utils import transform_commands, to_lines +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import run_commands +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import vyos_argument_spec + + +def parse_commands(module, warnings): +    commands = transform_commands(module) + +    if module.check_mode: +        for item in list(commands): +            if not item['command'].startswith('show'): +                warnings.append( +                    'Only show commands are supported when using check mode, not ' +                    'executing %s' % item['command'] +                ) +                commands.remove(item) + +    return commands + + +def main(): +    spec = dict( +        commands=dict(type='list', required=True), + +        wait_for=dict(type='list', aliases=['waitfor']), +        match=dict(default='all', choices=['all', 'any']), + +        retries=dict(default=10, type='int'), +        interval=dict(default=1, type='int') +    ) + +    spec.update(vyos_argument_spec) + +    module = AnsibleModule(argument_spec=spec, supports_check_mode=True) + +    warnings = list() +    result = {'changed': False, 'warnings': warnings} +    commands = parse_commands(module, warnings) +    wait_for = module.params['wait_for'] or list() + +    try: +        conditionals = [Conditional(c) for c in wait_for] +    except AttributeError as exc: +        module.fail_json(msg=to_text(exc)) + +    retries = module.params['retries'] +    interval = module.params['interval'] +    match = module.params['match'] + +    for _ in range(retries): +        responses = run_commands(module, commands) + +        for item in list(conditionals): +            if item(responses): +                if match == 'any': +                    conditionals = list() +                    break +                conditionals.remove(item) + +        if not conditionals: +            break + +        time.sleep(interval) + +    if conditionals: +        failed_conditions = [item.raw for item in conditionals] +        msg = 'One or more conditional statements have not been satisfied' +        module.fail_json(msg=msg, failed_conditions=failed_conditions) + +    result.update({ +        'stdout': responses, +        'stdout_lines': list(to_lines(responses)), +    }) + +    module.exit_json(**result) + + +if __name__ == '__main__': +    main() diff --git a/plugins/modules/vyos_config.py b/plugins/modules/vyos_config.py new file mode 100644 index 00000000..6ed07fcf --- /dev/null +++ b/plugins/modules/vyos_config.py @@ -0,0 +1,344 @@ +#!/usr/bin/python +# +# 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 <http://www.gnu.org/licenses/>. +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', +                    'status': ['preview'], +                    'supported_by': 'network'} + + +DOCUMENTATION = """ +--- +module: vyos_config +version_added: "2.2" +author: "Nathaniel Case (@qalthos)" +short_description: Manage VyOS configuration on remote device +description: +  - This module provides configuration file management of VyOS +    devices. It provides arguments for managing both the +    configuration file and state of the active configuration. All +    configuration statements are based on `set` and `delete` commands +    in the device configuration. +extends_documentation_fragment: vyos +notes: +  - Tested against VYOS 1.1.7 +  - Abbreviated commands are NOT idempotent, see +    L(Network FAQ,../network/user_guide/faq.html#why-do-the-config-modules-always-return-changed-true-with-abbreviated-commands). +options: +  lines: +    description: +      - The ordered set of configuration lines to be managed and +        compared with the existing configuration on the remote +        device. +  src: +    description: +      - The C(src) argument specifies the path to the source config +        file to load.  The source config file can either be in +        bracket format or set format.  The source file can include +        Jinja2 template variables. +  match: +    description: +      - The C(match) argument controls the method used to match +        against the current active configuration.  By default, the +        desired config is matched against the active config and the +        deltas are loaded.  If the C(match) argument is set to C(none) +        the active configuration is ignored and the configuration is +        always loaded. +    default: line +    choices: ['line', 'none'] +  backup: +    description: +      - The C(backup) argument will backup the current devices active +        configuration to the Ansible control host prior to making any +        changes. If the C(backup_options) value is not given, the +        backup file will be located in the backup folder in the playbook +        root directory or role root directory, if playbook is part of an +        ansible role. If the directory does not exist, it is created. +    type: bool +    default: 'no' +  comment: +    description: +      - Allows a commit description to be specified to be included +        when the configuration is committed.  If the configuration is +        not changed or committed, this argument is ignored. +    default: 'configured by vyos_config' +  config: +    description: +      - The C(config) argument specifies the base configuration to use +        to compare against the desired configuration.  If this value +        is not specified, the module will automatically retrieve the +        current active configuration from the remote device. +  save: +    description: +      - The C(save) argument controls whether or not changes made +        to the active configuration are saved to disk.  This is +        independent of committing the config.  When set to True, the +        active configuration is saved. +    type: bool +    default: 'no' +  backup_options: +    description: +      - This is a dict object containing configurable options related to backup file path. +        The value of this option is read only when C(backup) is set to I(yes), if C(backup) is set +        to I(no) this option will be silently ignored. +    suboptions: +      filename: +        description: +          - The filename to be used to store the backup configuration. If the the filename +            is not given it will be generated based on the hostname, current time and date +            in format defined by <hostname>_config.<current-date>@<current-time> +      dir_path: +        description: +          - This option provides the path ending with directory name in which the backup +            configuration file will be stored. If the directory does not exist it will be first +            created and the filename is either the value of C(filename) or default filename +            as described in C(filename) options description. If the path value is not given +            in that case a I(backup) directory will be created in the current working directory +            and backup configuration will be copied in C(filename) within I(backup) directory. +        type: path +    type: dict +    version_added: "2.8" +""" + +EXAMPLES = """ +- name: configure the remote device +  vyos_config: +    lines: +      - set system host-name {{ inventory_hostname }} +      - set service lldp +      - delete service dhcp-server + +- name: backup and load from file +  vyos_config: +    src: vyos.cfg +    backup: yes + +- name: render a Jinja2 template onto the VyOS router +  vyos_config: +    src: vyos_template.j2 + +- name: for idempotency, use full-form commands +  vyos_config: +    lines: +      # - set int eth eth2 description 'OUTSIDE' +      - set interface ethernet eth2 description 'OUTSIDE' + +- name: configurable backup path +  vyos_config: +    backup: yes +    backup_options: +      filename: backup.cfg +      dir_path: /home/user +""" + +RETURN = """ +commands: +  description: The list of configuration commands sent to the device +  returned: always +  type: list +  sample: ['...', '...'] +filtered: +  description: The list of configuration commands removed to avoid a load failure +  returned: always +  type: list +  sample: ['...', '...'] +backup_path: +  description: The full path to the backup file +  returned: when backup is yes +  type: str +  sample: /playbooks/ansible/backup/vyos_config.2016-07-16@22:28:34 +filename: +  description: The name of the backup file +  returned: when backup is yes and filename is not specified in backup options +  type: str +  sample: vyos_config.2016-07-16@22:28:34 +shortname: +  description: The full path to the backup file excluding the timestamp +  returned: when backup is yes and filename is not specified in backup options +  type: str +  sample: /playbooks/ansible/backup/vyos_config +date: +  description: The date extracted from the backup file name +  returned: when backup is yes +  type: str +  sample: "2016-07-16" +time: +  description: The time extracted from the backup file name +  returned: when backup is yes +  type: str +  sample: "22:28:34" +""" +import re + +from ansible.module_utils._text import to_text +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.connection import ConnectionError +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import load_config, get_config, run_commands +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import vyos_argument_spec, get_connection + + +DEFAULT_COMMENT = 'configured by vyos_config' + +CONFIG_FILTERS = [ +    re.compile(r'set system login user \S+ authentication encrypted-password') +] + + +def get_candidate(module): +    contents = module.params['src'] or module.params['lines'] + +    if module.params['src']: +        contents = format_commands(contents.splitlines()) + +    contents = '\n'.join(contents) +    return contents + + +def format_commands(commands): +    return [line for line in commands if len(line.strip()) > 0] + + +def diff_config(commands, config): +    config = [str(c).replace("'", '') for c in config.splitlines()] + +    updates = list() +    visited = set() + +    for line in commands: +        item = str(line).replace("'", '') + +        if not item.startswith('set') and not item.startswith('delete'): +            raise ValueError('line must start with either `set` or `delete`') + +        elif item.startswith('set') and item not in config: +            updates.append(line) + +        elif item.startswith('delete'): +            if not config: +                updates.append(line) +            else: +                item = re.sub(r'delete', 'set', item) +                for entry in config: +                    if entry.startswith(item) and line not in visited: +                        updates.append(line) +                        visited.add(line) + +    return list(updates) + + +def sanitize_config(config, result): +    result['filtered'] = list() +    index_to_filter = list() +    for regex in CONFIG_FILTERS: +        for index, line in enumerate(list(config)): +            if regex.search(line): +                result['filtered'].append(line) +                index_to_filter.append(index) +    # Delete all filtered configs +    for filter_index in sorted(index_to_filter, reverse=True): +        del config[filter_index] + + +def run(module, result): +    # get the current active config from the node or passed in via +    # the config param +    config = module.params['config'] or get_config(module) + +    # create the candidate config object from the arguments +    candidate = get_candidate(module) + +    # create loadable config that includes only the configuration updates +    connection = get_connection(module) +    try: +        response = connection.get_diff(candidate=candidate, running=config, diff_match=module.params['match']) +    except ConnectionError as exc: +        module.fail_json(msg=to_text(exc, errors='surrogate_then_replace')) + +    commands = response.get('config_diff') +    sanitize_config(commands, result) + +    result['commands'] = commands + +    commit = not module.check_mode +    comment = module.params['comment'] + +    diff = None +    if commands: +        diff = load_config(module, commands, commit=commit, comment=comment) + +        if result.get('filtered'): +            result['warnings'].append('Some configuration commands were ' +                                      'removed, please see the filtered key') + +        result['changed'] = True + +    if module._diff: +        result['diff'] = {'prepared': diff} + + +def main(): +    backup_spec = dict( +        filename=dict(), +        dir_path=dict(type='path') +    ) +    argument_spec = dict( +        src=dict(type='path'), +        lines=dict(type='list'), + +        match=dict(default='line', choices=['line', 'none']), + +        comment=dict(default=DEFAULT_COMMENT), + +        config=dict(), + +        backup=dict(type='bool', default=False), +        backup_options=dict(type='dict', options=backup_spec), +        save=dict(type='bool', default=False), +    ) + +    argument_spec.update(vyos_argument_spec) + +    mutually_exclusive = [('lines', 'src')] + +    module = AnsibleModule( +        argument_spec=argument_spec, +        mutually_exclusive=mutually_exclusive, +        supports_check_mode=True +    ) + +    warnings = list() + +    result = dict(changed=False, warnings=warnings) + +    if module.params['backup']: +        result['__backup__'] = get_config(module=module) + +    if any((module.params['src'], module.params['lines'])): +        run(module, result) + +    if module.params['save']: +        diff = run_commands(module, commands=['configure', 'compare saved'])[1] +        if diff != '[edit]': +            run_commands(module, commands=['save']) +            result['changed'] = True +        run_commands(module, commands=['exit']) + +    module.exit_json(**result) + + +if __name__ == '__main__': +    main() diff --git a/plugins/modules/vyos_facts.py b/plugins/modules/vyos_facts.py new file mode 100644 index 00000000..c480969b --- /dev/null +++ b/plugins/modules/vyos_facts.py @@ -0,0 +1,333 @@ +#!/usr/bin/python +# +# 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 <http://www.gnu.org/licenses/>. +# +ANSIBLE_METADATA = {'metadata_version': '1.1', +                    'status': ['preview'], +                    'supported_by': 'network'} + + +DOCUMENTATION = """ +--- +module: vyos_facts +version_added: "2.2" +author: "Nathaniel Case (@qalthos)" +short_description: Collect facts from remote devices running VyOS +description: +  - Collects a base set of device facts from a remote device that +    is running VyOS.  This module prepends all of the +    base network fact keys with U(ansible_net_<fact>).  The facts +    module will always collect a base set of facts from the device +    and can enable or disable collection of additional facts. +extends_documentation_fragment: vyos +notes: +  - Tested against VYOS 1.1.7 +options: +  gather_subset: +    description: +      - When supplied, this argument will restrict the facts collected +        to a given subset.  Possible values for this argument include +        all, default, config, and neighbors.  Can specify a list of +        values to include a larger subset.  Values can also be used +        with an initial C(M(!)) to specify that a specific subset should +        not be collected. +    required: false +    default: "!config" +""" + +EXAMPLES = """ +- name: collect all facts from the device +  vyos_facts: +    gather_subset: all + +- name: collect only the config and default facts +  vyos_facts: +    gather_subset: config + +- name: collect everything exception the config +  vyos_facts: +    gather_subset: "!config" +""" + +RETURN = """ +ansible_net_config: +  description: The running-config from the device +  returned: when config is configured +  type: str +ansible_net_commits: +  description: The set of available configuration revisions +  returned: when present +  type: list +ansible_net_hostname: +  description: The configured system hostname +  returned: always +  type: str +ansible_net_model: +  description: The device model string +  returned: always +  type: str +ansible_net_serialnum: +  description: The serial number of the device +  returned: always +  type: str +ansible_net_version: +  description: The version of the software running +  returned: always +  type: str +ansible_net_neighbors: +  description: The set of LLDP neighbors +  returned: when interface is configured +  type: list +ansible_net_gather_subset: +  description: The list of subsets gathered by the module +  returned: always +  type: list +ansible_net_api: +  description: The name of the transport +  returned: always +  type: str +ansible_net_python_version: +  description: The Python version Ansible controller is using +  returned: always +  type: str +""" + +import platform +import re + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six import iteritems +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import run_commands, get_capabilities +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import vyos_argument_spec + + +class FactsBase(object): + +    COMMANDS = frozenset() + +    def __init__(self, module): +        self.module = module +        self.facts = dict() +        self.responses = None + +    def populate(self): +        self.responses = run_commands(self.module, list(self.COMMANDS)) + + +class Default(FactsBase): + +    COMMANDS = [ +        'show version', +    ] + +    def populate(self): +        super(Default, self).populate() +        data = self.responses[0] +        self.facts['serialnum'] = self.parse_serialnum(data) +        self.facts.update(self.platform_facts()) + +    def parse_serialnum(self, data): +        match = re.search(r'HW S/N:\s+(\S+)', data) +        if match: +            return match.group(1) + +    def platform_facts(self): +        platform_facts = {} + +        resp = get_capabilities(self.module) +        device_info = resp['device_info'] + +        platform_facts['system'] = device_info['network_os'] + +        for item in ('model', 'image', 'version', 'platform', 'hostname'): +            val = device_info.get('network_os_%s' % item) +            if val: +                platform_facts[item] = val + +        platform_facts['api'] = resp['network_api'] +        platform_facts['python_version'] = platform.python_version() + +        return platform_facts + + +class Config(FactsBase): + +    COMMANDS = [ +        'show configuration commands', +        'show system commit', +    ] + +    def populate(self): +        super(Config, self).populate() + +        self.facts['config'] = self.responses + +        commits = self.responses[1] +        entries = list() +        entry = None + +        for line in commits.split('\n'): +            match = re.match(r'(\d+)\s+(.+)by(.+)via(.+)', line) +            if match: +                if entry: +                    entries.append(entry) + +                entry = dict(revision=match.group(1), +                             datetime=match.group(2), +                             by=str(match.group(3)).strip(), +                             via=str(match.group(4)).strip(), +                             comment=None) +            else: +                entry['comment'] = line.strip() + +        self.facts['commits'] = entries + + +class Neighbors(FactsBase): + +    COMMANDS = [ +        'show lldp neighbors', +        'show lldp neighbors detail', +    ] + +    def populate(self): +        super(Neighbors, self).populate() + +        all_neighbors = self.responses[0] +        if 'LLDP not configured' not in all_neighbors: +            neighbors = self.parse( +                self.responses[1] +            ) +            self.facts['neighbors'] = self.parse_neighbors(neighbors) + +    def parse(self, data): +        parsed = list() +        values = None +        for line in data.split('\n'): +            if not line: +                continue +            elif line[0] == ' ': +                values += '\n%s' % line +            elif line.startswith('Interface'): +                if values: +                    parsed.append(values) +                values = line +        if values: +            parsed.append(values) +        return parsed + +    def parse_neighbors(self, data): +        facts = dict() +        for item in data: +            interface = self.parse_interface(item) +            host = self.parse_host(item) +            port = self.parse_port(item) +            if interface not in facts: +                facts[interface] = list() +            facts[interface].append(dict(host=host, port=port)) +        return facts + +    def parse_interface(self, data): +        match = re.search(r'^Interface:\s+(\S+),', data) +        return match.group(1) + +    def parse_host(self, data): +        match = re.search(r'SysName:\s+(.+)$', data, re.M) +        if match: +            return match.group(1) + +    def parse_port(self, data): +        match = re.search(r'PortDescr:\s+(.+)$', data, re.M) +        if match: +            return match.group(1) + + +FACT_SUBSETS = dict( +    default=Default, +    neighbors=Neighbors, +    config=Config +) + +VALID_SUBSETS = frozenset(FACT_SUBSETS.keys()) + + +def main(): +    argument_spec = dict( +        gather_subset=dict(default=['!config'], type='list') +    ) + +    argument_spec.update(vyos_argument_spec) + +    module = AnsibleModule(argument_spec=argument_spec, +                           supports_check_mode=True) + +    warnings = list() + +    gather_subset = module.params['gather_subset'] + +    runable_subsets = set() +    exclude_subsets = set() + +    for subset in gather_subset: +        if subset == 'all': +            runable_subsets.update(VALID_SUBSETS) +            continue + +        if subset.startswith('!'): +            subset = subset[1:] +            if subset == 'all': +                exclude_subsets.update(VALID_SUBSETS) +                continue +            exclude = True +        else: +            exclude = False + +        if subset not in VALID_SUBSETS: +            module.fail_json(msg='Subset must be one of [%s], got %s' % +                             (', '.join(VALID_SUBSETS), subset)) + +        if exclude: +            exclude_subsets.add(subset) +        else: +            runable_subsets.add(subset) + +    if not runable_subsets: +        runable_subsets.update(VALID_SUBSETS) + +    runable_subsets.difference_update(exclude_subsets) +    runable_subsets.add('default') + +    facts = dict() +    facts['gather_subset'] = list(runable_subsets) + +    instances = list() +    for key in runable_subsets: +        instances.append(FACT_SUBSETS[key](module)) + +    for inst in instances: +        inst.populate() +        facts.update(inst.facts) + +    ansible_facts = dict() +    for key, value in iteritems(facts): +        key = 'ansible_net_%s' % key +        ansible_facts[key] = value + +    module.exit_json(ansible_facts=ansible_facts, warnings=warnings) + + +if __name__ == '__main__': +    main() diff --git a/plugins/modules/vyos_interface.py b/plugins/modules/vyos_interface.py new file mode 100644 index 00000000..e7af0c19 --- /dev/null +++ b/plugins/modules/vyos_interface.py @@ -0,0 +1,438 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017, Ansible by Red Hat, inc +# +# This file is part of Ansible by Red Hat +# +# 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 <http://www.gnu.org/licenses/>. +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', +                    'status': ['preview'], +                    'supported_by': 'network'} + + +DOCUMENTATION = """ +--- +module: vyos_interface +version_added: "2.4" +author: "Ganesh Nalawade (@ganeshrn)" +short_description: Manage Interface on VyOS network devices +description: +  - This module provides declarative management of Interfaces +    on VyOS network devices. +notes: +  - Tested against VYOS 1.1.7 +options: +  name: +    description: +      - Name of the Interface. +    required: true +  description: +    description: +      - Description of Interface. +  enabled: +    description: +      - Interface link status. +    type: bool +  speed: +    description: +      - Interface link speed. +  mtu: +    description: +      - Maximum size of transmit packet. +  duplex: +    description: +      - Interface link status. +    default: auto +    choices: ['full', 'half', 'auto'] +  delay: +    description: +      - Time in seconds to wait before checking for the operational state on remote +        device. This wait is applicable for operational state argument which are +        I(state) with values C(up)/C(down) and I(neighbors). +    default: 10 +  neighbors: +    description: +      - Check the operational state of given interface C(name) for LLDP neighbor. +      - The following suboptions are available. +    suboptions: +        host: +          description: +            - "LLDP neighbor host for given interface C(name)." +        port: +          description: +            - "LLDP neighbor port to which given interface C(name) is connected." +    version_added: 2.5 +  aggregate: +    description: List of Interfaces definitions. +  state: +    description: +      - State of the Interface configuration, C(up) means present and +        operationally up and C(down) means present and operationally C(down) +    default: present +    choices: ['present', 'absent', 'up', 'down'] +extends_documentation_fragment: vyos +""" + +EXAMPLES = """ +- name: configure interface +  vyos_interface: +    name: eth0 +    description: test-interface + +- name: remove interface +  vyos_interface: +    name: eth0 +    state: absent + +- name: make interface down +  vyos_interface: +    name: eth0 +    enabled: False + +- name: make interface up +  vyos_interface: +    name: eth0 +    enabled: True + +- name: Configure interface speed, mtu, duplex +  vyos_interface: +    name: eth5 +    state: present +    speed: 100 +    mtu: 256 +    duplex: full + +- name: Set interface using aggregate +  vyos_interface: +    aggregate: +      - { name: eth1, description: test-interface-1,  speed: 100, duplex: half, mtu: 512} +      - { name: eth2, description: test-interface-2,  speed: 1000, duplex: full, mtu: 256} + +- name: Disable interface on aggregate +  net_interface: +    aggregate: +      - name: eth1 +      - name: eth2 +    enabled: False + +- name: Delete interface using aggregate +  net_interface: +    aggregate: +      - name: eth1 +      - name: eth2 +    state: absent + +- name: Check lldp neighbors intent arguments +  vyos_interface: +    name: eth0 +    neighbors: +    - port: eth0 +      host: netdev + +- name: Config + intent +  vyos_interface: +    name: eth1 +    enabled: False +    state: down +""" + +RETURN = """ +commands: +  description: The list of configuration mode commands to send to the device +  returned: always, except for the platforms that use Netconf transport to manage the device. +  type: list +  sample: +    - set interfaces ethernet eth0 description "test-interface" +    - set interfaces ethernet eth0 speed 100 +    - set interfaces ethernet eth0 mtu 256 +    - set interfaces ethernet eth0 duplex full +""" +import re + +from copy import deepcopy +from time import sleep + +from ansible.module_utils._text import to_text +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.connection import exec_command +from ansible.module_utils.network.common.utils import conditional, remove_default_spec +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import load_config, get_config +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import vyos_argument_spec + + +def search_obj_in_list(name, lst): +    for o in lst: +        if o['name'] == name: +            return o + +    return None + + +def map_obj_to_commands(updates): +    commands = list() +    want, have = updates + +    params = ('speed', 'description', 'duplex', 'mtu') +    for w in want: +        name = w['name'] +        disable = w['disable'] +        state = w['state'] + +        obj_in_have = search_obj_in_list(name, have) +        set_interface = 'set interfaces ethernet ' + name +        delete_interface = 'delete interfaces ethernet ' + name + +        if state == 'absent' and obj_in_have: +            commands.append(delete_interface) +        elif state in ('present', 'up', 'down'): +            if obj_in_have: +                for item in params: +                    value = w.get(item) + +                    if value and value != obj_in_have.get(item): +                        if item == 'description': +                            value = "\'" + str(value) + "\'" +                        commands.append(set_interface + ' ' + item + ' ' + str(value)) + +                if disable and not obj_in_have.get('disable', False): +                    commands.append(set_interface + ' disable') +                elif not disable and obj_in_have.get('disable', False): +                    commands.append(delete_interface + ' disable') +            else: +                commands.append(set_interface) +                for item in params: +                    value = w.get(item) +                    if value: +                        if item == 'description': +                            value = "\'" + str(value) + "\'" +                        commands.append(set_interface + ' ' + item + ' ' + str(value)) + +                if disable: +                    commands.append(set_interface + ' disable') +    return commands + + +def map_config_to_obj(module): +    data = get_config(module) +    obj = [] +    for line in data.split('\n'): +        if line.startswith('set interfaces ethernet'): +            match = re.search(r'set interfaces ethernet (\S+)', line, re.M) +            name = match.group(1) +            if name: +                interface = {} +                for item in obj: +                    if item['name'] == name: +                        interface = item +                        break + +                if not interface: +                    interface = {'name': name} +                    obj.append(interface) + +                match = re.search(r'%s (\S+)' % name, line, re.M) +                if match: +                    param = match.group(1) +                    if param == 'description': +                        match = re.search(r'description (.+)', line, re.M) +                        description = match.group(1).strip("'") +                        interface['description'] = description +                    elif param == 'speed': +                        match = re.search(r'speed (\S+)', line, re.M) +                        speed = match.group(1).strip("'") +                        interface['speed'] = speed +                    elif param == 'mtu': +                        match = re.search(r'mtu (\S+)', line, re.M) +                        mtu = match.group(1).strip("'") +                        interface['mtu'] = int(mtu) +                    elif param == 'duplex': +                        match = re.search(r'duplex (\S+)', line, re.M) +                        duplex = match.group(1).strip("'") +                        interface['duplex'] = duplex +                    elif param.strip("'") == 'disable': +                        interface['disable'] = True + +    return obj + + +def map_params_to_obj(module): +    obj = [] +    aggregate = module.params.get('aggregate') +    if aggregate: +        for item in aggregate: +            for key in item: +                if item.get(key) is None: +                    item[key] = module.params[key] + +            d = item.copy() +            if d['enabled']: +                d['disable'] = False +            else: +                d['disable'] = True + +            obj.append(d) +    else: +        params = { +            'name': module.params['name'], +            'description': module.params['description'], +            'speed': module.params['speed'], +            'mtu': module.params['mtu'], +            'duplex': module.params['duplex'], +            'delay': module.params['delay'], +            'state': module.params['state'], +            'neighbors': module.params['neighbors'] +        } + +        if module.params['enabled']: +            params.update({'disable': False}) +        else: +            params.update({'disable': True}) + +        obj.append(params) +    return obj + + +def check_declarative_intent_params(module, want, result): +    failed_conditions = [] +    have_neighbors = None +    for w in want: +        want_state = w.get('state') +        want_neighbors = w.get('neighbors') + +        if want_state not in ('up', 'down') and not want_neighbors: +            continue + +        if result['changed']: +            sleep(w['delay']) + +        command = 'show interfaces ethernet %s' % w['name'] +        rc, out, err = exec_command(module, command) +        if rc != 0: +            module.fail_json(msg=to_text(err, errors='surrogate_then_replace'), command=command, rc=rc) + +        if want_state in ('up', 'down'): +            match = re.search(r'%s (\w+)' % 'state', out, re.M) +            have_state = None +            if match: +                have_state = match.group(1) +            if have_state is None or not conditional(want_state, have_state.strip().lower()): +                failed_conditions.append('state ' + 'eq(%s)' % want_state) + +        if want_neighbors: +            have_host = [] +            have_port = [] +            if have_neighbors is None: +                rc, have_neighbors, err = exec_command(module, 'show lldp neighbors detail') +                if rc != 0: +                    module.fail_json(msg=to_text(err, errors='surrogate_then_replace'), command=command, rc=rc) + +            if have_neighbors: +                lines = have_neighbors.strip().split('Interface: ') +                for line in lines: +                    field = line.split('\n') +                    if field[0].split(',')[0].strip() == w['name']: +                        for item in field: +                            if item.strip().startswith('SysName:'): +                                have_host.append(item.split(':')[1].strip()) +                            if item.strip().startswith('PortDescr:'): +                                have_port.append(item.split(':')[1].strip()) +            for item in want_neighbors: +                host = item.get('host') +                port = item.get('port') +                if host and host not in have_host: +                    failed_conditions.append('host ' + host) +                if port and port not in have_port: +                    failed_conditions.append('port ' + port) + +    return failed_conditions + + +def main(): +    """ main entry point for module execution +    """ +    neighbors_spec = dict( +        host=dict(), +        port=dict() +    ) + +    element_spec = dict( +        name=dict(), +        description=dict(), +        speed=dict(), +        mtu=dict(type='int'), +        duplex=dict(choices=['full', 'half', 'auto']), +        enabled=dict(default=True, type='bool'), +        neighbors=dict(type='list', elements='dict', options=neighbors_spec), +        delay=dict(default=10, type='int'), +        state=dict(default='present', +                   choices=['present', 'absent', 'up', 'down']) +    ) + +    aggregate_spec = deepcopy(element_spec) +    aggregate_spec['name'] = dict(required=True) + +    # remove default in aggregate spec, to handle common arguments +    remove_default_spec(aggregate_spec) + +    argument_spec = dict( +        aggregate=dict(type='list', elements='dict', options=aggregate_spec), +    ) + +    argument_spec.update(element_spec) +    argument_spec.update(vyos_argument_spec) + +    required_one_of = [['name', 'aggregate']] +    mutually_exclusive = [['name', 'aggregate']] + +    required_together = [['speed', 'duplex']] +    module = AnsibleModule(argument_spec=argument_spec, +                           required_one_of=required_one_of, +                           mutually_exclusive=mutually_exclusive, +                           required_together=required_together, +                           supports_check_mode=True) + +    warnings = list() + +    result = {'changed': False} + +    if warnings: +        result['warnings'] = warnings + +    want = map_params_to_obj(module) +    have = map_config_to_obj(module) + +    commands = map_obj_to_commands((want, have)) +    result['commands'] = commands + +    if commands: +        commit = not module.check_mode +        diff = load_config(module, commands, commit=commit) +        if diff: +            if module._diff: +                result['diff'] = {'prepared': diff} +        result['changed'] = True + +    failed_conditions = check_declarative_intent_params(module, want, result) + +    if failed_conditions: +        msg = 'One or more conditional statements have not been satisfied' +        module.fail_json(msg=msg, failed_conditions=failed_conditions) +    module.exit_json(**result) + + +if __name__ == '__main__': +    main() diff --git a/plugins/modules/vyos_l3_interface.py b/plugins/modules/vyos_l3_interface.py new file mode 100644 index 00000000..98fbb4b6 --- /dev/null +++ b/plugins/modules/vyos_l3_interface.py @@ -0,0 +1,285 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017, Ansible by Red Hat, inc +# +# This file is part of Ansible by Red Hat +# +# 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 <http://www.gnu.org/licenses/>. +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', +                    'status': ['preview'], +                    'supported_by': 'network'} + + +DOCUMENTATION = """ +--- +module: vyos_l3_interface +version_added: "2.4" +author: "Ricardo Carrillo Cruz (@rcarrillocruz)" +short_description: Manage L3 interfaces on VyOS network devices +description: +  - This module provides declarative management of L3 interfaces +    on VyOS network devices. +notes: +  - Tested against VYOS 1.1.7 +options: +  name: +    description: +      - Name of the L3 interface. +  ipv4: +    description: +      - IPv4 of the L3 interface. +  ipv6: +    description: +      - IPv6 of the L3 interface. +  aggregate: +    description: List of L3 interfaces definitions +  state: +    description: +      - State of the L3 interface configuration. +    default: present +    choices: ['present', 'absent'] +extends_documentation_fragment: vyos +""" + +EXAMPLES = """ +- name: Set eth0 IPv4 address +  vyos_l3_interface: +    name: eth0 +    ipv4: 192.168.0.1/24 + +- name: Remove eth0 IPv4 address +  vyos_l3_interface: +    name: eth0 +    state: absent + +- name: Set IP addresses on aggregate +  vyos_l3_interface: +    aggregate: +      - { name: eth1, ipv4: 192.168.2.10/24 } +      - { name: eth2, ipv4: 192.168.3.10/24, ipv6: "fd5d:12c9:2201:1::1/64" } + +- name: Remove IP addresses on aggregate +  vyos_l3_interface: +    aggregate: +      - { name: eth1, ipv4: 192.168.2.10/24 } +      - { name: eth2, ipv4: 192.168.3.10/24, ipv6: "fd5d:12c9:2201:1::1/64" } +    state: absent +""" + +RETURN = """ +commands: +  description: The list of configuration mode commands to send to the device +  returned: always, except for the platforms that use Netconf transport to manage the device. +  type: list +  sample: +    - set interfaces ethernet eth0 address '192.168.0.1/24' +""" + +import socket +import re + +from copy import deepcopy + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.common.utils import is_masklen, validate_ip_address +from ansible.module_utils.network.common.utils import remove_default_spec +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import load_config, run_commands +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import vyos_argument_spec + + +def is_ipv4(value): +    if value: +        address = value.split('/') +        if is_masklen(address[1]) and validate_ip_address(address[0]): +            return True +    return False + + +def is_ipv6(value): +    if value: +        address = value.split('/') +        if 0 <= int(address[1]) <= 128: +            try: +                socket.inet_pton(socket.AF_INET6, address[0]) +            except socket.error: +                return False +            return True +    return False + + +def search_obj_in_list(name, lst): +    for o in lst: +        if o['name'] == name: +            return o + +    return None + + +def map_obj_to_commands(updates, module): +    commands = list() +    want, have = updates + +    for w in want: +        name = w['name'] +        ipv4 = w['ipv4'] +        ipv6 = w['ipv6'] +        state = w['state'] + +        obj_in_have = search_obj_in_list(name, have) + +        if state == 'absent' and obj_in_have: +            if not ipv4 and not ipv6 and (obj_in_have['ipv4'] or obj_in_have['ipv6']): +                if name == "lo": +                    commands.append('delete interfaces loopback lo address') +                else: +                    commands.append('delete interfaces ethernet ' + name + ' address') +            else: +                if ipv4 and ipv4 in obj_in_have['ipv4']: +                    if name == "lo": +                        commands.append('delete interfaces loopback lo address ' + ipv4) +                    else: +                        commands.append('delete interfaces ethernet ' + name + ' address ' + ipv4) +                if ipv6 and ipv6 in obj_in_have['ipv6']: +                    if name == "lo": +                        commands.append('delete interfaces loopback lo address ' + ipv6) +                    else: +                        commands.append('delete interfaces ethernet ' + name + ' address ' + ipv6) +        elif (state == 'present' and obj_in_have): +            if ipv4 and ipv4 not in obj_in_have['ipv4']: +                if name == "lo": +                    commands.append('set interfaces loopback lo address ' + ipv4) +                else: +                    commands.append('set interfaces ethernet ' + name + ' address ' + ipv4) + +            if ipv6 and ipv6 not in obj_in_have['ipv6']: +                if name == "lo": +                    commands.append('set interfaces loopback lo address ' + ipv6) +                else: +                    commands.append('set interfaces ethernet ' + name + ' address ' + ipv6) + +    return commands + + +def map_config_to_obj(module): +    obj = [] +    output = run_commands(module, ['show interfaces']) +    lines = re.split(r'\n[e|l]', output[0])[1:] + +    if len(lines) > 0: +        for line in lines: +            splitted_line = line.split() + +            if len(splitted_line) > 0: +                ipv4 = [] +                ipv6 = [] + +                if splitted_line[0].lower().startswith('th'): +                    name = 'e' + splitted_line[0].lower() +                elif splitted_line[0].lower().startswith('o'): +                    name = 'l' + splitted_line[0].lower() + +                for i in splitted_line[1:]: +                    if (('.' in i or ':' in i) and '/' in i): +                        value = i.split(r'\n')[0] +                        if is_ipv4(value): +                            ipv4.append(value) +                        elif is_ipv6(value): +                            ipv6.append(value) + +                obj.append({'name': name, +                            'ipv4': ipv4, +                            'ipv6': ipv6}) + +    return obj + + +def map_params_to_obj(module): +    obj = [] + +    aggregate = module.params.get('aggregate') +    if aggregate: +        for item in aggregate: +            for key in item: +                if item.get(key) is None: +                    item[key] = module.params[key] + +            obj.append(item.copy()) +    else: +        obj.append({ +            'name': module.params['name'], +            'ipv4': module.params['ipv4'], +            'ipv6': module.params['ipv6'], +            'state': module.params['state'] +        }) + +    return obj + + +def main(): +    """ main entry point for module execution +    """ +    element_spec = dict( +        name=dict(), +        ipv4=dict(), +        ipv6=dict(), +        state=dict(default='present', +                   choices=['present', 'absent']) +    ) + +    aggregate_spec = deepcopy(element_spec) +    aggregate_spec['name'] = dict(required=True) + +    # remove default in aggregate spec, to handle common arguments +    remove_default_spec(aggregate_spec) + +    argument_spec = dict( +        aggregate=dict(type='list', elements='dict', options=aggregate_spec), +    ) + +    argument_spec.update(element_spec) +    argument_spec.update(vyos_argument_spec) + +    required_one_of = [['name', 'aggregate']] +    mutually_exclusive = [['name', 'aggregate']] +    module = AnsibleModule(argument_spec=argument_spec, +                           required_one_of=required_one_of, +                           mutually_exclusive=mutually_exclusive, +                           supports_check_mode=True) + +    warnings = list() + +    result = {'changed': False} + +    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: +        commit = not module.check_mode +        load_config(module, commands, commit=commit) +        result['changed'] = True + +    module.exit_json(**result) + + +if __name__ == '__main__': +    main() diff --git a/plugins/modules/vyos_linkagg.py b/plugins/modules/vyos_linkagg.py new file mode 100644 index 00000000..75ffa77d --- /dev/null +++ b/plugins/modules/vyos_linkagg.py @@ -0,0 +1,265 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017, Ansible by Red Hat, inc +# +# This file is part of Ansible by Red Hat +# +# 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 <http://www.gnu.org/licenses/>. +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', +                    'status': ['preview'], +                    'supported_by': 'network'} + + +DOCUMENTATION = """ +--- +module: vyos_linkagg +version_added: "2.4" +author: "Ricardo Carrillo Cruz (@rcarrillocruz)" +short_description: Manage link aggregation groups on VyOS network devices +description: +  - This module provides declarative management of link aggregation groups +    on VyOS network devices. +notes: +  - Tested against VYOS 1.1.7 +options: +  name: +    description: +      - Name of the link aggregation group. +    required: true +  mode: +    description: +      - Mode of the link aggregation group. +    choices: ['802.3ad', 'active-backup', 'broadcast', +              'round-robin', 'transmit-load-balance', +              'adaptive-load-balance', 'xor-hash', 'on'] +  members: +    description: +      - List of members of the link aggregation group. +  aggregate: +    description: List of link aggregation definitions. +  state: +    description: +      - State of the link aggregation group. +    default: present +    choices: ['present', 'absent', 'up', 'down'] +extends_documentation_fragment: vyos +""" + +EXAMPLES = """ +- name: configure link aggregation group +  vyos_linkagg: +    name: bond0 +    members: +      - eth0 +      - eth1 + +- name: remove configuration +  vyos_linkagg: +    name: bond0 +    state: absent + +- name: Create aggregate of linkagg definitions +  vyos_linkagg: +    aggregate: +        - { name: bond0, members: [eth1] } +        - { name: bond1, members: [eth2] } + +- name: Remove aggregate of linkagg definitions +  vyos_linkagg: +    aggregate: +      - name: bond0 +      - name: bond1 +    state: absent +""" + +RETURN = """ +commands: +  description: The list of configuration mode commands to send to the device +  returned: always, except for the platforms that use Netconf transport to manage the device. +  type: list +  sample: +    - set interfaces bonding bond0 +    - set interfaces ethernet eth0 bond-group 'bond0' +    - set interfaces ethernet eth1 bond-group 'bond0' +""" +from copy import deepcopy + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.common.utils import remove_default_spec +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import load_config, run_commands +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import vyos_argument_spec + + +def search_obj_in_list(name, lst): +    for o in lst: +        if o['name'] == name: +            return o + +    return None + + +def map_obj_to_commands(updates, module): +    commands = list() +    want, have = updates + +    for w in want: +        name = w['name'] +        members = w.get('members') or [] +        mode = w['mode'] + +        if mode == 'on': +            mode = '802.3ad' + +        state = w['state'] + +        obj_in_have = search_obj_in_list(name, have) + +        if state == 'absent': +            if obj_in_have: +                for m in obj_in_have['members']: +                    commands.append('delete interfaces ethernet ' + m + ' bond-group') + +                commands.append('delete interfaces bonding ' + name) +        else: +            if not obj_in_have: +                commands.append('set interfaces bonding ' + name + ' mode ' + mode) + +                for m in members: +                    commands.append('set interfaces ethernet ' + m + ' bond-group ' + name) + +                if state == 'down': +                    commands.append('set interfaces bonding ' + name + ' disable') +            else: +                if mode != obj_in_have['mode']: +                    commands.append('set interfaces bonding ' + name + ' mode ' + mode) + +                missing_members = list(set(members) - set(obj_in_have['members'])) +                for m in missing_members: +                    commands.append('set interfaces ethernet ' + m + ' bond-group ' + name) + +                if state == 'down' and obj_in_have['state'] == 'up': +                    commands.append('set interfaces bonding ' + name + ' disable') +                elif state == 'up' and obj_in_have['state'] == 'down': +                    commands.append('delete interfaces bonding ' + name + ' disable') + +    return commands + + +def map_config_to_obj(module): +    obj = [] +    output = run_commands(module, ['show interfaces bonding slaves']) +    lines = output[0].splitlines() + +    if len(lines) > 1: +        for line in lines[1:]: +            splitted_line = line.split() + +            name = splitted_line[0] +            mode = splitted_line[1] +            state = splitted_line[2] + +            if len(splitted_line) > 4: +                members = splitted_line[4:] +            else: +                members = [] + +            obj.append({'name': name, +                        'mode': mode, +                        'members': members, +                        'state': state}) + +    return obj + + +def map_params_to_obj(module): +    obj = [] +    aggregate = module.params.get('aggregate') +    if aggregate: +        for item in aggregate: +            for key in item: +                if item.get(key) is None: +                    item[key] = module.params[key] + +            obj.append(item.copy()) +    else: +        obj.append({ +            'name': module.params['name'], +            'mode': module.params['mode'], +            'members': module.params['members'], +            'state': module.params['state'] +        }) + +    return obj + + +def main(): +    """ main entry point for module execution +    """ +    element_spec = dict( +        name=dict(), +        mode=dict(choices=['802.3ad', 'active-backup', 'broadcast', +                           'round-robin', 'transmit-load-balance', +                           'adaptive-load-balance', 'xor-hash', 'on'], +                  default='802.3ad'), +        members=dict(type='list'), +        state=dict(default='present', +                   choices=['present', 'absent', 'up', 'down']) +    ) + +    aggregate_spec = deepcopy(element_spec) +    aggregate_spec['name'] = dict(required=True) + +    # remove default in aggregate spec, to handle common arguments +    remove_default_spec(aggregate_spec) + +    argument_spec = dict( +        aggregate=dict(type='list', elements='dict', options=aggregate_spec), +    ) + +    argument_spec.update(element_spec) +    argument_spec.update(vyos_argument_spec) + +    required_one_of = [['name', 'aggregate']] +    mutually_exclusive = [['name', 'aggregate']] +    module = AnsibleModule(argument_spec=argument_spec, +                           required_one_of=required_one_of, +                           mutually_exclusive=mutually_exclusive, +                           supports_check_mode=True) + +    warnings = list() + +    result = {'changed': False} + +    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: +        commit = not module.check_mode +        load_config(module, commands, commit=commit) +        result['changed'] = True + +    module.exit_json(**result) + + +if __name__ == '__main__': +    main() diff --git a/plugins/modules/vyos_lldp.py b/plugins/modules/vyos_lldp.py new file mode 100644 index 00000000..213e2ac5 --- /dev/null +++ b/plugins/modules/vyos_lldp.py @@ -0,0 +1,121 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017, Ansible by Red Hat, inc +# +# This file is part of Ansible by Red Hat +# +# 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 <http://www.gnu.org/licenses/>. +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', +                    'status': ['preview'], +                    'supported_by': 'network'} + + +DOCUMENTATION = """ +--- +module: vyos_lldp +version_added: "2.4" +author: "Ricardo Carrillo Cruz (@rcarrillocruz)" +short_description: Manage LLDP configuration on VyOS network devices +description: +  - This module provides declarative management of LLDP service +    on VyOS network devices. +notes: +  - Tested against VYOS 1.1.7 +options: +  state: +    description: +      - State of the LLDP configuration. +    default: present +    choices: ['present', 'absent'] +extends_documentation_fragment: vyos +""" + +EXAMPLES = """ +- name: Enable LLDP service +  vyos_lldp: +    state: present + +- name: Disable LLDP service +  vyos_lldp: +    state: absent +""" + +RETURN = """ +commands: +  description: The list of configuration mode commands to send to the device +  returned: always, except for the platforms that use Netconf transport to manage the device. +  type: list +  sample: +    - set service lldp +""" +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_config, load_config +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import vyos_argument_spec + + +def has_lldp(module): +    config = get_config(module).splitlines() + +    if "set service 'lldp'" in config or 'set service lldp' in config: +        return True +    else: +        return False + + +def main(): +    """ main entry point for module execution +    """ +    argument_spec = dict( +        interfaces=dict(type='list'), +        state=dict(default='present', +                   choices=['present', 'absent', +                            'enabled', 'disabled']) +    ) + +    argument_spec.update(vyos_argument_spec) + +    module = AnsibleModule(argument_spec=argument_spec, +                           supports_check_mode=True) + +    warnings = list() + +    result = {'changed': False} + +    if warnings: +        result['warnings'] = warnings + +    HAS_LLDP = has_lldp(module) + +    commands = [] + +    if module.params['state'] == 'absent' and HAS_LLDP: +        commands.append('delete service lldp') +    elif module.params['state'] == 'present' and not HAS_LLDP: +        commands.append('set service lldp') + +    result['commands'] = commands + +    if commands: +        commit = not module.check_mode +        load_config(module, commands, commit=commit) +        result['changed'] = True + +    module.exit_json(**result) + + +if __name__ == '__main__': +    main() diff --git a/plugins/modules/vyos_lldp_interface.py b/plugins/modules/vyos_lldp_interface.py new file mode 100644 index 00000000..f0cad833 --- /dev/null +++ b/plugins/modules/vyos_lldp_interface.py @@ -0,0 +1,228 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017, Ansible by Red Hat, inc +# +# This file is part of Ansible by Red Hat +# +# 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 <http://www.gnu.org/licenses/>. +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', +                    'status': ['preview'], +                    'supported_by': 'network'} + + +DOCUMENTATION = """ +--- +module: vyos_lldp_interface +version_added: "2.4" +author: "Ricardo Carrillo Cruz (@rcarrillocruz)" +short_description: Manage LLDP interfaces configuration on VyOS network devices +description: +  - This module provides declarative management of LLDP interfaces +    configuration on VyOS network devices. +notes: +  - Tested against VYOS 1.1.7 +options: +  name: +    description: +      - Name of the interface LLDP should be configured on. +  aggregate: +    description: List of interfaces LLDP should be configured on. +  state: +    description: +      - State of the LLDP configuration. +    default: present +    choices: ['present', 'absent', 'enabled', 'disabled'] +extends_documentation_fragment: vyos +""" + +EXAMPLES = """ +- name: Enable LLDP on eth1 +  net_lldp_interface: +    state: present + +- name: Enable LLDP on specific interfaces +  net_lldp_interface: +    interfaces: +      - eth1 +      - eth2 +    state: present + +- name: Disable LLDP globally +  net_lldp_interface: +    state: disabled + +- name: Create aggregate of LLDP interface configurations +  vyos_lldp_interface: +    aggregate: +    - name: eth1 +    - name: eth2 +    state: present + +- name: Delete aggregate of LLDP interface configurations +  vyos_lldp_interface: +    aggregate: +    - name: eth1 +    - name: eth2 +    state: absent +""" + +RETURN = """ +commands: +  description: The list of configuration mode commands to send to the device +  returned: always, except for the platforms that use Netconf transport to manage the device. +  type: list +  sample: +    - set service lldp eth1 +    - set service lldp eth2 disable +""" +from copy import deepcopy + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.common.utils import remove_default_spec +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_config, load_config +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import vyos_argument_spec + + +def search_obj_in_list(name, lst): +    for o in lst: +        if o['name'] == name: +            return o + +    return None + + +def map_obj_to_commands(updates, module): +    commands = list() +    want, have = updates + +    for w in want: +        name = w['name'] +        state = w['state'] + +        obj_in_have = search_obj_in_list(name, have) + +        if state == 'absent' and obj_in_have: +            commands.append('delete service lldp interface ' + name) +        elif state in ('present', 'enabled'): +            if not obj_in_have: +                commands.append('set service lldp interface ' + name) +            elif obj_in_have and obj_in_have['state'] == 'disabled' and state == 'enabled': +                commands.append('delete service lldp interface ' + name + ' disable') +        elif state == 'disabled': +            if not obj_in_have: +                commands.append('set service lldp interface ' + name) +                commands.append('set service lldp interface ' + name + ' disable') +            elif obj_in_have and obj_in_have['state'] != 'disabled': +                commands.append('set service lldp interface ' + name + ' disable') + +    return commands + + +def map_config_to_obj(module): +    obj = [] +    config = get_config(module).splitlines() + +    output = [c for c in config if c.startswith("set service lldp interface")] + +    for i in output: +        splitted_line = i.split() + +        if len(splitted_line) > 5: +            new_obj = {'name': splitted_line[4]} + +            if splitted_line[5] == "'disable'": +                new_obj['state'] = 'disabled' +        else: +            new_obj = {'name': splitted_line[4][1:-1]} +            new_obj['state'] = 'present' + +        obj.append(new_obj) + +    return obj + + +def map_params_to_obj(module): +    obj = [] + +    aggregate = module.params.get('aggregate') +    if aggregate: +        for item in aggregate: +            for key in item: +                if item.get(key) is None: +                    item[key] = module.params[key] + +            obj.append(item.copy()) +    else: +        obj.append({'name': module.params['name'], 'state': module.params['state']}) + +    return obj + + +def main(): +    """ main entry point for module execution +    """ +    element_spec = dict( +        name=dict(), +        state=dict(default='present', +                   choices=['present', 'absent', +                            'enabled', 'disabled']) +    ) + +    aggregate_spec = deepcopy(element_spec) +    aggregate_spec['name'] = dict(required=True) + +    # remove default in aggregate spec, to handle common arguments +    remove_default_spec(aggregate_spec) + +    argument_spec = dict( +        aggregate=dict(type='list', elements='dict', options=aggregate_spec), +    ) + +    argument_spec.update(element_spec) +    argument_spec.update(vyos_argument_spec) + +    required_one_of = [['name', 'aggregate']] +    mutually_exclusive = [['name', 'aggregate']] + +    module = AnsibleModule(argument_spec=argument_spec, +                           required_one_of=required_one_of, +                           mutually_exclusive=mutually_exclusive, +                           supports_check_mode=True) + +    warnings = list() + +    result = {'changed': False} + +    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: +        commit = not module.check_mode +        load_config(module, commands, commit=commit) +        result['changed'] = True + +    module.exit_json(**result) + + +if __name__ == '__main__': +    main() diff --git a/plugins/modules/vyos_logging.py b/plugins/modules/vyos_logging.py new file mode 100644 index 00000000..e7be3957 --- /dev/null +++ b/plugins/modules/vyos_logging.py @@ -0,0 +1,263 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017, Ansible by Red Hat, inc +# +# This file is part of Ansible by Red Hat +# +# 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 <http://www.gnu.org/licenses/>. +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', +                    'status': ['preview'], +                    'supported_by': 'network'} + +DOCUMENTATION = """ +--- +module: vyos_logging +version_added: "2.4" +author: "Trishna Guha (@trishnaguha)" +short_description: Manage logging on network devices +description: +  - This module provides declarative management of logging +    on Vyatta Vyos devices. +notes: +  - Tested against VYOS 1.1.7 +options: +  dest: +    description: +      - Destination of the logs. +    choices: ['console', 'file', 'global', 'host', 'user'] +  name: +    description: +      - If value of C(dest) is I(file) it indicates file-name, +        for I(user) it indicates username and for I(host) indicates +        the host name to be notified. +  facility: +    description: +      - Set logging facility. +  level: +    description: +      - Set logging severity levels. +  aggregate: +    description: List of logging definitions. +  state: +    description: +      - State of the logging configuration. +    default: present +    choices: ['present', 'absent'] +extends_documentation_fragment: vyos +""" + +EXAMPLES = """ +- name: configure console logging +  vyos_logging: +    dest: console +    facility: all +    level: crit + +- name: remove console logging configuration +  vyos_logging: +    dest: console +    state: absent + +- name: configure file logging +  vyos_logging: +    dest: file +    name: test +    facility: local3 +    level: err + +- name: Add logging aggregate +  vyos_logging: +    aggregate: +      - { dest: file, name: test1, facility: all, level: info } +      - { dest: file, name: test2, facility: news, level: debug } +    state: present + +- name: Remove logging aggregate +  vyos_logging: +    aggregate: +      - { dest: console, facility: all, level: info } +      - { dest: console, facility: daemon, level: warning } +      - { dest: file, name: test2, facility: news, level: debug } +    state: absent +""" + +RETURN = """ +commands: +  description: The list of configuration mode commands to send to the device +  returned: always +  type: list +  sample: +    - set system syslog global facility all level notice +""" + +import re + +from copy import deepcopy + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.common.utils import remove_default_spec +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_config, load_config +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import vyos_argument_spec + + +def spec_to_commands(updates, module): +    commands = list() +    want, have = updates + +    for w in want: +        dest = w['dest'] +        name = w['name'] +        facility = w['facility'] +        level = w['level'] +        state = w['state'] +        del w['state'] + +        if state == 'absent' and w in have: +            if w['name']: +                commands.append('delete system syslog {0} {1} facility {2} level {3}'.format( +                    dest, name, facility, level)) +            else: +                commands.append('delete system syslog {0} facility {1} level {2}'.format( +                    dest, facility, level)) +        elif state == 'present' and w not in have: +            if w['name']: +                commands.append('set system syslog {0} {1} facility {2} level {3}'.format( +                    dest, name, facility, level)) +            else: +                commands.append('set system syslog {0} facility {1} level {2}'.format( +                    dest, facility, level)) + +    return commands + + +def config_to_dict(module): +    data = get_config(module) +    obj = [] + +    for line in data.split('\n'): +        if line.startswith('set system syslog'): +            match = re.search(r'set system syslog (\S+)', line, re.M) +            dest = match.group(1) +            if dest == 'host': +                match = re.search(r'host (\S+)', line, re.M) +                name = match.group(1) +            elif dest == 'file': +                match = re.search(r'file (\S+)', line, re.M) +                name = match.group(1) +            elif dest == 'user': +                match = re.search(r'user (\S+)', line, re.M) +                name = match.group(1) +            else: +                name = None + +            if 'facility' in line: +                match = re.search(r'facility (\S+)', line, re.M) +                facility = match.group(1) +            if 'level' in line: +                match = re.search(r'level (\S+)', line, re.M) +                level = match.group(1).strip("'") + +                obj.append({'dest': dest, +                            'name': name, +                            'facility': facility, +                            'level': level}) + +    return obj + + +def map_params_to_obj(module, required_if=None): +    obj = [] + +    aggregate = module.params.get('aggregate') +    if aggregate: +        for item in aggregate: +            for key in item: +                if item.get(key) is None: +                    item[key] = module.params[key] + +            module._check_required_if(required_if, item) +            obj.append(item.copy()) + +    else: +        if module.params['dest'] not in ('host', 'file', 'user'): +            module.params['name'] = None + +        obj.append({ +            'dest': module.params['dest'], +            'name': module.params['name'], +            'facility': module.params['facility'], +            'level': module.params['level'], +            'state': module.params['state'] +        }) + +    return obj + + +def main(): +    """ main entry point for module execution +    """ +    element_spec = dict( +        dest=dict(type='str', choices=['console', 'file', 'global', 'host', 'user']), +        name=dict(type='str'), +        facility=dict(type='str'), +        level=dict(type='str'), +        state=dict(default='present', choices=['present', 'absent']), +    ) + +    aggregate_spec = deepcopy(element_spec) + +    # remove default in aggregate spec, to handle common arguments +    remove_default_spec(aggregate_spec) + +    argument_spec = dict( +        aggregate=dict(type='list', elements='dict', options=aggregate_spec), +    ) + +    argument_spec.update(element_spec) + +    argument_spec.update(vyos_argument_spec) +    required_if = [('dest', 'host', ['name', 'facility', 'level']), +                   ('dest', 'file', ['name', 'facility', 'level']), +                   ('dest', 'user', ['name', 'facility', 'level']), +                   ('dest', 'console', ['facility', 'level']), +                   ('dest', 'global', ['facility', 'level'])] + +    module = AnsibleModule(argument_spec=argument_spec, +                           required_if=required_if, +                           supports_check_mode=True) + +    warnings = list() + +    result = {'changed': False} +    if warnings: +        result['warnings'] = warnings +    want = map_params_to_obj(module, required_if=required_if) +    have = config_to_dict(module) + +    commands = spec_to_commands((want, have), module) +    result['commands'] = commands + +    if commands: +        commit = not module.check_mode +        load_config(module, commands, commit=commit) +        result['changed'] = True + +    module.exit_json(**result) + + +if __name__ == '__main__': +    main() diff --git a/plugins/modules/vyos_ping.py b/plugins/modules/vyos_ping.py new file mode 100644 index 00000000..fe7bd9d2 --- /dev/null +++ b/plugins/modules/vyos_ping.py @@ -0,0 +1,246 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017, Ansible by Red Hat, inc +# +# This file is part of Ansible by Red Hat +# +# 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 <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', +                    'status': ['preview'], +                    'supported_by': 'community'} + +DOCUMENTATION = """ +--- +module: vyos_ping +short_description: Tests reachability using ping from VyOS network devices +description: +  - Tests reachability using ping from a VyOS device to a remote destination. +  - Tested against VyOS 1.1.8 (helium) +  - For a general purpose network module, see the M(net_ping) module. +  - For Windows targets, use the M(win_ping) module instead. +  - For targets running Python, use the M(ping) module instead. +author: +  - Nilashish Chakraborty (@nilashishc) +version_added: '2.8' +options: +  dest: +    description: +      - The IP Address or hostname (resolvable by the device) of the remote node. +    required: true +  count: +    description: +      - Number of packets to send to check reachability. +    type: int +    default: 5 +  source: +    description: +      - The source interface or IP Address to use while sending the ping packet(s). +  ttl: +    description: +      - The time-to-live value for the ICMP packet(s). +    type: int +  size: +    description: +      - Determines the size (in bytes) of the ping packet(s). +    type: int +  interval: +    description: +      - Determines the interval (in seconds) between consecutive pings. +    type: int +  state: +    description: +      - Determines if the expected result is success or fail. +    choices: [ absent, present ] +    default: present +notes: +  - For a general purpose network module, see the M(net_ping) module. +  - For Windows targets, use the M(win_ping) module instead. +  - For targets running Python, use the M(ping) module instead. +extends_documentation_fragment: vyos +""" + +EXAMPLES = """ +- name: Test reachability to 10.10.10.10 +  vyos_ping: +    dest: 10.10.10.10 + +- name: Test reachability to 10.20.20.20 using source and ttl set +  vyos_ping: +    dest: 10.20.20.20 +    source: eth0 +    ttl: 128 + +- name: Test unreachability to 10.30.30.30 using interval +  vyos_ping: +    dest: 10.30.30.30 +    interval: 3 +    state: absent + +- name: Test reachability to 10.40.40.40 setting count and source +  vyos_ping: +    dest: 10.40.40.40 +    source: eth1 +    count: 20 +    size: 512 +""" + +RETURN = """ +commands: +  description: List of commands sent. +  returned: always +  type: list +  sample: ["ping 10.8.38.44 count 10 interface eth0 ttl 128"] +packet_loss: +  description: Percentage of packets lost. +  returned: always +  type: str +  sample: "0%" +packets_rx: +  description: Packets successfully received. +  returned: always +  type: int +  sample: 20 +packets_tx: +  description: Packets successfully transmitted. +  returned: always +  type: int +  sample: 20 +rtt: +  description: The round trip time (RTT) stats. +  returned: when ping succeeds +  type: dict +  sample: {"avg": 2, "max": 8, "min": 1, "mdev": 24} +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import run_commands +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import vyos_argument_spec +import re + + +def main(): +    """ main entry point for module execution +    """ +    argument_spec = dict( +        count=dict(type="int", default=5), +        dest=dict(type="str", required=True), +        source=dict(type="str"), +        ttl=dict(type='int'), +        size=dict(type='int'), +        interval=dict(type='int'), +        state=dict(type="str", choices=["absent", "present"], default="present"), +    ) + +    argument_spec.update(vyos_argument_spec) + +    module = AnsibleModule(argument_spec=argument_spec) + +    count = module.params["count"] +    dest = module.params["dest"] +    source = module.params["source"] +    size = module.params["size"] +    ttl = module.params["ttl"] +    interval = module.params["interval"] + +    warnings = list() + +    results = {} +    if warnings: +        results["warnings"] = warnings + +    results["commands"] = [build_ping(dest, count, size, interval, source, ttl)] + +    ping_results = run_commands(module, commands=results["commands"]) +    ping_results_list = ping_results[0].split("\n") + +    rtt_info, rate_info = None, None +    for line in ping_results_list: +        if line.startswith('rtt'): +            rtt_info = line +        if line.startswith('%s packets transmitted' % count): +            rate_info = line + +    if rtt_info: +        rtt = parse_rtt(rtt_info) +        for k, v in rtt.items(): +            if rtt[k] is not None: +                rtt[k] = int(v) +        results["rtt"] = rtt + +    pkt_loss, rx, tx = parse_rate(rate_info) +    results["packet_loss"] = str(pkt_loss) + "%" +    results["packets_rx"] = int(rx) +    results["packets_tx"] = int(tx) + +    validate_results(module, pkt_loss, results) + +    module.exit_json(**results) + + +def build_ping(dest, count, size=None, interval=None, source=None, ttl=None): +    cmd = "ping {0} count {1}".format(dest, str(count)) + +    if source: +        cmd += " interface {0}".format(source) + +    if ttl: +        cmd += " ttl {0}".format(str(ttl)) + +    if size: +        cmd += " size {0}".format(str(size)) + +    if interval: +        cmd += " interval {0}".format(str(interval)) + +    return cmd + + +def parse_rate(rate_info): +    rate_re = re.compile( +        r"(?P<tx>\d+) (?:\w+) (?:\w+), (?P<rx>\d+) (?:\w+), (?P<pkt_loss>\d+)% (?:\w+) (?:\w+), (?:\w+) (?P<time>\d+)") +    rate_err_re = re.compile( +        r"(?P<tx>\d+) (?:\w+) (?:\w+), (?P<rx>\d+) (?:\w+), (?:[+-])(?P<err>\d+) (?:\w+), (?P<pkt_loss>\d+)% (?:\w+) (?:\w+), (?:\w+) (?P<time>\d+)") + +    if rate_re.match(rate_info): +        rate = rate_re.match(rate_info) +    elif rate_err_re.match(rate_info): +        rate = rate_err_re.match(rate_info) + +    return rate.group("pkt_loss"), rate.group("rx"), rate.group("tx") + + +def parse_rtt(rtt_info): +    rtt_re = re.compile( +        r"rtt (?:.*)=(?:\s*)(?P<min>\d*).(?:\d*)/(?P<avg>\d*).(?:\d*)/(?P<max>\d+).(?:\d*)/(?P<mdev>\d*)") +    rtt = rtt_re.match(rtt_info) + +    return rtt.groupdict() + + +def validate_results(module, loss, results): +    state = module.params["state"] +    if state == "present" and int(loss) == 100: +        module.fail_json(msg="Ping failed unexpectedly", **results) +    elif state == "absent" and int(loss) < 100: +        module.fail_json(msg="Ping succeeded unexpectedly", **results) + + +if __name__ == "__main__": +    main() diff --git a/plugins/modules/vyos_static_route.py b/plugins/modules/vyos_static_route.py new file mode 100644 index 00000000..ec1c6c95 --- /dev/null +++ b/plugins/modules/vyos_static_route.py @@ -0,0 +1,265 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017, Ansible by Red Hat, inc +# +# This file is part of Ansible by Red Hat +# +# 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 <http://www.gnu.org/licenses/>. +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', +                    'status': ['preview'], +                    'supported_by': 'network'} + + +DOCUMENTATION = """ +--- +module: vyos_static_route +version_added: "2.4" +author: "Trishna Guha (@trishnaguha)" +short_description: Manage static IP routes on Vyatta VyOS network devices +description: +  - This module provides declarative management of static +    IP routes on Vyatta VyOS network devices. +notes: +  - Tested against VYOS 1.1.7 +options: +  prefix: +    description: +      - Network prefix of the static route. +        C(mask) param should be ignored if C(prefix) is provided +        with C(mask) value C(prefix/mask). +  mask: +    description: +      - Network prefix mask of the static route. +  next_hop: +    description: +      - Next hop IP of the static route. +  admin_distance: +    description: +      - Admin distance of the static route. +  aggregate: +    description: List of static route definitions +  state: +    description: +      - State of the static route configuration. +    default: present +    choices: ['present', 'absent'] +extends_documentation_fragment: vyos +""" + +EXAMPLES = """ +- name: configure static route +  vyos_static_route: +    prefix: 192.168.2.0 +    mask: 24 +    next_hop: 10.0.0.1 + +- name: configure static route prefix/mask +  vyos_static_route: +    prefix: 192.168.2.0/16 +    next_hop: 10.0.0.1 + +- name: remove configuration +  vyos_static_route: +    prefix: 192.168.2.0 +    mask: 16 +    next_hop: 10.0.0.1 +    state: absent + +- name: configure aggregates of static routes +  vyos_static_route: +    aggregate: +      - { prefix: 192.168.2.0, mask: 24, next_hop: 10.0.0.1 } +      - { prefix: 192.168.3.0, mask: 16, next_hop: 10.0.2.1 } +      - { prefix: 192.168.3.0/16, next_hop: 10.0.2.1 } + +- name: Remove static route collections +  vyos_static_route: +    aggregate: +      - { prefix: 172.24.1.0/24, next_hop: 192.168.42.64 } +      - { prefix: 172.24.3.0/24, next_hop: 192.168.42.64 } +    state: absent +""" + +RETURN = """ +commands: +  description: The list of configuration mode commands to send to the device +  returned: always +  type: list +  sample: +    - set protocols static route 192.168.2.0/16 next-hop 10.0.0.1 +""" +import re + +from copy import deepcopy + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.common.utils import remove_default_spec +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_config, load_config +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import vyos_argument_spec + + +def spec_to_commands(updates, module): +    commands = list() +    want, have = updates +    for w in want: +        prefix = w['prefix'] +        mask = w['mask'] +        next_hop = w['next_hop'] +        admin_distance = w['admin_distance'] +        state = w['state'] +        del w['state'] + +        if state == 'absent' and w in have: +            commands.append('delete protocols static route %s/%s' % (prefix, mask)) +        elif state == 'present' and w not in have: +            cmd = 'set protocols static route %s/%s next-hop %s' % (prefix, mask, next_hop) +            if admin_distance != 'None': +                cmd += ' distance %s' % (admin_distance) +            commands.append(cmd) + +    return commands + + +def config_to_dict(module): +    data = get_config(module) +    obj = [] + +    for line in data.split('\n'): +        if line.startswith('set protocols static route'): +            match = re.search(r'static route (\S+)', line, re.M) +            prefix = match.group(1).split('/')[0] +            mask = match.group(1).split('/')[1] +            if 'next-hop' in line: +                match_hop = re.search(r'next-hop (\S+)', line, re.M) +                next_hop = match_hop.group(1).strip("'") + +                match_distance = re.search(r'distance (\S+)', line, re.M) +                if match_distance is not None: +                    admin_distance = match_distance.group(1)[1:-1] +                else: +                    admin_distance = None + +                if admin_distance is not None: +                    obj.append({'prefix': prefix, +                                'mask': mask, +                                'next_hop': next_hop, +                                'admin_distance': admin_distance}) +                else: +                    obj.append({'prefix': prefix, +                                'mask': mask, +                                'next_hop': next_hop, +                                'admin_distance': 'None'}) + +    return obj + + +def map_params_to_obj(module, required_together=None): +    obj = [] +    aggregate = module.params.get('aggregate') +    if aggregate: +        for item in aggregate: +            for key in item: +                if item.get(key) is None: +                    item[key] = module.params[key] + +            module._check_required_together(required_together, item) +            d = item.copy() +            if '/' in d['prefix']: +                d['mask'] = d['prefix'].split('/')[1] +                d['prefix'] = d['prefix'].split('/')[0] + +            if 'admin_distance' in d: +                d['admin_distance'] = str(d['admin_distance']) + +            obj.append(d) +    else: +        prefix = module.params['prefix'].strip() +        if '/' in prefix: +            mask = prefix.split('/')[1] +            prefix = prefix.split('/')[0] +        else: +            mask = module.params['mask'].strip() +        next_hop = module.params['next_hop'].strip() +        admin_distance = str(module.params['admin_distance']) +        state = module.params['state'] + +        obj.append({ +            'prefix': prefix, +            'mask': mask, +            'next_hop': next_hop, +            'admin_distance': admin_distance, +            'state': state +        }) + +    return obj + + +def main(): +    """ main entry point for module execution +    """ +    element_spec = dict( +        prefix=dict(type='str'), +        mask=dict(type='str'), +        next_hop=dict(type='str'), +        admin_distance=dict(type='int'), +        state=dict(default='present', choices=['present', 'absent']) +    ) + +    aggregate_spec = deepcopy(element_spec) +    aggregate_spec['prefix'] = dict(required=True) + +    # remove default in aggregate spec, to handle common arguments +    remove_default_spec(aggregate_spec) + +    argument_spec = dict( +        aggregate=dict(type='list', elements='dict', options=aggregate_spec), +    ) + +    argument_spec.update(element_spec) +    argument_spec.update(vyos_argument_spec) + +    required_one_of = [['aggregate', 'prefix']] +    required_together = [['prefix', 'next_hop']] +    mutually_exclusive = [['aggregate', 'prefix']] + +    module = AnsibleModule(argument_spec=argument_spec, +                           required_one_of=required_one_of, +                           required_together=required_together, +                           mutually_exclusive=mutually_exclusive, +                           supports_check_mode=True) + +    warnings = list() + +    result = {'changed': False} +    if warnings: +        result['warnings'] = warnings +    want = map_params_to_obj(module, required_together=required_together) +    have = config_to_dict(module) + +    commands = spec_to_commands((want, have), module) +    result['commands'] = commands + +    if commands: +        commit = not module.check_mode +        load_config(module, commands, commit=commit) +        result['changed'] = True + +    module.exit_json(**result) + + +if __name__ == '__main__': +    main() diff --git a/plugins/modules/vyos_system.py b/plugins/modules/vyos_system.py new file mode 100644 index 00000000..f1f093d9 --- /dev/null +++ b/plugins/modules/vyos_system.py @@ -0,0 +1,211 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# 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 <http://www.gnu.org/licenses/>. +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', +                    'status': ['preview'], +                    'supported_by': 'network'} + + +DOCUMENTATION = """ +--- +module: "vyos_system" +version_added: "2.3" +author: "Nathaniel Case (@qalthos)" +short_description: Run `set system` commands on VyOS devices +description: +  - Runs one or more commands on remote devices running VyOS. +    This module can also be introspected to validate key parameters before +    returning successfully. +extends_documentation_fragment: vyos +notes: +  - Tested against VYOS 1.1.7 +options: +  host_name: +    description: +      - Configure the device hostname parameter. This option takes an ASCII string value. +  domain_name: +    description: +      - The new domain name to apply to the device. +  name_servers: +    description: +      - A list of name servers to use with the device. Mutually exclusive with +        I(domain_search) +    aliases: ['name_server'] +  domain_search: +    description: +      - A list of domain names to search. Mutually exclusive with +        I(name_server) +  state: +    description: +      - Whether to apply (C(present)) or remove (C(absent)) the settings. +    default: present +    choices: ['present', 'absent'] +""" + +RETURN = """ +commands: +  description: The list of configuration mode commands to send to the device +  returned: always +  type: list +  sample: +    - set system hostname vyos01 +    - set system domain-name foo.example.com +""" + +EXAMPLES = """ +- name: configure hostname and domain-name +  vyos_system: +    host_name: vyos01 +    domain_name: test.example.com + +- name: remove all configuration +  vyos_system: +    state: absent + +- name: configure name servers +  vyos_system: +    name_servers +      - 8.8.8.8 +      - 8.8.4.4 + +- name: configure domain search suffixes +  vyos_system: +    domain_search: +      - sub1.example.com +      - sub2.example.com +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_config, load_config +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import vyos_argument_spec + + +def spec_key_to_device_key(key): +    device_key = key.replace('_', '-') + +    # domain-search is longer than just it's key +    if device_key == 'domain-search': +        device_key += ' domain' + +    return device_key + + +def config_to_dict(module): +    data = get_config(module) + +    config = {'domain_search': [], 'name_server': []} + +    for line in data.split('\n'): +        if line.startswith('set system host-name'): +            config['host_name'] = line[22:-1] +        elif line.startswith('set system domain-name'): +            config['domain_name'] = line[24:-1] +        elif line.startswith('set system domain-search domain'): +            config['domain_search'].append(line[33:-1]) +        elif line.startswith('set system name-server'): +            config['name_server'].append(line[24:-1]) + +    return config + + +def spec_to_commands(want, have): +    commands = [] + +    state = want.pop('state') + +    # state='absent' by itself has special meaning +    if state == 'absent' and all(v is None for v in want.values()): +        # Clear everything +        for key in have: +            commands.append('delete system %s' % spec_key_to_device_key(key)) + +    for key in want: +        if want[key] is None: +            continue + +        current = have.get(key) +        proposed = want[key] +        device_key = spec_key_to_device_key(key) + +        # These keys are lists which may need to  be reconciled with the device +        if key in ['domain_search', 'name_server']: +            if not proposed: +                # Empty list was passed, delete all values +                commands.append("delete system %s" % device_key) +            for config in proposed: +                if state == 'absent' and config in current: +                    commands.append("delete system %s '%s'" % (device_key, config)) +                elif state == 'present' and config not in current: +                    commands.append("set system %s '%s'" % (device_key, config)) +        else: +            if state == 'absent' and current and proposed: +                commands.append('delete system %s' % device_key) +            elif state == 'present' and proposed and proposed != current: +                commands.append("set system %s '%s'" % (device_key, proposed)) + +    return commands + + +def map_param_to_obj(module): +    return { +        'host_name': module.params['host_name'], +        'domain_name': module.params['domain_name'], +        'domain_search': module.params['domain_search'], +        'name_server': module.params['name_server'], +        'state': module.params['state'] +    } + + +def main(): +    argument_spec = dict( +        host_name=dict(type='str'), +        domain_name=dict(type='str'), +        domain_search=dict(type='list'), +        name_server=dict(type='list', aliases=['name_servers']), +        state=dict(type='str', default='present', choices=['present', 'absent']), +    ) + +    argument_spec.update(vyos_argument_spec) + +    module = AnsibleModule( +        argument_spec=argument_spec, +        supports_check_mode=True, +        mutually_exclusive=[('domain_name', 'domain_search')], +    ) + +    warnings = list() + +    result = {'changed': False, 'warnings': warnings} + +    want = map_param_to_obj(module) +    have = config_to_dict(module) + +    commands = spec_to_commands(want, have) +    result['commands'] = commands + +    if commands: +        commit = not module.check_mode +        response = load_config(module, commands, commit=commit) +        result['changed'] = True + +    module.exit_json(**result) + + +if __name__ == '__main__': +    main() diff --git a/plugins/modules/vyos_user.py b/plugins/modules/vyos_user.py new file mode 100644 index 00000000..2981cef3 --- /dev/null +++ b/plugins/modules/vyos_user.py @@ -0,0 +1,340 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017, Ansible by Red Hat, inc +# +# This file is part of Ansible by Red Hat +# +# 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 <http://www.gnu.org/licenses/>. +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', +                    'status': ['preview'], +                    'supported_by': 'network'} + +DOCUMENTATION = """ +--- +module: vyos_user +version_added: "2.4" +author: "Trishna Guha (@trishnaguha)" +short_description: Manage the collection of local users on VyOS device +description: +  - This module provides declarative management of the local usernames +    configured on network devices. It allows playbooks to manage +    either individual usernames or the collection of usernames in the +    current running config. It also supports purging usernames from the +    configuration that are not explicitly defined. +notes: +  - Tested against VYOS 1.1.7 +options: +  aggregate: +    description: +      - The set of username objects to be configured on the remote +        VyOS device. The list entries can either be the username or +        a hash of username and properties. This argument is mutually +        exclusive with the C(name) argument. +    aliases: ['users', 'collection'] +  name: +    description: +      - The username to be configured on the VyOS device. +        This argument accepts a string value and is mutually exclusive +        with the C(aggregate) argument. +        Please note that this option is not same as C(provider username). +  full_name: +    description: +      - The C(full_name) argument provides the full name of the user +        account to be created on the remote device. This argument accepts +        any text string value. +  configured_password: +    description: +      - The password to be configured on the VyOS device. The +        password needs to be provided in clear and it will be encrypted +        on the device. +        Please note that this option is not same as C(provider password). +  update_password: +    description: +      - Since passwords are encrypted in the device running config, this +        argument will instruct the module when to change the password.  When +        set to C(always), the password will always be updated in the device +        and when set to C(on_create) the password will be updated only if +        the username is created. +    default: always +    choices: ['on_create', 'always'] +  level: +    description: +      - The C(level) argument configures the level of the user when logged +        into the system. This argument accepts string values admin or operator. +    aliases: ['role'] +  purge: +    description: +      - Instructs the module to consider the +        resource definition absolute. It will remove any previously +        configured usernames on the device with the exception of the +        `admin` user (the current defined set of users). +    type: bool +    default: false +  state: +    description: +      - Configures the state of the username definition +        as it relates to the device operational configuration. When set +        to I(present), the username(s) should be configured in the device active +        configuration and when set to I(absent) the username(s) should not be +        in the device active configuration +    default: present +    choices: ['present', 'absent'] +extends_documentation_fragment: vyos +""" + +EXAMPLES = """ +- name: create a new user +  vyos_user: +    name: ansible +    configured_password: password +    state: present +- name: remove all users except admin +  vyos_user: +    purge: yes +- name: set multiple users to level operator +  vyos_user: +    aggregate: +      - name: netop +      - name: netend +    level: operator +    state: present +- name: Change Password for User netop +  vyos_user: +    name: netop +    configured_password: "{{ new_password }}" +    update_password: always +    state: present +""" + +RETURN = """ +commands: +  description: The list of configuration mode commands to send to the device +  returned: always +  type: list +  sample: +    - set system login user test level operator +    - set system login user authentication plaintext-password password +""" + +import re + +from copy import deepcopy +from functools import partial + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.common.utils import remove_default_spec +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_config, load_config +from ansible.module_utils.six import iteritems +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import vyos_argument_spec + + +def validate_level(value, module): +    if value not in ('admin', 'operator'): +        module.fail_json(msg='level must be either admin or operator, got %s' % value) + + +def spec_to_commands(updates, module): +    commands = list() +    state = module.params['state'] +    update_password = module.params['update_password'] + +    def needs_update(want, have, x): +        return want.get(x) and (want.get(x) != have.get(x)) + +    def add(command, want, x): +        command.append('set system login user %s %s' % (want['name'], x)) + +    for update in updates: +        want, have = update + +        if want['state'] == 'absent': +            commands.append('delete system login user %s' % want['name']) +            continue + +        if needs_update(want, have, 'level'): +            add(commands, want, "level %s" % want['level']) + +        if needs_update(want, have, 'full_name'): +            add(commands, want, "full-name %s" % want['full_name']) + +        if needs_update(want, have, 'configured_password'): +            if update_password == 'always' or not have: +                add(commands, want, 'authentication plaintext-password %s' % want['configured_password']) + +    return commands + + +def parse_level(data): +    match = re.search(r'level (\S+)', data, re.M) +    if match: +        level = match.group(1)[1:-1] +        return level + + +def parse_full_name(data): +    match = re.search(r'full-name (\S+)', data, re.M) +    if match: +        full_name = match.group(1)[1:-1] +        return full_name + + +def config_to_dict(module): +    data = get_config(module) + +    match = re.findall(r'^set system login user (\S+)', data, re.M) +    if not match: +        return list() + +    instances = list() + +    for user in set(match): +        regex = r' %s .+$' % user +        cfg = re.findall(regex, data, re.M) +        cfg = '\n'.join(cfg) +        obj = { +            'name': user, +            'state': 'present', +            'configured_password': None, +            'level': parse_level(cfg), +            'full_name': parse_full_name(cfg) +        } +        instances.append(obj) + +    return instances + + +def get_param_value(key, item, module): +    # if key doesn't exist in the item, get it from module.params +    if not item.get(key): +        value = module.params[key] + +    # validate the param value (if validator func exists) +    validator = globals().get('validate_%s' % key) +    if all((value, validator)): +        validator(value, module) + +    return value + + +def map_params_to_obj(module): +    aggregate = module.params['aggregate'] +    if not aggregate: +        if not module.params['name'] and module.params['purge']: +            return list() +        else: +            users = [{'name': module.params['name']}] +    else: +        users = list() +        for item in aggregate: +            if not isinstance(item, dict): +                users.append({'name': item}) +            else: +                users.append(item) + +    objects = list() + +    for item in users: +        get_value = partial(get_param_value, item=item, module=module) +        item['configured_password'] = get_value('configured_password') +        item['full_name'] = get_value('full_name') +        item['level'] = get_value('level') +        item['state'] = get_value('state') +        objects.append(item) + +    return objects + + +def update_objects(want, have): +    updates = list() +    for entry in want: +        item = next((i for i in have if i['name'] == entry['name']), None) +        if item is None: +            updates.append((entry, {})) +        elif item: +            for key, value in iteritems(entry): +                if value and value != item[key]: +                    updates.append((entry, item)) +    return updates + + +def main(): +    """ main entry point for module execution +    """ +    element_spec = dict( +        name=dict(), + +        full_name=dict(), +        level=dict(aliases=['role']), + +        configured_password=dict(no_log=True), +        update_password=dict(default='always', choices=['on_create', 'always']), + +        state=dict(default='present', choices=['present', 'absent']) +    ) + +    aggregate_spec = deepcopy(element_spec) +    aggregate_spec['name'] = dict(required=True) + +    # remove default in aggregate spec, to handle common arguments +    remove_default_spec(aggregate_spec) + +    argument_spec = dict( +        aggregate=dict(type='list', elements='dict', options=aggregate_spec, aliases=['users', 'collection']), +        purge=dict(type='bool', default=False) +    ) + +    argument_spec.update(element_spec) +    argument_spec.update(vyos_argument_spec) + +    mutually_exclusive = [('name', 'aggregate')] +    module = AnsibleModule(argument_spec=argument_spec, +                           mutually_exclusive=mutually_exclusive, +                           supports_check_mode=True) + +    warnings = list() +    if module.params['password'] and not module.params['configured_password']: +        warnings.append( +            'The "password" argument is used to authenticate the current connection. ' + +            'To set a user password use "configured_password" instead.' +        ) + +    result = {'changed': False} +    if warnings: +        result['warnings'] = warnings + +    want = map_params_to_obj(module) +    have = config_to_dict(module) +    commands = spec_to_commands(update_objects(want, have), module) + +    if module.params['purge']: +        want_users = [x['name'] for x in want] +        have_users = [x['name'] for x in have] +        for item in set(have_users).difference(want_users): +            commands.append('delete system login user %s' % item) + +    result['commands'] = commands + +    if commands: +        commit = not module.check_mode +        load_config(module, commands, commit=commit) +        result['changed'] = True + +    module.exit_json(**result) + + +if __name__ == '__main__': +    main() diff --git a/plugins/modules/vyos_vlan.py b/plugins/modules/vyos_vlan.py new file mode 100644 index 00000000..ca7bafa7 --- /dev/null +++ b/plugins/modules/vyos_vlan.py @@ -0,0 +1,332 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2017, Ansible by Red Hat, inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', +                    'status': ['preview'], +                    'supported_by': 'network'} + +DOCUMENTATION = """ +--- +module: vyos_vlan +version_added: "2.5" +author: "Trishna Guha (@trishnaguha)" +short_description: Manage VLANs on VyOS network devices +description: +  - This module provides declarative management of VLANs +    on VyOS network devices. +notes: +  - Tested against VYOS 1.1.7 +options: +  name: +    description: +      - Name of the VLAN. +  address: +    description: +      - Configure Virtual interface address. +  vlan_id: +    description: +      - ID of the VLAN. Range 0-4094. +    required: true +  interfaces: +    description: +      - List of interfaces that should be associated to the VLAN. +    required: true +  associated_interfaces: +    description: +      - This is a intent option and checks the operational state of the for given vlan C(name) +        for associated interfaces. If the value in the C(associated_interfaces) does not match with +        the operational state of vlan on device it will result in failure. +    version_added: "2.5" +  delay: +    description: +      - Delay the play should wait to check for declarative intent params values. +    default: 10 +  aggregate: +    description: List of VLANs definitions. +  purge: +    description: +      - Purge VLANs not defined in the I(aggregate) parameter. +    default: no +    type: bool +  state: +    description: +      - State of the VLAN configuration. +    default: present +    choices: ['present', 'absent'] +extends_documentation_fragment: vyos +""" + +EXAMPLES = """ +- name: Create vlan +  vyos_vlan: +    vlan_id: 100 +    name: vlan-100 +    interfaces: eth1 +    state: present + +- name: Add interfaces to VLAN +  vyos_vlan: +    vlan_id: 100 +    interfaces: +      - eth1 +      - eth2 + +- name: Configure virtual interface address +  vyos_vlan: +    vlan_id: 100 +    interfaces: eth1 +    address: 172.26.100.37/24 + +- name: vlan interface config + intent +  vyos_vlan: +    vlan_id: 100 +    interfaces: eth0 +    associated_interfaces: +    - eth0 + +- name: vlan intent check +  vyos_vlan: +    vlan_id: 100 +    associated_interfaces: +    - eth3 +    - eth4 + +- name: Delete vlan +  vyos_vlan: +    vlan_id: 100 +    interfaces: eth1 +    state: absent +""" + +RETURN = """ +commands: +  description: The list of configuration mode commands to send to the device +  returned: always +  type: list +  sample: +    - set interfaces ethernet eth1 vif 100 description VLAN 100 +    - set interfaces ethernet eth1 vif 100 address 172.26.100.37/24 +    - delete interfaces ethernet eth1 vif 100 +""" +import re +import time + +from copy import deepcopy + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.common.utils import remove_default_spec +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import load_config, run_commands +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import vyos_argument_spec + + +def search_obj_in_list(vlan_id, lst): +    obj = list() +    for o in lst: +        if o['vlan_id'] == vlan_id: +            obj.append(o) +    return obj + + +def map_obj_to_commands(updates, module): +    commands = list() +    want, have = updates +    purge = module.params['purge'] + +    for w in want: +        vlan_id = w['vlan_id'] +        name = w['name'] +        address = w['address'] +        state = w['state'] +        interfaces = w['interfaces'] + +        obj_in_have = search_obj_in_list(vlan_id, have) + +        if state == 'absent': +            if obj_in_have: +                for obj in obj_in_have: +                    for i in obj['interfaces']: +                        commands.append('delete interfaces ethernet {0} vif {1}'.format(i, vlan_id)) + +        elif state == 'present': +            if not obj_in_have: +                if w['interfaces'] and w['vlan_id']: +                    for i in w['interfaces']: +                        cmd = 'set interfaces ethernet {0} vif {1}'.format(i, vlan_id) +                        if w['name']: +                            commands.append(cmd + ' description {0}'.format(name)) +                        elif w['address']: +                            commands.append(cmd + ' address {0}'.format(address)) +                        else: +                            commands.append(cmd) + +    if purge: +        for h in have: +            obj_in_want = search_obj_in_list(h['vlan_id'], want) +            if not obj_in_want: +                for i in h['interfaces']: +                    commands.append('delete interfaces ethernet {0} vif {1}'.format(i, h['vlan_id'])) + +    return commands + + +def map_params_to_obj(module): +    obj = [] +    aggregate = module.params.get('aggregate') +    if aggregate: +        for item in aggregate: +            for key in item: +                if item.get(key) is None: +                    item[key] = module.params[key] + +            d = item.copy() + +            if not d['vlan_id']: +                module.fail_json(msg='vlan_id is required') + +            d['vlan_id'] = str(d['vlan_id']) +            module._check_required_one_of(module.required_one_of, item) + +            obj.append(d) +    else: +        obj.append({ +            'vlan_id': str(module.params['vlan_id']), +            'name': module.params['name'], +            'address': module.params['address'], +            'state': module.params['state'], +            'interfaces': module.params['interfaces'], +            'associated_interfaces': module.params['associated_interfaces'] +        }) + +    return obj + + +def map_config_to_obj(module): +    objs = [] +    interfaces = list() + +    output = run_commands(module, 'show interfaces') +    lines = output[0].strip().splitlines()[3:] + +    for l in lines: +        splitted_line = re.split(r'\s{2,}', l.strip()) +        obj = {} + +        eth = splitted_line[0].strip("'") +        if eth.startswith('eth'): +            obj['interfaces'] = [] +            if '.' in eth: +                interface = eth.split('.')[0] +                obj['interfaces'].append(interface) +                obj['vlan_id'] = eth.split('.')[-1] +            else: +                obj['interfaces'].append(eth) +                obj['vlan_id'] = None + +            if splitted_line[1].strip("'") != '-': +                obj['address'] = splitted_line[1].strip("'") + +            if len(splitted_line) > 3: +                obj['name'] = splitted_line[3].strip("'") +            obj['state'] = 'present' +            objs.append(obj) + +    return objs + + +def check_declarative_intent_params(want, module, result): + +    have = None +    obj_interface = list() +    is_delay = False + +    for w in want: +        if w.get('associated_interfaces') is None: +            continue + +        if result['changed'] and not is_delay: +            time.sleep(module.params['delay']) +            is_delay = True + +        if have is None: +            have = map_config_to_obj(module) + +        obj_in_have = search_obj_in_list(w['vlan_id'], have) +        if obj_in_have: +            for obj in obj_in_have: +                obj_interface.extend(obj['interfaces']) + +    for w in want: +        if w.get('associated_interfaces') is None: +            continue +        for i in w['associated_interfaces']: +            if (set(obj_interface) - set(w['associated_interfaces'])) != set([]): +                module.fail_json(msg='Interface {0} not configured on vlan {1}'.format(i, w['vlan_id'])) + + +def main(): +    """ main entry point for module execution +    """ +    element_spec = dict( +        vlan_id=dict(type='int'), +        name=dict(), +        address=dict(), +        interfaces=dict(type='list'), +        associated_interfaces=dict(type='list'), +        delay=dict(default=10, type='int'), +        state=dict(default='present', +                   choices=['present', 'absent']) +    ) + +    aggregate_spec = deepcopy(element_spec) + +    # remove default in aggregate spec, to handle common arguments +    remove_default_spec(aggregate_spec) + +    argument_spec = dict( +        aggregate=dict(type='list', elements='dict', options=aggregate_spec), +        purge=dict(default=False, type='bool') +    ) + +    argument_spec.update(element_spec) +    argument_spec.update(vyos_argument_spec) + +    required_one_of = [['vlan_id', 'aggregate'], +                       ['aggregate', 'interfaces', 'associated_interfaces']] + +    mutually_exclusive = [['vlan_id', 'aggregate']] +    module = AnsibleModule(argument_spec=argument_spec, +                           supports_check_mode=True, +                           required_one_of=required_one_of, +                           mutually_exclusive=mutually_exclusive) + +    warnings = list() +    result = {'changed': False} + +    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: +        commit = not module.check_mode +        load_config(module, commands, commit=commit) +        result['changed'] = True + +    check_declarative_intent_params(want, module, result) + +    module.exit_json(**result) + + +if __name__ == '__main__': +    main() | 
