microvm-ctl 0.3.0__tar.gz → 0.3.1__tar.gz
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.
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/PKG-INFO +1 -1
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/microvm/__init__.py +1 -1
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/microvm/cli.py +5 -5
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/microvm/hooks/server.py +5 -1
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/microvm/integrations/durable.py +1 -1
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/microvm/integrations/stepfunctions.py +26 -4
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/microvm/lease.py +9 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/microvm_ctl.egg-info/PKG-INFO +1 -1
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/pyproject.toml +1 -1
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/tests/test_integrations.py +12 -2
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/tests/test_integrations_durable.py +1 -1
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/tests/test_integrations_scale.py +2 -1
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/tests/test_scale.py +21 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/LICENSE +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/README.md +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/microvm/bootstrap.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/microvm/client.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/microvm/config.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/microvm/data/lambda-microvms/2025-09-09/endpoint-rule-set-1.json.gz +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/microvm/data/lambda-microvms/2025-09-09/paginators-1.json +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/microvm/data/lambda-microvms/2025-09-09/service-2.json.gz +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/microvm/endpoint.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/microvm/fleet.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/microvm/hooks/__init__.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/microvm/images.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/microvm/integrations/__init__.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/microvm/monitor.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/microvm/playground/__init__.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/microvm/playground/server.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/microvm/playground/static/index.html +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/microvm/throttle.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/microvm_ctl.egg-info/SOURCES.txt +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/microvm_ctl.egg-info/dependency_links.txt +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/microvm_ctl.egg-info/entry_points.txt +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/microvm_ctl.egg-info/requires.txt +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/microvm_ctl.egg-info/top_level.txt +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/setup.cfg +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/tests/test_cli_scale.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/tests/test_config.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/tests/test_cost_model.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/tests/test_endpoint.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/tests/test_fleet.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/tests/test_hooks.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/tests/test_hooks_lease.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/tests/test_hooks_validate.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/tests/test_integrations_durable_scale.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/tests/test_lease.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/tests/test_monitor_jobs.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/tests/test_playground.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/tests/test_throttle.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: microvm-ctl
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Control and execution plane for AWS Lambda MicroVMs: build images, launch Firecracker microVMs, scale fleets inside your quotas, call into them securely, and watch it all live.
|
|
5
5
|
Author: Vivek Raja P S
|
|
6
6
|
License: Apache-2.0
|
|
@@ -12,7 +12,7 @@ from microvm.images import ImageBuilder, ImageBuildError
|
|
|
12
12
|
from microvm.lease import FanoutLimit, Lease, LeasePlan, LeasePlanRejected, LeasePolicy, plan_fanout
|
|
13
13
|
from microvm.monitor import CostModel, FleetMonitor
|
|
14
14
|
|
|
15
|
-
__version__ = "0.3.
|
|
15
|
+
__version__ = "0.3.1"
|
|
16
16
|
|
|
17
17
|
__all__ = [
|
|
18
18
|
"microvm_client",
|
|
@@ -531,11 +531,11 @@ def cmd_lease_run(args):
|
|
|
531
531
|
task = json.loads(args.task) if args.task else {}
|
|
532
532
|
except ValueError as e:
|
|
533
533
|
sys.exit(f"mvm lease run: --task is not valid JSON: {e}")
|
|
534
|
+
policy = _lease_policy(args)
|
|
534
535
|
lease = Lease(kind=args.kind, token=args.token or "", region=cfg.region, target=args.target,
|
|
535
|
-
heartbeat_s=args.heartbeat_every, id=args.id)
|
|
536
|
+
heartbeat_s=policy.heartbeat_every(args.heartbeat_every), id=args.id)
|
|
536
537
|
fm = FleetManager(cfg)
|
|
537
|
-
vm = fm.lease(args.image, lease, task,
|
|
538
|
-
execution_role=args.execution_role)
|
|
538
|
+
vm = fm.lease(args.image, lease, task, policy, version=args.version, execution_role=args.execution_role)
|
|
539
539
|
console.print(
|
|
540
540
|
f"[bold green]✓[/] {vm.microvm_id} {_state(vm.state)} "
|
|
541
541
|
f"[link=https://{vm.endpoint}]{vm.endpoint}[/link] lease {lease.kind}"
|
|
@@ -562,14 +562,14 @@ def cmd_lease_run_many(args, cfg):
|
|
|
562
562
|
sys.exit(f"mvm lease run: --task-template is not valid JSON: {e}")
|
|
563
563
|
if not isinstance(template, dict):
|
|
564
564
|
sys.exit("mvm lease run: --task-template must be a JSON object")
|
|
565
|
+
policy = _lease_policy(args)
|
|
565
566
|
leases, tasks = [], []
|
|
566
567
|
for i in range(args.shards):
|
|
567
568
|
token = args.token_template.replace("{i}", str(i)) if args.token_template else ""
|
|
568
569
|
lease_id = args.id.replace("{i}", str(i)) if args.id else None
|
|
569
570
|
leases.append(Lease(kind=kind, token=token, region=cfg.region, target=args.target,
|
|
570
|
-
heartbeat_s=args.heartbeat_every, id=lease_id))
|
|
571
|
+
heartbeat_s=policy.heartbeat_every(args.heartbeat_every), id=lease_id))
|
|
571
572
|
tasks.append(_fill_template(template, i))
|
|
572
|
-
policy = _lease_policy(args)
|
|
573
573
|
fm = FleetManager(cfg)
|
|
574
574
|
baseline = _baseline_mib(args, cfg)
|
|
575
575
|
plan = fm.plan(args.shards, baseline, policy)
|
|
@@ -514,7 +514,11 @@ class HookApp:
|
|
|
514
514
|
self._deliver(ctx, completer, "failure", ctx.completion(error=err))
|
|
515
515
|
|
|
516
516
|
def _heartbeat_loop(self, ctx: LeaseContext, completer, interval: float) -> None:
|
|
517
|
-
|
|
517
|
+
# The first heartbeat goes out at once: the orchestrator's heartbeat clock started before
|
|
518
|
+
# RunMicrovm returned, so waiting a full interval first can lose a tight lease.
|
|
519
|
+
first = True
|
|
520
|
+
while first or not ctx._stop.wait(interval):
|
|
521
|
+
first = False
|
|
518
522
|
try:
|
|
519
523
|
with ctx._lock: # serialized with _deliver: no heartbeat lands after the completion
|
|
520
524
|
if ctx.done:
|
|
@@ -187,7 +187,7 @@ def lease_microvm(
|
|
|
187
187
|
),
|
|
188
188
|
)
|
|
189
189
|
lease = Lease(kind="durable", token=callback.callback_id, region=region,
|
|
190
|
-
heartbeat_s=heartbeat_s, id=execution_name(context))
|
|
190
|
+
heartbeat_s=policy.heartbeat_every(heartbeat_s), id=execution_name(context))
|
|
191
191
|
policy_dict = {"budget_s": policy.budget_s, "heartbeat_timeout_s": policy.heartbeat_timeout_s,
|
|
192
192
|
"slack_s": policy.slack_s}
|
|
193
193
|
launch_once = StepConfig(step_semantics=StepSemantics.AT_MOST_ONCE_PER_RETRY,
|
|
@@ -103,8 +103,10 @@ def _lease_states(
|
|
|
103
103
|
task_expr: str, heartbeat_s: int, lease_id_expr: str, client_token_expr: str,
|
|
104
104
|
done_name: str = "Done",
|
|
105
105
|
) -> dict:
|
|
106
|
-
"""The states of one lease: `name` -> Terminate -> Done
|
|
107
|
-
|
|
106
|
+
"""The states of one lease: `name` -> Terminate -> Done; a typed failure ->
|
|
107
|
+
OnLeaseError -> TerminateFailed (the VM the cause names) -> Reap -> TerminateStale ->
|
|
108
|
+
Failed; a timeout -> OnLeaseError -> Reap -> TerminateStale -> Failed. Used as the whole
|
|
109
|
+
machine and as a Map item processor."""
|
|
108
110
|
max_duration = policy.max_duration()
|
|
109
111
|
stale_ms = max_duration * 1000
|
|
110
112
|
items_expr = (
|
|
@@ -118,7 +120,8 @@ def _lease_states(
|
|
|
118
120
|
"Arguments": {
|
|
119
121
|
"ImageIdentifier": image_arn,
|
|
120
122
|
"ExecutionRoleArn": execution_role_arn,
|
|
121
|
-
"RunHookPayload": run_hook_payload_expr(
|
|
123
|
+
"RunHookPayload": run_hook_payload_expr(
|
|
124
|
+
region, policy.heartbeat_every(heartbeat_s), task_expr, lease_id_expr),
|
|
122
125
|
"IdlePolicy": _idle_policy_args(policy),
|
|
123
126
|
"MaximumDurationInSeconds": max_duration,
|
|
124
127
|
"ClientToken": _jsonata(f"$substring({client_token_expr}, 0, 128)"),
|
|
@@ -129,11 +132,30 @@ def _lease_states(
|
|
|
129
132
|
"Catch": [{
|
|
130
133
|
"ErrorEquals": ["States.Timeout", "States.HeartbeatTimeout", "States.TaskFailed"],
|
|
131
134
|
"Assign": {"lease_error": _jsonata("$states.errorOutput")},
|
|
132
|
-
"Next": "
|
|
135
|
+
"Next": "OnLeaseError",
|
|
133
136
|
}],
|
|
134
137
|
"Assign": {"vm": _jsonata("$states.result.microvm_id")},
|
|
135
138
|
"Next": "Terminate",
|
|
136
139
|
},
|
|
140
|
+
# A typed failure's cause is the VM's own payload and names it: terminate that VM at
|
|
141
|
+
# once. A timeout carries no id; that VM is bounded by MaximumDurationInSeconds
|
|
142
|
+
# (budget + slack) and Reap terminates anything older than that.
|
|
143
|
+
"OnLeaseError": {
|
|
144
|
+
"Type": "Choice",
|
|
145
|
+
"Choices": [{
|
|
146
|
+
"Condition": _jsonata("$contains($string($lease_error.Cause), '\"microvm_id\"')"),
|
|
147
|
+
"Next": "TerminateFailed",
|
|
148
|
+
}],
|
|
149
|
+
"Default": "Reap",
|
|
150
|
+
},
|
|
151
|
+
"TerminateFailed": {
|
|
152
|
+
"Type": "Task",
|
|
153
|
+
"Resource": TERMINATE,
|
|
154
|
+
"Arguments": {"MicrovmIdentifier": _jsonata("$parse($lease_error.Cause).microvm_id")},
|
|
155
|
+
"Retry": _retry_throttling(),
|
|
156
|
+
"Catch": [{"ErrorEquals": ["States.ALL"], "Next": "Reap"}],
|
|
157
|
+
"Next": "Reap",
|
|
158
|
+
},
|
|
137
159
|
"Terminate": {
|
|
138
160
|
"Type": "Task",
|
|
139
161
|
"Resource": TERMINATE,
|
|
@@ -119,6 +119,15 @@ class LeasePolicy:
|
|
|
119
119
|
def to_dict(self) -> dict:
|
|
120
120
|
return asdict(self)
|
|
121
121
|
|
|
122
|
+
def heartbeat_every(self, requested: int | float | None = None) -> int:
|
|
123
|
+
"""The interval the VM should heartbeat at: the requested seconds (default 30), never
|
|
124
|
+
more than a third of `heartbeat_timeout_s` and never under 5 s. A heartbeat interval at
|
|
125
|
+
or above the timeout loses the lease before the first heartbeat lands, because the
|
|
126
|
+
orchestrator's clock starts before RunMicrovm returns."""
|
|
127
|
+
want = 30 if requested is None else float(requested)
|
|
128
|
+
ceiling = max(5.0, self.heartbeat_timeout_s / 3.0)
|
|
129
|
+
return int(max(5.0, min(want, ceiling)))
|
|
130
|
+
|
|
122
131
|
def idle_policy(self):
|
|
123
132
|
"""No auto-resume: a leased VM that goes idle is finished, not dormant."""
|
|
124
133
|
from microvm.fleet import IdlePolicy
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: microvm-ctl
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Control and execution plane for AWS Lambda MicroVMs: build images, launch Firecracker microVMs, scale fleets inside your quotas, call into them securely, and watch it all live.
|
|
5
5
|
Author: Vivek Raja P S
|
|
6
6
|
License: Apache-2.0
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "microvm-ctl"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.1"
|
|
8
8
|
description = "Control and execution plane for AWS Lambda MicroVMs: build images, launch Firecracker microVMs, scale fleets inside your quotas, call into them securely, and watch it all live."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -23,7 +23,8 @@ def test_asl_is_jsonata_and_json_serialisable():
|
|
|
23
23
|
json.dumps(asl) # no sets, no dataclasses
|
|
24
24
|
assert asl["QueryLanguage"] == "JSONata"
|
|
25
25
|
assert asl["StartAt"] == "Lease"
|
|
26
|
-
assert set(asl["States"]) == {"Lease", "Terminate", "
|
|
26
|
+
assert set(asl["States"]) == {"Lease", "Terminate", "OnLeaseError", "TerminateFailed", "Reap",
|
|
27
|
+
"TerminateStale", "Failed", "Done"}
|
|
27
28
|
assert asl["States"]["Done"] == {"Type": "Succeed"}
|
|
28
29
|
assert asl["States"]["Failed"]["Type"] == "Fail"
|
|
29
30
|
assert asl["States"]["Failed"]["Error"] == "LeaseFailed"
|
|
@@ -51,7 +52,16 @@ def test_lease_state_resources_and_arguments():
|
|
|
51
52
|
assert lease["Retry"][0]["BackoffRate"] == 2
|
|
52
53
|
catch = lease["Catch"][0]
|
|
53
54
|
assert catch["ErrorEquals"] == ["States.Timeout", "States.HeartbeatTimeout", "States.TaskFailed"]
|
|
54
|
-
assert catch["Next"] == "
|
|
55
|
+
assert catch["Next"] == "OnLeaseError"
|
|
56
|
+
asl = _asl()
|
|
57
|
+
choice = asl["States"]["OnLeaseError"]
|
|
58
|
+
assert choice["Type"] == "Choice" and choice["Default"] == "Reap"
|
|
59
|
+
assert choice["Choices"][0]["Next"] == "TerminateFailed"
|
|
60
|
+
assert "microvm_id" in choice["Choices"][0]["Condition"]
|
|
61
|
+
tf = asl["States"]["TerminateFailed"]
|
|
62
|
+
assert tf["Resource"] == asl["States"]["Terminate"]["Resource"]
|
|
63
|
+
assert "$parse($lease_error.Cause).microvm_id" in tf["Arguments"]["MicrovmIdentifier"]
|
|
64
|
+
assert tf["Next"] == "Reap" and tf["Catch"][0]["Next"] == "Reap"
|
|
55
65
|
assert catch["Assign"] == {"lease_error": "{% $states.errorOutput %}"}
|
|
56
66
|
|
|
57
67
|
|
|
@@ -91,7 +91,7 @@ def test_success_carries_the_lease_and_terminates_once(fm):
|
|
|
91
91
|
launch = fm.leases[0]
|
|
92
92
|
lease = launch["lease"]
|
|
93
93
|
assert lease.kind == "durable" and lease.token == cb and lease.region == "us-east-1"
|
|
94
|
-
assert lease.heartbeat_s ==
|
|
94
|
+
assert lease.heartbeat_s == 5 and lease.id # clamped: timeout 2 s -> floor of 5 s
|
|
95
95
|
assert launch["image"] == "handoff-agent" and launch["task"] == TASK
|
|
96
96
|
assert launch["policy"].budget_s == 4 and launch["policy"].max_duration() == 124
|
|
97
97
|
assert launch["version"] == "3" and launch["execution_role"] == "arn:aws:iam::1:role/agent"
|
|
@@ -11,7 +11,8 @@ from microvm.lease import LeasePolicy
|
|
|
11
11
|
IMAGE = "arn:aws:lambda:us-east-1:123456789012:microvm-image:handoff-agent"
|
|
12
12
|
ROLE = "arn:aws:iam::123456789012:role/agent"
|
|
13
13
|
TOPIC = "arn:aws:sns:us-east-1:1:t"
|
|
14
|
-
LEASE_STATES = {"Lease", "Terminate", "Reap", "TerminateStale", "Failed",
|
|
14
|
+
LEASE_STATES = {"Lease", "Terminate", "OnLeaseError", "TerminateFailed", "Reap", "TerminateStale", "Failed",
|
|
15
|
+
"Done"}
|
|
15
16
|
|
|
16
17
|
def _indexed(expr: str) -> str:
|
|
17
18
|
return "{% $map(" + expr + ", function($v, $i) { {'index': $i, 'task': $v} }) %}"
|
|
@@ -329,3 +329,24 @@ def test_package_exports():
|
|
|
329
329
|
|
|
330
330
|
assert microvm.LeasePlan is LeasePlan and microvm.LeasePlanRejected is LeasePlanRejected
|
|
331
331
|
assert microvm.FanoutLimit is FanoutLimit and microvm.plan_fanout is plan_fanout
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def test_heartbeat_every_clamps_to_a_third_of_the_timeout():
|
|
335
|
+
from microvm.lease import LeasePolicy
|
|
336
|
+
|
|
337
|
+
assert LeasePolicy().heartbeat_every() == 30 # 120 s timeout: the default 30 s stands
|
|
338
|
+
assert LeasePolicy(heartbeat_timeout_s=30).heartbeat_every() == 10
|
|
339
|
+
assert LeasePolicy(heartbeat_timeout_s=30).heartbeat_every(30) == 10
|
|
340
|
+
assert LeasePolicy(heartbeat_timeout_s=30).heartbeat_every(4) == 5
|
|
341
|
+
assert LeasePolicy(heartbeat_timeout_s=9).heartbeat_every() == 5
|
|
342
|
+
assert LeasePolicy(heartbeat_timeout_s=600).heartbeat_every(45) == 45
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def test_state_machine_and_durable_lease_use_the_clamped_interval():
|
|
346
|
+
from microvm.integrations.stepfunctions import lease_state_machine
|
|
347
|
+
from microvm.lease import LeasePolicy
|
|
348
|
+
|
|
349
|
+
asl = lease_state_machine(image_arn="arn:aws:lambda:us-east-1:1:microvm-image:x",
|
|
350
|
+
execution_role_arn="arn:aws:iam::1:role/r",
|
|
351
|
+
policy=LeasePolicy(heartbeat_timeout_s=30), region="us-east-1", heartbeat_s=30)
|
|
352
|
+
assert "'heartbeat_s': 10" in asl["States"]["Lease"]["Arguments"]["RunHookPayload"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/microvm/data/lambda-microvms/2025-09-09/paginators-1.json
RENAMED
|
File without changes
|
{microvm_ctl-0.3.0 → microvm_ctl-0.3.1}/microvm/data/lambda-microvms/2025-09-09/service-2.json.gz
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|