netconverter-cli 0.4.0__py3-none-any.whl

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,162 @@
1
+ """What the connected server accepts, in one shape for both credential profiles.
2
+
3
+ ``external`` credentials (an ``nck_*`` key or a Clerk sign-in) read
4
+ ``GET /capabilities``: every vendor, source→target path and job type the full
5
+ API accepts. ``quick`` credentials (0.1.x Terminal Quick pilot keys) keep the
6
+ pilot's fixed ASA → Palo Alto surface. Menus are built from this object, never
7
+ from a list in the client.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass, field
13
+
14
+ JOB_TYPES = ("convert", "validate", "optimize")
15
+ QUICK_SOURCES = ("cisco_asa", "palo_alto_set", "palo_alto_xml", "palo_alto_panorama")
16
+ QUICK_LIMIT_BYTES = 50_000
17
+ DEVICE_TYPES = ("firewall", "router", "switch")
18
+
19
+ # Names for codes a server response may not name (quick capabilities carry codes only).
20
+ NAMES = {
21
+ "cisco_asa": "Cisco ASA",
22
+ "palo_alto_set": "Palo Alto (Set)",
23
+ "palo_alto_xml": "Palo Alto (XML)",
24
+ "palo_alto_panorama": "Palo Alto (Panorama)",
25
+ }
26
+ # Human labels for option values shown in menus and the review card.
27
+ LABELS = {
28
+ "vr": "Virtual router (vr)",
29
+ "lr": "Logical router (lr)",
30
+ "like_for_like": "Like for like",
31
+ "ai_recommended": "AI recommended",
32
+ "convert": "Convert",
33
+ "validate": "Validate",
34
+ "optimize": "Optimize",
35
+ "analyze": "Validate",
36
+ }
37
+ FIELD_LABELS = {
38
+ "source_vendor": "Source",
39
+ "target_vendor": "Target",
40
+ "panos_target_version": "PAN-OS",
41
+ "routing_engine": "Routing",
42
+ "pipeline_mode": "Mode",
43
+ "remove_unused_objects": "Unused objects",
44
+ "panorama_device_group": "Device group",
45
+ "panorama_template": "Template",
46
+ "enable_appid": "App-ID",
47
+ "app_id_implementation": "App-ID mode",
48
+ "add_security_profiles": "Profiles",
49
+ "log_all_rules": "Log all rules",
50
+ "implicit_deny": "Explicit deny",
51
+ "default_profile_group": "Profile group",
52
+ "zone_mapping": "Zone mapping",
53
+ "job_type": "Job",
54
+ "file": "File",
55
+ }
56
+
57
+
58
+ def device_type_of(code: str) -> str:
59
+ for kind in ("router", "switch"):
60
+ if code.endswith("_" + kind):
61
+ return kind
62
+ return "firewall"
63
+
64
+
65
+ @dataclass
66
+ class Catalog:
67
+ profile: str = "unknown" # external | quick | unknown (not loaded yet)
68
+ vendors: dict = field(default_factory=dict) # code -> {name, device_type, roles, input}
69
+ paths: list = field(default_factory=list) # [(source, target)]
70
+ job_types: tuple = ()
71
+ limits: dict = field(default_factory=dict)
72
+ remaining: dict = field(default_factory=dict)
73
+ quick: dict = field(default_factory=dict) # pilot-only conversion settings
74
+ trial: bool = False
75
+
76
+ @classmethod
77
+ def from_external(cls, caps: dict) -> "Catalog":
78
+ vendors = {}
79
+ for item in caps.get("vendors") or []:
80
+ if isinstance(item, dict) and isinstance(item.get("code"), str):
81
+ code = item["code"]
82
+ vendors[code] = {
83
+ "name": str(item.get("name") or NAMES.get(code) or code),
84
+ "device_type": item.get("device_type") or device_type_of(code),
85
+ "roles": list(item.get("roles") or []),
86
+ "input": item.get("input") or "text",
87
+ }
88
+ paths = [
89
+ (p["source"], p["target"]) for p in caps.get("paths") or []
90
+ if isinstance(p, dict) and p.get("source") in vendors and p.get("target") in vendors
91
+ ]
92
+ types = tuple(t for t in caps.get("job_types") or ("convert",) if t in JOB_TYPES)
93
+ return cls("external", vendors, paths, types or ("convert",),
94
+ dict(caps.get("limits") or {}), {}, {}, bool(caps.get("trial")))
95
+
96
+ @classmethod
97
+ def from_quick(cls, caps: dict) -> "Catalog":
98
+ targets = [t for t in caps.get("targets") or QUICK_SOURCES[1:]]
99
+ vendors = {code: {"name": NAMES.get(code, code), "device_type": "firewall",
100
+ "roles": ["source"] + (["target"] if code in targets else []),
101
+ "input": "text"} for code in QUICK_SOURCES}
102
+ paths = [("cisco_asa", t) for t in targets]
103
+ quick = {k: caps[k] for k in ("panos_versions", "routing_engines", "pipeline_modes") if caps.get(k)}
104
+ limits = {"max_config_bytes": (caps.get("limits") or {}).get("max_config_bytes") or QUICK_LIMIT_BYTES}
105
+ return cls("quick", vendors, paths, JOB_TYPES, limits,
106
+ dict(caps.get("remaining") or {}), quick)
107
+
108
+ # -- questions the menus ask ---------------------------------------------
109
+
110
+ @property
111
+ def loaded(self) -> bool:
112
+ return bool(self.vendors)
113
+
114
+ @property
115
+ def max_bytes(self) -> int:
116
+ value = self.limits.get("max_config_bytes")
117
+ return value if isinstance(value, int) and value > 0 else QUICK_LIMIT_BYTES
118
+
119
+ def name(self, code: str | None) -> str:
120
+ if not code:
121
+ return "—"
122
+ return self.vendors.get(code, {}).get("name") or NAMES.get(code) or code
123
+
124
+ def sources(self, job_type: str = "convert") -> list[str]:
125
+ """Sources this job type can start from, uploadable as a text file."""
126
+ with_paths = {s for s, _ in self.paths}
127
+ return [
128
+ code for code, v in self.vendors.items()
129
+ if "source" in v["roles"] and v["input"] == "text"
130
+ and (job_type != "convert" or code in with_paths)
131
+ ]
132
+
133
+ def targets(self, source: str) -> list[str]:
134
+ return [t for s, t in self.paths if s == source]
135
+
136
+ def grouped(self, codes: list[str]) -> list[tuple[str, str]]:
137
+ """(label, code) ordered firewall → router → switch, device type shown when ambiguous."""
138
+ names = [self.name(c) for c in codes]
139
+ out = []
140
+ for kind in DEVICE_TYPES:
141
+ for code in (c for c in codes if self.vendors.get(c, {}).get("device_type", "firewall") == kind):
142
+ label = self.name(code)
143
+ if names.count(label) > 1:
144
+ label += f" · {kind}"
145
+ out.append((label, code))
146
+ return out
147
+
148
+ def supports(self, job_type: str) -> bool:
149
+ return job_type in self.job_types
150
+
151
+ def describe(self) -> str:
152
+ if self.profile == "quick":
153
+ return "Pilot key · ASA → Palo Alto"
154
+ if not self.loaded:
155
+ return ""
156
+ return f"{len([v for v in self.vendors.values() if 'source' in v['roles']])} sources · {len(self.paths)} paths"
157
+
158
+
159
+ def label(value) -> str:
160
+ if isinstance(value, bool):
161
+ return "Yes" if value else "No"
162
+ return LABELS.get(value, str(value)) if isinstance(value, str) else str(value)