refactors junos modules to support persistent socket connections (#21365)

* updates junos_netconf module
* updates junos_command module
* updates junos_config module
* updates _junos_template module
* adds junos_rpc module
* adds junos_user module
pull/20872/head
Peter Sprygada 8 years ago committed by GitHub
parent 47870c3385
commit 02d2b753db

@ -1,5 +1,5 @@
# #
# (c) 2015 Peter Sprygada, <psprygada@ansible.com> # (c) 2017 Red Hat, Inc.
# #
# This file is part of Ansible # This file is part of Ansible
# #
@ -16,314 +16,203 @@
# You should have received a copy of the GNU General Public License # 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/>.
# #
import re from contextlib import contextmanager
import shlex
from ncclient.xml_ import new_ele, sub_ele, to_xml
from distutils.version import LooseVersion
from ansible.module_utils.basic import env_fallback
from ansible.module_utils.pycompat24 import get_exception from ansible.module_utils.netconf import send_request
from ansible.module_utils.network import register_transport, to_list from ansible.module_utils.netconf import discard_changes, validate
from ansible.module_utils.network import NetworkError from ansible.module_utils.network_common import to_list
from ansible.module_utils.shell import CliBase from ansible.module_utils.connection import exec_command
from ansible.module_utils.six import string_types
ACTIONS = frozenset(['merge', 'override', 'replace', 'update', 'set'])
JSON_ACTIONS = frozenset(['merge', 'override', 'update'])
FORMATS = frozenset(['xml', 'text', 'json'])
CONFIG_FORMATS = frozenset(['xml', 'text', 'json', 'set'])
_DEVICE_CONFIGS = {}
junos_argument_spec = {
'host': dict(),
'port': dict(type='int'),
'username': dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
'password': dict(fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD']), no_log=True),
'ssh_keyfile': dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
'timeout': dict(type='int', default=10),
'provider': dict(type='dict'),
}
def check_args(module, warnings):
provider = module.params['provider'] or {}
for key in junos_argument_spec:
if key in ('provider', 'transport') and module.params[key]:
warnings.append('argument %s has been deprecated and will be '
'removed in a future version' % key)
def validate_rollback_id(value):
try: try:
from jnpr.junos import Device if not 0 <= int(value) <= 49:
from jnpr.junos.utils.config import Config raise ValueError
from jnpr.junos.version import VERSION except ValueError:
from jnpr.junos.exception import RpcError, ConnectError, ConfigLoadError, CommitError module.fail_json(msg='rollback must be between 0 and 49')
from jnpr.junos.exception import LockError, UnlockError
if LooseVersion(VERSION) < LooseVersion('1.2.2'):
HAS_PYEZ = False
else:
HAS_PYEZ = True
except ImportError:
HAS_PYEZ = False
try: def load_configuration(module, candidate=None, action='merge', rollback=None, format='xml'):
import jxmlease
HAS_JXMLEASE = True
except ImportError:
HAS_JXMLEASE = False
try: if all((candidate is None, rollback is None)):
from lxml import etree module.fail_json(msg='one of candidate or rollback must be specified')
except ImportError:
import xml.etree.ElementTree as etree
elif all((candidate is not None, rollback is not None)):
module.fail_json(msg='candidate and rollback are mutually exclusive')
SUPPORTED_CONFIG_FORMATS = ['text', 'xml'] if format not in FORMATS:
module.fail_json(msg='invalid format specified')
if format == 'json' and action not in JSON_ACTIONS:
module.fail_json(msg='invalid action for format json')
elif format in ('text', 'xml') and action not in ACTIONS:
module.fail_json(msg='invalid action format %s' % format)
if action == 'set' and not format == 'text':
module.fail_json(msg='format must be text when action is set')
def xml_to_json(val): if rollback is not None:
if isinstance(val, string_types): validate_rollback_id(rollback)
return jxmlease.parse(val) xattrs = {'rollback': str(rollback)}
else: else:
return jxmlease.parse_etree(val) xattrs = {'action': action, 'format': format}
def xml_to_string(val):
return etree.tostring(val)
class Netconf(object):
def __init__(self):
if not HAS_PYEZ:
raise NetworkError(
msg='junos-eznc >= 1.2.2 is required but does not appear to be installed. '
'It can be installed using `pip install junos-eznc`'
)
if not HAS_JXMLEASE:
raise NetworkError(
msg='jxmlease is required but does not appear to be installed. '
'It can be installed using `pip install jxmlease`'
)
self.device = None
self.config = None
self._locked = False
self._connected = False
self.default_output = 'xml'
def raise_exc(self, msg):
if self.device:
if self._locked:
self.config.unlock()
self.disconnect()
raise NetworkError(msg)
def connect(self, params, **kwargs):
host = params['host']
kwargs = dict()
kwargs['port'] = params.get('port') or 830
kwargs['user'] = params['username']
if params['password']:
kwargs['passwd'] = params['password']
if params['ssh_keyfile']: obj = new_ele('load-configuration', xattrs)
kwargs['ssh_private_key_file'] = params['ssh_keyfile']
kwargs['gather_facts'] = False
try:
self.device = Device(host, **kwargs)
self.device.open()
self.device.timeout = params['timeout']
except ConnectError:
exc = get_exception()
self.raise_exc('unable to connect to %s: %s' % (host, str(exc)))
self.config = Config(self.device)
self._connected = True
def disconnect(self):
try:
self.device.close()
except AttributeError:
pass
self._connected = False
### Command methods ###
def run_commands(self, commands):
responses = list()
for cmd in commands:
meth = getattr(self, cmd.args.get('command_type'))
responses.append(meth(str(cmd), output=cmd.output))
for index, cmd in enumerate(commands):
if cmd.output == 'xml':
responses[index] = xml_to_json(responses[index])
elif cmd.args.get('command_type') == 'rpc':
responses[index] = str(responses[index].text).strip()
elif 'RpcError' in responses[index]:
raise NetworkError(responses[index])
return responses
def cli(self, commands, output='xml'):
'''Send commands to the device.'''
try:
return self.device.cli(commands, format=output, warning=False)
except (ValueError, RpcError):
exc = get_exception()
self.raise_exc('Unable to get cli output: %s' % str(exc))
def rpc(self, command, output='xml'): if candidate is not None:
name, kwargs = rpc_args(command) lookup = {'xml': 'configuration', 'text': 'configuration-text',
meth = getattr(self.device.rpc, name) 'set': 'configuration-set', 'json': 'configuration-json'}
reply = meth({'format': output}, **kwargs)
return reply
### Config methods ### if action == 'set':
cfg = sub_ele(obj, 'configuration-set')
def get_config(self, config_format="text"): cfg.text = '\n'.join(candidate)
if config_format not in SUPPORTED_CONFIG_FORMATS:
self.raise_exc(msg='invalid config format. Valid options are '
'%s' % ', '.join(SUPPORTED_CONFIG_FORMATS))
ele = self.rpc('get_configuration', output=config_format)
if config_format == 'text':
return unicode(ele.text).strip()
else: else:
return ele cfg = sub_ele(obj, lookup[format])
cfg.append(candidate)
def load_config(self, config, commit=False, replace=False, confirm=None,
comment=None, config_format='text', overwrite=False, merge=False): return send_request(module, obj)
if (overwrite or replace) and config_format == 'set':
self.raise_exc('replace/overwrite cannot be True when config_format is `set`') def get_configuration(module, compare=False, format='xml', rollback='0'):
if format not in CONFIG_FORMATS:
if replace: module.fail_json(msg='invalid config format specified')
merge = False xattrs = {'format': format}
if compare:
validate_rollback_id(rollback)
xattrs['compare'] = 'rollback'
xattrs['rollback'] = str(rollback)
return send_request(module, new_ele('get-configuration', xattrs))
def commit_configuration(module, confirm=False, check=False, comment=None, confirm_timeout=None):
obj = new_ele('commit-configuration')
if confirm:
sub_ele(obj, 'confirmed')
if check:
sub_ele(obj, 'check')
if comment:
children(obj, ('log', str(comment)))
if confirm_timeout:
children(obj, ('confirm-timeout', int(confirm_timeout)))
return send_request(module, obj)
self.lock_config() lock_configuration = lambda x: send_request(x, new_ele('lock-configuration'))
unlock_configuration = lambda x: send_request(x, new_ele('unlock-configuration'))
@contextmanager
def locked_config(module):
try: try:
candidate = '\n'.join(config) lock_configuration(module)
self.config.load(candidate, format=config_format, merge=merge, yield
overwrite=overwrite) finally:
unlock_configuration(module)
except ConfigLoadError:
exc = get_exception() def get_diff(module):
self.raise_exc('Unable to load config: %s' % str(exc)) reply = get_configuration(module, compare=True, format='text')
output = reply.xpath('//configuration-output')
diff = self.config.diff() if output:
return output[0].text
self.check_config()
def load(module, candidate, action='merge', commit=False, format='xml'):
if all((commit, diff)): """Loads a configuration element into the target system
self.commit_config(comment=comment, confirm=confirm) """
with locked_config(module):
self.unlock_config() resp = load_configuration(module, candidate, action=action, format=format)
validate(module)
diff = get_diff(module)
if diff:
diff = str(diff).strip()
if commit:
commit_configuration(module)
else:
discard_changes(module)
return diff return diff
def save_config(self):
raise NotImplementedError
### end of Config ###
def get_facts(self, refresh=True):
if refresh:
self.device.facts_refresh()
return self.device.facts
def unlock_config(self):
try:
self.config.unlock()
self._locked = False
except UnlockError:
exc = get_exception()
raise NetworkError('unable to unlock config: %s' % str(exc))
def lock_config(self): # START CLI FUNCTIONS
try:
self.config.lock()
self._locked = True
except LockError:
exc = get_exception()
raise NetworkError('unable to lock config: %s' % str(exc))
def check_config(self): def get_config(module, flags=[]):
if not self.config.commit_check(): cmd = 'show configuration '
self.raise_exc(msg='Commit check failed') cmd += ' '.join(flags)
cmd = cmd.strip()
def commit_config(self, comment=None, confirm=None):
try:
kwargs = dict(comment=comment)
if confirm and confirm > 0:
kwargs['confirm'] = confirm
return self.config.commit(**kwargs)
except CommitError:
exc = get_exception()
raise NetworkError('unable to commit config: %s' % str(exc))
def confirm_commit(self, checkonly=False):
try: try:
resp = self.rpc('get_commit_information') return _DEVICE_CONFIGS[cmd]
needs_confirm = 'commit confirmed, rollback' in resp[0][4].text except KeyError:
if checkonly: rc, out, err = exec_command(module, cmd)
return needs_confirm if rc != 0:
return self.commit_config() module.fail_json(msg='unable to retrieve current config', stderr=err)
except IndexError: cfg = str(out).strip()
# if there is no comment tag, the system is not in a commit _DEVICE_CONFIGS[cmd] = cfg
# confirmed state so just return return cfg
pass
def run_commands(module, commands, check_rc=True):
def rollback_config(self, identifier, commit=True, comment=None): responses = list()
for cmd in to_list(commands):
self.lock_config() cmd = module.jsonify(cmd)
rc, out, err = exec_command(module, cmd)
if check_rc and rc != 0:
module.fail_json(msg=err, rc=rc)
try: try:
self.config.rollback(identifier) out = module.from_json(out)
except ValueError: except ValueError:
exc = get_exception() out = str(out).strip()
self.raise_exc('Unable to rollback config: $s' % str(exc))
diff = self.config.diff()
if commit:
self.commit_config(comment=comment)
self.unlock_config()
return diff
Netconf = register_transport('netconf')(Netconf)
responses.append(out)
return responses
class Cli(CliBase): def load_config(module, config, commit=False, comment=None,
confirm=False, confirm_timeout=None):
CLI_PROMPTS_RE = [
re.compile(r"[\r\n]?[\w+\-\.:\/\[\]]+(?:\([^\)]+\)){,3}(?:>|#) ?$"),
re.compile(r"\[\w+\@[\w\-\.]+(?: [^\]])\] ?[>#\$] ?$")
]
CLI_ERRORS_RE = [ exec_command(module, 'configure')
re.compile(r"unkown command")
]
def connect(self, params, **kwargs): for item in to_list(config):
super(Cli, self).connect(params, **kwargs) rc, out, err = exec_command(module, item)
if self.shell._matched_prompt.strip().endswith('%'): if rc != 0:
self.execute('cli') module.fail_json(msg=str(err))
self.execute('set cli screen-length 0')
def configure(self, commands, comment=None): exec_command(module, 'top')
cmds = ['configure'] rc, diff, err = exec_command(module, 'show | compare')
cmds.extend(to_list(commands))
if commit:
cmd = 'commit'
if commit:
cmd = 'commit confirmed'
if commit_timeout:
cmd +' %s' % confirm_timeout
if comment: if comment:
cmds.append('commit and-quit comment "%s"' % comment) cmd += ' comment "%s"' % comment
cmd += ' and-quit'
exec_command(module, cmd)
else: else:
cmds.append('commit and-quit') for cmd in ['rollback 0', 'exit']:
exec_command(module, cmd)
responses = self.execute(cmds)
return responses[1:-1] return str(diff).strip()
Cli = register_transport('cli', default=True)(Cli)
def split(value):
lex = shlex.shlex(value)
lex.quotes = '"'
lex.whitespace_split = True
lex.commenters = ''
return list(lex)
def rpc_args(args):
kwargs = dict()
args = split(args)
name = args.pop(0)
for arg in args:
key, value = arg.split('=')
if str(value).upper() in ['TRUE', 'FALSE']:
kwargs[key] = bool(value)
elif re.match(r'^[0-9]+$', value):
kwargs[key] = int(value)
else:
kwargs[key] = str(value)
return (name, kwargs)

@ -127,22 +127,13 @@ def main():
transport=dict(default='netconf', choices=['netconf']) transport=dict(default='netconf', choices=['netconf'])
) )
module = NetworkModule(argument_spec=argument_spec, module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True) supports_check_mode=True)
comment = module.params['comment'] comment = module.params['comment']
confirm = module.params['confirm'] confirm = module.params['confirm']
commit = not module.check_mode commit = not module.check_mode
replace = False
overwrite = False
action = module.params['action'] action = module.params['action']
if action == 'overwrite':
overwrite = True
elif action == 'replace':
replace = True
src = module.params['src'] src = module.params['src']
fmt = module.params['config_format'] fmt = module.params['config_format']
@ -150,19 +141,16 @@ def main():
module.fail_json(msg="overwrite cannot be used when format is " module.fail_json(msg="overwrite cannot be used when format is "
"set per junos-pyez documentation") "set per junos-pyez documentation")
results = dict(changed=False) results = {'changed': False}
results['_backup'] = unicode(module.config.get_config()).strip()
try: if module.praams['backup']:
diff = module.config.load_config(src, commit=commit, replace=replace, results['__backup__'] = unicode(get_configuration(module))
confirm=confirm, comment=comment, config_format=fmt)
diff = load(module, src, **kwargs)
if diff: if diff:
results['changed'] = True results['changed'] = True
results['diff'] = dict(prepared=diff) if module._diff:
except NetworkError: results['diff'] = {'prepared': diff}
exc = get_exception()
module.fail_json(msg=str(exc), **exc.kwargs)
module.exit_json(**results) module.exit_json(**results)

@ -16,42 +16,32 @@
# 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'], ANSIBLE_METADATA = {
'status': ['preview'],
'supported_by': 'core', 'supported_by': 'core',
'version': '1.0'} 'version': '1.0'
}
DOCUMENTATION = """ DOCUMENTATION = """
--- ---
module: junos_command module: junos_command
version_added: "2.1" version_added: "2.1"
author: "Peter Sprygada (@privateip)" author: "Peter Sprygada (@privateip)"
short_description: Execute arbitrary commands on a remote device running Junos short_description: Run arbitrary commands on an Juniper junos device
description: description:
- Network devices running the Junos operating system provide a command - Sends an arbitrary set of commands to an junos node and returns the results
driven interface both over CLI and RPC. This module provides an read from the device. This module includes an
interface to execute commands using these functions and return the argument that will cause the module to wait for a specific condition
results to the Ansible playbook. In addition, this before returning or timing out if the condition is not met.
module can specify a set of conditionals to be evaluated against the
returned output, only returning control to the playbook once the
entire set of conditionals has been met.
extends_documentation_fragment: junos
options: options:
commands: commands:
description: description:
- The C(commands) to send to the remote device over the Netconf - The commands to send to the remote junos device over the
transport. The resulting output from the command configured provider. The resulting output from the command
is returned. If the I(wait_for) argument is provided, the is returned. If the I(wait_for) argument is provided, the
module is not returned until the condition is satisfied or module is not returned until the condition is satisfied or
the number of I(retries) has been exceeded. the number of I(retries) has been exceeded.
required: false required: true
default: null
rpcs:
description:
- The C(rpcs) argument accepts a list of RPCs to be executed
over a netconf session and the results from the RPC execution
is return to the playbook via the modules results dictionary.
required: false
default: null
wait_for: wait_for:
description: description:
- Specifies what to evaluate from the output of the command - Specifies what to evaluate from the output of the command
@ -77,9 +67,9 @@ options:
version_added: "2.2" version_added: "2.2"
retries: retries:
description: description:
- Specifies the number of retries a command should by tried - Specifies the number of retries a command should be tried
before it is considered failed. The command is run on the before it is considered failed. The command is run on the
target device every retry and evaluated against the I(waitfor) target device every retry and evaluated against the I(wait_for)
conditionals. conditionals.
required: false required: false
default: 10 default: 10
@ -91,214 +81,167 @@ options:
trying the command again. trying the command again.
required: false required: false
default: 1 default: 1
format:
description:
- Configures the encoding scheme to use when serializing output
from the device. This handles how to properly understand the
output and apply the conditionals path to the result set.
required: false
default: 'xml'
choices: ['xml', 'text', 'json']
requirements:
- junos-eznc
notes:
- This module requires the netconf system service be enabled on
the remote device being managed. 'json' format is supported
for JUNON version >= 14.2
""" """
EXAMPLES = """ EXAMPLES = """
# Note: examples below use the following provider dict to handle # Note: examples below use the following provider dict to handle
# transport and authentication to the node. # transport and authentication to the node.
--- ---
vars: - name: run show version on remote devices
netconf: junos_command:
host: "{{ inventory_hostname }}" commands: show version
username: ansible
password: Ansible
--- - name: run show version and check to see if output contains Juniper
- name: run a set of commands junos_command:
commands: show version
wait_for: result[0] contains Juniper
- name: run multiple commands on remote nodes
junos_command: junos_command:
commands: ['show version', 'show ip route'] commands:
provider: "{{ netconf }}" - show version
- show interfaces
- name: run a command with a conditional applied to the second command - name: run multiple commands and evaluate the output
junos_command: junos_command:
commands: commands:
- show version - show version
- show interfaces fxp0 - show interfaces
waitfor: wait_for:
- "result[1].interface-information.physical-interface.name eq fxp0" - result[0] contains Juniper
provider: "{{ netconf }}" - result[1] contains Loopback0
- name: collect interface information using rpc - name: run commands and specify the output format
junos_command: junos_command:
rpcs: commands:
- "get_interface_information interface=em0 media=True" - command: show version
- "get_interface_information interface=fxp0 media=True" output: json
provider: "{{ netconf }}"
""" """
RETURN = """ RETURN = """
stdout: failed_conditions:
description: The output from the commands read from the device
returned: always
type: list
sample: ['...', '...']
stdout_lines:
description: The output read from the device split into lines
returned: always
type: list
sample: [['...', '...'], ['...', '...']]
failed_conditionals:
description: the conditionals that failed description: the conditionals that failed
returned: failed returned: failed
type: list type: list
sample: ['...', '...'] sample: ['...', '...']
""" """
import time
from functools import partial
import ansible.module_utils.junos from ansible.module_utils.junos import run_commands
from ansible.module_utils.basic import get_exception from ansible.module_utils.junos import junos_argument_spec
from ansible.module_utils.network import NetworkModule, NetworkError from ansible.module_utils.junos import check_args as junos_check_args
from ansible.module_utils.netcli import CommandRunner from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.netcli import AddCommandError, FailedConditionsError
from ansible.module_utils.netcli import FailedConditionalError, AddConditionError
from ansible.module_utils.junos import xml_to_json
from ansible.module_utils.six import string_types from ansible.module_utils.six import string_types
from ansible.module_utils.netcli import Conditional
from ansible.module_utils.network_common import ComplexList
VALID_KEYS = { def check_args(module, warnings):
'cli': frozenset(['command', 'output', 'prompt', 'response']), junos_check_args(module, warnings)
'rpc': frozenset(['command', 'output'])
}
if module.params['rpcs']:
module.fail_json(msg='argument rpcs has been deprecated, please use '
'junos_rpc instead')
def to_lines(stdout): def to_lines(stdout):
lines = list()
for item in stdout: for item in stdout:
if isinstance(item, string_types): if isinstance(item, string_types):
item = str(item).split('\n') item = str(item).split('\n')
yield item lines.append(item)
return lines
def parse(module, command_type):
if command_type == 'cli':
items = module.params['commands']
elif command_type == 'rpc':
items = module.params['rpcs']
parsed = list()
for item in (items or list()):
if isinstance(item, string_types):
item = dict(command=item, output=None)
elif 'command' not in item:
module.fail_json(msg='command keyword argument is required')
elif item.get('output') not in [None, 'text', 'xml']:
module.fail_json(msg='invalid output specified for command'
'Supported values are `text` or `xml`')
elif not set(item.keys()).issubset(VALID_KEYS[command_type]):
module.fail_json(msg='unknown command keyword specified. Valid '
'values are %s' % ', '.join(VALID_KEYS[command_type]))
if not item['output']: def parse_commands(module, warnings):
item['output'] = module.params['display'] spec = dict(
command=dict(key=True),
output=dict(default=module.params['display'], choices=['text', 'json']),
prompt=dict(),
response=dict()
)
item['command_type'] = command_type transform = ComplexList(spec, module)
commands = transform(module.params['commands'])
# show configuration [options] will return as text for index, item in enumerate(commands):
if item['command'].startswith('show configuration'): if module.check_mode and not item['command'].startswith('show'):
item['output'] = 'text' warnings.append(
'Only show commands are supported when using check_mode, not '
'executing %s' % item['command']
)
parsed.append(item) if item['output'] == 'json' and 'display json' not in item['command']:
item['command'] += '| display json'
elif item['output'] == 'text' and 'display json' in item['command']:
item['command'] = item['command'].replace('| display json', '')
return parsed commands[index] = item
return commands
def main(): def main():
"""main entry point for Ansible module """entry point for module execution
""" """
argument_spec = dict(
commands=dict(type='list', required=True),
display=dict(choices=['text', 'json'], default='text'),
spec = dict( # deprecated (Ansible 2.3) - use junos_rpc
commands=dict(type='list'),
rpcs=dict(type='list'), rpcs=dict(type='list'),
display=dict(default='xml', choices=['text', 'xml', 'json'],
aliases=['format', 'output']),
wait_for=dict(type='list', aliases=['waitfor']), wait_for=dict(type='list', aliases=['waitfor']),
match=dict(default='all', choices=['all', 'any']), match=dict(default='all', choices=['all', 'any']),
retries=dict(default=10, type='int'), retries=dict(default=10, type='int'),
interval=dict(default=1, type='int'), interval=dict(default=1, type='int')
transport=dict(default='netconf', choices=['netconf'])
) )
mutually_exclusive = [('commands', 'rpcs')] argument_spec.update(junos_argument_spec)
module = NetworkModule(argument_spec=spec, module = AnsibleModule(argument_spec=argument_spec,
mutually_exclusive=mutually_exclusive,
supports_check_mode=True) supports_check_mode=True)
commands = list()
for key in VALID_KEYS.keys():
commands.extend(list(parse(module, key)))
conditionals = module.params['wait_for'] or list()
warnings = list() warnings = list()
check_args(module, warnings)
commands = parse_commands(module, warnings)
wait_for = module.params['wait_for'] or list()
conditionals = [Conditional(c) for c in wait_for]
retries = module.params['retries']
interval = module.params['interval']
match = module.params['match']
while retries > 0:
responses = run_commands(module, commands)
for item in list(conditionals):
if item(responses):
if match == 'any':
conditionals = list()
break
conditionals.remove(item)
if not conditionals:
break
time.sleep(interval)
retries -= 1
if conditionals:
failed_conditions = [item.raw for item in conditionals]
msg = 'One or more conditional statements have not be satisfied'
module.fail_json(msg=msg, failed_conditions=failed_conditions)
result = {
'changed': False,
'warnings': warnings,
'stdout': responses,
'stdout_lines': to_lines(responses)
}
runner = CommandRunner(module)
for cmd in commands:
if module.check_mode and not cmd['command'].startswith('show'):
warnings.append('only show commands are supported when using '
'check mode, not executing `%s`' % cmd['command'])
else:
if cmd['command'].startswith('co'):
module.fail_json(msg='junos_command does not support running '
'config mode commands. Please use '
'junos_config instead')
try:
runner.add_command(**cmd)
except AddCommandError:
exc = get_exception()
warnings.append('duplicate command detected: %s' % cmd)
try:
for item in conditionals:
runner.add_conditional(item)
except (ValueError, AddConditionError):
exc = get_exception()
module.fail_json(msg=str(exc), condition=exc.condition)
runner.retries = module.params['retries']
runner.interval = module.params['interval']
runner.match = module.params['match']
try:
runner.run()
except FailedConditionsError:
exc = get_exception()
module.fail_json(msg=str(exc), failed_conditions=exc.failed_conditions)
except FailedConditionalError:
exc = get_exception()
module.fail_json(msg=str(exc), failed_conditional=exc.failed_conditional)
except NetworkError:
exc = get_exception()
module.fail_json(msg=str(exc))
result = dict(changed=False, stdout=list())
for cmd in commands:
try:
output = runner.get_command(cmd['command'], cmd.get('output'))
except ValueError:
output = 'command not executed due to check_mode, see warnings'
result['stdout'].append(output)
result['warnings'] = warnings
result['stdout_lines'] = list(to_lines(result['stdout']))
module.exit_json(**result) module.exit_json(**result)

