hpavil 0.1.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.
- bridge_inventory/__init__.py +6 -0
- bridge_inventory/__main__.py +5 -0
- bridge_inventory/api/__init__.py +2 -0
- bridge_inventory/api/server.py +94 -0
- bridge_inventory/app.py +88 -0
- bridge_inventory/cli.py +66 -0
- bridge_inventory/core/__init__.py +2 -0
- bridge_inventory/core/models.py +212 -0
- bridge_inventory/core/parser.py +491 -0
- bridge_inventory/core/remediation/__init__.py +3 -0
- bridge_inventory/core/remediation/bridge_port.py +246 -0
- bridge_inventory/core/resolver.py +236 -0
- bridge_inventory/core/text.py +61 -0
- bridge_inventory/core/topology/__init__.py +19 -0
- bridge_inventory/core/topology/models.py +123 -0
- bridge_inventory/core/topology/parser.py +150 -0
- bridge_inventory/core/topology/resolver.py +176 -0
- bridge_inventory/infra/__init__.py +2 -0
- bridge_inventory/infra/file_scanner.py +44 -0
- bridge_inventory/infra/sqlite_store.py +674 -0
- hpavil-0.1.0.dist-info/METADATA +5 -0
- hpavil-0.1.0.dist-info/RECORD +24 -0
- hpavil-0.1.0.dist-info/WHEEL +4 -0
- hpavil-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
5
|
+
from typing import Any, Callable, Dict, Optional
|
|
6
|
+
from urllib.parse import parse_qs, urlparse
|
|
7
|
+
|
|
8
|
+
from ..app import BridgeInventoryService
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class JsonApiHandler(BaseHTTPRequestHandler):
|
|
12
|
+
service: BridgeInventoryService
|
|
13
|
+
|
|
14
|
+
def do_GET(self) -> None:
|
|
15
|
+
self._handle("GET")
|
|
16
|
+
|
|
17
|
+
def do_POST(self) -> None:
|
|
18
|
+
self._handle("POST")
|
|
19
|
+
|
|
20
|
+
def log_message(self, format: str, *args: object) -> None:
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
def _handle(self, method: str) -> None:
|
|
24
|
+
parsed = urlparse(self.path)
|
|
25
|
+
try:
|
|
26
|
+
if method == "GET" and parsed.path == "/health":
|
|
27
|
+
self._json(200, self.service.health())
|
|
28
|
+
elif method == "POST" and parsed.path == "/scan":
|
|
29
|
+
self._json(200, self.service.scan())
|
|
30
|
+
elif method == "GET" and parsed.path == "/summary":
|
|
31
|
+
self._json(200, self.service.summary())
|
|
32
|
+
elif method == "GET" and parsed.path == "/source-files":
|
|
33
|
+
self._json(200, {"items": self.service.source_files()})
|
|
34
|
+
elif method == "GET" and parsed.path == "/brokers":
|
|
35
|
+
self._json(200, {"items": self.service.brokers()})
|
|
36
|
+
elif method == "GET" and parsed.path == "/bridges":
|
|
37
|
+
query = parse_qs(parsed.query)
|
|
38
|
+
bridge_type = query.get("type", [None])[0]
|
|
39
|
+
self._json(200, {"items": self.service.bridges(bridge_type=bridge_type)})
|
|
40
|
+
elif method == "GET" and parsed.path.startswith("/bridges/"):
|
|
41
|
+
bridge_id = parsed.path.removeprefix("/bridges/")
|
|
42
|
+
bridge = self.service.bridge(bridge_id)
|
|
43
|
+
if bridge is None:
|
|
44
|
+
self._json(404, {"error": "bridge_not_found", "id": bridge_id})
|
|
45
|
+
else:
|
|
46
|
+
self._json(200, bridge)
|
|
47
|
+
elif method == "GET" and parsed.path == "/bridge-relationships":
|
|
48
|
+
self._json(200, {"items": self.service.relationships()})
|
|
49
|
+
elif method == "GET" and parsed.path == "/topology-relationships":
|
|
50
|
+
self._json(200, {"items": self.service.topology_relationships()})
|
|
51
|
+
elif method == "GET" and parsed.path == "/bridge-port-remediation-plan":
|
|
52
|
+
query = parse_qs(parsed.query)
|
|
53
|
+
broker = query.get("broker", [None])[0]
|
|
54
|
+
desired_port = _int_query(query, "desired_port", 55443)
|
|
55
|
+
self._json(
|
|
56
|
+
200,
|
|
57
|
+
self.service.bridge_port_remediation_plan(
|
|
58
|
+
broker=broker, desired_port=desired_port
|
|
59
|
+
),
|
|
60
|
+
)
|
|
61
|
+
elif method == "GET" and parsed.path == "/parser-warnings":
|
|
62
|
+
self._json(200, {"items": self.service.warnings()})
|
|
63
|
+
else:
|
|
64
|
+
self._json(404, {"error": "not_found", "path": parsed.path})
|
|
65
|
+
except Exception as exc:
|
|
66
|
+
self._json(500, {"error": "internal_error", "message": str(exc)})
|
|
67
|
+
|
|
68
|
+
def _json(self, status: int, payload: Any) -> None:
|
|
69
|
+
data = json.dumps(payload, indent=2, sort_keys=True).encode("utf-8")
|
|
70
|
+
self.send_response(status)
|
|
71
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
72
|
+
self.send_header("Content-Length", str(len(data)))
|
|
73
|
+
self.end_headers()
|
|
74
|
+
self.wfile.write(data)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def make_server(
|
|
78
|
+
service: BridgeInventoryService, host: str = "127.0.0.1", port: int = 8787
|
|
79
|
+
) -> ThreadingHTTPServer:
|
|
80
|
+
class Handler(JsonApiHandler):
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
Handler.service = service
|
|
84
|
+
return ThreadingHTTPServer((host, port), Handler)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _int_query(query: Dict[str, list[str]], name: str, default: int) -> int:
|
|
88
|
+
values = query.get(name)
|
|
89
|
+
if not values or values[0] is None:
|
|
90
|
+
return default
|
|
91
|
+
try:
|
|
92
|
+
return int(values[0])
|
|
93
|
+
except ValueError:
|
|
94
|
+
return default
|
bridge_inventory/app.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
from .core.parser import SolaceCliParser
|
|
7
|
+
from .core.remediation import BridgePortRemediationPlanner
|
|
8
|
+
from .core.resolver import BridgeRelationshipResolver
|
|
9
|
+
from .core.topology import TopologyParser, TopologyResolver
|
|
10
|
+
from .infra.file_scanner import CliDirectoryScanner
|
|
11
|
+
from .infra.sqlite_store import SQLiteInventoryStore
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BridgeInventoryService:
|
|
15
|
+
def __init__(self, cli_dir: Path, db_path: Path):
|
|
16
|
+
self.cli_dir = cli_dir
|
|
17
|
+
self.db_path = db_path
|
|
18
|
+
self.scanner = CliDirectoryScanner(cli_dir)
|
|
19
|
+
self.parser = SolaceCliParser()
|
|
20
|
+
self.resolver = BridgeRelationshipResolver()
|
|
21
|
+
self.topology_parser = TopologyParser()
|
|
22
|
+
self.topology_resolver = TopologyResolver()
|
|
23
|
+
self.bridge_port_remediation_planner = BridgePortRemediationPlanner()
|
|
24
|
+
self.store = SQLiteInventoryStore(db_path)
|
|
25
|
+
|
|
26
|
+
def scan(self) -> Dict[str, Any]:
|
|
27
|
+
source_files = self.scanner.scan()
|
|
28
|
+
parsed = []
|
|
29
|
+
topology_configs = []
|
|
30
|
+
for source_file in source_files:
|
|
31
|
+
broker_config = self.parser.parse_file(source_file.path)
|
|
32
|
+
parsed.append(broker_config)
|
|
33
|
+
topology_configs.append(
|
|
34
|
+
self.topology_parser.parse_file(
|
|
35
|
+
source_file.path, broker_config.identity.broker_name
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
relationships = self.resolver.resolve(parsed)
|
|
39
|
+
topology_relationships = self.topology_resolver.resolve(topology_configs)
|
|
40
|
+
self.store.replace_inventory(
|
|
41
|
+
source_files, parsed, relationships, topology_relationships
|
|
42
|
+
)
|
|
43
|
+
summary = self.store.summary()
|
|
44
|
+
summary["cli_dir"] = str(self.cli_dir.resolve())
|
|
45
|
+
summary["db_path"] = str(self.db_path.resolve())
|
|
46
|
+
return summary
|
|
47
|
+
|
|
48
|
+
def health(self) -> Dict[str, Any]:
|
|
49
|
+
self.store.initialize()
|
|
50
|
+
return {"status": "ok", "cli_dir": str(self.cli_dir), "db_path": str(self.db_path)}
|
|
51
|
+
|
|
52
|
+
def source_files(self) -> List[Dict[str, Any]]:
|
|
53
|
+
return self.store.list_source_files()
|
|
54
|
+
|
|
55
|
+
def brokers(self) -> List[Dict[str, Any]]:
|
|
56
|
+
return self.store.list_brokers()
|
|
57
|
+
|
|
58
|
+
def bridges(self, bridge_type: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
59
|
+
return self.store.list_bridges(bridge_type=bridge_type)
|
|
60
|
+
|
|
61
|
+
def bridge(self, bridge_id: str) -> Optional[Dict[str, Any]]:
|
|
62
|
+
return self.store.get_bridge(bridge_id)
|
|
63
|
+
|
|
64
|
+
def relationships(self) -> List[Dict[str, Any]]:
|
|
65
|
+
return self.store.list_relationships()
|
|
66
|
+
|
|
67
|
+
def topology_relationships(self) -> List[Dict[str, Any]]:
|
|
68
|
+
return self.store.list_topology_relationships()
|
|
69
|
+
|
|
70
|
+
def bridge_port_remediation_plan(
|
|
71
|
+
self, broker: Optional[str] = None, desired_port: int = 55443
|
|
72
|
+
) -> Dict[str, Any]:
|
|
73
|
+
return self.bridge_port_remediation_planner.plan(
|
|
74
|
+
bridges=self.bridges("message_vpn_bridge"),
|
|
75
|
+
relationships=self.relationships(),
|
|
76
|
+
broker=broker,
|
|
77
|
+
desired_port=desired_port,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def warnings(self) -> List[Dict[str, Any]]:
|
|
81
|
+
return self.store.list_warnings()
|
|
82
|
+
|
|
83
|
+
def summary(self) -> Dict[str, Any]:
|
|
84
|
+
self.store.initialize()
|
|
85
|
+
summary = self.store.summary()
|
|
86
|
+
summary["cli_dir"] = str(self.cli_dir.resolve())
|
|
87
|
+
summary["db_path"] = str(self.db_path.resolve())
|
|
88
|
+
return summary
|
bridge_inventory/cli.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .app import BridgeInventoryService
|
|
8
|
+
from .api.server import make_server
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main(argv: list[str] | None = None) -> int:
|
|
12
|
+
parser = argparse.ArgumentParser(description="Solace bridge inventory backend")
|
|
13
|
+
subcommands = parser.add_subparsers(dest="command", required=True)
|
|
14
|
+
|
|
15
|
+
scan_parser = subcommands.add_parser("scan", help="Scan local .cli files")
|
|
16
|
+
add_common_args(scan_parser)
|
|
17
|
+
|
|
18
|
+
serve_parser = subcommands.add_parser("serve", help="Run the REST API")
|
|
19
|
+
add_common_args(serve_parser)
|
|
20
|
+
serve_parser.add_argument("--host", default="127.0.0.1")
|
|
21
|
+
serve_parser.add_argument("--port", type=int, default=8787)
|
|
22
|
+
serve_parser.add_argument(
|
|
23
|
+
"--no-initial-scan",
|
|
24
|
+
action="store_true",
|
|
25
|
+
help="Start API without scanning first",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
args = parser.parse_args(argv)
|
|
29
|
+
service = BridgeInventoryService(Path(args.cli_dir), Path(args.db))
|
|
30
|
+
|
|
31
|
+
if args.command == "scan":
|
|
32
|
+
print(json.dumps(service.scan(), indent=2, sort_keys=True))
|
|
33
|
+
return 0
|
|
34
|
+
if args.command == "serve":
|
|
35
|
+
if not args.no_initial_scan:
|
|
36
|
+
summary = service.scan()
|
|
37
|
+
print(json.dumps({"initial_scan": summary}, indent=2, sort_keys=True))
|
|
38
|
+
server = make_server(service, host=args.host, port=args.port)
|
|
39
|
+
url = f"http://{args.host}:{args.port}"
|
|
40
|
+
print(f"Serving bridge inventory API at {url}")
|
|
41
|
+
print("Endpoints: /health /summary /bridges /bridge-relationships /topology-relationships /bridge-port-remediation-plan")
|
|
42
|
+
try:
|
|
43
|
+
server.serve_forever()
|
|
44
|
+
except KeyboardInterrupt:
|
|
45
|
+
print("\nStopping server")
|
|
46
|
+
finally:
|
|
47
|
+
server.server_close()
|
|
48
|
+
return 0
|
|
49
|
+
return 2
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def add_common_args(parser: argparse.ArgumentParser) -> None:
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"--cli-dir",
|
|
55
|
+
default=".",
|
|
56
|
+
help="Directory containing local .cli files",
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"--db",
|
|
60
|
+
default=".bridge_inventory.sqlite",
|
|
61
|
+
help="SQLite database path",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
if __name__ == "__main__":
|
|
66
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict, dataclass, field
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def without_none(value: Dict[str, Any]) -> Dict[str, Any]:
|
|
8
|
+
return {k: v for k, v in value.items() if v is not None}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class ParserWarning:
|
|
13
|
+
source_file: str
|
|
14
|
+
line_number: int
|
|
15
|
+
code: str
|
|
16
|
+
message: str
|
|
17
|
+
|
|
18
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
19
|
+
return asdict(self)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class BrokerIdentity:
|
|
24
|
+
source_file: str
|
|
25
|
+
broker_name: str
|
|
26
|
+
router_name: Optional[str] = None
|
|
27
|
+
version: Optional[str] = None
|
|
28
|
+
generated_at: Optional[str] = None
|
|
29
|
+
redacted: Optional[bool] = None
|
|
30
|
+
aliases: List[str] = field(default_factory=list)
|
|
31
|
+
|
|
32
|
+
def all_aliases(self) -> List[str]:
|
|
33
|
+
aliases = [self.broker_name]
|
|
34
|
+
if self.router_name:
|
|
35
|
+
aliases.append(self.router_name)
|
|
36
|
+
aliases.extend(self.aliases)
|
|
37
|
+
seen = set()
|
|
38
|
+
result = []
|
|
39
|
+
for alias in aliases:
|
|
40
|
+
if alias and alias not in seen:
|
|
41
|
+
seen.add(alias)
|
|
42
|
+
result.append(alias)
|
|
43
|
+
return result
|
|
44
|
+
|
|
45
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
46
|
+
return without_none(asdict(self))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class MessageVpn:
|
|
51
|
+
broker_name: str
|
|
52
|
+
name: str
|
|
53
|
+
admin_state: Optional[str] = None
|
|
54
|
+
replication_state: Optional[str] = None
|
|
55
|
+
|
|
56
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
57
|
+
return without_none(asdict(self))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class MessageVpnBridgeTarget:
|
|
62
|
+
remote_vpn: str
|
|
63
|
+
remote_kind: Optional[str] = None
|
|
64
|
+
remote_value: Optional[str] = None
|
|
65
|
+
remote_broker: Optional[str] = None
|
|
66
|
+
admin_state: Optional[str] = None
|
|
67
|
+
tls_enabled: Optional[bool] = None
|
|
68
|
+
compressed_data_enabled: Optional[bool] = None
|
|
69
|
+
message_spool_queue: Optional[str] = None
|
|
70
|
+
window_size: Optional[int] = None
|
|
71
|
+
connect_order: Optional[int] = None
|
|
72
|
+
unidirectional_client_profile: Optional[str] = None
|
|
73
|
+
source_line: Optional[int] = None
|
|
74
|
+
|
|
75
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
76
|
+
return without_none(asdict(self))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class MessageVpnBridge:
|
|
81
|
+
broker_name: str
|
|
82
|
+
source_vpn: str
|
|
83
|
+
bridge_name: str
|
|
84
|
+
redundancy: Optional[str] = None
|
|
85
|
+
admin_state: Optional[str] = None
|
|
86
|
+
max_ttl: Optional[int] = None
|
|
87
|
+
auth_scheme: Optional[str] = None
|
|
88
|
+
client_username: Optional[str] = None
|
|
89
|
+
retry_count: Optional[int] = None
|
|
90
|
+
retry_delay: Optional[int] = None
|
|
91
|
+
source_line: Optional[int] = None
|
|
92
|
+
targets: List[MessageVpnBridgeTarget] = field(default_factory=list)
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def bridge_type(self) -> str:
|
|
96
|
+
return "message_vpn_bridge"
|
|
97
|
+
|
|
98
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
99
|
+
data = without_none(asdict(self))
|
|
100
|
+
data["bridge_type"] = self.bridge_type
|
|
101
|
+
return data
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass
|
|
105
|
+
class VpnReplicationBridge:
|
|
106
|
+
broker_name: str
|
|
107
|
+
source_vpn: str
|
|
108
|
+
bridge_name: str = "#MSGVPN_REPLICATION_BRIDGE"
|
|
109
|
+
admin_state: Optional[str] = None
|
|
110
|
+
replication_state: Optional[str] = None
|
|
111
|
+
transaction_replication_mode: Optional[str] = None
|
|
112
|
+
auth_scheme: Optional[str] = None
|
|
113
|
+
client_username: Optional[str] = None
|
|
114
|
+
tls_enabled: Optional[bool] = None
|
|
115
|
+
compressed_data_enabled: Optional[bool] = None
|
|
116
|
+
window_size: Optional[int] = None
|
|
117
|
+
retry_delay: Optional[int] = None
|
|
118
|
+
unidirectional_client_profile: Optional[str] = None
|
|
119
|
+
queue_max_spool_usage: Optional[int] = None
|
|
120
|
+
queue_reject_msg_to_sender_on_discard: Optional[bool] = None
|
|
121
|
+
source_line: Optional[int] = None
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def bridge_type(self) -> str:
|
|
125
|
+
return "vpn_replication_bridge"
|
|
126
|
+
|
|
127
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
128
|
+
data = without_none(asdict(self))
|
|
129
|
+
data["bridge_type"] = self.bridge_type
|
|
130
|
+
return data
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass
|
|
134
|
+
class ConfigSyncReplicationBridge:
|
|
135
|
+
broker_name: str
|
|
136
|
+
bridge_name: str = "#CONFIG_SYNC_REPLICATION_BRIDGE"
|
|
137
|
+
admin_state: Optional[str] = None
|
|
138
|
+
remote_broker: Optional[str] = None
|
|
139
|
+
replication_mate_connect_via: Optional[str] = None
|
|
140
|
+
replication_mate_virtual_router: Optional[str] = None
|
|
141
|
+
auth_scheme: Optional[str] = None
|
|
142
|
+
tls_enabled: Optional[bool] = None
|
|
143
|
+
compressed_data_enabled: Optional[bool] = None
|
|
144
|
+
window_size: Optional[int] = None
|
|
145
|
+
retry_delay: Optional[int] = None
|
|
146
|
+
source_line: Optional[int] = None
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def bridge_type(self) -> str:
|
|
150
|
+
return "config_sync_replication_bridge"
|
|
151
|
+
|
|
152
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
153
|
+
data = without_none(asdict(self))
|
|
154
|
+
data["bridge_type"] = self.bridge_type
|
|
155
|
+
return data
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@dataclass
|
|
159
|
+
class ParsedBrokerConfig:
|
|
160
|
+
identity: BrokerIdentity
|
|
161
|
+
message_vpns: List[MessageVpn] = field(default_factory=list)
|
|
162
|
+
message_vpn_bridges: List[MessageVpnBridge] = field(default_factory=list)
|
|
163
|
+
vpn_replication_bridges: List[VpnReplicationBridge] = field(default_factory=list)
|
|
164
|
+
config_sync_replication_bridge: Optional[ConfigSyncReplicationBridge] = None
|
|
165
|
+
warnings: List[ParserWarning] = field(default_factory=list)
|
|
166
|
+
|
|
167
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
168
|
+
return {
|
|
169
|
+
"identity": self.identity.to_dict(),
|
|
170
|
+
"message_vpns": [x.to_dict() for x in self.message_vpns],
|
|
171
|
+
"message_vpn_bridges": [x.to_dict() for x in self.message_vpn_bridges],
|
|
172
|
+
"vpn_replication_bridges": [
|
|
173
|
+
x.to_dict() for x in self.vpn_replication_bridges
|
|
174
|
+
],
|
|
175
|
+
"config_sync_replication_bridge": (
|
|
176
|
+
self.config_sync_replication_bridge.to_dict()
|
|
177
|
+
if self.config_sync_replication_bridge
|
|
178
|
+
else None
|
|
179
|
+
),
|
|
180
|
+
"warnings": [x.to_dict() for x in self.warnings],
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@dataclass
|
|
185
|
+
class BridgeRelationship:
|
|
186
|
+
relationship_type: str
|
|
187
|
+
direction: str
|
|
188
|
+
source_broker: str
|
|
189
|
+
bridge_name: str
|
|
190
|
+
source_vpn: Optional[str] = None
|
|
191
|
+
remote_broker: Optional[str] = None
|
|
192
|
+
remote_vpn: Optional[str] = None
|
|
193
|
+
remote_endpoint: Optional[str] = None
|
|
194
|
+
status: str = "unresolved"
|
|
195
|
+
counterpart_bridge_name: Optional[str] = None
|
|
196
|
+
notes: List[str] = field(default_factory=list)
|
|
197
|
+
details: Dict[str, Any] = field(default_factory=dict)
|
|
198
|
+
|
|
199
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
200
|
+
return without_none(asdict(self))
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@dataclass
|
|
204
|
+
class Inventory:
|
|
205
|
+
brokers: List[ParsedBrokerConfig]
|
|
206
|
+
relationships: List[BridgeRelationship]
|
|
207
|
+
|
|
208
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
209
|
+
return {
|
|
210
|
+
"brokers": [x.to_dict() for x in self.brokers],
|
|
211
|
+
"relationships": [x.to_dict() for x in self.relationships],
|
|
212
|
+
}
|