3535 Flag ,
3636 Out ,
3737 ParseError ,
38+ SideEffect ,
3839 Subprocess ,
3940 Timeout ,
4041)
8990 LinuxUser ,
9091 ReleaseId ,
9192 ResourceId ,
93+ ResourceKind ,
9294 TcpPort ,
9395)
9496from cloudfall .importer import (
207209)
208210# 79 and 80 mean what they mean on cloudfall-engine; 81 and 82 stay the
209211# engine's (artifact build, playbook run) for the commands that wrap it.
212+ PROJECT_MARKERS = tuple (kind .directory for kind in ResourceKind )
213+ """A project holds at least one resource kind directory, such as ``servers``."""
210214GIT_TIMEOUT_SECONDS = 30
211215"""How long one git step of ``init`` may take."""
212216_NOT_WRITABLE = frozenset ({errno .EACCES , errno .EPERM , errno .EROFS })
@@ -1275,23 +1279,20 @@ def __post_init__(self) -> None:
12751279 _inside_project (self .output_dir , "output-dir" )
12761280
12771281
1278- @dataclass (frozen = True , slots = True )
1279- class DashboardBuilt :
1280- """The static dashboard written, and the fleet health it shows."""
1282+ class DashboardPayload (Payload ):
1283+ """``dashboard build``: the pages written, and the fleet health they show."""
12811284
1282- effect : str
1283- status : str
1284- health : str
1285- tasks : int
1286- dashboard : dict [str , str ]
1285+ command = "dashboard build"
12871286
12881287
12891288@dashboard .command (
12901289 "build" ,
12911290 description = "Build a static read-only operations dashboard" ,
1292- danger_level = "mutating " ,
1291+ danger_level = "safe " ,
12931292 timeout = 120 ,
12941293 supports_raw_payload = True ,
1294+ project_root = PROJECT_MARKERS ,
1295+ filesystem_side_effects = [SideEffect ("{project_root}/tmp/dashboard/" , "cache" )],
12951296 exit_codes = ["PROJECT_INVALID" , "CONFIG_INVALID" , "PERMISSION_DENIED" ],
12961297 examples = [
12971298 (
@@ -1302,23 +1303,22 @@ class DashboardBuilt:
13021303)
13031304def dashboard_build (
13041305 args : DashboardBuildArgs , _ctx : Ctx , fleet : Fleet
1305- ) -> DashboardBuilt :
1306+ ) -> DashboardPayload :
13061307 """Write the dashboard pages for the fleet's current evidence."""
13071308 view = args .operations_view (fleet )
1308- output = fleet .path (args .output_dir )
1309- effect = "updated" if output .exists () else "created"
13101309 try :
1311- artifacts = build_dashboard (view , output )
1310+ artifacts = build_dashboard (view , fleet . path ( args . output_dir ) )
13121311 except OSError as error :
13131312 if error .errno not in _NOT_WRITABLE :
13141313 raise
13151314 raise _not_writable (error ) from error
1316- return DashboardBuilt (
1317- effect = effect ,
1318- status = "ok" ,
1319- health = view .health .value ,
1320- tasks = len (view .tasks ),
1321- dashboard = artifacts .as_dict (),
1315+ return DashboardPayload (
1316+ {
1317+ "status" : "ok" ,
1318+ "health" : view .health .value ,
1319+ "tasks" : len (view .tasks ),
1320+ "dashboard" : artifacts .as_dict (),
1321+ }
13221322 )
13231323
13241324
@@ -1336,27 +1336,28 @@ def __post_init__(self) -> None:
13361336 _inside_project (self .output_dir , "output-dir" )
13371337
13381338
1339- @dataclass (frozen = True , slots = True )
1340- class ServicesInspected :
1341- """The domain observation files written."""
1339+ class InspectedPayload (Payload ):
1340+ """``services inspect``: the domain observation files written."""
13421341
1343- effect : str
1344- status : str
1345- observations : list [str ] = Out (ordered = True )
1342+ command = "services inspect"
13461343
13471344
13481345@services .command (
13491346 "inspect" ,
13501347 description = "Collect DNS, TLS, origin, and public route evidence" ,
1351- danger_level = "mutating " ,
1348+ danger_level = "safe " ,
13521349 # DNS, TLS, and HTTP probes of every declared domain, one after another.
13531350 timeout = 600 ,
1351+ project_root = PROJECT_MARKERS ,
1352+ filesystem_side_effects = [
1353+ SideEffect ("{project_root}/tmp/observed-services/" , "cache" )
1354+ ],
13541355 exit_codes = ["PROJECT_INVALID" , "CONFIG_INVALID" , "PERMISSION_DENIED" ],
13551356 examples = [("Probe every declared domain" , "cloudfall services inspect" )],
13561357)
13571358def services_inspect (
13581359 args : ServicesInspectArgs , _ctx : Ctx , fleet : Fleet
1359- ) -> ServicesInspected :
1360+ ) -> InspectedPayload :
13601361 """Probe each declared domain and write one observation per domain."""
13611362 try :
13621363 paths = inspect_domains (
@@ -1369,10 +1370,8 @@ def services_inspect(
13691370 if error .errno not in _NOT_WRITABLE :
13701371 raise
13711372 raise _not_writable (error ) from error
1372- return ServicesInspected (
1373- effect = "created" ,
1374- status = "ok" ,
1375- observations = [str (path ) for path in paths ],
1373+ return InspectedPayload (
1374+ {"status" : "ok" , "observations" : [str (path ) for path in paths ]}
13761375 )
13771376
13781377
@@ -1699,6 +1698,7 @@ class ApproveDecisionArgs(DecisionsArgs):
16991698 )
17001699 yes : bool = Flag (
17011700 default = False ,
1701+ confirm = True ,
17021702 description = (
17031703 "Change the servers; without it the command shows the recorded "
17041704 "proposal and runs nothing"
@@ -1805,10 +1805,9 @@ def operations_approve(args: ApproveDecisionArgs, ctx: Ctx) -> Decided:
18051805 raise Exit .NOT_FOUND (error .detail , context = {"code" : error .code }) from error
18061806 raise _record_invalid (error ) from error
18071807 if not args .yes :
1808- # Nothing ran. treaty reads would_* only from a dry run it switched,
1809- # and --yes is the inverse of its --dry-run (treaty #197).
1808+ # Without --yes the run is treaty's dry run (Flag(confirm=True)).
18101809 return Decided (
1811- effect = "noop " ,
1810+ effect = "would_update " ,
18121811 status = "pending" ,
18131812 decision = decision .as_document (),
18141813 next = [
@@ -2030,6 +2029,7 @@ class MigrateArgs(EngineArgs):
20302029 )
20312030 yes : bool = Flag (
20322031 default = False ,
2032+ confirm = True ,
20332033 description = (
20342034 "Change the servers; without it the command prints the plan and "
20352035 "runs nothing"
@@ -2150,8 +2150,8 @@ def migrate(args: MigrateArgs, ctx: Ctx, fleet: Fleet) -> Migration:
21502150 ) from error
21512151 status = str (result ["status" ])
21522152 migration = Migration (
2153- # A plan changes nothing: noop, as without --yes in `operations approve` .
2154- effect = "noop " if status == "plan" else "updated" ,
2153+ # A plan is the dry run --yes confirms .
2154+ effect = "would_update " if status == "plan" else "updated" ,
21552155 status = status ,
21562156 steps = list (cast ("list[dict[str, object]]" , result ["steps" ])),
21572157 completed = int (cast ("int" , result ["completed" ])),
@@ -2259,7 +2259,7 @@ def why(args: WhyArgs, _ctx: Ctx) -> WhyPayload:
22592259 "Some servers produced no snapshot or Ansible failed; data says which"
22602260 ),
22612261 retryable = True ,
2262- # The hosts are only read ; a retry rewrites the snapshots it wrote.
2262+ # observe only reads the hosts ; a retry rewrites the snapshots it wrote.
22632263 side_effects = "none" ,
22642264 suggestion = "read data.missing and data.detail, fix the hosts, then observe again" ,
22652265)
@@ -2290,7 +2290,6 @@ class ObservedPayload(Payload):
22902290 """``observe``: which servers produced a snapshot."""
22912291
22922292 command = "observe"
2293- writes = True
22942293
22952294
22962295def _playbook_runner (
@@ -2311,7 +2310,14 @@ def run(argv: Sequence[str], environment: Mapping[str, str]) -> PlaybookRun:
23112310 "Collect read-only server snapshots from the fleet; servers without one "
23122311 "exit non-zero with the run in data"
23132312 ),
2314- danger_level = "mutating" ,
2313+ # It only reads the hosts; its snapshots and the inventory overlay are
2314+ # regenerated under the project on every run.
2315+ danger_level = "safe" ,
2316+ project_root = PROJECT_MARKERS ,
2317+ filesystem_side_effects = [
2318+ SideEffect ("{project_root}/tmp/observed/" , "cache" ),
2319+ SideEffect ("{project_root}/tmp/cloudfall/" , "cache" ),
2320+ ],
23152321 exit_codes = [
23162322 "PROJECT_INVALID" ,
23172323 "CONFIG_INVALID" ,
@@ -2334,7 +2340,6 @@ def observe(args: ObserveArgs, ctx: Ctx, fleet: Fleet) -> ObservedPayload:
23342340 else (fleet .directory / "tmp/ansible-inventory.json" ,)
23352341 )
23362342 output = fleet .path (args .output_dir )
2337- effect = "updated" if output .exists () else "created"
23382343 try :
23392344 request = ObservationRequest (
23402345 inventory_sources = sources ,
@@ -2353,7 +2358,7 @@ def observe(args: ObserveArgs, ctx: Ctx, fleet: Fleet) -> ObservedPayload:
23532358 )
23542359 except ObserveError as error :
23552360 raise Exit .PRECONDITION (error .detail , context = {"code" : error .code }) from error
2356- payload = ObservedPayload ({ ** result .as_dict (), "effect" : effect } )
2361+ payload = ObservedPayload (result .as_dict ())
23572362 if not result .complete :
23582363 message = f"{ len (result .missing )} server(s) produced no snapshot"
23592364 raise Exit .INCOMPLETE (message , data = payload )
@@ -2657,6 +2662,7 @@ class ComponentArgs(EngineArgs):
26572662 component : ResourceId = Arg (description = "Component to act on" )
26582663 yes : bool = Flag (
26592664 default = False ,
2665+ confirm = True ,
26602666 description = (
26612667 "Change the servers; without it the command validates the request "
26622668 "and only shows what it would do"
@@ -2718,6 +2724,7 @@ class DataMigrateArgs(EngineArgs):
27182724 )
27192725 yes : bool = Flag (
27202726 default = False ,
2727+ confirm = True ,
27212728 description = (
27222729 "Change the servers; without it the command validates the request "
27232730 "and only shows what it would do"
@@ -2759,9 +2766,12 @@ class DataMigratePayload(Payload):
27592766
27602767
27612768def _plan (preview : LifecyclePreview ) -> dict [str , object ]:
2762- # Nothing ran: noop, as treaty reads would_* only from its own dry run
2763- # (treaty #197).
2764- return {** preview .as_dict (), "instruction" : _PLAN_INSTRUCTION , "effect" : "noop" }
2769+ # Without --yes the run is treaty's dry run (Flag(confirm=True)).
2770+ return {
2771+ ** preview .as_dict (),
2772+ "instruction" : _PLAN_INSTRUCTION ,
2773+ "effect" : "would_update" ,
2774+ }
27652775
27662776
27672777_LIFECYCLE_EXIT_CODES = [
0 commit comments