From 98b4b3ded90fa3ebc8db151e16154a22ac80e243 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Thu, 19 Jun 2025 11:47:51 +0200
Subject: [PATCH] Add config_mode private to junos_config module
---
.gitignore | 2 +
.../plugins/modules/junos_config.py | 592 ++++++++++++++++++
.../plugins/modules/nokia_netconf_config.py | 1 -
3 files changed, 594 insertions(+), 1 deletion(-)
create mode 100644 geant/gap_ansible/plugins/modules/junos_config.py
diff --git a/.gitignore b/.gitignore
index 8230afb2..10ad0484 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,5 @@ __pycache__/
.DS_Store
.vscode
geant/gap_ansible/.ansible-lint
+.venv
+geant-gap_ansible-*.tar.gz
diff --git a/geant/gap_ansible/plugins/modules/junos_config.py b/geant/gap_ansible/plugins/modules/junos_config.py
new file mode 100644
index 00000000..c2581ce6
--- /dev/null
+++ b/geant/gap_ansible/plugins/modules/junos_config.py
@@ -0,0 +1,592 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2017, Ansible by Red Hat, inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+from contextlib import contextmanager
+
+DOCUMENTATION = """
+module: junos_config
+author: Peter Sprygada (@privateip)
+short_description: Manage configuration on devices running Juniper JUNOS
+description:
+- This module provides an implementation for working with the active configuration
+ running on Juniper JUNOS devices. It provides a set of arguments for loading configuration,
+ performing rollback operations and zeroing the active configuration on the device.
+version_added: 1.0.0
+extends_documentation_fragment:
+- junipernetworks.junos.junos
+options:
+ lines:
+ description:
+ - This argument takes a list of C(set) or C(delete) configuration lines to push
+ into the remote device. Each line must start with either C(set) or C(delete). This
+ argument is mutually exclusive with the I(src) argument.
+ type: list
+ aliases:
+ - commands
+ elements: str
+ src:
+ description:
+ - The I(src) argument provides a path to the configuration file to load into the
+ remote system. The path can either be a full system path to the configuration
+ file if the value starts with / or relative to the root of the implemented role
+ or playbook. This argument is mutually exclusive with the I(lines) argument.
+ type: path
+ src_format:
+ description:
+ - The I(src_format) argument specifies the format of the configuration found int
+ I(src). If the I(src_format) argument is not provided, the module will attempt
+ to determine the format of the configuration file specified in I(src).
+ type: str
+ choices:
+ - xml
+ - set
+ - text
+ - json
+ rollback:
+ description:
+ - The C(rollback) argument instructs the module to rollback the current configuration
+ to the identifier specified in the argument. If the specified rollback identifier
+ does not exist on the remote device, the module will fail. To rollback to the
+ most recent commit, set the C(rollback) argument to 0.
+ type: int
+ zeroize:
+ description:
+ - The C(zeroize) argument is used to completely sanitize the remote device configuration
+ back to initial defaults. This argument will effectively remove all current
+ configuration statements on the remote device.
+ type: bool
+ default: false
+ confirm:
+ description:
+ - The C(confirm) argument will configure a time out value in minutes for the commit
+ to be confirmed before it is automatically rolled back. If the C(confirm) argument
+ is set to False, this argument is silently ignored. If the value for this argument
+ is set to 0, the commit is confirmed immediately.
+ type: int
+ default: 0
+ comment:
+ description:
+ - The C(comment) argument specifies a text string to be used when committing the
+ configuration. If the C(confirm) argument is set to False, this argument is
+ silently ignored.
+ default: configured by junos_config
+ type: str
+ replace:
+ description:
+ - The C(replace) argument will instruct the remote device to replace the current
+ configuration hierarchy with the one specified in the corresponding hierarchy
+ of the source configuration loaded from this module.
+ - Note this argument should be considered deprecated. To achieve the equivalent,
+ set the I(update) argument to C(replace). This argument will be removed in a
+ future release. The C(replace) and C(update) argument is mutually exclusive.
+ type: bool
+ backup:
+ description:
+ - This argument will cause the module to create a full backup of the current C(running-config)
+ from the remote device before any changes are made. If the C(backup_options)
+ value is not given, the backup file is written to the C(backup) folder in the
+ playbook root directory or role root directory, if playbook is part of an ansible
+ role. If the directory does not exist, it is created.
+ type: bool
+ default: false
+ update:
+ description:
+ - This argument will decide how to load the configuration data particularly when
+ the candidate configuration and loaded configuration contain conflicting statements.
+ Following are accepted values. C(merge) combines the data in the loaded configuration
+ with the candidate configuration. If statements in the loaded configuration
+ conflict with statements in the candidate configuration, the loaded statements
+ replace the candidate ones. C(override) discards the entire candidate configuration
+ and replaces it with the loaded configuration. C(replace) substitutes each hierarchy
+ level in the loaded configuration for the corresponding level. C(update) is
+ similar to the override option. The new configuration completely replaces the
+ existing configuration. The difference comes when the configuration is later
+ committed. This option performs a 'diff' between the new candidate configuration
+ and the existing committed configuration. It then only notifies system processes
+ responsible for the changed portions of the configuration, and only marks the
+ actual configuration changes as 'changed'.
+ type: str
+ default: merge
+ choices:
+ - merge
+ - override
+ - replace
+ - update
+ confirm_commit:
+ description:
+ - This argument will execute commit operation on remote device. It can be used
+ to confirm a previous commit.
+ type: bool
+ default: false
+ check_commit:
+ description:
+ - This argument will check correctness of syntax; do not apply changes.
+ - Note that this argument can be used to confirm verified configuration done via
+ commit confirmed operation
+ type: bool
+ default: false
+ backup_options:
+ description:
+ - This is a dict object containing configurable options related to backup file
+ path. The value of this option is read only when C(backup) is set to I(true),
+ if C(backup) is set to I(false) this option will be silently ignored.
+ suboptions:
+ filename:
+ description:
+ - The filename to be used to store the backup configuration. If the filename
+ is not given it will be generated based on the hostname, current time and
+ date in format defined by <hostname>_config.<current-date>@<current-time>
+ type: str
+ dir_path:
+ description:
+ - This option provides the path ending with directory name in which the backup
+ configuration file will be stored. If the directory does not exist it will
+ be first created and the filename is either the value of C(filename) or
+ default filename as described in C(filename) options description. If the
+ path value is not given in that case a I(backup) directory will be created
+ in the current working directory and backup configuration will be copied
+ in C(filename) within I(backup) directory.
+ type: path
+ backup_format:
+ description:
+ - This argument specifies the format of the configuration the backup file will
+ be stored as. If the argument is not specified, the module will use the 'set'
+ format.
+ type: str
+ default: set
+ choices:
+ - xml
+ - set
+ - text
+ - json
+ type: dict
+config_mode:
+ description:
+ - If set to “private”, open a private candidate datastore instead of the global one.
+ type: str
+ choices: [ private ]
+ required: false
+ default: null
+requirements:
+- ncclient (>=v0.5.2)
+notes:
+- This module requires the netconf system service be enabled on the remote device
+ being managed.
+- Abbreviated commands are NOT idempotent, see L(Network FAQ,../network/user_guide/faq.html)
+- Loading JSON-formatted configuration I(json) is supported starting in Junos OS Release
+ 16.1 onwards.
+- Update C(override) not currently compatible with C(set) notation.
+- Tested against vSRX JUNOS version 15.1X49-D15.4, vqfx-10000 JUNOS Version 15.1X53-D60.4.
+- Recommended connection is C(netconf). See L(the Junos OS Platform Options,../network/user_guide/platform_junos.html).
+- This module also works with C(local) connections for legacy playbooks.
+"""
+
+EXAMPLES = """
+- name: load configure file into device
+ junipernetworks.junos.junos_config:
+ src: srx.cfg
+ comment: update config
+
+- name: load configure lines into device
+ junipernetworks.junos.junos_config:
+ lines:
+ - set interfaces ge-0/0/1 unit 0 description "Test interface"
+ - set vlans vlan01 description "Test vlan"
+ comment: update config
+
+- name: Set routed VLAN interface (RVI) IPv4 address
+ junipernetworks.junos.junos_config:
+ lines:
+ - set vlans vlan01 vlan-id 1
+ - set interfaces irb unit 10 family inet address 10.0.0.1/24
+ - set vlans vlan01 l3-interface irb.10
+
+- name: Check correctness of commit configuration
+ junipernetworks.junos.junos_config:
+ check_commit: true
+
+- name: rollback the configuration to id 10
+ junipernetworks.junos.junos_config:
+ rollback: 10
+
+- name: zero out the current configuration
+ junipernetworks.junos.junos_config:
+ zeroize: true
+
+- name: Set VLAN access and trunking
+ junipernetworks.junos.junos_config:
+ lines:
+ - set vlans vlan02 vlan-id 6
+ - set interfaces ge-0/0/6.0 family ethernet-switching interface-mode access vlan
+ members vlan02
+ - set interfaces ge-0/0/6.0 family ethernet-switching interface-mode trunk vlan
+ members vlan02
+
+- name: confirm a previous commit
+ junipernetworks.junos.junos_config:
+ confirm_commit: true
+
+- name: for idempotency, use full-form commands
+ junipernetworks.junos.junos_config:
+ lines:
+ # - set int ge-0/0/1 unit 0 desc "Test interface"
+ - set interfaces ge-0/0/1 unit 0 description "Test interface"
+
+- name: configurable backup path
+ junipernetworks.junos.junos_config:
+ src: srx.cfg
+ backup: true
+ backup_options:
+ filename: backup.cfg
+ dir_path: /home/user
+"""
+
+RETURN = """
+backup_path:
+ description: The full path to the backup file
+ returned: when backup is true
+ type: str
+ sample: /playbooks/ansible/backup/config.2016-07-16@22:28:34
+filename:
+ description: The name of the backup file
+ returned: when backup is true and filename is not specified in backup options
+ type: str
+ sample: junos01_config.2016-07-16@22:28:34
+shortname:
+ description: The full path to the backup file excluding the timestamp
+ returned: when backup is true and filename is not specified in backup options
+ type: str
+ sample: /playbooks/ansible/backup/junos01_config
+date:
+ description: The date extracted from the backup file name
+ returned: when backup is true
+ type: str
+ sample: "2016-07-16"
+time:
+ description: The time extracted from the backup file name
+ returned: when backup is true
+ type: str
+ sample: "22:28:34"
+"""
+import json
+import re
+
+from ansible.module_utils._text import to_native, to_text
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.six import string_types
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.netconf import (
+ exec_rpc,
+)
+from ansible.module_utils.connection import ConnectionError
+from ansible_collections.junipernetworks.junos.plugins.module_utils.network.junos.junos import (
+ commit_configuration,
+ discard_changes,
+ get_configuration,
+ get_diff,
+ load_config,
+ load_configuration,
+ tostring,
+ get_connection,
+ lock_configuration,
+ unlock_configuration,
+)
+
+try:
+ from lxml.etree import Element, fromstring
+except ImportError:
+ from xml.etree.ElementTree import Element, fromstring
+
+try:
+ from lxml.etree import ParseError
+except ImportError:
+ try:
+ from xml.etree.ElementTree import ParseError
+ except ImportError:
+ # for Python < 2.7
+ from xml.parsers.expat import ExpatError
+
+ ParseError = ExpatError
+
+USE_PERSISTENT_CONNECTION = True
+DEFAULT_COMMENT = "configured by junos_config"
+
+
+def check_args(module, warnings):
+ if module.params["replace"] is not None:
+ module.fail_json(msg="argument replace is deprecated, use update")
+
+
+def zeroize(module):
+ return exec_rpc(
+ module,
+ tostring(Element("request-system-zeroize")),
+ ignore_warning=False,
+ )
+
+
+def rollback(ele, id="0"):
+ return get_diff(ele, id)
+
+
+def guess_format(config):
+ try:
+ json.loads(config)
+ return "json"
+ except ValueError:
+ pass
+
+ try:
+ fromstring(config)
+ return "xml"
+ except ParseError:
+ pass
+
+ if config.startswith("set") or config.startswith("delete"):
+ return "set"
+
+ return "text"
+
+
+def filter_delete_statements(module, candidate):
+ reply = get_configuration(module, format="set")
+ match = reply.find(".//configuration-set")
+ if match is None:
+ # Could not find configuration-set in reply, perhaps device does not support it?
+ return candidate
+ config = to_native(match.text, encoding="latin-1")
+
+ modified_candidate = candidate[:]
+ for index, line in reversed(list(enumerate(candidate))):
+ if line.startswith("delete"):
+ newline = re.sub("^delete", "set", line)
+ if newline not in config:
+ del modified_candidate[index]
+
+ return modified_candidate
+
+
+def configure_device(module, warnings, candidate):
+ kwargs = {}
+ config_format = None
+
+ if module.params["src"]:
+ config_format = module.params["src_format"] or guess_format(
+ str(candidate),
+ )
+ if config_format == "set":
+ kwargs.update({"format": "text", "action": "set"})
+ else:
+ kwargs.update(
+ {"format": config_format, "action": module.params["update"]},
+ )
+
+ if isinstance(candidate, string_types):
+ candidate = candidate.split("\n")
+
+ # this is done to filter out `delete ...` statements which map to
+ # nothing in the config as that will cause an exception to be raised
+ if any((module.params["lines"], config_format == "set")):
+ candidate = filter_delete_statements(module, candidate)
+ kwargs["format"] = "text"
+ kwargs["action"] = "set"
+
+ return load_config(module, candidate, warnings, **kwargs)
+
+
+@contextmanager
+def safe_locked_config(module):
+ """Lock the candidate—either shared or private—depending on config_mode."""
+ conn = get_connection(module)
+ private = module.params.get("config_mode") == "private"
+
+ try:
+ if private:
+ # Try to clean up any stale private first
+ try:
+ conn.execute_rpc(
+ tostring(
+ Element("close-configuration",
+ {"xmlns": "http://xml.juniper.net/xnm/1.1/xnm"}
+ )
+ )
+ )
+ except ConnectionError:
+ pass
+
+ # Open your own private sandbox
+ conn.execute_rpc(
+ tostring(
+ Element("open-configuration",
+ {"xmlns": "http://xml.juniper.net/xnm/1.1/xnm",
+ "private": "true"}
+ )
+ )
+ )
+
+ else:
+ # shared candidate: standard lock
+ lock_configuration(module)
+ except ConnectionError as exc:
+ module.fail_json(msg=f"Could not acquire {'private' if private else 'shared'} lock: {to_text(exc)}")
+
+ # Hand control back to the caller
+ try:
+ yield
+ finally:
+ # Teardown / unlock
+ try:
+ if private:
+ conn.execute_rpc(
+ tostring(Element("close-configuration",
+ {"xmlns": "http://xml.juniper.net/xnm/1.1/xnm"}
+ ))
+ )
+ else:
+ unlock_configuration(module)
+ except ConnectionError as exc:
+ module.warn(f"Ignoring failure closing {'private' if private else 'shared'} lock: {to_text(exc)}")
+
+
+def main():
+ """main entry point for module execution"""
+ backup_spec = dict(
+ filename=dict(),
+ dir_path=dict(type="path"),
+ backup_format=dict(
+ default="set",
+ choices=["xml", "text", "set", "json"],
+ ),
+ )
+ argument_spec = dict(
+ lines=dict(aliases=["commands"], type="list", elements="str"),
+ src=dict(type="path"),
+ src_format=dict(choices=["xml", "text", "set", "json"]),
+ # update operations
+ update=dict(
+ default="merge",
+ choices=["merge", "override", "replace", "update"],
+ ),
+ # deprecated replace in Ansible 2.3
+ replace=dict(type="bool"),
+ confirm=dict(default=0, type="int"),
+ comment=dict(default=DEFAULT_COMMENT),
+ confirm_commit=dict(type="bool", default=False),
+ check_commit=dict(type="bool", default=False),
+ # config operations
+ backup=dict(type="bool", default=False),
+ backup_options=dict(type="dict", options=backup_spec),
+ rollback=dict(type="int"),
+ zeroize=dict(default=False, type="bool"),
+ config_mode=dict(type='str', choices=['private'], required=False, default=None),
+ )
+
+ mutually_exclusive = [("lines", "src", "rollback", "zeroize")]
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ mutually_exclusive=mutually_exclusive,
+ supports_check_mode=True,
+ )
+
+ warnings = list()
+ check_args(module, warnings)
+
+ candidate = module.params["lines"] or module.params["src"]
+ commit = not module.check_mode
+
+ result = {"changed": False, "warnings": warnings}
+
+ if module.params["backup"]:
+ if module.params["backup_options"] is not None:
+ conf_format = module.params["backup_options"]["backup_format"]
+ else:
+ conf_format = "set"
+ reply = get_configuration(module, format=conf_format)
+ if reply is None:
+ module.fail_json(msg="unable to retrieve device configuration")
+ else:
+ if conf_format in ["set", "text"]:
+ reply = reply.find(
+ ".//configuration-%s" % conf_format,
+ ).text.strip()
+ elif conf_format in "xml":
+ reply = str(
+ tostring(reply.find(".//configuration"), pretty_print=True),
+ ).strip()
+ elif conf_format in "json":
+ reply = str(reply.xpath("//rpc-reply/text()")[0]).strip()
+ if not isinstance(reply, str):
+ module.fail_json(
+ msg="unable to format retrieved device configuration",
+ )
+ result["__backup__"] = reply
+
+ rollback_id = module.params["rollback"]
+ if isinstance(rollback_id, int) and rollback_id >= 0:
+ diff = rollback(module, rollback_id)
+ if commit:
+ kwargs = {"comment": module.params["comment"]}
+ with safe_locked_config(module):
+ load_configuration(module, rollback=rollback_id)
+ commit_configuration(module, **kwargs)
+ if module._diff:
+ result["diff"] = {"prepared": diff}
+ result["changed"] = True
+
+ elif module.params["zeroize"]:
+ if commit:
+ zeroize(module)
+ result["changed"] = True
+
+ else:
+ if candidate:
+ with safe_locked_config(module):
+ diff = configure_device(module, warnings, candidate)
+ if diff:
+ if commit:
+ kwargs = {
+ "comment": module.params["comment"],
+ "check": module.params["check_commit"],
+ }
+
+ confirm = module.params["confirm"]
+ if confirm > 0:
+ kwargs.update(
+ {
+ "confirm": True,
+ "confirm_timeout": to_text(
+ confirm,
+ errors="surrogate_then_replace",
+ ),
+ },
+ )
+ commit_configuration(module, **kwargs)
+ else:
+ discard_changes(module)
+ result["changed"] = True
+
+ if module._diff:
+ result["diff"] = {"prepared": diff}
+
+ elif module.params["check_commit"]:
+ commit_configuration(module, check=True)
+
+ elif module.params["confirm_commit"]:
+ with safe_locked_config(module):
+ # confirm a previous commit
+ commit_configuration(module)
+
+ result["changed"] = True
+
+ module.exit_json(**result)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/geant/gap_ansible/plugins/modules/nokia_netconf_config.py b/geant/gap_ansible/plugins/modules/nokia_netconf_config.py
index ca63f799..20736e57 100644
--- a/geant/gap_ansible/plugins/modules/nokia_netconf_config.py
+++ b/geant/gap_ansible/plugins/modules/nokia_netconf_config.py
@@ -396,7 +396,6 @@ EXAMPLES = """
# "interfaces.interface.config.enabled": "true",
# "interfaces.interface.config.mtu": "0",
# "interfaces.interface.config.name": "Ethernet2",
-
"""
RETURN = """
--
GitLab