mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-07-22 12:50:22 -07:00
Remove cliconf from httpapi connection (#46813)
* Bare minimum rip out cliconf * nxapi changeover * Update documentation, move options * Memoize device_info * Gratuitous rename to underscore use of local api implementation Fixup eos module_utils like nxos * Streamline version and image scans * Expose get_capabilities through module_utils * Add load_config to module_utils * Support rpcs using both args and kwargs * Add get_config for nxos * Add get_diff * module context, pulled from nxapi We could probably do this correctly later * Fix eos issues * Limit connection._sub_plugin to only one plugin
This commit is contained in:
parent
32dbb99bb8
commit
02432565cd
14 changed files with 568 additions and 255 deletions
|
@ -100,10 +100,7 @@ def exec_command(module, command):
|
|||
def request_builder(method_, *args, **kwargs):
|
||||
reqid = str(uuid.uuid4())
|
||||
req = {'jsonrpc': '2.0', 'method': method_, 'id': reqid}
|
||||
|
||||
params = args or kwargs or None
|
||||
if params:
|
||||
req['params'] = params
|
||||
req['params'] = (args, kwargs)
|
||||
|
||||
return req
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
# 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 json
|
||||
import os
|
||||
import time
|
||||
|
||||
|
@ -99,10 +100,15 @@ def get_connection(module):
|
|||
global _DEVICE_CONNECTION
|
||||
if not _DEVICE_CONNECTION:
|
||||
load_params(module)
|
||||
if is_eapi(module):
|
||||
conn = Eapi(module)
|
||||
if is_local_eapi(module):
|
||||
conn = LocalEapi(module)
|
||||
else:
|
||||
conn = Cli(module)
|
||||
connection_proxy = Connection(module._socket_path)
|
||||
cap = json.loads(connection_proxy.get_capabilities())
|
||||
if cap['network_api'] == 'cliconf':
|
||||
conn = Cli(module)
|
||||
elif cap['network_api'] == 'eapi':
|
||||
conn = HttpApi(module)
|
||||
_DEVICE_CONNECTION = conn
|
||||
return _DEVICE_CONNECTION
|
||||
|
||||
|
@ -180,7 +186,7 @@ class Cli:
|
|||
return diff
|
||||
|
||||
|
||||
class Eapi:
|
||||
class LocalEapi:
|
||||
|
||||
def __init__(self, module):
|
||||
self._module = module
|
||||
|
@ -394,18 +400,187 @@ class Eapi:
|
|||
return diff
|
||||
|
||||
|
||||
class HttpApi:
|
||||
def __init__(self, module):
|
||||
self._module = module
|
||||
self._device_configs = {}
|
||||
self._session_support = None
|
||||
self._connection_obj = None
|
||||
|
||||
@property
|
||||
def _connection(self):
|
||||
if not self._connection_obj:
|
||||
self._connection_obj = Connection(self._module._socket_path)
|
||||
|
||||
return self._connection_obj
|
||||
|
||||
def run_commands(self, commands, check_rc=True):
|
||||
"""Runs list of commands on remote device and returns results
|
||||
"""
|
||||
output = None
|
||||
queue = list()
|
||||
responses = list()
|
||||
|
||||
def run_queue(queue, output):
|
||||
try:
|
||||
response = to_list(self._connection.send_request(queue, output=output))
|
||||
except Exception as exc:
|
||||
if check_rc:
|
||||
raise
|
||||
return to_text(exc)
|
||||
|
||||
if output == 'json':
|
||||
response = [json.loads(item) for item in response]
|
||||
return response
|
||||
|
||||
for item in to_list(commands):
|
||||
cmd_output = 'text'
|
||||
if isinstance(item, dict):
|
||||
command = item['command']
|
||||
if 'output' in item:
|
||||
cmd_output = item['output']
|
||||
else:
|
||||
command = item
|
||||
|
||||
# Emulate '| json' from CLI
|
||||
if is_json(command):
|
||||
command = command.rsplit('|', 1)[0]
|
||||
cmd_output = 'json'
|
||||
|
||||
if output and output != cmd_output:
|
||||
responses.extend(run_queue(queue, output))
|
||||
queue = list()
|
||||
|
||||
output = cmd_output
|
||||
queue.append(command)
|
||||
|
||||
if queue:
|
||||
responses.extend(run_queue(queue, output))
|
||||
|
||||
return responses
|
||||
|
||||
def get_config(self, flags=None):
|
||||
"""Retrieves the current config from the device or cache
|
||||
"""
|
||||
flags = [] if flags is None else flags
|
||||
|
||||
cmd = 'show running-config '
|
||||
cmd += ' '.join(flags)
|
||||
cmd = cmd.strip()
|
||||
|
||||
try:
|
||||
return self._device_configs[cmd]
|
||||
except KeyError:
|
||||
try:
|
||||
out = self._connection.send_request(cmd)
|
||||
except ConnectionError as exc:
|
||||
self._module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
|
||||
|
||||
cfg = to_text(out).strip()
|
||||
self._device_configs[cmd] = cfg
|
||||
return cfg
|
||||
|
||||
def get_diff(self, candidate=None, running=None, diff_match='line', diff_ignore_lines=None, path=None, diff_replace='line'):
|
||||
diff = {}
|
||||
|
||||
# prepare candidate configuration
|
||||
candidate_obj = NetworkConfig(indent=3)
|
||||
candidate_obj.load(candidate)
|
||||
|
||||
if running and diff_match != 'none' and diff_replace != 'config':
|
||||
# running configuration
|
||||
running_obj = NetworkConfig(indent=3, contents=running, ignore_lines=diff_ignore_lines)
|
||||
configdiffobjs = candidate_obj.difference(running_obj, path=path, match=diff_match, replace=diff_replace)
|
||||
|
||||
else:
|
||||
configdiffobjs = candidate_obj.items
|
||||
|
||||
diff['config_diff'] = dumps(configdiffobjs, 'commands') if configdiffobjs else {}
|
||||
return diff
|
||||
|
||||
def load_config(self, config, commit=False, replace=False):
|
||||
"""Loads the configuration onto the remote devices
|
||||
|
||||
If the device doesn't support configuration sessions, this will
|
||||
fallback to using configure() to load the commands. If that happens,
|
||||
there will be no returned diff or session values
|
||||
"""
|
||||
return self.edit_config(config, commit, replace)
|
||||
|
||||
def edit_config(self, config, commit=False, replace=False):
|
||||
"""Loads the configuration onto the remote devices
|
||||
|
||||
If the device doesn't support configuration sessions, this will
|
||||
fallback to using configure() to load the commands. If that happens,
|
||||
there will be no returned diff or session values
|
||||
"""
|
||||
session = 'ansible_%s' % int(time.time())
|
||||
result = {'session': session}
|
||||
banner_cmd = None
|
||||
banner_input = []
|
||||
|
||||
commands = ['configure session %s' % session]
|
||||
if replace:
|
||||
commands.append('rollback clean-config')
|
||||
|
||||
for command in config:
|
||||
if command.startswith('banner'):
|
||||
banner_cmd = command
|
||||
banner_input = []
|
||||
elif banner_cmd:
|
||||
if command == 'EOF':
|
||||
command = {'cmd': banner_cmd, 'input': '\n'.join(banner_input)}
|
||||
banner_cmd = None
|
||||
commands.append(command)
|
||||
else:
|
||||
banner_input.append(command)
|
||||
continue
|
||||
else:
|
||||
commands.append(command)
|
||||
|
||||
try:
|
||||
response = self._connection.send_request(commands)
|
||||
except Exception:
|
||||
commands = ['configure session %s' % session, 'abort']
|
||||
response = self._connection.send_request(commands, output='text')
|
||||
raise
|
||||
|
||||
commands = ['configure session %s' % session, 'show session-config diffs']
|
||||
if commit:
|
||||
commands.append('commit')
|
||||
else:
|
||||
commands.append('abort')
|
||||
|
||||
response = self._connection.send_request(commands, output='text')
|
||||
diff = response[1].strip()
|
||||
if diff:
|
||||
result['diff'] = diff
|
||||
|
||||
return result
|
||||
|
||||
def get_capabilities(self):
|
||||
"""Returns platform info of the remove device
|
||||
"""
|
||||
try:
|
||||
capabilities = self._connection.get_capabilities()
|
||||
except ConnectionError as exc:
|
||||
self._module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
|
||||
|
||||
return json.loads(capabilities)
|
||||
|
||||
|
||||
def is_json(cmd):
|
||||
return to_native(cmd, errors='surrogate_then_replace').endswith('| json')
|
||||
return to_text(cmd, errors='surrogate_then_replace').endswith('| json')
|
||||
|
||||
|
||||
def is_eapi(module):
|
||||
def is_local_eapi(module):
|
||||
transport = module.params['transport']
|
||||
provider_transport = (module.params['provider'] or {}).get('transport')
|
||||
return 'eapi' in (transport, provider_transport)
|
||||
|
||||
|
||||
def to_command(module, commands):
|
||||
if is_eapi(module):
|
||||
if is_local_eapi(module):
|
||||
default_output = 'json'
|
||||
else:
|
||||
default_output = 'text'
|
||||
|
|
|
@ -105,10 +105,15 @@ def get_connection(module):
|
|||
global _DEVICE_CONNECTION
|
||||
if not _DEVICE_CONNECTION:
|
||||
load_params(module)
|
||||
if is_nxapi(module):
|
||||
conn = Nxapi(module)
|
||||
if is_local_nxapi(module):
|
||||
conn = LocalNxapi(module)
|
||||
else:
|
||||
conn = Cli(module)
|
||||
connection_proxy = Connection(module._socket_path)
|
||||
cap = json.loads(connection_proxy.get_capabilities())
|
||||
if cap['network_api'] == 'cliconf':
|
||||
conn = Cli(module)
|
||||
elif cap['network_api'] == 'nxapi':
|
||||
conn = HttpApi(module)
|
||||
_DEVICE_CONNECTION = conn
|
||||
return _DEVICE_CONNECTION
|
||||
|
||||
|
@ -244,7 +249,7 @@ class Cli:
|
|||
return None
|
||||
|
||||
|
||||
class Nxapi:
|
||||
class LocalNxapi:
|
||||
|
||||
OUTPUT_TO_COMMAND_TYPE = {
|
||||
'text': 'cli_show_ascii',
|
||||
|
@ -496,22 +501,178 @@ class Nxapi:
|
|||
return None
|
||||
|
||||
|
||||
class HttpApi:
|
||||
def __init__(self, module):
|
||||
self._module = module
|
||||
self._device_configs = {}
|
||||
self._module_context = {}
|
||||
self._connection_obj = None
|
||||
|
||||
@property
|
||||
def _connection(self):
|
||||
if not self._connection_obj:
|
||||
self._connection_obj = Connection(self._module._socket_path)
|
||||
|
||||
return self._connection_obj
|
||||
|
||||
def run_commands(self, commands, check_rc=True):
|
||||
"""Runs list of commands on remote device and returns results
|
||||
"""
|
||||
try:
|
||||
out = self._connection.send_request(commands)
|
||||
except ConnectionError as exc:
|
||||
if check_rc is True:
|
||||
raise
|
||||
out = to_text(exc)
|
||||
|
||||
out = to_list(out)
|
||||
if not out[0]:
|
||||
return out
|
||||
|
||||
for index, response in enumerate(out):
|
||||
if response[0] == '{':
|
||||
out[index] = json.loads(response)
|
||||
|
||||
return out
|
||||
|
||||
def get_config(self, flags=None):
|
||||
"""Retrieves the current config from the device or cache
|
||||
"""
|
||||
flags = [] if flags is None else flags
|
||||
|
||||
cmd = 'show running-config '
|
||||
cmd += ' '.join(flags)
|
||||
cmd = cmd.strip()
|
||||
|
||||
try:
|
||||
return self._device_configs[cmd]
|
||||
except KeyError:
|
||||
try:
|
||||
out = self._connection.send_request(cmd)
|
||||
except ConnectionError as exc:
|
||||
self._module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
|
||||
|
||||
cfg = to_text(out).strip()
|
||||
self._device_configs[cmd] = cfg
|
||||
return cfg
|
||||
|
||||
def get_diff(self, candidate=None, running=None, diff_match='line', diff_ignore_lines=None, path=None, diff_replace='line'):
|
||||
diff = {}
|
||||
|
||||
# prepare candidate configuration
|
||||
candidate_obj = NetworkConfig(indent=2)
|
||||
candidate_obj.load(candidate)
|
||||
|
||||
if running and diff_match != 'none' and diff_replace != 'config':
|
||||
# running configuration
|
||||
running_obj = NetworkConfig(indent=2, contents=running, ignore_lines=diff_ignore_lines)
|
||||
configdiffobjs = candidate_obj.difference(running_obj, path=path, match=diff_match, replace=diff_replace)
|
||||
|
||||
else:
|
||||
configdiffobjs = candidate_obj.items
|
||||
|
||||
diff['config_diff'] = dumps(configdiffobjs, 'commands') if configdiffobjs else ''
|
||||
return diff
|
||||
|
||||
def load_config(self, commands, return_error=False, opts=None, replace=None):
|
||||
"""Sends the ordered set of commands to the device
|
||||
"""
|
||||
if opts is None:
|
||||
opts = {}
|
||||
|
||||
responses = []
|
||||
try:
|
||||
resp = self.edit_config(commands, replace=replace)
|
||||
except ConnectionError as exc:
|
||||
code = getattr(exc, 'code', 1)
|
||||
message = getattr(exc, 'err', exc)
|
||||
err = to_text(message, errors='surrogate_then_replace')
|
||||
if opts.get('ignore_timeout') and code:
|
||||
responses.append(code)
|
||||
return responses
|
||||
elif code and 'no graceful-restart' in err:
|
||||
if 'ISSU/HA will be affected if Graceful Restart is disabled' in err:
|
||||
msg = ['']
|
||||
responses.extend(msg)
|
||||
return responses
|
||||
else:
|
||||
self._module.fail_json(msg=err)
|
||||
elif code:
|
||||
self._module.fail_json(msg=err)
|
||||
|
||||
responses.extend(resp)
|
||||
return responses
|
||||
|
||||
def edit_config(self, candidate=None, commit=True, replace=None, comment=None):
|
||||
resp = list()
|
||||
|
||||
self.check_edit_config_capability(candidate, commit, replace, comment)
|
||||
|
||||
if replace:
|
||||
candidate = 'config replace {0}'.format(replace)
|
||||
|
||||
responses = self._connection.send_request(candidate, output='config')
|
||||
for response in to_list(responses):
|
||||
if response != '{}':
|
||||
resp.append(response)
|
||||
if not resp:
|
||||
resp = ['']
|
||||
|
||||
return resp
|
||||
|
||||
def get_capabilities(self):
|
||||
"""Returns platform info of the remove device
|
||||
"""
|
||||
try:
|
||||
capabilities = self._connection.get_capabilities()
|
||||
except ConnectionError as exc:
|
||||
self._module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
|
||||
|
||||
return json.loads(capabilities)
|
||||
|
||||
def check_edit_config_capability(self, candidate=None, commit=True, replace=None, comment=None):
|
||||
operations = self._connection.get_device_operations()
|
||||
|
||||
if not candidate and not replace:
|
||||
raise ValueError("must provide a candidate or replace to load configuration")
|
||||
|
||||
if commit not in (True, False):
|
||||
raise ValueError("'commit' must be a bool, got %s" % commit)
|
||||
|
||||
if replace and not operations.get('supports_replace'):
|
||||
raise ValueError("configuration replace is not supported")
|
||||
|
||||
if comment and not operations.get('supports_commit_comment', False):
|
||||
raise ValueError("commit comment is not supported")
|
||||
|
||||
def read_module_context(self, module_key):
|
||||
if self._module_context.get(module_key):
|
||||
return self._module_context[module_key]
|
||||
|
||||
return None
|
||||
|
||||
def save_module_context(self, module_key, module_context):
|
||||
self._module_context[module_key] = module_context
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def is_json(cmd):
|
||||
return str(cmd).endswith('| json')
|
||||
return to_text(cmd).endswith('| json')
|
||||
|
||||
|
||||
def is_text(cmd):
|
||||
return not is_json(cmd)
|
||||
|
||||
|
||||
def is_nxapi(module):
|
||||
def is_local_nxapi(module):
|
||||
transport = module.params['transport']
|
||||
provider_transport = (module.params['provider'] or {}).get('transport')
|
||||
return 'nxapi' in (transport, provider_transport)
|
||||
|
||||
|
||||
def to_command(module, commands):
|
||||
if is_nxapi(module):
|
||||
if is_local_nxapi(module):
|
||||
default_output = 'json'
|
||||
else:
|
||||
default_output = 'text'
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue