golden hour
/opt/saltstack/salt/lib/python3.10/site-packages/salt/utils
⬆️ Go Up
Upload
File/Folder
Size
Actions
__init__.py
237 B
Del
OK
__pycache__
-
Del
OK
aggregation.py
5.17 KB
Del
OK
ansible.py
1.48 KB
Del
OK
args.py
18.33 KB
Del
OK
asynchronous.py
4.06 KB
Del
OK
atomicfile.py
5.33 KB
Del
OK
aws.py
20.37 KB
Del
OK
azurearm.py
11.42 KB
Del
OK
beacons.py
517 B
Del
OK
boto3mod.py
8.36 KB
Del
OK
boto_elb_tag.py
3.02 KB
Del
OK
botomod.py
7.98 KB
Del
OK
cache.py
11.49 KB
Del
OK
channel.py
489 B
Del
OK
cloud.py
116.3 KB
Del
OK
color.py
2.72 KB
Del
OK
compat.py
1.89 KB
Del
OK
configcomparer.py
3.88 KB
Del
OK
configparser.py
10.82 KB
Del
OK
context.py
6.8 KB
Del
OK
crypt.py
5 KB
Del
OK
ctx.py
1.42 KB
Del
OK
data.py
53.03 KB
Del
OK
dateutils.py
2.3 KB
Del
OK
debug.py
4.19 KB
Del
OK
decorators
-
Del
OK
dictdiffer.py
16.48 KB
Del
OK
dicttrim.py
3.9 KB
Del
OK
dictupdate.py
11.33 KB
Del
OK
dns.py
35.21 KB
Del
OK
doc.py
2.25 KB
Del
OK
dockermod
-
Del
OK
entrypoints.py
1.83 KB
Del
OK
environment.py
2.2 KB
Del
OK
error.py
1.18 KB
Del
OK
etcd_util.py
33.25 KB
Del
OK
event.py
52.45 KB
Del
OK
extend.py
8.87 KB
Del
OK
extmods.py
6.04 KB
Del
OK
filebuffer.py
3.15 KB
Del
OK
files.py
27.94 KB
Del
OK
find.py
22.08 KB
Del
OK
fsutils.py
3.29 KB
Del
OK
functools.py
6.02 KB
Del
OK
gitfs.py
130.41 KB
Del
OK
github.py
1.52 KB
Del
OK
gzip_util.py
2.86 KB
Del
OK
hashutils.py
5.91 KB
Del
OK
http.py
33.9 KB
Del
OK
iam.py
1.22 KB
Del
OK
icinga2.py
754 B
Del
OK
idem.py
1.22 KB
Del
OK
immutabletypes.py
2.46 KB
Del
OK
itertools.py
2.36 KB
Del
OK
jid.py
3 KB
Del
OK
jinja.py
33.92 KB
Del
OK
job.py
6.89 KB
Del
OK
json.py
3.78 KB
Del
OK
kickstart.py
41.04 KB
Del
OK
kinds.py
493 B
Del
OK
lazy.py
3.06 KB
Del
OK
listdiffer.py
10.9 KB
Del
OK
locales.py
2.06 KB
Del
OK
mac_utils.py
14.01 KB
Del
OK
mako.py
3.97 KB
Del
OK
master.py
29.7 KB
Del
OK
mattermost.py
1.77 KB
Del
OK
memcached.py
3.56 KB
Del
OK
migrations.py
1.46 KB
Del
OK
mine.py
3.68 KB
Del
OK
minion.py
4.13 KB
Del
OK
minions.py
43.38 KB
Del
OK
mount.py
1.15 KB
Del
OK
msazure.py
5.36 KB
Del
OK
msgpack.py
4.69 KB
Del
OK
nacl.py
13.65 KB
Del
OK
namecheap.py
4.32 KB
Del
OK
napalm.py
23.22 KB
Del
OK
nb_popen.py
7.24 KB
Del
OK
network.py
73.86 KB
Del
OK
nxos.py
12.94 KB
Del
OK
nxos_api.py
4 KB
Del
OK
odict.py
13.21 KB
Del
OK
openstack
-
Del
OK
oset.py
6.41 KB
Del
OK
pagerduty.py
3.03 KB
Del
OK
parsers.py
122.69 KB
Del
OK
path.py
11.24 KB
Del
OK
pbm.py
9.81 KB
Del
OK
pkg
-
Del
OK
platform.py
5.84 KB
Del
OK
powershell.py
4.15 KB
Del
OK
preseed.py
2.64 KB
Del
OK
process.py
40.76 KB
Del
OK
profile.py
3.21 KB
Del
OK
proxy.py
331 B
Del
OK
psutil_compat.py
3.63 KB
Del
OK
pushover.py
4.51 KB
Del
OK
pycrypto.py
5.41 KB
Del
OK
pydsl.py
13.74 KB
Del
OK
pyobjects.py
10.75 KB
Del
OK
reactor.py
18.99 KB
Del
OK
reclass.py
752 B
Del
OK
roster_matcher.py
3.55 KB
Del
OK
rsax931.py
8.42 KB
Del
OK
s3.py
8.78 KB
Del
OK
saltclass.py
14.27 KB
Del
OK
sanitizers.py
2.51 KB
Del
OK
schedule.py
71.81 KB
Del
OK
schema.py
54.26 KB
Del
OK
sdb.py
4.04 KB
Del
OK
slack.py
3.58 KB
Del
OK
smb.py
11.16 KB
Del
OK
smtp.py
3.27 KB
Del
OK
ssdp.py
14.75 KB
Del
OK
ssh.py
769 B
Del
OK
state.py
8.43 KB
Del
OK
stringio.py
355 B
Del
OK
stringutils.py
16.95 KB
Del
OK
systemd.py
5.51 KB
Del
OK
templates.py
24.03 KB
Del
OK
textformat.py
5.03 KB
Del
OK
thin.py
31.91 KB
Del
OK
timed_subprocess.py
4.06 KB
Del
OK
timeout.py
1.53 KB
Del
OK
timeutil.py
2.4 KB
Del
OK
url.py
5 KB
Del
OK
user.py
11.86 KB
Del
OK
validate
-
Del
OK
value.py
247 B
Del
OK
vault.py
21.74 KB
Del
OK
verify.py
25.34 KB
Del
OK
versions.py
17.17 KB
Del
OK
virt.py
3.24 KB
Del
OK
virtualbox.py
22.43 KB
Del
OK
vmware.py
129.74 KB
Del
OK
vsan.py
17.18 KB
Del
OK
vt.py
31.47 KB
Del
OK
vt_helper.py
4.4 KB
Del
OK
win_chcp.py
3.7 KB
Del
OK
win_dacl.py
95.49 KB
Del
OK
win_dotnet.py
4.74 KB
Del
OK
win_functions.py
12.69 KB
Del
OK
win_lgpo_auditpol.py
8.48 KB
Del
OK
win_lgpo_netsh.py
17.87 KB
Del
OK
win_lgpo_reg.py
16.98 KB
Del
OK
win_network.py
16.35 KB
Del
OK
win_osinfo.py
2.83 KB
Del
OK
win_pdh.py
13.85 KB
Del
OK
win_reg.py
30.82 KB
Del
OK
win_runas.py
10.53 KB
Del
OK
win_service.py
5.2 KB
Del
OK
win_system.py
14.47 KB
Del
OK
win_update.py
40.36 KB
Del
OK
winapi.py
818 B
Del
OK
x509.py
73.19 KB
Del
OK
xdg.py
316 B
Del
OK
xmlutil.py
13.91 KB
Del
OK
yaml.py
349 B
Del
OK
yamldumper.py
3.37 KB
Del
OK
yamlencoding.py
1.55 KB
Del
OK
yamllint.py
1.61 KB
Del
OK
yamlloader.py
6.04 KB
Del
OK
yamlloader_old.py
8.15 KB
Del
OK
yast.py
619 B
Del
OK
zeromq.py
1.74 KB
Del
OK
zfs.py
19.15 KB
Del
OK
Edit: napalm.py
""" Utils for the NAPALM modules and proxy. .. seealso:: - :mod:`NAPALM grains: select network devices based on their characteristics <salt.grains.napalm>` - :mod:`NET module: network basic features <salt.modules.napalm_network>` - :mod:`NTP operational and configuration management module <salt.modules.napalm_ntp>` - :mod:`BGP operational and configuration management module <salt.modules.napalm_bgp>` - :mod:`Routes details <salt.modules.napalm_route>` - :mod:`SNMP configuration module <salt.modules.napalm_snmp>` - :mod:`Users configuration management <salt.modules.napalm_users>` .. versionadded:: 2017.7.0 """ import copy import importlib import logging import traceback from functools import wraps import salt.output import salt.utils.args import salt.utils.platform try: # will try to import NAPALM # https://github.com/napalm-automation/napalm # pylint: disable=unused-import,no-name-in-module import napalm # pylint: enable=unused-import,no-name-in-module HAS_NAPALM = True try: NAPALM_MAJOR = int(napalm.__version__.split(".")[0]) except AttributeError: NAPALM_MAJOR = 0 except ImportError: HAS_NAPALM = False try: # try importing ConnectionClosedException # from napalm-base # this exception has been introduced only in version 0.24.0 from napalm.base.exceptions import ConnectionClosedException HAS_CONN_CLOSED_EXC_CLASS = True except ImportError: HAS_CONN_CLOSED_EXC_CLASS = False log = logging.getLogger(__file__) def is_proxy(opts): """ Is this a NAPALM proxy? """ return ( salt.utils.platform.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? """ return not salt.utils.platform.is_proxy() and "napalm" in opts def virtual(opts, virtualname, filename): """ Returns the __virtual__. """ if (HAS_NAPALM and NAPALM_MAJOR >= 2) and (is_proxy(opts) or is_minion(opts)): return virtualname else: return ( False, '"{vname}"" {filename} cannot be loaded: ' "NAPALM is not installed: ``pip install napalm``".format( vname=virtualname, filename="({filename})".format(filename=filename) ), ) def call(napalm_device, method, *args, **kwargs): """ Calls arbitrary methods from the network driver instance. Please check the readthedocs_ page for the updated list of getters. .. _readthedocs: http://napalm.readthedocs.org/en/latest/support/index.html#getters-support-matrix method Specifies the name of the method to be called. *args Arguments. **kwargs More arguments. :return: A dictionary with three keys: * result (True/False): if the operation succeeded * out (object): returns the object as-is from the call * comment (string): provides more details in case the call failed * traceback (string): complete traceback in case of exception. \ Please submit an issue including this traceback \ on the `correct driver repo`_ and make sure to read the FAQ_ .. _`correct driver repo`: https://github.com/napalm-automation/napalm/issues/new .. FAQ_: https://github.com/napalm-automation/napalm#faq Example: .. code-block:: python salt.utils.napalm.call( napalm_object, 'cli', [ 'show version', 'show chassis fan' ] ) """ result = False out = None opts = napalm_device.get("__opts__", {}) retry = kwargs.pop("__retry", True) # retry executing the task? force_reconnect = kwargs.get("force_reconnect", False) if force_reconnect: log.debug("Forced reconnection initiated") log.debug("The current opts (under the proxy key):") log.debug(opts["proxy"]) opts["proxy"].update(**kwargs) log.debug("Updated to:") log.debug(opts["proxy"]) napalm_device = get_device(opts) try: if not napalm_device.get("UP", False): raise Exception("not connected") # if connected will try to execute desired command kwargs_copy = {} kwargs_copy.update(kwargs) for karg, warg in kwargs_copy.items(): # lets clear None arguments # to not be sent to NAPALM methods if warg is None: kwargs.pop(karg) out = getattr(napalm_device.get("DRIVER"), method)(*args, **kwargs) # calls the method with the specified parameters result = True except Exception as error: # pylint: disable=broad-except # either not connected # either unable to execute the command hostname = napalm_device.get("HOSTNAME", "[unspecified hostname]") err_tb = ( traceback.format_exc() ) # let's get the full traceback and display for debugging reasons. if isinstance(error, NotImplementedError): comment = ( "{method} is not implemented for the NAPALM {driver} driver!".format( method=method, driver=napalm_device.get("DRIVER_NAME") ) ) elif ( retry and HAS_CONN_CLOSED_EXC_CLASS and isinstance(error, ConnectionClosedException) ): # Received disconection whilst executing the operation. # Instructed to retry (default behaviour) # thus trying to re-establish the connection # and re-execute the command # if any of the operations (close, open, call) will rise again ConnectionClosedException # it will fail loudly. kwargs["__retry"] = False # do not attempt re-executing comment = "Disconnected from {device}. Trying to reconnect.".format( device=hostname ) log.error(err_tb) log.error(comment) log.debug("Clearing the connection with %s", hostname) call(napalm_device, "close", __retry=False) # safely close the connection # Make sure we don't leave any TCP connection open behind # if we fail to close properly, we might not be able to access the log.debug("Re-opening the connection with %s", hostname) call(napalm_device, "open", __retry=False) log.debug("Connection re-opened with %s", hostname) log.debug("Re-executing %s", method) return call(napalm_device, method, *args, **kwargs) # If still not able to reconnect and execute the task, # the proxy keepalive feature (if enabled) will attempt # to reconnect. # If the device is using a SSH-based connection, the failure # will also notify the paramiko transport and the `is_alive` flag # is going to be set correctly. # More background: the network device may decide to disconnect, # although the SSH session itself is alive and usable, the reason # being the lack of activity on the CLI. # Paramiko's keepalive doesn't help in this case, as the ServerAliveInterval # are targeting the transport layer, whilst the device takes the decision # when there isn't any activity on the CLI, thus at the application layer. # Moreover, the disconnect is silent and paramiko's is_alive flag will # continue to return True, although the connection is already unusable. # For more info, see https://github.com/paramiko/paramiko/issues/813. # But after a command fails, the `is_alive` flag becomes aware of these # changes and will return False from there on. And this is how the # Salt proxy keepalive helps: immediately after the first failure, it # will know the state of the connection and will try reconnecting. else: comment = ( 'Cannot execute "{method}" on {device}{port} as {user}. Reason:' " {error}!".format( device=napalm_device.get("HOSTNAME", "[unspecified hostname]"), port=( ":{port}".format( port=napalm_device.get("OPTIONAL_ARGS", {}).get("port") ) if napalm_device.get("OPTIONAL_ARGS", {}).get("port") else "" ), user=napalm_device.get("USERNAME", ""), method=method, error=error, ) ) log.error(comment) log.error(err_tb) return {"out": {}, "result": False, "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 explicitly set as False napalm_device["DRIVER"].close() return {"out": out, "result": result, "comment": ""} def get_device_opts(opts, salt_obj=None): """ Returns the options of the napalm device. :pram: opts :return: the network device opts """ network_device = {} # by default, look in the proxy config details device_dict = opts.get("proxy", {}) if is_proxy(opts) else opts.get("napalm", {}) if opts.get("proxy") or opts.get("napalm"): opts["multiprocessing"] = device_dict.get("multiprocessing", False) # Most NAPALM drivers are SSH-based, so multiprocessing should default to False. # But the user can be allows one to have a different value for the multiprocessing, which will # override the opts. if not device_dict: # still not able to setup log.error( "Incorrect minion config. Please specify at least the napalm driver name!" ) # either under the proxy hier, either under the napalm in the config file network_device["HOSTNAME"] = ( device_dict.get("host") or device_dict.get("hostname") or device_dict.get("fqdn") or device_dict.get("ip") ) network_device["USERNAME"] = device_dict.get("username") or device_dict.get("user") network_device["DRIVER_NAME"] = device_dict.get("driver") or device_dict.get("os") network_device["PASSWORD"] = ( device_dict.get("passwd") or device_dict.get("password") or device_dict.get("pass") or "" ) 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["PROVIDER"] = device_dict.get("provider") network_device["UP"] = False # get driver object form NAPALM if "config_lock" not in network_device["OPTIONAL_ARGS"]: network_device["OPTIONAL_ARGS"]["config_lock"] = False if ( network_device["ALWAYS_ALIVE"] and "keepalive" not in network_device["OPTIONAL_ARGS"] ): network_device["OPTIONAL_ARGS"]["keepalive"] = 5 # 5 seconds keepalive return network_device def get_device(opts, salt_obj=None): """ Initialise the connection with the network device through NAPALM. :param: opts :return: the network device object """ log.debug("Setting up NAPALM connection") network_device = get_device_opts(opts, salt_obj=salt_obj) provider_lib = napalm.base if network_device.get("PROVIDER"): # Configuration example: # provider: napalm_base_example try: provider_lib = importlib.import_module(network_device.get("PROVIDER")) except ImportError as ierr: log.error( "Unable to import %s", network_device.get("PROVIDER"), exc_info=True ) _driver_ = provider_lib.get_network_driver(network_device.get("DRIVER_NAME")) try: network_device["DRIVER"] = _driver_( network_device.get("HOSTNAME", ""), network_device.get("USERNAME", ""), network_device.get("PASSWORD", ""), timeout=network_device["TIMEOUT"], optional_args=network_device["OPTIONAL_ARGS"], ) network_device.get("DRIVER").open() # no exception raised here, means connection established network_device["UP"] = True except napalm.base.exceptions.ConnectionException as error: base_err_msg = "Cannot connect to {hostname}{port} as {username}.".format( hostname=network_device.get("HOSTNAME", "[unspecified hostname]"), port=( ":{port}".format( port=network_device.get("OPTIONAL_ARGS", {}).get("port") ) if network_device.get("OPTIONAL_ARGS", {}).get("port") else "" ), username=network_device.get("USERNAME", ""), ) log.error(base_err_msg) log.error("Please check error: %s", error) raise napalm.base.exceptions.ConnectionException(base_err_msg) return network_device def proxy_napalm_wrap(func): """ This decorator is used to make the execution module functions available outside a proxy minion, or when running inside a proxy minion. If we are running in a proxy, retrieve the connection details from the __proxy__ injected variable. If we are not, then use the connection information from the opts. :param func: :return: """ @wraps(func) def func_wrapper(*args, **kwargs): wrapped_global_namespace = func.__globals__ # get __opts__ and __proxy__ from func_globals proxy = wrapped_global_namespace.get("__proxy__") opts = copy.deepcopy(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 always_alive = opts.get("proxy", {}).get("always_alive", True) # force_reconnect is a magic keyword arg that allows one to establish # a separate connection to the network device running under an always # alive Proxy Minion, using new credentials (overriding the ones # configured in the opts / pillar. force_reconnect = kwargs.get("force_reconnect", False) if force_reconnect: log.debug("Usage of reconnect force detected") log.debug("Opts before merging") log.debug(opts["proxy"]) opts["proxy"].update(**kwargs) log.debug("Opts after merging") log.debug(opts["proxy"]) if is_proxy(opts) and always_alive: # if it is running in a NAPALM Proxy and it's using the default # always alive behaviour, will get the cached copy of the network # device object which should preserve the connection. if force_reconnect: wrapped_global_namespace["napalm_device"] = get_device(opts) else: wrapped_global_namespace["napalm_device"] = proxy["napalm.get_device"]() elif is_proxy(opts) 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 # 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) 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 overridden. # 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" ] else: # if not a NAPLAM proxy # thus it is running on a regular minion, directly on the network device # or another flavour of Minion from where we can invoke arbitrary # NAPALM commands # get __salt__ from func_globals log.debug("Not running in a NAPALM Proxy Minion") _salt_obj = wrapped_global_namespace.get("__salt__") napalm_opts = _salt_obj["config.get"]("napalm", {}) napalm_inventory = _salt_obj["config.get"]("napalm_inventory", {}) log.debug("NAPALM opts found in the Minion config") log.debug(napalm_opts) clean_kwargs = salt.utils.args.clean_kwargs(**kwargs) napalm_opts.update(clean_kwargs) # no need for deeper merge log.debug("Merging the found opts with the CLI args") log.debug(napalm_opts) host = ( napalm_opts.get("host") or napalm_opts.get("hostname") or napalm_opts.get("fqdn") or napalm_opts.get("ip") ) if ( host and napalm_inventory and isinstance(napalm_inventory, dict) and host in napalm_inventory ): inventory_opts = napalm_inventory[host] log.debug("Found %s in the NAPALM inventory:", host) log.debug(inventory_opts) napalm_opts.update(inventory_opts) log.debug( "Merging the config for %s with the details found in the napalm" " inventory:", host, ) log.debug(napalm_opts) opts = copy.deepcopy(opts) # make sure we don't override the original # opts, but just inject the CLI args from the kwargs to into the # object manipulated by ``get_device_opts`` to extract the # connection details, then use then to establish the connection. opts["napalm"] = napalm_opts 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 overridden. # 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 ret = func(*args, **kwargs) if force_reconnect: log.debug("That was a forced reconnect, gracefully clearing up") device = wrapped_global_namespace["napalm_device"] closing = call(device, "close", __retry=False) return ret return func_wrapper def default_ret(name): """ Return the default dict of the state output. """ ret = {"name": name, "changes": {}, "result": False, "comment": ""} return ret def loaded_ret(ret, loaded, test, debug, compliance_report=False, opts=None): """ Return the final state output. ret The initial state output structure. loaded The loaded dictionary. """ # Always get the comment changes = {} ret["comment"] = loaded["comment"] if "diff" in loaded: changes["diff"] = loaded["diff"] if "commit_id" in loaded: changes["commit_id"] = loaded["commit_id"] if "compliance_report" in loaded: if compliance_report: changes["compliance_report"] = loaded["compliance_report"] if debug and "loaded_config" in loaded: changes["loaded_config"] = loaded["loaded_config"] if changes.get("diff"): ret["comment"] = "{comment_base}\n\nConfiguration diff:\n\n{diff}".format( comment_base=ret["comment"], diff=changes["diff"] ) if changes.get("loaded_config"): ret["comment"] = "{comment_base}\n\nLoaded config:\n\n{loaded_cfg}".format( comment_base=ret["comment"], loaded_cfg=changes["loaded_config"] ) if changes.get("compliance_report"): ret["comment"] = "{comment_base}\n\nCompliance report:\n\n{compliance}".format( comment_base=ret["comment"], compliance=salt.output.string_format( changes["compliance_report"], "nested", opts=opts ), ) if not loaded.get("result", False): # Failure of some sort return ret if not loaded.get("already_configured", True): # We're making changes if test: ret["result"] = None return ret # Not test, changes were applied ret.update( { "result": True, "changes": changes, "comment": "Configuration changed!\n{}".format(loaded["comment"]), } ) return ret # No changes ret.update({"result": True, "changes": {}}) return ret
Save