diff --git a/salt/modules/napalm_network.py b/salt/modules/napalm_network.py index d45287d5cf..ee9c739cb7 100644 --- a/salt/modules/napalm_network.py +++ b/salt/modules/napalm_network.py @@ -95,6 +95,25 @@ def _filter_dict(input_dict, search_key, search_value): return output_dict +def _explicit_close(napalm_device): + ''' + Will explicitely close the config session with the network device, + when running in a now-always-alive proxy minion or regular minion. + This helper must be used in configuration-related functions, + as the session is preserved and not closed before making any changes. + ''' + if salt.utils.napalm.not_always_alive(__opts__): + # force closing the configuration session + # when running in a non-always-alive proxy + # or regular minion + try: + napalm_device['DRIVER'].close() + except Exception as err: + log.error('Unable to close the temp connection with the device:') + log.error(err) + log.error('Please report.') + + def _config_logic(napalm_device, loaded_result, test=False, @@ -131,7 +150,6 @@ def _config_logic(napalm_device, loaded_result.pop('out', '') # not needed _loaded_res = loaded_result.get('result', False) - if not _loaded_res or test: # if unable to load the config (errors / warnings) # or in testing mode, @@ -147,11 +165,13 @@ def _config_logic(napalm_device, loaded_result['result'] = False # make sure it notifies # that something went wrong + _explicit_close(napalm_device) return loaded_result loaded_result['comment'] += 'Configuration discarded.' # loaded_result['result'] = False not necessary # as the result can be true when test=True + _explicit_close(napalm_device) return loaded_result if not test and commit_config: @@ -181,10 +201,11 @@ def _config_logic(napalm_device, else 'Unable to discard config.' loaded_result['result'] = False # notify if anything goes wrong + _explicit_close(napalm_device) return loaded_result loaded_result['already_configured'] = True loaded_result['comment'] = 'Already configured.' - + _explicit_close(napalm_device) return loaded_result @@ -841,6 +862,15 @@ def load_config(filename=None, fun = 'load_merge_candidate' if replace: fun = 'load_replace_candidate' + if salt.utils.napalm.not_always_alive(__opts__): + # if a not-always-alive proxy + # or regular minion + # do not close the connection after loading the config + # this will be handled in _config_logic + # after running the other features: + # compare_config, discard / commit + # which have to be over the same session + napalm_device['CLOSE'] = False # pylint: disable=undefined-variable _loaded = salt.utils.napalm.call( napalm_device, # pylint: disable=undefined-variable fun, @@ -1207,6 +1237,15 @@ def load_template(template_name, fun = 'load_merge_candidate' if replace: # replace requested fun = 'load_replace_candidate' + if salt.utils.napalm.not_always_alive(__opts__): + # if a not-always-alive proxy + # or regular minion + # do not close the connection after loading the config + # this will be handled in _config_logic + # after running the other features: + # compare_config, discard / commit + # which have to be over the same session + napalm_device['CLOSE'] = False # pylint: disable=undefined-variable _loaded = salt.utils.napalm.call( napalm_device, # pylint: disable=undefined-variable fun, @@ -1228,12 +1267,21 @@ def load_template(template_name, 'opts': __opts__ # inject opts content } ) + if salt.utils.napalm.not_always_alive(__opts__): + # if a not-always-alive proxy + # or regular minion + # do not close the connection after loading the config + # this will be handled in _config_logic + # after running the other features: + # compare_config, discard / commit + # which have to be over the same session + # so we'll set the CLOSE global explicitely as False + napalm_device['CLOSE'] = False # pylint: disable=undefined-variable _loaded = salt.utils.napalm.call( napalm_device, # pylint: disable=undefined-variable 'load_template', **load_templates_params ) - return _config_logic(napalm_device, # pylint: disable=undefined-variable _loaded, test=test, diff --git a/salt/proxy/napalm.py b/salt/proxy/napalm.py index f4b6b19502..4d51479ef3 100644 --- a/salt/proxy/napalm.py +++ b/salt/proxy/napalm.py @@ -129,6 +129,9 @@ def alive(opts): .. versionadded:: Nitrogen ''' + if salt.utils.napalm.not_always_alive(opts): + return True # don't force reconnection for not-always alive proxies + # or regular minion is_alive_ret = call('is_alive', **{}) if not is_alive_ret.get('result', False): log.debug('[{proxyid}] Unable to execute `is_alive`: {comment}'.format( diff --git a/salt/utils/napalm.py b/salt/utils/napalm.py index 817d71ad3f..f17e0a35cb 100644 --- a/salt/utils/napalm.py +++ b/salt/utils/napalm.py @@ -19,6 +19,7 @@ from __future__ import absolute_import import traceback import logging +from functools import wraps log = logging.getLogger(__file__) import salt.utils @@ -44,6 +45,20 @@ def is_proxy(opts): return salt.utils.is_proxy() and opts.get('proxy', {}).get('proxytype') == 'napalm' +def is_always_alive(opts): + ''' + Is always alive required? + ''' + return opts.get('proxy', {}).get('always_alive', True) + + +def not_always_alive(opts): + ''' + Should this proxy be always alive? + ''' + return (is_proxy(opts) and not is_always_alive(opts)) or is_minion(opts) + + def is_minion(opts): ''' Is this a NAPALM straight minion? @@ -113,6 +128,7 @@ def call(napalm_device, method, *args, **kwargs): ''' result = False out = None + opts = napalm_device.get('__opts__', {}) try: if not napalm_device.get('UP', False): raise Exception('not connected') @@ -153,6 +169,13 @@ def call(napalm_device, method, *args, **kwargs): 'comment': comment, 'traceback': err_tb } + finally: + if opts and not_always_alive(opts) and napalm_device.get('CLOSE', True): + # either running in a not-always-alive proxy + # either running in a regular minion + # close the connection when the call is over + # unless the CLOSE is explicitely set as False + napalm_device['DRIVER'].close() return { 'out': out, 'result': result, @@ -172,7 +195,7 @@ def get_device(opts, salt_obj=None): device_dict = opts.get('proxy', {}) or opts.get('napalm', {}) if salt_obj and not device_dict: # get the connection details from the opts - device_dict = salt_obj['config.option']('napalm') + device_dict = salt_obj['config.merge']('napalm') if not device_dict: # still not able to setup log.error('Incorrect minion config. Please specify at least the napalm driver name!') @@ -183,8 +206,9 @@ def get_device(opts, salt_obj=None): network_device['PASSWORD'] = device_dict.get('passwd') or device_dict.get('password') or device_dict.get('pass') network_device['TIMEOUT'] = device_dict.get('timeout', 60) network_device['OPTIONAL_ARGS'] = device_dict.get('optional_args', {}) + network_device['ALWAYS_ALIVE'] = device_dict.get('always_alive', True) network_device['UP'] = False - # get driver object form NAPALM + # get driver object from NAPALM if 'config_lock' not in list(network_device['OPTIONAL_ARGS'].keys()): network_device['OPTIONAL_ARGS']['config_lock'] = False _driver_ = napalm_base.get_network_driver(network_device.get('DRIVER_NAME')) @@ -226,20 +250,25 @@ def proxy_napalm_wrap(func): :param func: :return: ''' + @wraps(func) def func_wrapper(*args, **kwargs): wrapped_global_namespace = func.__globals__ - # get __proxy__ from func_globals + # get __opts__ and __proxy__ from func_globals proxy = wrapped_global_namespace.get('__proxy__') - + opts = wrapped_global_namespace.get('__opts__') # in any case, will inject the `napalm_device` global # the execution modules will make use of this variable from now on # previously they were accessing the device properties through the __proxy__ object - if salt.utils.is_proxy(): + always_alive = opts.get('proxy', {}).get('always_alive', True) + if salt.utils.is_proxy() and always_alive: + # if it is running in a proxy and it's using the default always alive behaviour, + # will get the cached copy of the network device wrapped_global_namespace['napalm_device'] = proxy['napalm.get_device']() - else: - # get __opts__ and __salt__ from func_globals - opts = wrapped_global_namespace.get('__opts__') - _salt_obj = wrapped_global_namespace.get('__salt__') + elif salt.utils.is_proxy() and not always_alive: + # if still proxy, but the user does not want the SSH session always alive + # get a new device instance + # which establishes a new connection + # which is closed just before the call() function defined above returns if 'inherit_napalm_device' not in kwargs or ('inherit_napalm_device' in kwargs and not kwargs['inherit_napalm_device']): # try to open a new connection @@ -247,8 +276,9 @@ def proxy_napalm_wrap(func): # for configuration management this is very important, # in order to make sure we are editing the same session. try: - wrapped_global_namespace['napalm_device'] = get_device(opts, salt_obj=_salt_obj) + wrapped_global_namespace['napalm_device'] = get_device(opts) except napalm_base.exceptions.ConnectionException as nce: + log.error(nce) return '{base_msg}. See log for details.'.format( base_msg=str(nce.msg) ) @@ -260,5 +290,35 @@ def proxy_napalm_wrap(func): # as all actions must be issued within the same configuration session # otherwise we risk to open multiple sessions wrapped_global_namespace['napalm_device'] = kwargs['inherit_napalm_device'] + else: + # if no proxy + # thus it is running on a regular minion, directly on the network device + # get __salt__ from func_globals + _salt_obj = wrapped_global_namespace.get('__salt__') + if 'inherit_napalm_device' not in kwargs or ('inherit_napalm_device' in kwargs and + not kwargs['inherit_napalm_device']): + # try to open a new connection + # but only if the function does not inherit the napalm driver + # for configuration management this is very important, + # in order to make sure we are editing the same session. + try: + wrapped_global_namespace['napalm_device'] = get_device(opts, salt_obj=_salt_obj) + except napalm_base.exceptions.ConnectionException as nce: + log.error(nce) + return '{base_msg}. See log for details.'.format( + base_msg=str(nce.msg) + ) + else: + # in case the `inherit_napalm_device` is set + # and it also has a non-empty value, + # the global var `napalm_device` will be overriden. + # this is extremely important for configuration-related features + # as all actions must be issued within the same configuration session + # otherwise we risk to open multiple sessions + wrapped_global_namespace['napalm_device'] = kwargs['inherit_napalm_device'] + if not_always_alive(opts): + # inject the __opts__ only when not always alive + # otherwise, we don't want to overload the always-alive proxies + wrapped_global_namespace['napalm_device']['__opts__'] = opts return func(*args, **kwargs) return func_wrapper