@ -16,9 +16,11 @@
# 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'], ANSIBLE_METADATA = {
'status': ['preview'],
'supported_by': 'core', 'supported_by': 'core',
'version': '1.0'} 'version': '1.0'
}
DOCUMENTATION = """ DOCUMENTATION = """
--- ---
@ -31,7 +33,6 @@ description:
configuration running on Juniper JUNOS devices. It provides a set configuration running on Juniper JUNOS devices. It provides a set
of arguments for loading configuration, performing rollback operations of arguments for loading configuration, performing rollback operations
and zeroing the active configuration on the device. and zeroing the active configuration on the device.
extends_documentation_fragment: junos
options: options:
lines: lines:
description: description:
@ -144,16 +145,6 @@ notes:
""" """
EXAMPLES = """ EXAMPLES = """
# Note: examples below use the following provider dict to handle
# transport and authentication to the node.
---
vars:
netconf:
host: "{{ inventory_hostname }}"
username: ansible
password: Ansible
---
- name: load configure file into device - name: load configure file into device
junos_config: junos_config:
src: srx.cfg src: srx.cfg
@ -182,19 +173,27 @@ backup_path:
type: path type: path
sample: /playbooks/ansible/backup/config.2016-07-16@22:28:34 sample: /playbooks/ansible/backup/config.2016-07-16@22:28:34
""" """
import re
import json import json
from xml.etree import ElementTree from xml.etree import ElementTree
from ncclient.xml_ import to_xml
import ansible.module_utils.junos from ansible.module_utils.junos import get_diff, load
from ansible.module_utils.junos import locked_config, load_configuration
from ansible.module_utils.basic import get_exception from ansible.module_utils.junos import get_configuration
from ansible.module_utils.network import NetworkModule, NetworkError from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.netcfg import NetworkConfig from ansible.module_utils.netcfg import NetworkConfig
DEFAULT_COMMENT = 'configured by junos_config' DEFAULT_COMMENT = 'configured by junos_config'
def check_args(module, warnings):
if module.params['zeroize']:
module.fail_json(msg='argument zeroize is deprecated and no longer '
'supported, use junos_command instead')
if module.params['replace'] is not None:
module.fail_json(msg='argument replace is deprecated, use update')
def guess_format(config): def guess_format(config):
try: try:
@ -233,91 +232,63 @@ def config_to_commands(config):
return commands return commands
def diff_commands(commands, config): def filter_delete_statements(module, candidate):
config = [unicode(c).replace("'", '') for c in config] reply = get_configuration(module, format='set')
config = reply.xpath('//configuration-set')[0].text.strip()
updates = list() for index, line in enumerate(candidate):
visited = set() if line.startswith('delete'):
newline = re.sub('^delete', 'set', line)
for index, item in enumerate(commands): if newline not in config:
if len(item) > 0: del candidate[index]
if not item.startswith('set') and not item.startswith('delete'): return candidate
raise ValueError('line must start with either `set` or `delete`')
def load_config(module):
elif item.startswith('set') and item[4:] not in config:
updates.append(item)
elif item.startswith('delete'):
for entry in config + commands[0:index]:
if entry.startswith('set'):
entry = entry[4:]
if entry.startswith(item[7:]) and item not in visited:
updates.append(item)
visited.add(item)
return updates
def load_config(module, result):
candidate = module.params['lines'] or module.params['src'] candidate = module.params['lines'] or module.params['src']
if isinstance(candidate, basestring): if isinstance(candidate, basestring):
candidate = candidate.split('\n') candidate = candidate.split('\n')
kwargs = dict() confirm = module.params['confirm'] > 0
kwargs['comment'] = module.params['comment'] confirm_timeout = module.params['confirm']
kwargs['confirm'] = module.params['confirm']
kwargs[module.params['update']] = True kwargs = {
kwargs['commit'] = not module.check_mode 'confirm': module.params['confirm'] is not None,
kwargs['replace'] = module.params['replace'] 'confirm_timeout': module.params['confirm_timeout'],
'comment': module.params['comment'],
'commit': not module.check_mode,
}
if module.params['src']: if module.params['src']:
config_format = module.params['src_format'] or guess_format(str(candidate)) config_format = module.params['src_format'] or guess_format(str(candidate))
elif module.params['lines']: kwargs.update({'format': config_format, 'action': module.params['update']})
config_format = 'set'
kwargs['config_format'] = config_format
# this is done to filter out `delete ...` statements which map to # this is done to filter out `delete ...` statements which map to
# nothing in the config as that will cause an exception to be raised # nothing in the config as that will cause an exception to be raised
if config_format == 'set': if module.params['lines']:
config = module.config.get_config() candidate = filter_delete_statements(module, candidate)
config = config_to_commands(config) kwargs.update({'action': 'set', 'format': 'text'})
candidate = diff_commands(candidate, config)
diff = module.config.load_config(candidate, **kwargs)
if diff: return load(module, candidate, **kwargs)
result['changed'] = True
result['diff'] = dict(prepared=diff)
def rollback_config(module, result): def rollback_config(module, result):
rollback = module.params['rollback'] rollback = module.params['rollback']
diff = None
kwargs = dict(comment=module.params['comment'], with locked_config:
commit=not module.check_mode) load_configuration(module, rollback=rollback)
diff = get_diff(module)
diff = module.connection.rollback_config(rollback, **kwargs)
if diff: return diff
result['changed'] = True
result['diff'] = dict(prepared=diff)
def zeroize_config(module, result): def confirm_config(module):
if not module.check_mode: with locked_config:
module.connection.cli('request system zeroize') commit_configuration(confirm=True)
result['changed'] = True
def confirm_config(module, result): def update_result(module, result, diff=None):
checkonly = module.check_mode if diff == '':
result['changed'] = module.connection.confirm_commit(checkonly) diff = None
result['changed'] = diff is not None
def run(module, result): if module._diff:
if module.params['rollback']: result['diff'] = {'prepared': diff}
return rollback_config(module, result)
elif module.params['zeroize']:
return zeroize_config(module, result)
elif not any((module.params['src'], module.params['lines'])):
return confirm_config(module, result)
else:
return load_config(module, result)
def main(): def main():
@ -330,8 +301,10 @@ def main():
src_format=dict(choices=['xml', 'text', 'set', 'json']), src_format=dict(choices=['xml', 'text', 'set', 'json']),
# update operations # update operations
update=dict(default='merge', choices=['merge', 'overwrite', 'replace']), update=dict(default='merge', choices=['merge', 'overwrite', 'replace', 'update']),
replace=dict(default=False, type='bool'),
# deprecated replace in Ansible 2.3
replace=dict(type='bool'),
confirm=dict(default=0, type='int'), confirm=dict(default=0, type='int'),
comment=dict(default=DEFAULT_COMMENT), comment=dict(default=DEFAULT_COMMENT),
@ -339,36 +312,35 @@ def main():
# config operations # config operations
backup=dict(type='bool', default=False), backup=dict(type='bool', default=False),
rollback=dict(type='int'), rollback=dict(type='int'),
zeroize=dict(default=False, type='bool'),
transport=dict(default='netconf', choices=['netconf']) # deprecated zeroize in Ansible 2.3
zeroize=dict(default=False, type='bool'),
) )
mutually_exclusive = [('lines', 'rollback'), ('lines', 'zeroize'), mutually_exclusive = [('lines', 'src', 'rollback')]
('rollback', 'zeroize'), ('lines', 'src'),
('src', 'zeroize'), ('src', 'rollback'),
('update', 'replace')]
required_if = [('replace', True, ['src']),
('update', 'merge', ['src', 'lines'], True),
('update', 'overwrite', ['src', 'lines'], True),
('update', 'replace', ['src', 'lines'], True)]
module = NetworkModule(argument_spec=argument_spec, module = AnsibleModule(argument_spec=argument_spec,
mutually_exclusive=mutually_exclusive, mutually_exclusive=mutually_exclusive,
required_if=required_if,
supports_check_mode=True) supports_check_mode=True)
result = dict(changed=False) warnings = list()
check_args(module, warnings)
result = {'changed': False, 'warnings': warnings}
if module.params['backup']: if module.params['backup']:
result['__backup__'] = module.config.get_config() result['__backup__'] = get_configuration()
try: if module.params['rollback']:
run(module, result) diff = get_diff(module)
except NetworkError: update_result(module, result, diff)
exc = get_exception()
module.fail_json(msg=str(exc), **exc.kwargs) elif not any((module.params['src'], module.params['lines'])):
confirm_config(module)
else:
diff = load_config(module)
update_result(module, result, diff)
module.exit_json(**result) module.exit_json(**result)

@ -16,9 +16,11 @@
# 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'], ANSIBLE_METADATA = {
'status': ['preview'],
'supported_by': 'core', 'supported_by': 'core',
'version': '1.0'} 'version': '1.0'
}
DOCUMENTATION = """ DOCUMENTATION = """
--- ---
@ -56,27 +58,14 @@ options:
""" """
EXAMPLES = """ EXAMPLES = """
# Note: examples below use the following provider dict to handle
# transport and authentication to the node.
---
vars:
cli:
host: "{{ inventory_hostname }}"
username: ansible
password: Ansible
transport: cli
---
- name: enable netconf service on port 830 - name: enable netconf service on port 830
junos_netconf: junos_netconf:
listens_on: 830 listens_on: 830
state: present state: present
provider: "{{ cli }}"
- name: disable netconf service - name: disable netconf service
junos_netconf: junos_netconf:
state: absent state: absent
provider: "{{ cli }}"
""" """
RETURN = """ RETURN = """
@ -88,64 +77,94 @@ commands:
""" """
import re import re
import ansible.module_utils.junos from ansible.module_utils.junos import load_config, get_config
from ansible.module_utils.junos import junos_argument_spec, check_args
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six import iteritems
def map_obj_to_commands(updates, module):
want, have = updates
commands = list()
if want['state'] == 'present' and have['state'] == 'absent':
commands.append(
'set system services netconf ssh port %s' % want['netconf_port']
)
elif want['state'] == 'absent' and have['state'] == 'present':
commands.append('delete system services netconf')
elif want['netconf_port'] != have.get('netconf_port'):
commands.append(
'set system services netconf ssh port %s' % want['netconf_port']
)
from ansible.module_utils.basic import get_exception return commands
from ansible.module_utils.network import NetworkModule, NetworkError
def parse_port(config): def parse_port(config):
match = re.search(r'port (\d+)', config) match = re.search(r'port (\d+)', config)
if match: if match:
return int(match.group(1)) return int(match.group(1))
def get_instance(module): def map_config_to_obj(module):
cmd = 'show configuration system services netconf' config = get_config(module, ['system services netconf'])
cfg = module.cli(cmd)[0] obj = {'state': 'absent'}
result = dict(state='absent') if config:
if cfg: obj.update({
result = dict(state='present') 'state': 'present',
result['port'] = parse_port(cfg) 'netconf_port': parse_port(config)
return result })
return obj
def validate_netconf_port(value, module):
if not 1 <= value <= 65535:
module.fail_json(msg='netconf_port must be between 1 and 65535')
def map_params_to_obj(module):
obj = {
'netconf_port': module.params['netconf_port'],
'state': module.params['state']
}
for key, value in iteritems(obj):
# validate the param value (if validator func exists)
validator = globals().get('validate_%s' % key)
if all((value, validator)):
validator(value, module)
return obj
def main(): def main():
"""main entry point for module execution """main entry point for module execution
""" """
argument_spec = dict( argument_spec = dict(
netconf_port=dict(type='int', default=830, aliases=['listens_on']), netconf_port=dict(type='int', default=830, aliases=['listens_on']),
state=dict(default='present', choices=['present', 'absent']), state=dict(default='present', choices=['present', 'absent']),
transport=dict(default='cli', choices=['cli'])
) )
module = NetworkModule(argument_spec=argument_spec, module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True) supports_check_mode=True)
state = module.params['state'] warnings = list()
port = module.params['netconf_port'] check_args(module, warnings)
result = dict(changed=False) result = {'changed': False, 'warnings': warnings}
instance = get_instance(module) want = map_params_to_obj(module)
have = map_config_to_obj(module)
if state == 'present' and instance.get('state') == 'absent': commands = map_obj_to_commands((want, have), module)
commands = 'set system services netconf ssh port %s' % port result['commands'] = commands
elif state == 'present' and port != instance.get('port'):
commands = 'set system services netconf ssh port %s' % port
elif state == 'absent' and instance.get('state') == 'present':
commands = 'delete system services netconf'
else:
commands = None
if commands: if commands:
if not module.check_mode: commit = not module.check_mode
try: diff = load_config(module, commands, commit=commit)
comment = 'configuration updated by junos_netconf' if diff and module._diff:
module.config(commands, comment=comment) if module._diff:
except NetworkError: result['diff'] = {'prepared': diff}
exc = get_exception()
module.fail_json(msg=str(exc), **exc.kwargs)
result['changed'] = True result['changed'] = True
result['commands'] = commands
module.exit_json(**result) module.exit_json(**result)

@ -0,0 +1,151 @@
#!/usr/bin/python
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#
ANSIBLE_METADATA = {
'status': ['preview'],
'supported_by': 'core',
'version': '1.0'
}
DOCUMENTATION = """
---
module: junos_rpc
version_added: "2.3"
author: "Peter Sprygada (@privateip)"
short_description: Runs an arbitrary RPC on the remote device over NetConf
description:
- Sends a request to the remote device running JUNOS to execute the
specified RPC using the NetConf transport. The reply is then
returned to the playbook in the c(xml) key. If an alternate output
format is requested, the reply is transformed to the requested output.
options:
rpc:
description:
- The C(rpc) argument specifies the RPC call to send to the
remote devices to be executed. The RPC Reply message is parsed
and the contents are returned to the playbook.
required: true
args:
description:
- The C(args) argument provides a set of arguments for the RPC
call and are encoded in the request message. This argument
accepts a set of key=value arguments.
required: false
default: null
output:
description:
- The C(output) argument specifies the desired output of the
return data. This argument accepts one of C(xml), C(text),
or C(json). For C(json), the JUNOS device must be running a
version of software that supports native JSON output.
required: false
default: xml
"""
EXAMPLES = """
- name: collect interface information using rpc
junos_rpc:
rpc: get-interface-information
args:
interface: em0
media: True
- name: get system information
junos_rpc:
rpc: get-system-information
"""
RETURN = """
xml:
description: The xml return string from the rpc request
returned: always
output:
description: The rpc rely converted to the output format
returned: always
output_lines:
description: The text output split into lines for readability
returned: always
"""
from ncclient.xml_ import new_ele, sub_ele, to_xml, to_ele
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.netconf import send_request
from ansible.module_utils.six import iteritems
def main():
"""main entry point for Ansible module
"""
argument_spec = dict(
rpc=dict(required=True),
args=dict(type='dict'),
output=dict(default='xml', choices=['xml', 'json', 'text']),
)
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=False)
result = {'changed': False}
rpc = str(module.params['rpc']).replace('_', '-')
if all((module.check_mode, not rpc.startswith('get'))):
module.fail_json(msg='invalid rpc for running in check_mode')
args = module.params['args'] or {}
xattrs = {'format': module.params['output']}
element = new_ele(module.params['rpc'], xattrs)
for key, value in iteritems(args):
key = str(key).replace('_', '-')
if isinstance(value, list):
for item in value:
child = sub_ele(element, key)
if item is not True:
child.text = item
else:
child = sub_ele(element, key)
if value is not True:
child.text = value
reply = send_request(module, element)
result['xml'] = str(to_xml(reply))
if module.params['output'] == 'text':
reply = to_ele(reply)
data = reply.xpath('//output')
result['output'] = data[0].text.strip()
result['output_lines'] = result['output'].split('\n')
elif module.params['output'] == 'json':
reply = to_ele(reply)
data = reply.xpath('//rpc-reply')
result['output'] = module.from_json(data[0].text.strip())
else:
result['output'] = str(to_xml(reply)).split('\n')
module.exit_json(**result)
if __name__ == '__main__':
main()

@ -0,0 +1,256 @@
#!/usr/bin/python
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#
ANSIBLE_METADATA = {
'status': ['preview'],
'supported_by': 'core',
'version': '1.0'
}
DOCUMENTATION = """
---
module: junos_user
version_added: "2.3"
author: "Peter Sprygada (@privateip)"
short_description: Manage local user accounts on Juniper devices
description:
- This module manages locally configured user accounts on remote
network devices running the JUNOS operating system. It provides
a set of arguments for creating, removing and updating locally
defined accounts
options:
users:
description:
- The C(users) argument defines a list of users to be configured
on the remote device. The list of users will be compared against
the current users and only changes will be added or removed from
the device configuration. This argument is mutually exclusive with
the name argument.
required: False
default: null
name:
description:
- The C(name) argument defines the username of the user to be created
on the system. This argument must follow appropriate usernaming
conventions for the target device running JUNOS. This argument is
mutually exclusive with the C(users) argument.
required: false
default: null
full_name:
description:
- The C(full_name) argument provides the full name of the user
account to be created on the remote device. This argument accepts
any text string value.
required: false
default: null
role:
description:
- The C(role) argument defines the role of the user account on the
remote system. User accounts can have more than one role
configured.
required: false
default: read-only
choices: ['operator', 'read-only', 'super-user', 'unauthorized']
sshkey:
description:
- The C(sshkey) argument defines the public SSH key to be configured
for the user account on the remote system. This argument must
be a valid SSH key
required: false
default: null
purge:
description:
- The C(purge) argument instructs the module to consider the
users definition absolute. It will remove any previously configured
users on the device with the exception of the current defined
set of users.
required: false
default: false
state:
description:
- The C(state) argument configures the state of the user definitions
as it relates to the device operational configuration. When set
to I(present), the user should be configured in the device active
configuration and when set to I(absent) the user should not be
in the device active configuration
required: false
default: present
choices: ['present', 'absent']
"""
EXAMPLES = """
- name: create new user account
junos_user:
name: ansible
role: super-user
sshkey: "{{ lookup('file', '~/.ssh/ansible.pub') }}"
state: present
- name: remove a user account
junos_user:
name: ansible
state: absent
- name: remove all user accounts except ansible
junos_user:
name: ansible
purge: yes
"""
RETURN = """
"""
from functools import partial
from ncclient.xml_ import new_ele, sub_ele, to_xml
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.junos import load
from ansible.module_utils.six import iteritems
ROLES = ['operator', 'read-only', 'super-user', 'unauthorized']
def map_obj_to_ele(want):
element = new_ele('system')
login = sub_ele(element, 'login', {'replace': 'replace'})
for item in want:
if item['state'] != 'present':
operation = 'delete'
else:
operation = 'replace'
user = sub_ele(login, 'user', {'operation': operation})
sub_ele(user, 'name').text = item['name']
if operation == 'replace':
sub_ele(user, 'class').text = item['role']
if item.get('full_name'):
sub_ele(user, 'full-name').text = item['full_name']
if item.get('sshkey'):
auth = sub_ele(user, 'authentication')
ssh_rsa = sub_ele(auth, 'ssh-rsa')
key = sub_ele(ssh_rsa, 'name').text = item['sshkey']
return element
def get_param_value(key, item, module):
# if key doesn't exist in the item, get it from module.params
if not item.get(key):
value = module.params[key]
# if key does exist, do a type check on it to validate it
else:
value_type = module.argument_spec[key].get('type', 'str')
type_checker = module._CHECK_ARGUMENT_TYPES_DISPATCHER[value_type]
type_checker(item[key])
value = item[key]
# validate the param value (if validator func exists)
validator = globals().get('validate_%s' % key)
if all((value, validator)):
validator(value, module)
return value
def map_params_to_obj(module):
users = module.params['users']
if not users:
if not module.params['name'] and module.params['purge']:
return list()
elif not module.params['name']:
module.fail_json(msg='missing required argument: name')
else:
collection = [{'name': module.params['name']}]
else:
collection = list()
for item in users:
if not isinstance(item, dict):
collection.append({'username': item})
elif 'name' not in item:
module.fail_json(msg='missing required argument: name')
else:
collection.append(item)
objects = list()
for item in collection:
get_value = partial(get_param_value, item=item, module=module)
item.update({
'full_name': get_value('full_name'),
'role': get_value('role'),
'sshkey': get_value('sshkey'),
'state': get_value('state')
})
for key, value in iteritems(item):
# validate the param value (if validator func exists)
validator = globals().get('validate_%s' % key)
if all((value, validator)):
validator(value, module)
objects.append(item)
return objects
def main():
""" main entry point for module execution
"""
argument_spec = dict(
users=dict(type='list'),
name=dict(),
full_name=dict(),
role=dict(choices=ROLES, default='unauthorized'),
sshkey=dict(),
purge=dict(type='bool'),
state=dict(choices=['present', 'absent'], default='present')
)
mutually_exclusive = [('users', 'name')]
module = AnsibleModule(argument_spec=argument_spec,
mutually_exclusive=mutually_exclusive,
supports_check_mode=True)
result = {'changed': False}
want = map_params_to_obj(module)
ele = map_obj_to_ele(want)
kwargs = {'commit': not module.check_mode}
if module.params['purge']:
kwargs['action'] = 'replace'
diff = load(module, ele, **kwargs)
if diff:
result.update({
'changed': True,
'diff': {'prepared': diff}
})
module.exit_json(**result)
if __name__ == "__main__":
main()

@ -0,0 +1,119 @@
#
# (c) 2016 Red Hat Inc.
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
import sys
import copy
from ansible.plugins.action.normal import ActionModule as _ActionModule
from ansible.utils.path import unfrackpath
from ansible.plugins import connection_loader
from ansible.compat.six import iteritems
from ansible.module_utils.junos import junos_argument_spec
from ansible.module_utils.basic import AnsibleFallbackNotFound
from ansible.module_utils._text import to_bytes
from ncclient.xml_ import new_ele, sub_ele, to_xml
try:
from __main__ import display
except ImportError:
from ansible.utils.display import Display
display = Display()
class ActionModule(_ActionModule):
def run(self, tmp=None, task_vars=None):
if self._play_context.connection != 'local':
return dict(
fail=True,
msg='invalid connection specified, expected connection=local, '
'got %s' % self._play_context.connection
)
provider = self.load_provider()
pc = copy.deepcopy(self._play_context)
pc.network_os = 'junos'
if self._task.action in ('junos_command', 'junos_netconf', 'junos_config', '_junos_template'):
pc.connection = 'network_cli'
pc.port = provider['port'] or self._play_context.port or 22
else:
pc.connection = 'netconf'
pc.port = provider['port'] or self._play_context.port or 830
pc.remote_user = provider['username'] or self._play_context.connection_user
pc.password = provider['password'] or self._play_context.password
pc.private_key_file = provider['ssh_keyfile'] or self._play_context.private_key_file
socket_path = self._get_socket_path(pc)
if not os.path.exists(socket_path):
# start the connection if it isn't started
connection = self._shared_loader_obj.connection_loader.get('persistent', pc, sys.stdin)
if pc.connection == 'network_cli':
rc, out, err = connection.exec_command('show version')
display.vvv('%s %s %s' % (rc, out, err))
if pc.connection == 'netconf':
# <get-software-information />
req = new_ele('get-software-information')
connection.exec_command(to_xml(req))
task_vars['ansible_socket'] = socket_path
return super(ActionModule, self).run(tmp, task_vars)
def _get_socket_path(self, play_context):
ssh = connection_loader.get('ssh', class_only=True)
path = unfrackpath("$HOME/.ansible/pc")
cp = ssh._create_control_path(play_context.remote_addr, play_context.port, play_context.remote_user)
return cp % dict(directory=path)
def load_provider(self):
provider = self._task.args.get('provider', {})
for key, value in iteritems(junos_argument_spec):
if key != 'provider' and key not in provider:
if key in self._task.args:
provider[key] = self._task.args[key]
elif 'fallback' in value:
provider[key] = self._fallback(value['fallback'])
elif key not in provider:
provider[key] = None
return provider
def _fallback(self, fallback):
strategy = fallback[0]
args = []
kwargs = {}
for item in fallback[1:]:
if isinstance(item, dict):
kwargs = item
else:
args = item
try:
return strategy(*args, **kwargs)
except AnsibleFallbackNotFound:
pass

@ -1,5 +1,5 @@
# #
# Copyright 2015 Peter Sprygada <psprygada@ansible.com> # (c) 2017, Red Hat, Inc.
# #
# This file is part of Ansible # This file is part of Ansible
# #
@ -19,10 +19,95 @@
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
from ansible.plugins.action import ActionBase import os
from ansible.plugins.action.net_config import ActionModule as NetActionModule import re
import time
import glob
class ActionModule(NetActionModule, ActionBase): from ansible.plugins.action.junos import ActionModule as _ActionModule
pass from ansible.module_utils._text import to_text
from ansible.module_utils.six.moves.urllib.parse import urlsplit
from ansible.utils.vars import merge_hash
PRIVATE_KEYS_RE = re.compile('__.+__')
class ActionModule(_ActionModule):
def run(self, tmp=None, task_vars=None):
if self._task.args.get('src'):
try:
self._handle_template()
except ValueError as exc:
return dict(failed=True, msg=exc.message)
result = super(ActionModule, self).run(tmp, task_vars)
if self._task.args.get('backup') and result.get('__backup__'):
# User requested backup and no error occurred in module.
# NOTE: If there is a parameter error, _backup key may not be in results.
filepath = self._write_backup(task_vars['inventory_hostname'],
result['__backup__'])
result['backup_path'] = filepath
# strip out any keys that have two leading and two trailing
# underscore characters
for key in result.keys():
if PRIVATE_KEYS_RE.match(key):
del result[key]
return result
def _get_working_path(self):
cwd = self._loader.get_basedir()
if self._task._role is not None:
cwd = self._task._role._role_path
return cwd
def _write_backup(self, host, contents):
backup_path = self._get_working_path() + '/backup'
if not os.path.exists(backup_path):
os.mkdir(backup_path)
for fn in glob.glob('%s/%s*' % (backup_path, host)):
os.remove(fn)
tstamp = time.strftime("%Y-%m-%d@%H:%M:%S", time.localtime(time.time()))
filename = '%s/%s_config.%s' % (backup_path, host, tstamp)
open(filename, 'w').write(contents)
return filename
def _handle_template(self):
src = self._task.args.get('src')
working_path = self._get_working_path()
if os.path.isabs(src) or urlsplit('src').scheme:
source = src
else:
source = self._loader.path_dwim_relative(working_path, 'templates', src)
if not source:
source = self._loader.path_dwim_relative(working_path, src)
if not os.path.exists(source):
raise ValueError('path specified in src not found')
try:
with open(source, 'r') as f:
template_data = to_text(f.read())
except IOError:
return dict(failed=True, msg='unable to load src file')
# Create a template search path in the following order:
# [working_path, self_role_path, dependent_role_paths, dirname(source)]
searchpath = [working_path]
if self._task._role is not None:
searchpath.append(self._task._role._role_path)
if hasattr(self._task, "_block:"):
dep_chain = self._task._block.get_dep_chain()
if dep_chain is not None:
for role in dep_chain:
searchpath.append(role._role_path)
searchpath.append(os.path.dirname(source))
self._templar.environment.loader.searchpath = searchpath
self._task.args['src'] = self._templar.template(template_data)

@ -1,5 +1,5 @@
# #
# Copyright 2015 Peter Sprygada <psprygada@ansible.com> # (c) 2017 Red Hat, Inc.
# #
# This file is part of Ansible # This file is part of Ansible
# #
@ -19,12 +19,18 @@
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
from ansible.plugins.action import ActionBase import os
from ansible.plugins.action.net_template import ActionModule as NetActionModule import time
import glob
import urlparse
class ActionModule(NetActionModule, ActionBase): from ansible.module_utils._text import to_text
from ansible.plugins.action.junos import ActionModule as _ActionModule
class ActionModule(_ActionModule):
def run(self, tmp=None, task_vars=None): def run(self, tmp=None, task_vars=None):
src = self._task.args.get('src') src = self._task.args.get('src')
if self._task.args.get('config_format') is None: if self._task.args.get('config_format') is None:
@ -40,5 +46,72 @@ class ActionModule(NetActionModule, ActionBase):
if self._task.args.get('comment') is None: if self._task.args.get('comment') is None:
self._task.args['comment'] = self._task.name self._task.args['comment'] = self._task.name
return super(ActionModule, self).run(tmp, task_vars) try:
self._handle_template()
except (ValueError, AttributeError) as exc:
return dict(failed=True, msg=exc.message)
result = super(ActionModule, self).run(tmp, task_vars)
if self._task.args.get('backup') and result.get('__backup__'):
# User requested backup and no error occurred in module.
# NOTE: If there is a parameter error, __backup__ key may not be in results.
self._write_backup(task_vars['inventory_hostname'], result['__backup__'])
if '__backup__' in result:
del result['__backup__']
return result
def _get_working_path(self):
cwd = self._loader.get_basedir()
if self._task._role is not None:
cwd = self._task._role._role_path
return cwd
def _write_backup(self, host, contents):
backup_path = self._get_working_path() + '/backup'
if not os.path.exists(backup_path):
os.mkdir(backup_path)
for fn in glob.glob('%s/%s*' % (backup_path, host)):
os.remove(fn)
tstamp = time.strftime("%Y-%m-%d@%H:%M:%S", time.localtime(time.time()))
filename = '%s/%s_config.%s' % (backup_path, host, tstamp)
open(filename, 'w').write(contents)
def _handle_template(self):
src = self._task.args.get('src')
if not src:
raise ValueError('missing required arguments: src')
working_path = self._get_working_path()
if os.path.isabs(src) or urlparse.urlsplit(src).scheme:
source = src
else:
source = self._loader.path_dwim_relative(working_path, 'templates', src)
if not source:
source = self._loader.path_dwim_relative(working_path, src)
if not os.path.exists(source):
return
try:
with open(source, 'r') as f:
template_data = to_text(f.read())
except IOError:
return dict(failed=True, msg='unable to load src file')
# Create a template search path in the following order:
# [working_path, self_role_path, dependent_role_paths, dirname(source)]
searchpath = [working_path]
if self._task._role is not None:
searchpath.append(self._task._role._role_path)
if hasattr(self._task, "_block:"):
dep_chain = self._task._block.get_dep_chain()
if dep_chain is not None:
for role in dep_chain:
searchpath.append(role._role_path)
searchpath.append(os.path.dirname(source))
self._templar.environment.loader.searchpath = searchpath
self._task.args['src'] = self._templar.template(template_data)

@ -54,10 +54,3 @@ class TerminalModule(TerminalBase):
self._exec_cli_command(c) self._exec_cli_command(c)
except AnsibleConnectionFailure: except AnsibleConnectionFailure:
raise AnsibleConnectionFailure('unable to set terminal parameters') raise AnsibleConnectionFailure('unable to set terminal parameters')
@staticmethod
def guess_network_os(conn):
stdin, stdout, stderr = conn.exec_command('show version')
if 'Junos' in stdout.read():
return 'junos'

@ -18,6 +18,7 @@
/lib/ansible/modules/remote_management/foreman/ /lib/ansible/modules/remote_management/foreman/
/lib/ansible/modules/monitoring/zabbix.*.py /lib/ansible/modules/monitoring/zabbix.*.py
/lib/ansible/modules/network/avi/ /lib/ansible/modules/network/avi/
/lib/ansible/modules/network/junos/
/lib/ansible/modules/network/f5/ /lib/ansible/modules/network/f5/
/lib/ansible/modules/network/nmcli.py /lib/ansible/modules/network/nmcli.py
/lib/ansible/modules/notification/pushbullet.py /lib/ansible/modules/notification/pushbullet.py

@ -0,0 +1,122 @@
# (c) 2016 Red Hat Inc.
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
import json
import sys
from ansible.compat.tests import unittest
from ansible.compat.tests.mock import patch, Mock
from ansible.module_utils import basic
from ansible.module_utils._text import to_bytes
def set_module_args(args):
args = json.dumps({'ANSIBLE_MODULE_ARGS': args})
basic._ANSIBLE_ARGS = to_bytes(args)
fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures')
fixture_data = {}
def load_fixture(name):
path = os.path.join(fixture_path, name)
if path in fixture_data:
return fixture_data[path]
with open(path) as f:
data = f.read()
try:
data = json.loads(data)
except:
pass
fixture_data[path] = data
return data
class AnsibleExitJson(Exception):
pass
class AnsibleFailJson(Exception):
pass
mock_modules = {
'ncclient': Mock(),
'ncclient.xml_': Mock()
}
patch_import = patch.dict('sys.modules', mock_modules)
patch_import.start()
class TestJunosModule(unittest.TestCase):
def execute_module(self, failed=False, changed=False, commands=None,
sort=True, defaults=False):
self.load_fixtures(commands)
if failed:
result = self.failed()
self.assertTrue(result['failed'], result)
else:
result = self.changed(changed)
self.assertEqual(result['changed'], changed, result)
if commands:
if sort:
self.assertEqual(sorted(commands), sorted(result['commands']), result['commands'])
else:
self.assertEqual(commands, result['commands'], result['commands'])
return result
def failed(self):
def fail_json(*args, **kwargs):
kwargs['failed'] = True
raise AnsibleFailJson(kwargs)
with patch.object(basic.AnsibleModule, 'fail_json', fail_json):
with self.assertRaises(AnsibleFailJson) as exc:
self.module.main()
result = exc.exception.args[0]
self.assertTrue(result['failed'], result)
return result
def changed(self, changed=False):
def exit_json(*args, **kwargs):
if 'changed' not in kwargs:
kwargs['changed'] = False
raise AnsibleExitJson(kwargs)
with patch.object(basic.AnsibleModule, 'exit_json', exit_json):
with self.assertRaises(AnsibleExitJson) as exc:
self.module.main()
result = exc.exception.args[0]
self.assertEqual(result['changed'], changed, result)
return result
def load_fixtures(self, commands=None):
pass

@ -19,175 +19,87 @@
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
import os
import sys
import json import json
try:
from StringIO import StringIO
except ImportError:
from io import StringIO
from ansible.compat.tests import unittest
from ansible.compat.tests.mock import patch, MagicMock
from ansible.module_utils import basic
from ansible.module_utils._text import to_bytes
jnpr_mock = MagicMock()
jxmlease_mock = MagicMock()
modules = {
'jnpr': jnpr_mock,
'jnpr.junos': jnpr_mock.junos,
'jnpr.junos.utils': jnpr_mock.junos.utils,
'jnpr.junos.utils.config': jnpr_mock.junos.utils.config,
'jnpr.junos.version': jnpr_mock.junos.version,
'jnpr.junos.exception': jnpr_mock.junos.execption,
'jxmlease': jxmlease_mock
}
setattr(jnpr_mock.junos.version, 'VERSION', '2.0.0')
module_patcher = patch.dict('sys.modules', modules)
module_patcher.start()
from ansible.modules.network.junos import junos_command
def set_module_args(args):
args = json.dumps({'ANSIBLE_MODULE_ARGS': args})
basic._ANSIBLE_ARGS = to_bytes(args)
fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures')
fixture_data = {}
from ansible.compat.tests.mock import patch
from .junos_module import TestJunosModule, load_fixture, set_module_args
def load_fixture(name): from ansible.modules.network.junos import junos_command
path = os.path.join(fixture_path, name)
if path in fixture_data:
return fixture_data[path]
with open(path) as f:
data = f.read()
try:
data = json.loads(data)
except:
pass
fixture_data[path] = data
return data
rpc_command_map = {
'get_software_information': 'show version'
}
class TestJunosCommandModule(TestJunosModule):
class test_junosCommandModule(unittest.TestCase): module = junos_command
def setUp(self): def setUp(self):
self.mock_run_commands = patch('ansible.module_utils.junos.Netconf.run_commands') self.mock_run_commands = patch('ansible.modules.network.junos.junos_command.run_commands')
self.run_commands = self.mock_run_commands.start() self.run_commands = self.mock_run_commands.start()
self.mock_connect = patch('ansible.module_utils.junos.Netconf.connect')
self.mock_connect.start()
self.saved_stdout = sys.stdout
def tearDown(self): def tearDown(self):
self.mock_run_commands.stop() self.mock_run_commands.stop()
self.mock_connect.stop()
sys.stdout = self.saved_stdout
def execute_module(self, failed=False, changed=False, fmt='text', cmd_type='commands'): def load_fixtures(self, commands=None):
def load_from_file(*args, **kwargs): def load_from_file(*args, **kwargs):
commands = args[0] module, commands = args
output = list() output = list()
for item in commands: for item in commands:
try: try:
obj = json.loads(str(item)) obj = json.loads(item['command'])
command = obj['command'] command = obj['command']
except ValueError: except ValueError:
command = item command = item['command']
if cmd_type == 'rpcs': filename = 'junos_command_%s.txt' % str(command).replace(' ', '_')
command = rpc_command_map[str(command)]
filename = os.path.join('output',
str(command).replace(' ', '_') + '.{0}'.format(fmt))
output.append(load_fixture(filename)) output.append(load_fixture(filename))
return output return output
self.run_commands.side_effect = load_from_file self.run_commands.side_effect = load_from_file
out = StringIO()
sys.stdout = out
with self.assertRaises(SystemExit):
junos_command.main()
result = json.loads(out.getvalue().strip())
if failed:
self.assertTrue(result.get('failed'))
else:
self.assertEqual(result.get('changed'), changed, result)
return result
def test_junos_command_format_text(self): def test_junos_command_format_text(self):
set_module_args(dict(commands=['show version'], host='test', format='text')) set_module_args(dict(commands=['show version'], display='text'))
result = self.execute_module() result = self.execute_module()
self.assertEqual(len(result['stdout']), 1) self.assertEqual(len(result['stdout']), 1)
self.assertTrue(result['stdout'][0].startswith('Hostname')) self.assertTrue(result['stdout'][0].startswith('Hostname'))
def test_junos_command_multiple(self): def test_junos_command_multiple(self):
set_module_args(dict(commands=['show version', 'show version'], host='test', format='text')) set_module_args(dict(commands=['show version', 'show version'], display='text'))
result = self.execute_module() result = self.execute_module()
self.assertEqual(len(result['stdout']), 2) self.assertEqual(len(result['stdout']), 2)
self.assertTrue(result['stdout'][0].startswith('Hostname')) self.assertTrue(result['stdout'][0].startswith('Hostname'))
def test_junos_command_wait_for(self): def test_junos_command_wait_for(self):
wait_for = 'result[0] contains "Hostname"' wait_for = 'result[0] contains "Hostname"'
set_module_args(dict(commands=['show version'], host='test', wait_for=wait_for, format='text')) set_module_args(dict(commands=['show version'], wait_for=wait_for, display='text'))
self.execute_module() self.execute_module()
def test_junos_command_wait_for_fails(self): def test_junos_command_wait_for_fails(self):
wait_for = 'result[0] contains "test string"' wait_for = 'result[0] contains "test string"'
set_module_args(dict(commands=['show version'], host='test', wait_for=wait_for, format='text')) set_module_args(dict(commands=['show version'], wait_for=wait_for, display='text'))
self.execute_module(failed=True) self.execute_module(failed=True)
self.assertEqual(self.run_commands.call_count, 10) self.assertEqual(self.run_commands.call_count, 10)
def test_junos_command_retries(self): def test_junos_command_retries(self):
wait_for = 'result[0] contains "test string"' wait_for = 'result[0] contains "test string"'
set_module_args(dict(commands=['show version'], host='test', wait_for=wait_for, retries=2, format='text')) set_module_args(dict(commands=['show version'], wait_for=wait_for, retries=2, display='text'))
self.execute_module(failed=True) self.execute_module(failed=True)
self.assertEqual(self.run_commands.call_count, 2) self.assertEqual(self.run_commands.call_count, 2)
def test_junos_command_match_any(self): def test_junos_command_match_any(self):
wait_for = ['result[0] contains "Hostname"', wait_for = ['result[0] contains "Hostname"',
'result[0] contains "test string"'] 'result[0] contains "test string"']
set_module_args(dict(commands=['show version'], host='test', wait_for=wait_for, match='any', format='text')) set_module_args(dict(commands=['show version'], wait_for=wait_for, match='any', display='text'))
self.execute_module() self.execute_module()
def test_junos_command_match_all(self): def test_junos_command_match_all(self):
wait_for = ['result[0] contains "Hostname"', wait_for = ['result[0] contains "Hostname"',
'result[0] contains "Model"'] 'result[0] contains "Model"']
set_module_args(dict(commands=['show version'], host='test', wait_for=wait_for, match='all', format='text')) set_module_args(dict(commands=['show version'], wait_for=wait_for, match='all', display='text'))
self.execute_module() self.execute_module()
def test_junos_command_match_all_failure(self): def test_junos_command_match_all_failure(self):
wait_for = ['result[0] contains "Hostname"', wait_for = ['result[0] contains "Hostname"',
'result[0] contains "test string"'] 'result[0] contains "test string"']
commands = ['show version', 'show version'] commands = ['show version', 'show version']
set_module_args(dict(commands=commands, host='test', wait_for=wait_for, match='all', format='text')) set_module_args(dict(commands=commands, wait_for=wait_for, match='all', display='text'))
self.execute_module(failed=True) self.execute_module(failed=True)
def test_junos_command_rpc_format_text(self):
set_module_args(dict(rpcs=['get_software_information'], host='test', format='text'))
result = self.execute_module(fmt='text', cmd_type='rpcs')
self.assertEqual(len(result['stdout']), 1)
self.assertTrue(result['stdout'][0].startswith('Hostname'))
def test_junos_command_rpc_format_text_multiple(self):
set_module_args(dict(commands=['get_software_information', 'get_software_information'], host='test',
format='text'))
result = self.execute_module(cmd_type='rpcs')
self.assertEqual(len(result['stdout']), 2)
self.assertTrue(result['stdout'][0].startswith('Hostname'))

Loading…
Cancel
Save