Setting the file. One moment.
Bootstrap Azure · Foundry Iq · microsoft/azure-skills · Skills Docs
ContentsBack to the top of the page 10.10
File Cu Canary
— line 241
This file
Number 10.39
Position 39 of 77
Type Python
Size 42 KB
Lines 716 helpers/ bootstrap_azure.py
Python · 716 lines · 42 KB
13
try
:
14 from ._progress import Progress, add_progress_argument, reporting
15 from ._common import HelperFailure, blocked_result, digest, emit_result, normalize_azure_location, reject_secrets
16 from ._bootstrap_io import MAX_BYTES , failure, private_directory, private_file, read_json, run_cli
17 except ImportError :
18 from _progress import Progress, add_progress_argument, reporting
19 from _common import HelperFailure, blocked_result, digest, emit_result, normalize_azure_location, reject_secrets
20 from _bootstrap_io import MAX_BYTES , failure, private_directory, private_file, read_json, run_cli
21
22 API = "2025-05-01"
23 PROVIDER_API = "2021-04-01"
24 SCHEMA = "2.0"
25 WAIT_CONDITION = (
26 "(contains(['succeeded','Succeeded','SUCCEEDED'], properties.provisioningState) && "
27 "contains(['running','Running','RUNNING'], properties.status)) || "
28 "contains(['failed','Failed','FAILED','canceled','Canceled','CANCELED','cancelled','Cancelled','CANCELLED',"
29 "'deleting','Deleting','DELETING','deleted','Deleted','DELETED'], properties.provisioningState) || "
30 "contains(['error','Error','ERROR','degraded','Degraded','DEGRADED'], properties.status)"
31 )
32 GUID = re.compile( r " [ 0-9a-f ] {8} - [ 0-9a-f ] {4} - [ 0-9a-f ] {4} - [ 0-9a-f ] {4} - [ 0-9a-f ] {12} " )
33 HASH = re.compile( r "sha256: [ 0-9a-f ] {64} " )
34 TEXT = re.compile( r " [ A-Za-z0-9 ._@:/()- ] {1,256} " )
35 POLICY_ID = re.compile(
36 r " (?: /subscriptions/ [ 0-9a-f- ] {36} (?: /resourceGroups/ [ A-Za-z0-9_.()- ] + "
37 r " (?: /providers/Microsoft . Search/searchServices/ [ a-z0-9- ] + ) ? ) ? "
38 r " | /providers/Microsoft . Management/managementGroups/ [ A-Za-z0-9_.- ] + ) ? "
39 r "/providers/Microsoft . Authorization/ (?: policyAssignments | policyDefinitions | policySetDefinitions | policyExemptions ) / [ A-Za-z0-9_.- ] + " ,
40 re. IGNORECASE ,
41 )
42
43
44 def closed (value, keys, label):
45 if not isinstance (value, dict ) or set (value) != set (keys):
46 raise failure( "bootstrap-input-invalid" , label + " needs exactly the documented fields." )
47
48
49 def text (value, pattern = TEXT ):
50 return isinstance (value, str ) and pattern.fullmatch(value) is not None
51
52
53 def validate (request):
54 closed(request, {
55 "schema_version" , "resource_kind" , "action" , "subscription_id" , "tenant_id" ,
56 "resource_group" , "name" , "location" , "sku" , "replicas" , "partitions" ,
57 "public_network_access" , "tags" , "owner" , "prerequisites" , "limits" , "receipt_dir" ,
58 }, "Bootstrap choices" )
59 if request[ "schema_version" ] != SCHEMA :
60 raise failure( "bootstrap-contract-version" , "Regenerate choices and artifacts using bootstrap schema 2.0 and native wait limits." )
61 reject_secrets(request)
62 if (
63 request[ "resource_kind" ] != "search"
64 or request[ "action" ] not in ( "create" , "reuse" )
65 or not text(request[ "subscription_id" ], GUID ) or not text(request[ "tenant_id" ], GUID )
66 or not text(request[ "resource_group" ], re.compile( r " [ A-Za-z0-9_()- ][ A-Za-z0-9_.()- ] {0,89} " ))
67 or request[ "resource_group" ].endswith( "." )
68 or not text(request[ "name" ], re.compile( r " (?= . {2,60} $ ) [ a-z0-9 ][ a-z0-9 ] + (?: - [ a-z0-9 ] + ) * " ))
69 or normalize_azure_location(request[ "location" ]) is None
70 or request[ "sku" ] not in ( "basic" , "standard" )
71 or type (request[ "replicas" ]) is not int or not 1 <= request[ "replicas" ] <= ( 3 if request[ "sku" ] == "basic" else 12 )
72 or type (request[ "partitions" ]) is not int or request[ "partitions" ] not in (
73 ( 1 ,) if request[ "sku" ] == "basic" else ( 1 , 2 , 3 , 4 , 6 , 12 ))
74 or request[ "public_network_access" ] != "Enabled" or not text(request[ "owner" ])
75 ):
76 raise failure( "bootstrap-input-invalid" , "Select supported, explicit Search identity, keyless public networking and capacity." )
77 tags = request[ "tags" ]
78 if not isinstance (tags, dict ) or len (tags) > 20 or any (
79 not text(k, re.compile( r " [ A-Za-z0-9_.- ] {1,128} " )) or not text(v) for k, v in tags.items()
80 ):
81 raise failure( "bootstrap-input-invalid" , "Tags must be bounded non-secret labels." )
82 if request[ "action" ] == "create" and any (k in tags for k in ( "foundry-iq-owner" , "foundry-iq-operation" )):
83 raise failure( "bootstrap-input-invalid" , "Creation ownership tags are generated, not caller overrides." )
84 closed(request[ "prerequisites" ], { "quota" , "pricing" , "network" , "requirements" , "exclusive_name_authority" }, "Prerequisites" )
85 for key, value in request[ "prerequisites" ].items():
86 if key == "exclusive_name_authority" and request[ "action" ] == "reuse" and value is None :
87 continue
88 field = f "prerequisites. { key } "
89 if value is None or isinstance (value, str ) and not value.strip( " " ):
90 raise failure( "bootstrap-prerequisite-missing" , f " { field } requires nonempty owner-verified evidence." )
91 if not isinstance (value, str ) or len (value) > 256 :
92 raise failure( "bootstrap-prerequisite-invalid" , f " { field } must be a string of 1-256 printable characters." )
93 for position, character in enumerate (value, 1 ):
94 if not character.isprintable():
95 raise failure(
96 "bootstrap-prerequisite-invalid" ,
97 f " { field } contains non-printable U+ { ord (character) :04X} at character { position } ; evidence values are withheld." ,
98 )
99 closed(request[ "limits" ], { "command_seconds" , "wait_seconds" , "wait_interval_seconds" }, "Limits" )
100 for key, ceiling in (( "command_seconds" , 60 ), ( "wait_seconds" , 900 ), ( "wait_interval_seconds" , 30 )):
101 if type (request[ "limits" ][key]) is not int or not 1 <= request[ "limits" ][key] <= ceiling:
102 raise failure( "bootstrap-input-invalid" , "Select explicit bounded command and readiness limits." )
103 private_directory(request[ "receipt_dir" ])
104 return request
105
106
107 def ids (request):
108 group = "/subscriptions/" + request[ "subscription_id" ] + "/resourceGroups/" + request[ "resource_group" ]
109 return group, group + "/providers/Microsoft.Search/searchServices/" + request[ "name" ]
110
111
112 def provider_id (request):
113 return "/subscriptions/" + request[ "subscription_id" ] + "/providers/Microsoft.Search"
114
115
116 def search_locations (value, expected_id):
117 if (
118 not isinstance (value, dict ) or folded(value.get( "id" )) != expected_id.casefold()
119 or folded(value.get( "namespace" )) != "microsoft.search"
120 or not isinstance (value.get( "resourceTypes" ), list )
121 or any ( not isinstance (item, dict ) or not isinstance (item.get( "resourceType" ), str )
122 for item in value[ "resourceTypes" ])
123 ):
124 raise failure( "bootstrap-region-metadata-invalid" , "Search provider metadata is incomplete or belongs to another subscription/provider." )
125 services = [item for item in value[ "resourceTypes" ] if folded(item[ "resourceType" ]) == "searchservices" ]
126 if len (services) != 1 :
127 raise failure( "bootstrap-region-metadata-invalid" , "Exactly one Search searchServices resource type is required." )
128 locations = services[ 0 ].get( "locations" )
129 if (
130 not isinstance (locations, list ) or not 1 <= len (locations) <= 256
131 or any (normalize_azure_location(location) is None for location in locations)
132 ):
133 raise failure( "bootstrap-region-metadata-invalid" , "Search supported locations are missing, malformed or exceed the bounded list." )
134 return sorted ({normalize_azure_location(location) for location in locations})
135
136
137 def desired (request, operation_id):
138 # Only new planning normalizes choices; retained approved bodies keep their original representation.
139 tags = dict (request[ "tags" ])
140 if request[ "action" ] == "create" :
141 tags.update({ "foundry-iq-owner" : request[ "owner" ], "foundry-iq-operation" : operation_id})
142 return {
143 "location" : request[ "location" ], "tags" : tags, "sku" : { "name" : request[ "sku" ]},
144 "identity" : { "type" : "SystemAssigned" },
145 "properties" : { "replicaCount" : request[ "replicas" ], "partitionCount" : request[ "partitions" ],
146 "publicNetworkAccess" : "Enabled" , "disableLocalAuth" : True },
147 }
148
149
150 def _json (raw):
151 if len (raw) > MAX_BYTES :
152 raise failure( "bootstrap-cli-output-limit" , "CLI response exceeds its bound." )
153 try :
154 value = json.loads(raw.decode( "utf-8" ))
155 json.dumps(value, allow_nan = False )
156 if not isinstance (value, dict ) or value.get( "nextLink" ) or value.get( "nextToken" ):
157 raise ValueError ( "Not a complete object" )
158 return value
159 except ( ValueError , UnicodeError , RecursionError ) as exc:
160 raise failure( "bootstrap-response-invalid" , "CLI must return one complete JSON object; empty or truncated output is not evidence." ) from exc
161
162
163 def _error (raw):
164 """Only codes/UUIDs/reference IDs survive; arbitrary Azure messages are not safe receipts."""
165 message = raw.decode( "utf-8" , errors = "replace" )
166 code_match = re.search( r '"code" \s * : \s * " ([ A-Za-z ][ A-Za-z0-9_.- ] {0,100} ) " | (?: ERROR: \s * ) ? \( ([ A-Za-z ][ A-Za-z0-9_.- ] {0,100} ) \) ' , message)
167 code = next ((g for g in code_match.groups() if g), None ) if code_match else "bootstrap-cli-failed"
168 request_match = re.search( r "request [\s - ] ? id [ \" ' \s : ] + ([ 0-9a-f- ] {36} ) " , message, re.I)
169 request_id = request_match.group( 1 ).lower() if request_match and GUID .fullmatch(request_match.group( 1 ).lower()) else None
170 status_match = re.search( r '"status (?: Code ) ? " \s * : \s * ( 4 \d\d | 5 \d\d) ' , message)
171 status = int (status_match.group( 1 )) if status_match else None
172 reason = re.match( r " (?: ERROR: \s * ) ? ([ A-Za-z ] + ) \( " , message.strip())
173 if status is None and reason:
174 status = { "Bad Request" : 400 , "Unauthorized" : 401 , "Forbidden" : 403 , "Not Found" : 404 ,
175 "Conflict" : 409 , "Too Many Requests" : 429 , "Internal Server Error" : 500 ,
176 "Service Unavailable" : 503 , "Gateway Timeout" : 504 }.get(reason.group( 1 ))
177 references = sorted ( set ( POLICY_ID .findall(message)))
178 implicated = code == "RequestDisallowedByPolicy" or bool (references) or '"PolicyViolation"' in message
179 result = failure(code, "Azure CLI failed; untrusted original message text is withheld from output and receipts." )
180 result.request_id, result.http_status = request_id, status
181 result.policy_ids = references[: 8 ]
182 result.policy_implicated = implicated
183 result.message_digest = digest(message)
184 result.policy_overflow = len (references) > 8
185 return result
186
187
188 def projection (resource, required_tags = ()):
189 """Store selected configuration, not arbitrary provider fields/error text."""
190 if resource is None :
191 return None
192 props, identity = resource.get( "properties" , {}), resource.get( "identity" , {})
193 if not isinstance (props, dict ) or not isinstance (identity, dict ):
194 raise failure( "bootstrap-response-invalid" , "Search properties and identity must be objects." )
195 tags = resource.get( "tags" )
196 selected = {k.casefold() for k in required_tags}
197 return {
198 "id" : resource.get( "id" ), "location" : resource.get( "location" ),
199 "tags" : {k: v for k, v in tags.items() if folded(k) in selected} if isinstance (tags, dict ) else tags,
200 "sku" : { "name" : resource[ "sku" ].get( "name" )} if isinstance (resource.get( "sku" ), dict ) else resource.get( "sku" ),
201 "identity" : {k: identity.get(k) for k in ( "type" , "principalId" , "tenantId" )},
202 "properties" : {k: props.get(k) for k in (
203 "replicaCount" , "partitionCount" , "publicNetworkAccess" , "disableLocalAuth" ,
204 "provisioningState" , "status" , "endpoint" ,
205 )},
206 }
207
208
209 def matching (resource, request, body):
210 value = projection(resource, body[ "tags" ])
211 tags = resource.get( "tags" ) if resource is not None else None
212 if tags is None and not body[ "tags" ]:
213 tags = {}
214 group, target = ids(request)
215 location = normalize_azure_location(body[ "location" ])
216 if (
217 value is None or not isinstance (value[ "id" ], str ) or value[ "id" ].casefold() != target.casefold()
218 or location is None or normalize_azure_location(value[ "location" ]) != location
219 or not isinstance (tags, dict )
220 or any ([v for k, v in tags.items() if folded(k) == key.casefold()] != [expected]
221 for key, expected in body[ "tags" ].items())
222 or not isinstance (value[ "sku" ], dict ) or folded(value[ "sku" ].get( "name" )) != body[ "sku" ][ "name" ]
223 or folded(value[ "identity" ][ "type" ]) != "systemassigned"
224 or any ( type (value[ "properties" ][k]) is not type (v) or (
225 folded(value[ "properties" ][k]) != v.casefold() if isinstance (v, str ) else value[ "properties" ][k] != v
226 ) for k, v in body[ "properties" ].items())
227 ):
228 raise failure( "bootstrap-state-conflict" , "Selected Search definition differs; shared or foreign state will not be modified." )
229 props = resource[ "properties" ]
230 if props.get( "authOptions" ) not in ( None , {}) or resource[ "identity" ].get( "userAssignedIdentities" ) not in ( None , {}):
231 raise failure( "bootstrap-state-conflict" , "Observed authentication configuration is not the selected keyless system identity." )
232 rules = props.get( "networkRuleSet" )
233 if rules is not None and ( not isinstance (rules, dict ) or rules.get( "ipRules" ) not in ( None , [])
234 or (rules.get( "bypass" ) is not None and folded(rules[ "bypass" ]) != "none" )):
235 raise failure( "bootstrap-state-conflict" , "Additional network restrictions require the owning procedure." )
236 if props.get( "privateEndpointConnections" ) not in ( None , []) or props.get( "sharedPrivateLinkResources" ) not in ( None , []):
237 raise failure( "bootstrap-state-conflict" , "Private/shared network resources are outside this bootstrap slice." )
238 return value
239
240
241 def folded (value):
242 return value.casefold() if isinstance (value, str ) else None
243
244
245 def reuse_binding (resource, request, body):
246 if resource is None :
247 return None
248 value = ready(resource, request, body)
249 if value is None :
250 raise failure( "bootstrap-not-ready" , "Fresh Search readback does not establish ARM readiness." )
251 return { "id" : folded(value[ "id" ]), "principal" : folded(value[ "identity" ][ "principalId" ]),
252 "tenant" : folded(value[ "identity" ][ "tenantId" ])}
253
254
255 def ready (resource, request, body):
256 return _readiness(resource, request, matching(resource, request, body))
257
258
259 def _readiness (resource, request, value):
260 props = value[ "properties" ]
261 if folded(props[ "provisioningState" ]) in ( "failed" , "canceled" , "cancelled" , "deleting" , "deleted" ) or folded(props[ "status" ]) in ( "error" , "degraded" ):
262 evidence = [resource.get( "error" ), resource[ "properties" ].get( "error" ),
263 resource[ "properties" ].get( "statusDetails" )]
264 raw = " \n " .join(value if isinstance (value, str ) else json.dumps(value)
265 for value in evidence if isinstance (value, ( str , dict , list )))
266 error = _error(raw.encode( "utf-8" ))
267 if error.code == "bootstrap-cli-failed" :
268 error.code = "bootstrap-provisioning-failed"
269 error.message = "Search entered a failed, deleting or degraded ARM state; provider message text is withheld."
270 error.args = (error.message,)
271 error.creation_failure = True
272 raise error
273 if (
274 folded(props[ "provisioningState" ]) != "succeeded" or folded(props[ "status" ]) != "running"
275 or not text(folded(value[ "identity" ][ "principalId" ]), GUID )
276 or folded(value[ "identity" ][ "tenantId" ]) != request[ "tenant_id" ]
277 or folded(props[ "endpoint" ]) not in (
278 "https://" + request[ "name" ] + ".search.windows.net" ,
279 "https://" + request[ "name" ] + ".search.windows.net/" ,
280 )
281 ):
282 return None
283 return value
284
285
286 class Session :
287 def __init__ (self, request, operation_id, cli = run_cli, clock = time.monotonic, * , target = None ):
288 self .request, self .operation_id = request, operation_id
289 self .cli, self .clock = cli, clock
290 self .directory = private_directory(request[ "receipt_dir" ])
291 self .sequence = 0
292 self .attempted = False
293 self .approved = False
294 self .owned = False
295 self .warnings = []
296 self .diagnosed = False
297 self .policy_references = []
298 self .target = target if target is not None else ids(request)[ 1 ]
299
300 def record (self, event, value):
301 self .sequence += 1
302 private_file( self .directory, f " { self .operation_id } . { self .sequence :03d} . { uuid.uuid4().hex } .receipt.json" , {
303 "schema_version" : "1.0" , "operation_id" : self .operation_id, "operation" : "bootstrap-search" ,
304 "target" : self .target, "event" : event, "evidence" : value,
305 })
306
307 def call (self, args, timeout = None , mutation = False , waiting = False ):
308 self .record( "command" , { "argv" : args})
309 if mutation:
310 self .attempted = True
311 try :
312 response = self .cli(
313 args + [ "--subscription" , self .request[ "subscription_id" ]],
314 self .request[ "limits" ][ "wait_seconds" ] if waiting else (
315 min ( self .request[ "limits" ][ "command_seconds" ], timeout) if timeout is not None else self .request[ "limits" ][ "command_seconds" ]),
316 )
317 except HelperFailure as error:
318 if mutation and error.code in ( "bootstrap-tool-unavailable" , "bootstrap-cli-start-failed" ):
319 self .attempted = False
320 raise
321 rc, stdout, stderr = response
322 if len (stdout) > MAX_BYTES or len (stderr) > MAX_BYTES :
323 raise failure( "bootstrap-cli-output-limit" , "CLI output exceeded its bound." )
324 if rc:
325 error = _error(stderr or stdout)
326 try :
327 self .record( "failure" , { "code" : error.code, "status" : error.http_status, "request_id" : error.request_id,
328 "message_digest" : error.message_digest, "message_withheld" : True ,
329 "policy_ids" : error.policy_ids, "policy_references_limited" : error.policy_overflow,
330 "warnings" : error.warnings})
331 except HelperFailure:
332 self .warnings.append( "Original CLI failure could not be persisted; retain the returned failure and ownership." )
333 raise error
334 if waiting:
335 if stdout.strip() not in ( b "" , b "null" ):
336 raise failure( "bootstrap-wait-result-invalid" , "Native wait returned an unexpected result; readiness is unresolved." )
337 return None
338 return _json(stdout)
339
340 def get (self, timeout = None ):
341 try :
342 result = self .call([ "rest" , "--method" , "get" , "--url" ,
343 "https://management.azure.com" + self .target + "?api-version=" + API ], timeout)
344 except HelperFailure as exc:
345 if (exc.code == "ResourceNotFound" and exc.http_status in ( None , 404 )
346 and not getattr (exc, "cleanup_unconfirmed" , False )):
347 self .record( "readback" , { "id" : self .target, "absence" : "ResourceNotFound" })
348 return None
349 raise
350 if not isinstance (result.get( "id" ), str ) or result[ "id" ].casefold() != self .target.casefold():
351 raise failure( "bootstrap-response-invalid" , "ARM returned another identity." )
352 self .record( "readback" , { "id" : self .target, "digest" : digest(result)})
353 return result
354
355 def account_context (self):
356 account = self .call([ "account" , "show" ])
357 if (folded(account.get( "id" )) != self .request[ "subscription_id" ] or folded(account.get( "tenantId" )) != self .request[ "tenant_id" ]
358 or folded(account.get( "state" )) != "enabled" or account.get( "environmentName" ) != "AzureCloud" ):
359 raise failure( "bootstrap-context-conflict" , "Signed-in subscription/tenant is not the selected enabled context." )
360 user = account.get( "user" )
361 if not isinstance (user, dict ) or not text(user.get( "name" )) or folded(user.get( "type" )) not in ( "user" , "serviceprincipal" ):
362 raise failure( "bootstrap-context-conflict" , "Signed-in principal evidence is missing or unsupported." )
363 principal = folded(user[ "name" ]) if text(folded(user[ "name" ]), GUID ) else user[ "name" ]
364 return { "id" : folded(account[ "id" ]), "tenantId" : folded(account[ "tenantId" ]), "environmentName" : account[ "environmentName" ],
365 "user" : { "name" : principal, "type" : folded(user[ "type" ])}}
366
367 def context (self):
368 account = self .account_context()
369 group = self .call([ "group" , "show" , "--name" , self .request[ "resource_group" ]])
370 props = group.get( "properties" )
371 if (
372 not isinstance (group.get( "id" ), str ) or group[ "id" ].casefold() != ids( self .request)[ 0 ].casefold()
373 or not isinstance (props, dict ) or folded(props.get( "provisioningState" )) != "succeeded"
374 ):
375 raise failure( "bootstrap-group-not-ready" , "The explicitly selected existing resource group must be ready." )
376 context = { "account" : digest(account), "group" : digest({ "id" : folded(group[ "id" ]), "ready" : True })}
377 self .record( "context" , { "account" : account, "group_id" : ids( self .request)[ 0 ], "group_digest" : context[ "group" ]})
378 return context
379
380 def regions (self, * , selected = None ):
381 identity = provider_id( self .request)
382 value = self .call([ "rest" , "--method" , "get" , "--url" ,
383 "https://management.azure.com" + identity + "?api-version=" + PROVIDER_API ])
384 locations = search_locations(value, identity)
385 self .record( "search-regions" , { "provider_id" : identity, "resource_type" : "searchServices" ,
386 "api_version" : PROVIDER_API , "digest" : digest(value), "locations" : locations})
387 if selected is not None and normalize_azure_location(selected) not in locations:
388 error = failure( "bootstrap-region-unsupported" , "Select an advertised Search region; no typo correction or deployment-capacity inference." )
389 error.available_locations = locations
390 raise error
391 return locations
392
393 def policy_evidence (self, original):
394 if self .diagnosed or not getattr (original, "policy_implicated" , False ):
395 return
396 self .diagnosed = True
397 end = self .clock() + 60
398 queue = list (original.policy_ids)
399 seen = set ()
400 self .warnings.append( "Policy evidence is diagnostic only; cause/compliance remains unresolved for the policy owner." )
401 scopes = { self .target.casefold(), ids( self .request)[ 0 ].casefold(), ( "/subscriptions/" + self .request[ "subscription_id" ]).casefold()}
402 while queue and len (seen) < 8 :
403 identity = queue.pop( 0 )
404 if identity.casefold() in seen:
405 continue
406 seen.add(identity.casefold())
407 self .policy_references.append(identity)
408 remaining = end - self .clock()
409 if remaining <= 0 :
410 break
411 scope, tail = re.split( r "/providers/Microsoft . Authorization/" , identity, flags = re.I)
412 if scope.casefold() not in scopes or "/" not in tail:
413 self .warnings.append( "Referenced policy scope is unsupported; hand off to the policy owner." )
414 continue
415 kind, name = tail.split( "/" , 1 )
416 if kind.casefold() == "policyassignments" :
417 args = [ "policy" , "assignment" , "show" , "--name" , name, "--scope" , scope]
418 elif kind.casefold() == "policydefinitions" and scope.casefold() == ( "/subscriptions/" + self .request[ "subscription_id" ]).casefold():
419 args = [ "policy" , "definition" , "show" , "--name" , name]
420 else :
421 self .warnings.append( "Referenced policy kind requires owner investigation." )
422 continue
423 try :
424 value = self .call(args, remaining)
425 if self .clock() >= end:
426 raise failure( "bootstrap-policy-timeout" , "Policy evidence arrived after the diagnostic budget." )
427 if not isinstance (value.get( "id" ), str ) or value[ "id" ].casefold() != identity.casefold():
428 raise failure( "bootstrap-policy-evidence-mismatch" , "Policy read returned another identity." )
429 # Values/rules can contain sensitive literals: retain identity, digest and
430 # classification only; the policy owner reads the referenced object.
431 properties = value.get( "properties" , value)
432 if not isinstance (properties, dict ):
433 raise failure( "bootstrap-response-invalid" , "Policy properties are malformed." )
434 reference = properties.get( "policyDefinitionId" )
435 if isinstance (reference, str ) and POLICY_ID .fullmatch(reference):
436 queue.append(reference)
437 self .record( "policy-evidence" , { "id" : identity, "digest" : digest(value),
438 "referenced_definition_id" : reference if isinstance (reference, str ) and POLICY_ID .fullmatch(reference) else None ,
439 "enforcementMode" : properties.get( "enforcementMode" ) if properties.get( "enforcementMode" ) in ( "Default" , "DoNotEnforce" , "Enroll" ) else None ,
440 "interpretation" : "unresolved" })
441 except HelperFailure as secondary:
442 self .warnings.append( "Secondary policy diagnostic failed: " + secondary.code)
443 self .warnings.extend(secondary.warnings)
444 if queue or getattr (original, "policy_overflow" , False ) or not original.policy_ids:
445 self .warnings.append( "Policy references are missing or exceed this bounded collector; no truncated completeness claim." )
446
447 def blocked (self, error):
448 if self .attempted:
449 error.partial = True
450 error.writes = [{ "operation" : "PUT" , "resource_id" : self .target, "submission" : "attempted-unverified" }]
451 error.resources_remaining = [{ "resource_id" : self .target, "run_owned" : self .owned,
452 "cleanup" : "separate consent and fresh ownership/children/roles required" }]
453 error.warnings.extend( self .warnings)
454 result = blocked_result(error, outcome = "bootstrap-search" , fingerprint = None , owner = self .request.get( "owner" ))
455 if error.code == "bootstrap-region-unsupported" and hasattr (error, "available_locations" ):
456 result[ "available_locations" ] = error.available_locations
457 if result[ "status" ] == "partial" :
458 result[ "approved_plan" ] = { "confirmed" : self .approved, "artifact_id" : self .operation_id}
459 result[ "attempted_writes" ] = result.pop( "completed_writes" )
460 result[ "completed_writes" ] = []
461 if not self .owned:
462 result[ "resources_remaining" ][ "unverified" ] = result[ "resources_remaining" ][ "run_owned" ]
463 result[ "resources_remaining" ][ "run_owned" ] = []
464 result[ "receipt_id" ] = self .operation_id
465 if self .diagnosed:
466 result[ "policy_handoff" ] = { "referenced_ids" : self .policy_references, "complete" : False ,
467 "next_step" : "Policy owner investigates exact references; no correction or retry is approved." }
468 return result
469
470
471 def discover_regions (request, * , cli = run_cli):
472 closed(request, { "schema_version" , "subscription_id" , "tenant_id" , "receipt_dir" , "limits" }, "Region discovery" )
473 reject_secrets(request)
474 closed(request[ "limits" ], { "command_seconds" }, "Region discovery limits" )
475 if (
476 request[ "schema_version" ] != SCHEMA
477 or not text(request[ "subscription_id" ], GUID ) or not text(request[ "tenant_id" ], GUID )
478 or type (request[ "limits" ][ "command_seconds" ]) is not int
479 or not 1 <= request[ "limits" ][ "command_seconds" ] <= 60
480 ):
481 raise failure( "bootstrap-input-invalid" , "Select subscription/tenant and a 1-60 second region-discovery command limit." )
482 session = Session(request, str (uuid.uuid4()), cli, target = provider_id(request))
483 try :
484 session.record( "context" , { "account" : session.account_context()})
485 locations = session.regions()
486 return { "status" : "discovered" , "resource_type" : "Microsoft.Search/searchServices" ,
487 "available_locations" : locations, "receipt_id" : session.operation_id,
488 "mutation_approval_required" : False , "execution_required" : False , "writes_performed" : [],
489 "verification" : "Advertised region support only; not SKU, quota, capacity, models or residency approval." }
490 except HelperFailure as error:
491 return session.blocked(error)
492
493
494 def plan_request (request, * , cli = run_cli, clock = time.monotonic, execution_output = None ):
495 validate(request)
496 request = copy.deepcopy(request)
497 request[ "location" ] = normalize_azure_location(request[ "location" ])
498 operation_id = str (uuid.uuid4())
499 session = Session(request, operation_id, cli, clock)
500 try :
501 context = session.context()
502 observed = session.get()
503 body = desired(request, operation_id)
504 if request[ "action" ] == "create" :
505 if observed is not None :
506 raise failure( "bootstrap-state-conflict" , "New intent requires exact-name absence; never overwrite or silently reuse." )
507 session.regions( selected = request[ "location" ])
508 refreshed = session.get()
509 if refreshed is not None :
510 raise failure( "bootstrap-state-drift" , "Exact-name absence changed; creation is blocked." )
511 else :
512 binding = reuse_binding(observed, request, body)
513 if binding is None :
514 raise failure( "bootstrap-not-ready" , "Reuse requires a running, keyless Search service with identity readback." )
515 refreshed = session.get()
516 if reuse_binding(refreshed, request, body) != binding:
517 raise failure( "bootstrap-state-drift" , "Selected Search identity changed; obtain fresh evidence." )
518 artifact = {
519 "operation" : "bootstrap-search" , "operation_id" : operation_id, "created_at" : int (time.time()),
520 "choices" : copy.deepcopy(request),
521 "body" : body, "before" : { ** context, "search" : digest(observed)},
522 }
523 envelope = { "schema_version" : SCHEMA , "plan" : artifact,
524 "approval" : { "confirmed" : False , "fingerprint" : digest(artifact)}}
525 if execution_output is None :
526 private_file(session.directory, operation_id + ".plan.json" , envelope)
527 else :
528 try :
529 from .private_artifacts import retain_execution_input
530 except ImportError :
531 from private_artifacts import retain_execution_input
532 execution_artifact = retain_execution_input(envelope, execution_output)
533 session.record( "planned" , { "body" : body, "before" : artifact[ "before" ],
534 "readback" : projection(refreshed, body[ "tags" ]), "run_owned" : False })
535 create = request[ "action" ] == "create"
536 if execution_output is not None :
537 return {
538 "status" : "planned" if create else "reused" , "artifact_id" : operation_id,
539 "execution_artifact" : execution_artifact, "execution_required" : create,
540 "mutation_approval_required" : create, "azure_mutation_performed" : False ,
541 "local_filesystem" : { "artifact_created" : True , "receipts_created" : True ,
542 "existing_files_changed" : False },
543 "summary" : "Unapproved bootstrap artifact retained privately; review before separate approval." ,
544 }
545 return {
546 "status" : "planned" if create else "reused" , "artifact_id" : operation_id,
547 "execution_required" : create, "mutation_approval_required" : create,
548 "approval_summary" : { "action" : request[ "action" ], "resource_id" : session.target,
549 "location" : request[ "location" ], "sku" : request[ "sku" ],
550 "replicas" : request[ "replicas" ], "partitions" : request[ "partitions" ],
551 "network" : "public; local authentication disabled; SystemAssigned" ,
552 "tags" : body[ "tags" ], "prerequisites" : {
553 key: { "evidence_present" : value is not None ,
554 "verification" : "caller-attested; not helper-verified" if value is not None else "not required for reuse" }
555 for key, value in request[ "prerequisites" ].items()
556 },
557 "limits" : request[ "limits" ], "cleanup" : "separate approval" },
558 "verification" : { "arm_readiness" : "verified" if not create else "unverified" ,
559 "data_plane_access" : "unverified" , "ingestion" : "unverified" , "retrieval" : "unverified" },
560 "writes_performed" : [], "run_owned" : False ,
561 "observed_dependency" : projection(refreshed, body[ "tags" ]),
562 }
563 except HelperFailure as error:
564 return session.blocked(error)
565
566
567 def validate_artifact (envelope):
568 closed(envelope, { "schema_version" , "plan" , "approval" }, "Execution artifact" )
569 if envelope[ "schema_version" ] != SCHEMA :
570 raise failure( "bootstrap-contract-version" , "Regenerate the retained artifact with bootstrap schema 2.0; do not edit its checksum." )
571 plan, approval = envelope[ "plan" ], envelope[ "approval" ]
572 closed(plan, { "operation" , "operation_id" , "created_at" , "choices" , "body" , "before" }, "Execution plan" )
573 closed(approval, { "confirmed" , "fingerprint" }, "Approval" )
574 if (
575 plan[ "operation" ] != "bootstrap-search"
576 or not text(plan[ "operation_id" ], GUID ) or type (approval[ "confirmed" ]) is not bool
577 or approval[ "fingerprint" ] != digest(plan)
578 ):
579 raise failure( "approval-mismatch" , "Retain the unchanged planner artifact; integrity is checked internally." )
580 validate(plan[ "choices" ])
581 if type (plan[ "created_at" ]) is not int or not plan[ "created_at" ] <= time.time() <= plan[ "created_at" ] + 900 :
582 raise failure( "bootstrap-artifact-expired" , "Rerun planning; retained artifacts expire after fifteen minutes." )
583 closed(plan[ "before" ], { "account" , "group" , "search" }, "Before-state" )
584 if any ( not text(v, HASH ) for v in plan[ "before" ].values()) or plan[ "body" ] != desired(plan[ "choices" ], plan[ "operation_id" ]):
585 raise failure( "bootstrap-artifact-invalid" , "Artifact body or before-state differs from generated choices." )
586 return plan
587
588
589 @reporting ( "search-bootstrap" )
590 def apply_artifact (envelope, * , approve = False , cli = run_cli, clock = time.monotonic, progress = None ):
591 progress.update( "validation" )
592 plan = validate_artifact(envelope)
593 if approve is not True :
594 raise failure( "approval-missing" , "Explicit approval of the unchanged artifact is required before any execution reads or writes." )
595 request = plan[ "choices" ]
596 if request[ "action" ] != "create" :
597 raise failure( "bootstrap-execution-unnecessary" , "Read-only reuse needs fresh planning, not mutation approval or execution." )
598 session = Session(request, plan[ "operation_id" ], cli, clock)
599 session.approved = True
600 body = plan[ "body" ]
601 try :
602 session.record( "approved" , { "plan" : plan, "approval" : { "confirmed" : True , "fingerprint" : digest(plan)}})
603 progress.update( "context-check" )
604 context = session.context()
605 if context != {k: plan[ "before" ][k] for k in ( "account" , "group" )}:
606 raise failure( "bootstrap-state-drift" , "Account/group changed; rerun planning and discard old consent." )
607 progress.update( "region-check" )
608 session.regions( selected = request[ "location" ])
609 progress.update( "absence-check" )
610 if plan[ "before" ][ "search" ] != digest( None ) or session.get() is not None :
611 raise failure( "bootstrap-state-drift" , "Exact-name absence changed; never overwrite or retry through another name." )
612 body_path = private_file(session.directory, plan[ "operation_id" ] + "." + uuid.uuid4().hex + ".body.json" , body)
613 command = [ "rest" , "--method" , "put" , "--url" ,
614 "https://management.azure.com" + session.target + "?api-version=" + API ,
615 "--headers" , "Content-Type=application/json" , "x-ms-client-request-id=" + str (uuid.uuid4()),
616 "--body" , "@" + str (body_path)]
617 private_file(session.directory, plan[ "operation_id" ] + ".submission.json" ,
618 { "operation_id" : plan[ "operation_id" ], "target" : session.target, "body" : body, "command" : command})
619 progress.update( "search-submit" )
620 try :
621 session.call(command, mutation = True )
622 except HelperFailure as original:
623 if not session.attempted:
624 raise
625 # Never retry PUT. Neither diagnostics nor a readback replaces the first error.
626 progress.update( "arm-readback" )
627 try :
628 observed = session.get()
629 if observed is not None :
630 matching(observed, request, body)
631 session.owned = True
632 session.record( "failure-reconciliation" , { "readback" : projection(observed, body[ "tags" ]), "run_owned" : session.owned})
633 if (original.code in ( "RequestDisallowedByPolicy" , "AuthorizationFailed" , "InvalidSkuName" )
634 and observed is None and not getattr (original, "cleanup_unconfirmed" , False )):
635 session.attempted = False
636 except HelperFailure as secondary:
637 session.warnings.append( "Read-only reconciliation failed: " + secondary.code)
638 session.warnings.extend(secondary.warnings)
639 session.policy_evidence(original)
640 raise original
641 progress.update( "arm-wait" )
642 try :
643 session.call([ "resource" , "wait" , "--ids" , session.target, "--api-version" , API ,
644 "--custom" , WAIT_CONDITION , "--interval" , str (request[ "limits" ][ "wait_interval_seconds" ]),
645 "--timeout" , str (request[ "limits" ][ "wait_seconds" ])], waiting = True )
646 except HelperFailure as original:
647 try :
648 progress.update( "arm-readback" )
649 observed = session.get()
650 if observed is not None :
651 value = matching(observed, request, body)
652 session.owned = True
653 try :
654 _readiness(observed, request, value)
655 except HelperFailure as provider:
656 session.warnings.append( "Terminal provider readback: " + provider.code)
657 session.record( "terminal-provider-failure" , {
658 "code" : provider.code, "status" : provider.http_status, "request_id" : provider.request_id,
659 "message_digest" : provider.message_digest, "message_withheld" : True })
660 session.policy_evidence(provider)
661 session.record( "wait-failure-readback" , { "readback" : projection(observed, body[ "tags" ]), "run_owned" : session.owned})
662 except HelperFailure as secondary:
663 session.warnings.append( "Wait failure readback failed: " + secondary.code)
664 session.warnings.extend(secondary.warnings)
665 raise original
666 progress.update( "arm-readback" )
667 observed = session.get()
668 value = matching(observed, request, body) if observed is not None else None
669 if value is not None :
670 session.owned = True
671 value = _readiness(observed, request, value)
672 if value is None :
673 raise failure( "bootstrap-readiness-timeout" , "Final ARM readiness was not established; preserve the resource and operation receipts." )
674 session.record( "arm-ready" , { "readback" : value, "run_owned" : True })
675 return { "status" : "completed" , "receipt_id" : session.operation_id, "resource_id" : session.target,
676 "run_owned" : True , "writes_performed" : [{ "operation" : "PUT" , "resource_id" : session.target}],
677 "observed_dependency" : value,
678 "verification" : { "arm_readiness" : "verified" , "data_plane_access" : "unverified" ,
679 "ingestion" : "unverified" , "retrieval" : "unverified" },
680 "cleanup" : "separate approval with fresh ownership/children/roles" }
681 except HelperFailure as error:
682 if session.attempted and getattr (error, "creation_failure" , False ):
683 session.policy_evidence(error)
684 return session.blocked(error)
685
686
687 def main (argv = None ):
688 try :
689 from .private_artifacts import add_execution_output_argument, validate_execution_output_mode
690 except ImportError :
691 from private_artifacts import add_execution_output_argument, validate_execution_output_mode
692 parser = argparse.ArgumentParser( description = "Discover Search regions or plan/apply one selected service; no models, roles or cleanup." )
693 mode = parser.add_mutually_exclusive_group( required = True )
694 mode.add_argument( "--plan" , type = Path)
695 mode.add_argument( "--apply" , type = Path)
696 mode.add_argument( "--regions" , type = Path)
697 parser.add_argument( "--approve" , action = "store_true" )
698 add_execution_output_argument(parser)
699 add_progress_argument(parser)
700 args = parser.parse_args(argv)
701 try :
702 validate_execution_output_mode(args)
703 if (args.plan or args.regions) and args.approve:
704 raise failure( "bootstrap-input-invalid" , "Planning cannot approve mutations." )
705 document = read_json(args.plan or args.apply or args.regions)
706 result = (discover_regions(document) if args.regions else
707 plan_request(document, ** ({ "execution_output" : args.execution_output} if args.execution_output else {})) if args.plan else apply_artifact(
708 document, approve = args.approve, progress = Progress( "search-bootstrap" , enabled = args.progress)))
709 except HelperFailure as error:
710 result = blocked_result(error, outcome = "bootstrap-search" , fingerprint = None )
711 emit_result(result)
712 return 3 if result[ "status" ] == "partial" else 2 if result[ "status" ] == "blocked" else 0
713
714
715 if __name__ == "__main__" :
716 sys.exit(main())