Resolving differences in core modules post-merge

This commit is contained in:
James Cammarata 2016-12-07 21:33:38 -05:00 committed by Matt Clay
commit 8afa090417
115 changed files with 2651 additions and 17913 deletions

View file

@ -86,6 +86,7 @@ options:
required: false
choices: [ "yes", "no" ]
default: "no"
version_added: '2.1'
sha256sum:
description:
- If a SHA-256 checksum is passed to this parameter, the digest of the

View file

@ -66,6 +66,7 @@ options:
- The serialization format of the body. When set to json, encodes the
body argument, if needed, and automatically sets the Content-Type header accordingly.
required: false
choices: [ "raw", "json" ]
default: raw
version_added: "2.0"
method:

View file

@ -72,14 +72,11 @@ options:
default: null
zeroize:
description:
- The C(zeroize) argument is used to completely santaize the
- The C(zeroize) argument is used to completely sanitize the
remote device configuration back to initial defaults. This
argument will effectively remove all current configuration
statements on the remote device.
required: false
choices:
- yes
- no
default: null
confirm:
description:

View file

@ -32,14 +32,14 @@ author:
- Jason Edelman (@jedelman8)
- Gabriele Gerbino (@GGabriele)
notes:
- I(state)=absent removes the ACE if it exists
- I(state)=delete_acl deleted the ACL if it exists
- for idempotency, use port numbers for the src/dest port
- C(state=absent) removes the ACE if it exists.
- C(state=delete_acl) deleted the ACL if it exists.
- For idempotency, use port numbers for the src/dest port
params like I(src_port1) and names for the well defined protocols
for the I(proto) param.
- while this module is idempotent in that if the ace as presented in the
task is identical to the one on the switch, no changes will be made. If
there is any difference, what is in Ansible will be pushed (configured
- Although this module is idempotent in that if the ace as presented in
the task is identical to the one on the switch, no changes will be made.
If there is any difference, what is in Ansible will be pushed (configured
options will be overridden). This is to improve security, but at the
same time remember an ACE is removed, then re-added, so if there is a
change, the new ACE will be exactly what parameters you are sending to
@ -47,151 +47,152 @@ notes:
options:
seq:
description:
- sequence number of the entry (ACE)
- Sequence number of the entry (ACE).
required: false
default: null
name:
description:
- Case sensitive name of the access list (ACL)
- Case sensitive name of the access list (ACL).
required: true
action:
description:
- action of the ACE
- Action of the ACE.
required: false
default: null
choices: ['permit', 'deny', 'remark']
remark:
description:
- If action is set to remark, this is the description
- If action is set to remark, this is the description.
required: false
default: null
proto:
description:
- port number or protocol (as supported by the switch)
- Port number or protocol (as supported by the switch).
required: false
default: null
src:
description:
- src ip and mask using IP/MASK notation and supports keyword 'any'
- Source ip and mask using IP/MASK notation and
supports keyword 'any'.
required: false
default: null
src_port_op:
description:
- src port operands such as eq, neq, gt, lt, range
- Source port operands such as eq, neq, gt, lt, range.
required: false
default: null
choices: ['any', 'eq', 'gt', 'lt', 'neq', 'range']
src_port1:
description:
- port/protocol and also first (lower) port when using range
operand
- Port/protocol and also first (lower) port when using range
operand.
required: false
default: null
src_port2:
description:
- second (end) port when using range operand
- Second (end) port when using range operand.
required: false
default: null
dest:
description:
- dest ip and mask using IP/MASK notation and supports the
keyword 'any'
- Destination ip and mask using IP/MASK notation and supports the
keyword 'any'.
required: false
default: null
dest_port_op:
description:
- dest port operands such as eq, neq, gt, lt, range
- Destination port operands such as eq, neq, gt, lt, range.
required: false
default: null
choices: ['any', 'eq', 'gt', 'lt', 'neq', 'range']
dest_port1:
description:
- port/protocol and also first (lower) port when using range
operand
- Port/protocol and also first (lower) port when using range
operand.
required: false
default: null
dest_port2:
description:
- second (end) port when using range operand
- Second (end) port when using range operand.
required: false
default: null
log:
description:
- Log matches against this entry
- Log matches against this entry.
required: false
default: null
choices: ['enable']
urg:
description:
- Match on the URG bit
- Match on the URG bit.
required: false
default: null
choices: ['enable']
ack:
description:
- Match on the ACK bit
- Match on the ACK bit.
required: false
default: null
choices: ['enable']
psh:
description:
- Match on the PSH bit
- Match on the PSH bit.
required: false
default: null
choices: ['enable']
rst:
description:
- Match on the RST bit
- Match on the RST bit.
required: false
default: null
choices: ['enable']
syn:
description:
- Match on the SYN bit
- Match on the SYN bit.
required: false
default: null
choices: ['enable']
fin:
description:
- Match on the FIN bit
- Match on the FIN bit.
required: false
default: null
choices: ['enable']
established:
description:
- Match established connections
- Match established connections.
required: false
default: null
choices: ['enable']
fragments:
description:
- Check non-initial fragments
- Check non-initial fragments.
required: false
default: null
choices: ['enable']
time-range:
description:
- Name of time-range to apply
- Name of time-range to apply.
required: false
default: null
precedence:
description:
- Match packets with given precedence
- Match packets with given precedence.
required: false
default: null
choices: ['critical', 'flash', 'flash-override', 'immediate',
'internet', 'network', 'priority', 'routine']
dscp:
description:
- Match packets with given dscp value
- Match packets with given dscp value.
required: false
default: null
choices: ['af11, 'af12, 'af13, 'af21', 'af22', 'af23','af31','af32',
choices: ['af11', 'af12', 'af13', 'af21', 'af22', 'af23','af31','af32',
'af33', 'af41', 'af42', 'af43', 'cs1', 'cs2', 'cs3', 'cs4',
'cs5', 'cs6', 'cs7', 'default', 'ef']
state:
description:
- Specify desired state of the resource
- Specify desired state of the resource.
required: false
default: present
choices: ['present','absent','delete_acl']
@ -220,6 +221,7 @@ proposed:
"proto": "tcp", "seq": "10", "src": "1.1.1.1/24"}
existing:
description: k/v pairs of existing ACL entries.
returned: always
type: dict
sample: {}
end_state:
@ -240,217 +242,32 @@ changed:
sample: true
'''
# COMMON CODE FOR MIGRATION
import re
import time
import collections
import itertools
import shlex
import json
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
from ansible.module_utils.netcfg import parse
from ansible.module_utils.urls import fetch_url
# COMMON CODE FOR MIGRATION
import re
from ansible.module_utils.basic import get_exception
from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import ShellError
try:
from ansible.module_utils.nxos import get_module
except ImportError:
from ansible.module_utils.nxos import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!']
class ConfigLine(object):
def __init__(self, text):
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
return list()
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
class CustomNetworkConfig(NetworkConfig):
def expand_section(self, configobj, S=None):
if S is None:
@ -462,14 +279,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S)
return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path):
for item in self.items:
if item.text == path[-1]:
@ -477,93 +286,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]:
return item
def get_children(self, path):
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try:
if item != current[index]:
updates.append(item)
except IndexError:
updates.append(item)
elif match == 'exact':
if path:
current = other.get_children(path) or list()
else:
current = other.items
if len(current) != len(config):
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def add(self, lines, parents=None):
@ -615,303 +354,44 @@ class CustomNetworkConfig(object):
self.items.append(item)
def argument_spec():
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
def get_network_module(**kwargs):
try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
errors_re=CLI_ERRORS_RE)
self.shell.open(host, port=port, username=username,
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
return get_module(**kwargs)
except NameError:
return NetworkModule(**kwargs)
def send(self, commands, encoding='text'):
try:
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
def get_config(module, include_defaults=False):
config = module.params['config']
if not config:
cmd = 'show running-config'
if module.params['include_defaults']:
cmd += ' all'
if module.params['transport'] == 'nxapi':
config = module.execute([cmd], command_type='cli_show_ascii')[0]
else:
config = module.execute([cmd])[0]
try:
config = module.get_config()
except AttributeError:
defaults = module.params['include_defaults']
config = module.config.get_config(include_defaults=defaults)
return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate):
config = custom_get_config(module)
config = get_config(module)
commands = candidate.difference(config)
commands = [str(c).strip() for c in commands]
save_config = module.params['save_config']
save_config = module.params['save']
result = dict(changed=False)
if commands:
if not module.check_mode:
try:
module.configure(commands)
except AttributeError:
module.config(commands)
if save_config:
try:
module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True
result['updates'] = commands
@ -919,6 +399,7 @@ def load_config(module, candidate):
return result
# END OF COMMON CODE
def get_cli_body_ssh(command, response, module):
"""Get response for when transport=cli. This is kind of a hack and mainly
needed because these modules were originally written for NX-API. And
@ -941,6 +422,11 @@ def get_cli_body_ssh(command, response, module):
def execute_show(cmds, module, command_type=None):
command_type_map = {
'cli_show': 'json',
'cli_show_ascii': 'text'
}
try:
if command_type:
response = module.execute(cmds, command_type=command_type)
@ -1195,9 +681,11 @@ def main():
host=dict(required=True),
username=dict(type='str'),
password=dict(no_log=True, type='str'),
include_defaults=dict(default=False),
config=dict(),
save=dict(type='bool', default=False)
)
argument_spec.update(nxos_argument_spec)
module = get_module(argument_spec=argument_spec,
module = get_network_module(argument_spec=argument_spec,
supports_check_mode=True)
state = module.params['state']

View file

@ -88,11 +88,6 @@ acl_applied_to:
type: list
sample: [{"acl_type": "Router ACL", "direction": "egress",
"interface": "Ethernet1/41", "name": "ANSIBLE"}]
state:
description: state as sent in from the playbook
returned: always
type: string
sample: "present"
updates:
description: commands sent to the device
returned: always
@ -105,211 +100,32 @@ changed:
sample: true
'''
# COMMON CODE FOR MIGRATION
import re
import time
import collections
import itertools
import shlex
import itertools
import json
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE
# COMMON CODE FOR MIGRATION
import re
DEFAULT_COMMENT_TOKENS = ['#', '!']
from ansible.module_utils.basic import get_exception
from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import ShellError
class ConfigLine(object):
try:
from ansible.module_utils.nxos import get_module
except ImportError:
from ansible.module_utils.nxos import NetworkModule
def __init__(self, text):
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
return list()
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
class CustomNetworkConfig(NetworkConfig):
def expand_section(self, configobj, S=None):
if S is None:
@ -321,14 +137,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S)
return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path):
for item in self.items:
if item.text == path[-1]:
@ -336,93 +144,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]:
return item
def get_children(self, path):
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try:
if item != current[index]:
updates.append(item)
except IndexError:
updates.append(item)
elif match == 'exact':
if path:
current = other.get_children(path) or list()
else:
current = other.items
if len(current) != len(config):
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def add(self, lines, parents=None):
@ -474,18 +212,20 @@ class CustomNetworkConfig(object):
self.items.append(item)
def argument_spec():
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
def get_network_module(**kwargs):
try:
return get_module(**kwargs)
except NameError:
return NetworkModule(**kwargs)
def get_config(module):
config = module.params['running_config']
def get_config(module, include_defaults=False):
config = module.params['config']
if not config:
try:
config = module.get_config()
except AttributeError:
defaults = module.params['include_defaults']
config = module.config.get_config(include_defaults=defaults)
return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate):
@ -494,15 +234,22 @@ def load_config(module, candidate):
commands = candidate.difference(config)
commands = [str(c).strip() for c in commands]
save_config = module.params['save_config']
save_config = module.params['save']
result = dict(changed=False)
if commands:
if not module.check_mode:
try:
module.configure(commands)
except AttributeError:
module.config(commands)
if save_config:
try:
module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True
result['updates'] = commands
@ -511,6 +258,7 @@ def load_config(module, candidate):
# END OF COMMON CODE
def get_cli_body_ssh(command, response, module):
"""Get response for when transport=cli. This is kind of a hack and mainly
needed because these modules were originally written for NX-API. And
@ -533,6 +281,11 @@ def get_cli_body_ssh(command, response, module):
def execute_show(cmds, module, command_type=None):
command_type_map = {
'cli_show': 'json',
'cli_show_ascii': 'text'
}
try:
if command_type:
response = module.execute(cmds, command_type=command_type)
@ -542,6 +295,19 @@ def execute_show(cmds, module, command_type=None):
clie = get_exception()
module.fail_json(msg='Error sending {0}'.format(cmds),
error=str(clie))
except AttributeError:
try:
if command_type:
command_type = command_type_map.get(command_type)
module.cli.add_commands(cmds, output=command_type)
response = module.cli.run_commands()
else:
module.cli.add_commands(cmds, output=command_type)
response = module.cli.run_commands()
except ShellError:
clie = get_exception()
module.fail_json(msg='Error sending {0}'.format(cmds),
error=str(clie))
return response
@ -705,9 +471,11 @@ def main():
direction=dict(required=True, choices=['egress', 'ingress']),
state=dict(choices=['absent', 'present'],
default='present'),
include_defaults=dict(default=True),
config=dict(),
save=dict(type='bool', default=False)
)
argument_spec.update(nxos_argument_spec)
module = get_module(argument_spec=argument_spec,
module = get_network_module(argument_spec=argument_spec,
supports_check_mode=True)
state = module.params['state']
@ -763,7 +531,6 @@ def main():
results = {}
results['proposed'] = proposed
results['existing'] = existing
results['state'] = state
results['updates'] = cmds
results['changed'] = changed
results['end_state'] = end_state
@ -771,10 +538,6 @@ def main():
module.exit_json(**results)
from ansible.module_utils.basic import *
from ansible.module_utils.urls import *
from ansible.module_utils.shell import *
from ansible.module_utils.netcfg import *
from ansible.module_utils.nxos import *
if __name__ == '__main__':
main()

File diff suppressed because it is too large Load diff

View file

@ -24,14 +24,14 @@ DOCUMENTATION = '''
---
module: nxos_bgp_af
version_added: "2.2"
short_description: Manages BGP Address-family configuration
short_description: Manages BGP Address-family configuration.
description:
- Manages BGP Address-family configurations on NX-OS switches
- Manages BGP Address-family configurations on NX-OS switches.
author: Gabriele Gerbino (@GGabriele)
extends_documentation_fragment: nxos
notes:
- State 'absent' removes the whole BGP ASN configuration
- default, where supported, restores params default value
- C(state=absent) removes the whole BGP ASN configuration
- Default, where supported, restores params default value.
options:
asn:
description:
@ -45,7 +45,7 @@ options:
required: true
afi:
description:
- Address Family Identifie.
- Address Family Identifier.
required: true
choices: ['ipv4','ipv6', 'vpnv4', 'vpnv6', 'l2vpn']
safi:
@ -56,7 +56,7 @@ options:
additional_paths_install:
description:
- Install a backup path into the forwarding table and provide
prefix 'independent convergence (PIC) in case of a PE-CE link
prefix independent convergence (PIC) in case of a PE-CE link
failure.
required: false
choices: ['true','false']
@ -175,7 +175,7 @@ options:
keyword which indicates that attributes should be copied from
the aggregate. For example [['lax_inject_map', 'lax_exist_map'],
['nyc_inject_map', 'nyc_exist_map', 'copy-attributes'],
['fsd_inject_map', 'fsd_exist_map']]
['fsd_inject_map', 'fsd_exist_map']].
required: false
default: null
maximum_paths:
@ -194,9 +194,9 @@ options:
- Networks to configure. Valid value is a list of network
prefixes to advertise. The list must be in the form of an array.
Each entry in the array must include a prefix address and an
optional route-map. Example: [['10.0.0.0/16', 'routemap_LA'],
optional route-map. For example [['10.0.0.0/16', 'routemap_LA'],
['192.168.1.1', 'Chicago'], ['192.168.2.0/24],
['192.168.3.0/24', 'routemap_NYC']]
['192.168.3.0/24', 'routemap_NYC']].
required: false
default: null
next_hop_route_map:
@ -213,7 +213,7 @@ options:
redistribute from; the second entry defines a route-map name.
A route-map is highly advised but may be optional on some
platforms, in which case it may be omitted from the array list.
Example: [['direct', 'rm_direct'], ['lisp', 'rm_lisp']]
For example [['direct', 'rm_direct'], ['lisp', 'rm_lisp']].
required: false
default: null
suppress_inactive:
@ -237,16 +237,11 @@ options:
default: null
state:
description:
- Determines whether the config should be present or not on the device.
- Determines whether the config should be present or not
on the device.
required: false
default: present
choices: ['present','absent']
m_facts:
description:
- Used to print module facts
required: false
default: false
choices: ['true','false']
'''
EXAMPLES = '''
# configure a simple address-family
@ -262,17 +257,18 @@ EXAMPLES = '''
RETURN = '''
proposed:
description: k/v pairs of parameters passed into module
returned: always
returned: verbose mode
type: dict
sample: {"advertise_l2vpn_evpn": true, "afi": "ipv4",
"asn": "65535", "safi": "unicast", "vrf": "TESTING"}
existing:
description: k/v pairs of existing BGP AF configuration
returned: verbose mode
type: dict
sample: {}
end_state:
description: k/v pairs of BGP AF configuration after module execution
returned: always
returned: verbose mode
type: dict
sample: {"additional_paths_install": false,
"additional_paths_receive": false,
@ -305,12 +301,7 @@ changed:
'''
# COMMON CODE FOR MIGRATION
import re
import time
import collections
import itertools
import shlex
import ansible.module_utils.nxos
from ansible.module_utils.basic import get_exception
@ -319,200 +310,17 @@ from ansible.module_utils.network import NetworkModule
from ansible.module_utils.shell import ShellError
DEFAULT_COMMENT_TOKENS = ['#', '!']
class ConfigLine(object):
def __init__(self, text):
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
return list()
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
class CustomNetworkConfig(NetworkConfig):
def expand_section(self, configobj, S=None):
if S is None:
@ -524,14 +332,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S)
return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path):
for item in self.items:
if item.text == path[-1]:
@ -539,93 +339,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]:
return item
def get_children(self, path):
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try:
if item != current[index]:
updates.append(item)
except IndexError:
updates.append(item)
elif match == 'exact':
if path:
current = other.get_children(path) or list()
else:
current = other.items
if len(current) != len(config):
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def add(self, lines, parents=None):
@ -677,303 +407,44 @@ class CustomNetworkConfig(object):
self.items.append(item)
def argument_spec():
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
def get_network_module(**kwargs):
try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
errors_re=CLI_ERRORS_RE)
self.shell.open(host, port=port, username=username,
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
return get_module(**kwargs)
except NameError:
return NetworkModule(**kwargs)
def send(self, commands, encoding='text'):
try:
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
def get_config(module, include_defaults=False):
config = module.params['config']
if not config:
cmd = 'show running-config'
if module.params['include_defaults']:
cmd += ' all'
if module.params['transport'] == 'nxapi':
config = module.execute([cmd], command_type='cli_show_ascii')[0]
else:
config = module.execute([cmd])[0]
try:
config = module.get_config()
except AttributeError:
defaults = module.params['include_defaults']
config = module.config.get_config(include_defaults=defaults)
return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate):
config = custom_get_config(module)
config = get_config(module)
commands = candidate.difference(config)
commands = [str(c).strip() for c in commands]
save_config = module.params['save_config']
save_config = module.params['save']
result = dict(changed=False)
if commands:
if not module.check_mode:
try:
module.configure(commands)
except AttributeError:
module.config(commands)
if save_config:
try:
module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True
result['updates'] = commands
@ -1213,7 +684,7 @@ def get_value(arg, config, module):
def get_existing(module, args):
existing = {}
netcfg = custom_get_config(module)
netcfg = get_config(module)
try:
asn_regex = '.*router\sbgp\s(?P<existing_asn>\d+).*'
@ -1515,13 +986,13 @@ def main():
suppress_inactive=dict(required=False, type='bool'),
table_map=dict(required=False, type='str'),
table_map_filter=dict(required=False, type='bool'),
m_facts=dict(required=False, default=False, type='bool'),
state=dict(choices=['present', 'absent'], default='present',
required=False),
include_defaults=dict(default=True)
include_defaults=dict(default=True),
config=dict(),
save=dict(type='bool', default=False)
)
argument_spec.update(nxos_argument_spec)
module = get_module(argument_spec=argument_spec,
module = get_network_module(argument_spec=argument_spec,
required_together=[DAMPENING_PARAMS,
['distance_ibgp',
'distance_ebgp',
@ -1624,7 +1095,7 @@ def main():
result['updates'] = []
result['connected'] = module.connected
if module.params['m_facts']:
if module._verbosity > 0:
end_state = invoke('get_existing', module, args)
result['end_state'] = end_state
result['existing'] = existing

View file

@ -24,18 +24,18 @@ DOCUMENTATION = '''
---
module: nxos_bgp_neighbor
version_added: "2.2"
short_description: Manages BGP neighbors configurations
short_description: Manages BGP neighbors configurations.
description:
- Manages BGP neighbors configurations on NX-OS switches
- Manages BGP neighbors configurations on NX-OS switches.
author: Gabriele Gerbino (@GGabriele)
extends_documentation_fragment: nxos
notes:
- State 'absent' removes the whole BGP neighbor configuration
- default, where supported, restores params default value
- C(state=absent) removes the whole BGP neighbor configuration.
- Default, where supported, restores params default value.
options:
asn:
description:
- BGP autonomous system number. Valid values are String,
- BGP autonomous system number. Valid values are string,
Integer in ASPLAIN or ASDOT notation.
required: true
vrf:
@ -180,16 +180,11 @@ options:
default: null
state:
description:
- Determines whether the config should be present or not on the device.
- Determines whether the config should be present or not
on the device.
required: false
default: present
choices: ['present','absent']
m_facts:
description:
- Used to print module facts
required: false
default: false
choices: ['true','false']
'''
EXAMPLES = '''
# create a new neighbor
@ -210,7 +205,7 @@ EXAMPLES = '''
RETURN = '''
proposed:
description: k/v pairs of parameters passed into module
returned: always
returned: verbose mode
type: dict
sample: {"asn": "65535", "description": "just a description",
"local_as": "20", "neighbor": "3.3.3.3",
@ -218,11 +213,12 @@ proposed:
"update_source": "Ethernet1/3", "vrf": "default"}
existing:
description: k/v pairs of existing BGP neighbor configuration
returned: verbose mode
type: dict
sample: {}
end_state:
description: k/v pairs of BGP neighbor configuration after module execution
returned: always
returned: verbose mode
type: dict
sample: {"asn": "65535", "capability_negotiation": false,
"connected_check": false, "description": "just a description",
@ -250,12 +246,7 @@ changed:
'''
# COMMON CODE FOR MIGRATION
import re
import time
import collections
import itertools
import shlex
import ansible.module_utils.nxos
from ansible.module_utils.basic import get_exception
@ -263,200 +254,17 @@ from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.network import NetworkModule
from ansible.module_utils.shell import ShellError
DEFAULT_COMMENT_TOKENS = ['#', '!']
class ConfigLine(object):
def __init__(self, text):
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
return list()
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
class CustomNetworkConfig(NetworkConfig):
def expand_section(self, configobj, S=None):
if S is None:
@ -468,14 +276,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S)
return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path):
for item in self.items:
if item.text == path[-1]:
@ -483,93 +283,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]:
return item
def get_children(self, path):
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try:
if item != current[index]:
updates.append(item)
except IndexError:
updates.append(item)
elif match == 'exact':
if path:
current = other.get_children(path) or list()
else:
current = other.items
if len(current) != len(config):
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def add(self, lines, parents=None):
@ -621,303 +351,44 @@ class CustomNetworkConfig(object):
self.items.append(item)
def argument_spec():
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
def get_network_module(**kwargs):
try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
errors_re=CLI_ERRORS_RE)
self.shell.open(host, port=port, username=username,
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
return get_module(**kwargs)
except NameError:
return NetworkModule(**kwargs)
def send(self, commands, encoding='text'):
try:
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
def get_config(module, include_defaults=False):
config = module.params['config']
if not config:
cmd = 'show running-config'
if module.params['include_defaults']:
cmd += ' all'
if module.params['transport'] == 'nxapi':
config = module.execute([cmd], command_type='cli_show_ascii')[0]
else:
config = module.execute([cmd])[0]
try:
config = module.get_config()
except AttributeError:
defaults = module.params['include_defaults']
config = module.config.get_config(include_defaults=defaults)
return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate):
config = custom_get_config(module)
config = get_config(module)
commands = candidate.difference(config)
commands = [str(c).strip() for c in commands]
save_config = module.params['save_config']
save_config = module.params['save']
result = dict(changed=False)
if commands:
if not module.check_mode:
try:
module.configure(commands)
except AttributeError:
module.config(commands)
if save_config:
try:
module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True
result['updates'] = commands
@ -1045,7 +516,7 @@ def get_custom_value(arg, config, module):
def get_existing(module, args):
existing = {}
netcfg = custom_get_config(module)
netcfg = get_config(module)
custom = [
'log_neighbor_changes',
'pwd',
@ -1207,12 +678,13 @@ def main():
m_facts=dict(required=False, default=False, type='bool'),
state=dict(choices=['present', 'absent'], default='present',
required=False),
include_defaults=dict(default=True)
include_defaults=dict(default=True),
config=dict(),
save=dict(type='bool', default=False)
)
argument_spec.update(nxos_argument_spec)
module = get_module(argument_spec=argument_spec,
required_together=[['pwd', 'pwd_type'],
['timers_holdtime', 'timers_keepalive']],
module = get_network_module(argument_spec=argument_spec,
required_together=[['timer_bgp_hold',
'timer_bgp_keepalive']],
supports_check_mode=True)
state = module.params['state']
@ -1281,7 +753,7 @@ def main():
result['updates'] = []
result['connected'] = module.connected
if module.params['m_facts']:
if module._verbosity > 0:
end_state = invoke('get_existing', module, args)
result['end_state'] = end_state
result['existing'] = existing

View file

@ -24,17 +24,17 @@ DOCUMENTATION = '''
---
module: nxos_bgp_neighbor_af
version_added: "2.2"
short_description: Manages BGP address-family's neighbors configuration
short_description: Manages BGP address-family's neighbors configuration.
description:
- Manages BGP address-family's neighbors configurations on NX-OS switches
- Manages BGP address-family's neighbors configurations on NX-OS switches.
author: Gabriele Gerbino (@GGabriele)
extends_documentation_fragment: nxos
notes:
- State 'absent' removes the whole BGP address-family's
neighbor configuration
- default, when supported, removes properties
- to defaults maximum-prefix configuration, only
max_prefix_limit=default is needed
- C(state=absent) removes the whole BGP address-family's
neighbor configuration.
- Default, when supported, removes properties
- In order to default maximum-prefix configuration, only
C(max_prefix_limit=default) is needed.
options:
asn:
description:
@ -65,7 +65,7 @@ options:
additional_paths_receive:
description:
- Valid values are enable for basic command enablement; disable
for disabling the command at the neighbor_af level
for disabling the command at the neighbor af level
(it adds the disable keyword to the basic command); and inherit
to remove the command at this level (the command value is
inherited from a higher BGP layer).
@ -75,7 +75,7 @@ options:
additional_paths_send:
description:
- Valid values are enable for basic command enablement; disable
for disabling the command at the neighbor_af level
for disabling the command at the neighbor af level
(it adds the disable keyword to the basic command); and inherit
to remove the command at this level (the command value is
inherited from a higher BGP layer).
@ -129,7 +129,7 @@ options:
default_originate_route_map:
description:
- Optional route-map for the default_originate property. Can be
used independently or in conjunction with default_originate.
used independently or in conjunction with C(default_originate).
Valid values are a string defining a route-map name,
or 'default'.
required: false
@ -248,21 +248,16 @@ options:
default: null
weight:
description:
- weight value. Valid values are an integer value or 'default'.
- Weight value. Valid values are an integer value or 'default'.
required: false
default: null
state:
description:
- Determines whether the config should be present or not on the device.
- Determines whether the config should be present or not
on the device.
required: false
default: present
choices: ['present','absent']
m_facts:
description:
- Used to print module facts
required: false
default: false
choices: ['true','false']
'''
EXAMPLES = '''
configure RR client
@ -281,18 +276,19 @@ configure RR client
RETURN = '''
proposed:
description: k/v pairs of parameters passed into module
returned: always
returned: verbose mode
type: dict
sample: {"afi": "ipv4", "asn": "65535",
"neighbor": "3.3.3.3", "route_reflector_client": true,
"safi": "unicast", "vrf": "default"}
existing:
description: k/v pairs of existing configuration
returned: verbose mode
type: dict
sample: {}
end_state:
description: k/v pairs of configuration after module execution
returned: always
returned: verbose mode
type: dict
sample: {"additional_paths_receive": "inherit",
"additional_paths_send": "inherit",
@ -327,12 +323,7 @@ changed:
# COMMON CODE FOR MIGRATION
import re
import time
import collections
import itertools
import shlex
import ansible.module_utils.nxos
from ansible.module_utils.basic import get_exception
@ -340,200 +331,17 @@ from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.network import NetworkModule
from ansible.module_utils.shell import ShellError
DEFAULT_COMMENT_TOKENS = ['#', '!']
class ConfigLine(object):
def __init__(self, text):
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
return list()
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
class CustomNetworkConfig(NetworkConfig):
def expand_section(self, configobj, S=None):
if S is None:
@ -545,14 +353,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S)
return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path):
for item in self.items:
if item.text == path[-1]:
@ -560,93 +360,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]:
return item
def get_children(self, path):
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try:
if item != current[index]:
updates.append(item)
except IndexError:
updates.append(item)
elif match == 'exact':
if path:
current = other.get_children(path) or list()
else:
current = other.items
if len(current) != len(config):
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def add(self, lines, parents=None):
@ -698,303 +428,44 @@ class CustomNetworkConfig(object):
self.items.append(item)
def argument_spec():
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
def get_network_module(**kwargs):
try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
errors_re=CLI_ERRORS_RE)
self.shell.open(host, port=port, username=username,
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
return get_module(**kwargs)
except NameError:
return NetworkModule(**kwargs)
def send(self, commands, encoding='text'):
try:
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
def get_config(module, include_defaults=False):
config = module.params['config']
if not config:
cmd = 'show running-config'
if module.params['include_defaults']:
cmd += ' all'
if module.params['transport'] == 'nxapi':
config = module.execute([cmd], command_type='cli_show_ascii')[0]
else:
config = module.execute([cmd])[0]
try:
config = module.get_config()
except AttributeError:
defaults = module.params['include_defaults']
config = module.config.get_config(include_defaults=defaults)
return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate):
config = custom_get_config(module)
config = get_config(module)
commands = candidate.difference(config)
commands = [str(c).strip() for c in commands]
save_config = module.params['save_config']
save_config = module.params['save']
result = dict(changed=False)
if commands:
if not module.check_mode:
try:
module.configure(commands)
except AttributeError:
module.config(commands)
if save_config:
try:
module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True
result['updates'] = commands
@ -1176,7 +647,7 @@ def get_custom_value(arg, config, module):
def get_existing(module, args):
existing = {}
netcfg = custom_get_config(module)
netcfg = get_config(module)
custom = [
'allowas_in_max',
@ -1527,13 +998,13 @@ def main():
suppress_inactive=dict(required=False, type='bool'),
unsuppress_map=dict(required=False, type='str'),
weight=dict(required=False, type='str'),
m_facts=dict(required=False, default=False, type='bool'),
state=dict(choices=['present', 'absent'], default='present',
required=False),
include_defaults=dict(default=True)
include_defaults=dict(default=True),
config=dict(),
save=dict(type='bool', default=False)
)
argument_spec.update(nxos_argument_spec)
module = get_module(argument_spec=argument_spec,
module = get_network_module(argument_spec=argument_spec,
mutually_exclusive=[['advertise_map_exist',
'advertise_map_non_exist']],
supports_check_mode=True)
@ -1635,7 +1106,7 @@ def main():
result['updates'] = []
result['connected'] = module.connected
if module.params['m_facts']:
if module._verbosity > 0:
end_state = invoke('get_existing', module, args)
result['end_state'] = end_state
result['existing'] = existing

View file

@ -35,12 +35,6 @@ options:
- EVPN control plane.
required: true
choices: ['true', 'false']
m_facts:
description:
- Used to print module facts
required: false
default: false
choices: ['true','false']
'''
EXAMPLES = '''
- nxos_evpn_global:
@ -53,17 +47,17 @@ EXAMPLES = '''
RETURN = '''
proposed:
description: k/v pairs of parameters passed into module
returned: when I(m_facts)=true
returned: verbose mode
type: dict
sample: {"nv_overlay_evpn": true}
existing:
description: k/v pairs of existing configuration
returned: when I(m_facts)=true
returned: verbose mode
type: dict
sample: {"nv_overlay_evpn": false}
end_state:
description: k/v pairs of configuration after module execution
returned: when I(m_facts)=true
returned: verbose mode
type: dict
sample: {"nv_overlay_evpn": true}
updates:
@ -80,213 +74,25 @@ changed:
# COMMON CODE FOR MIGRATION
import re
import time
import collections
import itertools
import shlex
import itertools
DEFAULT_COMMENT_TOKENS = ['#', '!']
import ansible.module_utils.nxos
from ansible.module_utils.basic import get_exception
from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.network import NetworkModule
from ansible.module_utils.shell import ShellError
class ConfigLine(object):
def __init__(self, text):
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
return list()
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
class CustomNetworkConfig(NetworkConfig):
def expand_section(self, configobj, S=None):
if S is None:
@ -298,14 +104,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S)
return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path):
for item in self.items:
if item.text == path[-1]:
@ -313,93 +111,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]:
return item
def get_children(self, path):
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try:
if item != current[index]:
updates.append(item)
except IndexError:
updates.append(item)
elif match == 'exact':
if path:
current = other.get_children(path) or list()
else:
current = other.items
if len(current) != len(config):
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def add(self, lines, parents=None):
@ -451,18 +179,20 @@ class CustomNetworkConfig(object):
self.items.append(item)
def argument_spec():
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
def get_network_module(**kwargs):
try:
return get_module(**kwargs)
except NameError:
return NetworkModule(**kwargs)
def get_config(module):
config = module.params['running_config']
def get_config(module, include_defaults=False):
config = module.params['config']
if not config:
try:
config = module.get_config()
except AttributeError:
defaults = module.params['include_defaults']
config = module.config.get_config(include_defaults=defaults)
return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate):
@ -471,15 +201,22 @@ def load_config(module, candidate):
commands = candidate.difference(config)
commands = [str(c).strip() for c in commands]
save_config = module.params['save_config']
save_config = module.params['save']
result = dict(changed=False)
if commands:
if not module.check_mode:
try:
module.configure(commands)
except AttributeError:
module.config(commands)
if save_config:
try:
module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True
result['updates'] = commands
@ -545,11 +282,11 @@ def get_commands(module, existing, proposed, candidate):
def main():
argument_spec = dict(
nv_overlay_evpn=dict(required=True, type='bool'),
m_facts=dict(required=False, default=False, type='bool'),
include_defaults=dict(default=True)
include_defaults=dict(default=True),
config=dict(),
save=dict(type='bool', default=False)
)
argument_spec.update(nxos_argument_spec)
module = get_module(argument_spec=argument_spec,
module = get_network_module(argument_spec=argument_spec,
supports_check_mode=True)
existing = invoke('get_existing', module)
@ -571,7 +308,7 @@ def main():
result['updates'] = []
result['connected'] = module.connected
if module.params['m_facts']:
if module._verbosity > 0:
end_state = invoke('get_existing', module)
result['end_state'] = end_state
result['existing'] = existing
@ -580,10 +317,5 @@ def main():
module.exit_json(**result)
from ansible.module_utils.basic import *
from ansible.module_utils.urls import *
from ansible.module_utils.shell import *
from ansible.module_utils.netcfg import *
from ansible.module_utils.nxos import *
if __name__ == '__main__':
main()

View file

@ -24,7 +24,7 @@ DOCUMENTATION = '''
---
module: nxos_evpn_vni
version_added: "2.2"
short_description: Manages Cisco EVPN VXLAN Network Identifier (VNI)
short_description: Manages Cisco EVPN VXLAN Network Identifier (VNI).
description:
- Manages Cisco Ethernet Virtual Private Network (EVPN) VXLAN Network
Identifier (VNI) configurations of a Nexus device.
@ -34,14 +34,14 @@ notes:
- default, where supported, restores params default value.
- RD override is not permitted. You should set it to the default values
first and then reconfigure it.
- route_target_both, route_target_import and route_target_export valid
values are a list of extended communities, (i.e. ['1.2.3.4:5', '33:55'])
or the keywords 'auto' or 'default'.
- The route_target_both property is discouraged due to the inconsistent
- C(route_target_both), C(route_target_import) and
C(route_target_export valid) values are a list of extended communities,
(i.e. ['1.2.3.4:5', '33:55']) or the keywords 'auto' or 'default'.
- The C(route_target_both) property is discouraged due to the inconsistent
behavior of the property across Nexus platforms and image versions.
For this reason it is recommended to use explicit 'route_target_export'
and 'route_target_import' properties instead of route_target_both.
- RD valid values are a String in one of the route-distinguisher formats,
For this reason it is recommended to use explicit C(route_target_export)
and C(route_target_import) properties instead of C(route_target_both).
- RD valid values are a string in one of the route-distinguisher formats,
the keyword 'auto', or the keyword 'default'.
options:
vni:
@ -74,16 +74,11 @@ options:
default: null
state:
description:
- Determines whether the config should be present or not on the device.
- Determines whether the config should be present or not
on the device.
required: false
default: present
choices: ['present','absent']
m_facts:
description:
- Used to print module facts
required: false
default: false
choices: ['true','false']
'''
EXAMPLES = '''
- nxos_evpn_vni:
@ -102,19 +97,20 @@ EXAMPLES = '''
RETURN = '''
proposed:
description: k/v pairs of parameters passed into module
returned: always
returned: verbose mode
type: dict
sample: {"route_target_import": ["5000:10", "4100:100",
"5001:10"],"vni": "6000"}
existing:
description: k/v pairs of existing EVPN VNI configuration
returned: verbose mode
type: dict
sample: {"route_distinguisher": "70:10", "route_target_both": [],
"route_target_export": [], "route_target_import": [
"4100:100", "5000:10"], "vni": "6000"}
end_state:
description: k/v pairs of EVPN VNI configuration after module execution
returned: always
returned: verbose mode
type: dict
sample: {"route_distinguisher": "70:10", "route_target_both": [],
"route_target_export": [], "route_target_import": [
@ -132,12 +128,7 @@ changed:
'''
# COMMON CODE FOR MIGRATION
import re
import time
import collections
import itertools
import shlex
import ansible.module_utils.nxos
from ansible.module_utils.basic import get_exception
@ -145,200 +136,17 @@ from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import ShellError
from ansible.module_utils.network import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!']
class ConfigLine(object):
def __init__(self, text):
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
return list()
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
class CustomNetworkConfig(NetworkConfig):
def expand_section(self, configobj, S=None):
if S is None:
@ -350,14 +158,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S)
return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path):
for item in self.items:
if item.text == path[-1]:
@ -365,93 +165,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]:
return item
def get_children(self, path):
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try:
if item != current[index]:
updates.append(item)
except IndexError:
updates.append(item)
elif match == 'exact':
if path:
current = other.get_children(path) or list()
else:
current = other.items
if len(current) != len(config):
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def add(self, lines, parents=None):
@ -503,303 +233,44 @@ class CustomNetworkConfig(object):
self.items.append(item)
def argument_spec():
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
def get_network_module(**kwargs):
try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
errors_re=CLI_ERRORS_RE)
self.shell.open(host, port=port, username=username,
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
return get_module(**kwargs)
except NameError:
return NetworkModule(**kwargs)
def send(self, commands, encoding='text'):
try:
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
def get_config(module, include_defaults=False):
config = module.params['config']
if not config:
cmd = 'show running-config'
if module.params['include_defaults']:
cmd += ' all'
if module.params['transport'] == 'nxapi':
config = module.execute([cmd], command_type='cli_show_ascii')[0]
else:
config = module.execute([cmd])[0]
try:
config = module.get_config()
except AttributeError:
defaults = module.params['include_defaults']
config = module.config.get_config(include_defaults=defaults)
return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate):
config = custom_get_config(module)
config = get_config(module)
commands = candidate.difference(config)
commands = [str(c).strip() for c in commands]
save_config = module.params['save_config']
save_config = module.params['save']
result = dict(changed=False)
if commands:
if not module.check_mode:
try:
module.configure(commands)
except AttributeError:
module.config(commands)
if save_config:
try:
module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True
result['updates'] = commands
@ -847,7 +318,7 @@ def get_route_target_value(arg, config, module):
def get_existing(module, args):
existing = {}
netcfg = custom_get_config(module)
netcfg = get_config(module)
parents = ['evpn', 'vni {0} l2'.format(module.params['vni'])]
config = netcfg.get_section(parents)
@ -943,13 +414,13 @@ def main():
route_target_both=dict(required=False, type='list'),
route_target_import=dict(required=False, type='list'),
route_target_export=dict(required=False, type='list'),
m_facts=dict(required=False, default=False, type='bool'),
state=dict(choices=['present', 'absent'], default='present',
required=False),
include_defaults=dict(default=True)
include_defaults=dict(default=True),
config=dict(),
save=dict(type='bool', default=False)
)
argument_spec.update(nxos_argument_spec)
module = get_module(argument_spec=argument_spec,
module = get_network_module(argument_spec=argument_spec,
supports_check_mode=True)
state = module.params['state']
@ -996,7 +467,7 @@ def main():
candidate.add(remove_commands, parents=parents)
result = execute_config(module, candidate)
time.sleep(20)
time.sleep(30)
candidate = CustomNetworkConfig(indent=3)
candidate.add(commands, parents=parents)
@ -1005,7 +476,7 @@ def main():
result['updates'] = []
result['connected'] = module.connected
if module.params['m_facts']:
if module._verbosity > 0:
end_state = invoke('get_existing', module, args)
result['end_state'] = end_state
result['existing'] = existing

View file

@ -24,9 +24,9 @@ DOCUMENTATION = '''
---
module: nxos_feature
version_added: "2.1"
short_description: Manage features in NX-OS switches
short_description: Manage features in NX-OS switches.
description:
- Offers ability to enable and disable features in NX-OS
- Offers ability to enable and disable features in NX-OS.
extends_documentation_fragment: nxos
author:
- Jason Edelman (@jedelman8)
@ -81,11 +81,6 @@ end_state:
returned: always
type: dict
sample: {"state": "disabled"}
state:
description: state as sent in from the playbook
returned: always
type: string
sample: "disabled"
updates:
description: commands sent to the device
returned: always
@ -103,217 +98,32 @@ feature:
sample: "vpc"
'''
import json
import collections
# COMMON CODE FOR MIGRATION
import re
import time
import collections
import itertools
import shlex
import json
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
from ansible.module_utils.netcfg import parse
from ansible.module_utils.urls import fetch_url
from ansible.module_utils.basic import get_exception
from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import ShellError
try:
from ansible.module_utils.nxos import get_module
except ImportError:
from ansible.module_utils.nxos import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!']
class ConfigLine(object):
def __init__(self, text):
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
return list()
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
class CustomNetworkConfig(NetworkConfig):
def expand_section(self, configobj, S=None):
if S is None:
@ -325,14 +135,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S)
return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path):
for item in self.items:
if item.text == path[-1]:
@ -340,93 +142,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]:
return item
def get_children(self, path):
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try:
if item != current[index]:
updates.append(item)
except IndexError:
updates.append(item)
elif match == 'exact':
if path:
current = other.get_children(path) or list()
else:
current = other.items
if len(current) != len(config):
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def add(self, lines, parents=None):
@ -478,303 +210,44 @@ class CustomNetworkConfig(object):
self.items.append(item)
def argument_spec():
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
def get_network_module(**kwargs):
try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
errors_re=CLI_ERRORS_RE)
self.shell.open(host, port=port, username=username,
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
return get_module(**kwargs)
except NameError:
return NetworkModule(**kwargs)
def send(self, commands, encoding='text'):
try:
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
def get_config(module, include_defaults=False):
config = module.params['config']
if not config:
cmd = 'show running-config'
if module.params['include_defaults']:
cmd += ' all'
if module.params['transport'] == 'nxapi':
config = module.execute([cmd], command_type='cli_show_ascii')[0]
else:
config = module.execute([cmd])[0]
try:
config = module.get_config()
except AttributeError:
defaults = module.params['include_defaults']
config = module.config.get_config(include_defaults=defaults)
return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate):
config = custom_get_config(module)
config = get_config(module)
commands = candidate.difference(config)
commands = [str(c).strip() for c in commands]
save_config = module.params['save_config']
save_config = module.params['save']
result = dict(changed=False)
if commands:
if not module.check_mode:
try:
module.configure(commands)
except AttributeError:
module.config(commands)
if save_config:
try:
module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True
result['updates'] = commands
@ -820,6 +293,11 @@ def get_cli_body_ssh(command, response, module):
def execute_show(cmds, module, command_type=None):
command_type_map = {
'cli_show': 'json',
'cli_show_ascii': 'text'
}
try:
if command_type:
response = module.execute(cmds, command_type=command_type)
@ -970,9 +448,11 @@ def main():
feature=dict(type='str', required=True),
state=dict(choices=['enabled', 'disabled'], default='enabled',
required=False),
include_defaults=dict(default=False)
include_defaults=dict(default=False),
config=dict(),
save=dict(type='bool', default=False)
)
module = get_module(argument_spec=argument_spec,
module = get_network_module(argument_spec=argument_spec,
supports_check_mode=True)
feature = validate_feature(module)

View file

@ -26,14 +26,16 @@ module: nxos_file_copy
version_added: "2.2"
short_description: Copy a file to a remote NXOS device over SCP.
description:
- Copy a file to the flash (or bootflash) remote network device on NXOS devices
- Copy a file to the flash (or bootflash) remote network device
on NXOS devices.
author:
- Jason Edelman (@jedelman8)
- Gabriele Gerbino (@GGabriele)
extends_documentation_fragment: nxos
notes:
- The feature must be enabled with feature scp-server.
- If the file is already present (md5 sums match), no transfer will take place.
- If the file is already present (md5 sums match), no transfer will
take place.
- Check mode will tell you if the file would be copied.
options:
local_file:
@ -49,7 +51,8 @@ options:
file_system:
description:
- The remote file system of the device. If omitted,
devices that support a file_system parameter will use their default values.
devices that support a file_system parameter will use
their default values.
required: false
default: null
'''
@ -87,214 +90,28 @@ import paramiko
import time
# COMMON CODE FOR MIGRATION
import re
import time
import collections
import itertools
import shlex
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
from ansible.module_utils.netcfg import parse
from ansible.module_utils.urls import fetch_url
from ansible.module_utils.basic import get_exception
from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import ShellError
try:
from ansible.module_utils.nxos import get_module
except ImportError:
from ansible.module_utils.nxos import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!']
class ConfigLine(object):
def __init__(self, text):
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
return list()
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
class CustomNetworkConfig(NetworkConfig):
def expand_section(self, configobj, S=None):
if S is None:
@ -306,14 +123,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S)
return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path):
for item in self.items:
if item.text == path[-1]:
@ -321,93 +130,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]:
return item
def get_children(self, path):
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try:
if item != current[index]:
updates.append(item)
except IndexError:
updates.append(item)
elif match == 'exact':
if path:
current = other.get_children(path) or list()
else:
current = other.items
if len(current) != len(config):
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def add(self, lines, parents=None):
@ -459,303 +198,44 @@ class CustomNetworkConfig(object):
self.items.append(item)
def argument_spec():
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
def get_network_module(**kwargs):
try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
errors_re=CLI_ERRORS_RE)
self.shell.open(host, port=port, username=username,
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
return get_module(**kwargs)
except NameError:
return NetworkModule(**kwargs)
def send(self, commands, encoding='text'):
try:
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
def get_config(module, include_defaults=False):
config = module.params['config']
if not config:
cmd = 'show running-config'
if module.params['include_defaults']:
cmd += ' all'
if module.params['transport'] == 'nxapi':
config = module.execute([cmd], command_type='cli_show_ascii')[0]
else:
config = module.execute([cmd])[0]
try:
config = module.get_config()
except AttributeError:
defaults = module.params['include_defaults']
config = module.config.get_config(include_defaults=defaults)
return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate):
config = custom_get_config(module)
config = get_config(module)
commands = candidate.difference(config)
commands = [str(c).strip() for c in commands]
save_config = module.params['save_config']
save_config = module.params['save']
result = dict(changed=False)
if commands:
if not module.check_mode:
try:
module.configure(commands)
except AttributeError:
module.config(commands)
if save_config:
try:
module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True
result['updates'] = commands
@ -764,6 +244,11 @@ def load_config(module, candidate):
# END OF COMMON CODE
def execute_show(cmds, module, command_type=None):
command_type_map = {
'cli_show': 'json',
'cli_show_ascii': 'text'
}
try:
if command_type:
response = module.execute(cmds, command_type=command_type)
@ -773,6 +258,19 @@ def execute_show(cmds, module, command_type=None):
clie = get_exception()
module.fail_json(msg='Error sending {0}'.format(cmds),
error=str(clie))
except AttributeError:
try:
if command_type:
command_type = command_type_map.get(command_type)
module.cli.add_commands(cmds, output=command_type)
response = module.cli.run_commands()
else:
module.cli.add_commands(cmds, output=command_type)
response = module.cli.run_commands()
except ShellError:
clie = get_exception()
module.fail_json(msg='Error sending {0}'.format(cmds),
error=str(clie))
return response
@ -850,7 +348,7 @@ def transfer_file(module, dest):
scp = SCPClient(ssh.get_transport())
try:
scp.put(module.params['local_file'], full_remote_path)
except Exception as e:
except:
time.sleep(10)
temp_size = verify_remote_file_exists(
module, dest, file_system=module.params['file_system'])
@ -870,8 +368,11 @@ def main():
local_file=dict(required=True),
remote_file=dict(required=False),
file_system=dict(required=False, default='bootflash:'),
include_defaults=dict(default=True),
config=dict(),
save=dict(type='bool', default=False)
)
module = get_module(argument_spec=argument_spec,
module = get_network_module(argument_spec=argument_spec,
supports_check_mode=True)
local_file = module.params['local_file']

View file

@ -25,60 +25,60 @@ DOCUMENTATION = '''
---
module: nxos_hsrp
version_added: "2.2"
short_description: Manages HSRP configuration on NX-OS switches
short_description: Manages HSRP configuration on NX-OS switches.
description:
- Manages HSRP configuration on NX-OS switches
- Manages HSRP configuration on NX-OS switches.
extends_documentation_fragment: nxos
author:
- Jason Edelman (@jedelman8)
- Gabriele Gerbino (@GGabriele)
notes:
- HSRP feature needs to be enabled first on the system
- SVIs must exist before using this module
- Interface must be a L3 port before using this module
- HSRP cannot be configured on loopback interfaces
- HSRP feature needs to be enabled first on the system.
- SVIs must exist before using this module.
- Interface must be a L3 port before using this module.
- HSRP cannot be configured on loopback interfaces.
- MD5 authentication is only possible with HSRPv2 while it is ignored if
HSRPv1 is used instead, while it will not raise any error. Here we allow
MD5 authentication only with HSRPv2 in order to enforce better practice.
options:
group:
description:
- HSRP group number
- HSRP group number.
required: true
interface:
description:
- Full name of interface that is being managed for HSRP
- Full name of interface that is being managed for HSRP.
required: true
version:
description:
- HSRP version
- HSRP version.
required: false
default: 2
choices: ['1','2']
priority:
description:
- HSRP priority
- HSRP priority.
required: false
default: null
vip:
description:
- HSRP virtual IP address
- HSRP virtual IP address.
required: false
default: null
auth_string:
description:
- Authentication string
- Authentication string.
required: false
default: null
auth_type:
description:
- Authentication type
- Authentication type.
required: false
default: null
choices: ['text','md5']
state:
description:
- Specify desired state of the resource
- Specify desired state of the resource.
required: false
choices: ['present','absent']
default: 'present'
@ -143,7 +143,6 @@ changed:
sample: true
'''
DEFAULT_COMMENT_TOKENS = ['#', '!']
import json
# COMMON CODE FOR MIGRATION
@ -154,198 +153,17 @@ from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import ShellError
from ansible.module_utils.network import NetworkModule
class ConfigLine(object):
def __init__(self, text):
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
return list()
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
class CustomNetworkConfig(NetworkConfig):
def expand_section(self, configobj, S=None):
if S is None:
@ -357,14 +175,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S)
return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path):
for item in self.items:
if item.text == path[-1]:
@ -372,93 +182,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]:
return item
def get_children(self, path):
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try:
if item != current[index]:
updates.append(item)
except IndexError:
updates.append(item)
elif match == 'exact':
if path:
current = other.get_children(path) or list()
else:
current = other.items
if len(current) != len(config):
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def add(self, lines, parents=None):
@ -510,303 +250,44 @@ class CustomNetworkConfig(object):
self.items.append(item)
def argument_spec():
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
def get_network_module(**kwargs):
try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
errors_re=CLI_ERRORS_RE)
self.shell.open(host, port=port, username=username,
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
return get_module(**kwargs)
except NameError:
return NetworkModule(**kwargs)
def send(self, commands, encoding='text'):
try:
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
def get_config(module, include_defaults=False):
config = module.params['config']
if not config:
cmd = 'show running-config'
if module.params['include_defaults']:
cmd += ' all'
if module.params['transport'] == 'nxapi':
config = module.execute([cmd], command_type='cli_show_ascii')[0]
else:
config = module.execute([cmd])[0]
try:
config = module.get_config()
except AttributeError:
defaults = module.params['include_defaults']
config = module.config.get_config(include_defaults=defaults)
return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate):
config = custom_get_config(module)
config = get_config(module)
commands = candidate.difference(config)
commands = [str(c).strip() for c in commands]
save_config = module.params['save_config']
save_config = module.params['save']
result = dict(changed=False)
if commands:
if not module.check_mode:
try:
module.configure(commands)
except AttributeError:
module.config(commands)
if save_config:
try:
module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True
result['updates'] = commands
@ -856,6 +337,11 @@ def get_cli_body_ssh(command, response, module):
def execute_show(cmds, module, command_type=None):
command_type_map = {
'cli_show': 'json',
'cli_show_ascii': 'text'
}
try:
if command_type:
response = module.execute(cmds, command_type=command_type)
@ -863,7 +349,7 @@ def execute_show(cmds, module, command_type=None):
response = module.execute(cmds)
except ShellError:
clie = get_exception()
module.fail_json(msg='Error sending {0}'.format(command),
module.fail_json(msg='Error sending {0}'.format(cmds),
error=str(clie))
except AttributeError:
try:
@ -1129,8 +615,11 @@ def main():
auth_string=dict(type='str', required=False),
state=dict(choices=['absent', 'present'], required=False,
default='present'),
include_defaults=dict(default=True),
config=dict(),
save=dict(type='bool', default=False)
)
module = get_module(argument_spec=argument_spec,
module = get_network_module(argument_spec=argument_spec,
supports_check_mode=True)
interface = module.params['interface'].lower()

View file

@ -24,17 +24,18 @@ DOCUMENTATION = '''
---
module: nxos_igmp
version_added: "2.2"
short_description: Manages IGMP global configuration
short_description: Manages IGMP global configuration.
description:
- Manages IGMP global configuration configuration settings
- Manages IGMP global configuration configuration settings.
extends_documentation_fragment: nxos
author:
- Jason Edelman (@jedelman8)
- Gabriele Gerbino (@GGabriele)
notes:
- When state=default, all supported params will be reset to a default state
- When C(state=default), all supported params will be reset to a
default state.
- If restart is set to true with other params set, the restart will happen
last, i.e. after the configuration takes place
last, i.e. after the configuration takes place.
options:
flush_routes:
description:
@ -46,19 +47,19 @@ options:
enforce_rtr_alert:
description:
- Enables or disables the enforce router alert option check for
IGMPv2 and IGMPv3 packets
IGMPv2 and IGMPv3 packets.
required: false
default: null
choices: ['true', 'false']
restart:
description:
- restarts the igmp process (using an exec config command)
- Restarts the igmp process (using an exec config command).
required: false
default: null
choices: ['true', 'false']
state:
description:
- Manages desired state of the resource
- Manages desired state of the resource.
required: false
default: present
choices: ['present', 'default']
@ -84,17 +85,17 @@ EXAMPLES = '''
RETURN = '''
proposed:
description: k/v pairs of parameters passed into module
returned: when C(m_facts)=true
returned: verbose mode
type: dict
sample: {"enforce_rtr_alert": true, "flush_routes": true}
existing:
description: k/v pairs of existing IGMP configuration
returned: when C(m_facts)=true
returned: verbose mode
type: dict
sample: {"enforce_rtr_alert": true, "flush_routes": false}
end_state:
description: k/v pairs of IGMP configuration after module execution
returned: when C(m_facts)=true
returned: verbose mode
type: dict
sample: {"enforce_rtr_alert": true, "flush_routes": true}
updates:
@ -110,215 +111,28 @@ changed:
'''
# COMMON CODE FOR MIGRATION
import re
import time
import collections
import itertools
import shlex
import json
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
from ansible.module_utils.netcfg import parse
from ansible.module_utils.urls import fetch_url
from ansible.module_utils.basic import get_exception
from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import ShellError
try:
from ansible.module_utils.nxos import get_module
except ImportError:
from ansible.module_utils.nxos import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!']
class ConfigLine(object):
def __init__(self, text):
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
return list()
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
class CustomNetworkConfig(NetworkConfig):
def expand_section(self, configobj, S=None):
if S is None:
@ -330,14 +144,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S)
return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path):
for item in self.items:
if item.text == path[-1]:
@ -345,93 +151,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]:
return item
def get_children(self, path):
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try:
if item != current[index]:
updates.append(item)
except IndexError:
updates.append(item)
elif match == 'exact':
if path:
current = other.get_children(path) or list()
else:
current = other.items
if len(current) != len(config):
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def add(self, lines, parents=None):
@ -483,303 +219,44 @@ class CustomNetworkConfig(object):
self.items.append(item)
def argument_spec():
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
def get_network_module(**kwargs):
try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
errors_re=CLI_ERRORS_RE)
self.shell.open(host, port=port, username=username,
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
return get_module(**kwargs)
except NameError:
return NetworkModule(**kwargs)
def send(self, commands, encoding='text'):
try:
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
def get_config(module, include_defaults=False):
config = module.params['config']
if not config:
cmd = 'show running-config'
if module.params['include_defaults']:
cmd += ' all'
if module.params['transport'] == 'nxapi':
config = module.execute([cmd], command_type='cli_show_ascii')[0]
else:
config = module.execute([cmd])[0]
try:
config = module.get_config()
except AttributeError:
defaults = module.params['include_defaults']
config = module.config.get_config(include_defaults=defaults)
return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate):
config = custom_get_config(module)
config = get_config(module)
commands = candidate.difference(config)
commands = [str(c).strip() for c in commands]
save_config = module.params['save_config']
save_config = module.params['save']
result = dict(changed=False)
if commands:
if not module.check_mode:
try:
module.configure(commands)
except AttributeError:
module.config(commands)
if save_config:
try:
module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True
result['updates'] = commands
@ -806,7 +283,7 @@ def get_value(arg, config):
def get_existing(module, args):
existing = {}
config = str(custom_get_config(module))
config = str(get_config(module))
for arg in args:
existing[arg] = get_value(arg, config)
@ -862,11 +339,11 @@ def main():
enforce_rtr_alert=dict(type='bool'),
restart=dict(type='bool', default=False),
state=dict(choices=['present', 'default'], default='present'),
m_facts=dict(required=False, default=False, type='bool'),
include_defaults=dict(default=False)
include_defaults=dict(default=False),
config=dict(),
save=dict(type='bool', default=False)
)
argument_spec.update(nxos_argument_spec)
module = get_module(argument_spec=argument_spec,
module = get_network_module(argument_spec=argument_spec,
supports_check_mode=True)
state = module.params['state']
@ -909,7 +386,7 @@ def main():
if restart:
proposed['restart'] = restart
result['connected'] = module.connected
if module.params['m_facts']:
if module._verbosity > 0:
end_state = invoke('get_existing', module, args)
result['end_state'] = end_state
result['existing'] = existing

View file

@ -24,35 +24,32 @@ DOCUMENTATION = '''
---
module: nxos_igmp_interface
version_added: "2.2"
short_description: Manages IGMP interface configuration
short_description: Manages IGMP interface configuration.
description:
- Manages IGMP interface configuration settings
- Manages IGMP interface configuration settings.
extends_documentation_fragment: nxos
author:
- Jason Edelman (@jedelman8)
- Gabriele Gerbino (@GGabriele)
notes:
- When state=default, supported params will be reset to a default state.
These include: version, startup_query_interval, startup_query_count,
robustness, querier_timeout, query_mrt, query_interval, last_member_qrt,
last_member_query_count, group_timeout, report_llg, and immediate_leave
- When state=absent, all configs for oif_prefix, oif_source, and
oif_routemap will be removed.
- PIM must be enabled to use this module
- This module is for Layer 3 interfaces
- When C(state=default), supported params will be reset to a default state.
These include C(version), C(startup_query_interval),
C(startup_query_count), C(robustness), C(querier_timeout), C(query_mrt),
C(query_interval), C(last_member_qrt), C(last_member_query_count),
C(group_timeout), C(report_llg), and C(immediate_leave).
- When C(state=absent), all configs for C(oif_prefix), C(oif_source), and
C(oif_routemap) will be removed.
- PIM must be enabled to use this module.
- This module is for Layer 3 interfaces.
- Route-map check not performed (same as CLI) check when configuring
route-map with 'static-oif'
- If restart is set to true with other params set, the restart will happen
last, i.e. after the configuration takes place
- While username and password are not required params, they are
if you are not using the .netauth file. .netauth file is recommended
as it will clean up the each task in the playbook by not requiring
the username and password params for every tasks.
- Using the username and password params will override the .netauth file
last, i.e. after the configuration takes place.
options:
interface:
description:
- The FULL interface name for IGMP configuration.
- The full interface name for IGMP configuration.
e.g. I(Ethernet1/2).
required: true
version:
description:
@ -102,7 +99,7 @@ options:
description:
- Sets the query interval waited after sending membership reports
before the software deletes the group state. Values can range
from 1 to 25 seconds. The default is 1 second
from 1 to 25 seconds. The default is 1 second.
required: false
default: null
last_member_query_count:
@ -156,13 +153,13 @@ options:
default: null
restart:
description:
- Restart IGMP
- Restart IGMP.
required: false
choices: ['true', 'false']
default: null
state:
description:
- Manages desired state of the resource
- Manages desired state of the resource.
required: false
default: present
choices: ['present', 'default']
@ -236,217 +233,32 @@ changed:
sample: true
'''
import json
import collections
# COMMON CODE FOR MIGRATION
import re
import time
import collections
import itertools
import shlex
import json
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
from ansible.module_utils.netcfg import parse
from ansible.module_utils.urls import fetch_url
from ansible.module_utils.basic import get_exception
from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import ShellError
try:
from ansible.module_utils.nxos import get_module
except ImportError:
from ansible.module_utils.nxos import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!']
class ConfigLine(object):
def __init__(self, text):
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
return list()
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
class CustomNetworkConfig(NetworkConfig):
def expand_section(self, configobj, S=None):
if S is None:
@ -458,14 +270,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S)
return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path):
for item in self.items:
if item.text == path[-1]:
@ -473,93 +277,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]:
return item
def get_children(self, path):
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try:
if item != current[index]:
updates.append(item)
except IndexError:
updates.append(item)
elif match == 'exact':
if path:
current = other.get_children(path) or list()
else:
current = other.items
if len(current) != len(config):
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def add(self, lines, parents=None):
@ -611,303 +345,44 @@ class CustomNetworkConfig(object):
self.items.append(item)
def argument_spec():
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
def get_network_module(**kwargs):
try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
errors_re=CLI_ERRORS_RE)
self.shell.open(host, port=port, username=username,
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
return get_module(**kwargs)
except NameError:
return NetworkModule(**kwargs)
def send(self, commands, encoding='text'):
try:
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
def get_config(module, include_defaults=False):
config = module.params['config']
if not config:
cmd = 'show running-config'
if module.params['include_defaults']:
cmd += ' all'
if module.params['transport'] == 'nxapi':
config = module.execute([cmd], command_type='cli_show_ascii')[0]
else:
config = module.execute([cmd])[0]
try:
config = module.get_config()
except AttributeError:
defaults = module.params['include_defaults']
config = module.config.get_config(include_defaults=defaults)
return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate):
config = custom_get_config(module)
config = get_config(module)
commands = candidate.difference(config)
commands = [str(c).strip() for c in commands]
save_config = module.params['save_config']
save_config = module.params['save']
result = dict(changed=False)
if commands:
if not module.check_mode:
try:
module.configure(commands)
except AttributeError:
module.config(commands)
if save_config:
try:
module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True
result['updates'] = commands
@ -915,7 +390,6 @@ def load_config(module, candidate):
return result
# END OF COMMON CODE
def get_cli_body_ssh(command, response, module):
"""Get response for when transport=cli. This is kind of a hack and mainly
needed because these modules were originally written for NX-API. And
@ -939,6 +413,11 @@ def get_cli_body_ssh(command, response, module):
def execute_show(cmds, module, command_type=None):
command_type_map = {
'cli_show': 'json',
'cli_show_ascii': 'text'
}
try:
if command_type:
response = module.execute(cmds, command_type=command_type)
@ -946,7 +425,7 @@ def execute_show(cmds, module, command_type=None):
response = module.execute(cmds)
except ShellError:
clie = get_exception()
module.fail_json(msg='Error sending {0}'.format(command),
module.fail_json(msg='Error sending {0}'.format(cmds),
error=str(clie))
except AttributeError:
try:
@ -1272,10 +751,11 @@ def main():
restart=dict(type='bool', default=False),
state=dict(choices=['present', 'absent', 'default'],
default='present'),
include_defaults=dict(default=True)
include_defaults=dict(default=True),
config=dict(),
save=dict(type='bool', default=False)
)
argument_spec.update(nxos_argument_spec)
module = get_module(argument_spec=argument_spec,
module = get_network_module(argument_spec=argument_spec,
supports_check_mode=True)
state = module.params['state']

View file

@ -30,11 +30,11 @@ description:
author: Gabriele Gerbino (@GGabriele)
extends_documentation_fragment: nxos
notes:
- default, where supported, restores params default value
- Default, where supported, restores params default value.
- To remove an existing authentication configuration you should use
message_digest_key_id=default plus all other options matching their
C(message_digest_key_id=default) plus all other options matching their
existing values.
- State absent remove the whole OSPF interface configuration
- C(state=absent) removes the whole OSPF interface configuration.
options:
interface:
description:
@ -84,7 +84,7 @@ options:
default: null
message_digest_key_id:
description:
- md5 authentication key-id associated with the ospf instance.
- Md5 authentication key-id associated with the ospf instance.
If this is present, message_digest_encryption_type,
message_digest_algorithm_type and message_digest_password are
mandatory. Valid value is an integer and 'default'.
@ -111,16 +111,11 @@ options:
default: null
state:
description:
- Determines whether the config should be present or not on the device.
- Determines whether the config should be present or not
on the device.
required: false
default: present
choices: ['present','absent']
m_facts:
description:
- Used to print module facts
required: false
default: false
choices: ['true','false']
'''
EXAMPLES = '''
- nxos_interface_ospf:
@ -136,11 +131,12 @@ EXAMPLES = '''
RETURN = '''
proposed:
description: k/v pairs of parameters passed into module
returned: always
returned: verbose mode
type: dict
sample: {"area": "1", "interface": "ethernet1/32", "ospf": "1"}
existing:
description: k/v pairs of existing OSPF configuration
returned: verbose mode
type: dict
sample: {"area": "", "cost": "", "dead_interval": "",
"hello_interval": "", "interface": "ethernet1/32",
@ -150,7 +146,7 @@ existing:
"ospf": "", "passive_interface": false}
end_state:
description: k/v pairs of OSPF configuration after module execution
returned: always
returned: verbose mode
type: dict
sample: {"area": "0.0.0.1", "cost": "", "dead_interval": "",
"hello_interval": "", "interface": "ethernet1/32",
@ -172,14 +168,8 @@ changed:
# COMMON CODE FOR MIGRATION
import re
import time
import collections
import itertools
import shlex
DEFAULT_COMMENT_TOKENS = ['#', '!']
import ansible.module_utils.nxos
from ansible.module_utils.basic import get_exception
from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
@ -187,198 +177,16 @@ from ansible.module_utils.network import NetworkModule
from ansible.module_utils.shell import ShellError
class ConfigLine(object):
def __init__(self, text):
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
return list()
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
class CustomNetworkConfig(NetworkConfig):
def expand_section(self, configobj, S=None):
if S is None:
@ -390,14 +198,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S)
return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path):
for item in self.items:
if item.text == path[-1]:
@ -405,93 +205,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]:
return item
def get_children(self, path):
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try:
if item != current[index]:
updates.append(item)
except IndexError:
updates.append(item)
elif match == 'exact':
if path:
current = other.get_children(path) or list()
else:
current = other.items
if len(current) != len(config):
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def add(self, lines, parents=None):
@ -543,303 +273,44 @@ class CustomNetworkConfig(object):
self.items.append(item)
def argument_spec():
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
def get_network_module(**kwargs):
try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
errors_re=CLI_ERRORS_RE)
self.shell.open(host, port=port, username=username,
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
return get_module(**kwargs)
except NameError:
return NetworkModule(**kwargs)
def send(self, commands, encoding='text'):
try:
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
def get_config(module, include_defaults=False):
config = module.params['config']
if not config:
cmd = 'show running-config'
if module.params['include_defaults']:
cmd += ' all'
if module.params['transport'] == 'nxapi':
config = module.execute([cmd], command_type='cli_show_ascii')[0]
else:
config = module.execute([cmd])[0]
try:
config = module.get_config()
except AttributeError:
defaults = module.params['include_defaults']
config = module.config.get_config(include_defaults=defaults)
return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate):
config = custom_get_config(module)
config = get_config(module)
commands = candidate.difference(config)
commands = [str(c).strip() for c in commands]
save_config = module.params['save_config']
save_config = module.params['save']
result = dict(changed=False)
if commands:
if not module.check_mode:
try:
module.configure(commands)
except AttributeError:
module.config(commands)
if save_config:
try:
module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True
result['updates'] = commands
@ -954,7 +425,7 @@ def get_value(arg, config, module):
def get_existing(module, args):
existing = {}
netcfg = custom_get_config(module)
netcfg = get_config(module)
parents = ['interface {0}'.format(module.params['interface'].capitalize())]
config = netcfg.get_section(parents)
if 'ospf' in config:
@ -1123,13 +594,13 @@ def main():
message_digest_encryption_type=dict(required=False, type='str',
choices=['cisco_type_7','3des']),
message_digest_password=dict(required=False, type='str'),
m_facts=dict(required=False, default=False, type='bool'),
state=dict(choices=['present', 'absent'], default='present',
required=False),
include_defaults=dict(default=True)
include_defaults=dict(default=True),
config=dict(),
save=dict(type='bool', default=False)
)
argument_spec.update(nxos_argument_spec)
module = get_module(argument_spec=argument_spec,
module = get_network_module(argument_spec=argument_spec,
required_together=[['message_digest_key_id',
'message_digest_algorithm_type',
'message_digest_encryption_type',
@ -1197,7 +668,7 @@ def main():
result['updates'] = []
result['connected'] = module.connected
if module.params['m_facts']:
if module._verbosity > 0:
end_state = invoke('get_existing', module, args)
result['end_state'] = end_state
result['existing'] = existing

View file

@ -24,37 +24,37 @@ DOCUMENTATION = '''
---
module: nxos_ip_interface
version_added: "2.1"
short_description: Manages L3 attributes for IPv4 and IPv6 interfaces
short_description: Manages L3 attributes for IPv4 and IPv6 interfaces.
description:
- Manages Layer 3 attributes for IPv4 and IPv6 interfaces
- Manages Layer 3 attributes for IPv4 and IPv6 interfaces.
extends_documentation_fragment: nxos
author:
- Jason Edelman (@jedelman8)
- Gabriele Gerbino (@GGabriele)
notes:
- Interface must already be a L3 port when using this module
- Logical interfaces (po, loop, svi) must be created first
- I(mask) must be inserted in decimal format (i.e. 24) for
- Interface must already be a L3 port when using this module.
- Logical interfaces (po, loop, svi) must be created first.
- C(mask) must be inserted in decimal format (i.e. 24) for
both IPv6 and IPv4.
- A single interface can have multiple IPv6 configured.
options:
interface:
description:
- Full name of interface, i.e. Ethernet1/1, vlan10
- Full name of interface, i.e. Ethernet1/1, vlan10.
required: true
addr:
description:
- IPv4 or IPv6 Address
- IPv4 or IPv6 Address.
required: false
default: null
mask:
description:
- Subnet mask for IPv4 or IPv6 Address in decimal format
- Subnet mask for IPv4 or IPv6 Address in decimal format.
required: false
default: null
state:
description:
- Specify desired state of the resource
- Specify desired state of the resource.
required: false
default: present
choices: ['present','absent']
@ -99,11 +99,6 @@ end_state:
sample: {"addresses": [{"addr": "20.20.20.20", "mask": 24}],
"interface": "ethernet1/32", "prefix": "20.20.20.0",
"type": "ethernet", "vrf": "default"}
state:
description: state as sent in from the playbook
returned: always
type: string
sample: "present"
updates:
description: commands sent to the device
returned: always
@ -116,216 +111,32 @@ changed:
sample: true
'''
# COMMON CODE FOR MIGRATION
import re
import time
import collections
import itertools
import shlex
import json
import collections
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
from ansible.module_utils.netcfg import parse
from ansible.module_utils.urls import fetch_url
# COMMON CODE FOR MIGRATION
import re
from ansible.module_utils.basic import get_exception
from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import ShellError
try:
from ansible.module_utils.nxos import get_module
except ImportError:
from ansible.module_utils.nxos import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!']
class ConfigLine(object):
def __init__(self, text):
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
return list()
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
class CustomNetworkConfig(NetworkConfig):
def expand_section(self, configobj, S=None):
if S is None:
@ -337,14 +148,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S)
return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path):
for item in self.items:
if item.text == path[-1]:
@ -352,93 +155,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]:
return item
def get_children(self, path):
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try:
if item != current[index]:
updates.append(item)
except IndexError:
updates.append(item)
elif match == 'exact':
if path:
current = other.get_children(path) or list()
else:
current = other.items
if len(current) != len(config):
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def add(self, lines, parents=None):
@ -490,303 +223,44 @@ class CustomNetworkConfig(object):
self.items.append(item)
def argument_spec():
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
def get_network_module(**kwargs):
try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
errors_re=CLI_ERRORS_RE)
self.shell.open(host, port=port, username=username,
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
return get_module(**kwargs)
except NameError:
return NetworkModule(**kwargs)
def send(self, commands, encoding='text'):
try:
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
def get_config(module, include_defaults=False):
config = module.params['config']
if not config:
cmd = 'show running-config'
if module.params['include_defaults']:
cmd += ' all'
if module.params['transport'] == 'nxapi':
config = module.execute([cmd], command_type='cli_show_ascii')[0]
else:
config = module.execute([cmd])[0]
try:
config = module.get_config()
except AttributeError:
defaults = module.params['include_defaults']
config = module.config.get_config(include_defaults=defaults)
return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate):
config = custom_get_config(module)
config = get_config(module)
commands = candidate.difference(config)
commands = [str(c).strip() for c in commands]
save_config = module.params['save_config']
save_config = module.params['save']
result = dict(changed=False)
if commands:
if not module.check_mode:
try:
module.configure(commands)
except AttributeError:
module.config(commands)
if save_config:
try:
module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True
result['updates'] = commands
@ -834,6 +308,11 @@ def get_cli_body_ssh(command, response, module):
def execute_show(cmds, module, command_type=None):
command_type_map = {
'cli_show': 'json',
'cli_show_ascii': 'text'
}
try:
if command_type:
response = module.execute(cmds, command_type=command_type)
@ -1157,9 +636,11 @@ def main():
mask=dict(type='str', required=False),
state=dict(required=False, default='present',
choices=['present', 'absent']),
include_defaults=dict(default=True)
include_defaults=dict(default=True),
config=dict(),
save=dict(type='bool', default=False)
)
module = get_module(argument_spec=argument_spec,
module = get_network_module(argument_spec=argument_spec,
supports_check_mode=True)
addr = module.params['addr']

View file

@ -24,6 +24,7 @@ DOCUMENTATION = '''
---
module: nxos_ntp_options
version_added: "2.2"
short_description: Manages NTP options.
description:
- Manages NTP options, e.g. authoritative server and logging.

View file

@ -36,16 +36,11 @@ options:
required: true
state:
description:
- Determines whether the config should be present or not on the device.
- Determines whether the config should be present or not
on the device.
required: false
default: present
choices: ['present','absent']
m_facts:
description:
- Used to print module facts
required: false
default: false
choices: ['true','false']
'''
EXAMPLES = '''
@ -60,22 +55,22 @@ EXAMPLES = '''
RETURN = '''
proposed:
description: k/v pairs of parameters passed into module
returned: when I(m_facts)=true
returned: verbose mode
type: dict
sample: {"ospf": "1"}
existing:
description: k/v pairs of existing configuration
returned: when I(m_facts)=true
returned: verbose mode
type: dict
sample: {"ospf": ["2"]}
end_state:
description: k/v pairs of configuration after module execution
returned: when I(m_facts)=true
returned: verbose mode
type: dict
sample: {"ospf": ["1", "2"]}
updates:
description: commands sent to the device
returned: when I(m_facts)=true
returned: always
type: list
sample: ["router ospf 1"]
changed:
@ -87,213 +82,25 @@ changed:
# COMMON CODE FOR MIGRATION
import re
import time
import collections
import itertools
import shlex
import itertools
DEFAULT_COMMENT_TOKENS = ['#', '!']
import ansible.module_utils.nxos
from ansible.module_utils.basic import get_exception
from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.network import NetworkModule
from ansible.module_utils.shell import ShellError
class ConfigLine(object):
def __init__(self, text):
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
return list()
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
class CustomNetworkConfig(NetworkConfig):
def expand_section(self, configobj, S=None):
if S is None:
@ -305,14 +112,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S)
return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path):
for item in self.items:
if item.text == path[-1]:
@ -320,93 +119,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]:
return item
def get_children(self, path):
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try:
if item != current[index]:
updates.append(item)
except IndexError:
updates.append(item)
elif match == 'exact':
if path:
current = other.get_children(path) or list()
else:
current = other.items
if len(current) != len(config):
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def add(self, lines, parents=None):
@ -458,18 +187,20 @@ class CustomNetworkConfig(object):
self.items.append(item)
def argument_spec():
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
def get_network_module(**kwargs):
try:
return get_module(**kwargs)
except NameError:
return NetworkModule(**kwargs)
def get_config(module):
config = module.params['running_config']
def get_config(module, include_defaults=False):
config = module.params['config']
if not config:
try:
config = module.get_config()
except AttributeError:
defaults = module.params['include_defaults']
config = module.config.get_config(include_defaults=defaults)
return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate):
@ -478,15 +209,22 @@ def load_config(module, candidate):
commands = candidate.difference(config)
commands = [str(c).strip() for c in commands]
save_config = module.params['save_config']
save_config = module.params['save']
result = dict(changed=False)
if commands:
if not module.check_mode:
try:
module.configure(commands)
except AttributeError:
module.config(commands)
if save_config:
try:
module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True
result['updates'] = commands
@ -560,13 +298,13 @@ def state_absent(module, proposed, candidate):
def main():
argument_spec = dict(
ospf=dict(required=True, type='str'),
m_facts=dict(required=False, default=False, type='bool'),
state=dict(choices=['present', 'absent'], default='present',
required=False),
include_defaults=dict(default=True)
include_defaults=dict(default=True),
config=dict(),
save=dict(type='bool', default=False)
)
argument_spec.update(nxos_argument_spec)
module = get_module(argument_spec=argument_spec,
module = get_network_module(argument_spec=argument_spec,
supports_check_mode=True)
state = module.params['state']
@ -596,7 +334,7 @@ def main():
result['updates'] = []
result['connected'] = module.connected
if module.params['m_facts']:
if module._verbosity > 0:
end_state = invoke('get_existing', module)
result['end_state'] = end_state
result['existing'] = existing
@ -605,11 +343,5 @@ def main():
module.exit_json(**result)
from ansible.module_utils.basic import *
from ansible.module_utils.urls import *
from ansible.module_utils.shell import *
from ansible.module_utils.netcfg import *
from ansible.module_utils.nxos import *
if __name__ == '__main__':
main()

View file

@ -111,12 +111,6 @@ options:
Valid values are an integer, in Mbps, or the keyword 'default'.
required: false
default: null
m_facts:
description:
- Used to print module facts
required: false
default: false
choices: ['true','false']
'''
EXAMPLES = '''
@ -129,7 +123,6 @@ EXAMPLES = '''
timer_throttle_lsa_hold: 1100
timer_throttle_lsa_max: 3000
vrf: test
m_facts: true
state: present
username: "{{ un }}"
password: "{{ pwd }}"
@ -139,7 +132,7 @@ EXAMPLES = '''
RETURN = '''
proposed:
description: k/v pairs of parameters passed into module
returned: when I(m_facts)=true
returned: verbose mode
type: dict
sample: {"ospf": "1", "timer_throttle_lsa_hold": "1100",
"timer_throttle_lsa_max": "3000", "timer_throttle_lsa_start": "60",
@ -148,7 +141,7 @@ proposed:
"vrf": "test"}
existing:
description: k/v pairs of existing configuration
returned: when I(m_facts)=true
returned: verbose mode
type: dict
sample: {"auto_cost": "40000", "default_metric": "", "log_adjacency": "",
"ospf": "1", "router_id": "", "timer_throttle_lsa_hold": "5000",
@ -158,7 +151,7 @@ existing:
"timer_throttle_spf_start": "200", "vrf": "test"}
end_state:
description: k/v pairs of configuration after module execution
returned: when I(m_facts)=true
returned: verbose mode
type: dict
sample: {"auto_cost": "40000", "default_metric": "", "log_adjacency": "",
"ospf": "1", "router_id": "", "timer_throttle_lsa_hold": "1100",
@ -180,12 +173,7 @@ changed:
'''
# COMMON CODE FOR MIGRATION
import re
import time
import collections
import itertools
import shlex
import ansible.module_utils.nxos
from ansible.module_utils.basic import get_exception
@ -193,200 +181,17 @@ from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.network import NetworkModule
from ansible.module_utils.shell import ShellError
DEFAULT_COMMENT_TOKENS = ['#', '!']
class ConfigLine(object):
def __init__(self, text):
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
return list()
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
class CustomNetworkConfig(NetworkConfig):
def expand_section(self, configobj, S=None):
if S is None:
@ -398,14 +203,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S)
return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path):
for item in self.items:
if item.text == path[-1]:
@ -413,93 +210,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]:
return item
def get_children(self, path):
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try:
if item != current[index]:
updates.append(item)
except IndexError:
updates.append(item)
elif match == 'exact':
if path:
current = other.get_children(path) or list()
else:
current = other.items
if len(current) != len(config):
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def add(self, lines, parents=None):
@ -551,303 +278,44 @@ class CustomNetworkConfig(object):
self.items.append(item)
def argument_spec():
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
def get_network_module(**kwargs):
try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
errors_re=CLI_ERRORS_RE)
self.shell.open(host, port=port, username=username,
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
return get_module(**kwargs)
except NameError:
return NetworkModule(**kwargs)
def send(self, commands, encoding='text'):
try:
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
def get_config(module, include_defaults=False):
config = module.params['config']
if not config:
cmd = 'show running-config'
if module.params['include_defaults']:
cmd += ' all'
if module.params['transport'] == 'nxapi':
config = module.execute([cmd], command_type='cli_show_ascii')[0]
else:
config = module.execute([cmd])[0]
try:
config = module.get_config()
except AttributeError:
defaults = module.params['include_defaults']
config = module.config.get_config(include_defaults=defaults)
return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate):
config = custom_get_config(module)
config = get_config(module)
commands = candidate.difference(config)
commands = [str(c).strip() for c in commands]
save_config = module.params['save_config']
save_config = module.params['save']
result = dict(changed=False)
if commands:
if not module.check_mode:
try:
module.configure(commands)
except AttributeError:
module.config(commands)
if save_config:
try:
module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True
result['updates'] = commands
@ -855,6 +323,7 @@ def load_config(module, candidate):
return result
# END OF COMMON CODE
PARAM_TO_COMMAND_KEYMAP = {
'router_id': 'router-id',
'default_metric': 'default-metric',
@ -912,7 +381,7 @@ def get_value(arg, config, module):
def get_existing(module, args):
existing = {}
netcfg = custom_get_config(module)
netcfg = get_config(module)
parents = ['router ospf {0}'.format(module.params['ospf'])]
if module.params['vrf'] != 'default':
@ -1052,13 +521,13 @@ def main():
timer_throttle_spf_hold=dict(required=False, type='str'),
timer_throttle_spf_max=dict(required=False, type='str'),
auto_cost=dict(required=False, type='str'),
m_facts=dict(required=False, default=False, type='bool'),
state=dict(choices=['present', 'absent'], default='present',
required=False),
include_defaults=dict(default=True)
include_defaults=dict(default=True),
config=dict(),
save=dict(type='bool', default=False)
)
argument_spec.update(nxos_argument_spec)
module = get_module(argument_spec=argument_spec,
module = get_network_module(argument_spec=argument_spec,
supports_check_mode=True)
state = module.params['state']
@ -1111,7 +580,7 @@ def main():
result['updates'] = []
result['connected'] = module.connected
if module.params['m_facts']:
if module._verbosity > 0:
end_state = invoke('get_existing', module, args)
result['end_state'] = end_state
result['existing'] = existing

View file

@ -30,7 +30,7 @@ description:
author: Gabriele Gerbino (@GGabriele)
extends_documentation_fragment: nxos
notes:
- default, where supported, restores params default value
- Default restores params default value
- Supported MAC address format are "E.E.E", "EE-EE-EE-EE-EE-EE",
"EE:EE:EE:EE:EE:EE" and "EEEE.EEEE.EEEE"
options:
@ -39,13 +39,8 @@ options:
- Anycast gateway mac of the switch.
required: true
default: null
m_facts:
description:
- Used to print module facts
required: false
default: false
choices: ['true','false']
'''
EXAMPLES = '''
- nxos_overlay_global:
anycast_gateway_mac: "b.b.b"
@ -57,23 +52,56 @@ EXAMPLES = '''
RETURN = '''
proposed:
description: k/v pairs of parameters passed into module
returned: always
returned: verbose mode
type: dict
sample: {"12:34:56:78:9a:bc"}
sample: {"asn": "65535", "router_id": "1.1.1.1", "vrf": "test"}
existing:
description: k/v pairs of existing configuration
description: k/v pairs of existing BGP configuration
returned: verbose mode
type: dict
sample: {"anycast_gateway_mac": "000E.000E.000E"}
sample: {"asn": "65535", "bestpath_always_compare_med": false,
"bestpath_aspath_multipath_relax": false,
"bestpath_compare_neighborid": false,
"bestpath_compare_routerid": false,
"bestpath_cost_community_ignore": false,
"bestpath_med_confed": false,
"bestpath_med_missing_as_worst": false,
"bestpath_med_non_deterministic": false, "cluster_id": "",
"confederation_id": "", "confederation_peers": "",
"graceful_restart": true, "graceful_restart_helper": false,
"graceful_restart_timers_restart": "120",
"graceful_restart_timers_stalepath_time": "300", "local_as": "",
"log_neighbor_changes": false, "maxas_limit": "",
"neighbor_down_fib_accelerate": false, "reconnect_interval": "60",
"router_id": "11.11.11.11", "suppress_fib_pending": false,
"timer_bestpath_limit": "", "timer_bgp_hold": "180",
"timer_bgp_keepalive": "60", "vrf": "test"}
end_state:
description: k/v pairs of configuration after module execution
returned: always
description: k/v pairs of BGP configuration after module execution
returned: verbose mode
type: dict
sample: {"anycast_gateway_mac": "1234.5678.9ABC"}
sample: {"asn": "65535", "bestpath_always_compare_med": false,
"bestpath_aspath_multipath_relax": false,
"bestpath_compare_neighborid": false,
"bestpath_compare_routerid": false,
"bestpath_cost_community_ignore": false,
"bestpath_med_confed": false,
"bestpath_med_missing_as_worst": false,
"bestpath_med_non_deterministic": false, "cluster_id": "",
"confederation_id": "", "confederation_peers": "",
"graceful_restart": true, "graceful_restart_helper": false,
"graceful_restart_timers_restart": "120",
"graceful_restart_timers_stalepath_time": "300", "local_as": "",
"log_neighbor_changes": false, "maxas_limit": "",
"neighbor_down_fib_accelerate": false, "reconnect_interval": "60",
"router_id": "1.1.1.1", "suppress_fib_pending": false,
"timer_bestpath_limit": "", "timer_bgp_hold": "180",
"timer_bgp_keepalive": "60", "vrf": "test"}
updates:
description: commands sent to the device
returned: always
type: list
sample: ["fabric forwarding anycast-gateway-mac 1234.5678.9ABC"]
sample: ["router bgp 65535", "vrf test", "router-id 1.1.1.1"]
changed:
description: check to see if a change was made on the device
returned: always
@ -82,214 +110,28 @@ changed:
'''
# COMMON CODE FOR MIGRATION
import re
import time
import collections
import itertools
import shlex
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
from ansible.module_utils.netcfg import parse
from ansible.module_utils.urls import fetch_url
from ansible.module_utils.basic import get_exception
from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import ShellError
try:
from ansible.module_utils.nxos import get_module
except ImportError:
from ansible.module_utils.nxos import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!']
class ConfigLine(object):
def __init__(self, text):
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
return list()
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
class CustomNetworkConfig(NetworkConfig):
def expand_section(self, configobj, S=None):
if S is None:
@ -301,14 +143,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S)
return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path):
for item in self.items:
if item.text == path[-1]:
@ -316,93 +150,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]:
return item
def get_children(self, path):
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try:
if item != current[index]:
updates.append(item)
except IndexError:
updates.append(item)
elif match == 'exact':
if path:
current = other.get_children(path) or list()
else:
current = other.items
if len(current) != len(config):
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def add(self, lines, parents=None):
@ -454,303 +218,44 @@ class CustomNetworkConfig(object):
self.items.append(item)
def argument_spec():
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
def get_network_module(**kwargs):
try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
errors_re=CLI_ERRORS_RE)
self.shell.open(host, port=port, username=username,
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
return get_module(**kwargs)
except NameError:
return NetworkModule(**kwargs)
def send(self, commands, encoding='text'):
try:
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
def get_config(module, include_defaults=False):
config = module.params['config']
if not config:
cmd = 'show running-config'
if module.params['include_defaults']:
cmd += ' all'
if module.params['transport'] == 'nxapi':
config = module.execute([cmd], command_type='cli_show_ascii')[0]
else:
config = module.execute([cmd])[0]
try:
config = module.get_config()
except AttributeError:
defaults = module.params['include_defaults']
config = module.config.get_config(include_defaults=defaults)
return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate):
config = custom_get_config(module)
config = get_config(module)
commands = candidate.difference(config)
commands = [str(c).strip() for c in commands]
save_config = module.params['save_config']
save_config = module.params['save']
result = dict(changed=False)
if commands:
if not module.check_mode:
try:
module.configure(commands)
except AttributeError:
module.config(commands)
if save_config:
try:
module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True
result['updates'] = commands
@ -779,7 +284,7 @@ def get_value(arg, config, module):
def get_existing(module, args):
existing = {}
config = str(custom_get_config(module))
config = str(get_config(module))
for arg in args:
existing[arg] = get_value(arg, config, module)
@ -867,10 +372,11 @@ def main():
argument_spec = dict(
anycast_gateway_mac=dict(required=True, type='str'),
m_facts=dict(required=False, default=False, type='bool'),
include_defaults=dict(default=True)
include_defaults=dict(default=True),
config=dict(),
save=dict(type='bool', default=False)
)
argument_spec.update(nxos_argument_spec)
module = get_module(argument_spec=argument_spec,
module = get_network_module(argument_spec=argument_spec,
supports_check_mode=True)
args = [
@ -894,7 +400,7 @@ def main():
module.fail_json(msg=str(exc))
result['connected'] = module.connected
if module.params['m_facts']:
if module._verbosity > 0:
end_state = invoke('get_existing', module, args)
result['end_state'] = end_state
result['existing'] = existing

View file

@ -24,9 +24,9 @@ DOCUMENTATION = '''
---
module: nxos_pim
version_added: "2.2"
short_description: Manages configuration of an Protocol Independent Multicast (PIM) instance.
short_description: Manages configuration of a PIM instance.
description:
- Manages configuration of an Protocol Independent Multicast (PIM) instance.
- Manages configuration of a Protocol Independent Multicast (PIM) instance.
author: Gabriele Gerbino (@GGabriele)
extends_documentation_fragment: nxos
options:
@ -35,12 +35,6 @@ options:
- Configure group ranges for Source Specific Multicast (SSM).
Valid values are multicast addresses or the keyword 'none'.
required: true
m_facts:
description:
- Used to print module facts
required: false
default: false
choices: ['true','false']
'''
EXAMPLES = '''
- nxos_pim:
@ -53,16 +47,17 @@ EXAMPLES = '''
RETURN = '''
proposed:
description: k/v pairs of parameters passed into module
returned: always
returned: verbose mode
type: dict
sample: {"ssm_range": "232.0.0.0/8"}
existing:
description: k/v pairs of existing PIM configuration
returned: verbose mode
type: dict
sample: {"ssm_range": none}
end_state:
description: k/v pairs of BGP configuration after module execution
returned: always
returned: verbose mode
type: dict
sample: {"ssm_range": "232.0.0.0/8"}
updates:
@ -79,214 +74,28 @@ changed:
# COMMON CODE FOR MIGRATION
import re
import time
import collections
import itertools
import shlex
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
from ansible.module_utils.netcfg import parse
from ansible.module_utils.urls import fetch_url
from ansible.module_utils.basic import get_exception
from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import ShellError
try:
from ansible.module_utils.nxos import get_module
except ImportError:
from ansible.module_utils.nxos import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!']
class ConfigLine(object):
def __init__(self, text):
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
return list()
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
class CustomNetworkConfig(NetworkConfig):
def expand_section(self, configobj, S=None):
if S is None:
@ -298,14 +107,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S)
return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path):
for item in self.items:
if item.text == path[-1]:
@ -313,93 +114,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]:
return item
def get_children(self, path):
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try:
if item != current[index]:
updates.append(item)
except IndexError:
updates.append(item)
elif match == 'exact':
if path:
current = other.get_children(path) or list()
else:
current = other.items
if len(current) != len(config):
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def add(self, lines, parents=None):
@ -451,303 +182,44 @@ class CustomNetworkConfig(object):
self.items.append(item)
def argument_spec():
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
def get_network_module(**kwargs):
try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
errors_re=CLI_ERRORS_RE)
self.shell.open(host, port=port, username=username,
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
return get_module(**kwargs)
except NameError:
return NetworkModule(**kwargs)
def send(self, commands, encoding='text'):
try:
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
def get_config(module, include_defaults=False):
config = module.params['config']
if not config:
cmd = 'show running-config'
if module.params['include_defaults']:
cmd += ' all'
if module.params['transport'] == 'nxapi':
config = module.execute([cmd], command_type='cli_show_ascii')[0]
else:
config = module.execute([cmd])[0]
try:
config = module.get_config()
except AttributeError:
defaults = module.params['include_defaults']
config = module.config.get_config(include_defaults=defaults)
return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate):
config = custom_get_config(module)
config = get_config(module)
commands = candidate.difference(config)
commands = [str(c).strip() for c in commands]
save_config = module.params['save_config']
save_config = module.params['save']
result = dict(changed=False)
if commands:
if not module.check_mode:
try:
module.configure(commands)
except AttributeError:
module.config(commands)
if save_config:
try:
module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True
result['updates'] = commands
@ -779,7 +251,7 @@ def get_value(arg, config, module):
def get_existing(module, args):
existing = {}
config = str(custom_get_config(module))
config = str(get_config(module))
for arg in args:
existing[arg] = get_value(arg, config, module)
return existing
@ -815,10 +287,11 @@ def main():
argument_spec = dict(
ssm_range=dict(required=True, type='str'),
m_facts=dict(required=False, default=False, type='bool'),
include_defaults=dict(default=False)
include_defaults=dict(default=False),
config=dict(),
save=dict(type='bool', default=False)
)
argument_spec.update(nxos_argument_spec)
module = get_module(argument_spec=argument_spec,
module = get_network_module(argument_spec=argument_spec,
supports_check_mode=True)
splitted_ssm_range = module.params['ssm_range'].split('.')
@ -847,7 +320,7 @@ def main():
module.fail_json(msg=str(exc))
result['connected'] = module.connected
if module.params['m_facts']:
if module._verbosity > 0:
end_state = invoke('get_existing', module, args)
result['end_state'] = end_state
result['existing'] = existing

View file

@ -24,15 +24,14 @@ DOCUMENTATION = '''
---
module: nxos_pim_rp_address
version_added: "2.2"
short_description: Manages configuration of an Protocol Independent Multicast
(PIM) static rendezvous point (RP) address instance.
short_description: Manages configuration of an PIM static RP address instance.
description:
- Manages configuration of an Protocol Independent Multicast (PIM) static
rendezvous point (RP) address instance.
author: Gabriele Gerbino (@GGabriele)
extends_documentation_fragment: nxos
notes:
- state=absent remove the whole rp-address configuration, if existing.
- C(state=absent) remove the whole rp-address configuration, if existing.
options:
rp_address:
description:
@ -63,12 +62,6 @@ options:
required: false
choices: ['true','false']
default: null
m_facts:
description:
- Used to print module facts
required: false
default: false
choices: ['true','false']
'''
EXAMPLES = '''
- nxos_pim_rp_address:
@ -82,16 +75,17 @@ EXAMPLES = '''
RETURN = '''
proposed:
description: k/v pairs of parameters passed into module
returned: always
returned: verbose mode
type: dict
sample: {"rp_address": "10.1.1.21"}
existing:
description: list of existing pim rp-address configuration entries
returned: verbose mode
type: list
sample: []
end_state:
description: pim rp-address configuration entries after module execution
returned: always
returned: verbose mode
type: list
sample: [{"bidir": false, "group_list": "224.0.0.0/4",
"rp_address": "10.1.1.21"}]
@ -110,214 +104,28 @@ changed:
# COMMON CODE FOR MIGRATION
import re
import time
import collections
import itertools
import shlex
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
from ansible.module_utils.netcfg import parse
from ansible.module_utils.urls import fetch_url
from ansible.module_utils.basic import get_exception
from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import ShellError
try:
from ansible.module_utils.nxos import get_module
except ImportError:
from ansible.module_utils.nxos import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!']
class ConfigLine(object):
def __init__(self, text):
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
return list()
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
class CustomNetworkConfig(NetworkConfig):
def expand_section(self, configobj, S=None):
if S is None:
@ -329,14 +137,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S)
return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path):
for item in self.items:
if item.text == path[-1]:
@ -344,93 +144,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]:
return item
def get_children(self, path):
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try:
if item != current[index]:
updates.append(item)
except IndexError:
updates.append(item)
elif match == 'exact':
if path:
current = other.get_children(path) or list()
else:
current = other.items
if len(current) != len(config):
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def add(self, lines, parents=None):
@ -482,303 +212,44 @@ class CustomNetworkConfig(object):
self.items.append(item)
def argument_spec():
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
def get_network_module(**kwargs):
try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
errors_re=CLI_ERRORS_RE)
self.shell.open(host, port=port, username=username,
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
return get_module(**kwargs)
except NameError:
return NetworkModule(**kwargs)
def send(self, commands, encoding='text'):
try:
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
def get_config(module, include_defaults=False):
config = module.params['config']
if not config:
cmd = 'show running-config'
if module.params['include_defaults']:
cmd += ' all'
if module.params['transport'] == 'nxapi':
config = module.execute([cmd], command_type='cli_show_ascii')[0]
else:
config = module.execute([cmd])[0]
try:
config = module.get_config()
except AttributeError:
defaults = module.params['include_defaults']
config = module.config.get_config(include_defaults=defaults)
return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate):
config = custom_get_config(module)
config = get_config(module)
commands = candidate.difference(config)
commands = [str(c).strip() for c in commands]
save_config = module.params['save_config']
save_config = module.params['save']
result = dict(changed=False)
if commands:
if not module.check_mode:
try:
module.configure(commands)
except AttributeError:
module.config(commands)
if save_config:
try:
module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True
result['updates'] = commands
@ -825,7 +296,7 @@ def get_value(config, module):
def get_existing(module, args):
existing = {}
config = str(custom_get_config(module))
config = str(get_config(module))
existing = get_value(config, module)
return existing
@ -880,13 +351,13 @@ def main():
prefix_list=dict(required=False, type='str'),
route_map=dict(required=False, type='str'),
bidir=dict(required=False, type='bool'),
m_facts=dict(required=False, default=False, type='bool'),
state=dict(choices=['present', 'absent'], default='present',
required=False),
include_defaults=dict(default=False)
include_defaults=dict(default=False),
config=dict(),
save=dict(type='bool', default=False)
)
argument_spec.update(nxos_argument_spec)
module = get_module(argument_spec=argument_spec,
module = get_network_module(argument_spec=argument_spec,
mutually_exclusive=[['group_list', 'route_map'],
['group_list', 'prefix_list'],
['route_map', 'prefix_list']],
@ -929,7 +400,7 @@ def main():
module.fail_json(msg=str(exc))
result['connected'] = module.connected
if module.params['m_facts']:
if module._verbosity > 0:
end_state = invoke('get_existing', module, args)
result['end_state'] = end_state
result['existing'] = existing

View file

@ -24,29 +24,31 @@ DOCUMENTATION = '''
---
module: nxos_ping
version_added: "2.1"
short_description: Tests reachability using ping from Nexus switch
short_description: Tests reachability using ping from Nexus switch.
description:
- Tests reachability using ping from switch to a remote destination
- Tests reachability using ping from switch to a remote destination.
extends_documentation_fragment: nxos
author: Jason Edelman (@jedelman8), Gabriele Gerbino (@GGabriele)
author:
- Jason Edelman (@jedelman8)
- Gabriele Gerbino (@GGabriele)
options:
dest:
description:
- IP address or hostname (resolvable by switch) of remote node
- IP address or hostname (resolvable by switch) of remote node.
required: true
count:
description:
- Number of packets to send
- Number of packets to send.
required: false
default: 2
source:
description:
- Source IP Address
- Source IP Address.
required: false
default: null
vrf:
description:
- Outgoing VRF
- Outgoing VRF.
required: false
default: null
'''
@ -114,216 +116,32 @@ packet_loss:
sample: "0.00%"
'''
# COMMON CODE FOR MIGRATION
import re
import time
import collections
import itertools
import shlex
import json
import collections
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
from ansible.module_utils.netcfg import parse
from ansible.module_utils.urls import fetch_url
# COMMON CODE FOR MIGRATION
import re
from ansible.module_utils.basic import get_exception
from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import ShellError
try:
from ansible.module_utils.nxos import get_module
except ImportError:
from ansible.module_utils.nxos import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!']
class ConfigLine(object):
def __init__(self, text):
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
return list()
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
class CustomNetworkConfig(NetworkConfig):
def expand_section(self, configobj, S=None):
if S is None:
@ -335,14 +153,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S)
return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path):
for item in self.items:
if item.text == path[-1]:
@ -350,93 +160,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]:
return item
def get_children(self, path):
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try:
if item != current[index]:
updates.append(item)
except IndexError:
updates.append(item)
elif match == 'exact':
if path:
current = other.get_children(path) or list()
else:
current = other.items
if len(current) != len(config):
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def add(self, lines, parents=None):
@ -488,303 +228,44 @@ class CustomNetworkConfig(object):
self.items.append(item)
def argument_spec():
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
def get_network_module(**kwargs):
try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
errors_re=CLI_ERRORS_RE)
self.shell.open(host, port=port, username=username,
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
return get_module(**kwargs)
except NameError:
return NetworkModule(**kwargs)
def send(self, commands, encoding='text'):
try:
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
def get_config(module, include_defaults=False):
config = module.params['config']
if not config:
cmd = 'show running-config'
if module.params['include_defaults']:
cmd += ' all'
if module.params['transport'] == 'nxapi':
config = module.execute([cmd], command_type='cli_show_ascii')[0]
else:
config = module.execute([cmd])[0]
try:
config = module.get_config()
except AttributeError:
defaults = module.params['include_defaults']
config = module.config.get_config(include_defaults=defaults)
return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate):
config = custom_get_config(module)
config = get_config(module)
commands = candidate.difference(config)
commands = [str(c).strip() for c in commands]
save_config = module.params['save_config']
save_config = module.params['save']
result = dict(changed=False)
if commands:
if not module.check_mode:
try:
module.configure(commands)
except AttributeError:
module.config(commands)
if save_config:
try:
module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True
result['updates'] = commands
@ -833,6 +314,11 @@ def get_statistics_summary_line(response_as_list):
def execute_show(cmds, module, command_type=None):
command_type_map = {
'cli_show': 'json',
'cli_show_ascii': 'text'
}
try:
if command_type:
response = module.execute(cmds, command_type=command_type)
@ -842,6 +328,19 @@ def execute_show(cmds, module, command_type=None):
clie = get_exception()
module.fail_json(msg='Error sending {0}'.format(cmds),
error=str(clie))
except AttributeError:
try:
if command_type:
command_type = command_type_map.get(command_type)
module.cli.add_commands(cmds, output=command_type)
response = module.cli.run_commands()
else:
module.cli.add_commands(cmds, output=command_type)
response = module.cli.run_commands()
except ShellError:
clie = get_exception()
module.fail_json(msg='Error sending {0}'.format(cmds),
error=str(clie))
return response
@ -885,8 +384,11 @@ def main():
source=dict(required=False),
state=dict(required=False, choices=['present', 'absent'],
default='present'),
include_defaults=dict(default=False),
config=dict(),
save=dict(type='bool', default=False)
)
module = get_module(argument_spec=argument_spec,
module = get_network_module(argument_spec=argument_spec,
supports_check_mode=True)
destination = module.params['dest']

View file

@ -26,7 +26,7 @@ module: nxos_reboot
version_added: 2.2
short_description: Reboot a network device.
description:
- Reboot a network device
- Reboot a network device.
extends_documentation_fragment: nxos
author:
- Jason Edelman (@jedelman8)
@ -58,215 +58,32 @@ rebooted:
sample: true
'''
# COMMON CODE FOR MIGRATION
import re
import time
import json
import collections
import itertools
import shlex
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
from ansible.module_utils.netcfg import parse
from ansible.module_utils.urls import fetch_url
# COMMON CODE FOR MIGRATION
import re
from ansible.module_utils.basic import get_exception
from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import ShellError
try:
from ansible.module_utils.nxos import get_module
except ImportError:
from ansible.module_utils.nxos import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!']
class ConfigLine(object):
def __init__(self, text):
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
return list()
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
class CustomNetworkConfig(NetworkConfig):
def expand_section(self, configobj, S=None):
if S is None:
@ -278,14 +95,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S)
return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path):
for item in self.items:
if item.text == path[-1]:
@ -293,93 +102,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]:
return item
def get_children(self, path):
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try:
if item != current[index]:
updates.append(item)
except IndexError:
updates.append(item)
elif match == 'exact':
if path:
current = other.get_children(path) or list()
else:
current = other.items
if len(current) != len(config):
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def add(self, lines, parents=None):
@ -431,303 +170,44 @@ class CustomNetworkConfig(object):
self.items.append(item)
def argument_spec():
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
def get_network_module(**kwargs):
try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
errors_re=CLI_ERRORS_RE)
self.shell.open(host, port=port, username=username,
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
return get_module(**kwargs)
except NameError:
return NetworkModule(**kwargs)
def send(self, commands, encoding='text'):
try:
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
def get_config(module, include_defaults=False):
config = module.params['config']
if not config:
cmd = 'show running-config'
if module.params['include_defaults']:
cmd += ' all'
if module.params['transport'] == 'nxapi':
config = module.execute([cmd], command_type='cli_show_ascii')[0]
else:
config = module.execute([cmd])[0]
try:
config = module.get_config()
except AttributeError:
defaults = module.params['include_defaults']
config = module.config.get_config(include_defaults=defaults)
return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate):
config = custom_get_config(module)
config = get_config(module)
commands = candidate.difference(config)
commands = [str(c).strip() for c in commands]
save_config = module.params['save_config']
save_config = module.params['save']
result = dict(changed=False)
if commands:
if not module.check_mode:
try:
module.configure(commands)
except AttributeError:
module.config(commands)
if save_config:
try:
module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True
result['updates'] = commands
@ -743,6 +223,11 @@ def reboot(module):
def execute_show(cmds, module, command_type=None):
command_type_map = {
'cli_show': 'json',
'cli_show_ascii': 'text'
}
try:
if command_type:
response = module.execute(cmds, command_type=command_type)
@ -752,12 +237,25 @@ def execute_show(cmds, module, command_type=None):
clie = get_exception()
module.fail_json(msg='Error sending {0}'.format(cmds),
error=str(clie))
except AttributeError:
try:
if command_type:
command_type = command_type_map.get(command_type)
module.cli.add_commands(cmds, output=command_type)
response = module.cli.run_commands()
else:
module.cli.add_commands(cmds, output=command_type)
response = module.cli.run_commands()
except ShellError:
clie = get_exception()
module.fail_json(msg='Error sending {0}'.format(cmds),
error=str(clie))
return response
def execute_show_command(command, module, command_type='cli_show'):
if module.params['transport'] == 'cli':
body = execute_show(command, module, reboot=reboot)
body = execute_show(command, module)
elif module.params['transport'] == 'nxapi':
body = execute_show(command, module, command_type=command_type)
@ -765,16 +263,18 @@ def execute_show_command(command, module, command_type='cli_show'):
def disable_confirmation(module):
command = 'terminal dont-ask'
command = ['terminal dont-ask']
body = execute_show_command(command, module, command_type='cli_show_ascii')[0]
def main():
argument_spec = dict(
confirm=dict(required=True, type='bool'),
include_defaults=dict(default=False),
config=dict(),
save=dict(type='bool', default=False)
)
argument_spec.update(nxos_argument_spec)
module = get_module(argument_spec=argument_spec,
module = get_network_module(argument_spec=argument_spec,
supports_check_mode=True)
confirm = module.params['confirm']

View file

@ -23,24 +23,29 @@ ANSIBLE_METADATA = {'status': ['preview'],
DOCUMENTATION = '''
---
module: nxos_rollback
short_description: Set a checkpoint or rollback to a checkpoint
version_added: "2.2"
short_description: Set a checkpoint or rollback to a checkpoint.
description:
- This module offers the ability to set a configuration checkpoint file or rollback
to a configuration checkpoint file on Cisco NXOS switches-
- This module offers the ability to set a configuration checkpoint
file or rollback to a configuration checkpoint file on Cisco NXOS
switches.
extends_documentation_fragment: nxos
author:
- Jason Edelman (@jedelman8)
- Gabriele Gerbino (@GGabriele)
notes:
- Sometimes C(transport)=nxapi may cause a timeout error.
- Sometimes C(transport=nxapi) may cause a timeout error.
options:
checkpoint_file:
description:
- Name of checkpoint file to create. Mutually exclusive with rollback_to.
- Name of checkpoint file to create. Mutually exclusive
with rollback_to.
required: false
default: null
rollback_to:
description:
- Name of checkpoint file to rollback to. Mutually exclusive with checkpoint_file.
- Name of checkpoint file to rollback to. Mutually exclusive
with checkpoint_file.
required: false
default: null
'''
@ -73,214 +78,28 @@ status:
# COMMON CODE FOR MIGRATION
import re
import time
import collections
import itertools
import shlex
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
from ansible.module_utils.netcfg import parse
from ansible.module_utils.urls import fetch_url
from ansible.module_utils.basic import get_exception
from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import ShellError
try:
from ansible.module_utils.nxos import get_module
except ImportError:
from ansible.module_utils.nxos import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!']
class ConfigLine(object):
def __init__(self, text):
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
return list()
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
class CustomNetworkConfig(NetworkConfig):
def expand_section(self, configobj, S=None):
if S is None:
@ -292,14 +111,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S)
return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path):
for item in self.items:
if item.text == path[-1]:
@ -307,93 +118,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]:
return item
def get_children(self, path):
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try:
if item != current[index]:
updates.append(item)
except IndexError:
updates.append(item)
elif match == 'exact':
if path:
current = other.get_children(path) or list()
else:
current = other.items
if len(current) != len(config):
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def add(self, lines, parents=None):
@ -445,303 +186,44 @@ class CustomNetworkConfig(object):
self.items.append(item)
def argument_spec():
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST', timeout=20)
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
def get_network_module(**kwargs):
try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
errors_re=CLI_ERRORS_RE)
self.shell.open(host, port=port, username=username,
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
return get_module(**kwargs)
except NameError:
return NetworkModule(**kwargs)
def send(self, commands, encoding='text'):
try:
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
def get_config(module, include_defaults=False):
config = module.params['config']
if not config:
cmd = 'show running-config'
if module.params['include_defaults']:
cmd += ' all'
if module.params['transport'] == 'nxapi':
config = module.execute([cmd], command_type='cli_show_ascii')[0]
else:
config = module.execute([cmd])[0]
try:
config = module.get_config()
except AttributeError:
defaults = module.params['include_defaults']
config = module.config.get_config(include_defaults=defaults)
return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate):
config = custom_get_config(module)
config = get_config(module)
commands = candidate.difference(config)
commands = [str(c).strip() for c in commands]
save_config = module.params['save_config']
save_config = module.params['save']
result = dict(changed=False)
if commands:
if not module.check_mode:
try:
module.configure(commands)
except AttributeError:
module.config(commands)
if save_config:
try:
module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True
result['updates'] = commands
@ -751,15 +233,34 @@ def load_config(module, candidate):
def execute_commands(cmds, module, command_type=None):
command_type_map = {
'cli_show': 'json',
'cli_show_ascii': 'text'
}
try:
if command_type:
module.execute(cmds, command_type=command_type)
response = module.execute(cmds, command_type=command_type)
else:
module.execute(cmds)
response = module.execute(cmds)
except ShellError:
clie = get_exception()
module.fail_json(msg='Error sending {0}'.format(cmds),
error=str(clie))
except AttributeError:
try:
if command_type:
command_type = command_type_map.get(command_type)
module.cli.add_commands(cmds, output=command_type)
response = module.cli.run_commands()
else:
module.cli.add_commands(cmds, output=command_type)
response = module.cli.run_commands()
except ShellError:
clie = get_exception()
module.fail_json(msg='Error sending {0}'.format(cmds),
error=str(clie))
return response
def prepare_show_command(command, module):
@ -776,16 +277,27 @@ def checkpoint(filename, module):
def rollback(filename, module):
commands = ['rollback running-config file %s' % filename]
try:
module.configure(commands)
except AttributeError:
try:
module.cli.add_commands(commands, output='config')
module.cli.run_commands()
except ShellError:
clie = get_exception()
module.fail_json(msg='Error sending CLI commands',
error=str(clie), commands=commands)
def main():
argument_spec = dict(
checkpoint_file=dict(required=False),
rollback_to=dict(required=False),
include_defaults=dict(default=True),
config=dict(),
save=dict(type='bool', default=False)
)
argument_spec.update(nxos_argument_spec)
module = get_module(argument_spec=argument_spec,
module = get_network_module(argument_spec=argument_spec,
mutually_exclusive=[['checkpoint_file',
'rollback_to']],
supports_check_mode=False)

View file

@ -32,11 +32,11 @@ author: Gabriele Gerbino (@GGabriele)
notes:
- The module can only activate and commit a package,
not remove or deactivate it.
- Use I(transport)=nxapi to avoid connection timeout
- Use C(transport=nxapi) to avoid connection timeout
options:
pkg:
description:
- Name of the remote package
- Name of the remote package.
required: true
file_system:
description:
@ -81,215 +81,32 @@ changed:
'''
import time
# COMMON CODE FOR MIGRATION
import re
import time
import json
import collections
import itertools
import shlex
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
from ansible.module_utils.netcfg import parse
from ansible.module_utils.urls import fetch_url
# COMMON CODE FOR MIGRATION
import re
from ansible.module_utils.basic import get_exception
from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import ShellError
try:
from ansible.module_utils.nxos import get_module
except ImportError:
from ansible.module_utils.nxos import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!']
class ConfigLine(object):
def __init__(self, text):
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
return list()
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
class CustomNetworkConfig(NetworkConfig):
def expand_section(self, configobj, S=None):
if S is None:
@ -301,14 +118,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S)
return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path):
for item in self.items:
if item.text == path[-1]:
@ -316,93 +125,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]:
return item
def get_children(self, path):
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try:
if item != current[index]:
updates.append(item)
except IndexError:
updates.append(item)
elif match == 'exact':
if path:
current = other.get_children(path) or list()
else:
current = other.items
if len(current) != len(config):
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def add(self, lines, parents=None):
@ -454,303 +193,44 @@ class CustomNetworkConfig(object):
self.items.append(item)
def argument_spec():
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
def get_network_module(**kwargs):
try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
errors_re=CLI_ERRORS_RE)
self.shell.open(host, port=port, username=username,
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
return get_module(**kwargs)
except NameError:
return NetworkModule(**kwargs)
def send(self, commands, encoding='text'):
try:
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
def get_config(module, include_defaults=False):
config = module.params['config']
if not config:
cmd = 'show running-config'
if module.params['include_defaults']:
cmd += ' all'
if module.params['transport'] == 'nxapi':
config = module.execute([cmd], command_type='cli_show_ascii')[0]
else:
config = module.execute([cmd])[0]
try:
config = module.get_config()
except AttributeError:
defaults = module.params['include_defaults']
config = module.config.get_config(include_defaults=defaults)
return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate):
config = custom_get_config(module)
config = get_config(module)
commands = candidate.difference(config)
commands = [str(c).strip() for c in commands]
save_config = module.params['save_config']
save_config = module.params['save']
result = dict(changed=False)
if commands:
if not module.check_mode:
try:
module.configure(commands)
except AttributeError:
module.config(commands)
if save_config:
try:
module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True
result['updates'] = commands
@ -759,6 +239,11 @@ def load_config(module, candidate):
# END OF COMMON CODE
def execute_show(cmds, module, command_type=None):
command_type_map = {
'cli_show': 'json',
'cli_show_ascii': 'text'
}
try:
if command_type:
response = module.execute(cmds, command_type=command_type)
@ -861,8 +346,11 @@ def main():
argument_spec = dict(
pkg=dict(required=True),
file_system=dict(required=False, default='bootflash:'),
include_defaults=dict(default=False),
config=dict(),
save=dict(type='bool', default=False)
)
module = get_module(argument_spec=argument_spec,
module = get_network_module(argument_spec=argument_spec,
supports_check_mode=True)
pkg = module.params['pkg']

View file

@ -1,18 +1,21 @@
#!/usr/bin/env python
#!/usr/bin/python
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#
# Copyright 2015 Jason Edelman <jedelman8@gmail.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
ANSIBLE_METADATA = {'status': ['preview'],
'supported_by': 'community',
@ -26,7 +29,9 @@ short_description: Manages SNMP host configuration.
description:
- Manages SNMP host configuration parameters.
extends_documentation_fragment: nxos
author: Jason Edelman (@jedelman8)
author:
- Jason Edelman (@jedelman8)
- Gabriele Gerbino (@GGabriele)
notes:
- C(state=absent) removes the host configuration if it is configured.
options:

View file

@ -24,9 +24,11 @@ ANSIBLE_METADATA = {'status': ['preview'],
DOCUMENTATION = '''
---
module: nxos_snmp_location
version_added: "2.2"
short_description: Manages SNMP location information.
description:
- Manages SNMP location configuration.
extends_documentation_fragment: nxos
author:
- Jason Edelman (@jedelman8)
- Gabriele Gerbino (@GGabriele)

View file

@ -29,12 +29,13 @@ description:
author: Gabriele Gerbino (@GGabriele)
extends_documentation_fragment: nxos
notes:
- If no vrf is supplied, vrf is set to default
- If state=absent, the route will be removed, regardless of the non-required parameters.
- If no vrf is supplied, vrf is set to default.
- If C(state=absent), the route will be removed, regardless of the
non-required parameters.
options:
prefix:
description:
- Destination prefix of static route
- Destination prefix of static route.
required: true
next_hop:
description:
@ -43,7 +44,7 @@ options:
required: true
vrf:
description:
- VRF for static route
- VRF for static route.
required: false
default: default
tag:
@ -58,20 +59,14 @@ options:
default: null
pref:
description:
- Preference or administrative difference of route (range 1-255)
- Preference or administrative difference of route (range 1-255).
required: false
default: null
state:
description:
- Manage the state of the resource
- Manage the state of the resource.
required: true
choices: ['present','absent']
m_facts:
description:
- Used to print module facts
required: false
default: false
choices: ['true','false']
'''
EXAMPLES = '''
@ -88,18 +83,19 @@ EXAMPLES = '''
RETURN = '''
proposed:
description: k/v pairs of parameters passed into module
returned: always
returned: verbose mode
type: dict
sample: {"next_hop": "3.3.3.3", "pref": "100",
"prefix": "192.168.20.64/24", "route_name": "testing",
"vrf": "default"}
existing:
description: k/v pairs of existing configuration
returned: verbose mode
type: dict
sample: {}
end_state:
description: k/v pairs of configuration after module execution
returned: always
returned: verbose mode
type: dict
sample: {"next_hop": "3.3.3.3", "pref": "100",
"prefix": "192.168.20.0/24", "route_name": "testing",
@ -117,193 +113,42 @@ changed:
'''
# COMMON CODE FOR MIGRATION
import re
import time
import collections
import itertools
import shlex
import ansible.module_utils.nxos
from ansible.module_utils.basic import get_exception
from ansible.module_utils.netcfg import NetworkConfig, ConfigLine, dumps
from ansible.module_utils.network import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!']
class ConfigLine(object):
def __init__(self, text):
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
return list()
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
class CustomNetworkConfig(NetworkConfig):
def expand_section(self, configobj, S=None):
if S is None:
S = list()
S.append(configobj)
for child in configobj.children:
if child in S:
continue
self.expand_section(child, S)
return S
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
def get_object(self, path):
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
@ -324,119 +169,6 @@ class CustomNetworkConfig(object):
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def expand_section(self, configobj, S=None):
if S is None:
S = list()
S.append(configobj)
for child in configobj.children:
if child in S:
continue
self.expand_section(child, S)
return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path):
for item in self.items:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def get_children(self, path):
obj = self.get_object(path)
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try:
if item != current[index]:
updates.append(item)
except IndexError:
updates.append(item)
elif match == 'exact':
if path:
current = other.get_children(path) or list()
else:
current = other.items
if len(current) != len(config):
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
def add(self, lines, parents=None):
"""Adds one or lines of configuration
@ -487,303 +219,44 @@ class CustomNetworkConfig(object):
self.items.append(item)
def argument_spec():
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
def get_network_module(**kwargs):
try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
errors_re=CLI_ERRORS_RE)
self.shell.open(host, port=port, username=username,
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
return get_module(**kwargs)
except NameError:
return NetworkModule(**kwargs)
def send(self, commands, encoding='text'):
try:
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
def get_config(module, include_defaults=False):
config = module.params['config']
if not config:
cmd = 'show running-config'
if module.params['include_defaults']:
cmd += ' all'
if module.params['transport'] == 'nxapi':
config = module.execute([cmd], command_type='cli_show_ascii')[0]
else:
config = module.execute([cmd])[0]
try:
config = module.get_config()
except AttributeError:
defaults = module.params['include_defaults']
config = module.config.get_config(include_defaults=defaults)
return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate):
config = custom_get_config(module)
config = get_config(module)
commands = candidate.difference(config)
commands = [str(c).strip() for c in commands]
save_config = module.params['save_config']
save_config = module.params['save']
result = dict(changed=False)
if commands:
if not module.check_mode:
try:
module.configure(commands)
except AttributeError:
module.config(commands)
if save_config:
try:
module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True
result['updates'] = commands
@ -809,7 +282,7 @@ def state_present(module, candidate, prefix):
def state_absent(module, candidate, prefix):
netcfg = custom_get_config(module)
netcfg = get_config(module)
commands = list()
parents = 'vrf context {0}'.format(module.params['vrf'])
invoke('set_route', module, commands, prefix)
@ -828,17 +301,13 @@ def state_absent(module, candidate, prefix):
def fix_prefix_to_regex(prefix):
prefix = prefix.split('.')
prefix = '\.'.join(prefix)
prefix = prefix.split('/')
prefix = '\/'.join(prefix)
prefix = prefix.replace('.', '\.').replace('/', '\/')
return prefix
def get_existing(module, prefix, warnings):
key_map = ['tag', 'pref', 'route_name', 'next_hop']
netcfg = custom_get_config(module)
netcfg = get_config(module)
parents = 'vrf context {0}'.format(module.params['vrf'])
prefix_to_regex = fix_prefix_to_regex(prefix)
@ -952,16 +421,17 @@ def main():
tag=dict(type='str'),
route_name=dict(type='str'),
pref=dict(type='str'),
m_facts=dict(required=False, default=False, type='bool'),
state=dict(choices=['absent', 'present'],
default='present'),
include_defaults=dict(default=True)
include_defaults=dict(default=True),
config=dict(),
save=dict(type='bool', default=False)
)
argument_spec.update(nxos_argument_spec)
module = get_module(argument_spec=argument_spec,
module = get_network_module(argument_spec=argument_spec,
supports_check_mode=True)
m_facts = module.params['m_facts']
state = module.params['state']
result = dict(changed=False)
@ -981,16 +451,15 @@ def main():
try:
response = load_config(module, candidate)
result.update(response)
except ShellError:
except Exception:
exc = get_exception()
module.fail_json(msg=str(exc))
else:
result['updates'] = []
result['warnings'] = warnings
result['connected'] = module.connected
if module.params['m_facts']:
if module._verbosity > 0:
end_state = invoke('get_existing', module, prefix, warnings)
result['end_state'] = end_state
result['existing'] = existing

View file

@ -145,8 +145,8 @@ end_state:
updates:
description: command string sent to the device
returned: always
type: string
sample: "interface eth1/5 ; switchport access vlan 20 ;"
type: list
sample: ["interface eth1/5", "switchport access vlan 20"]
changed:
description: check to see if a change was made on the device
returned: always

View file

@ -13,7 +13,7 @@
# 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/> .
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#
ANSIBLE_METADATA = {'status': ['preview'],
@ -24,51 +24,51 @@ DOCUMENTATION = '''
---
module: nxos_vlan
version_added: "2.1"
short_description: Manages VLAN resources and attributes
short_description: Manages VLAN resources and attributes.
description:
- Manages VLAN configurations on NX-OS switches
- Manages VLAN configurations on NX-OS switches.
author: Jason Edelman (@jedelman8)
extends_documentation_fragment: nxos
options:
vlan_id:
description:
- single vlan id
- Single VLAN ID.
required: false
default: null
vlan_range:
description:
- range of VLANs such as 2-10 or 2,5,10-15, etc.
- Range of VLANs such as 2-10 or 2,5,10-15, etc.
required: false
default: null
name:
description:
- name of VLAN
- Name of VLAN.
required: false
default: null
vlan_state:
description:
- Manage the vlan operational state of the VLAN
(equivalent to state {active | suspend} command
(equivalent to state {active | suspend} command.
required: false
default: active
choices: ['active','suspend']
admin_state:
description:
- Manage the vlan admin state of the VLAN equivalent
to shut/no shut in vlan config mode
- Manage the VLAN administrative state of the VLAN equivalent
to shut/no shut in VLAN config mode.
required: false
default: up
choices: ['up','down']
mapped_vni:
description:
- The Virtual Network Identifier (VNI) id that is mapped to the
- The Virtual Network Identifier (VNI) ID that is mapped to the
VLAN. Valid values are integer and keyword 'default'.
required: false
default: null
version_added: "2.2"
state:
description:
- Manage the state of the resource
- Manage the state of the resource.
required: false
default: present
choices: ['present','absent']
@ -141,11 +141,6 @@ end_state:
type: dict or null
sample: {"admin_state": "down", "name": "app_vlan", "vlan_id": "20",
"vlan_state": "suspend", "mapped_vni": "5000"}
state:
description: state as sent in from the playbook
returned: always
type: string
sample: "present"
updates:
description: command string sent to the device
returned: always
@ -159,216 +154,32 @@ changed:
'''
# COMMON CODE FOR MIGRATION
import re
import time
import collections
import itertools
import shlex
import json
import collections
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
from ansible.module_utils.netcfg import parse
from ansible.module_utils.urls import fetch_url
# COMMON CODE FOR MIGRATION
import re
from ansible.module_utils.basic import get_exception
from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import ShellError
try:
from ansible.module_utils.nxos import get_module
except ImportError:
from ansible.module_utils.nxos import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!']
class ConfigLine(object):
def __init__(self, text):
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
return list()
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
class CustomNetworkConfig(NetworkConfig):
def expand_section(self, configobj, S=None):
if S is None:
@ -380,14 +191,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S)
return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path):
for item in self.items:
if item.text == path[-1]:
@ -395,93 +198,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]:
return item
def get_children(self, path):
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try:
if item != current[index]:
updates.append(item)
except IndexError:
updates.append(item)
elif match == 'exact':
if path:
current = other.get_children(path) or list()
else:
current = other.items
if len(current) != len(config):
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def add(self, lines, parents=None):
@ -533,303 +266,44 @@ class CustomNetworkConfig(object):
self.items.append(item)
def argument_spec():
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
def get_network_module(**kwargs):
try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
errors_re=CLI_ERRORS_RE)
self.shell.open(host, port=port, username=username,
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
return get_module(**kwargs)
except NameError:
return NetworkModule(**kwargs)
def send(self, commands, encoding='text'):
try:
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
def get_config(module, include_defaults=False):
config = module.params['config']
if not config:
cmd = 'show running-config'
if module.params['include_defaults']:
cmd += ' all'
if module.params['transport'] == 'nxapi':
config = module.execute([cmd], command_type='cli_show_ascii')[0]
else:
config = module.execute([cmd])[0]
try:
config = module.get_config()
except AttributeError:
defaults = module.params['include_defaults']
config = module.config.get_config(include_defaults=defaults)
return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate):
config = custom_get_config(module)
config = get_config(module)
commands = candidate.difference(config)
commands = [str(c).strip() for c in commands]
save_config = module.params['save_config']
save_config = module.params['save']
result = dict(changed=False)
if commands:
if not module.check_mode:
try:
module.configure(commands)
except AttributeError:
module.config(commands)
if save_config:
try:
module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True
result['updates'] = commands
@ -1035,6 +509,11 @@ def get_cli_body_ssh(command, response, module):
def execute_show(cmds, module, command_type=None):
command_type_map = {
'cli_show': 'json',
'cli_show_ascii': 'text'
}
try:
if command_type:
response = module.execute(cmds, command_type=command_type)
@ -1042,7 +521,7 @@ def execute_show(cmds, module, command_type=None):
response = module.execute(cmds)
except ShellError:
clie = get_exception()
module.fail_json(msg='Error sending {0}'.format(command),
module.fail_json(msg='Error sending {0}'.format(cmds),
error=str(clie))
except AttributeError:
try:
@ -1084,8 +563,11 @@ def main():
state=dict(choices=['present', 'absent'], default='present',
required=False),
admin_state=dict(choices=['up', 'down'], required=False),
include_defaults=dict(default=False),
config=dict(),
save=dict(type='bool', default=False)
)
module = get_module(argument_spec=argument_spec,
module = get_network_module(argument_spec=argument_spec,
mutually_exclusive=[['vlan_range', 'name'],
['vlan_id', 'vlan_range']],
supports_check_mode=True)

View file

@ -149,215 +149,28 @@ import json
import collections
# COMMON CODE FOR MIGRATION
import re
import time
import collections
import itertools
import shlex
import json
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
from ansible.module_utils.netcfg import parse
from ansible.module_utils.urls import fetch_url
from ansible.module_utils.basic import get_exception
from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import ShellError
try:
from ansible.module_utils.nxos import get_module
except ImportError:
from ansible.module_utils.nxos import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!']
class ConfigLine(object):
def __init__(self, text):
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
return list()
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
class CustomNetworkConfig(NetworkConfig):
def expand_section(self, configobj, S=None):
if S is None:
@ -369,14 +182,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S)
return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path):
for item in self.items:
if item.text == path[-1]:
@ -384,93 +189,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]:
return item
def get_children(self, path):
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try:
if item != current[index]:
updates.append(item)
except IndexError:
updates.append(item)
elif match == 'exact':
if path:
current = other.get_children(path) or list()
else:
current = other.items
if len(current) != len(config):
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def add(self, lines, parents=None):
@ -522,303 +257,44 @@ class CustomNetworkConfig(object):
self.items.append(item)
def argument_spec():
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
def get_network_module(**kwargs):
try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
errors_re=CLI_ERRORS_RE)
self.shell.open(host, port=port, username=username,
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
return get_module(**kwargs)
except NameError:
return NetworkModule(**kwargs)
def send(self, commands, encoding='text'):
try:
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
def get_config(module, include_defaults=False):
config = module.params['config']
if not config:
cmd = 'show running-config'
if module.params['include_defaults']:
cmd += ' all'
if module.params['transport'] == 'nxapi':
config = module.execute([cmd], command_type='cli_show_ascii')[0]
else:
config = module.execute([cmd])[0]
try:
config = module.get_config()
except AttributeError:
defaults = module.params['include_defaults']
config = module.config.get_config(include_defaults=defaults)
return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate):
config = custom_get_config(module)
config = get_config(module)
commands = candidate.difference(config)
commands = [str(c).strip() for c in commands]
save_config = module.params['save_config']
save_config = module.params['save']
result = dict(changed=False)
if commands:
if not module.check_mode:
try:
module.configure(commands)
except AttributeError:
module.config(commands)
if save_config:
try:
module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True
result['updates'] = commands
@ -865,6 +341,11 @@ def get_cli_body_ssh(command, response, module):
def execute_show(cmds, module, command_type=None):
command_type_map = {
'cli_show': 'json',
'cli_show_ascii': 'text'
}
try:
if command_type:
response = module.execute(cmds, command_type=command_type)
@ -1092,9 +573,11 @@ def main():
auto_recovery=dict(required=True, type='bool'),
delay_restore=dict(required=False, type='str'),
state=dict(choices=['absent', 'present'], default='present'),
include_defaults=dict(default=False)
include_defaults=dict(default=False),
config=dict(),
save=dict(type='bool', default=False)
)
module = get_module(argument_spec=argument_spec,
module = get_network_module(argument_spec=argument_spec,
supports_check_mode=True)
domain = module.params['domain']

View file

@ -41,11 +41,11 @@ notes:
options:
portchannel:
description:
- group number of the portchannel that will be configured
- Group number of the portchannel that will be configured.
required: true
vpc:
description:
- vpc group/id that will be configured on associated portchannel
- VPC group/id that will be configured on associated portchannel.
required: false
default: null
peer_link:
@ -55,7 +55,7 @@ options:
default: null
state:
description:
- Manages desired state of the resource
- Manages desired state of the resource.
required: true
choices: ['present','absent']
'''
@ -96,216 +96,33 @@ changed:
sample: true
'''
# COMMON CODE FOR MIGRATION
import re
import time
import collections
import itertools
import shlex
import json
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
from ansible.module_utils.netcfg import parse
from ansible.module_utils.urls import fetch_url
# COMMON CODE FOR MIGRATION
import re
from ansible.module_utils.basic import get_exception
from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import ShellError
try:
from ansible.module_utils.nxos import get_module
except ImportError:
from ansible.module_utils.nxos import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!']
class ConfigLine(object):
def __init__(self, text):
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
return list()
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
class CustomNetworkConfig(NetworkConfig):
def expand_section(self, configobj, S=None):
if S is None:
@ -317,14 +134,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S)
return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path):
for item in self.items:
if item.text == path[-1]:
@ -332,93 +141,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]:
return item
def get_children(self, path):
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try:
if item != current[index]:
updates.append(item)
except IndexError:
updates.append(item)
elif match == 'exact':
if path:
current = other.get_children(path) or list()
else:
current = other.items
if len(current) != len(config):
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def add(self, lines, parents=None):
@ -470,303 +209,44 @@ class CustomNetworkConfig(object):
self.items.append(item)
def argument_spec():
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
def get_network_module(**kwargs):
try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
errors_re=CLI_ERRORS_RE)
self.shell.open(host, port=port, username=username,
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
return get_module(**kwargs)
except NameError:
return NetworkModule(**kwargs)
def send(self, commands, encoding='text'):
try:
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
def get_config(module, include_defaults=False):
config = module.params['config']
if not config:
cmd = 'show running-config'
if module.params['include_defaults']:
cmd += ' all'
if module.params['transport'] == 'nxapi':
config = module.execute([cmd], command_type='cli_show_ascii')[0]
else:
config = module.execute([cmd])[0]
try:
config = module.get_config()
except AttributeError:
defaults = module.params['include_defaults']
config = module.config.get_config(include_defaults=defaults)
return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate):
config = custom_get_config(module)
config = get_config(module)
commands = candidate.difference(config)
commands = [str(c).strip() for c in commands]
save_config = module.params['save_config']
save_config = module.params['save']
result = dict(changed=False)
if commands:
if not module.check_mode:
try:
module.configure(commands)
except AttributeError:
module.config(commands)
if save_config:
try:
module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True
result['updates'] = commands
@ -776,7 +256,7 @@ def load_config(module, candidate):
def execute_config_command(commands, module):
try:
output = module.configure(commands)
response = module.configure(commands)
except ShellError:
clie = get_exception()
module.fail_json(msg='Error sending CLI commands',
@ -812,6 +292,11 @@ def get_cli_body_ssh(command, response, module):
def execute_show(cmds, module, command_type=None):
command_type_map = {
'cli_show': 'json',
'cli_show_ascii': 'text'
}
try:
if command_type:
response = module.execute(cmds, command_type=command_type)
@ -996,8 +481,11 @@ def main():
vpc=dict(required=False, type='str'),
peer_link=dict(required=False, type='bool'),
state=dict(choices=['absent', 'present'], default='present'),
include_defaults=dict(default=False),
config=dict(),
save=dict(type='bool', default=False)
)
module = get_module(argument_spec=argument_spec,
module = get_network_module(argument_spec=argument_spec,
mutually_exclusive=[['vpc', 'peer_link']],
supports_check_mode=True)

View file

@ -24,9 +24,9 @@ DOCUMENTATION = '''
---
module: nxos_vrf
version_added: "2.1"
short_description: Manages global VRF configuration
short_description: Manages global VRF configuration.
description:
- Manages global VRF configuration
- Manages global VRF configuration.
extends_documentation_fragment: nxos
author:
- Jason Edelman (@jedelman8)
@ -34,20 +34,20 @@ author:
notes:
- Cisco NX-OS creates the default VRF by itself. Therefore,
you're not allowed to use default as I(vrf) name in this module.
- I(vrf) name must be shorter than 32 chars.
- C(vrf) name must be shorter than 32 chars.
- VRF names are not case sensible in NX-OS. Anyway, the name is stored
just like it's inserted by the user and it'll not be changed again
unless the VRF is removed and re-created. i.e. I(vrf=NTC) will create
a VRF named NTC, but running it again with I(vrf=ntc) will not cause
unless the VRF is removed and re-created. i.e. C(vrf=NTC) will create
a VRF named NTC, but running it again with C(vrf=ntc) will not cause
a configuration change.
options:
vrf:
description:
- Name of VRF to be managed
- Name of VRF to be managed.
required: true
admin_state:
description:
- Administrative state of the VRF
- Administrative state of the VRF.
required: false
default: up
choices: ['up','down']
@ -68,13 +68,13 @@ options:
version_added: "2.2"
state:
description:
- Manages desired state of the resource
- Manages desired state of the resource.
required: false
default: present
choices: ['present','absent']
description:
description:
- Description of the VRF
- Description of the VRF.
required: false
default: null
'''
@ -121,13 +121,7 @@ changed:
import json
# COMMON CODE FOR MIGRATION
import re
import time
import collections
import itertools
import shlex
import json
import ansible.module_utils.nxos
from ansible.module_utils.basic import get_exception
@ -135,200 +129,17 @@ from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import ShellError
from ansible.module_utils.network import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!']
class ConfigLine(object):
def __init__(self, text):
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
return list()
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
class CustomNetworkConfig(NetworkConfig):
def expand_section(self, configobj, S=None):
if S is None:
@ -340,14 +151,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S)
return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path):
for item in self.items:
if item.text == path[-1]:
@ -355,93 +158,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]:
return item
def get_children(self, path):
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try:
if item != current[index]:
updates.append(item)
except IndexError:
updates.append(item)
elif match == 'exact':
if path:
current = other.get_children(path) or list()
else:
current = other.items
if len(current) != len(config):
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def add(self, lines, parents=None):
@ -493,303 +226,44 @@ class CustomNetworkConfig(object):
self.items.append(item)
def argument_spec():
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
def get_network_module(**kwargs):
try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
errors_re=CLI_ERRORS_RE)
self.shell.open(host, port=port, username=username,
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
return get_module(**kwargs)
except NameError:
return NetworkModule(**kwargs)
def send(self, commands, encoding='text'):
try:
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
def get_config(module, include_defaults=False):
config = module.params['config']
if not config:
cmd = 'show running-config'
if module.params['include_defaults']:
cmd += ' all'
if module.params['transport'] == 'nxapi':
config = module.execute([cmd], command_type='cli_show_ascii')[0]
else:
config = module.execute([cmd])[0]
try:
config = module.get_config()
except AttributeError:
defaults = module.params['include_defaults']
config = module.config.get_config(include_defaults=defaults)
return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate):
config = custom_get_config(module)
config = get_config(module)
commands = candidate.difference(config)
commands = [str(c).strip() for c in commands]
save_config = module.params['save_config']
save_config = module.params['save']
result = dict(changed=False)
if commands:
if not module.check_mode:
try:
module.configure(commands)
except AttributeError:
module.config(commands)
if save_config:
try:
module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True
result['updates'] = commands
@ -834,6 +308,11 @@ def get_cli_body_ssh_vrf(module, command, response):
def execute_show(cmds, module, command_type=None):
command_type_map = {
'cli_show': 'json',
'cli_show_ascii': 'text'
}
try:
if command_type:
response = module.execute(cmds, command_type=command_type)
@ -974,8 +453,11 @@ def main():
required=False),
state=dict(default='present', choices=['present', 'absent'],
required=False),
include_defaults=dict(default=False),
config=dict(),
save=dict(type='bool', default=False)
)
module = get_module(argument_spec=argument_spec,
module = get_network_module(argument_spec=argument_spec,
supports_check_mode=True)
vrf = module.params['vrf']

View file

@ -22,16 +22,15 @@ ANSIBLE_METADATA = {'status': ['preview'],
DOCUMENTATION = '''
---
module: nxos_vxlan_vtep_vni
module: nxos_vrf_af
version_added: "2.2"
short_description: Creates a Virtual Network Identifier member (VNI)
short_description: Manages VRF AF.
description:
- Creates a Virtual Network Identifier member (VNI) for an NVE
overlay interface.
- Manages VRF AF
author: Gabriele Gerbino (@GGabriele)
extends_documentation_fragment: nxos
notes:
- default, where supported, restores params default value
- Default, where supported, restores params default value.
options:
vrf:
description:
@ -58,16 +57,11 @@ options:
default: null
state:
description:
- Determines whether the config should be present or not on the device.
- Determines whether the config should be present or
not on the device.
required: false
default: present
choices: ['present','absent']
m_facts:
description:
- Used to print module facts
required: false
default: false
choices: ['true','false']
'''
EXAMPLES = '''
- nxos_vrf_af:
@ -78,22 +72,22 @@ EXAMPLES = '''
password: "{{ pwd }}"
host: "{{ inventory_hostname }}"
'''
RETURN = '''
proposed:
description: k/v pairs of parameters passed into module
returned: always
returned: verbose mode
type: dict
sample: {"afi": "ipv4", "route_target_both_auto_evpn": true,
"safi": "unicast", "vrf": "test"}
existing:
description: k/v pairs of existing configuration
returned: verbose mode
type: dict
sample: {"afi": "ipv4", "route_target_both_auto_evpn": false,
"safi": "unicast", "vrf": "test"}
end_state:
description: k/v pairs of configuration after module execution
returned: always
returned: verbose mode
type: dict
sample: {"afi": "ipv4", "route_target_both_auto_evpn": true,
"safi": "unicast", "vrf": "test"}
@ -111,214 +105,28 @@ changed:
'''
# COMMON CODE FOR MIGRATION
import re
import time
import collections
import itertools
import shlex
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
from ansible.module_utils.netcfg import parse
from ansible.module_utils.urls import fetch_url
from ansible.module_utils.basic import get_exception
from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import ShellError
try:
from ansible.module_utils.nxos import get_module
except ImportError:
from ansible.module_utils.nxos import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!']
class ConfigLine(object):
def __init__(self, text):
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
return list()
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
class CustomNetworkConfig(NetworkConfig):
def expand_section(self, configobj, S=None):
if S is None:
@ -330,14 +138,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S)
return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path):
for item in self.items:
if item.text == path[-1]:
@ -345,93 +145,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]:
return item
def get_children(self, path):
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try:
if item != current[index]:
updates.append(item)
except IndexError:
updates.append(item)
elif match == 'exact':
if path:
current = other.get_children(path) or list()
else:
current = other.items
if len(current) != len(config):
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def add(self, lines, parents=None):
@ -483,303 +213,44 @@ class CustomNetworkConfig(object):
self.items.append(item)
def argument_spec():
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
def get_network_module(**kwargs):
try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
errors_re=CLI_ERRORS_RE)
self.shell.open(host, port=port, username=username,
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
return get_module(**kwargs)
except NameError:
return NetworkModule(**kwargs)
def send(self, commands, encoding='text'):
try:
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
def get_config(module, include_defaults=False):
config = module.params['config']
if not config:
cmd = 'show running-config'
if module.params['include_defaults']:
cmd += ' all'
if module.params['transport'] == 'nxapi':
config = module.execute([cmd], command_type='cli_show_ascii')[0]
else:
config = module.execute([cmd])[0]
try:
config = module.get_config()
except AttributeError:
defaults = module.params['include_defaults']
config = module.config.get_config(include_defaults=defaults)
return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate):
config = custom_get_config(module)
config = get_config(module)
commands = candidate.difference(config)
commands = [str(c).strip() for c in commands]
save_config = module.params['save_config']
save_config = module.params['save']
result = dict(changed=False)
if commands:
if not module.check_mode:
try:
module.configure(commands)
except AttributeError:
module.config(commands)
if save_config:
try:
module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True
result['updates'] = commands
@ -819,7 +290,7 @@ def get_value(arg, config, module):
def get_existing(module, args):
existing = {}
netcfg = custom_get_config(module)
netcfg = get_config(module)
parents = ['vrf context {0}'.format(module.params['vrf'])]
parents.append('address-family {0} {1}'.format(module.params['afi'],
@ -903,10 +374,11 @@ def main():
m_facts=dict(required=False, default=False, type='bool'),
state=dict(choices=['present', 'absent'], default='present',
required=False),
include_defaults=dict(default=False)
include_defaults=dict(default=False),
config=dict(),
save=dict(type='bool', default=False)
)
argument_spec.update(nxos_argument_spec)
module = get_module(argument_spec=argument_spec,
module = get_network_module(argument_spec=argument_spec,
supports_check_mode=True)
state = module.params['state']
@ -948,7 +420,7 @@ def main():
result['updates'] = []
result['connected'] = module.connected
if module.params['m_facts']:
if module._verbosity > 0:
end_state = invoke('get_existing', module, args)
result['end_state'] = end_state
result['existing'] = existing

View file

@ -24,30 +24,32 @@ DOCUMENTATION = '''
---
module: nxos_vrf_interface
version_added: "2.1"
short_description: Manages interface specific VRF configuration
short_description: Manages interface specific VRF configuration.
description:
- Manages interface specific VRF configuration
- Manages interface specific VRF configuration.
extends_documentation_fragment: nxos
author: Jason Edelman (@jedelman8), Gabriele Gerbino (@GGabriele)
author:
- Jason Edelman (@jedelman8)
- Gabriele Gerbino (@GGabriele)
notes:
- VRF needs to be added globally with M(nxos_vrf) before
adding a VRF to an interface
adding a VRF to an interface.
- Remove a VRF from an interface will still remove
all L3 attributes just as it does from CLI
all L3 attributes just as it does from CLI.
- VRF is not read from an interface until IP address is
configured on that interface
configured on that interface.
options:
vrf:
description:
- Name of VRF to be managed
- Name of VRF to be managed.
required: true
interface:
description:
- Full name of interface to be managed, i.e. Ethernet1/1
- Full name of interface to be managed, i.e. Ethernet1/1.
required: true
state:
description:
- Manages desired state of the resource
- Manages desired state of the resource.
required: false
default: present
choices: ['present','absent']
@ -96,216 +98,32 @@ changed:
sample: true
'''
# COMMON CODE FOR MIGRATION
import re
import time
import collections
import itertools
import shlex
import json
import collections
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
from ansible.module_utils.netcfg import parse
from ansible.module_utils.urls import fetch_url
# COMMON CODE FOR MIGRATION
import re
from ansible.module_utils.basic import get_exception
from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import ShellError
try:
from ansible.module_utils.nxos import get_module
except ImportError:
from ansible.module_utils.nxos import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!']
class ConfigLine(object):
def __init__(self, text):
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
return list()
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
class CustomNetworkConfig(NetworkConfig):
def expand_section(self, configobj, S=None):
if S is None:
@ -317,14 +135,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S)
return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path):
for item in self.items:
if item.text == path[-1]:
@ -332,93 +142,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]:
return item
def get_children(self, path):
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
return self.to_block(section)
except ValueError:
return list()
def get_section_objects(self, path):
if not isinstance(path, list):
path = [path]
obj = self.get_object(path)
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try:
if item != current[index]:
updates.append(item)
except IndexError:
updates.append(item)
elif match == 'exact':
if path:
current = other.get_children(path) or list()
else:
current = other.items
if len(current) != len(config):
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def add(self, lines, parents=None):
@ -470,303 +210,44 @@ class CustomNetworkConfig(object):
self.items.append(item)
def argument_spec():
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
def get_network_module(**kwargs):
try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
errors_re=CLI_ERRORS_RE)
self.shell.open(host, port=port, username=username,
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
return get_module(**kwargs)
except NameError:
return NetworkModule(**kwargs)
def send(self, commands, encoding='text'):
try:
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
def get_config(module, include_defaults=False):
config = module.params['config']
if not config:
cmd = 'show running-config'
if module.params['include_defaults']:
cmd += ' all'
if module.params['transport'] == 'nxapi':
config = module.execute([cmd], command_type='cli_show_ascii')[0]
else:
config = module.execute([cmd])[0]
try:
config = module.get_config()
except AttributeError:
defaults = module.params['include_defaults']
config = module.config.get_config(include_defaults=defaults)
return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate):
config = custom_get_config(module)
config = get_config(module)
commands = candidate.difference(config)
commands = [str(c).strip() for c in commands]
save_config = module.params['save_config']
save_config = module.params['save']
result = dict(changed=False)
if commands:
if not module.check_mode:
try:
module.configure(commands)
except AttributeError:
module.config(commands)
if save_config:
try:
module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True
result['updates'] = commands
@ -810,6 +291,11 @@ def get_cli_body_ssh_vrf_interface(command, response, module):
def execute_show(cmds, module, command_type=None):
command_type_map = {
'cli_show': 'json',
'cli_show_ascii': 'text'
}
try:
if command_type:
response = module.execute(cmds, command_type=command_type)
@ -817,7 +303,7 @@ def execute_show(cmds, module, command_type=None):
response = module.execute(cmds)
except ShellError:
clie = get_exception()
module.fail_json(msg='Error sending {0}'.format(command),
module.fail_json(msg='Error sending {0}'.format(cmds),
error=str(clie))
except AttributeError:
try:
@ -936,8 +422,11 @@ def main():
interface=dict(type='str', required=True),
state=dict(default='present', choices=['present', 'absent'],
required=False),
include_defaults=dict(default=False),
config=dict(),
save=dict(type='bool', default=False)
)
module = get_module(argument_spec=argument_spec,
module = get_network_module(argument_spec=argument_spec,
supports_check_mode=True)
vrf = module.params['vrf']

View file

@ -25,53 +25,53 @@ DOCUMENTATION = '''
---
module: nxos_vrrp
version_added: "2.1"
short_description: Manages VRRP configuration on NX-OS switches
short_description: Manages VRRP configuration on NX-OS switches.
description:
- Manages VRRP configuration on NX-OS switches
- Manages VRRP configuration on NX-OS switches.
extends_documentation_fragment: nxos
author:
- Jason Edelman (@jedelman8)
- Gabriele Gerbino (@GGabriele)
notes:
- VRRP feature needs to be enabled first on the system
- SVIs must exist before using this module
- Interface must be a L3 port before using this module
- I(state)=absent removes the vrrp group if it exists on the device
- VRRP cannot be configured on loopback interfaces
- VRRP feature needs to be enabled first on the system.
- SVIs must exist before using this module.
- Interface must be a L3 port before using this module.
- C(state=absent) removes the VRRP group if it exists on the device.
- VRRP cannot be configured on loopback interfaces.
options:
group:
description:
- VRRP group number
- VRRP group number.
required: true
interface:
description:
- Full name of interface that is being managed for VRRP
- Full name of interface that is being managed for VRRP.
required: true
priority:
description:
- VRRP priority
- VRRP priority.
required: false
default: null
vip:
description:
- VRRP virtual IP address
- VRRP virtual IP address.
required: false
default: null
authentication:
description:
- clear text authentication string
- Clear text authentication string.
required: false
default: null
admin_state:
description:
- Used to enable or disable the VRRP process
- Used to enable or disable the VRRP process.
required: false
choices: ['shutdown', 'no shutdown']
default: no shutdown
version_added: "2.2"
state:
description:
- Specify desired state of the resource
- Specify desired state of the resource.
required: false
default: present
choices: ['present','absent']

View file

@ -25,9 +25,9 @@ DOCUMENTATION = '''
module: nxos_vtp
version_added: "2.2"
short_description: Manages VTP configuration.
short_description: Manages VTP password configuration.
description:
- Manages VTP configuration
- Manages VTP password configuration.
extends_documentation_fragment: nxos
author:
- Gabriele Gerbino (@GGabriele)

View file

@ -24,7 +24,7 @@ DOCUMENTATION = '''
---
module: nxos_vxlan_vtep
version_added: "2.2"
short_description: Manages VXLAN Network Virtualization Endpoint (NVE)
short_description: Manages VXLAN Network Virtualization Endpoint (NVE).
description:
- Manages VXLAN Network Virtualization Endpoint (NVE) overlay interface
that terminates VXLAN tunnels.
@ -32,13 +32,13 @@ author: Gabriele Gerbino (@GGabriele)
extends_documentation_fragment: nxos
notes:
- The module is used to manage NVE properties, not to create NVE
interfaces. Use nxos_interface if you wish to do so.
- State 'absent' removes the interface
- default, where supported, restores params default value
interfaces. Use M(nxos_interface) if you wish to do so.
- C(state=absent) removes the interface.
- Default, where supported, restores params default value.
options:
interface:
description:
- Interface name for the VXLAN Network Virtualization Endpoint
- Interface name for the VXLAN Network Virtualization Endpoint.
required: true
description:
description:
@ -71,7 +71,8 @@ options:
default: null
state:
description:
- Determines whether the config should be present or not on the device.
- Determines whether the config should be present or not
on the device.
required: false
default: present
choices: ['present','absent']

View file

@ -31,11 +31,11 @@ description:
author: Gabriele Gerbino (@GGabriele)
extends_documentation_fragment: nxos
notes:
- default, where supported, restores params default value
- default, where supported, restores params default value.
options:
interface:
description:
- Interface name for the VXLAN Network Virtualization Endpoint
- Interface name for the VXLAN Network Virtualization Endpoint.
required: true
vni:
description:
@ -77,7 +77,8 @@ options:
default: null
state:
description:
- Determines whether the config should be present or not on the device.
- Determines whether the config should be present or not
on the device.
required: false
default: present
choices: ['present','absent']
@ -489,7 +490,6 @@ def main():
suppress_arp=dict(required=False, type='bool'),
ingress_replication=dict(required=False, type='str',
choices=['bgp', 'static', 'default']),
m_facts=dict(required=False, default=False, type='bool'),
state=dict(choices=['present', 'absent'], default='present',
required=False),
include_defaults=dict(default=True),