1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00

Contributing new module to manage HUAWEI data center CloudEngine switch. (#19793)

* HUAWEI data center CloudEngine switch module

* HUAWEI data center CloudEngine switch module

* HUAWEI data center CloudEngine switch module

* Update __init__.py

* Update __init__.py

* HUAWEI data center CloudEngine switch module

* HUAWEI data center CloudEngine switch module

* Update __init__.py

* Delete __init__.py

* HUAWEI data center CloudEngine switch module

* HUAWEI data center CloudEngine switch module

* modify init file

* Update cloudengine.py

* Update cloudengine.py
This commit is contained in:
Jacky Gao 2017-01-04 11:30:59 +08:00 committed by Peter Sprygada
parent fd3ae0bf80
commit 08e2a5d4fb
4 changed files with 610 additions and 0 deletions

View file

@ -0,0 +1,270 @@
# This code is part of Ansible, but is an independent component.
# This particular file snippet, and this file snippet only, is BSD licensed.
# Modules you write using this snippet, which is embedded dynamically by Ansible
# still belong to the author of the module, and may assign their own license
# to the complete work.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
import re
import time
from ansible.module_utils.basic import json
from ansible.module_utils.network import NetworkError
from ansible.module_utils.network import add_argument,\
register_transport, to_list
from ansible.module_utils.shell import CliBase
try:
from ncclient import manager
HAS_NCCLIENT = True
except ImportError:
HAS_NCCLIENT = False
pass
add_argument('use_ssl', dict(default=False, type='bool'))
add_argument('validate_certs', dict(default=True, type='bool'))
def ce_unknown_host_cb(host, fingerprint):
""" ce_unknown_host_cb """
return True
class CeConfigMixin(object):
""" CeConfigMixin """
def get_config(self, include_defaults=False, include_all=False, regular="", **kwargs):
""" get_config """
cmd = 'display current-configuration '
if include_all:
cmd += ' all'
if include_defaults:
cmd += ' include-default'
if regular:
cmd += ' ' + regular
return self.execute([cmd])[0]
def load_config(self, config):
""" load_config """
checkpoint = 'ansible_%s' % int(time.time())
try:
self.execute(['system-view immediately',
'commit label %s' % checkpoint], output='text')
except TypeError:
self.execute(['system-view immediately',
'commit label %s' % checkpoint])
try:
try:
self.configure(config)
except NetworkError:
self.load_checkpoint(checkpoint)
raise
finally:
# get commit id and clear it
responses = self.execute(
'display configuration commit list '
'label | include %s' % checkpoint)
match = re.match(
r'[\r\n]?\d+\s+(\d{10})\s+ansible.*', responses[0])
if match is not None:
try:
self.execute(['return',
'clear configuration commit %s '
'label' % match.group(1)], output='text')
except TypeError:
self.execute(['return',
'clear configuration commit %s '
'label' % match.group(1)])
def save_config(self, **kwargs):
""" save_config """
try:
self.execute(['return', 'save'], output='text')
except TypeError:
self.execute(['return', 'save'])
def load_checkpoint(self, checkpoint):
""" load_checkpoint """
try:
self.execute(
['return', 'rollback configuration to '
'label %s' % checkpoint], output='text')
except TypeError:
self.execute(
['return', 'rollback configuration to'
' label %s' % checkpoint])
pass
class Netconf(object):
""" Netconf """
def __init__(self, **kwargs):
if not HAS_NCCLIENT:
raise Exception("the ncclient library is required")
self.mc = None
host = kwargs["host"]
port = kwargs["port"]
username = kwargs["username"]
password = kwargs["password"]
self.mc = manager.connect(host=host, port=port,
username=username,
password=password,
unknown_host_cb=ce_unknown_host_cb,
allow_agent=False,
look_for_keys=False,
hostkey_verify=False,
device_params={'name': 'huawei'},
timeout=30)
def __del__(self):
self.mc.close_session()
def set_config(self, **kwargs):
""" set_config """
confstr = kwargs["config"]
con_obj = self.mc.edit_config(target='running', config=confstr)
return con_obj
def get_config(self, **kwargs):
""" get_config """
filterstr = kwargs["filter"]
con_obj = self.mc.get(filter=filterstr)
return con_obj
def execute_action(self, **kwargs):
"""huawei execute-action"""
confstr = kwargs["action"]
con_obj = self.mc.action(action=confstr)
return con_obj
def execute_cli(self, **kwargs):
"""huawei execute-cli"""
confstr = kwargs["command"]
con_obj = self.mc.cli(command=confstr)
return con_obj
def get_netconf(**kwargs):
""" get_netconf """
return Netconf(**kwargs)
class Cli(CeConfigMixin, CliBase):
""" Cli """
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[<|\[]{1}.+[>|\]]{1}(?:\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"),
re.compile(r"Error\[\d+\]: ")
]
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
def connect(self, params, **kwargs):
""" connect """
super(Cli, self).connect(params, kickstart=False, **kwargs)
self.shell.send('screen-length 0 temporary')
self.shell.send('mmi-mode enable')
def run_commands(self, commands):
""" run_commands """
cmds = list(prepare_commands(commands))
responses = self.execute(cmds)
for index, cmd in enumerate(commands):
raw = cmd.args.get('raw') or False
if cmd.output == 'json' and not raw:
try:
responses[index] = json.loads(responses[index])
except ValueError:
raise NetworkError(
msg='unable to load response from device',
response=responses[index], command=str(cmd)
)
return responses
def configure(self, commands, **kwargs):
""" configure """
commands = prepare_config(commands)
responses = self.execute(commands)
responses.pop(0)
return responses
Cli = register_transport('cli', default=True)(Cli)
def prepare_config(commands):
""" prepare_config """
prepared = list()
prepared.extend(to_list(commands))
prepared.append('return')
return prepared
def prepare_commands(commands):
""" prepare_commands """
jsonify = lambda x: '%s | json' % x
for cmd in to_list(commands):
if cmd.output == 'json':
cmd.command_string = jsonify(cmd)
if cmd.command.endswith('| json'):
cmd.output = 'json'
yield cmd

View file

@ -0,0 +1,266 @@
#!/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': 'community',
'version': '1.0'}
DOCUMENTATION = """
---
module: ce_command
version_added: "2.3"
author: "JackyGao2016 (@CloudEngine-Ansible)"
short_description: Run arbitrary command on HUAWEI CloudEngine devices
description:
- Sends an arbitrary command to an HUAWEI CloudEngine node and returns
the results read from the device. The ce_command module includes an
argument that will cause the module to wait for a specific condition
before returning or timing out if the condition is not met.
extends_documentation_fragment: cloudengine
options:
commands:
description:
- The commands to send to the remote HUAWEI CloudEngine device
over the configured provider. The resulting output from the
command is returned. If the I(wait_for) argument is provided,
the module is not returned until the condition is satisfied
or the number of I(retries) has been exceeded.
required: true
wait_for:
description:
- Specifies what to evaluate from the output of the command
and what conditionals to apply. This argument will cause
the task to wait for a particular conditional to be true
before moving forward. If the conditional is not true
by the configured retries, the task fails. See examples.
required: false
default: null
aliases: ['waitfor']
match:
description:
- The I(match) argument is used in conjunction with the
I(wait_for) argument to specify the match policy. Valid
values are C(all) or C(any). If the value is set to C(all)
then all conditionals in the I(wait_for) must be satisfied. If
the value is set to C(any) then only one of the values must be
satisfied.
required: false
default: all
retries:
description:
- Specifies the number of retries a command should by tried
before it is considered failed. The command is run on the
target device every retry and evaluated against the I(wait_for)
conditionals.
required: false
default: 10
interval:
description:
- Configures the interval in seconds to wait between retries
of the command. If the command does not pass the specified
conditional, the interval indicates how to long to wait before
trying the command again.
required: false
default: 1
"""
EXAMPLES = """
# Note: examples below use the following provider dict to handle
# transport and authentication to the node.
vars:
cli:
host: "{{ inventory_hostname }}"
username: admin
password: admin
transport: cli
- name: run display version on remote devices
ce_command:
commands: display version
provider: "{{ cli }}"
- name: run display version and check to see if output contains HUAWEI
ce_command:
commands: display version
wait_for: result[0] contains HUAWEI
provider: "{{ cli }}"
- name: run multiple commands on remote nodes
ce_command:
commands:
- display version
- display device
provider: "{{ cli }}"
- name: run multiple commands and evaluate the output
ce_command:
commands:
- display version
- display device
wait_for:
- result[0] contains HUAWEI
- result[1] contains Device
provider: "{{ cli }}"
- name: run commands and specify the output format
ce_command:
commands:
- command: display version
output: json
provider: "{{ cli }}"
"""
RETURN = """
stdout:
description: the set of responses from the commands
returned: always
type: list
sample: ['...', '...']
stdout_lines:
description: The value of stdout split into a list
returned: always
type: list
sample: [['...', '...'], ['...'], ['...']]
failed_conditions:
description: the conditionals that failed
returned: failed
type: list
sample: ['...', '...']
"""
from ansible.module_utils.basic import get_exception
from ansible.module_utils.network import NetworkModule, NetworkError
from ansible.module_utils.netcli import CommandRunner
from ansible.module_utils.netcli import FailedConditionsError
from ansible.module_utils.netcli import FailedConditionalError
from ansible.module_utils.netcli import AddCommandError, AddConditionError
import ansible.module_utils.cloudengine
VALID_KEYS = ['command', 'output', 'prompt', 'response']
def to_lines(stdout):
""" to lines """
for item in stdout:
if isinstance(item, basestring):
item = str(item).split('\n')
yield item
def parse_commands(module):
""" parse commands """
for cmd in module.params['commands']:
if isinstance(cmd, basestring):
cmd = dict(command=cmd, output=None)
elif 'command' not in cmd:
module.fail_json(msg='command keyword argument is required')
elif cmd.get('output') not in [None, 'text', 'json']:
module.fail_json(msg='invalid output specified for command')
elif not set(cmd.keys()).issubset(VALID_KEYS):
module.fail_json(msg='unknown keyword specified')
yield cmd
def main():
""" main """
spec = dict(
commands=dict(type='list', required=True),
wait_for=dict(type='list', aliases=['waitfor']),
match=dict(default='all', choices=['any', 'all']),
retries=dict(default=10, type='int'),
interval=dict(default=1, type='int')
)
module = NetworkModule(argument_spec=spec,
supports_check_mode=True)
commands = list(parse_commands(module))
conditionals = module.params['wait_for'] or list()
warnings = list()
runner = CommandRunner(module)
for cmd in commands:
if module.check_mode and not cmd['command'].startswith('dis'):
warnings.append('only display commands are supported when using '
'check mode, not executing `%s`' % cmd['command'])
else:
if cmd['command'].startswith('sys'):
module.fail_json(msg='ce_command does not support running '
'config mode commands. Please use '
'ce_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 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), **exc.kwargs)
result = dict(changed=False)
result['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)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,74 @@
#
#
# 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/>.
class ModuleDocFragment(object):
# Standard files documentation fragment
DOCUMENTATION = """
options:
host:
description:
- Specifies the DNS host name or address for connecting to the remote
device over the specified transport. The value of host is used as
the destination address for the transport.
required: true
port:
description:
- Specifies the port to use when building the connection to the remote
device. This value applies to either I(cli) or I(netconf). The port
value will default to the appropriate transport common port if
none is provided in the task. (cli=22, netconf=22).
required: false
default: 0 (use common port)
username:
description:
- Configures the username to use to authenticate the connection to
the remote device. This value is used to authenticate the CLI login.
If the value is not specified in the task, the value of environment
variable C(ANSIBLE_NET_USERNAME) will be used instead.
required: false
password:
description:
- Specifies the password to use to authenticate the connection to
the remote device. This is a common argument used for cli
transports. If the value is not specified in the task, the
value of environment variable C(ANSIBLE_NET_PASSWORD) will be used instead.
required: false
default: null
ssh_keyfile:
description:
- Specifies the SSH key to use to authenticate the connection to
the remote device. This argument is used for the I(cli)
transport. If the value is not specified in the task, the
value of environment variable C(ANSIBLE_NET_SSH_KEYFILE) will be used instead.
required: false
transport:
description:
- Configures the transport connection to use when connecting to the
remote device. The transport argument supports connectivity to the
device over cli (ssh).
required: true
default: cli
provider:
description:
- Convenience method that allows all I(cloudengine) arguments to be passed as
a dict object. All constraints (required, choices, etc) must be
met either by individual arguments or values in this dict.
required: false
default: null
"""