acex-driver-juniper-junoscli 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: acex-driver-juniper-junoscli
3
+ Version: 1.0.0
4
+ Summary: ACE-X Driver: Juniper Junos CLI
5
+ License: AGPL-3.0
6
+ Author: Johan Lahti
7
+ Author-email: johan.lahti@acebit.se
8
+ Requires-Python: >=3.13,<4.0
9
+ Classifier: License :: OSI Approved :: GNU Affero General Public License v3
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Requires-Dist: acex-devkit (>=1.14.0,<2.0.0)
14
+ Requires-Dist: netmiko (>=4.6.0,<5.0.0)
15
+ Project-URL: Homepage, https://github.com/acex-labs/acex
16
+ Project-URL: Repository, https://github.com/acex-labs/acex
@@ -0,0 +1,29 @@
1
+ [tool.poetry]
2
+ name = "acex-driver-juniper-junoscli"
3
+ version = "1.0.0"
4
+ description = "ACE-X Driver: Juniper Junos CLI"
5
+ authors = ["Johan Lahti <johan.lahti@acebit.se>"]
6
+ license = "AGPL-3.0"
7
+ homepage = "https://github.com/acex-labs/acex"
8
+ repository = "https://github.com/acex-labs/acex"
9
+
10
+ packages = [
11
+ { include = "acex_driver_juniper_junos_cli", from = "src" }
12
+ ]
13
+
14
+ [tool.poetry.dependencies]
15
+ python = "^3.13"
16
+ netmiko = "^4.6.0"
17
+ acex-devkit = "^1.14.0"
18
+
19
+ [build-system]
20
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
21
+ build-backend = "poetry.core.masonry.api"
22
+
23
+ [tool.poetry.plugins."acex.neds"]
24
+ juniper_junos_cli = "acex_driver_juniper_junos_cli.junos_cli:JunosCLI"
25
+
26
+ [dependency-groups]
27
+ dev = [
28
+ "ipython (>=9.10.0,<10.0.0)"
29
+ ]
@@ -0,0 +1,67 @@
1
+ """
2
+ Per-type rendering of `Augment`-class components into Junos CLI lines.
3
+
4
+ Mirrors the Cisco driver's dispatch pattern. Augments unknown to this
5
+ driver (no renderer registered for that type) are silently skipped.
6
+
7
+ Junos-set commands are flat (no indented blocks like Cisco), so renderer
8
+ functions emit fully-formed `set ...` lines. They receive `target_path`
9
+ so they can extract context (e.g. the SNMP community name) from it.
10
+
11
+ To add a new Juniper augment type:
12
+ 1. Define the augment component + payload model in the backend
13
+ 2. Register a renderer here under its `type` key
14
+ """
15
+ from collections import defaultdict
16
+ from typing import Callable, Dict, List
17
+
18
+
19
+ def _render_snmp_community_clients(aug: dict, target_path: str) -> List[str]:
20
+ """`set snmp community <name> clients <prefix>` — one line per prefix."""
21
+ community_name = target_path.rsplit(".", 1)[-1]
22
+ clients = (aug.get("clients") or {}).get("value") or []
23
+ return [
24
+ f"set snmp community {community_name} clients {prefix}"
25
+ for prefix in clients
26
+ ]
27
+
28
+
29
+ AUGMENT_RENDERERS: Dict[str, Callable[[dict, str], List[str]]] = {
30
+ "juniper.snmp_community_clients": _render_snmp_community_clients,
31
+ }
32
+
33
+
34
+ def resolve_augment_lines(configuration: dict) -> Dict[str, List[str]]:
35
+ """
36
+ Walk every targetable tree node, collect its `augments` dict, dispatch each
37
+ augment to its renderer, and return a {target_path: [cli_line, ...]} dict
38
+ for use in Jinja.
39
+
40
+ Augments unknown to this driver (no renderer registered for that type)
41
+ are silently skipped — that's the point of augment-on-target dispatch.
42
+ """
43
+ by_target: Dict[str, List[str]] = defaultdict(list)
44
+
45
+ def _collect(target_path: str, augments: dict):
46
+ for aug_type, aug in (augments or {}).items():
47
+ renderer = AUGMENT_RENDERERS.get(aug_type)
48
+ if renderer is None:
49
+ continue
50
+ by_target[target_path].extend(renderer(aug, target_path))
51
+
52
+ for name, intf in (configuration.get("interfaces") or {}).items():
53
+ _collect(f"interfaces.{name}", intf.get("augments"))
54
+
55
+ for name, tmpl in (configuration.get("interface_templates") or {}).items():
56
+ _collect(f"interface_templates.{name}", tmpl.get("augments"))
57
+
58
+ # SNMP communities — Junos has per-community augments (e.g. clients list)
59
+ for name, com in (((configuration.get("system") or {}).get("snmp") or {}).get("communities") or {}).items():
60
+ _collect(f"system.snmp.communities.{name}", com.get("augments"))
61
+
62
+ # Singletons under system.* — extend here as more Augmentable targets land.
63
+ vtp_cfg = ((configuration.get("system") or {}).get("vtp") or {}).get("config")
64
+ if vtp_cfg:
65
+ _collect("system.vtp.config", vtp_cfg.get("augments"))
66
+
67
+ return dict(by_target)
@@ -0,0 +1,139 @@
1
+
2
+ from contextlib import contextmanager
3
+ from typing import Optional
4
+ from acex_devkit.models.composed_configuration import ComposedConfiguration
5
+ from acex_devkit.models.node_response import NodeListItem
6
+ from acex_devkit.models.management_connection import ManagementConnection
7
+ from netmiko import ConnectHandler
8
+
9
+ from acex_devkit.drivers import NetworkElementDriver, TransportBase
10
+ from acex_devkit.configdiffer import Diff
11
+
12
+ from .renderer import JunosCLIRenderer
13
+ from .parser import JunosCLIParser
14
+
15
+
16
+ class JunosCLITransport(TransportBase):
17
+
18
+ def __init__(self):
19
+ self._session_conn: Optional[ConnectHandler] = None
20
+
21
+ def _open_connection(self, connection: ManagementConnection, **kwargs) -> ConnectHandler:
22
+ username = kwargs.get("username")
23
+ password = kwargs.get("password")
24
+ if not username or not password:
25
+ raise ValueError("Credentials required: username and password must be provided")
26
+ device = {
27
+ "device_type": "juniper_junos",
28
+ "host": connection.target_ip,
29
+ "username": username,
30
+ "password": password,
31
+ "port": 22,
32
+ "conn_timeout": 30,
33
+ }
34
+ return ConnectHandler(**device)
35
+
36
+ @contextmanager
37
+ def session(self, connection: ManagementConnection, **kwargs):
38
+ """Hold one SSH session open for the duration of the block."""
39
+ conn = self._open_connection(connection, **kwargs)
40
+ self._session_conn = conn
41
+ try:
42
+ yield self
43
+ finally:
44
+ self._session_conn = None
45
+ try:
46
+ conn.disconnect()
47
+ except Exception:
48
+ pass
49
+
50
+ @contextmanager
51
+ def _conn(self, connection: ManagementConnection, **kwargs):
52
+ """Yield active session conn or open a one-shot, closing what we own."""
53
+ if self._session_conn is not None:
54
+ yield self._session_conn
55
+ return
56
+ conn = self._open_connection(connection, **kwargs)
57
+ try:
58
+ yield conn
59
+ finally:
60
+ try:
61
+ conn.disconnect()
62
+ except Exception:
63
+ pass
64
+
65
+ def get_config(self, node: NodeListItem, connection: ManagementConnection, **kwargs) -> str:
66
+ with self._conn(connection, **kwargs) as conn:
67
+ # `display set` flattens hierarchical config into reproducible
68
+ # `set` lines; `no-more` disables paging.
69
+ return conn.send_command(
70
+ "show configuration | display set | no-more",
71
+ read_timeout=120,
72
+ )
73
+
74
+ def send_config(self, node: NodeListItem, connection: ManagementConnection, commands: list[str], **kwargs) -> str:
75
+ # netmiko's juniper_junos handler enters configure mode, sends the
76
+ # commands, commits, and exits — driven by send_config_set.
77
+ with self._conn(connection, **kwargs) as conn:
78
+ return conn.send_config_set(commands)
79
+
80
+ def execute(self, node: NodeListItem, connection: ManagementConnection, commands: list[str], **kwargs) -> list[str]:
81
+ with self._conn(connection, **kwargs) as conn:
82
+ return [conn.send_command(cmd, read_timeout=120) for cmd in commands]
83
+
84
+ def get_lldp_neighbors(self, node: NodeListItem, connection: ManagementConnection, **kwargs) -> list[dict]:
85
+ with self._conn(connection, **kwargs) as conn:
86
+ try:
87
+ raw = conn.send_command("show lldp neighbors", read_timeout=60)
88
+ except Exception:
89
+ return []
90
+ return self._parse_lldp_table(raw)
91
+
92
+ @staticmethod
93
+ def _parse_lldp_table(raw: str) -> list[dict]:
94
+ # `show lldp neighbors` returns a fixed-width table:
95
+ # Local Interface Parent Interface Chassis Id Port info System Name
96
+ # ge-0/0/0 - 00:11:22:33:44:55 ge-0/0/1 peer-switch
97
+ neighbors = []
98
+ lines = [ln.rstrip() for ln in raw.splitlines() if ln.strip()]
99
+ # Skip header line if present
100
+ for line in lines:
101
+ if line.lower().startswith("local interface"):
102
+ continue
103
+ parts = line.split()
104
+ # Expected at minimum: local, parent, chassis_id, port_id, sysname
105
+ if len(parts) < 5:
106
+ continue
107
+ local_iface = parts[0]
108
+ remote_port = parts[-2]
109
+ remote_dev = parts[-1]
110
+ neighbors.append({
111
+ "local_interface": local_iface,
112
+ "remote_device": remote_dev,
113
+ "remote_interface": remote_port,
114
+ "discovery_protocol": "lldp",
115
+ })
116
+ return neighbors
117
+
118
+
119
+ class JunosCLI(NetworkElementDriver):
120
+ """Juniper Junos CLI driver."""
121
+
122
+ version = "1.0.0"
123
+ renderer_class = JunosCLIRenderer
124
+ transport_class = JunosCLITransport
125
+ parser_class = JunosCLIParser
126
+
127
+ def render(self, configuration: ComposedConfiguration, asset):
128
+ return self.renderer.render(configuration, asset)
129
+
130
+ def parse(self, configuration: str) -> ComposedConfiguration:
131
+ return self.parser.parse(configuration)
132
+
133
+ def render_patch(self, diff: Diff, node_instance: "NodeInstance"):
134
+ return self.renderer.render_patch(diff, node_instance)
135
+
136
+ def apply_patch(self, diff: Diff, node_instance, node: NodeListItem, connection: ManagementConnection, **kwargs):
137
+ rendered = self.render_patch(diff, node_instance=node_instance)
138
+ commands = [c.strip() for c in rendered.splitlines() if c.strip()]
139
+ return self.transport.send_config(node, connection, commands, **kwargs)
@@ -0,0 +1,17 @@
1
+ """Junos CLI configuration parser.
2
+
3
+ Parsing a real Junos `show configuration` (hierarchical or set-format) into
4
+ the device-agnostic model is a significant undertaking. This stub keeps the
5
+ driver instantiable so deployment-side flows (render/render_patch/apply_patch)
6
+ work; calling parse() raises until a real implementation lands.
7
+ """
8
+
9
+ from acex.plugins.neds.core import ParserBase
10
+
11
+
12
+ class JunosCLIParser(ParserBase):
13
+ def parse(self, configuration: str):
14
+ raise NotImplementedError(
15
+ "JunosCLIParser.parse is not yet implemented. "
16
+ "Implement parsing of `show configuration | display set` output here."
17
+ )
@@ -0,0 +1,191 @@
1
+ from collections import defaultdict
2
+ from typing import Any, Callable, Dict, List, Optional
3
+ from pathlib import Path
4
+
5
+ from jinja2 import Environment, FileSystemLoader, StrictUndefined
6
+
7
+ from acex.plugins.neds.core import RendererBase
8
+ from acex_devkit.configdiffer import Diff
9
+ from acex_devkit.configdiffer.command import Command, Context
10
+ from acex_devkit.models.composed_configuration import ComposedConfiguration
11
+
12
+ from .augment_renderers import resolve_augment_lines
13
+
14
+
15
+ class GeneratorRegistry:
16
+ def __init__(self):
17
+ self._patterns: list[tuple[tuple, Callable]] = []
18
+
19
+ def register(self, pattern: tuple, generator: Callable):
20
+ self._patterns.append((pattern, generator))
21
+
22
+ def resolve(self, path: tuple):
23
+ for pattern, generator in self._patterns:
24
+ if self._match(path, pattern):
25
+ return generator
26
+ return None
27
+
28
+ def _match(self, path, pattern):
29
+ if len(path) < len(pattern):
30
+ return False
31
+ for p, pat in zip(path, pattern):
32
+ if pat != "*" and p != pat:
33
+ return False
34
+ return True
35
+
36
+
37
+ # Junos uses different interface name prefixes per port speed.
38
+ # Speeds are in kbps to match the AttributeValue[int] convention used elsewhere.
39
+ PORT_PREFIX_BY_SPEED = {
40
+ 1_000_000: "ge", # 1 Gbps
41
+ 10_000_000: "xe", # 10 Gbps
42
+ 25_000_000: "et", # 25 Gbps (some platforms)
43
+ 40_000_000: "et", # 40 Gbps
44
+ 100_000_000: "et", # 100 Gbps
45
+ }
46
+
47
+
48
+ class JunosCLIRenderer(RendererBase):
49
+
50
+ def _load_template_file(self) -> str:
51
+ path = Path(__file__).parent
52
+ env = Environment(loader=FileSystemLoader(path), undefined=StrictUndefined)
53
+ return env.get_template("template.j2")
54
+
55
+ def render(self, configuration: ComposedConfiguration, asset) -> Any:
56
+ """Render the configuration model for Junos CLI devices."""
57
+ if isinstance(configuration, ComposedConfiguration):
58
+ configuration = configuration.model_dump(mode="json")
59
+ else:
60
+ raise ValueError(
61
+ f"Configuration must be a ComposedConfiguration instance. Not {type(configuration)}"
62
+ )
63
+
64
+ processed_config = self.pre_process(configuration, asset)
65
+ template = self._load_template_file()
66
+ return template.render(configuration=processed_config)
67
+
68
+ # ── Patch rendering ─────────────────────────────────────────
69
+ # Junos `set` / `delete` lines are fully qualified per command, so
70
+ # there's no enter/exit context to track — generators emit complete
71
+ # lines and rendering is just a join.
72
+
73
+ def _register(self):
74
+ self.registry.register(('system', 'config'), self._generate_system_config_commands)
75
+ self.registry.register(('interfaces', '*'), self._generate_interface_config_commands)
76
+
77
+ def render_patch(self, diff: Diff, node_instance: "NodeInstance"):
78
+ """Render device commands for a Diff using registered generators."""
79
+ self.registry = GeneratorRegistry()
80
+ self._register()
81
+
82
+ commands: List[Command] = []
83
+ for change in diff.get_all_changes():
84
+ generator = self.registry.resolve(tuple(change.path))
85
+ if generator is None:
86
+ continue
87
+ commands.extend(generator(change, node_instance))
88
+
89
+ return "\n".join(c.command for c in commands)
90
+
91
+ def _generate_system_config_commands(self, component_change, node_instance) -> List[Command]:
92
+ ctx = Context(path=[])
93
+ commands: List[Command] = []
94
+ for attr in component_change.changed_attributes:
95
+ if attr.attribute_name == "hostname":
96
+ if component_change.op in ("add", "change"):
97
+ commands.append(Command(context=ctx, command=f"set system host-name {attr.after.value}"))
98
+ elif component_change.op == "remove":
99
+ commands.append(Command(context=ctx, command="delete system host-name"))
100
+ return commands
101
+
102
+ def _generate_interface_config_commands(self, component_change, node_instance) -> List[Command]:
103
+ # Stub mirroring the Cisco renderer's placeholder — real attribute
104
+ # mapping per change.op / attr.attribute_name lands here.
105
+ ctx = Context(path=component_change.path)
106
+ return [Command(context=ctx, command="set interfaces TODO description \"TODO\"")]
107
+
108
+ def pre_process(self, configuration: dict, asset) -> Dict[str, Any]:
109
+ """Pre-process the configuration model before rendering j2."""
110
+ configuration = self._physical_interface_names(configuration, asset)
111
+ self._resolve_lag_lacp(configuration)
112
+ configuration["augment_lines"] = resolve_augment_lines(configuration)
113
+ return configuration
114
+
115
+ def _resolve_lag_lacp(self, config: dict) -> None:
116
+ """
117
+ Bubble per-member LACP settings up to the LAG itself, plus count LAGs
118
+ for chassis-level `aggregated-devices` config. The model keeps LACP
119
+ timing/mode on member ports (Cisco-flavored convention); Junos needs
120
+ them on the LAG.
121
+
122
+ Sets on each LAG: `_lacp_enabled`, `_lacp_mode`, `_lacp_interval`.
123
+ Sets on config root: `_lag_count`.
124
+ """
125
+ interfaces = config.get("interfaces") or {}
126
+
127
+ members_by_lag: Dict[int, list] = defaultdict(list)
128
+ for intf in interfaces.values():
129
+ if intf.get("type") != "ethernetCsmacd":
130
+ continue
131
+ agg = (intf.get("aggregate_id") or {}).get("value")
132
+ if agg is None:
133
+ continue
134
+ members_by_lag[agg].append(intf)
135
+
136
+ for intf in interfaces.values():
137
+ if intf.get("type") != "ieee8023adLag":
138
+ continue
139
+ agg_id = (intf.get("aggregate_id") or {}).get("value")
140
+ if agg_id is None:
141
+ continue
142
+ for member in members_by_lag.get(agg_id, []):
143
+ if (member.get("lacp_enabled") or {}).get("value"):
144
+ intf["_lacp_enabled"] = True
145
+ mode = (member.get("lacp_mode") or {}).get("value")
146
+ intf["_lacp_mode"] = mode if mode in ("active", "passive") else "active"
147
+ interval = (member.get("lacp_interval") or {}).get("value")
148
+ if interval:
149
+ intf["_lacp_interval"] = interval
150
+ break
151
+
152
+ lag_count = sum(1 for i in interfaces.values() if i.get("type") == "ieee8023adLag")
153
+ if lag_count > 0:
154
+ config["_lag_count"] = lag_count
155
+
156
+ def _physical_interface_names(self, config: dict, asset) -> dict:
157
+ """
158
+ Resolve physical/management interface names per Junos conventions.
159
+
160
+ - ethernetCsmacd: ge-/xe-/et- prefix per speed, suffix `<stack>/<module>/<port>`
161
+ using the asset's stack_index/module_index/index from the model.
162
+ - managementInterface: hardcoded to `me0` (switch-style); routers use
163
+ `fxp0` — extend if/when we model that.
164
+ - softwareLoopback: `lo0` with the index becoming the unit number, set
165
+ on the model so the template can use it.
166
+ """
167
+ for _, intf in (config.get("interfaces") or {}).items():
168
+ t = intf.get("type")
169
+
170
+ if t == "ethernetCsmacd":
171
+ speed = (intf.get("speed") or {}).get("value") or 1_000_000
172
+ prefix = PORT_PREFIX_BY_SPEED.get(speed, "ge")
173
+ stack_index = (intf.get("stack_index") or {}).get("value") or 0
174
+ module_index = (intf.get("module_index") or {}).get("value") or 0
175
+ port_index = intf["index"]["value"]
176
+ intf["name"] = f"{prefix}-{stack_index}/{module_index}/{port_index}"
177
+
178
+ elif t == "managementInterface":
179
+ # Junos out-of-band mgmt; default to me0 for switches.
180
+ intf["name"] = "me0"
181
+
182
+ elif t == "softwareLoopback":
183
+ # Junos models loopback as lo0 unit N — name doesn't appear
184
+ # standalone, but having it deterministic helps templates.
185
+ intf["name"] = "lo0"
186
+
187
+ elif t == "ieee8023adLag":
188
+ # Junos uses ae<N> for aggregated Ethernet.
189
+ intf["name"] = f"ae{intf['index']['value']}"
190
+
191
+ return config
@@ -0,0 +1,176 @@
1
+ {#- Junos CLI day-0 template — renders against the current ComposedConfiguration shape.
2
+ Scope: hostname, domain, DNS, SSH, NTP, interfaces (mgmt/loopback/ethernet),
3
+ SNMP communities (with per-community augments), VLANs, static routes.
4
+ Out of scope: AAA, logging beyond defaults, ACLs, complex routing, sFlow. -#}
5
+ {%- if configuration._lag_count %}
6
+ set chassis aggregated-devices ethernet device-count {{ configuration._lag_count }}
7
+ {%- endif %}
8
+ {%- if configuration.system.config.hostname %}
9
+ set system host-name {{ configuration.system.config.hostname.value }}
10
+ {%- endif %}
11
+ {%- if configuration.system.config.domain_name %}
12
+ set system domain-name {{ configuration.system.config.domain_name.value }}
13
+ {%- endif %}
14
+ {%- if configuration.system.config.location %}
15
+ set system location "{{ configuration.system.config.location.value }}"
16
+ {%- endif %}
17
+ {%- if configuration.system.config.contact %}
18
+ set system contact "{{ configuration.system.config.contact.value }}"
19
+ {%- endif %}
20
+ {#- DNS -#}
21
+ {%- if configuration.system.dns and configuration.system.dns.dns_servers %}
22
+ {%- for _, dns in configuration.system.dns.dns_servers.items() %}
23
+ {%- if dns.address %}
24
+ set system name-server {{ dns.address.value }}
25
+ {%- endif %}
26
+ {%- endfor %}
27
+ {%- endif %}
28
+ {#- NTP -#}
29
+ {%- if configuration.system.ntp and configuration.system.ntp.servers %}
30
+ {%- for _, ntp in configuration.system.ntp.servers.items() %}
31
+ set system ntp server {{ ntp.address.value }}{% if ntp.prefer and ntp.prefer.value %} prefer{% endif %}
32
+ {%- endfor %}
33
+ {%- endif %}
34
+ {#- SSH / NETCONF -#}
35
+ {%- set ssh = configuration.system.ssh.config if configuration.system.ssh else None %}
36
+ {%- if ssh and ssh.enable and ssh.enable.value %}
37
+ set system services ssh
38
+ {%- if ssh.timeout %}
39
+ set system services ssh idle-timeout {{ ssh.timeout.value }}
40
+ {%- endif %}
41
+ {%- endif %}
42
+ set system services netconf ssh
43
+ {#- Management interface -#}
44
+ {%- for _, intf in configuration.interfaces.items() %}
45
+ {%- if intf.type == "managementInterface" %}
46
+ {%- if intf.description %}
47
+ set interfaces {{ intf.name }} description "{{ intf.description.value }}"
48
+ {%- endif %}
49
+ {%- if intf.ipv4 %}
50
+ set interfaces {{ intf.name }} unit 0 family inet address {{ intf.ipv4.value }}
51
+ {%- endif %}
52
+ {%- if intf.enabled and not intf.enabled.value %}
53
+ set interfaces {{ intf.name }} disable
54
+ {%- endif %}
55
+ {%- endif %}
56
+ {%- endfor %}
57
+ {#- Loopback interfaces (lo0 with multiple units) -#}
58
+ {%- for _, intf in configuration.interfaces.items() %}
59
+ {%- if intf.type == "softwareLoopback" %}
60
+ {%- if intf.ipv4 %}
61
+ set interfaces {{ intf.name }} unit {{ intf.index.value }} family inet address {{ intf.ipv4.value }}
62
+ {%- endif %}
63
+ {%- if intf.description %}
64
+ set interfaces {{ intf.name }} unit {{ intf.index.value }} description "{{ intf.description.value }}"
65
+ {%- endif %}
66
+ {%- endif %}
67
+ {%- endfor %}
68
+ {#- Physical Ethernet interfaces -#}
69
+ {%- for _, intf in configuration.interfaces.items() %}
70
+ {%- if intf.type == "ethernetCsmacd" %}
71
+ {%- if intf.description %}
72
+ set interfaces {{ intf.name }} description "{{ intf.description.value }}"
73
+ {%- endif %}
74
+ {%- if intf.mtu %}
75
+ set interfaces {{ intf.name }} mtu {{ intf.mtu.value }}
76
+ {%- endif %}
77
+ {%- if intf.ipv4 %}
78
+ set interfaces {{ intf.name }} unit 0 family inet address {{ intf.ipv4.value }}
79
+ {%- endif %}
80
+ {%- if intf.switchport and intf.switchport.value %}
81
+ {%- if intf.switchport_mode and intf.switchport_mode.value == "access" %}
82
+ set interfaces {{ intf.name }} unit 0 family ethernet-switching interface-mode access
83
+ {%- if intf.access_vlan %}
84
+ set interfaces {{ intf.name }} unit 0 family ethernet-switching vlan members {{ intf.access_vlan.value }}
85
+ {%- endif %}
86
+ {%- endif %}
87
+ {%- if intf.switchport_mode and intf.switchport_mode.value == "trunk" %}
88
+ set interfaces {{ intf.name }} unit 0 family ethernet-switching interface-mode trunk
89
+ {%- if intf.trunk_allowed_vlans %}
90
+ {%- for vlan in intf.trunk_allowed_vlans.value %}
91
+ set interfaces {{ intf.name }} unit 0 family ethernet-switching vlan members {{ vlan }}
92
+ {%- endfor %}
93
+ {%- endif %}
94
+ {%- if intf.native_vlan %}
95
+ set interfaces {{ intf.name }} native-vlan-id {{ intf.native_vlan.value }}
96
+ {%- endif %}
97
+ {%- endif %}
98
+ {%- endif %}
99
+ {%- if intf.aggregate_id %}
100
+ set interfaces {{ intf.name }} ether-options 802.3ad ae{{ intf.aggregate_id.value }}
101
+ {%- endif %}
102
+ {%- if intf.enabled and not intf.enabled.value %}
103
+ set interfaces {{ intf.name }} disable
104
+ {%- endif %}
105
+ {%- endif %}
106
+ {%- endfor %}
107
+ {#- LAG interfaces (ae<N>) -#}
108
+ {%- for _, intf in configuration.interfaces.items() %}
109
+ {%- if intf.type == "ieee8023adLag" %}
110
+ {%- if intf.description %}
111
+ set interfaces {{ intf.name }} description "{{ intf.description.value }}"
112
+ {%- endif %}
113
+ {%- if intf.mtu %}
114
+ set interfaces {{ intf.name }} mtu {{ intf.mtu.value }}
115
+ {%- endif %}
116
+ {%- if intf._lacp_enabled %}
117
+ set interfaces {{ intf.name }} aggregated-ether-options lacp {{ intf._lacp_mode }}
118
+ {%- if intf._lacp_interval %}
119
+ set interfaces {{ intf.name }} aggregated-ether-options lacp periodic {{ intf._lacp_interval }}
120
+ {%- endif %}
121
+ {%- endif %}
122
+ {%- if intf.switchport and intf.switchport.value %}
123
+ {%- if intf.switchport_mode and intf.switchport_mode.value == "access" %}
124
+ set interfaces {{ intf.name }} unit 0 family ethernet-switching interface-mode access
125
+ {%- endif %}
126
+ {%- if intf.switchport_mode and intf.switchport_mode.value == "trunk" %}
127
+ set interfaces {{ intf.name }} unit 0 family ethernet-switching interface-mode trunk
128
+ {%- if intf.trunk_allowed_vlans %}
129
+ {%- for vlan in intf.trunk_allowed_vlans.value %}
130
+ set interfaces {{ intf.name }} unit 0 family ethernet-switching vlan members {{ vlan }}
131
+ {%- endfor %}
132
+ {%- endif %}
133
+ {%- if intf.native_vlan %}
134
+ set interfaces {{ intf.name }} native-vlan-id {{ intf.native_vlan.value }}
135
+ {%- endif %}
136
+ {%- endif %}
137
+ {%- endif %}
138
+ {%- if intf.enabled and not intf.enabled.value %}
139
+ set interfaces {{ intf.name }} disable
140
+ {%- endif %}
141
+ {%- endif %}
142
+ {%- endfor %}
143
+ {#- VLANs (defined under network_instances.<ni>.vlans) -#}
144
+ {%- for _, ni in configuration.network_instances.items() %}
145
+ {%- if ni.vlans %}
146
+ {%- for _, vlan in ni.vlans.items() %}
147
+ {%- set vname = vlan.vlan_name.value if vlan.vlan_name else vlan.name.value %}
148
+ set vlans {{ vname }} vlan-id {{ vlan.vlan_id.value }}
149
+ {%- endfor %}
150
+ {%- endif %}
151
+ {%- endfor %}
152
+ {#- SNMP communities (with per-community augments such as juniper.snmp_community_clients) -#}
153
+ {%- if configuration.system.snmp and configuration.system.snmp.communities %}
154
+ {%- for name, com in configuration.system.snmp.communities.items() %}
155
+ set snmp community {{ name }} authorization {% if com.access and com.access.value == "READ_WRITE" %}read-write{% else %}read-only{% endif %}
156
+ {%- for line in configuration.augment_lines.get("system.snmp.communities." ~ name, []) %}
157
+ {{ line }}
158
+ {%- endfor %}
159
+ {%- endfor %}
160
+ {%- endif %}
161
+ {#- Static routes (per network_instance) -#}
162
+ {%- for ni_name, ni in configuration.network_instances.items() %}
163
+ {%- if ni.protocols and ni.protocols.static_routes %}
164
+ {%- for _, route in ni.protocols.static_routes.items() %}
165
+ {%- if route.next_hops %}
166
+ {%- for _, hop in route.next_hops.items() %}
167
+ {%- if ni_name == "global" %}
168
+ set routing-options static route {{ route.prefix.value }} next-hop {{ hop.next_hop.value }}
169
+ {%- else %}
170
+ set routing-instances {{ ni_name }} routing-options static route {{ route.prefix.value }} next-hop {{ hop.next_hop.value }}
171
+ {%- endif %}
172
+ {%- endfor %}
173
+ {%- endif %}
174
+ {%- endfor %}
175
+ {%- endif %}
176
+ {%- endfor %}