iris-pex-embedded-python 3.7.2b2__py3-none-any.whl → 4.0.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.
- iop/__init__.py +276 -18
- iop/__main__.py +3 -3
- iop/cli/__init__.py +1 -0
- iop/cli/formatting.py +45 -0
- iop/{_cli.py → cli/main.py} +238 -216
- iop/cli/parser.py +165 -0
- iop/cli/types.py +73 -0
- iop/cls/IOP/Common.cls +11 -2
- iop/cls/IOP/PrivateSession/Duplex.cls +1 -1
- iop/cls/IOP/Service/Remote/Rest/v1.cls +152 -5
- iop/cls/IOP/Utils.cls +814 -7
- iop/cls/IOP/Wrapper.cls +1 -1
- iop/components/__init__.py +16 -0
- iop/{_async_request.py → components/async_request.py} +7 -7
- iop/components/business_host.py +363 -0
- iop/components/business_operation.py +84 -0
- iop/{_business_process.py → components/business_process.py} +145 -125
- iop/components/business_service.py +106 -0
- iop/{_common.py → components/common.py} +160 -164
- iop/{_debugpy.py → components/debugpy.py} +11 -11
- iop/{_generator_request.py → components/generator_request.py} +4 -4
- iop/{_inbound_adapter.py → components/inbound_adapter.py} +10 -18
- iop/{_log_manager.py → components/log_manager.py} +1 -1
- iop/{_outbound_adapter.py → components/outbound_adapter.py} +1 -1
- iop/components/polling_business_service.py +48 -0
- iop/{_private_session_duplex.py → components/private_session_duplex.py} +7 -9
- iop/{_private_session_process.py → components/private_session_process.py} +2 -2
- iop/{_settings.py → components/settings.py} +23 -3
- iop/messages/__init__.py +7 -0
- iop/messages/base.py +61 -0
- iop/{_decorators.py → messages/decorators.py} +8 -2
- iop/messages/dispatch.py +351 -0
- iop/{_persistent_message.py → messages/persistent.py} +24 -19
- iop/{_serialization.py → messages/serialization.py} +34 -21
- iop/{_message_validator.py → messages/validation.py} +15 -13
- iop/migration/__init__.py +1 -0
- iop/migration/io.py +61 -0
- iop/migration/manifest.py +534 -0
- iop/migration/plans.py +80 -0
- iop/migration/utils.py +1126 -0
- iop/production/__init__.py +55 -0
- iop/production/actions.py +525 -0
- iop/production/common.py +103 -0
- iop/production/component.py +234 -0
- iop/production/declarations.py +190 -0
- iop/production/declarative.py +214 -0
- iop/production/diff.py +327 -0
- iop/production/import_.py +296 -0
- iop/production/inspection.py +92 -0
- iop/production/model.py +1143 -0
- iop/production/planning.py +678 -0
- iop/production/reconstruction.py +341 -0
- iop/production/rendering.py +610 -0
- iop/production/runtime.py +91 -0
- iop/production/source_inference.py +729 -0
- iop/production/types.py +546 -0
- iop/production/validation.py +452 -0
- iop/runtime/__init__.py +1 -0
- iop/runtime/director.py +415 -0
- iop/runtime/environment.py +59 -0
- iop/{_iris.py → runtime/iris.py} +2 -2
- iop/runtime/local.py +145 -0
- iop/{_director_protocol.py → runtime/protocol.py} +25 -8
- iop/runtime/remote/__init__.py +12 -0
- iop/runtime/remote/client.py +90 -0
- iop/runtime/remote/director.py +283 -0
- iop/runtime/remote/migration.py +57 -0
- iop/runtime/remote/settings.py +69 -0
- iop/runtime/remote/setup.py +72 -0
- {iris_pex_embedded_python-3.7.2b2.dist-info → iris_pex_embedded_python-4.0.0.dist-info}/METADATA +11 -2
- iris_pex_embedded_python-4.0.0.dist-info/RECORD +99 -0
- iris_pex_embedded_python-4.0.0.dist-info/entry_points.txt +2 -0
- iris_pex_embedded_python-4.0.0.dist-info/top_level.txt +1 -0
- grongier/__init__.py +0 -0
- grongier/cls/Grongier/PEX/BusinessOperation.cls +0 -8
- grongier/cls/Grongier/PEX/BusinessProcess.cls +0 -13
- grongier/cls/Grongier/PEX/BusinessService.cls +0 -8
- grongier/cls/Grongier/PEX/Common.cls +0 -10
- grongier/cls/Grongier/PEX/Director.cls +0 -10
- grongier/cls/Grongier/PEX/Duplex/Operation.cls +0 -4
- grongier/cls/Grongier/PEX/Duplex/Process.cls +0 -13
- grongier/cls/Grongier/PEX/Duplex/Service.cls +0 -4
- grongier/cls/Grongier/PEX/InboundAdapter.cls +0 -8
- grongier/cls/Grongier/PEX/Message.cls +0 -13
- grongier/cls/Grongier/PEX/OutboundAdapter.cls +0 -8
- grongier/cls/Grongier/PEX/PickleMessage.cls +0 -13
- grongier/cls/Grongier/PEX/PrivateSession/Duplex.cls +0 -8
- grongier/cls/Grongier/PEX/PrivateSession/Message/Ack.cls +0 -14
- grongier/cls/Grongier/PEX/PrivateSession/Message/Poll.cls +0 -14
- grongier/cls/Grongier/PEX/PrivateSession/Message/Start.cls +0 -14
- grongier/cls/Grongier/PEX/PrivateSession/Message/Stop.cls +0 -14
- grongier/cls/Grongier/PEX/Test.cls +0 -10
- grongier/cls/Grongier/PEX/Utils.cls +0 -10
- grongier/cls/Grongier/Service/WSGI.cls +0 -4
- grongier/pex/__init__.py +0 -75
- grongier/pex/__main__.py +0 -4
- grongier/pex/_business_host.py +0 -1
- grongier/pex/_cli.py +0 -4
- grongier/pex/_common.py +0 -1
- grongier/pex/_director.py +0 -1
- grongier/pex/_utils.py +0 -1
- grongier/pex/wsgi/handlers.py +0 -104
- iop/_business_host.py +0 -322
- iop/_business_operation.py +0 -77
- iop/_business_service.py +0 -67
- iop/_director.py +0 -333
- iop/_dispatch.py +0 -194
- iop/_local.py +0 -106
- iop/_message.py +0 -45
- iop/_polling_business_service.py +0 -6
- iop/_remote.py +0 -473
- iop/_utils.py +0 -912
- iop/cls/IOP/Service/WSGI.cls +0 -310
- iop/wsgi/handlers.py +0 -112
- iris_pex_embedded_python-3.7.2b2.dist-info/RECORD +0 -96
- iris_pex_embedded_python-3.7.2b2.dist-info/entry_points.txt +0 -2
- iris_pex_embedded_python-3.7.2b2.dist-info/top_level.txt +0 -2
- {iris_pex_embedded_python-3.7.2b2.dist-info → iris_pex_embedded_python-4.0.0.dist-info}/WHEEL +0 -0
- {iris_pex_embedded_python-3.7.2b2.dist-info → iris_pex_embedded_python-4.0.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .component import ComponentRef as ComponentRef
|
|
4
|
+
from .declarations import ComponentItem as ComponentItem
|
|
5
|
+
from .declarations import OperationItem as OperationItem
|
|
6
|
+
from .declarations import ProcessItem as ProcessItem
|
|
7
|
+
from .declarations import Route as Route
|
|
8
|
+
from .declarations import ServiceItem as ServiceItem
|
|
9
|
+
from .model import Production as Production
|
|
10
|
+
from .planning import ProductionApplyResult as ProductionApplyResult
|
|
11
|
+
from .planning import ProductionChangePlan as ProductionChangePlan
|
|
12
|
+
from .planning import ProductionPlanOperation as ProductionPlanOperation
|
|
13
|
+
from .planning import ProductionVerifyResult as ProductionVerifyResult
|
|
14
|
+
from .runtime import resolve_target as resolve_target
|
|
15
|
+
from .types import GraphEdge as GraphEdge
|
|
16
|
+
from .types import GraphNode as GraphNode
|
|
17
|
+
from .types import PersistentMessageRegistration as PersistentMessageRegistration
|
|
18
|
+
from .types import ProductionDiff as ProductionDiff
|
|
19
|
+
from .types import ProductionDiffEntry as ProductionDiffEntry
|
|
20
|
+
from .types import ProductionGraph as ProductionGraph
|
|
21
|
+
from .types import TargetSetting as TargetSetting
|
|
22
|
+
from .types import TargetSettingRef as TargetSettingRef
|
|
23
|
+
from .types import target as target
|
|
24
|
+
from .validation import ProductionValidationError as ProductionValidationError
|
|
25
|
+
from .validation import ProductionValidationIssue as ProductionValidationIssue
|
|
26
|
+
from .validation import ProductionValidationReport as ProductionValidationReport
|
|
27
|
+
from .validation import ProductionValidationWarning as ProductionValidationWarning
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"ComponentRef",
|
|
31
|
+
"ComponentItem",
|
|
32
|
+
"GraphEdge",
|
|
33
|
+
"GraphNode",
|
|
34
|
+
"OperationItem",
|
|
35
|
+
"PersistentMessageRegistration",
|
|
36
|
+
"ProcessItem",
|
|
37
|
+
"Production",
|
|
38
|
+
"ProductionApplyResult",
|
|
39
|
+
"ProductionChangePlan",
|
|
40
|
+
"ProductionDiff",
|
|
41
|
+
"ProductionDiffEntry",
|
|
42
|
+
"ProductionGraph",
|
|
43
|
+
"ProductionPlanOperation",
|
|
44
|
+
"ProductionVerifyResult",
|
|
45
|
+
"ProductionValidationError",
|
|
46
|
+
"ProductionValidationIssue",
|
|
47
|
+
"ProductionValidationReport",
|
|
48
|
+
"ProductionValidationWarning",
|
|
49
|
+
"Route",
|
|
50
|
+
"ServiceItem",
|
|
51
|
+
"TargetSetting",
|
|
52
|
+
"TargetSettingRef",
|
|
53
|
+
"resolve_target",
|
|
54
|
+
"target",
|
|
55
|
+
]
|
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .component import ComponentRef
|
|
8
|
+
from .import_ import _normalize_queue_info
|
|
9
|
+
from .planning import (
|
|
10
|
+
ProductionApplyResult,
|
|
11
|
+
ProductionChangePlan,
|
|
12
|
+
ProductionVerifyResult,
|
|
13
|
+
create_backup,
|
|
14
|
+
production_fingerprint,
|
|
15
|
+
skipped_operation_results,
|
|
16
|
+
verify_change_plan,
|
|
17
|
+
)
|
|
18
|
+
from .rendering import message_to_classname_body
|
|
19
|
+
from .runtime import _has_remote_director, _ProductionRuntime, _temporary_env
|
|
20
|
+
from .types import TargetSettingRef
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def start(production, detach: bool = True) -> None:
|
|
24
|
+
director = _ProductionRuntime(production).director
|
|
25
|
+
if detach:
|
|
26
|
+
director.start_production(production.name)
|
|
27
|
+
else:
|
|
28
|
+
director.start_production_with_log(production.name)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def stop(production) -> None:
|
|
32
|
+
director = _ProductionRuntime(production).director
|
|
33
|
+
require_current_production(production, director, "stop")
|
|
34
|
+
director.stop_production()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def restart(production) -> None:
|
|
38
|
+
director = _ProductionRuntime(production).director
|
|
39
|
+
require_current_production(production, director, "restart")
|
|
40
|
+
director.restart_production()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def kill(production) -> None:
|
|
44
|
+
director = _ProductionRuntime(production).director
|
|
45
|
+
require_current_production(production, director, "kill")
|
|
46
|
+
director.shutdown_production()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def status(production) -> dict:
|
|
50
|
+
return _ProductionRuntime(production).director.status_production()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def queue(production, *, refresh: bool = True) -> dict[str, dict[str, Any]]:
|
|
54
|
+
if not refresh:
|
|
55
|
+
return {name: dict(value) for name, value in production._queue_info.items()}
|
|
56
|
+
info = _ProductionRuntime(production).director.export_production_queue_info(
|
|
57
|
+
production.name
|
|
58
|
+
)
|
|
59
|
+
queue_map, _queue_warnings = _normalize_queue_info(info)
|
|
60
|
+
production._queue_info = queue_map
|
|
61
|
+
return {name: dict(value) for name, value in queue_map.items()}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def update(production) -> None:
|
|
65
|
+
director = _ProductionRuntime(production).director
|
|
66
|
+
require_current_production(production, director, "update")
|
|
67
|
+
director.update_production()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def start_component(
|
|
71
|
+
production,
|
|
72
|
+
component: ComponentRef | TargetSettingRef | str,
|
|
73
|
+
) -> None:
|
|
74
|
+
component_name = production._runtime_component_name(component)
|
|
75
|
+
director = _ProductionRuntime(production).director
|
|
76
|
+
require_current_runtime(
|
|
77
|
+
production,
|
|
78
|
+
director,
|
|
79
|
+
f"start component {component_name!r} in production {production.name!r}",
|
|
80
|
+
)
|
|
81
|
+
director.start_component(component_name)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def stop_component(
|
|
85
|
+
production,
|
|
86
|
+
component: ComponentRef | TargetSettingRef | str,
|
|
87
|
+
) -> None:
|
|
88
|
+
component_name = production._runtime_component_name(component)
|
|
89
|
+
director = _ProductionRuntime(production).director
|
|
90
|
+
require_current_runtime(
|
|
91
|
+
production,
|
|
92
|
+
director,
|
|
93
|
+
f"stop component {component_name!r} in production {production.name!r}",
|
|
94
|
+
)
|
|
95
|
+
director.stop_component(component_name)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def restart_component(
|
|
99
|
+
production,
|
|
100
|
+
component: ComponentRef | TargetSettingRef | str,
|
|
101
|
+
) -> None:
|
|
102
|
+
component_name = production._runtime_component_name(component)
|
|
103
|
+
director = _ProductionRuntime(production).director
|
|
104
|
+
require_current_runtime(
|
|
105
|
+
production,
|
|
106
|
+
director,
|
|
107
|
+
f"restart component {component_name!r} in production {production.name!r}",
|
|
108
|
+
)
|
|
109
|
+
director.restart_component(component_name)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def export(production) -> dict:
|
|
113
|
+
return _ProductionRuntime(production).director.export_production(production.name)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def set_default(production) -> None:
|
|
117
|
+
_ProductionRuntime(production).director.set_default_production(production.name)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def sync(
|
|
121
|
+
production, *, root_path: str | None = None, update_runtime: bool = True
|
|
122
|
+
) -> None:
|
|
123
|
+
if _has_remote_director(production):
|
|
124
|
+
raise NotImplementedError(
|
|
125
|
+
"Production.sync() can only register directly with local IRIS. "
|
|
126
|
+
"Use `iop --migrate <settings_file>` for remote migrations."
|
|
127
|
+
)
|
|
128
|
+
from ..migration import utils as migration_utils
|
|
129
|
+
|
|
130
|
+
with _temporary_env("IRISNAMESPACE", production.namespace):
|
|
131
|
+
migration_utils.set_productions_settings([production], root_path)
|
|
132
|
+
if update_runtime:
|
|
133
|
+
from ..runtime.local import _LocalDirector
|
|
134
|
+
|
|
135
|
+
_LocalDirector().update_production()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def apply(
|
|
139
|
+
production,
|
|
140
|
+
*,
|
|
141
|
+
plan: ProductionChangePlan | None = None,
|
|
142
|
+
allow_destructive: bool = False,
|
|
143
|
+
backup_dir: str = ".iop/backups",
|
|
144
|
+
root_path: str | None = None,
|
|
145
|
+
update_runtime: bool = True,
|
|
146
|
+
) -> ProductionApplyResult:
|
|
147
|
+
director = _ProductionRuntime(production).director
|
|
148
|
+
if _director_is_remote(director) or _has_remote_director(production):
|
|
149
|
+
raise RuntimeError(
|
|
150
|
+
"Remote production plan apply is not supported in v1. "
|
|
151
|
+
"Run apply from a local IRIS environment."
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
if plan is None:
|
|
155
|
+
plan = production.plan()
|
|
156
|
+
if plan.production_name != production.name:
|
|
157
|
+
raise ValueError(
|
|
158
|
+
f"Plan targets {plan.production_name!r}, not {production.name!r}."
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
apply_operations = plan.operations_for_apply(
|
|
162
|
+
allow_destructive=allow_destructive,
|
|
163
|
+
)
|
|
164
|
+
skipped = skipped_operation_results(
|
|
165
|
+
plan,
|
|
166
|
+
allow_destructive=allow_destructive,
|
|
167
|
+
)
|
|
168
|
+
if not apply_operations:
|
|
169
|
+
return ProductionApplyResult(
|
|
170
|
+
plan_id=plan.id,
|
|
171
|
+
production_name=plan.production_name,
|
|
172
|
+
operations=tuple(skipped),
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
current, exported, connections, queues = _read_current_snapshot(production, director)
|
|
176
|
+
current_fingerprint = production_fingerprint(current)
|
|
177
|
+
if current_fingerprint != plan.source_fingerprint:
|
|
178
|
+
raise RuntimeError(
|
|
179
|
+
"Cannot apply production plan: current IRIS production changed since "
|
|
180
|
+
"the plan was created."
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
backup_path = create_backup(
|
|
184
|
+
backup_dir=backup_dir,
|
|
185
|
+
plan=plan,
|
|
186
|
+
current=current,
|
|
187
|
+
current_export=exported,
|
|
188
|
+
connections=connections,
|
|
189
|
+
queues=queues,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
_register_plan_classes(production, root_path=root_path)
|
|
193
|
+
raw_result = director.apply_production_plan(
|
|
194
|
+
plan.to_dict(),
|
|
195
|
+
allow_destructive=allow_destructive,
|
|
196
|
+
)
|
|
197
|
+
if isinstance(raw_result, dict) and raw_result.get("error"):
|
|
198
|
+
raise RuntimeError(str(raw_result["error"]))
|
|
199
|
+
operation_results = _apply_operation_results(raw_result)
|
|
200
|
+
operation_results.extend(skipped)
|
|
201
|
+
updated_runtime = _update_runtime_if_current(
|
|
202
|
+
production,
|
|
203
|
+
director,
|
|
204
|
+
update_runtime=update_runtime,
|
|
205
|
+
)
|
|
206
|
+
return ProductionApplyResult(
|
|
207
|
+
plan_id=plan.id,
|
|
208
|
+
production_name=plan.production_name,
|
|
209
|
+
backup_path=str(backup_path),
|
|
210
|
+
operations=tuple(operation_results),
|
|
211
|
+
updated_runtime=updated_runtime,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def verify(production, plan: ProductionChangePlan) -> ProductionVerifyResult:
|
|
216
|
+
director = _ProductionRuntime(production).director
|
|
217
|
+
current = production.from_iris(
|
|
218
|
+
plan.production_name,
|
|
219
|
+
namespace=production.namespace,
|
|
220
|
+
director=director,
|
|
221
|
+
)
|
|
222
|
+
return verify_change_plan(plan, current)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def rollback_backup(
|
|
226
|
+
backup_path: str,
|
|
227
|
+
*,
|
|
228
|
+
director: Any = None,
|
|
229
|
+
namespace: str | None = None,
|
|
230
|
+
allow_destructive: bool = False,
|
|
231
|
+
update_runtime: bool = True,
|
|
232
|
+
) -> ProductionVerifyResult:
|
|
233
|
+
if not allow_destructive:
|
|
234
|
+
raise RuntimeError(
|
|
235
|
+
"Rollback restores a full production export and requires "
|
|
236
|
+
"allow_destructive=True."
|
|
237
|
+
)
|
|
238
|
+
if director is None:
|
|
239
|
+
from ..runtime.local import _LocalDirector
|
|
240
|
+
|
|
241
|
+
director = _LocalDirector()
|
|
242
|
+
if _director_is_remote(director):
|
|
243
|
+
raise RuntimeError(
|
|
244
|
+
"Remote production rollback is not supported in v1. "
|
|
245
|
+
"Run rollback from a local IRIS environment."
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
path = Path(backup_path)
|
|
249
|
+
production_data = json.loads((path / "production.json").read_text(encoding="utf-8"))
|
|
250
|
+
metadata = json.loads((path / "metadata.json").read_text(encoding="utf-8"))
|
|
251
|
+
production_name = metadata.get("production") or next(iter(production_data))
|
|
252
|
+
|
|
253
|
+
from ..migration import utils as migration_utils
|
|
254
|
+
|
|
255
|
+
with _temporary_env("IRISNAMESPACE", namespace or metadata.get("namespace") or None):
|
|
256
|
+
migration_utils.register_production_definition(
|
|
257
|
+
production_name,
|
|
258
|
+
_normalized_production_definition(production_name, production_data),
|
|
259
|
+
)
|
|
260
|
+
if update_runtime:
|
|
261
|
+
_update_runtime_if_current_name(
|
|
262
|
+
production_name,
|
|
263
|
+
director,
|
|
264
|
+
update_runtime=True,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
from .model import Production
|
|
268
|
+
|
|
269
|
+
restored = Production.from_iris(
|
|
270
|
+
production_name,
|
|
271
|
+
director=director,
|
|
272
|
+
namespace=namespace or metadata.get("namespace") or None,
|
|
273
|
+
)
|
|
274
|
+
expected = _production_from_export(
|
|
275
|
+
production_name,
|
|
276
|
+
production_data,
|
|
277
|
+
director=director,
|
|
278
|
+
namespace=namespace or metadata.get("namespace") or None,
|
|
279
|
+
)
|
|
280
|
+
result_plan = ProductionChangePlan(
|
|
281
|
+
id=str(metadata.get("plan_id") or ""),
|
|
282
|
+
production_name=production_name,
|
|
283
|
+
namespace=str(namespace or metadata.get("namespace") or ""),
|
|
284
|
+
source_fingerprint=production_fingerprint(expected),
|
|
285
|
+
desired_fingerprint=production_fingerprint(expected),
|
|
286
|
+
)
|
|
287
|
+
if production_fingerprint(restored) != production_fingerprint(expected):
|
|
288
|
+
return ProductionVerifyResult(
|
|
289
|
+
plan_id=result_plan.id,
|
|
290
|
+
production_name=production_name,
|
|
291
|
+
failed_operations=(
|
|
292
|
+
{"id": "rollback", "path": production_name, "status": "failed"},
|
|
293
|
+
),
|
|
294
|
+
)
|
|
295
|
+
return ProductionVerifyResult(
|
|
296
|
+
plan_id=result_plan.id,
|
|
297
|
+
production_name=production_name,
|
|
298
|
+
converged_operations=("rollback",),
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def log(production, top: int | None = None) -> None:
|
|
303
|
+
director = _ProductionRuntime(production).director
|
|
304
|
+
if top is None:
|
|
305
|
+
director.log_production()
|
|
306
|
+
else:
|
|
307
|
+
director.log_production_top(top)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def test_component(
|
|
311
|
+
production,
|
|
312
|
+
target_or_ref: str | TargetSettingRef | ComponentRef,
|
|
313
|
+
message: Any = None,
|
|
314
|
+
classname: str | None = None,
|
|
315
|
+
body: str | dict | None = None,
|
|
316
|
+
) -> Any:
|
|
317
|
+
director = _ProductionRuntime(production).director
|
|
318
|
+
target_name = production.resolve_target(target_or_ref)
|
|
319
|
+
|
|
320
|
+
raise_if_existing_production_not_running(production, director)
|
|
321
|
+
|
|
322
|
+
if classname is None and body is None and message is not None:
|
|
323
|
+
classname, body = message_to_classname_body(message)
|
|
324
|
+
if classname is not None:
|
|
325
|
+
message = None
|
|
326
|
+
return director.test_component(
|
|
327
|
+
target_name,
|
|
328
|
+
message=message,
|
|
329
|
+
classname=classname,
|
|
330
|
+
body=body,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def raise_if_existing_production_not_running(production, director: Any) -> None:
|
|
335
|
+
production_name, state = read_status(production, director, "test")
|
|
336
|
+
state_lower = str(state).lower()
|
|
337
|
+
if production_name == production.name and str(state).lower() == "running":
|
|
338
|
+
return
|
|
339
|
+
|
|
340
|
+
if (
|
|
341
|
+
production_name
|
|
342
|
+
and production_name != production.name
|
|
343
|
+
and state_lower == "running"
|
|
344
|
+
):
|
|
345
|
+
raise RuntimeError(
|
|
346
|
+
f"Production {production.name!r} exists but is not running "
|
|
347
|
+
f"(currently running production is {production_name!r}). "
|
|
348
|
+
f"{switch_running_production_message(production, production_name)} "
|
|
349
|
+
"Do that before calling `prod.test(...)`."
|
|
350
|
+
)
|
|
351
|
+
if production_name and production_name != production.name:
|
|
352
|
+
detail = f"current default production is {production_name!r}"
|
|
353
|
+
elif state:
|
|
354
|
+
detail = f"current status is {state!r}"
|
|
355
|
+
else:
|
|
356
|
+
detail = "runtime status did not report a running production"
|
|
357
|
+
raise RuntimeError(
|
|
358
|
+
f"Production {production.name!r} exists but is not running ({detail}). "
|
|
359
|
+
f"Start it with `iop --start {production.name} --detach` or "
|
|
360
|
+
"`prod.start()` before calling `prod.test(...)`."
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def require_current_production(production, director: Any, action: str) -> None:
|
|
365
|
+
require_current_runtime(
|
|
366
|
+
production,
|
|
367
|
+
director,
|
|
368
|
+
f"{action} production {production.name!r}",
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def require_current_runtime(production, director: Any, action: str) -> None:
|
|
373
|
+
production_name, state = read_status(production, director, action)
|
|
374
|
+
if production_name == production.name:
|
|
375
|
+
return
|
|
376
|
+
|
|
377
|
+
if production_name:
|
|
378
|
+
detail = f"current/default production is {production_name!r}"
|
|
379
|
+
elif state:
|
|
380
|
+
detail = f"current status is {state!r}"
|
|
381
|
+
else:
|
|
382
|
+
detail = "runtime status did not report a current/default production"
|
|
383
|
+
raise RuntimeError(
|
|
384
|
+
f"Cannot {action}: {detail}. "
|
|
385
|
+
f"Select {production.name!r} with `prod.set_default()` or start it with "
|
|
386
|
+
"`prod.start()` before using this lifecycle method."
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def read_status(production, director: Any, action: str) -> tuple[str, str]:
|
|
391
|
+
try:
|
|
392
|
+
status = director.status_production()
|
|
393
|
+
except Exception as exc:
|
|
394
|
+
raise RuntimeError(
|
|
395
|
+
f"Cannot {action}: could not verify production status ({exc})."
|
|
396
|
+
) from exc
|
|
397
|
+
if not isinstance(status, dict):
|
|
398
|
+
raise RuntimeError(
|
|
399
|
+
f"Cannot {action}: production status response is invalid ({status!r})."
|
|
400
|
+
)
|
|
401
|
+
production_name = status.get("Production") or status.get("production") or ""
|
|
402
|
+
state = status.get("Status") or status.get("status") or ""
|
|
403
|
+
return str(production_name), str(state)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def switch_running_production_message(production, current_production: str) -> str:
|
|
407
|
+
return (
|
|
408
|
+
f"IRIS can run only one production at a time. Stop {current_production!r} "
|
|
409
|
+
f"first with `iop --stop`, then start {production.name!r} with "
|
|
410
|
+
f"`iop --start {production.name} --detach`; or call `prod.stop()` and "
|
|
411
|
+
"`prod.start()` explicitly."
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _read_current_snapshot(production, director: Any):
|
|
416
|
+
exported = director.export_production(production.name)
|
|
417
|
+
try:
|
|
418
|
+
connections = director.export_production_connections(production.name)
|
|
419
|
+
except Exception as exc:
|
|
420
|
+
connections = {"warnings": [f"Could not export runtime connections: {exc}"]}
|
|
421
|
+
try:
|
|
422
|
+
queues = director.export_production_queue_info(production.name)
|
|
423
|
+
except Exception as exc:
|
|
424
|
+
queues = {"warnings": [f"Could not export queue info: {exc}"]}
|
|
425
|
+
current = production.from_dict(
|
|
426
|
+
exported,
|
|
427
|
+
connections=connections,
|
|
428
|
+
queue_info=queues,
|
|
429
|
+
namespace=production.namespace,
|
|
430
|
+
director=director,
|
|
431
|
+
)
|
|
432
|
+
return current, exported, connections, queues
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _register_plan_classes(production, *, root_path: str | None = None) -> None:
|
|
436
|
+
from ..migration import utils as migration_utils
|
|
437
|
+
|
|
438
|
+
with _temporary_env("IRISNAMESPACE", production.namespace):
|
|
439
|
+
migration_utils._register_production_object_messages(production)
|
|
440
|
+
migration_utils._register_production_object_components(production, root_path)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _apply_operation_results(raw_result: Any) -> list[dict[str, Any]]:
|
|
444
|
+
if raw_result is None:
|
|
445
|
+
return []
|
|
446
|
+
if isinstance(raw_result, str):
|
|
447
|
+
raw_result = json.loads(raw_result)
|
|
448
|
+
if isinstance(raw_result, dict):
|
|
449
|
+
return [dict(item) for item in raw_result.get("operations", ())]
|
|
450
|
+
if isinstance(raw_result, list):
|
|
451
|
+
return [dict(item) for item in raw_result]
|
|
452
|
+
return []
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _update_runtime_if_current(
|
|
456
|
+
production,
|
|
457
|
+
director: Any,
|
|
458
|
+
*,
|
|
459
|
+
update_runtime: bool,
|
|
460
|
+
) -> bool:
|
|
461
|
+
return _update_runtime_if_current_name(
|
|
462
|
+
production.name,
|
|
463
|
+
director,
|
|
464
|
+
update_runtime=update_runtime,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _update_runtime_if_current_name(
|
|
469
|
+
production_name: str,
|
|
470
|
+
director: Any,
|
|
471
|
+
*,
|
|
472
|
+
update_runtime: bool,
|
|
473
|
+
) -> bool:
|
|
474
|
+
if not update_runtime:
|
|
475
|
+
return False
|
|
476
|
+
try:
|
|
477
|
+
status = director.status_production()
|
|
478
|
+
except Exception:
|
|
479
|
+
return False
|
|
480
|
+
current = status.get("Production") or status.get("production") or ""
|
|
481
|
+
if current != production_name:
|
|
482
|
+
return False
|
|
483
|
+
director.update_production()
|
|
484
|
+
return True
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _director_is_remote(director: Any) -> bool:
|
|
488
|
+
try:
|
|
489
|
+
from ..runtime.remote import _RemoteDirector
|
|
490
|
+
except Exception:
|
|
491
|
+
return False
|
|
492
|
+
return isinstance(director, _RemoteDirector)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _production_from_export(
|
|
496
|
+
production_name: str,
|
|
497
|
+
production_data: dict[str, Any],
|
|
498
|
+
*,
|
|
499
|
+
director: Any,
|
|
500
|
+
namespace: str | None,
|
|
501
|
+
):
|
|
502
|
+
from .model import Production
|
|
503
|
+
|
|
504
|
+
return Production.from_dict(
|
|
505
|
+
production_data,
|
|
506
|
+
namespace=namespace,
|
|
507
|
+
director=director,
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _normalized_production_definition(
|
|
512
|
+
production_name: str,
|
|
513
|
+
production_data: dict[str, Any],
|
|
514
|
+
) -> dict[str, Any]:
|
|
515
|
+
if "Production" in production_data:
|
|
516
|
+
definition = production_data["Production"]
|
|
517
|
+
else:
|
|
518
|
+
definition = production_data.get(production_name)
|
|
519
|
+
if definition is None and production_data:
|
|
520
|
+
definition = production_data[next(iter(production_data))]
|
|
521
|
+
if not isinstance(definition, dict):
|
|
522
|
+
raise ValueError("Production backup must contain a production definition.")
|
|
523
|
+
normalized = dict(definition)
|
|
524
|
+
normalized.setdefault("@Name", production_name)
|
|
525
|
+
return {"Production": normalized}
|
iop/production/common.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
PRODUCTION_SETTING_FIELDS: dict[str, tuple[str, Any]] = {
|
|
7
|
+
"shutdown_timeout": ("ShutdownTimeout", 120),
|
|
8
|
+
"update_timeout": ("UpdateTimeout", 10),
|
|
9
|
+
"alert_notification_manager": ("AlertNotificationManager", ""),
|
|
10
|
+
"alert_notification_operation": ("AlertNotificationOperation", ""),
|
|
11
|
+
"alert_notification_recipients": ("AlertNotificationRecipients", ""),
|
|
12
|
+
"alert_action_window": ("AlertActionWindow", 60),
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
PRODUCTION_SETTING_NAMES: dict[str, str] = {
|
|
16
|
+
field_name: iris_name
|
|
17
|
+
for field_name, (iris_name, _default) in PRODUCTION_SETTING_FIELDS.items()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
PRODUCTION_SETTING_FIELDS_BY_IRIS: dict[str, str] = {
|
|
21
|
+
iris_name: field_name
|
|
22
|
+
for field_name, (iris_name, _default) in PRODUCTION_SETTING_FIELDS.items()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
SETTING_NAME_ALIASES = {
|
|
26
|
+
"target_config_name": "TargetConfigName",
|
|
27
|
+
"target_config_names": "TargetConfigNames",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _bool_text(value: bool | str) -> str:
|
|
32
|
+
if isinstance(value, bool):
|
|
33
|
+
return "true" if value else "false"
|
|
34
|
+
normalized = str(value).lower().strip()
|
|
35
|
+
if normalized in ("1", "yes", "on"):
|
|
36
|
+
return "true"
|
|
37
|
+
if normalized in ("0", "no", "off"):
|
|
38
|
+
return "false"
|
|
39
|
+
return normalized
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _text_value(value: Any) -> str:
|
|
43
|
+
if value is None:
|
|
44
|
+
return ""
|
|
45
|
+
if isinstance(value, bool):
|
|
46
|
+
return _bool_text(value)
|
|
47
|
+
return str(value)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _auto_proxy_class_name(component_class: type) -> str:
|
|
51
|
+
return (
|
|
52
|
+
f"Python.{component_class.__module__}.{component_class.__name__}".replace(
|
|
53
|
+
"_", ""
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _adapter_type_from_component_class(component_class: type | None) -> str:
|
|
59
|
+
if component_class is None:
|
|
60
|
+
return ""
|
|
61
|
+
method = getattr(component_class, "get_adapter_type", None)
|
|
62
|
+
if callable(method):
|
|
63
|
+
value = method()
|
|
64
|
+
if value:
|
|
65
|
+
return str(value)
|
|
66
|
+
return ""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _adapter_type_from_class_name(class_name: str | None) -> str:
|
|
70
|
+
if not class_name or not class_name.startswith("Python."):
|
|
71
|
+
return ""
|
|
72
|
+
python_name = class_name.removeprefix("Python.")
|
|
73
|
+
module_name, separator, class_attr = python_name.rpartition(".")
|
|
74
|
+
if not separator:
|
|
75
|
+
return ""
|
|
76
|
+
try:
|
|
77
|
+
module = importlib.import_module(module_name)
|
|
78
|
+
component_class = getattr(module, class_attr)
|
|
79
|
+
except Exception:
|
|
80
|
+
return ""
|
|
81
|
+
if not isinstance(component_class, type):
|
|
82
|
+
return ""
|
|
83
|
+
return _adapter_type_from_component_class(component_class)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _settings_to_iris(target_name: str, values: dict[str, Any]) -> list[dict[str, str]]:
|
|
87
|
+
return [
|
|
88
|
+
{
|
|
89
|
+
"@Target": target_name,
|
|
90
|
+
"@Name": name,
|
|
91
|
+
"#text": _text_value(value),
|
|
92
|
+
}
|
|
93
|
+
for name, value in values.items()
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _apply_settings_update(target: dict[str, Any], updates: Any) -> None:
|
|
98
|
+
"""Merge *updates* into *target*, treating ``None`` values as removals."""
|
|
99
|
+
for key, value in (updates or {}).items():
|
|
100
|
+
if value is None:
|
|
101
|
+
target.pop(key, None)
|
|
102
|
+
else:
|
|
103
|
+
target[key] = value
|