mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-04-25 05:02:46 +00:00
Initial commit
This commit is contained in:
commit
aebc1b03fd
4861 changed files with 812621 additions and 0 deletions
276
plugins/modules/network/cnos/cnos_backup.py
Normal file
276
plugins/modules/network/cnos/cnos_backup.py
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
#
|
||||
# Copyright (C) 2017 Lenovo, Inc.
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
# Module to Backup Config to Lenovo Switches
|
||||
# Lenovo Networking
|
||||
#
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: cnos_backup
|
||||
author: "Anil Kumar Muraleedharan (@amuraleedhar)"
|
||||
short_description: Backup the current running or startup configuration to a
|
||||
remote server on devices running Lenovo CNOS
|
||||
description:
|
||||
- This module allows you to work with switch configurations. It provides a
|
||||
way to back up the running or startup configurations of a switch to a
|
||||
remote server. This is achieved by periodically saving a copy of the
|
||||
startup or running configuration of the network device to a remote server
|
||||
using FTP, SFTP, TFTP, or SCP. The first step is to create a directory from
|
||||
where the remote server can be reached. The next step is to provide the
|
||||
full file path of the location where the configuration will be backed up.
|
||||
Authentication details required by the remote server must be provided as
|
||||
well. This module uses SSH to manage network device configuration.
|
||||
The results of the operation will be placed in a directory named 'results'
|
||||
that must be created by the user in their local directory to where the
|
||||
playbook is run.
|
||||
extends_documentation_fragment:
|
||||
- community.general.cnos
|
||||
|
||||
options:
|
||||
configType:
|
||||
description:
|
||||
- This specifies what type of configuration will be backed up. The
|
||||
choices are the running or startup configurations. There is no
|
||||
default value, so it will result in an error if the input is
|
||||
incorrect.
|
||||
required: Yes
|
||||
default: Null
|
||||
choices: [running-config, startup-config]
|
||||
protocol:
|
||||
description:
|
||||
- This refers to the protocol used by the network device to
|
||||
interact with the remote server to where to upload the backup
|
||||
configuration. The choices are FTP, SFTP, TFTP, or SCP. Any other
|
||||
protocols will result in error. If this parameter is
|
||||
not specified, there is no default value to be used.
|
||||
required: Yes
|
||||
default: Null
|
||||
choices: [SFTP, SCP, FTP, TFTP]
|
||||
rcserverip:
|
||||
description:
|
||||
-This specifies the IP Address of the remote server to where the
|
||||
configuration will be backed up.
|
||||
required: Yes
|
||||
default: Null
|
||||
rcpath:
|
||||
description:
|
||||
- This specifies the full file path where the configuration file
|
||||
will be copied on the remote server. In case the relative path is
|
||||
used as the variable value, the root folder for the user of the
|
||||
server needs to be specified.
|
||||
required: Yes
|
||||
default: Null
|
||||
serverusername:
|
||||
description:
|
||||
- Specify the username for the server relating to the protocol
|
||||
used.
|
||||
required: Yes
|
||||
default: Null
|
||||
serverpassword:
|
||||
description:
|
||||
- Specify the password for the server relating to the protocol
|
||||
used.
|
||||
required: Yes
|
||||
default: Null
|
||||
'''
|
||||
EXAMPLES = '''
|
||||
Tasks : The following are examples of using the module cnos_backup.
|
||||
These are written in the main.yml file of the tasks directory.
|
||||
---
|
||||
- name: Test Running Config Backup
|
||||
cnos_backup:
|
||||
deviceType: "{{ hostvars[inventory_hostname]['deviceType'] }}"
|
||||
outputfile: "./results/test_backup_{{ inventory_hostname }}_output.txt"
|
||||
configType: running-config
|
||||
protocol: "sftp"
|
||||
serverip: "10.241.106.118"
|
||||
rcpath: "/root/cnos/G8272-running-config.txt"
|
||||
serverusername: "root"
|
||||
serverpassword: "root123"
|
||||
|
||||
- name: Test Startup Config Backup
|
||||
cnos_backup:
|
||||
deviceType: "{{ hostvars[inventory_hostname]['deviceType'] }}"
|
||||
outputfile: "./results/test_backup_{{ inventory_hostname }}_output.txt"
|
||||
configType: startup-config
|
||||
protocol: "sftp"
|
||||
serverip: "10.241.106.118"
|
||||
rcpath: "/root/cnos/G8272-startup-config.txt"
|
||||
serverusername: "root"
|
||||
serverpassword: "root123"
|
||||
|
||||
- name: Test Running Config Backup -TFTP
|
||||
cnos_backup:
|
||||
deviceType: "{{ hostvars[inventory_hostname]['deviceType'] }}"
|
||||
outputfile: "./results/test_backup_{{ inventory_hostname }}_output.txt"
|
||||
configType: running-config
|
||||
protocol: "tftp"
|
||||
serverip: "10.241.106.118"
|
||||
rcpath: "/anil/G8272-running-config.txt"
|
||||
serverusername: "root"
|
||||
serverpassword: "root123"
|
||||
|
||||
- name: Test Startup Config Backup - TFTP
|
||||
cnos_backup:
|
||||
deviceType: "{{ hostvars[inventory_hostname]['deviceType'] }}"
|
||||
outputfile: "./results/test_backup_{{ inventory_hostname }}_output.txt"
|
||||
configType: startup-config
|
||||
protocol: "tftp"
|
||||
serverip: "10.241.106.118"
|
||||
rcpath: "/anil/G8272-startup-config.txt"
|
||||
serverusername: "root"
|
||||
serverpassword: "root123"
|
||||
|
||||
'''
|
||||
RETURN = '''
|
||||
msg:
|
||||
description: Success or failure message
|
||||
returned: always
|
||||
type: str
|
||||
sample: "Config file transferred to server"
|
||||
'''
|
||||
|
||||
import sys
|
||||
import time
|
||||
import socket
|
||||
import array
|
||||
import json
|
||||
import time
|
||||
import re
|
||||
import os
|
||||
try:
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos import cnos
|
||||
HAS_LIB = True
|
||||
except Exception:
|
||||
HAS_LIB = False
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
# Utility Method to back up the running config or start up config
|
||||
# This method supports only SCP or SFTP or FTP or TFTP
|
||||
# Tuning of timeout parameter is pending
|
||||
def doConfigBackUp(module, prompt, answer):
|
||||
host = module.params['host']
|
||||
server = module.params['serverip']
|
||||
username = module.params['serverusername']
|
||||
password = module.params['serverpassword']
|
||||
protocol = module.params['protocol'].lower()
|
||||
rcPath = module.params['rcpath']
|
||||
configType = module.params['configType']
|
||||
confPath = rcPath + host + '_' + configType + '.txt'
|
||||
|
||||
retVal = ''
|
||||
|
||||
# config backup command happens here
|
||||
command = "copy " + configType + " " + protocol + " " + protocol + "://"
|
||||
command = command + username + "@" + server + "/" + confPath
|
||||
command = command + " vrf management\n"
|
||||
cnos.debugOutput(command + "\n")
|
||||
# cnos.checkForFirstTimeAccess(module, command, 'yes/no', 'yes')
|
||||
cmd = []
|
||||
if(protocol == "scp"):
|
||||
scp_cmd1 = [{'command': command, 'prompt': 'timeout:', 'answer': '0'}]
|
||||
scp_cmd2 = [{'command': '\n', 'prompt': 'Password:',
|
||||
'answer': password}]
|
||||
cmd.extend(scp_cmd1)
|
||||
cmd.extend(scp_cmd2)
|
||||
retVal = retVal + str(cnos.run_cnos_commands(module, cmd))
|
||||
elif(protocol == "sftp"):
|
||||
sftp_cmd = [{'command': command, 'prompt': 'Password:',
|
||||
'answer': password}]
|
||||
cmd.extend(sftp_cmd)
|
||||
retVal = retVal + str(cnos.run_cnos_commands(module, cmd))
|
||||
elif(protocol == "ftp"):
|
||||
ftp_cmd = [{'command': command, 'prompt': 'Password:',
|
||||
'answer': password}]
|
||||
cmd.extend(ftp_cmd)
|
||||
retVal = retVal + str(cnos.run_cnos_commands(module, cmd))
|
||||
elif(protocol == "tftp"):
|
||||
command = "copy " + configType + " " + protocol + " " + protocol
|
||||
command = command + "://" + server + "/" + confPath
|
||||
command = command + " vrf management\n"
|
||||
# cnos.debugOutput(command)
|
||||
tftp_cmd = [{'command': command, 'prompt': None, 'answer': None}]
|
||||
cmd.extend(tftp_cmd)
|
||||
retVal = retVal + str(cnos.run_cnos_commands(module, cmd))
|
||||
else:
|
||||
return "Error-110"
|
||||
|
||||
return retVal
|
||||
# EOM
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
outputfile=dict(required=True),
|
||||
host=dict(required=False),
|
||||
username=dict(required=False),
|
||||
password=dict(required=False, no_log=True),
|
||||
enablePassword=dict(required=False, no_log=True),
|
||||
deviceType=dict(required=True),
|
||||
configType=dict(required=True),
|
||||
protocol=dict(required=True),
|
||||
serverip=dict(required=True),
|
||||
rcpath=dict(required=True),
|
||||
serverusername=dict(required=False),
|
||||
serverpassword=dict(required=False, no_log=True),),
|
||||
supports_check_mode=False)
|
||||
|
||||
outputfile = module.params['outputfile']
|
||||
protocol = module.params['protocol'].lower()
|
||||
output = ''
|
||||
if(protocol == "tftp" or protocol == "ftp" or
|
||||
protocol == "sftp" or protocol == "scp"):
|
||||
transfer_status = doConfigBackUp(module, None, None)
|
||||
else:
|
||||
transfer_status = "Invalid Protocol option"
|
||||
|
||||
output = output + "\n Config Back Up status \n" + transfer_status
|
||||
|
||||
# Save it into the file
|
||||
path = outputfile.rsplit('/', 1)
|
||||
# cnos.debugOutput(path[0])
|
||||
if not os.path.exists(path[0]):
|
||||
os.makedirs(path[0])
|
||||
file = open(outputfile, "a")
|
||||
file.write(output)
|
||||
file.close()
|
||||
|
||||
# Logic to check when changes occur or not
|
||||
errorMsg = cnos.checkOutputForError(output)
|
||||
if(errorMsg is None):
|
||||
module.exit_json(changed=True, msg="Config file transferred to server")
|
||||
else:
|
||||
module.fail_json(msg=errorMsg)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
263
plugins/modules/network/cnos/cnos_banner.py
Normal file
263
plugins/modules/network/cnos/cnos_banner.py
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
#!/usr/bin/python
|
||||
#
|
||||
# Copyright (C) 2017 Lenovo, Inc.
|
||||
# (c) 2017, Ansible by Red Hat, inc
|
||||
# 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/>.
|
||||
#
|
||||
# Module to send banner commands to Lenovo Switches
|
||||
# Two types of banners are supported login and motd
|
||||
# Lenovo Networking
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: cnos_banner
|
||||
author: "Anil Kumar Muraleedharan (@amuraleedhar)"
|
||||
short_description: Manage multiline banners on Lenovo CNOS devices
|
||||
description:
|
||||
- This will configure both login and motd banners on remote devices
|
||||
running Lenovo CNOS. It allows playbooks to add or remote
|
||||
banner text from the active running configuration.
|
||||
notes:
|
||||
- Tested against CNOS 10.8.1
|
||||
options:
|
||||
banner:
|
||||
description:
|
||||
- Specifies which banner should be configured on the remote device.
|
||||
In Ansible 2.8 and earlier only I(login) and I(motd) were supported.
|
||||
required: true
|
||||
choices: ['login', 'motd']
|
||||
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']
|
||||
provider:
|
||||
description:
|
||||
- B(Deprecated)
|
||||
- "Starting with Ansible 2.5 we recommend using
|
||||
C(connection: network_cli)."
|
||||
- For more information please see the
|
||||
L(CNOS Platform Options guide, ../network/user_guide/platform_cnos.html).
|
||||
- HORIZONTALLINE
|
||||
- A dict object containing connection details.
|
||||
suboptions:
|
||||
host:
|
||||
description:
|
||||
- Specifies the DNS host name or address for connecting to the remote
|
||||
device over the specified transport. The value of host is used as
|
||||
the destination address for the transport.
|
||||
required: true
|
||||
port:
|
||||
description:
|
||||
- Specifies the port to use when building the connection to the
|
||||
remote device.
|
||||
default: 22
|
||||
username:
|
||||
description:
|
||||
- Configures the username to use to authenticate the connection to
|
||||
the remote device. This value is used to authenticate
|
||||
the SSH session. If the value is not specified in the task, the
|
||||
value of environment variable C(ANSIBLE_NET_USERNAME) will be used
|
||||
instead.
|
||||
password:
|
||||
description:
|
||||
- Specifies the password to use to authenticate the connection to
|
||||
the remote device. This value is used to authenticate
|
||||
the SSH session. If the value is not specified in the task, the
|
||||
value of environment variable C(ANSIBLE_NET_PASSWORD) will be used
|
||||
instead.
|
||||
timeout:
|
||||
description:
|
||||
- Specifies the timeout in seconds for communicating with the network
|
||||
device for either connecting or sending commands. If the timeout
|
||||
is exceeded before the operation is completed, the module will
|
||||
error.
|
||||
default: 10
|
||||
ssh_keyfile:
|
||||
description:
|
||||
- Specifies the SSH key to use to authenticate the connection to
|
||||
the remote device. This value is the path to the
|
||||
key used to authenticate the SSH session. If the value is not
|
||||
specified in the task, the value of environment variable
|
||||
C(ANSIBLE_NET_SSH_KEYFILE)will be used instead.
|
||||
authorize:
|
||||
description:
|
||||
- Instructs the module to enter privileged mode on the remote device
|
||||
before sending any commands. If not specified, the device will
|
||||
attempt to execute all commands in non-privileged mode. If the
|
||||
value is not specified in the task, the value of environment
|
||||
variable C(ANSIBLE_NET_AUTHORIZE) will be used instead.
|
||||
type: bool
|
||||
default: 'no'
|
||||
auth_pass:
|
||||
description:
|
||||
- Specifies the password to use if required to enter privileged mode
|
||||
on the remote device. If I(authorize) is false, then this argument
|
||||
does nothing. If the value is not specified in the task, the value
|
||||
of environment variable C(ANSIBLE_NET_AUTH_PASS) will be used
|
||||
instead.
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: configure the login banner
|
||||
cnos_banner:
|
||||
banner: login
|
||||
text: |
|
||||
this is my login banner
|
||||
that contains a multiline
|
||||
string
|
||||
state: present
|
||||
|
||||
- name: remove the motd banner
|
||||
cnos_banner:
|
||||
banner: motd
|
||||
state: absent
|
||||
|
||||
- name: Configure banner from file
|
||||
cnos_banner:
|
||||
banner: motd
|
||||
text: "{{ lookup('file', './config_partial/raw_banner.cfg') }}"
|
||||
state: present
|
||||
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
commands:
|
||||
description: The list of configuration mode commands to send to the device
|
||||
returned: always
|
||||
type: list
|
||||
sample:
|
||||
- banner login
|
||||
- this is my login banner
|
||||
- that contains a multiline
|
||||
- string
|
||||
"""
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.connection import exec_command
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import load_config, run_commands
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import check_args
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import cnos_argument_spec
|
||||
from ansible.module_utils._text import to_text
|
||||
import re
|
||||
|
||||
|
||||
def map_obj_to_commands(updates, module):
|
||||
commands = list()
|
||||
want, have = updates
|
||||
state = module.params['state']
|
||||
|
||||
if state == 'absent' and 'text' in have.keys() and have['text']:
|
||||
commands.append('no banner %s' % module.params['banner'])
|
||||
|
||||
elif state == 'present':
|
||||
if want['text'] and (want['text'] != have.get('text')):
|
||||
banner_cmd = 'banner %s ' % module.params['banner']
|
||||
for bline in want['text'].strip().splitlines():
|
||||
final_cmd = banner_cmd + bline.strip()
|
||||
commands.append(final_cmd)
|
||||
|
||||
return commands
|
||||
|
||||
|
||||
def map_config_to_obj(module):
|
||||
rc, out, err = exec_command(module,
|
||||
'show banner %s' % module.params['banner'])
|
||||
if rc == 0:
|
||||
output = out
|
||||
else:
|
||||
rc, out, err = exec_command(module,
|
||||
'show running-config | include banner %s'
|
||||
% module.params['banner'])
|
||||
if out:
|
||||
output = re.search(r'\^C(.*)\^C', out, re.S).group(1).strip()
|
||||
else:
|
||||
output = None
|
||||
obj = {'banner': module.params['banner'], 'state': 'absent'}
|
||||
if output:
|
||||
obj['text'] = output
|
||||
obj['state'] = 'present'
|
||||
return obj
|
||||
|
||||
|
||||
def map_params_to_obj(module):
|
||||
text = module.params['text']
|
||||
if text:
|
||||
text = to_text(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=['login', 'motd']),
|
||||
text=dict(),
|
||||
state=dict(default='present', choices=['present', 'absent'])
|
||||
)
|
||||
|
||||
argument_spec.update(cnos_argument_spec)
|
||||
|
||||
required_if = [('state', 'present', ('text',))]
|
||||
|
||||
module = AnsibleModule(argument_spec=argument_spec,
|
||||
required_if=required_if,
|
||||
supports_check_mode=True)
|
||||
|
||||
warnings = list()
|
||||
check_args(module, warnings)
|
||||
|
||||
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:
|
||||
if not module.check_mode:
|
||||
response = load_config(module, commands)
|
||||
|
||||
result['changed'] = True
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
1180
plugins/modules/network/cnos/cnos_bgp.py
Normal file
1180
plugins/modules/network/cnos/cnos_bgp.py
Normal file
File diff suppressed because it is too large
Load diff
208
plugins/modules/network/cnos/cnos_command.py
Normal file
208
plugins/modules/network/cnos/cnos_command.py
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# (C) 2017 Red Hat Inc.
|
||||
# Copyright (C) 2017 Lenovo.
|
||||
#
|
||||
# GNU General Public License v3.0+
|
||||
#
|
||||
# This program 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.
|
||||
#
|
||||
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
#
|
||||
# Module to execute CNOS Commands on Lenovo Switches.
|
||||
# Lenovo Networking
|
||||
#
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: cnos_command
|
||||
author: "Anil Kumar Muraleedharan (@amuraleedhar)"
|
||||
short_description: Run arbitrary commands on Lenovo CNOS devices
|
||||
description:
|
||||
- Sends arbitrary commands to an CNOS node and returns the results
|
||||
read from the device. The C(cnos_command) module includes an
|
||||
argument that will cause the module to wait for a specific condition
|
||||
before returning or timing out if the condition is not met.
|
||||
options:
|
||||
commands:
|
||||
description:
|
||||
- List of commands to send to the remote device.
|
||||
The resulting output from the command is returned.
|
||||
If the I(wait_for) argument is provided, the module is not
|
||||
returned until the condition is satisfied or the number of
|
||||
retires is expired.
|
||||
required: true
|
||||
wait_for:
|
||||
description:
|
||||
- List of conditions to evaluate against the output of the
|
||||
command. The task will wait for each condition to be true
|
||||
before moving forward. If the conditional is not true
|
||||
within the configured number of retries, the task fails.
|
||||
See examples.
|
||||
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 by tried
|
||||
before it is considered failed. The command is run on the
|
||||
target device every retry and evaluated against the
|
||||
I(wait_for) conditions.
|
||||
default: 10
|
||||
interval:
|
||||
description:
|
||||
- Configures the interval in seconds to wait between 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
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
---
|
||||
- name: test contains operator
|
||||
cnos_command:
|
||||
commands:
|
||||
- show version
|
||||
- show system memory
|
||||
wait_for:
|
||||
- "result[0] contains 'Lenovo'"
|
||||
- "result[1] contains 'MemFree'"
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- "result.changed == false"
|
||||
- "result.stdout is defined"
|
||||
|
||||
- name: get output for single command
|
||||
cnos_command:
|
||||
commands: ['show version']
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- "result.changed == false"
|
||||
- "result.stdout is defined"
|
||||
|
||||
- name: get output for multiple commands
|
||||
cnos_command:
|
||||
commands:
|
||||
- show version
|
||||
- show interface information
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- "result.changed == false"
|
||||
- "result.stdout is defined"
|
||||
- "result.stdout | length == 2"
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
stdout:
|
||||
description: the set of responses from the commands
|
||||
returned: always
|
||||
type: list
|
||||
sample: ['...', '...']
|
||||
stdout_lines:
|
||||
description: The value of stdout split into a list
|
||||
returned: always
|
||||
type: list
|
||||
sample: [['...', '...'], ['...'], ['...']]
|
||||
failed_conditions:
|
||||
description: the conditionals that failed
|
||||
returned: failed
|
||||
type: list
|
||||
sample: ['...', '...']
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import run_commands, check_args
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.parsing import Conditional
|
||||
from ansible.module_utils.six import string_types
|
||||
|
||||
|
||||
def to_lines(stdout):
|
||||
for item in stdout:
|
||||
if isinstance(item, string_types):
|
||||
item = str(item).split('\n')
|
||||
yield item
|
||||
|
||||
|
||||
def main():
|
||||
spec = dict(
|
||||
# { command: <str>, prompt: <str>, response: <str> }
|
||||
commands=dict(type='list', required=True),
|
||||
|
||||
wait_for=dict(type='list'),
|
||||
match=dict(default='all', choices=['all', 'any']),
|
||||
|
||||
retries=dict(default=10, type='int'),
|
||||
interval=dict(default=1, type='int')
|
||||
)
|
||||
|
||||
module = AnsibleModule(argument_spec=spec, supports_check_mode=True)
|
||||
result = {'changed': False}
|
||||
|
||||
wait_for = module.params['wait_for'] or list()
|
||||
conditionals = [Conditional(c) for c in wait_for]
|
||||
|
||||
commands = module.params['commands']
|
||||
retries = module.params['retries']
|
||||
interval = module.params['interval']
|
||||
match = module.params['match']
|
||||
|
||||
while retries > 0:
|
||||
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)
|
||||
retries -= 1
|
||||
|
||||
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({
|
||||
'changed': False,
|
||||
'stdout': responses,
|
||||
'stdout_lines': list(to_lines(responses))
|
||||
})
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
168
plugins/modules/network/cnos/cnos_conditional_command.py
Normal file
168
plugins/modules/network/cnos/cnos_conditional_command.py
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
#
|
||||
# Copyright (C) 2017 Lenovo, Inc.
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
# Module to send Conditional CLI commands to Lenovo Switches
|
||||
# Lenovo Networking
|
||||
#
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: cnos_conditional_command
|
||||
author: "Anil Kumar Muraleedharan (@amuraleedhar)"
|
||||
short_description: Execute a single command based on condition on devices
|
||||
running Lenovo CNOS
|
||||
description:
|
||||
- This module allows you to modify the running configuration of a switch. It
|
||||
provides a way to execute a single CNOS command on a network device by
|
||||
evaluating the current running configuration and executing the command only
|
||||
if the specific settings have not been already configured.
|
||||
The CNOS command is passed as an argument of the method.
|
||||
This module functions the same as the cnos_command module.
|
||||
The only exception is that following inventory variable can be specified
|
||||
["condition = <flag string>"]
|
||||
When this inventory variable is specified as the variable of a task, the
|
||||
command is executed for the network element that matches the flag string.
|
||||
Usually, commands are executed across a group of network devices. When
|
||||
there is a requirement to skip the execution of the command on one or
|
||||
more devices, it is recommended to use this module. This module uses SSH to
|
||||
manage network device configuration.
|
||||
extends_documentation_fragment:
|
||||
- community.general.cnos
|
||||
|
||||
options:
|
||||
clicommand:
|
||||
description:
|
||||
- This specifies the CLI command as an attribute to this method.
|
||||
The command is passed using double quotes. The variables can be
|
||||
placed directly on to the CLI commands or can be invoked
|
||||
from the vars directory.
|
||||
required: true
|
||||
default: Null
|
||||
condition:
|
||||
description:
|
||||
- If you specify condition=false in the inventory file against any
|
||||
device, the command execution is skipped for that device.
|
||||
required: true
|
||||
default: Null
|
||||
flag:
|
||||
description:
|
||||
- If a task needs to be executed, you have to set the flag the same
|
||||
as it is specified in the inventory for that device.
|
||||
required: true
|
||||
default: Null
|
||||
|
||||
'''
|
||||
EXAMPLES = '''
|
||||
Tasks : The following are examples of using the module
|
||||
cnos_conditional_command. These are written in the main.yml file of the tasks
|
||||
directory.
|
||||
---
|
||||
- name: Applying CLI template on VLAG Tier1 Leaf Switch1
|
||||
cnos_conditional_command:
|
||||
deviceType: "{{ hostvars[inventory_hostname]['deviceType'] }}"
|
||||
outputfile: "./results/test_conditional_command_
|
||||
{{ inventory_hostname }}_output.txt"
|
||||
condition: "{{ hostvars[inventory_hostname]['condition']}}"
|
||||
flag: leaf_switch2
|
||||
command: "spanning-tree mode enable"
|
||||
|
||||
'''
|
||||
RETURN = '''
|
||||
msg:
|
||||
description: Success or failure message
|
||||
returned: always
|
||||
type: str
|
||||
sample: "Command Applied"
|
||||
'''
|
||||
|
||||
import sys
|
||||
import time
|
||||
import socket
|
||||
import array
|
||||
import json
|
||||
import time
|
||||
import re
|
||||
import os
|
||||
try:
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos import cnos
|
||||
HAS_LIB = True
|
||||
except Exception:
|
||||
HAS_LIB = False
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
clicommand=dict(required=True),
|
||||
outputfile=dict(required=True),
|
||||
condition=dict(required=True),
|
||||
flag=dict(required=True),
|
||||
host=dict(required=False),
|
||||
deviceType=dict(required=True),
|
||||
username=dict(required=False),
|
||||
password=dict(required=False, no_log=True),
|
||||
enablePassword=dict(required=False,
|
||||
no_log=True), ), supports_check_mode=False)
|
||||
|
||||
condition = module.params['condition']
|
||||
flag = module.params['flag']
|
||||
cliCommand = module.params['clicommand']
|
||||
outputfile = module.params['outputfile']
|
||||
output = ''
|
||||
if (condition is None or condition != flag):
|
||||
module.exit_json(changed=True, msg="Command Skipped for this switch")
|
||||
return ''
|
||||
# Send the CLi command
|
||||
cmd = [{'command': cliCommand, 'prompt': None, 'answer': None}]
|
||||
output = output + str(cnos.run_cnos_commands(module, cmd))
|
||||
# Write to memory
|
||||
save_cmd = [{'command': 'save', 'prompt': None, 'answer': None}]
|
||||
cmd.extend(save_cmd)
|
||||
output = output + str(cnos.run_cnos_commands(module, cmd))
|
||||
|
||||
# Save it into the file
|
||||
path = outputfile.rsplit('/', 1)
|
||||
# cnos.debugOutput(path[0])
|
||||
if not os.path.exists(path[0]):
|
||||
os.makedirs(path[0])
|
||||
file = open(outputfile, "a")
|
||||
file.write(output)
|
||||
file.close()
|
||||
|
||||
# Logic to check when changes occur or not
|
||||
errorMsg = cnos.checkOutputForError(output)
|
||||
if(errorMsg is None):
|
||||
module.exit_json(changed=True,
|
||||
msg="CLI Command executed and results saved in file ")
|
||||
else:
|
||||
module.fail_json(msg=errorMsg)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
188
plugins/modules/network/cnos/cnos_conditional_template.py
Normal file
188
plugins/modules/network/cnos/cnos_conditional_template.py
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
#
|
||||
# Copyright (C) 2017 Lenovo, Inc.
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
# Module to send conditional template to Lenovo Switches
|
||||
# Lenovo Networking
|
||||
#
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: cnos_conditional_template
|
||||
author: "Anil Kumar Muraleedharan (@amuraleedhar)"
|
||||
short_description: Manage switch configuration using templates based on
|
||||
condition on devices running Lenovo CNOS
|
||||
description:
|
||||
- This module allows you to work with the running configuration of a
|
||||
switch. It provides a way to execute a set of CNOS commands on a switch by
|
||||
evaluating the current running configuration and executing the commands
|
||||
only if the specific settings have not been already configured.
|
||||
The configuration source can be a set of commands or a template written in
|
||||
the Jinja2 templating language. This module functions the same as the
|
||||
cnos_template module. The only exception is that the following inventory
|
||||
variable can be specified.
|
||||
["condition = <flag string>"]
|
||||
When this inventory variable is specified as the variable of a task, the
|
||||
template is executed for the network element that matches the flag string.
|
||||
Usually, templates are used when commands are the same across a group of
|
||||
network devices. When there is a requirement to skip the execution of the
|
||||
template on one or more devices, it is recommended to use this module.
|
||||
This module uses SSH to manage network device configuration.
|
||||
extends_documentation_fragment:
|
||||
- community.general.cnos
|
||||
|
||||
options:
|
||||
commandfile:
|
||||
description:
|
||||
- This specifies the path to the CNOS command file which needs to
|
||||
be applied. This usually comes from the commands folder. Generally
|
||||
this file is the output of the variables applied on a template
|
||||
file. So this command is preceded by a template module. The
|
||||
command file must contain the Ansible keyword
|
||||
{{ inventory_hostname }} and the condition flag in its filename to
|
||||
ensure that the command file is unique for each switch and
|
||||
condition. If this is omitted, the command file will be
|
||||
overwritten during iteration. For example,
|
||||
commandfile=./commands/clos_leaf_bgp_
|
||||
{{ inventory_hostname }}_LP21_commands.txt
|
||||
required: true
|
||||
default: Null
|
||||
condition:
|
||||
description:
|
||||
- If you specify condition=<flag string> in the inventory file
|
||||
against any device, the template execution is done for that device
|
||||
in case it matches the flag setting for that task.
|
||||
required: true
|
||||
default: Null
|
||||
flag:
|
||||
description:
|
||||
- If a task needs to be executed, you have to set the flag the same
|
||||
as it is specified in the inventory for that device.
|
||||
required: true
|
||||
default: Null
|
||||
'''
|
||||
EXAMPLES = '''
|
||||
Tasks : The following are examples of using the module
|
||||
cnos_conditional_template. These are written in the main.yml file of the
|
||||
tasks directory.
|
||||
---
|
||||
- name: Applying CLI template on VLAG Tier1 Leaf Switch1
|
||||
cnos_conditional_template:
|
||||
deviceType: "{{ hostvars[inventory_hostname]['deviceType'] }}"
|
||||
outputfile: "./results/vlag_1tier_leaf_switch1_
|
||||
{{ inventory_hostname }}_output.txt"
|
||||
condition: "{{ hostvars[inventory_hostname]['condition']}}"
|
||||
flag: "leaf_switch1"
|
||||
commandfile: "./commands/vlag_1tier_leaf_switch1_
|
||||
{{ inventory_hostname }}_commands.txt"
|
||||
stp_mode1: "disable"
|
||||
port_range1: "17,18,29,30"
|
||||
portchannel_interface_number1: 1001
|
||||
portchannel_mode1: active
|
||||
slot_chassis_number1: 1/48
|
||||
switchport_mode1: trunk
|
||||
'''
|
||||
RETURN = '''
|
||||
msg:
|
||||
description: Success or failure message
|
||||
returned: always
|
||||
type: str
|
||||
sample: "Template Applied."
|
||||
'''
|
||||
|
||||
import sys
|
||||
import time
|
||||
import socket
|
||||
import array
|
||||
import json
|
||||
import time
|
||||
import re
|
||||
import os
|
||||
try:
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos import cnos
|
||||
HAS_LIB = True
|
||||
except Exception:
|
||||
HAS_LIB = False
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
commandfile=dict(required=True),
|
||||
outputfile=dict(required=True),
|
||||
condition=dict(required=True),
|
||||
flag=dict(required=True),
|
||||
host=dict(required=False),
|
||||
deviceType=dict(required=True),
|
||||
username=dict(required=False),
|
||||
password=dict(required=False, no_log=True),
|
||||
enablePassword=dict(required=False, no_log=True),),
|
||||
supports_check_mode=False)
|
||||
|
||||
condition = module.params['condition']
|
||||
flag = module.params['flag']
|
||||
commandfile = module.params['commandfile']
|
||||
outputfile = module.params['outputfile']
|
||||
|
||||
output = ''
|
||||
if (condition is None or condition != flag):
|
||||
module.exit_json(changed=True, msg="Template Skipped for this switch")
|
||||
return " "
|
||||
# Send commands one by one
|
||||
f = open(commandfile, "r")
|
||||
cmd = []
|
||||
for line in f:
|
||||
# Omit the comment lines in template file
|
||||
if not line.startswith("#"):
|
||||
# cnos.debugOutput(line)
|
||||
command = line.strip()
|
||||
inner_cmd = [{'command': command, 'prompt': None, 'answer': None}]
|
||||
cmd.extend(inner_cmd)
|
||||
# Write to memory
|
||||
save_cmd = [{'command': 'save', 'prompt': None, 'answer': None}]
|
||||
cmd.extend(save_cmd)
|
||||
output = output + str(cnos.run_cnos_commands(module, cmd))
|
||||
# Write output to file
|
||||
path = outputfile.rsplit('/', 1)
|
||||
# cnos.debugOutput(path[0])
|
||||
if not os.path.exists(path[0]):
|
||||
os.makedirs(path[0])
|
||||
file = open(outputfile, "a")
|
||||
file.write(output)
|
||||
file.close()
|
||||
|
||||
# Logic to check when changes occur or not
|
||||
errorMsg = cnos.checkOutputForError(output)
|
||||
if(errorMsg is None):
|
||||
module.exit_json(changed=True, msg="Template Applied")
|
||||
else:
|
||||
module.fail_json(msg=errorMsg)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
307
plugins/modules/network/cnos/cnos_config.py
Normal file
307
plugins/modules/network/cnos/cnos_config.py
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# (C) 2017 Red Hat Inc.
|
||||
# Copyright (C) 2017 Lenovo.
|
||||
#
|
||||
# GNU General Public License v3.0+
|
||||
#
|
||||
# This program 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.
|
||||
#
|
||||
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
#
|
||||
# Module to configure Lenovo Switches.
|
||||
# Lenovo Networking
|
||||
#
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: cnos_config
|
||||
author: "Anil Kumar Muraleedharan (@amuraleedhar)"
|
||||
short_description: Manage Lenovo CNOS configuration sections
|
||||
description:
|
||||
- Lenovo CNOS configurations use a simple block indent file syntax
|
||||
for segmenting configuration into sections. This module provides
|
||||
an implementation for working with CNOS configuration sections in
|
||||
a deterministic way.
|
||||
notes:
|
||||
- Tested against CNOS 10.9.1
|
||||
options:
|
||||
lines:
|
||||
description:
|
||||
- The ordered set of commands that should be configured in the
|
||||
section. The commands must be the exact same commands as found
|
||||
in the device running-config. Be sure to note the configuration
|
||||
command syntax as some commands are automatically modified by the
|
||||
device config parser.
|
||||
aliases: ['commands']
|
||||
parents:
|
||||
description:
|
||||
- The ordered set of parents that uniquely identify the section
|
||||
the commands should be checked against. If the parents argument
|
||||
is omitted, the commands are checked against the set of top
|
||||
level or global commands.
|
||||
src:
|
||||
description:
|
||||
- Specifies the source path to the file that contains the configuration
|
||||
or configuration template to load. The path to the source file can
|
||||
either be the full path on the Ansible control host or a relative
|
||||
path from the playbook or role root directory. This argument is
|
||||
mutually exclusive with I(lines), I(parents).
|
||||
before:
|
||||
description:
|
||||
- The ordered set of commands to push on to the command stack if
|
||||
a change needs to be made. This allows the playbook designer
|
||||
the opportunity to perform configuration commands prior to pushing
|
||||
any changes without affecting how the set of commands are matched
|
||||
against the system.
|
||||
after:
|
||||
description:
|
||||
- The ordered set of commands to append to the end of the command
|
||||
stack if a change needs to be made. Just like with I(before) this
|
||||
allows the playbook designer to append a set of commands to be
|
||||
executed after the command set.
|
||||
match:
|
||||
description:
|
||||
- Instructs the module on the way to perform the matching of
|
||||
the set of commands against the current device config. If
|
||||
match is set to I(line), commands are matched line by line. If
|
||||
match is set to I(strict), command lines are matched with respect
|
||||
to position. If match is set to I(exact), command lines
|
||||
must be an equal match. Finally, if match is set to I(none), the
|
||||
module will not attempt to compare the source configuration with
|
||||
the running configuration on the remote device.
|
||||
default: line
|
||||
choices: ['line', 'strict', 'exact', 'none']
|
||||
replace:
|
||||
description:
|
||||
- Instructs the module on the way to perform the configuration
|
||||
on the device. If the replace argument is set to I(line) then
|
||||
the modified lines are pushed to the device in configuration
|
||||
mode. If the replace argument is set to I(block) then the entire
|
||||
command block is pushed to the device in configuration mode if any
|
||||
line is not correct.
|
||||
default: line
|
||||
choices: ['line', 'block', 'config']
|
||||
config:
|
||||
description:
|
||||
- The module, by default, will connect to the remote device and
|
||||
retrieve the current running-config to use as a base for comparing
|
||||
against the contents of source. There are times when it is not
|
||||
desirable to have the task get the current running-config for
|
||||
every task in a playbook. The I(config) argument allows the
|
||||
implementer to pass in the configuration to use as the base
|
||||
config for comparison.
|
||||
backup:
|
||||
description:
|
||||
- This argument will cause the module to create a full backup of
|
||||
the current C(running-config) from the remote device before any
|
||||
changes are made. If the C(backup_options) value is not given,
|
||||
the backup file is written to the C(backup) folder in the playbook
|
||||
root directory. 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 cnos_config'
|
||||
admin:
|
||||
description:
|
||||
- Enters into administration configuration mode for making config
|
||||
changes to the device.
|
||||
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 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
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
Tasks: The following are examples of using the module cnos_config.
|
||||
---
|
||||
- name: configure top level configuration
|
||||
cnos_config:
|
||||
"lines: hostname {{ inventory_hostname }}"
|
||||
|
||||
- name: configure interface settings
|
||||
cnos_config:
|
||||
lines:
|
||||
- enable
|
||||
- ip ospf enable
|
||||
parents: interface ip 13
|
||||
|
||||
- name: load a config from disk and replace the current config
|
||||
cnos_config:
|
||||
src: config.cfg
|
||||
backup: yes
|
||||
|
||||
- name: configurable backup path
|
||||
cnos_config:
|
||||
src: config.cfg
|
||||
backup: yes
|
||||
backup_options:
|
||||
filename: backup.cfg
|
||||
dir_path: /home/user
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
updates:
|
||||
description: The set of commands that will be pushed to the remote device
|
||||
returned: Only when lines is specified.
|
||||
type: list
|
||||
sample: ['...', '...']
|
||||
backup_path:
|
||||
description: The full path to the backup file
|
||||
returned: when backup is yes
|
||||
type: str
|
||||
sample: /playbooks/ansible/backup/cnos01.2016-07-16@22:28:34
|
||||
"""
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import load_config, get_config
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import check_args
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import NetworkConfig, dumps
|
||||
|
||||
|
||||
DEFAULT_COMMIT_COMMENT = 'configured by cnos_config'
|
||||
|
||||
|
||||
def get_running_config(module):
|
||||
contents = module.params['config']
|
||||
if not contents:
|
||||
contents = get_config(module)
|
||||
return NetworkConfig(indent=1, contents=contents)
|
||||
|
||||
|
||||
def get_candidate(module):
|
||||
candidate = NetworkConfig(indent=1)
|
||||
if module.params['src']:
|
||||
candidate.load(module.params['src'])
|
||||
elif module.params['lines']:
|
||||
parents = module.params['parents'] or list()
|
||||
candidate.add(module.params['lines'], parents=parents)
|
||||
return candidate
|
||||
|
||||
|
||||
def run(module, result):
|
||||
match = module.params['match']
|
||||
replace = module.params['replace']
|
||||
replace_config = replace == 'config'
|
||||
path = module.params['parents']
|
||||
comment = module.params['comment']
|
||||
admin = module.params['admin']
|
||||
check_mode = module.check_mode
|
||||
|
||||
candidate = get_candidate(module)
|
||||
|
||||
if match != 'none' and replace != 'config':
|
||||
contents = get_running_config(module)
|
||||
configobj = NetworkConfig(contents=contents, indent=1)
|
||||
commands = candidate.difference(configobj, path=path, match=match,
|
||||
replace=replace)
|
||||
else:
|
||||
commands = candidate.items
|
||||
|
||||
if commands:
|
||||
commands = dumps(commands, 'commands').split('\n')
|
||||
|
||||
if any((module.params['lines'], module.params['src'])):
|
||||
if module.params['before']:
|
||||
commands[:0] = module.params['before']
|
||||
|
||||
if module.params['after']:
|
||||
commands.extend(module.params['after'])
|
||||
|
||||
result['commands'] = commands
|
||||
|
||||
diff = load_config(module, commands)
|
||||
if diff:
|
||||
result['diff'] = dict(prepared=diff)
|
||||
result['changed'] = True
|
||||
|
||||
|
||||
def main():
|
||||
"""main entry point for module execution
|
||||
"""
|
||||
backup_spec = dict(
|
||||
filename=dict(),
|
||||
dir_path=dict(type='path')
|
||||
)
|
||||
argument_spec = dict(
|
||||
src=dict(type='path'),
|
||||
|
||||
lines=dict(aliases=['commands'], type='list'),
|
||||
parents=dict(type='list'),
|
||||
|
||||
before=dict(type='list'),
|
||||
after=dict(type='list'),
|
||||
|
||||
match=dict(default='line', choices=['line', 'strict',
|
||||
'exact', 'none']),
|
||||
replace=dict(default='line', choices=['line', 'block', 'config']),
|
||||
|
||||
config=dict(),
|
||||
backup=dict(type='bool', default=False),
|
||||
backup_options=dict(type='dict', options=backup_spec),
|
||||
comment=dict(default=DEFAULT_COMMIT_COMMENT),
|
||||
admin=dict(type='bool', default=False)
|
||||
)
|
||||
|
||||
mutually_exclusive = [('lines', 'src'),
|
||||
('parents', 'src')]
|
||||
|
||||
required_if = [('match', 'strict', ['lines']),
|
||||
('match', 'exact', ['lines']),
|
||||
('replace', 'block', ['lines']),
|
||||
('replace', 'config', ['src'])]
|
||||
|
||||
module = AnsibleModule(argument_spec=argument_spec,
|
||||
mutually_exclusive=mutually_exclusive,
|
||||
required_if=required_if,
|
||||
supports_check_mode=True)
|
||||
|
||||
warnings = list()
|
||||
check_args(module, warnings)
|
||||
|
||||
result = dict(changed=False, warnings=warnings)
|
||||
|
||||
if module.params['backup']:
|
||||
result['__backup__'] = get_config(module)
|
||||
|
||||
run(module, result)
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
116
plugins/modules/network/cnos/cnos_factory.py
Normal file
116
plugins/modules/network/cnos/cnos_factory.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
#
|
||||
# Copyright (C) 2017 Lenovo, Inc.
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
# Module to Reset to factory settings of Lenovo Switches
|
||||
# Lenovo Networking
|
||||
#
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: cnos_factory
|
||||
author: "Anil Kumar Muraleedharan (@amuraleedhar)"
|
||||
short_description: Reset the switch startup configuration to default (factory)
|
||||
on devices running Lenovo CNOS.
|
||||
description:
|
||||
- This module allows you to reset a switch's startup configuration. The
|
||||
method provides a way to reset the startup configuration to its factory
|
||||
settings. This is helpful when you want to move the switch to another
|
||||
topology as a new network device. This module uses SSH to manage network
|
||||
device configuration. The result of the operation can be viewed in results
|
||||
directory.
|
||||
extends_documentation_fragment:
|
||||
- community.general.cnos
|
||||
|
||||
options: {}
|
||||
|
||||
'''
|
||||
EXAMPLES = '''
|
||||
Tasks : The following are examples of using the module cnos_reload. These are
|
||||
written in the main.yml file of the tasks directory.
|
||||
---
|
||||
- name: Test Reset to factory
|
||||
cnos_factory:
|
||||
deviceType: "{{ hostvars[inventory_hostname]['deviceType'] }}"
|
||||
outputfile: "./results/test_factory_{{ inventory_hostname }}_output.txt"
|
||||
|
||||
'''
|
||||
RETURN = '''
|
||||
msg:
|
||||
description: Success or failure message
|
||||
returned: always
|
||||
type: str
|
||||
sample: "Switch Startup Config is Reset to factory settings"
|
||||
'''
|
||||
|
||||
import sys
|
||||
import time
|
||||
import socket
|
||||
import array
|
||||
import json
|
||||
import time
|
||||
import re
|
||||
try:
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos import cnos
|
||||
HAS_LIB = True
|
||||
except Exception:
|
||||
HAS_LIB = False
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
outputfile=dict(required=True),
|
||||
host=dict(required=False),
|
||||
username=dict(required=False),
|
||||
password=dict(required=False, no_log=True),
|
||||
enablePassword=dict(required=False, no_log=True),
|
||||
deviceType=dict(required=True),),
|
||||
supports_check_mode=False)
|
||||
|
||||
command = 'write erase'
|
||||
outputfile = module.params['outputfile']
|
||||
output = ''
|
||||
cmd = [{'command': command, 'prompt': '[n]', 'answer': 'y'}]
|
||||
output = output + str(cnos.run_cnos_commands(module, cmd))
|
||||
|
||||
# Save it into the file
|
||||
file = open(outputfile, "a")
|
||||
file.write(output)
|
||||
file.close()
|
||||
|
||||
errorMsg = cnos.checkOutputForError(output)
|
||||
if(errorMsg is None):
|
||||
module.exit_json(changed=True,
|
||||
msg="Switch Startup Config is Reset to Factory settings")
|
||||
else:
|
||||
module.fail_json(msg=errorMsg)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
539
plugins/modules/network/cnos/cnos_facts.py
Normal file
539
plugins/modules/network/cnos/cnos_facts.py
Normal file
|
|
@ -0,0 +1,539 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# (C) 2019 Red Hat Inc.
|
||||
# Copyright (C) 2019 Lenovo.
|
||||
#
|
||||
# GNU General Public License v3.0+
|
||||
#
|
||||
# This program 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.
|
||||
#
|
||||
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# Module to Collect facts from Lenovo Switches running Lenovo CNOS commands
|
||||
# Lenovo Networking
|
||||
#
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: cnos_facts
|
||||
author: "Anil Kumar Muraleedharan (@amuraleedhar)"
|
||||
short_description: Collect facts from remote devices running Lenovo CNOS
|
||||
description:
|
||||
- Collects a base set of device facts from a remote Lenovo device
|
||||
running on CNOS. This module prepends all of the
|
||||
base network fact keys with C(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.
|
||||
notes:
|
||||
- Tested against CNOS 10.8.1
|
||||
options:
|
||||
authorize:
|
||||
description:
|
||||
- Instructs the module to enter privileged mode on the remote device
|
||||
before sending any commands. If not specified, the device will
|
||||
attempt to execute all commands in non-privileged mode. If the value
|
||||
is not specified in the task, the value of environment variable
|
||||
C(ANSIBLE_NET_AUTHORIZE) will be used instead.
|
||||
type: bool
|
||||
default: 'no'
|
||||
auth_pass:
|
||||
description:
|
||||
- Specifies the password to use if required to enter privileged mode
|
||||
on the remote device. If I(authorize) is false, then this argument
|
||||
does nothing. If the value is not specified in the task, the value of
|
||||
environment variable C(ANSIBLE_NET_AUTH_PASS) will be used instead.
|
||||
gather_subset:
|
||||
description:
|
||||
- When supplied, this argument will restrict the facts collected
|
||||
to a given subset. Possible values for this argument include
|
||||
all, hardware, config, and interfaces. 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 = '''
|
||||
Tasks: The following are examples of using the module cnos_facts.
|
||||
---
|
||||
- name: Test cnos Facts
|
||||
cnos_facts:
|
||||
|
||||
---
|
||||
# Collect all facts from the device
|
||||
- cnos_facts:
|
||||
gather_subset: all
|
||||
|
||||
# Collect only the config and default facts
|
||||
- cnos_facts:
|
||||
gather_subset:
|
||||
- config
|
||||
|
||||
# Do not collect hardware facts
|
||||
- cnos_facts:
|
||||
gather_subset:
|
||||
- "!hardware"
|
||||
'''
|
||||
RETURN = '''
|
||||
ansible_net_gather_subset:
|
||||
description: The list of fact subsets collected from the device
|
||||
returned: always
|
||||
type: list
|
||||
# default
|
||||
ansible_net_model:
|
||||
description: The model name returned from the Lenovo CNOS device
|
||||
returned: always
|
||||
type: str
|
||||
ansible_net_serialnum:
|
||||
description: The serial number of the Lenovo CNOS device
|
||||
returned: always
|
||||
type: str
|
||||
ansible_net_version:
|
||||
description: The CNOS operating system version running on the remote device
|
||||
returned: always
|
||||
type: str
|
||||
ansible_net_hostname:
|
||||
description: The configured hostname of the device
|
||||
returned: always
|
||||
type: str
|
||||
ansible_net_image:
|
||||
description: Indicates the active image for the device
|
||||
returned: always
|
||||
type: str
|
||||
# hardware
|
||||
ansible_net_memfree_mb:
|
||||
description: The available free memory on the remote device in MB
|
||||
returned: when hardware is configured
|
||||
type: int
|
||||
# config
|
||||
ansible_net_config:
|
||||
description: The current active config from the device
|
||||
returned: when config is configured
|
||||
type: str
|
||||
# interfaces
|
||||
ansible_net_all_ipv4_addresses:
|
||||
description: All IPv4 addresses configured on the device
|
||||
returned: when interfaces is configured
|
||||
type: list
|
||||
ansible_net_all_ipv6_addresses:
|
||||
description: All IPv6 addresses configured on the device
|
||||
returned: when interfaces is configured
|
||||
type: list
|
||||
ansible_net_interfaces:
|
||||
description: A hash of all interfaces running on the system.
|
||||
This gives information on description, mac address, mtu, speed,
|
||||
duplex and operstatus
|
||||
returned: when interfaces is configured
|
||||
type: dict
|
||||
ansible_net_neighbors:
|
||||
description: The list of LLDP neighbors from the remote device
|
||||
returned: when interfaces is configured
|
||||
type: dict
|
||||
'''
|
||||
|
||||
import re
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import run_commands
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import check_args
|
||||
from ansible.module_utils._text import to_text
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.six import iteritems
|
||||
from ansible.module_utils.six.moves import zip
|
||||
|
||||
|
||||
class FactsBase(object):
|
||||
|
||||
COMMANDS = list()
|
||||
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
self.facts = dict()
|
||||
self.responses = None
|
||||
self.PERSISTENT_COMMAND_TIMEOUT = 60
|
||||
|
||||
def populate(self):
|
||||
self.responses = run_commands(self.module, self.COMMANDS,
|
||||
check_rc=False)
|
||||
|
||||
def run(self, cmd):
|
||||
return run_commands(self.module, cmd, check_rc=False)
|
||||
|
||||
|
||||
class Default(FactsBase):
|
||||
|
||||
COMMANDS = ['show sys-info', 'show running-config']
|
||||
|
||||
def populate(self):
|
||||
super(Default, self).populate()
|
||||
data = self.responses[0]
|
||||
data_run = self.responses[1]
|
||||
if data:
|
||||
self.facts['version'] = self.parse_version(data)
|
||||
self.facts['serialnum'] = self.parse_serialnum(data)
|
||||
self.facts['model'] = self.parse_model(data)
|
||||
self.facts['image'] = self.parse_image(data)
|
||||
if data_run:
|
||||
self.facts['hostname'] = self.parse_hostname(data_run)
|
||||
|
||||
def parse_version(self, data):
|
||||
for line in data.split('\n'):
|
||||
line = line.strip()
|
||||
match = re.match(r'System Software Revision (.*?)',
|
||||
line, re.M | re.I)
|
||||
if match:
|
||||
vers = line.split(':')
|
||||
ver = vers[1].strip()
|
||||
return ver
|
||||
return "NA"
|
||||
|
||||
def parse_hostname(self, data_run):
|
||||
for line in data_run.split('\n'):
|
||||
line = line.strip()
|
||||
match = re.match(r'hostname (.*?)', line, re.M | re.I)
|
||||
if match:
|
||||
hosts = line.split()
|
||||
hostname = hosts[1].strip('\"')
|
||||
return hostname
|
||||
return "NA"
|
||||
|
||||
def parse_model(self, data):
|
||||
for line in data.split('\n'):
|
||||
line = line.strip()
|
||||
match = re.match(r'System Model (.*?)', line, re.M | re.I)
|
||||
if match:
|
||||
mdls = line.split(':')
|
||||
mdl = mdls[1].strip()
|
||||
return mdl
|
||||
return "NA"
|
||||
|
||||
def parse_image(self, data):
|
||||
match = re.search(r'(.*) image(.*)', data, re.M | re.I)
|
||||
if match:
|
||||
return "Image1"
|
||||
else:
|
||||
return "Image2"
|
||||
|
||||
def parse_serialnum(self, data):
|
||||
for line in data.split('\n'):
|
||||
line = line.strip()
|
||||
match = re.match(r'System Serial Number (.*?)', line, re.M | re.I)
|
||||
if match:
|
||||
serNums = line.split(':')
|
||||
ser = serNums[1].strip()
|
||||
return ser
|
||||
return "NA"
|
||||
|
||||
|
||||
class Hardware(FactsBase):
|
||||
|
||||
COMMANDS = [
|
||||
'show running-config'
|
||||
]
|
||||
|
||||
def populate(self):
|
||||
super(Hardware, self).populate()
|
||||
data = self.run(['show process memory'])
|
||||
data = to_text(data, errors='surrogate_or_strict').strip()
|
||||
data = data.replace(r"\n", "\n")
|
||||
if data:
|
||||
for line in data.split('\n'):
|
||||
line = line.strip()
|
||||
match = re.match(r'Mem: (.*?)', line, re.M | re.I)
|
||||
if match:
|
||||
memline = line.split(':')
|
||||
mems = memline[1].strip().split()
|
||||
self.facts['memtotal_mb'] = int(mems[0]) / 1024
|
||||
self.facts['memused_mb'] = int(mems[1]) / 1024
|
||||
self.facts['memfree_mb'] = int(mems[2]) / 1024
|
||||
self.facts['memshared_mb'] = int(mems[3]) / 1024
|
||||
self.facts['memavailable_mb'] = int(mems[5]) / 1024
|
||||
|
||||
def parse_memtotal(self, data):
|
||||
match = re.search(r'^MemTotal:\s*(.*) kB', data, re.M | re.I)
|
||||
if match:
|
||||
return int(match.group(1)) / 1024
|
||||
|
||||
def parse_memfree(self, data):
|
||||
match = re.search(r'^MemFree:\s*(.*) kB', data, re.M | re.I)
|
||||
if match:
|
||||
return int(match.group(1)) / 1024
|
||||
|
||||
|
||||
class Config(FactsBase):
|
||||
|
||||
COMMANDS = ['show running-config']
|
||||
|
||||
def populate(self):
|
||||
super(Config, self).populate()
|
||||
data = self.responses[0]
|
||||
if data:
|
||||
self.facts['config'] = data
|
||||
|
||||
|
||||
class Interfaces(FactsBase):
|
||||
|
||||
COMMANDS = ['show interface brief']
|
||||
|
||||
def populate(self):
|
||||
super(Interfaces, self).populate()
|
||||
|
||||
self.facts['all_ipv4_addresses'] = list()
|
||||
self.facts['all_ipv6_addresses'] = list()
|
||||
|
||||
data1 = self.run(['show interface status'])
|
||||
data1 = to_text(data1, errors='surrogate_or_strict').strip()
|
||||
data1 = data1.replace(r"\n", "\n")
|
||||
data2 = self.run(['show interface mac-address'])
|
||||
data2 = to_text(data2, errors='surrogate_or_strict').strip()
|
||||
data2 = data2.replace(r"\n", "\n")
|
||||
lines1 = None
|
||||
lines2 = None
|
||||
if data1:
|
||||
lines1 = self.parse_interfaces(data1)
|
||||
if data2:
|
||||
lines2 = self.parse_interfaces(data2)
|
||||
if lines1 is not None and lines2 is not None:
|
||||
self.facts['interfaces'] = self.populate_interfaces(lines1, lines2)
|
||||
data3 = self.run(['show lldp neighbors'])
|
||||
data3 = to_text(data3, errors='surrogate_or_strict').strip()
|
||||
data3 = data3.replace(r"\n", "\n")
|
||||
if data3:
|
||||
lines3 = self.parse_neighbors(data3)
|
||||
if lines3 is not None:
|
||||
self.facts['neighbors'] = self.populate_neighbors(lines3)
|
||||
|
||||
data4 = self.run(['show ip interface brief vrf all'])
|
||||
data5 = self.run(['show ipv6 interface brief vrf all'])
|
||||
data4 = to_text(data4, errors='surrogate_or_strict').strip()
|
||||
data4 = data4.replace(r"\n", "\n")
|
||||
data5 = to_text(data5, errors='surrogate_or_strict').strip()
|
||||
data5 = data5.replace(r"\n", "\n")
|
||||
lines4 = None
|
||||
lines5 = None
|
||||
if data4:
|
||||
lines4 = self.parse_ipaddresses(data4)
|
||||
ipv4_interfaces = self.set_ip_interfaces(lines4)
|
||||
self.facts['all_ipv4_addresses'] = ipv4_interfaces
|
||||
if data5:
|
||||
lines5 = self.parse_ipaddresses(data5)
|
||||
ipv6_interfaces = self.set_ipv6_interfaces(lines5)
|
||||
self.facts['all_ipv6_addresses'] = ipv6_interfaces
|
||||
|
||||
def parse_ipaddresses(self, data):
|
||||
parsed = list()
|
||||
for line in data.split('\n'):
|
||||
if len(line) == 0:
|
||||
continue
|
||||
else:
|
||||
line = line.strip()
|
||||
match = re.match(r'^(Ethernet+)', line)
|
||||
if match:
|
||||
key = match.group(1)
|
||||
parsed.append(line)
|
||||
match = re.match(r'^(po+)', line)
|
||||
if match:
|
||||
key = match.group(1)
|
||||
parsed.append(line)
|
||||
match = re.match(r'^(mgmt+)', line)
|
||||
if match:
|
||||
key = match.group(1)
|
||||
parsed.append(line)
|
||||
match = re.match(r'^(loopback+)', line)
|
||||
if match:
|
||||
key = match.group(1)
|
||||
parsed.append(line)
|
||||
return parsed
|
||||
|
||||
def populate_interfaces(self, lines1, lines2):
|
||||
interfaces = dict()
|
||||
for line1, line2 in zip(lines1, lines2):
|
||||
line = line1 + " " + line2
|
||||
intfSplit = line.split()
|
||||
innerData = dict()
|
||||
innerData['description'] = intfSplit[1].strip()
|
||||
innerData['macaddress'] = intfSplit[8].strip()
|
||||
innerData['type'] = intfSplit[6].strip()
|
||||
innerData['speed'] = intfSplit[5].strip()
|
||||
innerData['duplex'] = intfSplit[4].strip()
|
||||
innerData['operstatus'] = intfSplit[2].strip()
|
||||
interfaces[intfSplit[0].strip()] = innerData
|
||||
return interfaces
|
||||
|
||||
def parse_interfaces(self, data):
|
||||
parsed = list()
|
||||
for line in data.split('\n'):
|
||||
if len(line) == 0:
|
||||
continue
|
||||
else:
|
||||
line = line.strip()
|
||||
match = re.match(r'^(Ethernet+)', line)
|
||||
if match:
|
||||
key = match.group(1)
|
||||
parsed.append(line)
|
||||
match = re.match(r'^(po+)', line)
|
||||
if match:
|
||||
key = match.group(1)
|
||||
parsed.append(line)
|
||||
match = re.match(r'^(mgmt+)', line)
|
||||
if match:
|
||||
key = match.group(1)
|
||||
parsed.append(line)
|
||||
return parsed
|
||||
|
||||
def set_ip_interfaces(self, line4):
|
||||
ipv4_addresses = list()
|
||||
for line in line4:
|
||||
ipv4Split = line.split()
|
||||
if 'Ethernet' in ipv4Split[0]:
|
||||
ipv4_addresses.append(ipv4Split[1])
|
||||
if 'mgmt' in ipv4Split[0]:
|
||||
ipv4_addresses.append(ipv4Split[1])
|
||||
if 'po' in ipv4Split[0]:
|
||||
ipv4_addresses.append(ipv4Split[1])
|
||||
if 'loopback' in ipv4Split[0]:
|
||||
ipv4_addresses.append(ipv4Split[1])
|
||||
return ipv4_addresses
|
||||
|
||||
def set_ipv6_interfaces(self, line4):
|
||||
ipv6_addresses = list()
|
||||
for line in line4:
|
||||
ipv6Split = line.split()
|
||||
if 'Ethernet' in ipv6Split[0]:
|
||||
ipv6_addresses.append(ipv6Split[1])
|
||||
if 'mgmt' in ipv6Split[0]:
|
||||
ipv6_addresses.append(ipv6Split[1])
|
||||
if 'po' in ipv6Split[0]:
|
||||
ipv6_addresses.append(ipv6Split[1])
|
||||
if 'loopback' in ipv6Split[0]:
|
||||
ipv6_addresses.append(ipv6Split[1])
|
||||
return ipv6_addresses
|
||||
|
||||
def populate_neighbors(self, lines3):
|
||||
neighbors = dict()
|
||||
device_name = ''
|
||||
for line in lines3:
|
||||
neighborSplit = line.split()
|
||||
innerData = dict()
|
||||
count = len(neighborSplit)
|
||||
if count == 5:
|
||||
local_interface = neighborSplit[1].strip()
|
||||
innerData['Device Name'] = neighborSplit[0].strip()
|
||||
innerData['Hold Time'] = neighborSplit[2].strip()
|
||||
innerData['Capability'] = neighborSplit[3].strip()
|
||||
innerData['Remote Port'] = neighborSplit[4].strip()
|
||||
neighbors[local_interface] = innerData
|
||||
elif count == 4:
|
||||
local_interface = neighborSplit[0].strip()
|
||||
innerData['Hold Time'] = neighborSplit[1].strip()
|
||||
innerData['Capability'] = neighborSplit[2].strip()
|
||||
innerData['Remote Port'] = neighborSplit[3].strip()
|
||||
neighbors[local_interface] = innerData
|
||||
return neighbors
|
||||
|
||||
def parse_neighbors(self, neighbors):
|
||||
parsed = list()
|
||||
for line in neighbors.split('\n'):
|
||||
if len(line) == 0:
|
||||
continue
|
||||
else:
|
||||
line = line.strip()
|
||||
if 'Ethernet' in line:
|
||||
parsed.append(line)
|
||||
if 'mgmt' in line:
|
||||
parsed.append(line)
|
||||
if 'po' in line:
|
||||
parsed.append(line)
|
||||
if 'loopback' in line:
|
||||
parsed.append(line)
|
||||
return parsed
|
||||
|
||||
|
||||
FACT_SUBSETS = dict(
|
||||
default=Default,
|
||||
hardware=Hardware,
|
||||
interfaces=Interfaces,
|
||||
config=Config,
|
||||
)
|
||||
|
||||
VALID_SUBSETS = frozenset(FACT_SUBSETS.keys())
|
||||
|
||||
PERSISTENT_COMMAND_TIMEOUT = 60
|
||||
|
||||
|
||||
def main():
|
||||
"""main entry point for module execution
|
||||
"""
|
||||
argument_spec = dict(
|
||||
gather_subset=dict(default=['!config'], type='list')
|
||||
)
|
||||
|
||||
module = AnsibleModule(argument_spec=argument_spec,
|
||||
supports_check_mode=True)
|
||||
|
||||
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='Bad 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
|
||||
|
||||
warnings = list()
|
||||
check_args(module, warnings)
|
||||
|
||||
module.exit_json(ansible_facts=ansible_facts, warnings=warnings)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
241
plugins/modules/network/cnos/cnos_image.py
Normal file
241
plugins/modules/network/cnos/cnos_image.py
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
#
|
||||
# Copyright (C) 2017 Lenovo, Inc.
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
# Module to download new image to Lenovo Switches
|
||||
# Lenovo Networking
|
||||
#
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: cnos_image
|
||||
author: "Anil Kumar Muraleedharan (@amuraleedhar)"
|
||||
short_description: Perform firmware upgrade/download from a remote server on
|
||||
devices running Lenovo CNOS
|
||||
description:
|
||||
- This module allows you to work with switch firmware images. It provides a
|
||||
way to download a firmware image to a network device from a remote server
|
||||
using FTP, SFTP, TFTP, or SCP. The first step is to create a directory
|
||||
from where the remote server can be reached. The next step is to provide
|
||||
the full file path of the image's location. Authentication details
|
||||
required by the remote server must be provided as well. By default, this
|
||||
method makes the newly downloaded firmware image the active image, which
|
||||
will be used by the switch during the next restart.
|
||||
This module uses SSH to manage network device configuration.
|
||||
The results of the operation will be placed in a directory named 'results'
|
||||
that must be created by the user in their local directory to where the
|
||||
playbook is run.
|
||||
extends_documentation_fragment:
|
||||
- community.general.cnos
|
||||
|
||||
options:
|
||||
protocol:
|
||||
description:
|
||||
- This refers to the protocol used by the network device to
|
||||
interact with the remote server from where to download the
|
||||
firmware image. The choices are FTP, SFTP, TFTP, or SCP. Any other
|
||||
protocols will result in error. If this parameter is not specified
|
||||
there is no default value to be used.
|
||||
required: true
|
||||
choices: [SFTP, SCP, FTP, TFTP]
|
||||
serverip:
|
||||
description:
|
||||
- This specifies the IP Address of the remote server from where the
|
||||
software image will be downloaded.
|
||||
required: true
|
||||
imgpath:
|
||||
description:
|
||||
- This specifies the full file path of the image located on the
|
||||
remote server. In case the relative path is used as the variable
|
||||
value, the root folder for the user of the server needs to be
|
||||
specified.
|
||||
required: true
|
||||
imgtype:
|
||||
description:
|
||||
- This specifies the firmware image type to be downloaded
|
||||
required: true
|
||||
choices: [all, boot, os, onie]
|
||||
serverusername:
|
||||
description:
|
||||
- Specify the username for the server relating to the protocol used
|
||||
required: true
|
||||
serverpassword:
|
||||
description:
|
||||
- Specify the password for the server relating to the protocol used
|
||||
'''
|
||||
EXAMPLES = '''
|
||||
Tasks : The following are examples of using the module cnos_image. These are
|
||||
written in the main.yml file of the tasks directory.
|
||||
---
|
||||
- name: Test Image transfer
|
||||
cnos_image:
|
||||
deviceType: "{{ hostvars[inventory_hostname]['deviceType'] }}"
|
||||
outputfile: "./results/test_image_{{ inventory_hostname }}_output.txt"
|
||||
protocol: "sftp"
|
||||
serverip: "10.241.106.118"
|
||||
imgpath: "/root/cnos_images/G8272-10.1.0.112.img"
|
||||
imgtype: "os"
|
||||
serverusername: "root"
|
||||
serverpassword: "root123"
|
||||
|
||||
- name: Test Image tftp
|
||||
cnos_image:
|
||||
deviceType: "{{ hostvars[inventory_hostname]['deviceType'] }}"
|
||||
outputfile: "./results/test_image_{{ inventory_hostname }}_output.txt"
|
||||
protocol: "tftp"
|
||||
serverip: "10.241.106.118"
|
||||
imgpath: "/anil/G8272-10.2.0.34.img"
|
||||
imgtype: "os"
|
||||
serverusername: "root"
|
||||
serverpassword: "root123"
|
||||
'''
|
||||
RETURN = '''
|
||||
msg:
|
||||
description: Success or failure message
|
||||
returned: always
|
||||
type: str
|
||||
sample: "Image file transferred to device"
|
||||
'''
|
||||
|
||||
import sys
|
||||
import time
|
||||
import socket
|
||||
import array
|
||||
import json
|
||||
import time
|
||||
import re
|
||||
import os
|
||||
try:
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos import cnos
|
||||
HAS_LIB = True
|
||||
except Exception:
|
||||
HAS_LIB = False
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
def doImageDownload(module, prompt, answer):
|
||||
protocol = module.params['protocol'].lower()
|
||||
server = module.params['serverip']
|
||||
imgPath = module.params['imgpath']
|
||||
imgType = module.params['imgtype']
|
||||
username = module.params['serverusername']
|
||||
password = module.params['serverpassword']
|
||||
retVal = ''
|
||||
command = "copy " + protocol + " " + protocol + "://" + username + "@"
|
||||
command = command + server + "/" + imgPath + " system-image "
|
||||
command = command + imgType + " vrf management"
|
||||
cmd = []
|
||||
if(protocol == "scp"):
|
||||
prompt = ['timeout', 'Confirm download operation', 'Password',
|
||||
'Do you want to change that to the standby image']
|
||||
answer = ['240', 'y', password, 'y']
|
||||
scp_cmd = [{'command': command, 'prompt': prompt, 'answer': answer,
|
||||
'check_all': True}]
|
||||
cmd.extend(scp_cmd)
|
||||
retVal = retVal + str(cnos.run_cnos_commands(module, cmd))
|
||||
elif(protocol == "sftp"):
|
||||
prompt = ['Confirm download operation', 'Password',
|
||||
'Do you want to change that to the standby image']
|
||||
answer = ['y', password, 'y']
|
||||
sftp_cmd = [{'command': command, 'prompt': prompt, 'answer': answer,
|
||||
'check_all': True}]
|
||||
cmd.extend(sftp_cmd)
|
||||
retVal = retVal + str(cnos.run_cnos_commands(module, cmd))
|
||||
elif(protocol == "ftp"):
|
||||
prompt = ['Confirm download operation', 'Password',
|
||||
'Do you want to change that to the standby image']
|
||||
answer = ['y', password, 'y']
|
||||
ftp_cmd = [{'command': command, 'prompt': prompt, 'answer': answer,
|
||||
'check_all': True}]
|
||||
cmd.extend(ftp_cmd)
|
||||
retVal = retVal + str(cnos.run_cnos_commands(module, cmd))
|
||||
elif(protocol == "tftp"):
|
||||
command = "copy " + protocol + " " + protocol + "://" + server
|
||||
command = command + "/" + imgPath + " system-image " + imgType
|
||||
command = command + " vrf management"
|
||||
prompt = ['Confirm download operation',
|
||||
'Do you want to change that to the standby image']
|
||||
answer = ['y', 'y']
|
||||
tftp_cmd = [{'command': command, 'prompt': prompt, 'answer': answer,
|
||||
'check_all': True}]
|
||||
cmd.extend(tftp_cmd)
|
||||
retVal = retVal + str(cnos.run_cnos_commands(module, cmd))
|
||||
else:
|
||||
return "Error-110"
|
||||
|
||||
return retVal
|
||||
# EOM
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
outputfile=dict(required=True),
|
||||
host=dict(required=False),
|
||||
username=dict(required=False),
|
||||
password=dict(required=False, no_log=True),
|
||||
enablePassword=dict(required=False, no_log=True),
|
||||
deviceType=dict(required=True),
|
||||
protocol=dict(required=True),
|
||||
serverip=dict(required=True),
|
||||
imgpath=dict(required=True),
|
||||
imgtype=dict(required=True),
|
||||
serverusername=dict(required=False),
|
||||
serverpassword=dict(required=False, no_log=True),),
|
||||
supports_check_mode=False)
|
||||
|
||||
outputfile = module.params['outputfile']
|
||||
protocol = module.params['protocol'].lower()
|
||||
output = ''
|
||||
|
||||
# Invoke method for image transfer from server
|
||||
if(protocol == "tftp" or protocol == "ftp" or protocol == "sftp" or
|
||||
protocol == "scp"):
|
||||
transfer_status = doImageDownload(module, None, None)
|
||||
else:
|
||||
transfer_status = "Invalid Protocol option"
|
||||
|
||||
output = output + "\n Image Transfer status \n" + transfer_status
|
||||
|
||||
# Save it into the file
|
||||
path = outputfile.rsplit('/', 1)
|
||||
if not os.path.exists(path[0]):
|
||||
os.makedirs(path[0])
|
||||
file = open(outputfile, "a")
|
||||
file.write(output)
|
||||
file.close()
|
||||
|
||||
# Logic to check when changes occur or not
|
||||
errorMsg = cnos.checkOutputForError(output)
|
||||
if(errorMsg is None):
|
||||
module.exit_json(changed=True, msg="Image file transferred to device")
|
||||
else:
|
||||
module.fail_json(msg=errorMsg)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
555
plugins/modules/network/cnos/cnos_interface.py
Normal file
555
plugins/modules/network/cnos/cnos_interface.py
Normal file
|
|
@ -0,0 +1,555 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2017 Lenovo, Inc.
|
||||
# (c) 2017, Ansible by Red Hat, inc
|
||||
# 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/>.
|
||||
#
|
||||
# Module to work on Interfaces with Lenovo Switches
|
||||
# Lenovo Networking
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: cnos_interface
|
||||
author: "Anil Kumar Muraleedharan(@amuraleedhar)"
|
||||
short_description: Manage Interface on Lenovo CNOS network devices
|
||||
description:
|
||||
- This module provides declarative management of Interfaces
|
||||
on Lenovo CNOS network devices.
|
||||
notes:
|
||||
- Tested against CNOS 10.8.1
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name of the Interface.
|
||||
required: true
|
||||
description:
|
||||
description:
|
||||
- Description of Interface.
|
||||
enabled:
|
||||
description:
|
||||
- Interface link status.
|
||||
type: bool
|
||||
default: True
|
||||
speed:
|
||||
description:
|
||||
- Interface link speed.
|
||||
mtu:
|
||||
description:
|
||||
- Maximum size of transmit packet.
|
||||
duplex:
|
||||
description:
|
||||
- Interface link status
|
||||
default: auto
|
||||
choices: ['full', 'half', 'auto']
|
||||
tx_rate:
|
||||
description:
|
||||
- Transmit rate in bits per second (bps).
|
||||
- This is state check parameter only.
|
||||
- Supports conditionals, see L(Conditionals in Networking Modules,
|
||||
../network/user_guide/network_working_with_command_output.html)
|
||||
rx_rate:
|
||||
description:
|
||||
- Receiver rate in bits per second (bps).
|
||||
- This is state check parameter only.
|
||||
- Supports conditionals, see L(Conditionals in Networking Modules,
|
||||
../network/user_guide/network_working_with_command_output.html)
|
||||
neighbors:
|
||||
description:
|
||||
- Check 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 interface C(name) is connected."
|
||||
aggregate:
|
||||
description: List of Interfaces definitions.
|
||||
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), I(tx_rate) and I(rx_rate)
|
||||
default: 20
|
||||
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']
|
||||
provider:
|
||||
description:
|
||||
- B(Deprecated)
|
||||
- "Starting with Ansible 2.5 we recommend using C(connection: network_cli)."
|
||||
- For more information please see the L(CNOS Platform Options guide, ../network/user_guide/platform_cnos.html).
|
||||
- HORIZONTALLINE
|
||||
- A dict object containing connection details.
|
||||
suboptions:
|
||||
host:
|
||||
description:
|
||||
- Specifies the DNS host name or address for connecting to the remote
|
||||
device over the specified transport. The value of host is used as
|
||||
the destination address for the transport.
|
||||
required: true
|
||||
port:
|
||||
description:
|
||||
- Specifies the port to use when building the connection to the remote device.
|
||||
default: 22
|
||||
username:
|
||||
description:
|
||||
- Configures the username to use to authenticate the connection to
|
||||
the remote device. This value is used to authenticate
|
||||
the SSH session. If the value is not specified in the task, the
|
||||
value of environment variable C(ANSIBLE_NET_USERNAME) will be used instead.
|
||||
password:
|
||||
description:
|
||||
- Specifies the password to use to authenticate the connection to
|
||||
the remote device. This value is used to authenticate
|
||||
the SSH session. If the value is not specified in the task, the
|
||||
value of environment variable C(ANSIBLE_NET_PASSWORD) will be used instead.
|
||||
timeout:
|
||||
description:
|
||||
- Specifies the timeout in seconds for communicating with the network device
|
||||
for either connecting or sending commands. If the timeout is
|
||||
exceeded before the operation is completed, the module will error.
|
||||
default: 10
|
||||
ssh_keyfile:
|
||||
description:
|
||||
- Specifies the SSH key to use to authenticate the connection to
|
||||
the remote device. This value is the path to the
|
||||
key used to authenticate the SSH session. If the value is not specified
|
||||
in the task, the value of environment variable C(ANSIBLE_NET_SSH_KEYFILE)
|
||||
will be used instead.
|
||||
authorize:
|
||||
description:
|
||||
- Instructs the module to enter privileged mode on the remote device
|
||||
before sending any commands. If not specified, the device will
|
||||
attempt to execute all commands in non-privileged mode. If the value
|
||||
is not specified in the task, the value of environment variable
|
||||
C(ANSIBLE_NET_AUTHORIZE) will be used instead.
|
||||
type: bool
|
||||
default: 'no'
|
||||
auth_pass:
|
||||
description:
|
||||
- Specifies the password to use if required to enter privileged mode
|
||||
on the remote device. If I(authorize) is false, then this argument
|
||||
does nothing. If the value is not specified in the task, the value of
|
||||
environment variable C(ANSIBLE_NET_AUTH_PASS) will be used instead.
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: configure interface
|
||||
cnos_interface:
|
||||
name: Ethernet1/33
|
||||
description: test-interface
|
||||
speed: 100
|
||||
duplex: half
|
||||
mtu: 999
|
||||
|
||||
- name: remove interface
|
||||
cnos_interface:
|
||||
name: loopback3
|
||||
state: absent
|
||||
|
||||
- name: make interface up
|
||||
cnos_interface:
|
||||
name: Ethernet1/33
|
||||
enabled: True
|
||||
|
||||
- name: make interface down
|
||||
cnos_interface:
|
||||
name: Ethernet1/33
|
||||
enabled: False
|
||||
|
||||
- name: Check intent arguments
|
||||
cnos_interface:
|
||||
name: Ethernet1/33
|
||||
state: up
|
||||
tx_rate: ge(0)
|
||||
rx_rate: le(0)
|
||||
|
||||
- name: Check neighbors intent arguments
|
||||
cnos_interface:
|
||||
name: Ethernet1/33
|
||||
neighbors:
|
||||
- port: eth0
|
||||
host: netdev
|
||||
|
||||
- name: Config + intent
|
||||
cnos_interface:
|
||||
name: Ethernet1/33
|
||||
enabled: False
|
||||
state: down
|
||||
|
||||
- name: Add interface using aggregate
|
||||
cnos_interface:
|
||||
aggregate:
|
||||
- { name: Ethernet1/33, mtu: 256, description: test-interface-1 }
|
||||
- { name: Ethernet1/44, mtu: 516, description: test-interface-2 }
|
||||
duplex: full
|
||||
speed: 100
|
||||
state: present
|
||||
|
||||
- name: Delete interface using aggregate
|
||||
cnos_interface:
|
||||
aggregate:
|
||||
- name: loopback3
|
||||
- name: loopback6
|
||||
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:
|
||||
- interface Ethernet1/33
|
||||
- description test-interface
|
||||
- duplex half
|
||||
- mtu 512
|
||||
"""
|
||||
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_collections.community.general.plugins.module_utils.network.cnos.cnos import get_config, load_config
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import cnos_argument_spec
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import debugOutput, check_args
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import NetworkConfig
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import conditional
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import remove_default_spec
|
||||
|
||||
|
||||
def validate_mtu(value, module):
|
||||
if value and not 64 <= int(value) <= 9216:
|
||||
module.fail_json(msg='mtu must be between 64 and 9216')
|
||||
|
||||
|
||||
def validate_param_values(module, obj, param=None):
|
||||
if param is None:
|
||||
param = module.params
|
||||
for key in obj:
|
||||
# validate the param value (if validator func exists)
|
||||
validator = globals().get('validate_%s' % key)
|
||||
if callable(validator):
|
||||
validator(param.get(key), module)
|
||||
|
||||
|
||||
def parse_shutdown(configobj, name):
|
||||
cfg = configobj['interface %s' % name]
|
||||
cfg = '\n'.join(cfg.children)
|
||||
match = re.search(r'^shutdown', cfg, re.M)
|
||||
if match:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def parse_config_argument(configobj, name, arg=None):
|
||||
cfg = configobj['interface %s' % name]
|
||||
cfg = '\n'.join(cfg.children)
|
||||
match = re.search(r'%s (.+)$' % arg, cfg, re.M)
|
||||
if match:
|
||||
return match.group(1)
|
||||
|
||||
|
||||
def search_obj_in_list(name, lst):
|
||||
for o in lst:
|
||||
if o['name'] == name:
|
||||
return o
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def add_command_to_interface(interface, cmd, commands):
|
||||
if interface not in commands:
|
||||
commands.append(interface)
|
||||
commands.append(cmd)
|
||||
|
||||
|
||||
def map_config_to_obj(module):
|
||||
config = get_config(module)
|
||||
configobj = NetworkConfig(indent=1, contents=config)
|
||||
|
||||
match = re.findall(r'^interface (\S+)', config, re.M)
|
||||
if not match:
|
||||
return list()
|
||||
|
||||
instances = list()
|
||||
|
||||
for item in set(match):
|
||||
obj = {
|
||||
'name': item,
|
||||
'description': parse_config_argument(configobj, item, 'description'),
|
||||
'speed': parse_config_argument(configobj, item, 'speed'),
|
||||
'duplex': parse_config_argument(configobj, item, 'duplex'),
|
||||
'mtu': parse_config_argument(configobj, item, 'mtu'),
|
||||
'disable': True if parse_shutdown(configobj, item) else False,
|
||||
'state': 'present'
|
||||
}
|
||||
instances.append(obj)
|
||||
return instances
|
||||
|
||||
|
||||
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]
|
||||
|
||||
validate_param_values(module, item, item)
|
||||
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'],
|
||||
'state': module.params['state'],
|
||||
'delay': module.params['delay'],
|
||||
'tx_rate': module.params['tx_rate'],
|
||||
'rx_rate': module.params['rx_rate'],
|
||||
'neighbors': module.params['neighbors']
|
||||
}
|
||||
|
||||
validate_param_values(module, params)
|
||||
if module.params['enabled']:
|
||||
params.update({'disable': False})
|
||||
else:
|
||||
params.update({'disable': True})
|
||||
|
||||
obj.append(params)
|
||||
return obj
|
||||
|
||||
|
||||
def map_obj_to_commands(updates):
|
||||
commands = list()
|
||||
want, have = updates
|
||||
|
||||
args = ('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)
|
||||
interface = 'interface ' + name
|
||||
if state == 'absent' and obj_in_have:
|
||||
commands.append('no ' + interface)
|
||||
elif state in ('present', 'up', 'down'):
|
||||
if obj_in_have:
|
||||
for item in args:
|
||||
candidate = w.get(item)
|
||||
running = obj_in_have.get(item)
|
||||
if candidate != running:
|
||||
if candidate:
|
||||
cmd = item + ' ' + str(candidate)
|
||||
add_command_to_interface(interface, cmd, commands)
|
||||
|
||||
if disable and not obj_in_have.get('disable', False):
|
||||
add_command_to_interface(interface, 'shutdown', commands)
|
||||
elif not disable and obj_in_have.get('disable', False):
|
||||
add_command_to_interface(interface, 'no shutdown', commands)
|
||||
else:
|
||||
commands.append(interface)
|
||||
for item in args:
|
||||
value = w.get(item)
|
||||
if value:
|
||||
commands.append(item + ' ' + str(value))
|
||||
|
||||
if disable:
|
||||
commands.append('no shutdown')
|
||||
return commands
|
||||
|
||||
|
||||
def check_declarative_intent_params(module, want, result):
|
||||
failed_conditions = []
|
||||
have_neighbors_lldp = None
|
||||
for w in want:
|
||||
want_state = w.get('state')
|
||||
want_tx_rate = w.get('tx_rate')
|
||||
want_rx_rate = w.get('rx_rate')
|
||||
want_neighbors = w.get('neighbors')
|
||||
|
||||
if want_state not in ('up', 'down') and not want_tx_rate and not want_rx_rate and not want_neighbors:
|
||||
continue
|
||||
|
||||
if result['changed']:
|
||||
sleep(w['delay'])
|
||||
|
||||
command = 'show interface %s brief' % 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'):
|
||||
state_data = out.strip().lower().split(w['name'])
|
||||
have_state = None
|
||||
have_state = state_data[1].split()[3]
|
||||
if have_state is None or not conditional(want_state, have_state.strip()):
|
||||
failed_conditions.append('state ' + 'eq(%s)' % want_state)
|
||||
|
||||
command = 'show interface %s' % w['name']
|
||||
rc, out, err = exec_command(module, command)
|
||||
have_tx_rate = None
|
||||
have_rx_rate = None
|
||||
rates = out.splitlines()
|
||||
for s in rates:
|
||||
s = s.strip()
|
||||
if 'output rate' in s and 'input rate' in s:
|
||||
sub = s.split()
|
||||
if want_tx_rate:
|
||||
have_tx_rate = sub[8]
|
||||
if have_tx_rate is None or not conditional(want_tx_rate, have_tx_rate.strip(), cast=int):
|
||||
failed_conditions.append('tx_rate ' + want_tx_rate)
|
||||
if want_rx_rate:
|
||||
have_rx_rate = sub[2]
|
||||
if have_rx_rate is None or not conditional(want_rx_rate, have_rx_rate.strip(), cast=int):
|
||||
failed_conditions.append('rx_rate ' + want_rx_rate)
|
||||
if want_neighbors:
|
||||
have_host = []
|
||||
have_port = []
|
||||
|
||||
# Process LLDP neighbors
|
||||
if have_neighbors_lldp is None:
|
||||
rc, have_neighbors_lldp, 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_lldp:
|
||||
lines = have_neighbors_lldp.strip().split('Local Port ID: ')
|
||||
for line in lines:
|
||||
field = line.split('\n')
|
||||
if field[0].strip() == w['name']:
|
||||
for item in field:
|
||||
if item.startswith('System Name:'):
|
||||
have_host.append(item.split(':')[1].strip())
|
||||
if item.startswith('Port Description:'):
|
||||
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(),
|
||||
duplex=dict(default='auto', choices=['full', 'half', 'auto']),
|
||||
enabled=dict(default=True, type='bool'),
|
||||
tx_rate=dict(),
|
||||
rx_rate=dict(),
|
||||
neighbors=dict(type='list', elements='dict', options=neighbors_spec),
|
||||
delay=dict(default=20, 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(cnos_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()
|
||||
check_args(module, warnings)
|
||||
|
||||
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:
|
||||
if not module.check_mode:
|
||||
load_config(module, commands)
|
||||
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()
|
||||
598
plugins/modules/network/cnos/cnos_l2_interface.py
Normal file
598
plugins/modules/network/cnos/cnos_l2_interface.py
Normal file
|
|
@ -0,0 +1,598 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2017 Lenovo, Inc.
|
||||
# (c) 2017, Ansible by Red Hat, inc
|
||||
# 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/>.
|
||||
#
|
||||
# Module to send banner commands to Lenovo Switches
|
||||
# Two types of banners are supported login and motd
|
||||
# Lenovo Networking
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: cnos_l2_interface
|
||||
short_description: Manage Layer-2 interface on Lenovo CNOS devices.
|
||||
description:
|
||||
- This module provides declarative management of Layer-2 interfaces on
|
||||
Lenovo CNOS devices.
|
||||
author:
|
||||
- Anil Kumar Muraleedharan (@amuraleedhar)
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Full name of the interface excluding any logical
|
||||
unit number, i.e. Ethernet1/3.
|
||||
required: true
|
||||
aliases: ['interface']
|
||||
mode:
|
||||
description:
|
||||
- Mode in which interface needs to be configured.
|
||||
default: access
|
||||
choices: ['access', 'trunk']
|
||||
access_vlan:
|
||||
description:
|
||||
- Configure given VLAN in access port.
|
||||
If C(mode=access), used as the access VLAN ID.
|
||||
trunk_vlans:
|
||||
description:
|
||||
- List of VLANs to be configured in trunk port.
|
||||
If C(mode=trunk), used as the VLAN range to ADD or REMOVE
|
||||
from the trunk.
|
||||
native_vlan:
|
||||
description:
|
||||
- Native VLAN to be configured in trunk port.
|
||||
If C(mode=trunk), used as the trunk native VLAN ID.
|
||||
trunk_allowed_vlans:
|
||||
description:
|
||||
- List of allowed VLANs in a given trunk port.
|
||||
If C(mode=trunk), these are the only VLANs that will be
|
||||
configured on the trunk, i.e. "2-10,15".
|
||||
aggregate:
|
||||
description:
|
||||
- List of Layer-2 interface definitions.
|
||||
state:
|
||||
description:
|
||||
- Manage the state of the Layer-2 Interface configuration.
|
||||
default: present
|
||||
choices: ['present','absent', 'unconfigured']
|
||||
provider:
|
||||
description:
|
||||
- B(Deprecated)
|
||||
- "Starting with Ansible 2.5 we recommend using
|
||||
C(connection: network_cli)."
|
||||
- For more information please see the
|
||||
L(CNOS Platform Options guide, ../network/user_guide/platform_cnos.html).
|
||||
- HORIZONTALLINE
|
||||
- A dict object containing connection details.
|
||||
suboptions:
|
||||
host:
|
||||
description:
|
||||
- Specifies the DNS host name or address for connecting to the remote
|
||||
device over the specified transport. The value of host is used as
|
||||
the destination address for the transport.
|
||||
required: true
|
||||
port:
|
||||
description:
|
||||
- Specifies the port to use when building the connection to the
|
||||
remote device.
|
||||
default: 22
|
||||
username:
|
||||
description:
|
||||
- Configures the username to use to authenticate the connection to
|
||||
the remote device. This value is used to authenticate
|
||||
the SSH session. If the value is not specified in the task, the
|
||||
value of environment variable C(ANSIBLE_NET_USERNAME) will be used
|
||||
instead.
|
||||
password:
|
||||
description:
|
||||
- Specifies the password to use to authenticate the connection to
|
||||
the remote device. This value is used to authenticate
|
||||
the SSH session. If the value is not specified in the task, the
|
||||
value of environment variable C(ANSIBLE_NET_PASSWORD) will be used
|
||||
instead.
|
||||
timeout:
|
||||
description:
|
||||
- Specifies the timeout in seconds for communicating with the network
|
||||
device for either connecting or sending commands. If the timeout
|
||||
is exceeded before the operation is completed, the module will
|
||||
error.
|
||||
default: 10
|
||||
ssh_keyfile:
|
||||
description:
|
||||
- Specifies the SSH key to use to authenticate the connection to
|
||||
the remote device. This value is the path to the
|
||||
key used to authenticate the SSH session. If the value is not
|
||||
specified in the task, the value of environment variable
|
||||
C(ANSIBLE_NET_SSH_KEYFILE)will be used instead.
|
||||
authorize:
|
||||
description:
|
||||
- Instructs the module to enter privileged mode on the remote device
|
||||
before sending any commands. If not specified, the device will
|
||||
attempt to execute all commands in non-privileged mode. If the
|
||||
value is not specified in the task, the value of environment
|
||||
variable C(ANSIBLE_NET_AUTHORIZE) will be used instead.
|
||||
type: bool
|
||||
default: 'no'
|
||||
auth_pass:
|
||||
description:
|
||||
- Specifies the password to use if required to enter privileged mode
|
||||
on the remote device. If I(authorize) is false, then this argument
|
||||
does nothing. If the value is not specified in the task, the value
|
||||
of environment variable C(ANSIBLE_NET_AUTH_PASS) will be used
|
||||
instead.
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Ensure Ethernet1/5 is in its default l2 interface state
|
||||
cnos_l2_interface:
|
||||
name: Ethernet1/5
|
||||
state: unconfigured
|
||||
|
||||
- name: Ensure Ethernet1/5 is configured for access vlan 20
|
||||
cnos_l2_interface:
|
||||
name: Ethernet1/5
|
||||
mode: access
|
||||
access_vlan: 20
|
||||
|
||||
- name: Ensure Ethernet1/5 only has vlans 5-10 as trunk vlans
|
||||
cnos_l2_interface:
|
||||
name: Ethernet1/5
|
||||
mode: trunk
|
||||
native_vlan: 10
|
||||
trunk_vlans: 5-10
|
||||
|
||||
- name: Ensure Ethernet1/5 is a trunk port and ensure 2-50 are being tagged
|
||||
(doesn't mean others aren't also being tagged)
|
||||
cnos_l2_interface:
|
||||
name: Ethernet1/5
|
||||
mode: trunk
|
||||
native_vlan: 10
|
||||
trunk_vlans: 2-50
|
||||
|
||||
- name: Ensure these VLANs are not being tagged on the trunk
|
||||
cnos_l2_interface:
|
||||
name: Ethernet1/5
|
||||
mode: trunk
|
||||
trunk_vlans: 51-4094
|
||||
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:
|
||||
- interface Ethernet1/5
|
||||
- switchport access vlan 20
|
||||
"""
|
||||
|
||||
import re
|
||||
from copy import deepcopy
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import remove_default_spec
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import get_config, load_config
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import cnos_argument_spec
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import run_commands
|
||||
|
||||
|
||||
def get_interface_type(interface):
|
||||
intf_type = 'unknown'
|
||||
if interface.upper()[:2] in ('ET', 'GI', 'FA', 'TE', 'FO', 'HU', 'TWE'):
|
||||
intf_type = 'ethernet'
|
||||
elif interface.upper().startswith('VL'):
|
||||
intf_type = 'svi'
|
||||
elif interface.upper().startswith('LO'):
|
||||
intf_type = 'loopback'
|
||||
elif interface.upper()[:2] in ('MG', 'MA'):
|
||||
intf_type = 'management'
|
||||
elif interface.upper().startswith('PO'):
|
||||
intf_type = 'portchannel'
|
||||
elif interface.upper().startswith('NV'):
|
||||
intf_type = 'nve'
|
||||
|
||||
return intf_type
|
||||
|
||||
|
||||
def is_switchport(name, module):
|
||||
intf_type = get_interface_type(name)
|
||||
|
||||
if intf_type in ('ethernet', 'portchannel'):
|
||||
config = run_commands(module,
|
||||
['show interface {0} switchport'.format(name)])[0]
|
||||
match = re.search(r'Switchport : enabled', config)
|
||||
return bool(match)
|
||||
return False
|
||||
|
||||
|
||||
def interface_is_portchannel(name, module):
|
||||
if get_interface_type(name) == 'ethernet':
|
||||
config = run_commands(module, ['show run interface {0}'.format(name)])[0]
|
||||
if any(c in config for c in ['channel group', 'channel-group']):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_switchport(name, module):
|
||||
config = run_commands(module,
|
||||
['show interface {0} switchport'.format(name)])[0]
|
||||
mode = re.search(r'Switchport mode : (?:.* )?(\w+)$', config, re.M)
|
||||
access = re.search(r'Configured Vlans : (\d+)', config)
|
||||
native = re.search(r'Default/Native Vlan : (\d+)', config)
|
||||
trunk = re.search(r'Enabled Vlans : (.+)$', config, re.M)
|
||||
if mode:
|
||||
mode = mode.group(1)
|
||||
if access:
|
||||
access = access.group(1)
|
||||
if native:
|
||||
native = native.group(1)
|
||||
if trunk:
|
||||
trunk = trunk.group(1)
|
||||
if trunk == 'ALL':
|
||||
trunk = '1-4094'
|
||||
|
||||
switchport_config = {
|
||||
"interface": name,
|
||||
"mode": mode,
|
||||
"access_vlan": access,
|
||||
"native_vlan": native,
|
||||
"trunk_vlans": trunk,
|
||||
}
|
||||
|
||||
return switchport_config
|
||||
|
||||
|
||||
def remove_switchport_config_commands(name, existing, proposed, module):
|
||||
mode = proposed.get('mode')
|
||||
commands = []
|
||||
command = None
|
||||
|
||||
if mode == 'access':
|
||||
av_check = existing.get('access_vlan') == proposed.get('access_vlan')
|
||||
if av_check:
|
||||
command = 'no switchport access vlan'
|
||||
commands.append(command)
|
||||
|
||||
elif mode == 'trunk':
|
||||
# Supported Remove Scenarios for trunk_vlans_list
|
||||
# 1) Existing: 1,2,3 Proposed: 1,2,3 - Remove all
|
||||
# 2) Existing: 1,2,3 Proposed: 1,2 - Remove 1,2 Leave 3
|
||||
# 3) Existing: 1,2,3 Proposed: 2,3 - Remove 2,3 Leave 1
|
||||
# 4) Existing: 1,2,3 Proposed: 4,5,6 - None removed.
|
||||
# 5) Existing: None Proposed: 1,2,3 - None removed.
|
||||
existing_vlans = existing.get('trunk_vlans_list')
|
||||
proposed_vlans = proposed.get('trunk_vlans_list')
|
||||
vlans_to_remove = set(proposed_vlans).intersection(existing_vlans)
|
||||
|
||||
if vlans_to_remove:
|
||||
proposed_allowed_vlans = proposed.get('trunk_allowed_vlans')
|
||||
remove_trunk_allowed_vlans = proposed.get('trunk_vlans',
|
||||
proposed_allowed_vlans)
|
||||
command = 'switchport trunk allowed vlan remove {0}'
|
||||
command = command.format(remove_trunk_allowed_vlans)
|
||||
commands.append(command)
|
||||
|
||||
native_check = existing.get('native_vlan') == proposed.get('native_vlan')
|
||||
if native_check and proposed.get('native_vlan'):
|
||||
command = 'no switchport trunk native vlan'
|
||||
commands.append(command)
|
||||
|
||||
if commands:
|
||||
commands.insert(0, 'interface ' + name)
|
||||
return commands
|
||||
|
||||
|
||||
def get_switchport_config_commands(name, existing, proposed, module):
|
||||
"""Gets commands required to config a given switchport interface
|
||||
"""
|
||||
|
||||
proposed_mode = proposed.get('mode')
|
||||
existing_mode = existing.get('mode')
|
||||
commands = []
|
||||
command = None
|
||||
|
||||
if proposed_mode != existing_mode:
|
||||
if proposed_mode == 'trunk':
|
||||
command = 'switchport mode trunk'
|
||||
elif proposed_mode == 'access':
|
||||
command = 'switchport mode access'
|
||||
|
||||
if command:
|
||||
commands.append(command)
|
||||
|
||||
if proposed_mode == 'access':
|
||||
av_check = str(existing.get('access_vlan')) == str(proposed.get('access_vlan'))
|
||||
if not av_check:
|
||||
command = 'switchport access vlan {0}'.format(proposed.get('access_vlan'))
|
||||
commands.append(command)
|
||||
|
||||
elif proposed_mode == 'trunk':
|
||||
tv_check = existing.get('trunk_vlans_list') == proposed.get('trunk_vlans_list')
|
||||
|
||||
if not tv_check:
|
||||
if proposed.get('allowed'):
|
||||
command = 'switchport trunk allowed vlan {0}'
|
||||
command = command.format(proposed.get('trunk_allowed_vlans'))
|
||||
commands.append(command)
|
||||
|
||||
else:
|
||||
existing_vlans = existing.get('trunk_vlans_list')
|
||||
proposed_vlans = proposed.get('trunk_vlans_list')
|
||||
vlans_to_add = set(proposed_vlans).difference(existing_vlans)
|
||||
if vlans_to_add:
|
||||
command = 'switchport trunk allowed vlan add {0}'
|
||||
command = command.format(proposed.get('trunk_vlans'))
|
||||
commands.append(command)
|
||||
|
||||
native_check = str(existing.get('native_vlan')) == str(proposed.get('native_vlan'))
|
||||
if not native_check and proposed.get('native_vlan'):
|
||||
command = 'switchport trunk native vlan {0}'
|
||||
command = command.format(proposed.get('native_vlan'))
|
||||
commands.append(command)
|
||||
|
||||
if commands:
|
||||
commands.insert(0, 'interface ' + name)
|
||||
return commands
|
||||
|
||||
|
||||
def is_switchport_default(existing):
|
||||
"""Determines if switchport has a default config based on mode
|
||||
Args:
|
||||
existing (dict): existing switchport configuration from Ansible mod
|
||||
Returns:
|
||||
boolean: True if switchport has OOB Layer 2 config, i.e.
|
||||
vlan 1 and trunk all and mode is access
|
||||
"""
|
||||
|
||||
c1 = str(existing['access_vlan']) == '1'
|
||||
c2 = str(existing['native_vlan']) == '1'
|
||||
c3 = existing['trunk_vlans'] == '1-4094'
|
||||
c4 = existing['mode'] == 'access'
|
||||
|
||||
default = c1 and c2 and c3 and c4
|
||||
|
||||
return default
|
||||
|
||||
|
||||
def default_switchport_config(name):
|
||||
commands = []
|
||||
commands.append('interface ' + name)
|
||||
commands.append('switchport mode access')
|
||||
commands.append('switch access vlan 1')
|
||||
commands.append('switchport trunk native vlan 1')
|
||||
commands.append('switchport trunk allowed vlan all')
|
||||
return commands
|
||||
|
||||
|
||||
def vlan_range_to_list(vlans):
|
||||
result = []
|
||||
if vlans:
|
||||
for part in vlans.split(','):
|
||||
if part.lower() == 'none':
|
||||
break
|
||||
if part:
|
||||
if '-' in part:
|
||||
start, stop = (int(i) for i in part.split('-'))
|
||||
result.extend(range(start, stop + 1))
|
||||
else:
|
||||
result.append(int(part))
|
||||
return sorted(result)
|
||||
|
||||
|
||||
def get_list_of_vlans(module):
|
||||
config = run_commands(module, ['show vlan'])[0]
|
||||
vlans = set()
|
||||
|
||||
lines = config.strip().splitlines()
|
||||
for line in lines:
|
||||
line_parts = line.split()
|
||||
if line_parts:
|
||||
try:
|
||||
int(line_parts[0])
|
||||
except ValueError:
|
||||
continue
|
||||
vlans.add(line_parts[0])
|
||||
|
||||
return list(vlans)
|
||||
|
||||
|
||||
def flatten_list(commands):
|
||||
flat_list = []
|
||||
for command in commands:
|
||||
if isinstance(command, list):
|
||||
flat_list.extend(command)
|
||||
else:
|
||||
flat_list.append(command)
|
||||
return flat_list
|
||||
|
||||
|
||||
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'],
|
||||
'access_vlan': module.params['access_vlan'],
|
||||
'native_vlan': module.params['native_vlan'],
|
||||
'trunk_vlans': module.params['trunk_vlans'],
|
||||
'trunk_allowed_vlans': module.params['trunk_allowed_vlans'],
|
||||
'state': module.params['state']
|
||||
})
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
def main():
|
||||
""" main entry point for module execution
|
||||
"""
|
||||
element_spec = dict(
|
||||
name=dict(type='str', aliases=['interface']),
|
||||
mode=dict(choices=['access', 'trunk'], default='access'),
|
||||
access_vlan=dict(type='str'),
|
||||
native_vlan=dict(type='str'),
|
||||
trunk_vlans=dict(type='str'),
|
||||
trunk_allowed_vlans=dict(type='str'),
|
||||
state=dict(choices=['absent', 'present', 'unconfigured'],
|
||||
default='present')
|
||||
)
|
||||
|
||||
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(cnos_argument_spec)
|
||||
|
||||
module = AnsibleModule(argument_spec=argument_spec,
|
||||
mutually_exclusive=[['access_vlan', 'trunk_vlans'],
|
||||
['access_vlan', 'native_vlan'],
|
||||
['access_vlan', 'trunk_allowed_vlans']],
|
||||
supports_check_mode=True)
|
||||
|
||||
warnings = list()
|
||||
commands = []
|
||||
result = {'changed': False, 'warnings': warnings}
|
||||
|
||||
want = map_params_to_obj(module)
|
||||
for w in want:
|
||||
name = w['name']
|
||||
mode = w['mode']
|
||||
access_vlan = w['access_vlan']
|
||||
state = w['state']
|
||||
trunk_vlans = w['trunk_vlans']
|
||||
native_vlan = w['native_vlan']
|
||||
trunk_allowed_vlans = w['trunk_allowed_vlans']
|
||||
|
||||
args = dict(name=name, mode=mode, access_vlan=access_vlan,
|
||||
native_vlan=native_vlan, trunk_vlans=trunk_vlans,
|
||||
trunk_allowed_vlans=trunk_allowed_vlans)
|
||||
|
||||
proposed = dict((k, v) for k, v in args.items() if v is not None)
|
||||
|
||||
name = name.lower()
|
||||
|
||||
if mode == 'access' and state == 'present' and not access_vlan:
|
||||
msg = 'access_vlan param required for mode=access && state=present'
|
||||
module.fail_json(msg=msg)
|
||||
|
||||
if mode == 'trunk' and access_vlan:
|
||||
msg = 'access_vlan param not supported when using mode=trunk'
|
||||
module.fail_json(msg=msg)
|
||||
|
||||
if not is_switchport(name, module):
|
||||
module.fail_json(msg='Ensure interface is configured to be a L2'
|
||||
'\nport first before using this module. You can use'
|
||||
'\nthe cnos_interface module for this.')
|
||||
|
||||
if interface_is_portchannel(name, module):
|
||||
module.fail_json(msg='Cannot change L2 config on physical '
|
||||
'\nport because it is in a portchannel. '
|
||||
'\nYou should update the portchannel config.')
|
||||
|
||||
# existing will never be null for Eth intfs as there is always a default
|
||||
existing = get_switchport(name, module)
|
||||
|
||||
# Safeguard check
|
||||
# If there isn't an existing, something is wrong per previous comment
|
||||
if not existing:
|
||||
msg = 'Make sure you are using the FULL interface name'
|
||||
module.fail_json(msg=msg)
|
||||
|
||||
if trunk_vlans or trunk_allowed_vlans:
|
||||
if trunk_vlans:
|
||||
trunk_vlans_list = vlan_range_to_list(trunk_vlans)
|
||||
elif trunk_allowed_vlans:
|
||||
trunk_vlans_list = vlan_range_to_list(trunk_allowed_vlans)
|
||||
proposed['allowed'] = True
|
||||
|
||||
existing_trunks_list = vlan_range_to_list((existing['trunk_vlans']))
|
||||
|
||||
existing['trunk_vlans_list'] = existing_trunks_list
|
||||
proposed['trunk_vlans_list'] = trunk_vlans_list
|
||||
|
||||
current_vlans = get_list_of_vlans(module)
|
||||
|
||||
if state == 'present':
|
||||
if access_vlan and access_vlan not in current_vlans:
|
||||
module.fail_json(msg='You are trying to configure a VLAN'
|
||||
' on an interface that\ndoes not exist on the '
|
||||
' switch yet!', vlan=access_vlan)
|
||||
elif native_vlan and native_vlan not in current_vlans:
|
||||
module.fail_json(msg='You are trying to configure a VLAN on'
|
||||
' an interface that\ndoes not exist on the '
|
||||
' switch yet!', vlan=native_vlan)
|
||||
else:
|
||||
command = get_switchport_config_commands(name, existing,
|
||||
proposed, module)
|
||||
commands.append(command)
|
||||
elif state == 'unconfigured':
|
||||
is_default = is_switchport_default(existing)
|
||||
if not is_default:
|
||||
command = default_switchport_config(name)
|
||||
commands.append(command)
|
||||
elif state == 'absent':
|
||||
command = remove_switchport_config_commands(name, existing,
|
||||
proposed, module)
|
||||
commands.append(command)
|
||||
|
||||
if trunk_vlans or trunk_allowed_vlans:
|
||||
existing.pop('trunk_vlans_list')
|
||||
proposed.pop('trunk_vlans_list')
|
||||
|
||||
cmds = flatten_list(commands)
|
||||
if cmds:
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=True, commands=cmds)
|
||||
else:
|
||||
result['changed'] = True
|
||||
load_config(module, cmds)
|
||||
if 'configure' in cmds:
|
||||
cmds.pop(0)
|
||||
|
||||
result['commands'] = cmds
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
461
plugins/modules/network/cnos/cnos_l3_interface.py
Normal file
461
plugins/modules/network/cnos/cnos_l3_interface.py
Normal file
|
|
@ -0,0 +1,461 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
#
|
||||
# Copyright (C) 2019 Lenovo, Inc.
|
||||
# (c) 2019, Ansible by Red Hat, inc
|
||||
# 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/>.
|
||||
#
|
||||
# Module to work on Link Aggregation with Lenovo Switches
|
||||
# Lenovo Networking
|
||||
#
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: cnos_l3_interface
|
||||
author: "Anil Kumar Muraleedharan (@amuraleedhar)"
|
||||
short_description: Manage Layer-3 interfaces on Lenovo CNOS network devices.
|
||||
description:
|
||||
- This module provides declarative management of Layer-3 interfaces
|
||||
on CNOS network devices.
|
||||
notes:
|
||||
- Tested against CNOS 10.8.1
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name of the Layer-3 interface to be configured eg. Ethernet1/2
|
||||
ipv4:
|
||||
description:
|
||||
- IPv4 address to be set for the Layer-3 interface mentioned in I(name)
|
||||
option. The address format is <ipv4 address>/<mask>, the mask is number
|
||||
in range 0-32 eg. 10.241.107.1/24
|
||||
ipv6:
|
||||
description:
|
||||
- IPv6 address to be set for the Layer-3 interface mentioned in I(name)
|
||||
option. The address format is <ipv6 address>/<mask>, the mask is number
|
||||
in range 0-128 eg. fd5d:12c9:2201:1::1/64
|
||||
aggregate:
|
||||
description:
|
||||
- List of Layer-3 interfaces definitions. Each of the entry in aggregate
|
||||
list should define name of interface C(name) and a optional C(ipv4) or
|
||||
C(ipv6) address.
|
||||
state:
|
||||
description:
|
||||
- State of the Layer-3 interface configuration. It indicates if the
|
||||
configuration should be present or absent on remote device.
|
||||
default: present
|
||||
choices: ['present', 'absent']
|
||||
provider:
|
||||
description:
|
||||
- B(Deprecated)
|
||||
- "Starting with Ansible 2.5 we recommend using
|
||||
C(connection: network_cli)."
|
||||
- For more information please see the
|
||||
L(CNOS Platform Options guide, ../network/user_guide/platform_cnos.html).
|
||||
- HORIZONTALLINE
|
||||
- A dict object containing connection details.
|
||||
suboptions:
|
||||
host:
|
||||
description:
|
||||
- Specifies the DNS host name or address for connecting to the remote
|
||||
device over the specified transport. The value of host is used as
|
||||
the destination address for the transport.
|
||||
required: true
|
||||
port:
|
||||
description:
|
||||
- Specifies the port to use when building the connection to the
|
||||
remote device.
|
||||
default: 22
|
||||
username:
|
||||
description:
|
||||
- Configures the username to use to authenticate the connection to
|
||||
the remote device. This value is used to authenticate
|
||||
the SSH session. If the value is not specified in the task, the
|
||||
value of environment variable C(ANSIBLE_NET_USERNAME) will be used
|
||||
instead.
|
||||
password:
|
||||
description:
|
||||
- Specifies the password to use to authenticate the connection to
|
||||
the remote device. This value is used to authenticate
|
||||
the SSH session. If the value is not specified in the task, the
|
||||
value of environment variable C(ANSIBLE_NET_PASSWORD) will be used
|
||||
instead.
|
||||
timeout:
|
||||
description:
|
||||
- Specifies the timeout in seconds for communicating with the network
|
||||
device for either connecting or sending commands. If the timeout
|
||||
is exceeded before the operation is completed, the module will
|
||||
error.
|
||||
default: 10
|
||||
ssh_keyfile:
|
||||
description:
|
||||
- Specifies the SSH key to use to authenticate the connection to
|
||||
the remote device. This value is the path to the
|
||||
key used to authenticate the SSH session. If the value is not
|
||||
specified in the task, the value of environment variable
|
||||
C(ANSIBLE_NET_SSH_KEYFILE)will be used instead.
|
||||
authorize:
|
||||
description:
|
||||
- Instructs the module to enter privileged mode on the remote device
|
||||
before sending any commands. If not specified, the device will
|
||||
attempt to execute all commands in non-privileged mode. If the
|
||||
value is not specified in the task, the value of environment
|
||||
variable C(ANSIBLE_NET_AUTHORIZE) will be used instead.
|
||||
type: bool
|
||||
default: 'no'
|
||||
auth_pass:
|
||||
description:
|
||||
- Specifies the password to use if required to enter privileged mode
|
||||
on the remote device. If I(authorize) is false, then this argument
|
||||
does nothing. If the value is not specified in the task, the value
|
||||
of environment variable C(ANSIBLE_NET_AUTH_PASS) will be used
|
||||
instead.
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Remove Ethernet1/33 IPv4 and IPv6 address
|
||||
cnos_l3_interface:
|
||||
name: Ethernet1/33
|
||||
state: absent
|
||||
|
||||
- name: Set Ethernet1/33 IPv4 address
|
||||
cnos_l3_interface:
|
||||
name: Ethernet1/33
|
||||
ipv4: 10.241.107.1/24
|
||||
|
||||
- name: Set Ethernet1/33 IPv6 address
|
||||
cnos_l3_interface:
|
||||
name: Ethernet1/33
|
||||
ipv6: "fd5d:12c9:2201:1::1/64"
|
||||
|
||||
- name: Set Ethernet1/33 in dhcp
|
||||
cnos_l3_interface:
|
||||
name: Ethernet1/33
|
||||
ipv4: dhcp
|
||||
ipv6: dhcp
|
||||
|
||||
- name: Set interface Vlan1 (SVI) IPv4 address
|
||||
cnos_l3_interface:
|
||||
name: Vlan1
|
||||
ipv4: 192.168.0.5/24
|
||||
|
||||
- name: Set IP addresses on aggregate
|
||||
cnos_l3_interface:
|
||||
aggregate:
|
||||
- { name: Ethernet1/33, ipv4: 10.241.107.1/24 }
|
||||
- { name: Ethernet1/44, ipv4: 10.240.106.1/24,
|
||||
ipv6: "fd5d:12c9:2201:1::1/64" }
|
||||
|
||||
- name: Remove IP addresses on aggregate
|
||||
cnos_l3_interface:
|
||||
aggregate:
|
||||
- { name: Ethernet1/33, ipv4: 10.241.107.1/24 }
|
||||
- { name: Ethernet1/44, ipv4: 10.240.106.1/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:
|
||||
- interface Ethernet1/33
|
||||
- ip address 10.241.107.1 255.255.255.0
|
||||
- ipv6 address fd5d:12c9:2201:1::1/64
|
||||
"""
|
||||
import re
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
from ansible.module_utils._text import to_text
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import get_config, load_config
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import cnos_argument_spec
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import run_commands
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import NetworkConfig
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import remove_default_spec
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import is_netmask, is_masklen
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_netmask, to_masklen
|
||||
|
||||
|
||||
def validate_ipv4(value, module):
|
||||
if value:
|
||||
address = value.split('/')
|
||||
if len(address) != 2:
|
||||
module.fail_json(
|
||||
msg='address format is <ipv4 address>/<mask>,got invalid format %s' % value)
|
||||
if not is_masklen(address[1]):
|
||||
module.fail_json(
|
||||
msg='invalid value for mask: %s, mask should be in range 0-32' % address[1])
|
||||
|
||||
|
||||
def validate_ipv6(value, module):
|
||||
if value:
|
||||
address = value.split('/')
|
||||
if len(address) != 2:
|
||||
module.fail_json(
|
||||
msg='address format is <ipv6 address>/<mask>, got invalid format %s' % value)
|
||||
else:
|
||||
if not 0 <= int(address[1]) <= 128:
|
||||
module.fail_json(
|
||||
msg='invalid value for mask: %s, mask should be in range 0-128' % address[1])
|
||||
|
||||
|
||||
def validate_param_values(module, obj, param=None):
|
||||
if param is None:
|
||||
param = module.params
|
||||
for key in obj:
|
||||
# validate the param value (if validator func exists)
|
||||
validator = globals().get('validate_%s' % key)
|
||||
if callable(validator):
|
||||
validator(param.get(key), module)
|
||||
|
||||
|
||||
def parse_config_argument(configobj, name, arg=None):
|
||||
cfg = configobj['interface %s' % name]
|
||||
cfg = '\n'.join(cfg.children)
|
||||
|
||||
values = []
|
||||
matches = re.finditer(r'%s (.+)$' % arg, cfg, re.M)
|
||||
for match in matches:
|
||||
match_str = match.group(1).strip()
|
||||
if arg == 'ipv6 address':
|
||||
values.append(match_str)
|
||||
else:
|
||||
values = match_str
|
||||
break
|
||||
|
||||
return values or None
|
||||
|
||||
|
||||
def search_obj_in_list(name, lst):
|
||||
for o in lst:
|
||||
if o['name'].lower() == name.lower():
|
||||
return o
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_interface_type(interface):
|
||||
intf_type = 'unknown'
|
||||
if interface.upper()[:2] in ('ET', 'GI', 'FA', 'TE', 'FO', 'HU', 'TWE'):
|
||||
intf_type = 'ethernet'
|
||||
elif interface.upper().startswith('VL'):
|
||||
intf_type = 'svi'
|
||||
elif interface.upper().startswith('LO'):
|
||||
intf_type = 'loopback'
|
||||
elif interface.upper()[:2] in ('MG', 'MA'):
|
||||
intf_type = 'management'
|
||||
elif interface.upper().startswith('PO'):
|
||||
intf_type = 'portchannel'
|
||||
elif interface.upper().startswith('NV'):
|
||||
intf_type = 'nve'
|
||||
|
||||
return intf_type
|
||||
|
||||
|
||||
def is_switchport(name, module):
|
||||
intf_type = get_interface_type(name)
|
||||
|
||||
if intf_type in ('ethernet', 'portchannel'):
|
||||
config = run_commands(module,
|
||||
['show interface {0} switchport'.format(name)])[0]
|
||||
match = re.search(r'Switchport : enabled', config)
|
||||
return bool(match)
|
||||
return False
|
||||
|
||||
|
||||
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']
|
||||
|
||||
interface = 'interface ' + name
|
||||
commands.append(interface)
|
||||
|
||||
obj_in_have = search_obj_in_list(name, have)
|
||||
if state == 'absent' and obj_in_have:
|
||||
if obj_in_have['ipv4']:
|
||||
if ipv4:
|
||||
address = ipv4.split('/')
|
||||
if len(address) == 2:
|
||||
ipv4 = '{0} {1}'.format(
|
||||
address[0], to_netmask(address[1]))
|
||||
commands.append('no ip address %s' % ipv4)
|
||||
else:
|
||||
commands.append('no ip address')
|
||||
if obj_in_have['ipv6']:
|
||||
if ipv6:
|
||||
commands.append('no ipv6 address %s' % ipv6)
|
||||
else:
|
||||
commands.append('no ipv6 address')
|
||||
if 'dhcp' in obj_in_have['ipv6']:
|
||||
commands.append('no ipv6 address dhcp')
|
||||
|
||||
elif state == 'present':
|
||||
if ipv4:
|
||||
if obj_in_have is None or obj_in_have.get('ipv4') is None or ipv4 != obj_in_have['ipv4']:
|
||||
address = ipv4.split('/')
|
||||
if len(address) == 2:
|
||||
ipv4 = '{0} {1}'.format(
|
||||
address[0], to_netmask(address[1]))
|
||||
commands.append('ip address %s' % ipv4)
|
||||
|
||||
if ipv6:
|
||||
if obj_in_have is None or obj_in_have.get('ipv6') is None or ipv6.lower() not in [addr.lower() for addr in obj_in_have['ipv6']]:
|
||||
commands.append('ipv6 address %s' % ipv6)
|
||||
if commands[-1] == interface:
|
||||
commands.pop(-1)
|
||||
|
||||
return commands
|
||||
|
||||
|
||||
def map_config_to_obj(module):
|
||||
config = get_config(module)
|
||||
configobj = NetworkConfig(indent=1, contents=config)
|
||||
|
||||
match = re.findall(r'^interface (\S+)', config, re.M)
|
||||
if not match:
|
||||
return list()
|
||||
|
||||
instances = list()
|
||||
|
||||
for item in set(match):
|
||||
ipv4 = parse_config_argument(configobj, item, 'ip address')
|
||||
if ipv4:
|
||||
# eg. 192.168.2.10 255.255.255.0 -> 192.168.2.10/24
|
||||
address = ipv4.strip().split(' ')
|
||||
if len(address) == 2 and is_netmask(address[1]):
|
||||
ipv4 = '{0}/{1}'.format(address[0], to_text(to_masklen(address[1])))
|
||||
|
||||
obj = {
|
||||
'name': item,
|
||||
'ipv4': ipv4,
|
||||
'ipv6': parse_config_argument(configobj, item, 'ipv6 address'),
|
||||
'state': 'present'
|
||||
}
|
||||
instances.append(obj)
|
||||
|
||||
return instances
|
||||
|
||||
|
||||
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]
|
||||
|
||||
validate_param_values(module, item, item)
|
||||
obj.append(item.copy())
|
||||
else:
|
||||
obj.append({
|
||||
'name': module.params['name'],
|
||||
'ipv4': module.params['ipv4'],
|
||||
'ipv6': module.params['ipv6'],
|
||||
'state': module.params['state']
|
||||
})
|
||||
|
||||
validate_param_values(module, obj)
|
||||
|
||||
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(cnos_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}
|
||||
|
||||
want = map_params_to_obj(module)
|
||||
for w in want:
|
||||
name = w['name']
|
||||
name = name.lower()
|
||||
if is_switchport(name, module):
|
||||
module.fail_json(msg='Ensure interface is configured to be a L3'
|
||||
'\nport first before using this module. You can use'
|
||||
'\nthe cnos_interface module for this.')
|
||||
|
||||
have = map_config_to_obj(module)
|
||||
|
||||
commands = map_obj_to_commands((want, have), module)
|
||||
result['commands'] = commands
|
||||
|
||||
if commands:
|
||||
if not module.check_mode:
|
||||
resp = load_config(module, commands)
|
||||
if resp is not None:
|
||||
warnings.extend((out for out in resp if out))
|
||||
|
||||
result['changed'] = True
|
||||
|
||||
if warnings:
|
||||
result['warnings'] = warnings
|
||||
if 'overlaps with address configured on' in warnings[0]:
|
||||
result['failed'] = True
|
||||
result['msg'] = warnings[0]
|
||||
if 'Cannot set overlapping address' in warnings[0]:
|
||||
result['failed'] = True
|
||||
result['msg'] = warnings[0]
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
391
plugins/modules/network/cnos/cnos_linkagg.py
Normal file
391
plugins/modules/network/cnos/cnos_linkagg.py
Normal file
|
|
@ -0,0 +1,391 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
#
|
||||
# Copyright (C) 2017 Lenovo, Inc.
|
||||
# (c) 2017, Ansible by Red Hat, inc
|
||||
# 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/>.
|
||||
#
|
||||
# Module to work on Link Aggregation with Lenovo Switches
|
||||
# Lenovo Networking
|
||||
#
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: cnos_linkagg
|
||||
author: "Anil Kumar Muraleedharan (@auraleedhar)"
|
||||
short_description: Manage link aggregation groups on Lenovo CNOS devices
|
||||
description:
|
||||
- This module provides declarative management of link aggregation groups
|
||||
on Lenovo CNOS network devices.
|
||||
notes:
|
||||
- Tested against CNOS 10.8.1
|
||||
options:
|
||||
group:
|
||||
description:
|
||||
- Channel-group number for the port-channel
|
||||
Link aggregation group. Range 1-255.
|
||||
mode:
|
||||
description:
|
||||
- Mode of the link aggregation group.
|
||||
choices: ['active', 'on', 'passive']
|
||||
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']
|
||||
purge:
|
||||
description:
|
||||
- Purge links not defined in the I(aggregate) parameter.
|
||||
type: bool
|
||||
default: no
|
||||
provider:
|
||||
description:
|
||||
- B(Deprecated)
|
||||
- "Starting with Ansible 2.5 we recommend using C(connection: network_cli)."
|
||||
- For more information please see the L(CNOS Platform Options guide, ../network/user_guide/platform_cnos.html).
|
||||
- HORIZONTALLINE
|
||||
- A dict object containing connection details.
|
||||
suboptions:
|
||||
host:
|
||||
description:
|
||||
- Specifies the DNS host name or address for connecting to the remote
|
||||
device over the specified transport. The value of host is used as
|
||||
the destination address for the transport.
|
||||
required: true
|
||||
port:
|
||||
description:
|
||||
- Specifies the port to use when building the connection to the remote device.
|
||||
default: 22
|
||||
username:
|
||||
description:
|
||||
- Configures the username to use to authenticate the connection to
|
||||
the remote device. This value is used to authenticate
|
||||
the SSH session. If the value is not specified in the task, the
|
||||
value of environment variable C(ANSIBLE_NET_USERNAME) will be used instead.
|
||||
password:
|
||||
description:
|
||||
- Specifies the password to use to authenticate the connection to
|
||||
the remote device. This value is used to authenticate
|
||||
the SSH session. If the value is not specified in the task, the
|
||||
value of environment variable C(ANSIBLE_NET_PASSWORD) will be used instead.
|
||||
timeout:
|
||||
description:
|
||||
- Specifies the timeout in seconds for communicating with the network device
|
||||
for either connecting or sending commands. If the timeout is
|
||||
exceeded before the operation is completed, the module will error.
|
||||
default: 10
|
||||
ssh_keyfile:
|
||||
description:
|
||||
- Specifies the SSH key to use to authenticate the connection to
|
||||
the remote device. This value is the path to the
|
||||
key used to authenticate the SSH session. If the value is not specified
|
||||
in the task, the value of environment variable C(ANSIBLE_NET_SSH_KEYFILE)
|
||||
will be used instead.
|
||||
authorize:
|
||||
description:
|
||||
- Instructs the module to enter privileged mode on the remote device
|
||||
before sending any commands. If not specified, the device will
|
||||
attempt to execute all commands in non-privileged mode. If the value
|
||||
is not specified in the task, the value of environment variable
|
||||
C(ANSIBLE_NET_AUTHORIZE) will be used instead.
|
||||
type: bool
|
||||
default: 'no'
|
||||
auth_pass:
|
||||
description:
|
||||
- Specifies the password to use if required to enter privileged mode
|
||||
on the remote device. If I(authorize) is false, then this argument
|
||||
does nothing. If the value is not specified in the task, the value of
|
||||
environment variable C(ANSIBLE_NET_AUTH_PASS) will be used instead.
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: create link aggregation group
|
||||
cnos_linkagg:
|
||||
group: 10
|
||||
state: present
|
||||
|
||||
- name: delete link aggregation group
|
||||
cnos_linkagg:
|
||||
group: 10
|
||||
state: absent
|
||||
|
||||
- name: set link aggregation group to members
|
||||
cnos_linkagg:
|
||||
group: 200
|
||||
mode: active
|
||||
members:
|
||||
- Ethernet1/33
|
||||
- Ethernet1/44
|
||||
|
||||
- name: remove link aggregation group from GigabitEthernet0/0
|
||||
cnos_linkagg:
|
||||
group: 200
|
||||
mode: active
|
||||
members:
|
||||
- Ethernet1/33
|
||||
|
||||
- name: Create aggregate of linkagg definitions
|
||||
cnos_linkagg:
|
||||
aggregate:
|
||||
- { group: 3, mode: on, members: [Ethernet1/33] }
|
||||
- { group: 100, mode: passive, members: [Ethernet1/44] }
|
||||
"""
|
||||
|
||||
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:
|
||||
- interface port-channel 30
|
||||
- interface Ethernet1/33
|
||||
- channel-group 30 mode on
|
||||
- no interface port-channel 30
|
||||
"""
|
||||
|
||||
import re
|
||||
from copy import deepcopy
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import CustomNetworkConfig
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import remove_default_spec
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import get_config, load_config
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import cnos_argument_spec
|
||||
|
||||
|
||||
def search_obj_in_list(group, lst):
|
||||
for o in lst:
|
||||
if o['group'] == group:
|
||||
return o
|
||||
|
||||
|
||||
def map_obj_to_commands(updates, module):
|
||||
commands = list()
|
||||
want, have = updates
|
||||
purge = module.params['purge']
|
||||
|
||||
for w in want:
|
||||
group = w['group']
|
||||
mode = w['mode']
|
||||
members = w.get('members') or []
|
||||
state = w['state']
|
||||
del w['state']
|
||||
|
||||
obj_in_have = search_obj_in_list(group, have)
|
||||
|
||||
if state == 'absent':
|
||||
if obj_in_have:
|
||||
commands.append('no interface port-channel {0}'.format(group))
|
||||
|
||||
elif state == 'present':
|
||||
cmd = ['interface port-channel {0}'.format(group),
|
||||
'exit']
|
||||
if not obj_in_have:
|
||||
if not group:
|
||||
module.fail_json(msg='group is a required option')
|
||||
commands.extend(cmd)
|
||||
|
||||
if members:
|
||||
for m in members:
|
||||
commands.append('interface {0}'.format(m))
|
||||
commands.append('channel-group {0} mode {1}'.format(group, mode))
|
||||
|
||||
else:
|
||||
if members:
|
||||
if 'members' not in obj_in_have.keys():
|
||||
for m in members:
|
||||
commands.extend(cmd)
|
||||
commands.append('interface {0}'.format(m))
|
||||
commands.append('channel-group {0} mode {1}'.format(group, mode))
|
||||
|
||||
elif set(members) != set(obj_in_have['members']):
|
||||
missing_members = list(set(members) - set(obj_in_have['members']))
|
||||
for m in missing_members:
|
||||
commands.extend(cmd)
|
||||
commands.append('interface {0}'.format(m))
|
||||
commands.append('channel-group {0} mode {1}'.format(group, mode))
|
||||
|
||||
superfluous_members = list(set(obj_in_have['members']) - set(members))
|
||||
for m in superfluous_members:
|
||||
commands.extend(cmd)
|
||||
commands.append('interface {0}'.format(m))
|
||||
commands.append('no channel-group')
|
||||
|
||||
if purge:
|
||||
for h in have:
|
||||
obj_in_want = search_obj_in_list(h['group'], want)
|
||||
if not obj_in_want:
|
||||
commands.append('no interface port-channel {0}'.format(h['group']))
|
||||
|
||||
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()
|
||||
d['group'] = str(d['group'])
|
||||
|
||||
obj.append(d)
|
||||
else:
|
||||
obj.append({
|
||||
'group': str(module.params['group']),
|
||||
'mode': module.params['mode'],
|
||||
'members': module.params['members'],
|
||||
'state': module.params['state']
|
||||
})
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
def parse_mode(module, config, group, member):
|
||||
mode = None
|
||||
netcfg = CustomNetworkConfig(indent=1, contents=config)
|
||||
parents = ['interface {0}'.format(member)]
|
||||
body = netcfg.get_section(parents)
|
||||
|
||||
match_int = re.findall(r'interface {0}\n'.format(member), body, re.M)
|
||||
if match_int:
|
||||
match = re.search(r'channel-group {0} mode (\S+)'.format(group),
|
||||
body, re.M)
|
||||
if match:
|
||||
mode = match.group(1)
|
||||
|
||||
return mode
|
||||
|
||||
|
||||
def parse_members(module, config, group):
|
||||
members = []
|
||||
|
||||
for line in config.strip().split('!'):
|
||||
l = line.strip()
|
||||
if l.startswith('interface'):
|
||||
match_group = re.findall(r'channel-group {0} mode'.format(group), l, re.M)
|
||||
if match_group:
|
||||
match = re.search(r'interface (\S+)', l, re.M)
|
||||
if match:
|
||||
members.append(match.group(1))
|
||||
|
||||
return members
|
||||
|
||||
|
||||
def get_channel(module, config, group):
|
||||
match = re.findall(r'^interface (\S+)', config, re.M)
|
||||
|
||||
if not match:
|
||||
return {}
|
||||
|
||||
channel = {}
|
||||
for item in set(match):
|
||||
member = item
|
||||
channel['mode'] = parse_mode(module, config, group, member)
|
||||
channel['members'] = parse_members(module, config, group)
|
||||
|
||||
return channel
|
||||
|
||||
|
||||
def map_config_to_obj(module):
|
||||
objs = list()
|
||||
config = get_config(module)
|
||||
|
||||
for line in config.split('\n'):
|
||||
l = line.strip()
|
||||
match = re.search(r'interface port-channel(\S+)', l, re.M)
|
||||
if match:
|
||||
obj = {}
|
||||
group = match.group(1)
|
||||
obj['group'] = group
|
||||
obj.update(get_channel(module, config, group))
|
||||
objs.append(obj)
|
||||
|
||||
return objs
|
||||
|
||||
|
||||
def main():
|
||||
""" main entry point for module execution
|
||||
"""
|
||||
element_spec = dict(
|
||||
group=dict(type='int'),
|
||||
mode=dict(choices=['active', 'on', 'passive']),
|
||||
members=dict(type='list'),
|
||||
state=dict(default='present',
|
||||
choices=['present', 'absent'])
|
||||
)
|
||||
|
||||
aggregate_spec = deepcopy(element_spec)
|
||||
aggregate_spec['group'] = dict(required=True)
|
||||
|
||||
required_one_of = [['group', 'aggregate']]
|
||||
required_together = [['members', 'mode']]
|
||||
mutually_exclusive = [['group', 'aggregate']]
|
||||
|
||||
# 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,
|
||||
required_together=required_together),
|
||||
purge=dict(default=False, type='bool')
|
||||
)
|
||||
|
||||
argument_spec.update(element_spec)
|
||||
argument_spec.update(cnos_argument_spec)
|
||||
|
||||
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)
|
||||
have = map_config_to_obj(module)
|
||||
|
||||
commands = map_obj_to_commands((want, have), module)
|
||||
result['commands'] = commands
|
||||
|
||||
if commands:
|
||||
if not module.check_mode:
|
||||
load_config(module, commands)
|
||||
result['changed'] = True
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
139
plugins/modules/network/cnos/cnos_lldp.py
Normal file
139
plugins/modules/network/cnos/cnos_lldp.py
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
#
|
||||
# Copyright (C) 2019 Lenovo.
|
||||
# (c) 2017, Ansible by Red Hat, inc
|
||||
# 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/>.
|
||||
#
|
||||
# Module to work on Link Aggregation with Lenovo Switches
|
||||
# Lenovo Networking
|
||||
#
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: cnos_lldp
|
||||
author: "Anil Kumar Muraleedharan (@amuraleedhar)"
|
||||
short_description: Manage LLDP configuration on Lenovo CNOS network devices.
|
||||
description:
|
||||
- This module provides declarative management of LLDP service
|
||||
on Lenovc CNOS network devices.
|
||||
notes:
|
||||
- Tested against CNOS 10.9.1
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- State of the LLDP configuration. If value is I(present) lldp will be
|
||||
enabled else if it is I(absent) it will be disabled.
|
||||
default: present
|
||||
choices: ['present', 'absent']
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Enable LLDP service
|
||||
cnos_lldp:
|
||||
state: present
|
||||
|
||||
- name: Disable LLDP service
|
||||
cnos_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:
|
||||
- lldp timer 1024
|
||||
- lldp trap-interval 330
|
||||
"""
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import get_config, load_config
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import cnos_argument_spec
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import debugOutput, run_commands
|
||||
from ansible.module_utils.connection import exec_command
|
||||
|
||||
|
||||
def get_ethernet_range(module):
|
||||
output = run_commands(module, ['show interface brief'])[0].split('\n')
|
||||
maxport = None
|
||||
last_interface = None
|
||||
for line in output:
|
||||
if line.startswith('Ethernet1/'):
|
||||
last_interface = line.split(' ')[0]
|
||||
if last_interface is not None:
|
||||
eths = last_interface.split('/')
|
||||
maxport = eths[1]
|
||||
return maxport
|
||||
|
||||
|
||||
def main():
|
||||
""" main entry point for module execution
|
||||
"""
|
||||
argument_spec = dict(
|
||||
state=dict(default='present',
|
||||
choices=['present', 'absent'])
|
||||
)
|
||||
|
||||
module = AnsibleModule(argument_spec=argument_spec,
|
||||
supports_check_mode=True)
|
||||
warnings = list()
|
||||
result = {'changed': False}
|
||||
if warnings:
|
||||
result['warnings'] = warnings
|
||||
|
||||
maxport = get_ethernet_range(module)
|
||||
commands = []
|
||||
prime_cmd = 'interface ethernet 1/1-' + maxport
|
||||
|
||||
if module.params['state'] == 'absent':
|
||||
commands.append(prime_cmd)
|
||||
commands.append('no lldp receive')
|
||||
commands.append('no lldp transmit')
|
||||
commands.append('exit')
|
||||
commands.append('interface mgmt 0')
|
||||
commands.append('no lldp receive')
|
||||
commands.append('no lldp transmit')
|
||||
commands.append('exit')
|
||||
elif module.params['state'] == 'present':
|
||||
commands.append(prime_cmd)
|
||||
commands.append('lldp receive')
|
||||
commands.append('lldp transmit')
|
||||
commands.append('exit')
|
||||
commands.append('interface mgmt 0')
|
||||
commands.append('lldp receive')
|
||||
commands.append('lldp transmit')
|
||||
commands.append('exit')
|
||||
|
||||
result['commands'] = commands
|
||||
|
||||
if commands:
|
||||
if not module.check_mode:
|
||||
load_config(module, commands)
|
||||
|
||||
result['changed'] = True
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
425
plugins/modules/network/cnos/cnos_logging.py
Normal file
425
plugins/modules/network/cnos/cnos_logging.py
Normal file
|
|
@ -0,0 +1,425 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
#
|
||||
# Copyright (C) 2019 Lenovo, Inc.
|
||||
# (c) 2017, Ansible by Red Hat, inc
|
||||
# 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/>.
|
||||
#
|
||||
# Module to work on Link Aggregation with Lenovo Switches
|
||||
# Lenovo Networking
|
||||
#
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: cnos_logging
|
||||
author: "Anil Kumar Muraleedharan (@amuraleedhar)"
|
||||
short_description: Manage logging on network devices
|
||||
description:
|
||||
- This module provides declarative management of logging
|
||||
on Cisco Cnos devices.
|
||||
notes:
|
||||
- Tested against CNOS 10.9.1
|
||||
options:
|
||||
dest:
|
||||
description:
|
||||
- Destination of the logs. Lenovo uses the term server instead of host in
|
||||
its CLI.
|
||||
choices: ['server', 'console', 'monitor', 'logfile']
|
||||
name:
|
||||
description:
|
||||
- If value of C(dest) is I(file) it indicates file-name
|
||||
and for I(server) indicates the server name to be notified.
|
||||
size:
|
||||
description:
|
||||
- Size of buffer. The acceptable value is in range from 4096 to
|
||||
4294967295 bytes.
|
||||
default: 10485760
|
||||
facility:
|
||||
description:
|
||||
- Set logging facility. This is applicable only for server logging
|
||||
level:
|
||||
description:
|
||||
- Set logging severity levels. 0-emerg;1-alert;2-crit;3-err;4-warn;
|
||||
5-notif;6-inform;7-debug
|
||||
default: 5
|
||||
aggregate:
|
||||
description: List of logging definitions.
|
||||
state:
|
||||
description:
|
||||
- State of the logging configuration.
|
||||
default: present
|
||||
choices: ['present', 'absent']
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: configure server logging
|
||||
cnos_logging:
|
||||
dest: server
|
||||
name: 10.241.107.224
|
||||
facility: local7
|
||||
state: present
|
||||
|
||||
- name: remove server logging configuration
|
||||
cnos_logging:
|
||||
dest: server
|
||||
name: 10.241.107.224
|
||||
state: absent
|
||||
|
||||
- name: configure console logging level and facility
|
||||
cnos_logging:
|
||||
dest: console
|
||||
level: 7
|
||||
state: present
|
||||
|
||||
- name: configure buffer size
|
||||
cnos_logging:
|
||||
dest: logfile
|
||||
level: 5
|
||||
name: testfile
|
||||
size: 5000
|
||||
|
||||
- name: Configure logging using aggregate
|
||||
cnos_logging:
|
||||
aggregate:
|
||||
- { dest: console, level: 6 }
|
||||
- { dest: logfile, size: 9000 }
|
||||
|
||||
- name: remove logging using aggregate
|
||||
cnos_logging:
|
||||
aggregate:
|
||||
- { dest: console, level: 6 }
|
||||
- { dest: logfile, name: anil, size: 9000 }
|
||||
state: absent
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
commands:
|
||||
description: The list of configuration mode commands to send to the device
|
||||
returned: always
|
||||
type: list
|
||||
sample:
|
||||
- logging console 7
|
||||
- logging server 10.241.107.224
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from copy import deepcopy
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import validate_ip_address
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import remove_default_spec
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import get_config, load_config
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import get_capabilities
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import check_args
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import cnos_argument_spec
|
||||
|
||||
|
||||
def validate_size(value, module):
|
||||
if value:
|
||||
if not int(4096) <= int(value) <= int(4294967295):
|
||||
module.fail_json(msg='size must be between 4096 and 4294967295')
|
||||
else:
|
||||
return value
|
||||
|
||||
|
||||
def map_obj_to_commands(updates, module):
|
||||
dest_group = ('console', 'monitor', 'logfile', 'server')
|
||||
commands = list()
|
||||
want, have = updates
|
||||
for w in want:
|
||||
dest = w['dest']
|
||||
name = w['name']
|
||||
size = w['size']
|
||||
facility = w['facility']
|
||||
level = w['level']
|
||||
state = w['state']
|
||||
del w['state']
|
||||
|
||||
if state == 'absent':
|
||||
if dest:
|
||||
|
||||
if dest == 'server':
|
||||
commands.append('no logging server {0}'.format(name))
|
||||
|
||||
elif dest in dest_group:
|
||||
commands.append('no logging {0}'.format(dest))
|
||||
|
||||
else:
|
||||
module.fail_json(msg='dest must be among console, monitor, logfile, server')
|
||||
|
||||
if state == 'present' and w not in have:
|
||||
if dest == 'server':
|
||||
cmd_str = 'logging server {0}'.format(name)
|
||||
if level is not None and level > 0 and level < 8:
|
||||
cmd_str = cmd_str + ' ' + level
|
||||
if facility is not None:
|
||||
cmd_str = cmd_str + ' facility ' + facility
|
||||
commands.append(cmd_str)
|
||||
|
||||
elif dest == 'logfile' and size:
|
||||
present = False
|
||||
|
||||
for entry in have:
|
||||
if entry['dest'] == 'logfile' and entry['size'] == size and entry['level'] == level:
|
||||
present = True
|
||||
|
||||
if not present:
|
||||
cmd_str = 'logging logfile '
|
||||
if name is not None:
|
||||
cmd_str = cmd_str + name
|
||||
if level and level != '7':
|
||||
cmd_str = cmd_str + ' ' + level
|
||||
else:
|
||||
cmd_str = cmd_str + ' 7'
|
||||
if size is not None:
|
||||
cmd_str = cmd_str + ' size ' + size
|
||||
commands.append(cmd_str)
|
||||
else:
|
||||
module.fail_json(msg='Name of the logfile is a mandatory parameter')
|
||||
|
||||
else:
|
||||
if dest:
|
||||
dest_cmd = 'logging {0}'.format(dest)
|
||||
if level:
|
||||
dest_cmd += ' {0}'.format(level)
|
||||
commands.append(dest_cmd)
|
||||
return commands
|
||||
|
||||
|
||||
def parse_facility(line, dest):
|
||||
facility = None
|
||||
if dest == 'server':
|
||||
result = line.split()
|
||||
i = 0
|
||||
for x in result:
|
||||
if x == 'facility':
|
||||
return result[i + 1]
|
||||
i = i + 1
|
||||
return facility
|
||||
|
||||
|
||||
def parse_size(line, dest):
|
||||
size = None
|
||||
if dest == 'logfile':
|
||||
if 'logging logfile' in line:
|
||||
result = line.split()
|
||||
i = 0
|
||||
for x in result:
|
||||
if x == 'size':
|
||||
return result[i + 1]
|
||||
i = i + 1
|
||||
return '10485760'
|
||||
return size
|
||||
|
||||
|
||||
def parse_name(line, dest):
|
||||
name = None
|
||||
if dest == 'server':
|
||||
if 'logging server' in line:
|
||||
result = line.split()
|
||||
i = 0
|
||||
for x in result:
|
||||
if x == 'server':
|
||||
name = result[i + 1]
|
||||
elif dest == 'logfile':
|
||||
if 'logging logfile' in line:
|
||||
result = line.split()
|
||||
i = 0
|
||||
for x in result:
|
||||
if x == 'logfile':
|
||||
name = result[i + 1]
|
||||
else:
|
||||
name = None
|
||||
return name
|
||||
|
||||
|
||||
def parse_level(line, dest):
|
||||
level_group = ('0', '1', '2', '3', '4', '5', '6', '7')
|
||||
level = '7'
|
||||
if dest == 'server':
|
||||
if 'logging server' in line:
|
||||
result = line.split()
|
||||
if(len(result) > 3):
|
||||
if result[3].isdigit():
|
||||
level = result[3]
|
||||
else:
|
||||
if dest == 'logfile':
|
||||
if 'logging logfile' in line:
|
||||
result = line.split()
|
||||
if result[3].isdigit():
|
||||
level = result[3]
|
||||
else:
|
||||
match = re.search(r'logging {0} (\S+)'.format(dest), line, re.M)
|
||||
|
||||
return level
|
||||
|
||||
|
||||
def map_config_to_obj(module):
|
||||
obj = []
|
||||
dest_group = ('console', 'server', 'monitor', 'logfile')
|
||||
data = get_config(module, flags=['| include logging'])
|
||||
index = 0
|
||||
for line in data.split('\n'):
|
||||
logs = line.split()
|
||||
index = len(logs)
|
||||
if index == 0 or index == 1:
|
||||
continue
|
||||
if logs[0] != 'logging':
|
||||
continue
|
||||
if logs[1] == 'monitor' or logs[1] == 'console':
|
||||
obj.append({'dest': logs[1], 'level': logs[2]})
|
||||
elif logs[1] == 'logfile':
|
||||
level = '5'
|
||||
if index > 3 and logs[3].isdigit():
|
||||
level = logs[3]
|
||||
size = '10485760'
|
||||
if len(logs) > 4:
|
||||
size = logs[5]
|
||||
obj.append({'dest': logs[1], 'name': logs[2], 'size': size, 'level': level})
|
||||
elif logs[1] == 'server':
|
||||
level = '5'
|
||||
facility = None
|
||||
|
||||
if index > 3 and logs[3].isdigit():
|
||||
level = logs[3]
|
||||
if index > 3 and logs[3] == 'facility':
|
||||
facility = logs[4]
|
||||
if index > 4 and logs[4] == 'facility':
|
||||
facility = logs[5]
|
||||
obj.append({'dest': logs[1], 'name': logs[2], 'facility': facility, 'level': level})
|
||||
else:
|
||||
continue
|
||||
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)
|
||||
|
||||
d = item.copy()
|
||||
if d['dest'] != 'server' and d['dest'] != 'logfile':
|
||||
d['name'] = None
|
||||
|
||||
if d['dest'] == 'logfile':
|
||||
if 'size' in d:
|
||||
d['size'] = str(validate_size(d['size'], module))
|
||||
elif 'size' not in d:
|
||||
d['size'] = str(10485760)
|
||||
else:
|
||||
pass
|
||||
|
||||
if d['dest'] != 'logfile':
|
||||
d['size'] = None
|
||||
|
||||
obj.append(d)
|
||||
|
||||
else:
|
||||
if module.params['dest'] != 'server' and module.params['dest'] != 'logfile':
|
||||
module.params['name'] = None
|
||||
|
||||
if module.params['dest'] == 'logfile':
|
||||
if not module.params['size']:
|
||||
module.params['size'] = str(10485760)
|
||||
else:
|
||||
module.params['size'] = None
|
||||
|
||||
if module.params['size'] is None:
|
||||
obj.append({
|
||||
'dest': module.params['dest'],
|
||||
'name': module.params['name'],
|
||||
'size': module.params['size'],
|
||||
'facility': module.params['facility'],
|
||||
'level': module.params['level'],
|
||||
'state': module.params['state']
|
||||
})
|
||||
|
||||
else:
|
||||
obj.append({
|
||||
'dest': module.params['dest'],
|
||||
'name': module.params['name'],
|
||||
'size': str(validate_size(module.params['size'], module)),
|
||||
'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=['server', 'console', 'monitor', 'logfile']),
|
||||
name=dict(type='str'),
|
||||
size=dict(type='int', default=10485760),
|
||||
facility=dict(type='str'),
|
||||
level=dict(type='str', default='5'),
|
||||
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)
|
||||
|
||||
required_if = [('dest', 'server', ['name'])]
|
||||
|
||||
module = AnsibleModule(argument_spec=argument_spec,
|
||||
required_if=required_if,
|
||||
supports_check_mode=True)
|
||||
warnings = list()
|
||||
check_args(module, warnings)
|
||||
|
||||
result = {'changed': False}
|
||||
if warnings:
|
||||
result['warnings'] = warnings
|
||||
|
||||
want = map_params_to_obj(module, required_if=required_if)
|
||||
have = map_config_to_obj(module)
|
||||
|
||||
commands = map_obj_to_commands((want, have), module)
|
||||
result['commands'] = commands
|
||||
|
||||
if commands:
|
||||
if not module.check_mode:
|
||||
load_config(module, commands)
|
||||
result['changed'] = True
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
114
plugins/modules/network/cnos/cnos_reload.py
Normal file
114
plugins/modules/network/cnos/cnos_reload.py
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
#
|
||||
# Copyright (C) 2017 Lenovo, Inc.
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
# Module to reload Lenovo Switches
|
||||
# Lenovo Networking
|
||||
#
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: cnos_reload
|
||||
author: "Anil Kumar Muraleedharan (@amuraleedhar)"
|
||||
short_description: Perform switch restart on devices running Lenovo CNOS
|
||||
description:
|
||||
- This module allows you to restart the switch using the current startup
|
||||
configuration. The module is usually invoked after the running
|
||||
configuration has been saved over the startup configuration.
|
||||
This module uses SSH to manage network device configuration.
|
||||
The results of the operation can be viewed in results directory.
|
||||
extends_documentation_fragment:
|
||||
- community.general.cnos
|
||||
|
||||
options: {}
|
||||
|
||||
'''
|
||||
EXAMPLES = '''
|
||||
Tasks : The following are examples of using the module cnos_reload. These are
|
||||
written in the main.yml file of the tasks directory.
|
||||
---
|
||||
- name: Test Reload
|
||||
cnos_reload:
|
||||
deviceType: "{{ hostvars[inventory_hostname]['deviceType'] }}"
|
||||
outputfile: "./results/test_reload_{{ inventory_hostname }}_output.txt"
|
||||
'''
|
||||
RETURN = '''
|
||||
msg:
|
||||
description: Success or failure message
|
||||
returned: always
|
||||
type: str
|
||||
sample: "Device is Reloading. Please wait..."
|
||||
'''
|
||||
|
||||
import sys
|
||||
import time
|
||||
import socket
|
||||
import array
|
||||
import json
|
||||
import time
|
||||
import re
|
||||
try:
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos import cnos
|
||||
HAS_LIB = True
|
||||
except Exception:
|
||||
HAS_LIB = False
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
outputfile=dict(required=True),
|
||||
host=dict(required=False),
|
||||
username=dict(required=False),
|
||||
password=dict(required=False, no_log=True),
|
||||
enablePassword=dict(required=False, no_log=True),
|
||||
deviceType=dict(required=True),),
|
||||
supports_check_mode=False)
|
||||
|
||||
command = 'reload'
|
||||
outputfile = module.params['outputfile']
|
||||
output = ''
|
||||
cmd = [{'command': command, 'prompt': 'reboot system? (y/n): ',
|
||||
'answer': 'y'}]
|
||||
output = output + str(cnos.run_cnos_commands(module, cmd))
|
||||
|
||||
# Save it into the file
|
||||
file = open(outputfile, "a")
|
||||
file.write(output)
|
||||
file.close()
|
||||
|
||||
errorMsg = cnos.checkOutputForError(output)
|
||||
if(errorMsg in "Device Response Timed out"):
|
||||
module.exit_json(changed=True,
|
||||
msg="Device is Reloading. Please wait...")
|
||||
else:
|
||||
module.fail_json(msg=errorMsg)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
285
plugins/modules/network/cnos/cnos_rollback.py
Normal file
285
plugins/modules/network/cnos/cnos_rollback.py
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
#
|
||||
# Copyright (C) 2017 Lenovo, Inc.
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
# Module to Rollback Config back to Lenovo Switches
|
||||
#
|
||||
# Lenovo Networking
|
||||
#
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: cnos_rollback
|
||||
author: "Anil Kumar Muraleedharan (@amuraleedhar)"
|
||||
short_description: Roll back the running or startup configuration from a remote
|
||||
server on devices running Lenovo CNOS
|
||||
description:
|
||||
- This module allows you to work with switch configurations. It provides a
|
||||
way to roll back configurations of a switch from a remote server. This is
|
||||
achieved by using startup or running configurations of the target device
|
||||
that were previously backed up to a remote server using FTP, SFTP, TFTP,
|
||||
or SCP. The first step is to create a directory from where the remote
|
||||
server can be reached. The next step is to provide the full file path of
|
||||
he backup configuration's location. Authentication details required by the
|
||||
remote server must be provided as well.
|
||||
By default, this method overwrites the switch's configuration file with
|
||||
the newly downloaded file. This module uses SSH to manage network device
|
||||
configuration. The results of the operation will be placed in a directory
|
||||
named 'results' that must be created by the user in their local directory
|
||||
to where the playbook is run.
|
||||
extends_documentation_fragment:
|
||||
- community.general.cnos
|
||||
|
||||
options:
|
||||
configType:
|
||||
description:
|
||||
- This refers to the type of configuration which will be used for
|
||||
the rolling back process. The choices are the running or startup
|
||||
configurations. There is no default value, so it will result
|
||||
in an error if the input is incorrect.
|
||||
required: Yes
|
||||
default: Null
|
||||
choices: [running-config, startup-config]
|
||||
protocol:
|
||||
description:
|
||||
- This refers to the protocol used by the network device to
|
||||
interact with the remote server from where to download the backup
|
||||
configuration. The choices are FTP, SFTP, TFTP, or SCP. Any other
|
||||
protocols will result in error. If this parameter is not
|
||||
specified, there is no default value to be used.
|
||||
required: Yes
|
||||
default: Null
|
||||
choices: [SFTP, SCP, FTP, TFTP]
|
||||
rcserverip:
|
||||
description:
|
||||
- This specifies the IP Address of the remote server from where the
|
||||
backup configuration will be downloaded.
|
||||
required: Yes
|
||||
default: Null
|
||||
rcpath:
|
||||
description:
|
||||
- This specifies the full file path of the configuration file
|
||||
located on the remote server. In case the relative path is used as
|
||||
the variable value, the root folder for the user of the server
|
||||
needs to be specified.
|
||||
required: Yes
|
||||
default: Null
|
||||
serverusername:
|
||||
description:
|
||||
- Specify username for the server relating to the protocol used.
|
||||
required: Yes
|
||||
default: Null
|
||||
serverpassword:
|
||||
description:
|
||||
- Specify password for the server relating to the protocol used.
|
||||
required: Yes
|
||||
default: Null
|
||||
'''
|
||||
EXAMPLES = '''
|
||||
Tasks : The following are examples of using the module cnos_rollback.
|
||||
These are written in the main.yml file of the tasks directory.
|
||||
---
|
||||
|
||||
- name: Test Rollback of config - Running config
|
||||
cnos_rolback:
|
||||
deviceType: "{{ hostvars[inventory_hostname]['deviceType'] }}"
|
||||
outputfile: "./results/test_rollback_{{ inventory_hostname }}_output.txt"
|
||||
configType: running-config
|
||||
protocol: "sftp"
|
||||
serverip: "10.241.106.118"
|
||||
rcpath: "/root/cnos/G8272-running-config.txt"
|
||||
serverusername: "root"
|
||||
serverpassword: "root123"
|
||||
|
||||
- name: Test Rollback of config - Startup config
|
||||
cnos_rolback:
|
||||
deviceType: "{{ hostvars[inventory_hostname]['deviceType'] }}"
|
||||
outputfile: "./results/test_rollback_{{ inventory_hostname }}_output.txt"
|
||||
configType: startup-config
|
||||
protocol: "sftp"
|
||||
serverip: "10.241.106.118"
|
||||
rcpath: "/root/cnos/G8272-startup-config.txt"
|
||||
serverusername: "root"
|
||||
serverpassword: "root123"
|
||||
|
||||
- name: Test Rollback of config - Running config - TFTP
|
||||
cnos_rolback:
|
||||
deviceType: "{{ hostvars[inventory_hostname]['deviceType'] }}"
|
||||
outputfile: "./results/test_rollback_{{ inventory_hostname }}_output.txt"
|
||||
configType: running-config
|
||||
protocol: "tftp"
|
||||
serverip: "10.241.106.118"
|
||||
rcpath: "/anil/G8272-running-config.txt"
|
||||
serverusername: "root"
|
||||
serverpassword: "root123"
|
||||
|
||||
- name: Test Rollback of config - Startup config - TFTP
|
||||
cnos_rolback:
|
||||
deviceType: "{{ hostvars[inventory_hostname]['deviceType'] }}"
|
||||
outputfile: "./results/test_rollback_{{ inventory_hostname }}_output.txt"
|
||||
configType: startup-config
|
||||
protocol: "tftp"
|
||||
serverip: "10.241.106.118"
|
||||
rcpath: "/anil/G8272-startup-config.txt"
|
||||
serverusername: "root"
|
||||
serverpassword: "root123"
|
||||
|
||||
'''
|
||||
RETURN = '''
|
||||
msg:
|
||||
description: Success or failure message
|
||||
returned: always
|
||||
type: str
|
||||
sample: "Config file transferred to Device"
|
||||
'''
|
||||
|
||||
import sys
|
||||
import time
|
||||
import socket
|
||||
import array
|
||||
import json
|
||||
import time
|
||||
import re
|
||||
import os
|
||||
try:
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos import cnos
|
||||
HAS_LIB = True
|
||||
except Exception:
|
||||
HAS_LIB = False
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
# Utility Method to rollback the running config or start up config
|
||||
# This method supports only SCP or SFTP or FTP or TFTP
|
||||
def doConfigRollBack(module, prompt, answer):
|
||||
host = module.params['host']
|
||||
server = module.params['serverip']
|
||||
username = module.params['serverusername']
|
||||
password = module.params['serverpassword']
|
||||
protocol = module.params['protocol'].lower()
|
||||
rcPath = module.params['rcpath']
|
||||
configType = module.params['configType']
|
||||
confPath = rcPath
|
||||
retVal = ''
|
||||
|
||||
command = "copy " + protocol + " " + protocol + "://"
|
||||
command = command + username + "@" + server + "/" + confPath
|
||||
command = command + " " + configType + " vrf management\n"
|
||||
cnos.debugOutput(command + "\n")
|
||||
# cnos.checkForFirstTimeAccess(module, command, 'yes/no', 'yes')
|
||||
cmd = []
|
||||
if(protocol == "scp"):
|
||||
scp_cmd1 = [{'command': command, 'prompt': 'timeout:', 'answer': '0'}]
|
||||
scp_cmd2 = [{'command': '\n', 'prompt': 'Password:',
|
||||
'answer': password}]
|
||||
cmd.extend(scp_cmd1)
|
||||
cmd.extend(scp_cmd2)
|
||||
if(configType == 'startup-config'):
|
||||
scp_cmd3 = [{'command': 'y', 'prompt': None, 'answer': None}]
|
||||
cmd.extend(scp_cmd3)
|
||||
retVal = retVal + str(cnos.run_cnos_commands(module, cmd))
|
||||
elif(protocol == "sftp"):
|
||||
sftp_cmd = [{'command': command, 'prompt': 'Password:',
|
||||
'answer': password}]
|
||||
cmd.extend(sftp_cmd)
|
||||
# cnos.debugOutput(configType + "\n")
|
||||
if(configType == 'startup-config'):
|
||||
sftp_cmd2 = [{'command': 'y', 'prompt': None, 'answer': None}]
|
||||
cmd.extend(sftp_cmd2)
|
||||
retVal = retVal + str(cnos.run_cnos_commands(module, cmd))
|
||||
elif(protocol == "ftp"):
|
||||
ftp_cmd = [{'command': command, 'prompt': 'Password:',
|
||||
'answer': password}]
|
||||
cmd.extend(ftp_cmd)
|
||||
if(configType == 'startup-config'):
|
||||
ftp_cmd2 = [{'command': 'y', 'prompt': None, 'answer': None}]
|
||||
cmd.extend(ftp_cmd2)
|
||||
retVal = retVal + str(cnos.run_cnos_commands(module, cmd))
|
||||
elif(protocol == "tftp"):
|
||||
command = "copy " + protocol + " " + protocol
|
||||
command = command + "://" + server + "/" + confPath
|
||||
command = command + " " + configType + " vrf management\n"
|
||||
cnos.debugOutput(command)
|
||||
tftp_cmd = [{'command': command, 'prompt': None, 'answer': None}]
|
||||
cmd.extend(tftp_cmd)
|
||||
if(configType == 'startup-config'):
|
||||
tftp_cmd2 = [{'command': 'y', 'prompt': None, 'answer': None}]
|
||||
cmd.extend(tftp_cmd2)
|
||||
retVal = retVal + str(cnos.run_cnos_commands(module, cmd))
|
||||
else:
|
||||
return "Error-110"
|
||||
|
||||
return retVal
|
||||
# EOM
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
outputfile=dict(required=True),
|
||||
host=dict(required=False),
|
||||
username=dict(required=False),
|
||||
password=dict(required=False, no_log=True),
|
||||
enablePassword=dict(required=False, no_log=True),
|
||||
deviceType=dict(required=True),
|
||||
configType=dict(required=True),
|
||||
protocol=dict(required=True),
|
||||
serverip=dict(required=True),
|
||||
rcpath=dict(required=True),
|
||||
serverusername=dict(required=False),
|
||||
serverpassword=dict(required=False, no_log=True),),
|
||||
supports_check_mode=False)
|
||||
|
||||
outputfile = module.params['outputfile']
|
||||
protocol = module.params['protocol'].lower()
|
||||
output = ''
|
||||
if protocol in ('tftp', 'ftp', 'sftp', 'scp'):
|
||||
transfer_status = doConfigRollBack(module, None, None)
|
||||
else:
|
||||
transfer_status = 'Invalid Protocol option'
|
||||
output = output + "\n Config Transfer status \n" + transfer_status
|
||||
|
||||
# Save it into the file
|
||||
if '/' in outputfile:
|
||||
path = outputfile.rsplit('/', 1)
|
||||
# cnos.debugOutput(path[0])
|
||||
if not os.path.exists(path[0]):
|
||||
os.makedirs(path[0])
|
||||
file = open(outputfile, "a")
|
||||
file.write(output)
|
||||
file.close()
|
||||
|
||||
# need to add logic to check when changes occur or not
|
||||
errorMsg = cnos.checkOutputForError(output)
|
||||
if(errorMsg is None):
|
||||
module.exit_json(changed=True, msg="Config file transferred to Device")
|
||||
else:
|
||||
module.fail_json(msg=errorMsg)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
116
plugins/modules/network/cnos/cnos_save.py
Normal file
116
plugins/modules/network/cnos/cnos_save.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
#
|
||||
# Copyright (C) 2017 Lenovo, Inc.
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
# Module to save running config to start up config to Lenovo Switches
|
||||
# Lenovo Networking
|
||||
#
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: cnos_save
|
||||
author: "Anil Kumar Muraleedharan (@amuraleedhar)"
|
||||
short_description: Save the running configuration as the startup configuration
|
||||
on devices running Lenovo CNOS
|
||||
description:
|
||||
- This module allows you to copy the running configuration of a switch over
|
||||
its startup configuration. It is recommended to use this module shortly
|
||||
after any major configuration changes so they persist after a switch
|
||||
restart. This module uses SSH to manage network device configuration.
|
||||
The results of the operation will be placed in a directory named 'results'
|
||||
that must be created by the user in their local directory to where the
|
||||
playbook is run.
|
||||
extends_documentation_fragment:
|
||||
- community.general.cnos
|
||||
|
||||
options: {}
|
||||
|
||||
'''
|
||||
EXAMPLES = '''
|
||||
Tasks : The following are examples of using the module cnos_save. These are
|
||||
written in the main.yml file of the tasks directory.
|
||||
---
|
||||
- name: Test Save
|
||||
cnos_save:
|
||||
deviceType: "{{ hostvars[inventory_hostname]['deviceType'] }}"
|
||||
outputfile: "./results/test_save_{{ inventory_hostname }}_output.txt"
|
||||
'''
|
||||
RETURN = '''
|
||||
msg:
|
||||
description: Success or failure message
|
||||
returned: always
|
||||
type: str
|
||||
sample: "Switch Running Config is Saved to Startup Config"
|
||||
'''
|
||||
|
||||
import sys
|
||||
import time
|
||||
import socket
|
||||
import array
|
||||
import json
|
||||
import time
|
||||
import re
|
||||
try:
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos import cnos
|
||||
HAS_LIB = True
|
||||
except Exception:
|
||||
HAS_LIB = False
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
outputfile=dict(required=True),
|
||||
host=dict(required=False),
|
||||
username=dict(required=False),
|
||||
password=dict(required=False, no_log=True),
|
||||
enablePassword=dict(required=False, no_log=True),
|
||||
deviceType=dict(required=True),),
|
||||
supports_check_mode=False)
|
||||
|
||||
command = 'write memory'
|
||||
outputfile = module.params['outputfile']
|
||||
output = ''
|
||||
cmd = [{'command': command, 'prompt': None, 'answer': None}]
|
||||
output = output + str(cnos.run_cnos_commands(module, cmd))
|
||||
|
||||
# Save it into the file
|
||||
file = open(outputfile, "a")
|
||||
file.write(output)
|
||||
file.close()
|
||||
|
||||
errorMsg = cnos.checkOutputForError(output)
|
||||
if(errorMsg is None):
|
||||
module.exit_json(changed=True,
|
||||
msg="Switch Running Config is Saved to Startup Config ")
|
||||
else:
|
||||
module.fail_json(msg=errorMsg)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
114
plugins/modules/network/cnos/cnos_showrun.py
Normal file
114
plugins/modules/network/cnos/cnos_showrun.py
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
#
|
||||
# Copyright (C) 2017 Lenovo, Inc.
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
# Module to display running config of Switches
|
||||
# Lenovo Networking
|
||||
#
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: cnos_showrun
|
||||
author: "Anil Kumar Muraleedharan (@amuraleedhar)"
|
||||
short_description: Collect the current running configuration on devices running on CNOS
|
||||
description:
|
||||
- This module allows you to view the switch running configuration. It
|
||||
executes the display running-config CLI command on a switch and returns a
|
||||
file containing the current running configuration of the target network
|
||||
device. This module uses SSH to manage network device configuration.
|
||||
The results of the operation will be placed in a directory named 'results'
|
||||
that must be created by the user in their local directory to where the
|
||||
playbook is run.
|
||||
extends_documentation_fragment:
|
||||
- community.general.cnos
|
||||
|
||||
options: {}
|
||||
|
||||
'''
|
||||
EXAMPLES = '''
|
||||
Tasks : The following are examples of using the module cnos_showrun. These are
|
||||
written in the main.yml file of the tasks directory.
|
||||
---
|
||||
- name: Run show running-config
|
||||
cnos_showrun:
|
||||
deviceType: "{{ hostvars[inventory_hostname]['deviceType'] }}"
|
||||
outputfile: "./results/test_showrun_{{ inventory_hostname }}_output.txt"
|
||||
|
||||
'''
|
||||
RETURN = '''
|
||||
msg:
|
||||
description: Success or failure message
|
||||
returned: always
|
||||
type: str
|
||||
sample: "Running Configuration saved in file"
|
||||
'''
|
||||
|
||||
import sys
|
||||
import time
|
||||
import socket
|
||||
import array
|
||||
import json
|
||||
import time
|
||||
import re
|
||||
try:
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos import cnos
|
||||
HAS_LIB = True
|
||||
except Exception:
|
||||
HAS_LIB = False
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
outputfile=dict(required=True),
|
||||
host=dict(required=False),
|
||||
username=dict(required=False),
|
||||
password=dict(required=False, no_log=True),
|
||||
enablePassword=dict(required=False, no_log=True),),
|
||||
supports_check_mode=False)
|
||||
|
||||
command = 'show running-config'
|
||||
outputfile = module.params['outputfile']
|
||||
output = ''
|
||||
cmd = [{'command': command, 'prompt': None, 'answer': None}]
|
||||
output = output + str(cnos.run_cnos_commands(module, cmd))
|
||||
# Save it into the file
|
||||
file = open(outputfile, "a")
|
||||
file.write(output)
|
||||
file.close()
|
||||
|
||||
errorMsg = cnos.checkOutputForError(output)
|
||||
if(errorMsg is None):
|
||||
module.exit_json(changed=True,
|
||||
msg="Running Configuration saved in file ")
|
||||
else:
|
||||
module.fail_json(msg=errorMsg)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
288
plugins/modules/network/cnos/cnos_static_route.py
Normal file
288
plugins/modules/network/cnos/cnos_static_route.py
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
#
|
||||
# Copyright (C) 2019 Lenovo, Inc.
|
||||
# (c) 2017, Ansible by Red Hat, inc
|
||||
# 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/>.
|
||||
#
|
||||
# Module to work on Link Aggregation with Lenovo Switches
|
||||
# Lenovo Networking
|
||||
#
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: cnos_static_route
|
||||
author: "Anil Kumar Muraleedharan (@amuraleedhar)"
|
||||
short_description: Manage static IP routes on Lenovo CNOS network devices
|
||||
description:
|
||||
- This module provides declarative management of static
|
||||
IP routes on Lenovo CNOS network devices.
|
||||
notes:
|
||||
- Tested against CNOS 10.10.1
|
||||
options:
|
||||
prefix:
|
||||
description:
|
||||
- Network prefix of the static route.
|
||||
mask:
|
||||
description:
|
||||
- Network prefix mask of the static route.
|
||||
next_hop:
|
||||
description:
|
||||
- Next hop IP of the static route.
|
||||
interface:
|
||||
description:
|
||||
- Interface of the static route.
|
||||
description:
|
||||
description:
|
||||
- Name of the static route
|
||||
aliases: ['description']
|
||||
admin_distance:
|
||||
description:
|
||||
- Admin distance of the static route.
|
||||
default: 1
|
||||
tag:
|
||||
description:
|
||||
- Set tag of the static route.
|
||||
aggregate:
|
||||
description: List of static route definitions.
|
||||
state:
|
||||
description:
|
||||
- State of the static route configuration.
|
||||
default: present
|
||||
choices: ['present', 'absent']
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: configure static route
|
||||
cnos_static_route:
|
||||
prefix: 10.241.107.0
|
||||
mask: 255.255.255.0
|
||||
next_hop: 10.241.106.1
|
||||
|
||||
- name: configure ultimate route with name and tag
|
||||
cnos_static_route:
|
||||
prefix: 10.241.107.0
|
||||
mask: 255.255.255.0
|
||||
interface: Ethernet1/13
|
||||
description: hello world
|
||||
tag: 100
|
||||
|
||||
- name: remove configuration
|
||||
cnos_static_route:
|
||||
prefix: 10.241.107.0
|
||||
mask: 255.255.255.0
|
||||
next_hop: 10.241.106.0
|
||||
state: absent
|
||||
|
||||
- name: Add static route aggregates
|
||||
cnos_static_route:
|
||||
aggregate:
|
||||
- { prefix: 10.241.107.0, mask: 255.255.255.0, next_hop: 10.241.105.0 }
|
||||
- { prefix: 10.241.106.0, mask: 255.255.255.0, next_hop: 10.241.104.0 }
|
||||
|
||||
- name: Remove static route aggregates
|
||||
cnos_static_route:
|
||||
aggregate:
|
||||
- { prefix: 10.241.107.0, mask: 255.255.255.0, next_hop: 10.241.105.0 }
|
||||
- { prefix: 10.241.106.0, mask: 255.255.255.0, next_hop: 10.241.104.0 }
|
||||
state: absent
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
commands:
|
||||
description: The list of configuration mode commands to send to the device
|
||||
returned: always
|
||||
type: list
|
||||
sample:
|
||||
- ip route 10.241.107.0 255.255.255.0 10.241.106.0
|
||||
"""
|
||||
from copy import deepcopy
|
||||
from re import findall
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.compat import ipaddress
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import validate_ip_address
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import remove_default_spec
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import get_config, load_config
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import check_args
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import cnos_argument_spec
|
||||
|
||||
|
||||
def map_obj_to_commands(want, have):
|
||||
commands = list()
|
||||
|
||||
for w in want:
|
||||
state = w['state']
|
||||
command = 'ip route'
|
||||
prefix = w['prefix']
|
||||
mask = w['mask']
|
||||
command = ' '.join((command, prefix, mask))
|
||||
|
||||
for key in ['interface', 'next_hop', 'admin_distance', 'tag',
|
||||
'description']:
|
||||
if w.get(key):
|
||||
if key == 'description' and len(w.get(key).split()) > 1:
|
||||
# name with multiple words needs to be quoted
|
||||
command = ' '.join((command, key, '"%s"' % w.get(key)))
|
||||
elif key in ('description', 'tag'):
|
||||
command = ' '.join((command, key, w.get(key)))
|
||||
else:
|
||||
command = ' '.join((command, w.get(key)))
|
||||
|
||||
if state == 'absent':
|
||||
commands.append('no %s' % command)
|
||||
elif state == 'present':
|
||||
commands.append(command)
|
||||
|
||||
return commands
|
||||
|
||||
|
||||
def map_config_to_obj(module):
|
||||
obj = []
|
||||
|
||||
out = get_config(module, flags='| include ip route')
|
||||
for line in out.splitlines():
|
||||
# Split by whitespace but do not split quotes, needed for description
|
||||
splitted_line = findall(r'[^"\s]\S*|".+?"', line)
|
||||
route = {}
|
||||
prefix_with_mask = splitted_line[2]
|
||||
prefix = None
|
||||
mask = None
|
||||
iface = None
|
||||
nhop = None
|
||||
if validate_ip_address(prefix_with_mask) is True:
|
||||
my_net = ipaddress.ip_network(prefix_with_mask)
|
||||
prefix = str(my_net.network_address)
|
||||
mask = str(my_net.netmask)
|
||||
route.update({'prefix': prefix,
|
||||
'mask': mask, 'admin_distance': '1'})
|
||||
if splitted_line[3] is not None:
|
||||
if validate_ip_address(splitted_line[3]) is False:
|
||||
iface = str(splitted_line[3])
|
||||
route.update(interface=iface)
|
||||
if validate_ip_address(splitted_line[4]) is True:
|
||||
nhop = str(splitted_line[4])
|
||||
route.update(next_hop=nhop)
|
||||
if splitted_line[5].isdigit():
|
||||
route.update(admin_distance=str(splitted_line[5]))
|
||||
elif splitted_line[4].isdigit():
|
||||
route.update(admin_distance=str(splitted_line[4]))
|
||||
else:
|
||||
if splitted_line[6] is not None and splitted_line[6].isdigit():
|
||||
route.update(admin_distance=str(splitted_line[6]))
|
||||
else:
|
||||
nhop = str(splitted_line[3])
|
||||
route.update(next_hop=nhop)
|
||||
if splitted_line[4].isdigit():
|
||||
route.update(admin_distance=str(splitted_line[4]))
|
||||
|
||||
index = 0
|
||||
for word in splitted_line:
|
||||
if word in ('tag', 'description'):
|
||||
route.update(word=splitted_line[index + 1])
|
||||
index = index + 1
|
||||
obj.append(route)
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
def map_params_to_obj(module, required_together=None):
|
||||
keys = ['prefix', 'mask', 'state', 'next_hop', 'interface', 'description',
|
||||
'admin_distance', 'tag']
|
||||
obj = []
|
||||
|
||||
aggregate = module.params.get('aggregate')
|
||||
if aggregate:
|
||||
for item in aggregate:
|
||||
route = item.copy()
|
||||
for key in keys:
|
||||
if route.get(key) is None:
|
||||
route[key] = module.params.get(key)
|
||||
|
||||
route = dict((k, v) for k, v in route.items() if v is not None)
|
||||
module._check_required_together(required_together, route)
|
||||
obj.append(route)
|
||||
else:
|
||||
module._check_required_together(required_together, module.params)
|
||||
route = dict()
|
||||
for key in keys:
|
||||
if module.params.get(key) is not None:
|
||||
route[key] = module.params.get(key)
|
||||
obj.append(route)
|
||||
|
||||
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'),
|
||||
interface=dict(type='str'),
|
||||
description=dict(type='str'),
|
||||
admin_distance=dict(type='str', default='1'),
|
||||
tag=dict(type='str'),
|
||||
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)
|
||||
|
||||
required_one_of = [['aggregate', 'prefix']]
|
||||
required_together = [['prefix', 'mask']]
|
||||
mutually_exclusive = [['aggregate', 'prefix']]
|
||||
|
||||
module = AnsibleModule(argument_spec=argument_spec,
|
||||
required_one_of=required_one_of,
|
||||
mutually_exclusive=mutually_exclusive,
|
||||
supports_check_mode=True)
|
||||
|
||||
warnings = list()
|
||||
check_args(module, warnings)
|
||||
|
||||
result = {'changed': False}
|
||||
if warnings:
|
||||
result['warnings'] = warnings
|
||||
want = map_params_to_obj(module, required_together=required_together)
|
||||
have = map_config_to_obj(module)
|
||||
|
||||
commands = map_obj_to_commands(want, have)
|
||||
result['commands'] = commands
|
||||
if commands:
|
||||
if not module.check_mode:
|
||||
load_config(module, commands)
|
||||
|
||||
result['changed'] = True
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
387
plugins/modules/network/cnos/cnos_system.py
Normal file
387
plugins/modules/network/cnos/cnos_system.py
Normal file
|
|
@ -0,0 +1,387 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
#
|
||||
# Copyright (C) 2019 Lenovo.
|
||||
# (c) 2017, Ansible by Red Hat, inc
|
||||
# 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/>.
|
||||
#
|
||||
# Module to work on System Configuration with Lenovo Switches
|
||||
# Lenovo Networking
|
||||
#
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: cnos_system
|
||||
author: "Anil Kumar Muraleedharan (@amuraleedhar)"
|
||||
short_description: Manage the system attributes on Lenovo CNOS devices
|
||||
description:
|
||||
- This module provides declarative management of node system attributes
|
||||
on Lenovo CNOS devices. It provides an option to configure host system
|
||||
parameters or remove those parameters from the device active
|
||||
configuration.
|
||||
options:
|
||||
hostname:
|
||||
description:
|
||||
- Configure the device hostname parameter. This option takes an
|
||||
ASCII string value or keyword 'default'
|
||||
domain_name:
|
||||
description:
|
||||
- Configures the default domain
|
||||
name suffix to be used when referencing this node by its
|
||||
FQDN. This argument accepts either a list of domain names or
|
||||
a list of dicts that configure the domain name and VRF name or
|
||||
keyword 'default'. See examples.
|
||||
lookup_enabled:
|
||||
description:
|
||||
- Administrative control for enabling or disabling DNS lookups.
|
||||
When this argument is set to True, lookups are performed and
|
||||
when it is set to False, lookups are not performed.
|
||||
type: bool
|
||||
domain_search:
|
||||
description:
|
||||
- Configures a list of domain
|
||||
name suffixes to search when performing DNS name resolution.
|
||||
This argument accepts either a list of domain names or
|
||||
a list of dicts that configure the domain name and VRF name or
|
||||
keyword 'default'. See examples.
|
||||
name_servers:
|
||||
description:
|
||||
- List of DNS name servers by IP address to use to perform name resolution
|
||||
lookups. This argument accepts either a list of DNS servers or
|
||||
a list of hashes that configure the name server and VRF name or
|
||||
keyword 'default'. See examples.
|
||||
lookup_source:
|
||||
description:
|
||||
- Provides one or more source interfaces to use for performing DNS
|
||||
lookups. The interface must be a valid interface configured.
|
||||
on the device.
|
||||
state:
|
||||
description:
|
||||
- State of the configuration
|
||||
values in the device's current active configuration. When set
|
||||
to I(present), the values should be configured in the device active
|
||||
configuration and when set to I(absent) the values should not be
|
||||
in the device active configuration
|
||||
default: present
|
||||
choices: ['present', 'absent']
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: configure hostname and domain-name
|
||||
cnos_system:
|
||||
hostname: cnos01
|
||||
domain_name: test.example.com
|
||||
|
||||
- name: remove configuration
|
||||
cnos_system:
|
||||
state: absent
|
||||
|
||||
- name: configure name servers
|
||||
cnos_system:
|
||||
name_servers:
|
||||
- 8.8.8.8
|
||||
- 8.8.4.4
|
||||
|
||||
- name: configure DNS Lookup sources
|
||||
cnos_system:
|
||||
lookup_source: MgmtEth0/0/CPU0/0
|
||||
lookup_enabled: yes
|
||||
|
||||
- name: configure name servers with VRF support
|
||||
nxos_system:
|
||||
name_servers:
|
||||
- { server: 8.8.8.8, vrf: mgmt }
|
||||
- { server: 8.8.4.4, vrf: mgmt }
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
commands:
|
||||
description: The list of configuration mode commands to send to the device
|
||||
returned: always
|
||||
type: list
|
||||
sample:
|
||||
- hostname cnos01
|
||||
- ip domain-name test.example.com vrf default
|
||||
"""
|
||||
import re
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import get_config, load_config
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import cnos_argument_spec
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import check_args, debugOutput
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.six import iteritems
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import NetworkConfig
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ComplexList
|
||||
|
||||
_CONFIGURED_VRFS = None
|
||||
|
||||
|
||||
def map_obj_to_commands(want, have, module):
|
||||
commands = list()
|
||||
state = module.params['state']
|
||||
|
||||
def needs_update(x):
|
||||
return want.get(x) and (want.get(x) != have.get(x))
|
||||
|
||||
def difference(x, y, z):
|
||||
return [item for item in x[z] if item not in y[z]]
|
||||
|
||||
if state == 'absent':
|
||||
if have['hostname']:
|
||||
commands.append('no hostname')
|
||||
|
||||
for item in have['domain_name']:
|
||||
my_vrf = 'default'
|
||||
if item['vrf'] is not None:
|
||||
my_vrf = item['vrf']
|
||||
cmd = 'no ip domain-name {0} vrf {1}'.format(item['name'], my_vrf)
|
||||
commands.append(cmd)
|
||||
|
||||
for item in have['domain_search']:
|
||||
my_vrf = 'default'
|
||||
if item['vrf'] is not None:
|
||||
my_vrf = item['vrf']
|
||||
cmd = 'no ip domain-list {0} vrf {1}'.format(item['name'], my_vrf)
|
||||
commands.append(cmd)
|
||||
|
||||
for item in have['name_servers']:
|
||||
my_vrf = 'default'
|
||||
if item['vrf'] is not None:
|
||||
my_vrf = item['vrf']
|
||||
cmd = 'no ip name-server {0} vrf {1}'.format(item['server'], my_vrf)
|
||||
commands.append(cmd)
|
||||
|
||||
if state == 'present':
|
||||
if needs_update('hostname'):
|
||||
if want['hostname'] == 'default':
|
||||
if have['hostname']:
|
||||
commands.append('no hostname')
|
||||
else:
|
||||
commands.append('hostname %s' % want['hostname'])
|
||||
|
||||
if want.get('lookup_enabled') is not None:
|
||||
if have.get('lookup_enabled') != want.get('lookup_enabled'):
|
||||
cmd = 'ip domain-lookup'
|
||||
if want['lookup_enabled'] is False:
|
||||
cmd = 'no %s' % cmd
|
||||
commands.append(cmd)
|
||||
|
||||
if want['domain_name']:
|
||||
if want.get('domain_name')[0]['name'] == 'default':
|
||||
if have['domain_name']:
|
||||
for item in have['domain_name']:
|
||||
my_vrf = 'default'
|
||||
if item['vrf'] is not None:
|
||||
my_vrf = item['vrf']
|
||||
cmd = 'no ip domain-name {0} vrf {1}'.format(item['name'], my_vrf)
|
||||
commands.append(cmd)
|
||||
else:
|
||||
for item in difference(have, want, 'domain_name'):
|
||||
my_vrf = 'default'
|
||||
if item['vrf'] is not None:
|
||||
my_vrf = item['vrf']
|
||||
cmd = 'no ip domain-name {0} vrf {1}'.format(item['name'], my_vrf)
|
||||
commands.append(cmd)
|
||||
for item in difference(want, have, 'domain_name'):
|
||||
my_vrf = 'default'
|
||||
if item['vrf'] is not None:
|
||||
my_vrf = item['vrf']
|
||||
cmd = 'ip domain-name {0} vrf {1}'.format(item['name'], my_vrf)
|
||||
commands.append(cmd)
|
||||
|
||||
if want['domain_search']:
|
||||
if want.get('domain_search')[0]['name'] == 'default':
|
||||
if have['domain_search']:
|
||||
for item in have['domain_search']:
|
||||
my_vrf = 'default'
|
||||
if item['vrf'] is not None:
|
||||
my_vrf = item['vrf']
|
||||
cmd = 'no ip domain-list {0} vrf {1}'.format(item['name'], my_vrf)
|
||||
commands.append(cmd)
|
||||
else:
|
||||
for item in difference(have, want, 'domain_search'):
|
||||
my_vrf = 'default'
|
||||
if item['vrf'] is not None:
|
||||
my_vrf = item['vrf']
|
||||
cmd = 'no ip domain-list {0} vrf {1}'.format(item['name'], my_vrf)
|
||||
commands.append(cmd)
|
||||
for item in difference(want, have, 'domain_search'):
|
||||
my_vrf = 'default'
|
||||
if item['vrf'] is not None:
|
||||
my_vrf = item['vrf']
|
||||
cmd = 'ip domain-list {0} vrf {1}'.format(item['name'], my_vrf)
|
||||
commands.append(cmd)
|
||||
|
||||
if want['name_servers']:
|
||||
if want.get('name_servers')[0]['server'] == 'default':
|
||||
if have['name_servers']:
|
||||
for item in have['name_servers']:
|
||||
my_vrf = 'default'
|
||||
if item['vrf'] is not None:
|
||||
my_vrf = item['vrf']
|
||||
cmd = 'no ip name-server {0} vrf {1}'.format(item['server'], my_vrf)
|
||||
commands.append(cmd)
|
||||
else:
|
||||
for item in difference(have, want, 'name_servers'):
|
||||
my_vrf = 'default'
|
||||
if item['vrf'] is not None:
|
||||
my_vrf = item['vrf']
|
||||
cmd = 'no ip name-server {0} vrf {1}'.format(item['server'], my_vrf)
|
||||
commands.append(cmd)
|
||||
for item in difference(want, have, 'name_servers'):
|
||||
my_vrf = 'default'
|
||||
if item['vrf'] is not None:
|
||||
my_vrf = item['vrf']
|
||||
cmd = 'ip name-server {0} vrf {1}'.format(item['server'], my_vrf)
|
||||
commands.append(cmd)
|
||||
|
||||
return commands
|
||||
|
||||
|
||||
def parse_hostname(config):
|
||||
match = re.search(r'^hostname (\S+)', config, re.M)
|
||||
if match:
|
||||
return match.group(1)
|
||||
|
||||
|
||||
def parse_domain_name(config):
|
||||
objects = list()
|
||||
myconf = config.splitlines()
|
||||
for line in myconf:
|
||||
if 'ip domain-name' in line:
|
||||
datas = line.split()
|
||||
objects.append({'name': datas[2], 'vrf': datas[4]})
|
||||
|
||||
return objects
|
||||
|
||||
|
||||
def parse_domain_search(config):
|
||||
objects = list()
|
||||
myconf = config.splitlines()
|
||||
for line in myconf:
|
||||
if 'ip domain-list' in line:
|
||||
datas = line.split()
|
||||
objects.append({'name': datas[2], 'vrf': datas[4]})
|
||||
|
||||
return objects
|
||||
|
||||
|
||||
def parse_name_servers(config):
|
||||
objects = list()
|
||||
myconf = config.splitlines()
|
||||
for line in myconf:
|
||||
if 'ip name-server' in line:
|
||||
datas = line.split()
|
||||
objects.append({'server': datas[2], 'vrf': datas[4]})
|
||||
|
||||
return objects
|
||||
|
||||
|
||||
def map_config_to_obj(module):
|
||||
config = get_config(module)
|
||||
configobj = NetworkConfig(indent=2, contents=config)
|
||||
|
||||
return {
|
||||
'hostname': parse_hostname(config),
|
||||
'lookup_enabled': 'no ip domain-lookup' not in config,
|
||||
'domain_name': parse_domain_name(config),
|
||||
'domain_search': parse_domain_search(config),
|
||||
'name_servers': parse_name_servers(config),
|
||||
}
|
||||
|
||||
|
||||
def map_params_to_obj(module):
|
||||
obj = {
|
||||
'hostname': module.params['hostname'],
|
||||
'lookup_enabled': module.params['lookup_enabled'],
|
||||
}
|
||||
|
||||
domain_name = ComplexList(dict(
|
||||
name=dict(key=True),
|
||||
vrf=dict()
|
||||
), module)
|
||||
|
||||
domain_search = ComplexList(dict(
|
||||
name=dict(key=True),
|
||||
vrf=dict()
|
||||
), module)
|
||||
|
||||
name_servers = ComplexList(dict(
|
||||
server=dict(key=True),
|
||||
vrf=dict()
|
||||
), module)
|
||||
|
||||
for arg, cast in [('domain_name', domain_name),
|
||||
('domain_search', domain_search),
|
||||
('name_servers', name_servers)]:
|
||||
if module.params[arg] is not None:
|
||||
obj[arg] = cast(module.params[arg])
|
||||
else:
|
||||
obj[arg] = None
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
def main():
|
||||
""" main entry point for module execution
|
||||
"""
|
||||
argument_spec = dict(
|
||||
hostname=dict(),
|
||||
lookup_enabled=dict(type='bool'),
|
||||
|
||||
# { name: <str>, vrf: <str> }
|
||||
domain_name=dict(type='list'),
|
||||
|
||||
# {name: <str>, vrf: <str> }
|
||||
domain_search=dict(type='list'),
|
||||
|
||||
# { server: <str>; vrf: <str> }
|
||||
name_servers=dict(type='list'),
|
||||
|
||||
lookup_source=dict(type='str'),
|
||||
state=dict(default='present', choices=['present', 'absent'])
|
||||
)
|
||||
|
||||
module = AnsibleModule(argument_spec=argument_spec,
|
||||
supports_check_mode=True)
|
||||
|
||||
warnings = list()
|
||||
check_args(module, warnings)
|
||||
|
||||
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:
|
||||
if not module.check_mode:
|
||||
load_config(module, commands)
|
||||
result['changed'] = True
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
151
plugins/modules/network/cnos/cnos_template.py
Normal file
151
plugins/modules/network/cnos/cnos_template.py
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
#
|
||||
# Copyright (C) 2017 Lenovo, Inc.
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
# Module to send CLI templates to Lenovo Switches
|
||||
# Lenovo Networking
|
||||
#
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: cnos_template
|
||||
author: "Anil Kumar Muraleedharan (@amuraleedhar)"
|
||||
short_description: Manage switch configuration using templates on devices running Lenovo CNOS
|
||||
description:
|
||||
- This module allows you to work with the running configuration of a switch. It provides a way
|
||||
to execute a set of CNOS commands on a switch by evaluating the current running configuration
|
||||
and executing the commands only if the specific settings have not been already configured.
|
||||
The configuration source can be a set of commands or a template written in the Jinja2 templating language.
|
||||
This module uses SSH to manage network device configuration.
|
||||
The results of the operation will be placed in a directory named 'results'
|
||||
that must be created by the user in their local directory to where the playbook is run.
|
||||
extends_documentation_fragment:
|
||||
- community.general.cnos
|
||||
|
||||
options:
|
||||
commandfile:
|
||||
description:
|
||||
- This specifies the path to the CNOS command file which needs to be applied. This usually
|
||||
comes from the commands folder. Generally this file is the output of the variables applied
|
||||
on a template file. So this command is preceded by a template module.
|
||||
Note The command file must contain the Ansible keyword {{ inventory_hostname }} in its
|
||||
filename to ensure that the command file is unique for each switch and condition.
|
||||
If this is omitted, the command file will be overwritten during iteration. For example,
|
||||
commandfile=./commands/clos_leaf_bgp_{{ inventory_hostname }}_commands.txt
|
||||
required: true
|
||||
default: Null
|
||||
'''
|
||||
EXAMPLES = '''
|
||||
Tasks : The following are examples of using the module cnos_template. These are written in the main.yml file of the tasks directory.
|
||||
---
|
||||
- name: Replace Config CLI command template with values
|
||||
template:
|
||||
src: demo_template.j2
|
||||
dest: "./commands/demo_template_{{ inventory_hostname }}_commands.txt"
|
||||
vlanid1: 13
|
||||
slot_chassis_number1: "1/2"
|
||||
portchannel_interface_number1: 100
|
||||
portchannel_mode1: "active"
|
||||
|
||||
- name: Applying CLI commands on Switches
|
||||
cnos_template:
|
||||
deviceType: "{{ hostvars[inventory_hostname]['deviceType'] }}"
|
||||
commandfile: "./commands/demo_template_{{ inventory_hostname }}_commands.txt"
|
||||
outputfile: "./results/demo_template_command_{{ inventory_hostname }}_output.txt"
|
||||
|
||||
'''
|
||||
RETURN = '''
|
||||
msg:
|
||||
description: Success or failure message
|
||||
returned: always
|
||||
type: str
|
||||
sample: "Template Applied."
|
||||
'''
|
||||
|
||||
import sys
|
||||
import time
|
||||
import socket
|
||||
import array
|
||||
import json
|
||||
import time
|
||||
import re
|
||||
import os
|
||||
try:
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos import cnos
|
||||
HAS_LIB = True
|
||||
except Exception:
|
||||
HAS_LIB = False
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
commandfile=dict(required=True),
|
||||
outputfile=dict(required=True),
|
||||
host=dict(required=False),
|
||||
deviceType=dict(required=True),
|
||||
username=dict(required=False),
|
||||
password=dict(required=False, no_log=True),
|
||||
enablePassword=dict(required=False, no_log=True),),
|
||||
supports_check_mode=False)
|
||||
commandfile = module.params['commandfile']
|
||||
outputfile = module.params['outputfile']
|
||||
output = ''
|
||||
|
||||
# Send commands one by one to the device
|
||||
f = open(commandfile, "r")
|
||||
cmd = []
|
||||
for line in f:
|
||||
# Omit the comment lines in template file
|
||||
if not line.startswith("#"):
|
||||
command = line.strip()
|
||||
inner_cmd = [{'command': command, 'prompt': None, 'answer': None}]
|
||||
cmd.extend(inner_cmd)
|
||||
# Write to memory
|
||||
save_cmd = [{'command': 'save', 'prompt': None, 'answer': None}]
|
||||
cmd.extend(save_cmd)
|
||||
output = output + str(cnos.run_cnos_commands(module, cmd))
|
||||
# Write output to file
|
||||
path = outputfile.rsplit('/', 1)
|
||||
# cnos.debugOutput(path[0])
|
||||
if not os.path.exists(path[0]):
|
||||
os.makedirs(path[0])
|
||||
file = open(outputfile, "a")
|
||||
file.write(output)
|
||||
file.close()
|
||||
|
||||
# Logic to check when changes occur or not
|
||||
errorMsg = cnos.checkOutputForError(output)
|
||||
if(errorMsg is None):
|
||||
module.exit_json(changed=True, msg="Template Applied")
|
||||
else:
|
||||
module.fail_json(msg=errorMsg)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
390
plugins/modules/network/cnos/cnos_user.py
Normal file
390
plugins/modules/network/cnos/cnos_user.py
Normal file
|
|
@ -0,0 +1,390 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
#
|
||||
# Copyright (C) 2019 Lenovo.
|
||||
# (c) 2017, Ansible by Red Hat, inc
|
||||
# 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/>.
|
||||
#
|
||||
# Module to work on management of local users on Lenovo CNOS Switches
|
||||
# Lenovo Networking
|
||||
#
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: cnos_user
|
||||
author: "Anil Kumar Muraleedharan (@amuraleedhar)"
|
||||
short_description: Manage the collection of local users on Lenovo CNOS devices
|
||||
description:
|
||||
- This module provides declarative management of the local usernames
|
||||
configured on Lenovo CNOS 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.
|
||||
options:
|
||||
aggregate:
|
||||
description:
|
||||
- The set of username objects to be configured on the remote
|
||||
Lenovo CNOS 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 remote Lenovo CNOS
|
||||
device. This argument accepts a string value and is mutually
|
||||
exclusive with the C(aggregate) argument.
|
||||
configured_password:
|
||||
description:
|
||||
- The password to be configured on the network device. The
|
||||
password needs to be provided in cleartext 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']
|
||||
role:
|
||||
description:
|
||||
- The C(role) argument configures the role for the username in the
|
||||
device running configuration. The argument accepts a string value
|
||||
defining the role name. This argument does not check if the role
|
||||
has been configured on the device.
|
||||
aliases: ['roles']
|
||||
sshkey:
|
||||
description:
|
||||
- The C(sshkey) argument defines the SSH public key to configure
|
||||
for the username. This argument accepts a valid SSH key value.
|
||||
purge:
|
||||
description:
|
||||
- The C(purge) argument 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 which cannot be deleted per cnos constraints.
|
||||
type: bool
|
||||
default: 'no'
|
||||
state:
|
||||
description:
|
||||
- The C(state) argument 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']
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: create a new user
|
||||
cnos_user:
|
||||
name: ansible
|
||||
sshkey: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
|
||||
state: present
|
||||
|
||||
- name: remove all users except admin
|
||||
cnos_user:
|
||||
purge: yes
|
||||
|
||||
- name: set multiple users role
|
||||
aggregate:
|
||||
- name: netop
|
||||
- name: netend
|
||||
role: network-operator
|
||||
state: present
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
commands:
|
||||
description: The list of configuration mode commands to send to the device
|
||||
returned: always
|
||||
type: list
|
||||
sample:
|
||||
- name ansible
|
||||
- name ansible password password
|
||||
start:
|
||||
description: The time the job started
|
||||
returned: always
|
||||
type: str
|
||||
sample: "2016-11-16 10:38:15.126146"
|
||||
end:
|
||||
description: The time the job ended
|
||||
returned: always
|
||||
type: str
|
||||
sample: "2016-11-16 10:38:25.595612"
|
||||
delta:
|
||||
description: The time elapsed to perform all operations
|
||||
returned: always
|
||||
type: str
|
||||
sample: "0:00:10.469466"
|
||||
"""
|
||||
import re
|
||||
|
||||
from copy import deepcopy
|
||||
from functools import partial
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import run_commands, load_config
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import get_config
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import cnos_argument_spec
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.six import string_types, iteritems
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import remove_default_spec
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import get_user_roles
|
||||
|
||||
|
||||
def validate_roles(value, module):
|
||||
for item in value:
|
||||
if item not in get_user_roles():
|
||||
module.fail_json(msg='invalid role specified')
|
||||
|
||||
|
||||
def map_obj_to_commands(updates, module):
|
||||
commands = list()
|
||||
state = module.params['state']
|
||||
update_password = module.params['update_password']
|
||||
|
||||
for update in updates:
|
||||
want, have = update
|
||||
|
||||
def needs_update(x):
|
||||
return want.get(x) and (want.get(x) != have.get(x))
|
||||
|
||||
def add(x):
|
||||
return commands.append('username %s %s' % (want['name'], x))
|
||||
|
||||
def remove(x):
|
||||
return commands.append('no username %s %s' % (want['name'], x))
|
||||
|
||||
if want['state'] == 'absent':
|
||||
commands.append('no username %s' % want['name'])
|
||||
continue
|
||||
|
||||
if want['state'] == 'present' and not have:
|
||||
commands.append('username %s' % want['name'])
|
||||
|
||||
if needs_update('configured_password'):
|
||||
if update_password == 'always' or not have:
|
||||
add('password %s' % want['configured_password'])
|
||||
|
||||
if needs_update('sshkey'):
|
||||
add('sshkey %s' % want['sshkey'])
|
||||
|
||||
if want['roles']:
|
||||
if have:
|
||||
for item in set(have['roles']).difference(want['roles']):
|
||||
remove('role %s' % item)
|
||||
|
||||
for item in set(want['roles']).difference(have['roles']):
|
||||
add('role %s' % item)
|
||||
else:
|
||||
for item in want['roles']:
|
||||
add('role %s' % item)
|
||||
|
||||
return commands
|
||||
|
||||
|
||||
def parse_password(data):
|
||||
if 'no password set' in data:
|
||||
return None
|
||||
return '<PASSWORD>'
|
||||
|
||||
|
||||
def parse_roles(data):
|
||||
roles = list()
|
||||
if 'role:' in data:
|
||||
items = data.split()
|
||||
my_item = items[items.index('role:') + 1]
|
||||
roles.append(my_item)
|
||||
return roles
|
||||
|
||||
|
||||
def parse_username(data):
|
||||
name = data.split(' ', 1)[0]
|
||||
username = name[1:]
|
||||
return username
|
||||
|
||||
|
||||
def parse_sshkey(data):
|
||||
key = None
|
||||
if 'sskkey:' in data:
|
||||
items = data.split()
|
||||
key = items[items.index('sshkey:') + 1]
|
||||
return key
|
||||
|
||||
|
||||
def map_config_to_obj(module):
|
||||
out = run_commands(module, ['show user-account'])
|
||||
data = out[0]
|
||||
objects = list()
|
||||
datum = data.split('User')
|
||||
|
||||
for item in datum:
|
||||
objects.append({
|
||||
'name': parse_username(item),
|
||||
'configured_password': parse_password(item),
|
||||
'sshkey': parse_sshkey(item),
|
||||
'roles': parse_roles(item),
|
||||
'state': 'present'
|
||||
})
|
||||
return objects
|
||||
|
||||
|
||||
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]
|
||||
|
||||
# if key does exist, do a type check on it to validate it
|
||||
else:
|
||||
value_type = module.argument_spec[key].get('type', 'str')
|
||||
type_checker = module._CHECK_ARGUMENT_TYPES_DISPATCHER[value_type]
|
||||
type_checker(item[key])
|
||||
value = item[key]
|
||||
|
||||
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()
|
||||
elif not module.params['name']:
|
||||
module.fail_json(msg='username is required')
|
||||
else:
|
||||
collection = [{'name': module.params['name']}]
|
||||
else:
|
||||
collection = list()
|
||||
for item in aggregate:
|
||||
if not isinstance(item, dict):
|
||||
collection.append({'name': item})
|
||||
elif 'name' not in item:
|
||||
module.fail_json(msg='name is required')
|
||||
else:
|
||||
collection.append(item)
|
||||
|
||||
objects = list()
|
||||
|
||||
for item in collection:
|
||||
get_value = partial(get_param_value, item=item, module=module)
|
||||
item.update({
|
||||
'configured_password': get_value('configured_password'),
|
||||
'sshkey': get_value('sshkey'),
|
||||
'roles': get_value('roles'),
|
||||
'state': get_value('state')
|
||||
})
|
||||
|
||||
for key, value in iteritems(item):
|
||||
if value:
|
||||
# validate the param value (if validator func exists)
|
||||
validator = globals().get('validate_%s' % key)
|
||||
if all((value, validator)):
|
||||
validator(value, module)
|
||||
|
||||
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 all((item is None, entry['state'] == 'present')):
|
||||
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(),
|
||||
configured_password=dict(no_log=True),
|
||||
update_password=dict(default='always', choices=['on_create', 'always']),
|
||||
roles=dict(type='list', aliases=['role']),
|
||||
sshkey=dict(),
|
||||
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, aliases=['collection', 'users']),
|
||||
purge=dict(type='bool', default=False)
|
||||
)
|
||||
|
||||
argument_spec.update(element_spec)
|
||||
|
||||
mutually_exclusive = [('name', 'aggregate')]
|
||||
|
||||
module = AnsibleModule(argument_spec=argument_spec,
|
||||
mutually_exclusive=mutually_exclusive,
|
||||
supports_check_mode=True)
|
||||
|
||||
warnings = list()
|
||||
|
||||
result = {'changed': False}
|
||||
result['warnings'] = warnings
|
||||
|
||||
want = map_params_to_obj(module)
|
||||
have = map_config_to_obj(module)
|
||||
|
||||
commands = map_obj_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):
|
||||
if item != 'admin':
|
||||
if not item.strip():
|
||||
continue
|
||||
item = item.replace("\\", "\\\\")
|
||||
commands.append('no username %s' % item)
|
||||
|
||||
result['commands'] = commands
|
||||
|
||||
# the cnos cli prevents this by rule but still
|
||||
if 'no username admin' in commands:
|
||||
module.fail_json(msg='Cannot delete the `admin` account')
|
||||
|
||||
if commands:
|
||||
if not module.check_mode:
|
||||
load_config(module, commands)
|
||||
result['changed'] = True
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
446
plugins/modules/network/cnos/cnos_vlag.py
Normal file
446
plugins/modules/network/cnos/cnos_vlag.py
Normal file
|
|
@ -0,0 +1,446 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
#
|
||||
# Copyright (C) 2017 Lenovo, Inc.
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
# Module to send VLAG commands to Lenovo Switches
|
||||
# Lenovo Networking
|
||||
#
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: cnos_vlag
|
||||
author: "Anil Kumar Muraleedharan (@amuraleedhar)"
|
||||
short_description: Manage VLAG resources and attributes on devices running
|
||||
Lenovo CNOS
|
||||
description:
|
||||
- This module allows you to work with virtual Link Aggregation Groups
|
||||
(vLAG) related configurations. The operators used are overloaded to ensure
|
||||
control over switch vLAG configurations. Apart from the regular device
|
||||
connection related attributes, there are four vLAG arguments which are
|
||||
overloaded variables that will perform further configurations. They are
|
||||
vlagArg1, vlagArg2, vlagArg3, and vlagArg4. For more details on how to use
|
||||
these arguments, see [Overloaded Variables].
|
||||
This module uses SSH to manage network device configuration.
|
||||
The results of the operation will be placed in a directory named 'results'
|
||||
that must be created by the user in their local directory to where the
|
||||
playbook is run.
|
||||
extends_documentation_fragment:
|
||||
- community.general.cnos
|
||||
|
||||
options:
|
||||
vlagArg1:
|
||||
description:
|
||||
- This is an overloaded vlag first argument. Usage of this argument can
|
||||
be found is the User Guide referenced above.
|
||||
required: Yes
|
||||
default: Null
|
||||
choices: [enable, auto-recovery,config-consistency,isl,mac-address-table,
|
||||
peer-gateway,priority,startup-delay,tier-id,vrrp,instance,hlthchk]
|
||||
vlagArg2:
|
||||
description:
|
||||
- This is an overloaded vlag second argument. Usage of this argument can
|
||||
be found is the User Guide referenced above.
|
||||
required: No
|
||||
default: Null
|
||||
choices: [Interval in seconds,disable or strict,Port Aggregation Number,
|
||||
VLAG priority,Delay time in seconds,VLAG tier-id value,
|
||||
VLAG instance number,keepalive-attempts,keepalive-interval,
|
||||
retry-interval,peer-ip]
|
||||
vlagArg3:
|
||||
description:
|
||||
- This is an overloaded vlag third argument. Usage of this argument can
|
||||
be found is the User Guide referenced above.
|
||||
required: No
|
||||
default: Null
|
||||
choices: [enable or port-aggregation,Number of keepalive attempts,
|
||||
Interval in seconds,Interval in seconds,
|
||||
VLAG health check peer IP4 address]
|
||||
vlagArg4:
|
||||
description:
|
||||
- This is an overloaded vlag fourth argument. Usage of this argument can
|
||||
be found is the User Guide referenced above.
|
||||
required: No
|
||||
default: Null
|
||||
choices: [Port Aggregation Number,default or management]
|
||||
|
||||
'''
|
||||
EXAMPLES = '''
|
||||
|
||||
Tasks : The following are examples of using the module cnos_vlag. These are
|
||||
written in the main.yml file of the tasks directory.
|
||||
---
|
||||
- name: Test Vlag - enable
|
||||
cnos_vlag:
|
||||
deviceType: "{{ hostvars[inventory_hostname]['deviceType']}}"
|
||||
outputfile: "./results/cnos_vlag_{{ inventory_hostname }}_output.txt"
|
||||
vlagArg1: "enable"
|
||||
|
||||
- name: Test Vlag - autorecovery
|
||||
cnos_vlag:
|
||||
deviceType: "{{ hostvars[inventory_hostname]['deviceType']}}"
|
||||
outputfile: "./results/cnos_vlag_{{ inventory_hostname }}_output.txt"
|
||||
vlagArg1: "auto-recovery"
|
||||
vlagArg2: 266
|
||||
|
||||
- name: Test Vlag - config-consistency
|
||||
cnos_vlag:
|
||||
deviceType: "{{ hostvars[inventory_hostname]['deviceType']}}"
|
||||
outputfile: "./results/cnos_vlag_{{ inventory_hostname }}_output.txt"
|
||||
vlagArg1: "config-consistency"
|
||||
vlagArg2: "strict"
|
||||
|
||||
- name: Test Vlag - isl
|
||||
cnos_vlag:
|
||||
deviceType: "{{ hostvars[inventory_hostname]['deviceType']}}"
|
||||
outputfile: "./results/cnos_vlag_{{ inventory_hostname }}_output.txt"
|
||||
vlagArg1: "isl"
|
||||
vlagArg2: 23
|
||||
|
||||
- name: Test Vlag - mac-address-table
|
||||
cnos_vlag:
|
||||
deviceType: "{{ hostvars[inventory_hostname]['deviceType']}}"
|
||||
outputfile: "./results/cnos_vlag_{{ inventory_hostname }}_output.txt"
|
||||
vlagArg1: "mac-address-table"
|
||||
|
||||
- name: Test Vlag - peer-gateway
|
||||
cnos_vlag:
|
||||
deviceType: "{{ hostvars[inventory_hostname]['deviceType']}}"
|
||||
outputfile: "./results/cnos_vlag_{{ inventory_hostname }}_output.txt"
|
||||
vlagArg1: "peer-gateway"
|
||||
|
||||
- name: Test Vlag - priority
|
||||
cnos_vlag:
|
||||
deviceType: "{{ hostvars[inventory_hostname]['deviceType']}}"
|
||||
outputfile: "./results/cnos_vlag_{{ inventory_hostname }}_output.txt"
|
||||
vlagArg1: "priority"
|
||||
vlagArg2: 1313
|
||||
|
||||
- name: Test Vlag - startup-delay
|
||||
cnos_vlag:
|
||||
deviceType: "{{ hostvars[inventory_hostname]['deviceType']}}"
|
||||
outputfile: "./results/cnos_vlag_{{ inventory_hostname }}_output.txt"
|
||||
vlagArg1: "startup-delay"
|
||||
vlagArg2: 323
|
||||
|
||||
- name: Test Vlag - tier-id
|
||||
cnos_vlag:
|
||||
deviceType: "{{ hostvars[inventory_hostname]['deviceType']}}"
|
||||
outputfile: "./results/cnos_vlag_{{ inventory_hostname }}_output.txt"
|
||||
vlagArg1: "tier-id"
|
||||
vlagArg2: 313
|
||||
|
||||
- name: Test Vlag - vrrp
|
||||
cnos_vlag:
|
||||
deviceType: "{{ hostvars[inventory_hostname]['deviceType']}}"
|
||||
outputfile: "./results/cnos_vlag_{{ inventory_hostname }}_output.txt"
|
||||
vlagArg1: "vrrp"
|
||||
|
||||
- name: Test Vlag - instance
|
||||
cnos_vlag:
|
||||
deviceType: "{{ hostvars[inventory_hostname]['deviceType']}}"
|
||||
outputfile: "./results/cnos_vlag_{{ inventory_hostname }}_output.txt"
|
||||
vlagArg1: "instance"
|
||||
vlagArg2: 33
|
||||
vlagArg3: 333
|
||||
|
||||
- name: Test Vlag - instance2
|
||||
cnos_vlag:
|
||||
deviceType: "{{ hostvars[inventory_hostname]['deviceType']}}"
|
||||
outputfile: "./results/cnos_vlag_{{ inventory_hostname }}_output.txt"
|
||||
vlagArg1: "instance"
|
||||
vlagArg2: "33"
|
||||
|
||||
- name: Test Vlag - keepalive-attempts
|
||||
cnos_vlag:
|
||||
deviceType: "{{ hostvars[inventory_hostname]['deviceType']}}"
|
||||
outputfile: "./results/cnos_vlag_{{ inventory_hostname }}_output.txt"
|
||||
vlagArg1: "hlthchk"
|
||||
vlagArg2: "keepalive-attempts"
|
||||
vlagArg3: 13
|
||||
|
||||
- name: Test Vlag - keepalive-interval
|
||||
cnos_vlag:
|
||||
deviceType: "{{ hostvars[inventory_hostname]['deviceType']}}"
|
||||
outputfile: "./results/cnos_vlag_{{ inventory_hostname }}_output.txt"
|
||||
vlagArg1: "hlthchk"
|
||||
vlagArg2: "keepalive-interval"
|
||||
vlagArg3: 131
|
||||
|
||||
- name: Test Vlag - retry-interval
|
||||
cnos_vlag:
|
||||
deviceType: "{{ hostvars[inventory_hostname]['deviceType']}}"
|
||||
outputfile: "./results/cnos_vlag_{{ inventory_hostname }}_output.txt"
|
||||
vlagArg1: "hlthchk"
|
||||
vlagArg2: "retry-interval"
|
||||
vlagArg3: 133
|
||||
|
||||
- name: Test Vlag - peer ip
|
||||
cnos_vlag:
|
||||
deviceType: "{{ hostvars[inventory_hostname]['deviceType']}}"
|
||||
outputfile: "./results/cnos_vlag_{{ inventory_hostname }}_output.txt"
|
||||
vlagArg1: "hlthchk"
|
||||
vlagArg2: "peer-ip"
|
||||
vlagArg3: "1.2.3.4"
|
||||
|
||||
'''
|
||||
RETURN = '''
|
||||
msg:
|
||||
description: Success or failure message
|
||||
returned: always
|
||||
type: str
|
||||
sample: "vLAG configurations accomplished"
|
||||
'''
|
||||
|
||||
import sys
|
||||
import time
|
||||
import socket
|
||||
import array
|
||||
import json
|
||||
import time
|
||||
import re
|
||||
try:
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos import cnos
|
||||
HAS_LIB = True
|
||||
except Exception:
|
||||
HAS_LIB = False
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
def vlagConfig(module, prompt, answer):
|
||||
|
||||
retVal = ''
|
||||
# vlag config command happens here.
|
||||
command = 'vlag '
|
||||
|
||||
vlagArg1 = module.params['vlagArg1']
|
||||
vlagArg2 = module.params['vlagArg2']
|
||||
vlagArg3 = module.params['vlagArg3']
|
||||
vlagArg4 = module.params['vlagArg4']
|
||||
deviceType = module.params['deviceType']
|
||||
|
||||
if(vlagArg1 == "enable"):
|
||||
# debugOutput("enable")
|
||||
command = command + vlagArg1 + " "
|
||||
|
||||
elif(vlagArg1 == "auto-recovery"):
|
||||
# debugOutput("auto-recovery")
|
||||
command = command + vlagArg1 + " "
|
||||
value = cnos.checkSanityofVariable(
|
||||
deviceType, "vlag_auto_recovery", vlagArg2)
|
||||
if(value == "ok"):
|
||||
command = command + vlagArg2
|
||||
else:
|
||||
retVal = "Error-160"
|
||||
return retVal
|
||||
|
||||
elif(vlagArg1 == "config-consistency"):
|
||||
# debugOutput("config-consistency")
|
||||
command = command + vlagArg1 + " "
|
||||
value = cnos.checkSanityofVariable(
|
||||
deviceType, "vlag_config_consistency", vlagArg2)
|
||||
if(value == "ok"):
|
||||
command = command + vlagArg2
|
||||
else:
|
||||
retVal = "Error-161"
|
||||
return retVal
|
||||
|
||||
elif(vlagArg1 == "isl"):
|
||||
# debugOutput("isl")
|
||||
command = command + vlagArg1 + " port-channel "
|
||||
value = cnos.checkSanityofVariable(
|
||||
deviceType, "vlag_port_aggregation", vlagArg2)
|
||||
if(value == "ok"):
|
||||
command = command + vlagArg2
|
||||
else:
|
||||
retVal = "Error-162"
|
||||
return retVal
|
||||
|
||||
elif(vlagArg1 == "mac-address-table"):
|
||||
# debugOutput("mac-address-table")
|
||||
command = command + vlagArg1 + " refresh"
|
||||
|
||||
elif(vlagArg1 == "peer-gateway"):
|
||||
# debugOutput("peer-gateway")
|
||||
command = command + vlagArg1 + " "
|
||||
|
||||
elif(vlagArg1 == "priority"):
|
||||
# debugOutput("priority")
|
||||
command = command + vlagArg1 + " "
|
||||
value = cnos.checkSanityofVariable(deviceType, "vlag_priority",
|
||||
vlagArg2)
|
||||
if(value == "ok"):
|
||||
command = command + vlagArg2
|
||||
else:
|
||||
retVal = "Error-163"
|
||||
return retVal
|
||||
|
||||
elif(vlagArg1 == "startup-delay"):
|
||||
# debugOutput("startup-delay")
|
||||
command = command + vlagArg1 + " "
|
||||
value = cnos.checkSanityofVariable(
|
||||
deviceType, "vlag_startup_delay", vlagArg2)
|
||||
if(value == "ok"):
|
||||
command = command + vlagArg2
|
||||
else:
|
||||
retVal = "Error-164"
|
||||
return retVal
|
||||
|
||||
elif(vlagArg1 == "tier-id"):
|
||||
# debugOutput("tier-id")
|
||||
command = command + vlagArg1 + " "
|
||||
value = cnos.checkSanityofVariable(deviceType, "vlag_tier_id", vlagArg2)
|
||||
if(value == "ok"):
|
||||
command = command + vlagArg2
|
||||
else:
|
||||
retVal = "Error-165"
|
||||
return retVal
|
||||
|
||||
elif(vlagArg1 == "vrrp"):
|
||||
# debugOutput("vrrp")
|
||||
command = command + vlagArg1 + " active"
|
||||
|
||||
elif(vlagArg1 == "instance"):
|
||||
# debugOutput("instance")
|
||||
command = command + vlagArg1 + " "
|
||||
value = cnos.checkSanityofVariable(deviceType, "vlag_instance",
|
||||
vlagArg2)
|
||||
if(value == "ok"):
|
||||
command = command + vlagArg2
|
||||
if(vlagArg3 is not None):
|
||||
command = command + " port-channel "
|
||||
value = cnos.checkSanityofVariable(
|
||||
deviceType, "vlag_port_aggregation", vlagArg3)
|
||||
if(value == "ok"):
|
||||
command = command + vlagArg3
|
||||
else:
|
||||
retVal = "Error-162"
|
||||
return retVal
|
||||
else:
|
||||
command = command + " enable "
|
||||
else:
|
||||
retVal = "Error-166"
|
||||
return retVal
|
||||
|
||||
elif(vlagArg1 == "hlthchk"):
|
||||
# debugOutput("hlthchk")
|
||||
command = command + vlagArg1 + " "
|
||||
value = cnos.checkSanityofVariable(
|
||||
deviceType, "vlag_hlthchk_options", vlagArg2)
|
||||
if(value == "ok"):
|
||||
if(vlagArg2 == "keepalive-attempts"):
|
||||
value = cnos.checkSanityofVariable(
|
||||
deviceType, "vlag_keepalive_attempts", vlagArg3)
|
||||
if(value == "ok"):
|
||||
command = command + vlagArg2 + " " + vlagArg3
|
||||
else:
|
||||
retVal = "Error-167"
|
||||
return retVal
|
||||
elif(vlagArg2 == "keepalive-interval"):
|
||||
value = cnos.checkSanityofVariable(
|
||||
deviceType, "vlag_keepalive_interval", vlagArg3)
|
||||
if(value == "ok"):
|
||||
command = command + vlagArg2 + " " + vlagArg3
|
||||
else:
|
||||
retVal = "Error-168"
|
||||
return retVal
|
||||
elif(vlagArg2 == "retry-interval"):
|
||||
value = cnos.checkSanityofVariable(
|
||||
deviceType, "vlag_retry_interval", vlagArg3)
|
||||
if(value == "ok"):
|
||||
command = command + vlagArg2 + " " + vlagArg3
|
||||
else:
|
||||
retVal = "Error-169"
|
||||
return retVal
|
||||
elif(vlagArg2 == "peer-ip"):
|
||||
# Here I am not taking care of IPV6 option.
|
||||
value = cnos.checkSanityofVariable(
|
||||
deviceType, "vlag_peerip", vlagArg3)
|
||||
if(value == "ok"):
|
||||
command = command + vlagArg2 + " " + vlagArg3
|
||||
if(vlagArg4 is not None):
|
||||
value = cnos.checkSanityofVariable(
|
||||
deviceType, "vlag_peerip_vrf", vlagArg4)
|
||||
if(value == "ok"):
|
||||
command = command + " vrf " + vlagArg4
|
||||
else:
|
||||
retVal = "Error-170"
|
||||
return retVal
|
||||
else:
|
||||
retVal = "Error-171"
|
||||
return retVal
|
||||
|
||||
else:
|
||||
retVal = "Error-172"
|
||||
return retVal
|
||||
|
||||
# debugOutput(command)
|
||||
cmd = [{'command': command, 'prompt': None, 'answer': None}]
|
||||
retVal = retVal + str(cnos.run_cnos_commands(module, cmd))
|
||||
return retVal
|
||||
# EOM
|
||||
|
||||
|
||||
def main():
|
||||
#
|
||||
# Define parameters for vlag creation entry
|
||||
#
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
outputfile=dict(required=True),
|
||||
host=dict(required=False),
|
||||
username=dict(required=False),
|
||||
password=dict(required=False, no_log=True),
|
||||
enablePassword=dict(required=False, no_log=True),
|
||||
deviceType=dict(required=True),
|
||||
vlagArg1=dict(required=True),
|
||||
vlagArg2=dict(required=False),
|
||||
vlagArg3=dict(required=False),
|
||||
vlagArg4=dict(required=False),),
|
||||
supports_check_mode=False)
|
||||
|
||||
outputfile = module.params['outputfile']
|
||||
output = ""
|
||||
|
||||
# Send the CLi command
|
||||
output = output + str(vlagConfig(module, '(config)#', None))
|
||||
|
||||
# Save it into the file
|
||||
file = open(outputfile, "a")
|
||||
file.write(output)
|
||||
file.close()
|
||||
|
||||
# need to add logic to check when changes occur or not
|
||||
errorMsg = cnos.checkOutputForError(output)
|
||||
if(errorMsg is None):
|
||||
module.exit_json(changed=True, msg="VLAG configurations accomplished")
|
||||
else:
|
||||
module.fail_json(msg=errorMsg)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
409
plugins/modules/network/cnos/cnos_vlan.py
Normal file
409
plugins/modules/network/cnos/cnos_vlan.py
Normal file
|
|
@ -0,0 +1,409 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (C) 2017 Lenovo, Inc.
|
||||
# (c) 2017, Ansible by Red Hat, inc
|
||||
# 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/>.
|
||||
#
|
||||
# Module to send VLAN commands to Lenovo Switches
|
||||
# Overloading aspect of vlan creation in a range is pending
|
||||
# Lenovo Networking
|
||||
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: cnos_vlan
|
||||
author: "Anil Kumar Mureleedharan(@amuraleedhar)"
|
||||
short_description: Manage VLANs on CNOS network devices
|
||||
description:
|
||||
- This module provides declarative management of VLANs
|
||||
on Lenovo CNOS network devices.
|
||||
notes:
|
||||
- Tested against CNOS 10.8.1
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name of the VLAN.
|
||||
vlan_id:
|
||||
description:
|
||||
- ID of the VLAN. Range 1-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 interfaces on device it will result in failure.
|
||||
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', 'active', 'suspend']
|
||||
provider:
|
||||
description:
|
||||
- B(Deprecated)
|
||||
- "Starting with Ansible 2.5 we recommend using C(connection: network_cli)."
|
||||
- For more information please see the L(CNOS Platform Options guide, ../network/user_guide/platform_cnos.html).
|
||||
- HORIZONTALLINE
|
||||
- A dict object containing connection details.
|
||||
suboptions:
|
||||
host:
|
||||
description:
|
||||
- Specifies the DNS host name or address for connecting to the remote
|
||||
device over the specified transport. The value of host is used as
|
||||
the destination address for the transport.
|
||||
required: true
|
||||
port:
|
||||
description:
|
||||
- Specifies the port to use when building the connection to the remote device.
|
||||
default: 22
|
||||
username:
|
||||
description:
|
||||
- Configures the username to use to authenticate the connection to
|
||||
the remote device. This value is used to authenticate
|
||||
the SSH session. If the value is not specified in the task, the
|
||||
value of environment variable C(ANSIBLE_NET_USERNAME) will be used instead.
|
||||
password:
|
||||
description:
|
||||
- Specifies the password to use to authenticate the connection to
|
||||
the remote device. This value is used to authenticate
|
||||
the SSH session. If the value is not specified in the task, the
|
||||
value of environment variable C(ANSIBLE_NET_PASSWORD) will be used instead.
|
||||
timeout:
|
||||
description:
|
||||
- Specifies the timeout in seconds for communicating with the network device
|
||||
for either connecting or sending commands. If the timeout is
|
||||
exceeded before the operation is completed, the module will error.
|
||||
default: 10
|
||||
ssh_keyfile:
|
||||
description:
|
||||
- Specifies the SSH key to use to authenticate the connection to
|
||||
the remote device. This value is the path to the
|
||||
key used to authenticate the SSH session. If the value is not specified
|
||||
in the task, the value of environment variable C(ANSIBLE_NET_SSH_KEYFILE)
|
||||
will be used instead.
|
||||
authorize:
|
||||
description:
|
||||
- Instructs the module to enter privileged mode on the remote device
|
||||
before sending any commands. If not specified, the device will
|
||||
attempt to execute all commands in non-privileged mode. If the value
|
||||
is not specified in the task, the value of environment variable
|
||||
C(ANSIBLE_NET_AUTHORIZE) will be used instead.
|
||||
type: bool
|
||||
default: 'no'
|
||||
auth_pass:
|
||||
description:
|
||||
- Specifies the password to use if required to enter privileged mode
|
||||
on the remote device. If I(authorize) is false, then this argument
|
||||
does nothing. If the value is not specified in the task, the value of
|
||||
environment variable C(ANSIBLE_NET_AUTH_PASS) will be used instead.
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Create vlan
|
||||
cnos_vlan:
|
||||
vlan_id: 100
|
||||
name: test-vlan
|
||||
state: present
|
||||
|
||||
- name: Add interfaces to VLAN
|
||||
cnos_vlan:
|
||||
vlan_id: 100
|
||||
interfaces:
|
||||
- Ethernet1/33
|
||||
- Ethernet1/44
|
||||
|
||||
- name: Check if interfaces is assigned to VLAN
|
||||
cnos_vlan:
|
||||
vlan_id: 100
|
||||
associated_interfaces:
|
||||
- Ethernet1/33
|
||||
- Ethernet1/44
|
||||
|
||||
- name: Delete vlan
|
||||
cnos_vlan:
|
||||
vlan_id: 100
|
||||
state: absent
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
commands:
|
||||
description: The list of configuration mode commands to send to the device
|
||||
returned: always
|
||||
type: list
|
||||
sample:
|
||||
- vlan 100
|
||||
- name test-vlan
|
||||
"""
|
||||
|
||||
import re
|
||||
import time
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import remove_default_spec
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import load_config, run_commands
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import debugOutput, check_args
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import cnos_argument_spec
|
||||
from ansible.module_utils._text import to_text
|
||||
|
||||
|
||||
def search_obj_in_list(vlan_id, lst):
|
||||
obj = list()
|
||||
for o in lst:
|
||||
if o['vlan_id'] == vlan_id:
|
||||
return o
|
||||
|
||||
|
||||
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']
|
||||
interfaces = w['interfaces']
|
||||
state = w['state']
|
||||
|
||||
obj_in_have = search_obj_in_list(vlan_id, have)
|
||||
|
||||
if state == 'absent':
|
||||
if obj_in_have:
|
||||
commands.append('no vlan {0}'.format(vlan_id))
|
||||
|
||||
elif state == 'present':
|
||||
if not obj_in_have:
|
||||
commands.append('vlan {0}'.format(vlan_id))
|
||||
if name:
|
||||
commands.append('name {0}'.format(name))
|
||||
|
||||
if interfaces:
|
||||
for i in interfaces:
|
||||
commands.append('interface {0}'.format(i))
|
||||
commands.append('switchport mode access')
|
||||
commands.append('switchport access vlan {0}'.format(vlan_id))
|
||||
|
||||
else:
|
||||
if name:
|
||||
if name != obj_in_have['name']:
|
||||
commands.append('vlan {0}'.format(vlan_id))
|
||||
commands.append('name {0}'.format(name))
|
||||
|
||||
if interfaces:
|
||||
if not obj_in_have['interfaces']:
|
||||
for i in interfaces:
|
||||
commands.append('vlan {0}'.format(vlan_id))
|
||||
commands.append('interface {0}'.format(i))
|
||||
commands.append('switchport mode access')
|
||||
commands.append('switchport access vlan {0}'.format(vlan_id))
|
||||
|
||||
elif set(interfaces) != set(obj_in_have['interfaces']):
|
||||
missing_interfaces = list(set(interfaces) - set(obj_in_have['interfaces']))
|
||||
for i in missing_interfaces:
|
||||
commands.append('vlan {0}'.format(vlan_id))
|
||||
commands.append('interface {0}'.format(i))
|
||||
commands.append('switchport mode access')
|
||||
commands.append('switchport access vlan {0}'.format(vlan_id))
|
||||
|
||||
superfluous_interfaces = list(set(obj_in_have['interfaces']) - set(interfaces))
|
||||
for i in superfluous_interfaces:
|
||||
commands.append('vlan {0}'.format(vlan_id))
|
||||
commands.append('interface {0}'.format(i))
|
||||
commands.append('switchport mode access')
|
||||
commands.append('no switchport access vlan')
|
||||
else:
|
||||
commands.append('vlan {0}'.format(vlan_id))
|
||||
if name:
|
||||
commands.append('name {0}'.format(name))
|
||||
commands.append('state {0}'.format(state))
|
||||
|
||||
if purge:
|
||||
for h in have:
|
||||
obj_in_want = search_obj_in_list(h['vlan_id'], want)
|
||||
if not obj_in_want and h['vlan_id'] != '1':
|
||||
commands.append('no vlan {0}'.format(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()
|
||||
d['vlan_id'] = str(d['vlan_id'])
|
||||
|
||||
obj.append(d)
|
||||
else:
|
||||
obj.append({
|
||||
'vlan_id': str(module.params['vlan_id']),
|
||||
'name': module.params['name'],
|
||||
'interfaces': module.params['interfaces'],
|
||||
# 'associated_interfaces': module.params['associated_interfaces'],
|
||||
'state': module.params['state']
|
||||
})
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
def parse_to_logical_rows(out):
|
||||
relevant_data = False
|
||||
cur_row = []
|
||||
for line in out.splitlines():
|
||||
if not line:
|
||||
"""Skip empty lines."""
|
||||
continue
|
||||
if '0' < line[0] <= '9':
|
||||
"""Line starting with a number."""
|
||||
if len(cur_row) > 0:
|
||||
yield cur_row
|
||||
cur_row = [] # Reset it to hold a next chunk
|
||||
relevant_data = True
|
||||
if relevant_data:
|
||||
data = line.strip().split('(')
|
||||
cur_row.append(data[0])
|
||||
yield cur_row
|
||||
|
||||
|
||||
def parse_to_obj(logical_rows):
|
||||
first_row = logical_rows[0]
|
||||
rest_rows = logical_rows[1:]
|
||||
vlan_data = first_row.split()
|
||||
obj = {}
|
||||
obj['vlan_id'] = vlan_data[0]
|
||||
obj['name'] = vlan_data[1]
|
||||
obj['state'] = vlan_data[2]
|
||||
obj['interfaces'] = rest_rows
|
||||
return obj
|
||||
|
||||
|
||||
def parse_vlan_brief(vlan_out):
|
||||
return [parse_to_obj(r) for r in parse_to_logical_rows(vlan_out)]
|
||||
|
||||
|
||||
def map_config_to_obj(module):
|
||||
return parse_vlan_brief(run_commands(module, ['show vlan brief'])[0])
|
||||
|
||||
|
||||
def check_declarative_intent_params(want, module, result):
|
||||
|
||||
have = None
|
||||
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)
|
||||
|
||||
for i in w['associated_interfaces']:
|
||||
obj_in_have = search_obj_in_list(w['vlan_id'], have)
|
||||
if obj_in_have and 'interfaces' in obj_in_have and i not in obj_in_have['interfaces']:
|
||||
module.fail_json(msg="Interface %s not configured on vlan %s" % (i, w['vlan_id']))
|
||||
|
||||
|
||||
def main():
|
||||
""" main entry point for module execution
|
||||
"""
|
||||
element_spec = dict(
|
||||
vlan_id=dict(type='int'),
|
||||
name=dict(),
|
||||
interfaces=dict(type='list'),
|
||||
associated_interfaces=dict(type='list'),
|
||||
delay=dict(default=10, type='int'),
|
||||
state=dict(default='present',
|
||||
choices=['present', 'absent', 'active', 'suspend'])
|
||||
)
|
||||
|
||||
aggregate_spec = deepcopy(element_spec)
|
||||
aggregate_spec['vlan_id'] = 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),
|
||||
purge=dict(default=False, type='bool')
|
||||
)
|
||||
|
||||
argument_spec.update(element_spec)
|
||||
argument_spec.update(cnos_argument_spec)
|
||||
|
||||
required_one_of = [['vlan_id', 'aggregate']]
|
||||
mutually_exclusive = [['vlan_id', '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:
|
||||
if not module.check_mode:
|
||||
load_config(module, commands)
|
||||
result['changed'] = True
|
||||
|
||||
check_declarative_intent_params(want, module, result)
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
369
plugins/modules/network/cnos/cnos_vrf.py
Normal file
369
plugins/modules/network/cnos/cnos_vrf.py
Normal file
|
|
@ -0,0 +1,369 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
#
|
||||
# Copyright (C) 2019 Lenovo.
|
||||
# (c) 2017, Ansible by Red Hat, inc
|
||||
# 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/>.
|
||||
#
|
||||
# Module to work on management of local users on Lenovo CNOS Switches
|
||||
# Lenovo Networking
|
||||
#
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: cnos_vrf
|
||||
author: "Anil Kumar Muraleedharan (@amuraleedhar)"
|
||||
short_description: Manage VRFs on Lenovo CNOS network devices
|
||||
description:
|
||||
- This module provides declarative management of VRFs
|
||||
on Lenovo CNOS network devices.
|
||||
notes:
|
||||
- Tested against CNOS 10.9.1
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name of the VRF.
|
||||
required: true
|
||||
rd:
|
||||
description:
|
||||
- Route distinguisher of the VRF
|
||||
interfaces:
|
||||
description:
|
||||
- Identifies the set of interfaces that
|
||||
should be configured in the VRF. Interfaces must be routed
|
||||
interfaces in order to be placed into a VRF. The name of interface
|
||||
should be in expanded format and not abbreviated.
|
||||
associated_interfaces:
|
||||
description:
|
||||
- This is a intent option and checks the operational state of the for
|
||||
given vrf C(name) for associated interfaces. If the value in the
|
||||
C(associated_interfaces) does not match with the operational state of
|
||||
vrf interfaces on device it will result in failure.
|
||||
aggregate:
|
||||
description: List of VRFs contexts
|
||||
purge:
|
||||
description:
|
||||
- Purge VRFs not defined in the I(aggregate) parameter.
|
||||
default: no
|
||||
type: bool
|
||||
delay:
|
||||
description:
|
||||
- Time in seconds to wait before checking for the operational state on
|
||||
remote device. This wait is applicable for operational state arguments.
|
||||
default: 10
|
||||
state:
|
||||
description:
|
||||
- State of the VRF configuration.
|
||||
default: present
|
||||
choices: ['present', 'absent']
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Create vrf
|
||||
cnos_vrf:
|
||||
name: test
|
||||
rd: 1:200
|
||||
interfaces:
|
||||
- Ethernet1/33
|
||||
state: present
|
||||
|
||||
- name: Delete VRFs
|
||||
cnos_vrf:
|
||||
name: test
|
||||
state: absent
|
||||
|
||||
- name: Create aggregate of VRFs with purge
|
||||
cnos_vrf:
|
||||
aggregate:
|
||||
- { name: test4, rd: "1:204" }
|
||||
- { name: test5, rd: "1:205" }
|
||||
state: present
|
||||
purge: yes
|
||||
|
||||
- name: Delete aggregate of VRFs
|
||||
cnos_vrf:
|
||||
aggregate:
|
||||
- name: test2
|
||||
- name: test3
|
||||
- name: test4
|
||||
- name: test5
|
||||
state: absent
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
commands:
|
||||
description: The list of configuration mode commands to send to the device
|
||||
returned: always
|
||||
type: list
|
||||
sample:
|
||||
- vrf context test
|
||||
- rd 1:100
|
||||
- interface Ethernet1/44
|
||||
- vrf member test
|
||||
"""
|
||||
import re
|
||||
import time
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import remove_default_spec
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import load_config, run_commands
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos.cnos import cnos_argument_spec, check_args
|
||||
|
||||
|
||||
def search_obj_in_list(name, lst):
|
||||
for o in lst:
|
||||
if o['name'] == name:
|
||||
return o
|
||||
|
||||
|
||||
def get_interface_type(interface):
|
||||
intf_type = 'unknown'
|
||||
if interface.upper()[:2] in ('ET', 'GI', 'FA', 'TE', 'FO', 'HU', 'TWE'):
|
||||
intf_type = 'ethernet'
|
||||
elif interface.upper().startswith('VL'):
|
||||
intf_type = 'svi'
|
||||
elif interface.upper().startswith('LO'):
|
||||
intf_type = 'loopback'
|
||||
elif interface.upper()[:2] in ('MG', 'MA'):
|
||||
intf_type = 'management'
|
||||
elif interface.upper().startswith('PO'):
|
||||
intf_type = 'portchannel'
|
||||
elif interface.upper().startswith('NV'):
|
||||
intf_type = 'nve'
|
||||
|
||||
return intf_type
|
||||
|
||||
|
||||
def is_switchport(name, module):
|
||||
intf_type = get_interface_type(name)
|
||||
|
||||
if intf_type in ('ethernet', 'portchannel'):
|
||||
config = run_commands(module,
|
||||
['show interface {0} switchport'.format(name)])[0]
|
||||
match = re.search(r'Switchport : enabled', config)
|
||||
return bool(match)
|
||||
return False
|
||||
|
||||
|
||||
def map_obj_to_commands(updates, module):
|
||||
commands = list()
|
||||
want, have = updates
|
||||
state = module.params['state']
|
||||
purge = module.params['purge']
|
||||
|
||||
for w in want:
|
||||
name = w['name']
|
||||
rd = w['rd']
|
||||
interfaces = w['interfaces']
|
||||
|
||||
obj_in_have = search_obj_in_list(name, have)
|
||||
|
||||
if name == 'default':
|
||||
module.fail_json(msg='VRF context default is reserved')
|
||||
elif len(name) > 63:
|
||||
module.fail_json(msg='VRF name is too long')
|
||||
if state == 'absent':
|
||||
if name == 'management':
|
||||
module.fail_json(msg='Management VRF context cannot be deleted')
|
||||
if obj_in_have:
|
||||
commands.append('no vrf context %s' % name)
|
||||
elif state == 'present':
|
||||
if not obj_in_have:
|
||||
commands.append('vrf context %s' % name)
|
||||
|
||||
if rd is not None:
|
||||
commands.append('rd %s' % rd)
|
||||
|
||||
if w['interfaces']:
|
||||
for i in w['interfaces']:
|
||||
commands.append('interface %s' % i)
|
||||
commands.append('vrf member %s' % w['name'])
|
||||
else:
|
||||
if w['rd'] is not None and w['rd'] != obj_in_have['rd']:
|
||||
commands.append('vrf context %s' % w['name'])
|
||||
commands.append('rd %s' % w['rd'])
|
||||
|
||||
if w['interfaces']:
|
||||
if not obj_in_have['interfaces']:
|
||||
for i in w['interfaces']:
|
||||
commands.append('interface %s' % i)
|
||||
commands.append('vrf member %s' % w['name'])
|
||||
elif set(w['interfaces']) != obj_in_have['interfaces']:
|
||||
missing_interfaces = list(set(w['interfaces']) - set(obj_in_have['interfaces']))
|
||||
|
||||
for i in missing_interfaces:
|
||||
commands.append('interface %s' % i)
|
||||
commands.append('vrf member %s' % w['name'])
|
||||
|
||||
if purge:
|
||||
for h in have:
|
||||
obj_in_want = search_obj_in_list(h['name'], want)
|
||||
if not obj_in_want:
|
||||
commands.append('no vrf context %s' % h['name'])
|
||||
|
||||
return commands
|
||||
|
||||
|
||||
def map_config_to_obj(module):
|
||||
objs = []
|
||||
output = run_commands(module, {'command': 'show vrf'})
|
||||
if output is not None:
|
||||
vrfText = output[0].strip()
|
||||
vrfList = vrfText.split('VRF')
|
||||
for vrfItem in vrfList:
|
||||
if 'FIB ID' in vrfItem:
|
||||
obj = dict()
|
||||
list_of_words = vrfItem.split()
|
||||
vrfName = list_of_words[0]
|
||||
obj['name'] = vrfName[:-1]
|
||||
obj['rd'] = list_of_words[list_of_words.index('RD') + 1]
|
||||
start = False
|
||||
obj['interfaces'] = []
|
||||
for intName in list_of_words:
|
||||
if 'Interfaces' in intName:
|
||||
start = True
|
||||
if start is True:
|
||||
if '!' not in intName and 'Interfaces' not in intName:
|
||||
obj['interfaces'].append(intName.strip().lower())
|
||||
objs.append(obj)
|
||||
else:
|
||||
module.fail_json(msg='Could not fetch VRF details from device')
|
||||
return objs
|
||||
|
||||
|
||||
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]
|
||||
|
||||
if item.get('interfaces'):
|
||||
item['interfaces'] = [intf.replace(" ", "").lower() for intf in item.get('interfaces') if intf]
|
||||
|
||||
if item.get('associated_interfaces'):
|
||||
item['associated_interfaces'] = [intf.replace(" ", "").lower() for intf in item.get('associated_interfaces') if intf]
|
||||
|
||||
obj.append(item.copy())
|
||||
else:
|
||||
obj.append({
|
||||
'name': module.params['name'],
|
||||
'state': module.params['state'],
|
||||
'rd': module.params['rd'],
|
||||
'interfaces': [intf.replace(" ", "").lower() for intf in module.params['interfaces']] if module.params['interfaces'] else [],
|
||||
'associated_interfaces': [intf.replace(" ", "").lower() for intf in
|
||||
module.params['associated_interfaces']] if module.params['associated_interfaces'] else []
|
||||
|
||||
})
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
def check_declarative_intent_params(want, module, result):
|
||||
have = None
|
||||
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)
|
||||
|
||||
for i in w['associated_interfaces']:
|
||||
obj_in_have = search_obj_in_list(w['name'], have)
|
||||
|
||||
if obj_in_have:
|
||||
interfaces = obj_in_have.get('interfaces')
|
||||
if interfaces is not None and i not in interfaces:
|
||||
module.fail_json(msg="Interface %s not configured on vrf %s" % (i, w['name']))
|
||||
|
||||
|
||||
def main():
|
||||
""" main entry point for module execution
|
||||
"""
|
||||
element_spec = dict(
|
||||
name=dict(),
|
||||
interfaces=dict(type='list'),
|
||||
associated_interfaces=dict(type='list'),
|
||||
delay=dict(default=10, type='int'),
|
||||
rd=dict(),
|
||||
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)
|
||||
|
||||
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()
|
||||
check_args(module, warnings)
|
||||
|
||||
result = {'changed': False}
|
||||
|
||||
if warnings:
|
||||
result['warnings'] = warnings
|
||||
|
||||
want = map_params_to_obj(module)
|
||||
for w in want:
|
||||
name = w['name']
|
||||
name = name.lower()
|
||||
if is_switchport(name, module):
|
||||
module.fail_json(msg='Ensure interface is configured to be a L3'
|
||||
'\nport first before using this module. You can use'
|
||||
'\nthe cnos_interface module for this.')
|
||||
have = map_config_to_obj(module)
|
||||
|
||||
commands = map_obj_to_commands((want, have), module)
|
||||
result['commands'] = commands
|
||||
|
||||
if commands:
|
||||
if not module.check_mode:
|
||||
load_config(module, commands)
|
||||
result['changed'] = True
|
||||
check_declarative_intent_params(want, module, result)
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue