microvm-ctl 0.1.0__tar.gz → 0.2.0__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.1.0 → microvm_ctl-0.2.0}/PKG-INFO +45 -8
- {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/README.md +40 -7
- {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm/__init__.py +4 -1
- {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm/bootstrap.py +6 -1
- {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm/cli.py +190 -2
- {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm/endpoint.py +42 -0
- {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm/fleet.py +54 -2
- microvm_ctl-0.2.0/microvm/hooks/server.py +669 -0
- {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm/images.py +33 -5
- microvm_ctl-0.2.0/microvm/integrations/__init__.py +16 -0
- microvm_ctl-0.2.0/microvm/integrations/durable.py +181 -0
- microvm_ctl-0.2.0/microvm/integrations/stepfunctions.py +210 -0
- microvm_ctl-0.2.0/microvm/lease.py +133 -0
- {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm/monitor.py +22 -15
- microvm_ctl-0.2.0/microvm/playground/__init__.py +5 -0
- microvm_ctl-0.2.0/microvm/playground/server.py +1849 -0
- microvm_ctl-0.2.0/microvm/playground/static/index.html +862 -0
- {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm_ctl.egg-info/PKG-INFO +45 -8
- {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm_ctl.egg-info/SOURCES.txt +12 -0
- {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm_ctl.egg-info/requires.txt +3 -0
- {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/pyproject.toml +5 -2
- {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/tests/test_endpoint.py +77 -1
- {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/tests/test_fleet.py +17 -0
- microvm_ctl-0.2.0/tests/test_hooks_lease.py +532 -0
- microvm_ctl-0.2.0/tests/test_integrations.py +330 -0
- microvm_ctl-0.2.0/tests/test_integrations_durable.py +170 -0
- microvm_ctl-0.2.0/tests/test_lease.py +131 -0
- microvm_ctl-0.2.0/tests/test_playground.py +228 -0
- microvm_ctl-0.1.0/microvm/hooks/server.py +0 -168
- {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/LICENSE +0 -0
- {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm/client.py +0 -0
- {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm/config.py +0 -0
- {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm/data/lambda-microvms/2025-09-09/endpoint-rule-set-1.json.gz +0 -0
- {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm/data/lambda-microvms/2025-09-09/paginators-1.json +0 -0
- {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm/data/lambda-microvms/2025-09-09/service-2.json.gz +0 -0
- {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm/hooks/__init__.py +0 -0
- {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm/throttle.py +0 -0
- {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm_ctl.egg-info/dependency_links.txt +0 -0
- {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm_ctl.egg-info/entry_points.txt +0 -0
- {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm_ctl.egg-info/top_level.txt +0 -0
- {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/setup.cfg +0 -0
- {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/tests/test_config.py +0 -0
- {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/tests/test_cost_model.py +0 -0
- {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/tests/test_hooks.py +0 -0
- {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/tests/test_hooks_validate.py +0 -0
- {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/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
|
+
Version: 0.2.0
|
|
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
|
|
@@ -9,6 +9,7 @@ Project-URL: Repository, https://github.com/Vivek0712/microvm-ctl
|
|
|
9
9
|
Project-URL: Documentation, https://github.com/Vivek0712/microvm-ctl/tree/main/docs
|
|
10
10
|
Project-URL: Changelog, https://github.com/Vivek0712/microvm-ctl/blob/main/CHANGELOG.md
|
|
11
11
|
Project-URL: Examples, https://github.com/Vivek0712/awesome-microvm
|
|
12
|
+
Project-URL: Articles, https://builder.aws.com/content/3JIDTpz0ZgatSBv24drra3gEod9/control-and-scale-aws-lambda-microvms-with-microvm-ctl
|
|
12
13
|
Keywords: aws,lambda,microvm,firecracker,sandbox,serverless,fleet,orchestrator
|
|
13
14
|
Classifier: Development Status :: 4 - Beta
|
|
14
15
|
Classifier: Intended Audience :: Developers
|
|
@@ -17,6 +18,7 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
17
18
|
Classifier: Programming Language :: Python :: 3.10
|
|
18
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
19
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
22
|
Classifier: Topic :: System :: Distributed Computing
|
|
21
23
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
24
|
Requires-Python: >=3.9
|
|
@@ -28,19 +30,21 @@ Requires-Dist: rich>=13.7
|
|
|
28
30
|
Provides-Extra: dev
|
|
29
31
|
Requires-Dist: pytest>=8; extra == "dev"
|
|
30
32
|
Requires-Dist: ruff>=0.5; extra == "dev"
|
|
33
|
+
Provides-Extra: durable
|
|
34
|
+
Requires-Dist: aws-durable-execution-sdk-python>=2.0.0; extra == "durable"
|
|
31
35
|
Dynamic: license-file
|
|
32
36
|
|
|
33
37
|
# microvm-ctl
|
|
34
38
|
|
|
35
|
-
**The control and execution plane for [AWS Lambda MicroVMs](https://docs.aws.amazon.com/lambda/latest/dg/lambda-microvms-guide.html).** Build a snapshot image from a Dockerfile, launch Firecracker microVMs in seconds, scale a fleet inside the quotas your account actually has, call into every VM over its authenticated endpoint,
|
|
39
|
+
**The control and execution plane for [AWS Lambda MicroVMs](https://docs.aws.amazon.com/lambda/latest/dg/lambda-microvms-guide.html).** Build a snapshot image from a Dockerfile, launch Firecracker microVMs in seconds, scale a fleet inside the quotas your account actually has, call into every VM over its authenticated endpoint, watch the whole thing live, and hand a VM a task from Step Functions, Lambda durable functions, or any orchestrator through a lease the VM completes itself. One Python SDK, one `mvm` command.
|
|
36
40
|
|
|
37
41
|
[](https://github.com/Vivek0712/microvm-ctl/actions/workflows/ci.yml)
|
|
38
42
|
[](https://pypi.org/project/microvm-ctl/)
|
|
39
|
-

|
|
40
44
|

|
|
41
45
|
|
|
42
46
|
```console
|
|
43
|
-
pip install microvm-ctl
|
|
47
|
+
pip install microvm-ctl # https://pypi.org/project/microvm-ctl/
|
|
44
48
|
|
|
45
49
|
mvm bootstrap # one time: S3 artifact bucket + build/execution IAM roles
|
|
46
50
|
mvm image build my-sandbox ./my-app # Dockerfile at ./my-app root -> runnable snapshot
|
|
@@ -50,6 +54,8 @@ mvm scale my-sandbox 10 # converge the fleet, throttled to yo
|
|
|
50
54
|
mvm top --watch # live state table
|
|
51
55
|
```
|
|
52
56
|
|
|
57
|
+
To see it used for real first, jump to [the eight examples](#see-it-working-eight-examples-and-the-article-series).
|
|
58
|
+
|
|
53
59
|
## Why this exists
|
|
54
60
|
|
|
55
61
|
Lambda MicroVMs exposes the primitive under Lambda itself: a Firecracker VM with a full AL2023 userland, a dedicated HTTPS endpoint, and a lifecycle you control (run, suspend, resume, terminate). The service deliberately stops there. There is no load balancer (one endpoint per VM), no fleet abstraction, no token management, no dashboard, and a fresh account runs quotas far below the published defaults. microvm-ctl fills that gap.
|
|
@@ -63,6 +69,8 @@ Lambda MicroVMs exposes the primitive under Lambda itself: a Firecracker VM with
|
|
|
63
69
|
| In-VM hook runtime | zero-dependency server for `/ready`, `/validate`, `/run`, `/resume`, `/suspend`, `/terminate` plus your own routes | `microvm/hooks/server.py` |
|
|
64
70
|
| Observability and cost | live fleet table, CloudWatch tail, a cost model that prices a session shape before you commit to it | `microvm/monitor.py` |
|
|
65
71
|
| Account bootstrap | artifact bucket plus separate build and execution roles | `microvm/bootstrap.py` |
|
|
72
|
+
| Leases | `Lease`, `LeasePolicy`, `FleetManager.lease`: one task per VM, completed by the VM through a task token, callback id, HTTP, SQS, or EventBridge | `microvm/lease.py` |
|
|
73
|
+
| Orchestrator integrations | generated Step Functions state machine and IAM (`mvm lease asl`, `mvm lease policy`), `lease_microvm` for Lambda durable functions | `microvm/integrations/` |
|
|
66
74
|
|
|
67
75
|
The `lambda-microvms` botocore service model ships inside the package, so the plane works on whatever boto3 you already have.
|
|
68
76
|
|
|
@@ -142,6 +150,14 @@ app.serve(port=8080)
|
|
|
142
150
|
|
|
143
151
|

|
|
144
152
|
|
|
153
|
+
## The playground
|
|
154
|
+
|
|
155
|
+
`mvm playground` opens a local web app that drives everything above against the live service: build images, run and scale fleets, call VMs, tail logs, price sessions, run a parameterised benchmark, and watch every AWS API call the process makes in a trace. A dry-run switch turns each mutating action into a printout of the exact request it would send. See [docs/playground.md](docs/playground.md).
|
|
156
|
+
|
|
157
|
+
```console
|
|
158
|
+
mvm playground # http://127.0.0.1:8765
|
|
159
|
+
```
|
|
160
|
+
|
|
145
161
|
## Documentation
|
|
146
162
|
|
|
147
163
|
- [Quickstart](docs/quickstart.md): from an empty account to a serving VM, with the environment variables explained.
|
|
@@ -150,11 +166,32 @@ app.serve(port=8080)
|
|
|
150
166
|
- [Architecture](docs/architecture.md): the two planes, the lifecycle state machine, the fleet design decisions, and the security model.
|
|
151
167
|
- [The hook contract](docs/hooks.md): what each hook is for and what breaks when you ignore it.
|
|
152
168
|
- [Quotas and cost](docs/quotas-and-cost.md): the quota walls, what counts against them, and the cost model with worked examples.
|
|
153
|
-
- [Troubleshooting](docs/troubleshooting.md): the errors
|
|
169
|
+
- [Troubleshooting](docs/troubleshooting.md): the errors I hit on the live service and what each one meant.
|
|
170
|
+
- [The playground](docs/playground.md): the local web app over the whole SDK, and how to host it.
|
|
171
|
+
- [Integrations](docs/integrations.md): the lease contract for handing a VM to Step Functions, Lambda durable functions, or your own orchestrator, and what the plane should own.
|
|
172
|
+
|
|
173
|
+
## See it working: eight examples and the article series
|
|
174
|
+
|
|
175
|
+
The fastest way to understand the plane is to read the apps built on it. The companion repo [awesome-microvm](https://github.com/Vivek0712/awesome-microvm) holds eight production-shaped examples, each a Dockerfile plus a single-file app, deployed and recorded on the live service:
|
|
176
|
+
|
|
177
|
+
| Example | What it shows |
|
|
178
|
+
|---|---|
|
|
179
|
+
| [code-sandbox](https://github.com/Vivek0712/awesome-microvm/tree/main/examples/code-sandbox) | untrusted or AI-written Python per session; state persists across calls and suspend |
|
|
180
|
+
| [ai-code-runner](https://github.com/Vivek0712/awesome-microvm/tree/main/examples/ai-code-runner) | Bedrock writes code, the VM runs it, tracebacks drive a self-repair loop |
|
|
181
|
+
| [agent-eval](https://github.com/Vivek0712/awesome-microvm/tree/main/examples/agent-eval) | `Fleet.scale_to` fan-out over byte-identical clones, scoreboard, drain |
|
|
182
|
+
| [notebook](https://github.com/Vivek0712/awesome-microvm/tree/main/examples/notebook) | a kernel whose namespace survives suspend and resume with the same PID |
|
|
183
|
+
| [data-analytics](https://github.com/Vivek0712/awesome-microvm/tree/main/examples/data-analytics) | DuckDB over S3 through the execution role; bulk data off the endpoint |
|
|
184
|
+
| [ci-runner](https://github.com/Vivek0712/awesome-microvm/tree/main/examples/ci-runner) | clone, test, report, terminate; `--max-duration` as the runaway cap |
|
|
185
|
+
| [pdf-service](https://github.com/Vivek0712/awesome-microvm/tree/main/examples/pdf-service) | untrusted HTML rendered in the VM; idle policy sleeps it between bursts |
|
|
186
|
+
| [multi-tenant-agents](https://github.com/Vivek0712/awesome-microvm/tree/main/examples/multi-tenant-agents) | one VM per tenant, identity via `runHookPayload`, `run_payload_factory` on a `Fleet` |
|
|
187
|
+
|
|
188
|
+
The three-part article series **Building on AWS Lambda MicroVMs** on the AWS Builder Center walks through them:
|
|
154
189
|
|
|
155
|
-
|
|
190
|
+
1. [Control and scale AWS Lambda MicroVMs with microvm-ctl](https://builder.aws.com/content/3JIDTpz0ZgatSBv24drra3gEod9/control-and-scale-aws-lambda-microvms-with-microvm-ctl): this plane and its measurements.
|
|
191
|
+
2. [Seven workloads Lambda could never run, until MicroVMs](https://builder.aws.com/content/3JJ2oNWY9EsZzivMMx044cSlrFQ/seven-workloads-lambda-could-never-run-until-microvms): the first seven examples through build, run, cost, and gotchas.
|
|
192
|
+
3. [A kernel for every customer: scaling AI agents to 1,000 tenants on AWS Lambda MicroVMs with microvm-ctl](https://builder.aws.com/content/3JJ7tASPSSUUTrcpWnWtxOuu8g3/a-kernel-for-every-customer-scaling-ai-agents-to-1000-tenants-on-aws-lambda-microvms-with-microvm-ctl): the multi-tenant finale with the decision guide.
|
|
156
193
|
|
|
157
|
-
|
|
194
|
+
The article sources and a long-form deep dive per example live under [awesome-microvm/blog](https://github.com/Vivek0712/awesome-microvm/tree/main/blog).
|
|
158
195
|
|
|
159
196
|
## Requirements and regions
|
|
160
197
|
|
|
@@ -162,7 +199,7 @@ Python 3.9 or newer on the machine running the plane. The VMs themselves are ARM
|
|
|
162
199
|
|
|
163
200
|
## Credits and inspiration
|
|
164
201
|
|
|
165
|
-
This project grew out of [lambda-microvm-starter](https://github.com/vidanov/lambda-microvm-starter) by [Alexey Vidanov](https://github.com/vidanov), the one-command on-ramp that deploys any Dockerfile to a Lambda MicroVM behind a public CloudFront URL. His starter kit and its troubleshooting notes were the first working map of the service
|
|
202
|
+
This project grew out of [lambda-microvm-starter](https://github.com/vidanov/lambda-microvm-starter) by [Alexey Vidanov](https://github.com/vidanov), the one-command on-ramp that deploys any Dockerfile to a Lambda MicroVM behind a public CloudFront URL. His starter kit and its troubleshooting notes were the first working map of the service I had, and several of the gotchas documented here were first written down there. microvm-ctl takes the next step from one deployed app to fleets, tokens, quotas, and cost, and I am grateful for the ground he covered first.
|
|
166
203
|
|
|
167
204
|
## Contributing
|
|
168
205
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# microvm-ctl
|
|
2
2
|
|
|
3
|
-
**The control and execution plane for [AWS Lambda MicroVMs](https://docs.aws.amazon.com/lambda/latest/dg/lambda-microvms-guide.html).** Build a snapshot image from a Dockerfile, launch Firecracker microVMs in seconds, scale a fleet inside the quotas your account actually has, call into every VM over its authenticated endpoint,
|
|
3
|
+
**The control and execution plane for [AWS Lambda MicroVMs](https://docs.aws.amazon.com/lambda/latest/dg/lambda-microvms-guide.html).** Build a snapshot image from a Dockerfile, launch Firecracker microVMs in seconds, scale a fleet inside the quotas your account actually has, call into every VM over its authenticated endpoint, watch the whole thing live, and hand a VM a task from Step Functions, Lambda durable functions, or any orchestrator through a lease the VM completes itself. One Python SDK, one `mvm` command.
|
|
4
4
|
|
|
5
5
|
[](https://github.com/Vivek0712/microvm-ctl/actions/workflows/ci.yml)
|
|
6
6
|
[](https://pypi.org/project/microvm-ctl/)
|
|
7
|
-

|
|
8
8
|

|
|
9
9
|
|
|
10
10
|
```console
|
|
11
|
-
pip install microvm-ctl
|
|
11
|
+
pip install microvm-ctl # https://pypi.org/project/microvm-ctl/
|
|
12
12
|
|
|
13
13
|
mvm bootstrap # one time: S3 artifact bucket + build/execution IAM roles
|
|
14
14
|
mvm image build my-sandbox ./my-app # Dockerfile at ./my-app root -> runnable snapshot
|
|
@@ -18,6 +18,8 @@ mvm scale my-sandbox 10 # converge the fleet, throttled to yo
|
|
|
18
18
|
mvm top --watch # live state table
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
To see it used for real first, jump to [the eight examples](#see-it-working-eight-examples-and-the-article-series).
|
|
22
|
+
|
|
21
23
|
## Why this exists
|
|
22
24
|
|
|
23
25
|
Lambda MicroVMs exposes the primitive under Lambda itself: a Firecracker VM with a full AL2023 userland, a dedicated HTTPS endpoint, and a lifecycle you control (run, suspend, resume, terminate). The service deliberately stops there. There is no load balancer (one endpoint per VM), no fleet abstraction, no token management, no dashboard, and a fresh account runs quotas far below the published defaults. microvm-ctl fills that gap.
|
|
@@ -31,6 +33,8 @@ Lambda MicroVMs exposes the primitive under Lambda itself: a Firecracker VM with
|
|
|
31
33
|
| In-VM hook runtime | zero-dependency server for `/ready`, `/validate`, `/run`, `/resume`, `/suspend`, `/terminate` plus your own routes | `microvm/hooks/server.py` |
|
|
32
34
|
| Observability and cost | live fleet table, CloudWatch tail, a cost model that prices a session shape before you commit to it | `microvm/monitor.py` |
|
|
33
35
|
| Account bootstrap | artifact bucket plus separate build and execution roles | `microvm/bootstrap.py` |
|
|
36
|
+
| Leases | `Lease`, `LeasePolicy`, `FleetManager.lease`: one task per VM, completed by the VM through a task token, callback id, HTTP, SQS, or EventBridge | `microvm/lease.py` |
|
|
37
|
+
| Orchestrator integrations | generated Step Functions state machine and IAM (`mvm lease asl`, `mvm lease policy`), `lease_microvm` for Lambda durable functions | `microvm/integrations/` |
|
|
34
38
|
|
|
35
39
|
The `lambda-microvms` botocore service model ships inside the package, so the plane works on whatever boto3 you already have.
|
|
36
40
|
|
|
@@ -110,6 +114,14 @@ app.serve(port=8080)
|
|
|
110
114
|
|
|
111
115
|

|
|
112
116
|
|
|
117
|
+
## The playground
|
|
118
|
+
|
|
119
|
+
`mvm playground` opens a local web app that drives everything above against the live service: build images, run and scale fleets, call VMs, tail logs, price sessions, run a parameterised benchmark, and watch every AWS API call the process makes in a trace. A dry-run switch turns each mutating action into a printout of the exact request it would send. See [docs/playground.md](docs/playground.md).
|
|
120
|
+
|
|
121
|
+
```console
|
|
122
|
+
mvm playground # http://127.0.0.1:8765
|
|
123
|
+
```
|
|
124
|
+
|
|
113
125
|
## Documentation
|
|
114
126
|
|
|
115
127
|
- [Quickstart](docs/quickstart.md): from an empty account to a serving VM, with the environment variables explained.
|
|
@@ -118,11 +130,32 @@ app.serve(port=8080)
|
|
|
118
130
|
- [Architecture](docs/architecture.md): the two planes, the lifecycle state machine, the fleet design decisions, and the security model.
|
|
119
131
|
- [The hook contract](docs/hooks.md): what each hook is for and what breaks when you ignore it.
|
|
120
132
|
- [Quotas and cost](docs/quotas-and-cost.md): the quota walls, what counts against them, and the cost model with worked examples.
|
|
121
|
-
- [Troubleshooting](docs/troubleshooting.md): the errors
|
|
133
|
+
- [Troubleshooting](docs/troubleshooting.md): the errors I hit on the live service and what each one meant.
|
|
134
|
+
- [The playground](docs/playground.md): the local web app over the whole SDK, and how to host it.
|
|
135
|
+
- [Integrations](docs/integrations.md): the lease contract for handing a VM to Step Functions, Lambda durable functions, or your own orchestrator, and what the plane should own.
|
|
136
|
+
|
|
137
|
+
## See it working: eight examples and the article series
|
|
138
|
+
|
|
139
|
+
The fastest way to understand the plane is to read the apps built on it. The companion repo [awesome-microvm](https://github.com/Vivek0712/awesome-microvm) holds eight production-shaped examples, each a Dockerfile plus a single-file app, deployed and recorded on the live service:
|
|
140
|
+
|
|
141
|
+
| Example | What it shows |
|
|
142
|
+
|---|---|
|
|
143
|
+
| [code-sandbox](https://github.com/Vivek0712/awesome-microvm/tree/main/examples/code-sandbox) | untrusted or AI-written Python per session; state persists across calls and suspend |
|
|
144
|
+
| [ai-code-runner](https://github.com/Vivek0712/awesome-microvm/tree/main/examples/ai-code-runner) | Bedrock writes code, the VM runs it, tracebacks drive a self-repair loop |
|
|
145
|
+
| [agent-eval](https://github.com/Vivek0712/awesome-microvm/tree/main/examples/agent-eval) | `Fleet.scale_to` fan-out over byte-identical clones, scoreboard, drain |
|
|
146
|
+
| [notebook](https://github.com/Vivek0712/awesome-microvm/tree/main/examples/notebook) | a kernel whose namespace survives suspend and resume with the same PID |
|
|
147
|
+
| [data-analytics](https://github.com/Vivek0712/awesome-microvm/tree/main/examples/data-analytics) | DuckDB over S3 through the execution role; bulk data off the endpoint |
|
|
148
|
+
| [ci-runner](https://github.com/Vivek0712/awesome-microvm/tree/main/examples/ci-runner) | clone, test, report, terminate; `--max-duration` as the runaway cap |
|
|
149
|
+
| [pdf-service](https://github.com/Vivek0712/awesome-microvm/tree/main/examples/pdf-service) | untrusted HTML rendered in the VM; idle policy sleeps it between bursts |
|
|
150
|
+
| [multi-tenant-agents](https://github.com/Vivek0712/awesome-microvm/tree/main/examples/multi-tenant-agents) | one VM per tenant, identity via `runHookPayload`, `run_payload_factory` on a `Fleet` |
|
|
151
|
+
|
|
152
|
+
The three-part article series **Building on AWS Lambda MicroVMs** on the AWS Builder Center walks through them:
|
|
122
153
|
|
|
123
|
-
|
|
154
|
+
1. [Control and scale AWS Lambda MicroVMs with microvm-ctl](https://builder.aws.com/content/3JIDTpz0ZgatSBv24drra3gEod9/control-and-scale-aws-lambda-microvms-with-microvm-ctl): this plane and its measurements.
|
|
155
|
+
2. [Seven workloads Lambda could never run, until MicroVMs](https://builder.aws.com/content/3JJ2oNWY9EsZzivMMx044cSlrFQ/seven-workloads-lambda-could-never-run-until-microvms): the first seven examples through build, run, cost, and gotchas.
|
|
156
|
+
3. [A kernel for every customer: scaling AI agents to 1,000 tenants on AWS Lambda MicroVMs with microvm-ctl](https://builder.aws.com/content/3JJ7tASPSSUUTrcpWnWtxOuu8g3/a-kernel-for-every-customer-scaling-ai-agents-to-1000-tenants-on-aws-lambda-microvms-with-microvm-ctl): the multi-tenant finale with the decision guide.
|
|
124
157
|
|
|
125
|
-
|
|
158
|
+
The article sources and a long-form deep dive per example live under [awesome-microvm/blog](https://github.com/Vivek0712/awesome-microvm/tree/main/blog).
|
|
126
159
|
|
|
127
160
|
## Requirements and regions
|
|
128
161
|
|
|
@@ -130,7 +163,7 @@ Python 3.9 or newer on the machine running the plane. The VMs themselves are ARM
|
|
|
130
163
|
|
|
131
164
|
## Credits and inspiration
|
|
132
165
|
|
|
133
|
-
This project grew out of [lambda-microvm-starter](https://github.com/vidanov/lambda-microvm-starter) by [Alexey Vidanov](https://github.com/vidanov), the one-command on-ramp that deploys any Dockerfile to a Lambda MicroVM behind a public CloudFront URL. His starter kit and its troubleshooting notes were the first working map of the service
|
|
166
|
+
This project grew out of [lambda-microvm-starter](https://github.com/vidanov/lambda-microvm-starter) by [Alexey Vidanov](https://github.com/vidanov), the one-command on-ramp that deploys any Dockerfile to a Lambda MicroVM behind a public CloudFront URL. His starter kit and its troubleshooting notes were the first working map of the service I had, and several of the gotchas documented here were first written down there. microvm-ctl takes the next step from one deployed app to fleets, tokens, quotas, and cost, and I am grateful for the ground he covered first.
|
|
134
167
|
|
|
135
168
|
## Contributing
|
|
136
169
|
|
|
@@ -9,9 +9,10 @@ from microvm.config import PlaneConfig
|
|
|
9
9
|
from microvm.endpoint import EndpointClient, EndpointError
|
|
10
10
|
from microvm.fleet import Fleet, FleetManager
|
|
11
11
|
from microvm.images import ImageBuilder, ImageBuildError
|
|
12
|
+
from microvm.lease import Lease, LeasePolicy
|
|
12
13
|
from microvm.monitor import CostModel, FleetMonitor
|
|
13
14
|
|
|
14
|
-
__version__ = "0.
|
|
15
|
+
__version__ = "0.2.0"
|
|
15
16
|
|
|
16
17
|
__all__ = [
|
|
17
18
|
"microvm_client",
|
|
@@ -21,6 +22,8 @@ __all__ = [
|
|
|
21
22
|
"ImageBuildError",
|
|
22
23
|
"Fleet",
|
|
23
24
|
"FleetManager",
|
|
25
|
+
"Lease",
|
|
26
|
+
"LeasePolicy",
|
|
24
27
|
"EndpointClient",
|
|
25
28
|
"EndpointError",
|
|
26
29
|
"FleetMonitor",
|
|
@@ -74,7 +74,12 @@ def bootstrap(cfg: PlaneConfig, prefix: str = "microvm-ctl") -> dict:
|
|
|
74
74
|
logs_stmt = {
|
|
75
75
|
"Effect": "Allow",
|
|
76
76
|
"Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
|
|
77
|
-
|
|
77
|
+
# the service writes to /aws/lambda-microvms/<image>; the old prefix is kept
|
|
78
|
+
# so roles bootstrapped by earlier releases keep working
|
|
79
|
+
"Resource": [
|
|
80
|
+
f"arn:aws:logs:{cfg.region}:{account}:log-group:/aws/lambda-microvms/*",
|
|
81
|
+
f"arn:aws:logs:{cfg.region}:{account}:log-group:/aws/lambda/microvms/*",
|
|
82
|
+
],
|
|
78
83
|
}
|
|
79
84
|
build_role = _ensure_role(
|
|
80
85
|
iam,
|
|
@@ -10,9 +10,12 @@
|
|
|
10
10
|
mvm suspend|resume|terminate ID... lifecycle control
|
|
11
11
|
mvm drain IMAGE terminate the whole fleet
|
|
12
12
|
mvm call ID /path [-X POST -d '{}'] authenticated request into the VM
|
|
13
|
+
mvm status ID | watch ID job telemetry from the hook runtime (/status, /events)
|
|
14
|
+
mvm lease asl|policy|run Step Functions ASL, IAM statements, one manual lease
|
|
13
15
|
mvm top [--image NAME] [--watch] live fleet dashboard
|
|
14
16
|
mvm logs IMAGE CloudWatch tail
|
|
15
17
|
mvm cost [--memory-gb 2 ...] session economics
|
|
18
|
+
mvm playground [--port 8765] browser UI over everything above, live against AWS
|
|
16
19
|
"""
|
|
17
20
|
|
|
18
21
|
from __future__ import annotations
|
|
@@ -253,11 +256,138 @@ def cmd_logs(args):
|
|
|
253
256
|
mon = FleetMonitor(_cfg(args))
|
|
254
257
|
events = mon.tail_logs(args.image, minutes=args.minutes)
|
|
255
258
|
if not events:
|
|
256
|
-
console.print(f"[dim]no events in
|
|
259
|
+
console.print(f"[dim]no events in {' or '.join(mon.log_groups(args.image))}[/]")
|
|
257
260
|
for e in events:
|
|
258
261
|
console.print(f"[dim]{e['stream'][:20]}[/] {e['message']}")
|
|
259
262
|
|
|
260
263
|
|
|
264
|
+
# ---------------------------------------------------------------- job telemetry
|
|
265
|
+
def _snapshot_table(snap: dict, title: str) -> Table:
|
|
266
|
+
t = Table(title=title, header_style="bold magenta", show_header=False)
|
|
267
|
+
t.add_column("field", style="cyan")
|
|
268
|
+
t.add_column("value")
|
|
269
|
+
prog = snap.get("progress") or {}
|
|
270
|
+
done, total = prog.get("done"), prog.get("total")
|
|
271
|
+
t.add_row("phase", str(snap.get("phase") or "-"))
|
|
272
|
+
t.add_row("elapsed", f"{snap.get('elapsed_s', 0):.0f} s")
|
|
273
|
+
t.add_row("progress", f"{done}/{total}" if total else str(done if done is not None else "-"))
|
|
274
|
+
for k, v in sorted((snap.get("counters") or {}).items()):
|
|
275
|
+
t.add_row(f"counter {k}", str(v))
|
|
276
|
+
lease = snap.get("lease")
|
|
277
|
+
if lease:
|
|
278
|
+
state = "done" if lease.get("done") else ("lost" if lease.get("lost") else "active")
|
|
279
|
+
err = lease.get("error")
|
|
280
|
+
colour = "red" if err else "green"
|
|
281
|
+
t.add_row("lease", f"{lease.get('kind')} {lease.get('id') or ''} "
|
|
282
|
+
f"heartbeats={lease.get('heartbeats', 0)} [{colour}]{state}[/]")
|
|
283
|
+
if err:
|
|
284
|
+
t.add_row("error", f"[red]{err.get('error_type')}[/] {err.get('message')}")
|
|
285
|
+
else:
|
|
286
|
+
t.add_row("lease", "[dim]none[/]")
|
|
287
|
+
return t
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _log_line(entry: dict) -> str:
|
|
291
|
+
level = entry.get("level", "info")
|
|
292
|
+
style = {"error": "red", "warning": "yellow", "warn": "yellow"}.get(level, "dim")
|
|
293
|
+
extra = {k: v for k, v in entry.items() if k not in ("t", "level", "msg", "phase", "microvm_id", "seq")}
|
|
294
|
+
tail = f" [dim]{json.dumps(extra, default=str)}[/]" if extra else ""
|
|
295
|
+
return f"[{style}]{level:<7}[/] [cyan]{entry.get('phase') or ''}[/] {entry.get('msg', '')}{tail}"
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def cmd_status(args):
|
|
299
|
+
client = EndpointClient(_cfg(args), args.id, ports=[args.port] if args.port else None)
|
|
300
|
+
snap = client.status()
|
|
301
|
+
console.print(_snapshot_table(snap, f"job on {args.id}"))
|
|
302
|
+
for entry in snap.get("log_tail") or []:
|
|
303
|
+
console.print(_log_line(entry) if isinstance(entry, dict) else str(entry))
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def cmd_watch(args):
|
|
307
|
+
from rich.live import Live
|
|
308
|
+
client = EndpointClient(_cfg(args), args.id, ports=[args.port] if args.port else None)
|
|
309
|
+
snap = client.status()
|
|
310
|
+
with Live(_snapshot_table(snap, f"job on {args.id}"), refresh_per_second=2, console=console) as live:
|
|
311
|
+
for obj in client.watch(timeout=args.timeout):
|
|
312
|
+
if not isinstance(obj, dict):
|
|
313
|
+
continue
|
|
314
|
+
if "msg" in obj:
|
|
315
|
+
live.console.print(_log_line(obj))
|
|
316
|
+
else:
|
|
317
|
+
live.update(_snapshot_table(obj, f"job on {args.id}"))
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# ---------------------------------------------------------------- leases
|
|
321
|
+
def _lease_policy(args):
|
|
322
|
+
from microvm.lease import LeasePolicy
|
|
323
|
+
return LeasePolicy(budget_s=args.budget, heartbeat_timeout_s=args.heartbeat, slack_s=args.slack)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _image_arn_offline(image: str, region: str, role_arn: str | None, profile) -> str:
|
|
327
|
+
"""Resolve a bare image name without calling STS when the account is readable
|
|
328
|
+
from the execution role ARN, so `mvm lease asl` works without credentials."""
|
|
329
|
+
from microvm.client import image_arn
|
|
330
|
+
if image.startswith("arn:"):
|
|
331
|
+
return image
|
|
332
|
+
parts = (role_arn or "").split(":")
|
|
333
|
+
if role_arn and role_arn.startswith("arn:") and len(parts) > 4 and parts[4]:
|
|
334
|
+
return f"arn:aws:lambda:{region}:{parts[4]}:microvm-image:{image}"
|
|
335
|
+
return image_arn(image, region, profile)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def cmd_lease_asl(args):
|
|
339
|
+
from microvm.integrations.stepfunctions import lease_state_machine
|
|
340
|
+
cfg = _cfg(args)
|
|
341
|
+
role = args.execution_role or cfg.execution_role_arn
|
|
342
|
+
if not role:
|
|
343
|
+
sys.exit("mvm lease asl: --execution-role ARN (or MVM_EXECUTION_ROLE_ARN) is required")
|
|
344
|
+
asl = lease_state_machine(
|
|
345
|
+
image_arn=_image_arn_offline(args.image, cfg.region, role, cfg.profile),
|
|
346
|
+
execution_role_arn=role, policy=_lease_policy(args), region=cfg.region,
|
|
347
|
+
name=args.name, task_expr=args.task_expr, heartbeat_s=args.heartbeat_every,
|
|
348
|
+
)
|
|
349
|
+
print(json.dumps(asl, indent=2))
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def cmd_lease_policy(args):
|
|
353
|
+
from microvm.integrations.stepfunctions import iam_statements, orchestrator_statements, policy_document
|
|
354
|
+
cfg = _cfg(args)
|
|
355
|
+
out = {
|
|
356
|
+
"execution_role": policy_document(iam_statements(args.kind, orchestrator_arn=args.orchestrator)),
|
|
357
|
+
"orchestrator_role": policy_document(
|
|
358
|
+
orchestrator_statements(args.execution_role or cfg.execution_role_arn)),
|
|
359
|
+
}
|
|
360
|
+
print(json.dumps(out, indent=2))
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def cmd_lease_run(args):
|
|
364
|
+
from microvm.lease import Lease
|
|
365
|
+
cfg = _cfg(args)
|
|
366
|
+
if args.kind != "none" and not args.token:
|
|
367
|
+
sys.exit(f"mvm lease run: --token is required for kind {args.kind}")
|
|
368
|
+
try:
|
|
369
|
+
task = json.loads(args.task) if args.task else {}
|
|
370
|
+
except ValueError as e:
|
|
371
|
+
sys.exit(f"mvm lease run: --task is not valid JSON: {e}")
|
|
372
|
+
lease = Lease(kind=args.kind, token=args.token or "", region=cfg.region, target=args.target,
|
|
373
|
+
heartbeat_s=args.heartbeat_every, id=args.id)
|
|
374
|
+
fm = FleetManager(cfg)
|
|
375
|
+
vm = fm.lease(args.image, lease, task, _lease_policy(args), version=args.version,
|
|
376
|
+
execution_role=args.execution_role)
|
|
377
|
+
console.print(
|
|
378
|
+
f"[bold green]✓[/] {vm.microvm_id} {_state(vm.state)} "
|
|
379
|
+
f"[link=https://{vm.endpoint}]{vm.endpoint}[/link] lease {lease.kind}"
|
|
380
|
+
)
|
|
381
|
+
if args.wait:
|
|
382
|
+
vm = fm.wait_until(vm.microvm_id, "RUNNING")
|
|
383
|
+
console.print(f" now {_state(vm.state)}; follow it with: mvm watch {vm.microvm_id}")
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def cmd_playground(args):
|
|
387
|
+
from microvm.playground import serve
|
|
388
|
+
serve(host=args.host, port=args.port, dry_run=args.dry_run, open_browser=not args.no_open, cfg=_cfg(args))
|
|
389
|
+
|
|
390
|
+
|
|
261
391
|
def cmd_cost(args):
|
|
262
392
|
model = CostModel(memory_gb=args.memory_gb, snapshot_gb=args.snapshot_gb)
|
|
263
393
|
s = model.session(args.active * 60, args.suspended * 60, args.cycles)
|
|
@@ -351,13 +481,63 @@ def main(argv: list[str] | None = None):
|
|
|
351
481
|
c.add_argument("--port", type=int, default=None, help="non-default app port")
|
|
352
482
|
c.set_defaults(fn=cmd_call)
|
|
353
483
|
|
|
484
|
+
st = sub.add_parser("status", help="job snapshot from the hook runtime (GET /status)")
|
|
485
|
+
st.add_argument("id")
|
|
486
|
+
st.add_argument("--port", type=int, default=None, help="non-default app port")
|
|
487
|
+
st.set_defaults(fn=cmd_status)
|
|
488
|
+
|
|
489
|
+
w = sub.add_parser("watch", help="live job table plus streamed log lines (GET /events)")
|
|
490
|
+
w.add_argument("id")
|
|
491
|
+
w.add_argument("--port", type=int, default=None, help="non-default app port")
|
|
492
|
+
w.add_argument("--timeout", type=float, default=600, help="stop after N seconds")
|
|
493
|
+
w.set_defaults(fn=cmd_watch)
|
|
494
|
+
|
|
495
|
+
le = sub.add_parser("lease", help="hand a VM a task through a lease").add_subparsers(
|
|
496
|
+
dest="sub", required=True)
|
|
497
|
+
|
|
498
|
+
def _policy_flags(sp):
|
|
499
|
+
sp.add_argument("--budget", type=int, default=900,
|
|
500
|
+
help="seconds the orchestrator waits (task timeout)")
|
|
501
|
+
sp.add_argument("--heartbeat", type=int, default=120, help="heartbeat timeout in seconds")
|
|
502
|
+
sp.add_argument("--slack", type=int, default=120, help="VM outlives the budget by this many seconds")
|
|
503
|
+
sp.add_argument("--heartbeat-every", type=int, default=30, help="seconds between VM heartbeats")
|
|
504
|
+
|
|
505
|
+
la = le.add_parser("asl", help="print the Step Functions state machine (JSONata) for one lease")
|
|
506
|
+
la.add_argument("--image", required=True, help="image name or ARN")
|
|
507
|
+
la.add_argument("--execution-role", default=None,
|
|
508
|
+
help="VM execution role ARN (default: MVM_EXECUTION_ROLE_ARN)")
|
|
509
|
+
la.add_argument("--name", default="Lease", help="name of the lease state")
|
|
510
|
+
la.add_argument("--task-expr", default="$states.input", help="JSONata expression for the task")
|
|
511
|
+
_policy_flags(la)
|
|
512
|
+
la.set_defaults(fn=cmd_lease_asl)
|
|
513
|
+
|
|
514
|
+
lp = le.add_parser("policy", help="print the IAM statements for the VM role and the orchestrator role")
|
|
515
|
+
lp.add_argument("--kind", required=True, choices=["sfn", "durable", "sqs", "eventbridge", "http"])
|
|
516
|
+
lp.add_argument("--orchestrator", default=None,
|
|
517
|
+
help="state machine, function, queue, or bus ARN the VM completes to")
|
|
518
|
+
lp.add_argument("--execution-role", default=None, help="VM execution role for iam:PassRole")
|
|
519
|
+
lp.set_defaults(fn=cmd_lease_policy)
|
|
520
|
+
|
|
521
|
+
lr = le.add_parser("run", help="launch one lease by hand (manual tests)")
|
|
522
|
+
lr.add_argument("image")
|
|
523
|
+
lr.add_argument("--kind", required=True, choices=["sfn", "durable", "http", "sqs", "eventbridge", "none"])
|
|
524
|
+
lr.add_argument("--token", default=None, help="task token, callback id, or bearer (not for kind none)")
|
|
525
|
+
lr.add_argument("--target", default=None, help="http URL, SQS queue URL, or event bus name")
|
|
526
|
+
lr.add_argument("--task", default=None, help="task JSON (pointers, not bodies; 4096 chars total)")
|
|
527
|
+
lr.add_argument("--id", default=None, help="human label carried as lease.id")
|
|
528
|
+
lr.add_argument("--version", help="image version (default: latest ACTIVE)")
|
|
529
|
+
lr.add_argument("--execution-role", default=None)
|
|
530
|
+
lr.add_argument("--wait", action="store_true", help="poll until RUNNING")
|
|
531
|
+
_policy_flags(lr)
|
|
532
|
+
lr.set_defaults(fn=cmd_lease_run)
|
|
533
|
+
|
|
354
534
|
tp = sub.add_parser("top", help="live fleet dashboard")
|
|
355
535
|
tp.add_argument("--image")
|
|
356
536
|
tp.add_argument("--watch", action="store_true")
|
|
357
537
|
tp.add_argument("--interval", type=float, default=3)
|
|
358
538
|
tp.set_defaults(fn=cmd_top)
|
|
359
539
|
|
|
360
|
-
lg = sub.add_parser("logs", help="tail CloudWatch logs for an image")
|
|
540
|
+
lg = sub.add_parser("logs", help="tail CloudWatch logs for an image (/aws/lambda-microvms/<image>)")
|
|
361
541
|
lg.add_argument("image")
|
|
362
542
|
lg.add_argument("--minutes", type=int, default=15)
|
|
363
543
|
lg.set_defaults(fn=cmd_logs)
|
|
@@ -371,6 +551,14 @@ def main(argv: list[str] | None = None):
|
|
|
371
551
|
co.add_argument("--cycles", type=int, default=1, help="suspend/resume cycles")
|
|
372
552
|
co.set_defaults(fn=cmd_cost)
|
|
373
553
|
|
|
554
|
+
pgp = sub.add_parser("playground", help="local browser UI that drives the SDK against the live service")
|
|
555
|
+
pgp.add_argument("--host", default="127.0.0.1")
|
|
556
|
+
pgp.add_argument("--port", type=int, default=8765)
|
|
557
|
+
pgp.add_argument("--dry-run", action="store_true",
|
|
558
|
+
help="start with dry run on: mutating actions are recorded, not sent")
|
|
559
|
+
pgp.add_argument("--no-open", action="store_true", help="do not open a browser tab")
|
|
560
|
+
pgp.set_defaults(fn=cmd_playground)
|
|
561
|
+
|
|
374
562
|
args = p.parse_args(argv)
|
|
375
563
|
try:
|
|
376
564
|
args.fn(args)
|
|
@@ -15,6 +15,7 @@ retries the two endpoint errors you must design for:
|
|
|
15
15
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
|
+
import json
|
|
18
19
|
import random
|
|
19
20
|
import time
|
|
20
21
|
|
|
@@ -127,6 +128,47 @@ class EndpointClient:
|
|
|
127
128
|
time.sleep(1)
|
|
128
129
|
raise EndpointError(f"{self.microvm_id} not serving {path} after {timeout}s")
|
|
129
130
|
|
|
131
|
+
# -- job telemetry (HookApp built-in /status and /events) -------------------
|
|
132
|
+
def status(self, since: int | None = None) -> dict:
|
|
133
|
+
"""The hook runtime's job snapshot (`GET /status`): phase, progress, counters,
|
|
134
|
+
the log tail, and the lease state. `since` returns only log lines with a
|
|
135
|
+
sequence number above it."""
|
|
136
|
+
path = "/status" if since is None else f"/status?since={int(since)}"
|
|
137
|
+
resp = self.get(path, timeout=10)
|
|
138
|
+
if resp.status_code != 200:
|
|
139
|
+
raise EndpointError(f"{self.microvm_id} answered {resp.status_code} on {path}")
|
|
140
|
+
return resp.json()
|
|
141
|
+
|
|
142
|
+
def watch(self, timeout: float = 600):
|
|
143
|
+
"""Stream `GET /events` (Server-Sent Events) and yield each parsed JSON object:
|
|
144
|
+
log lines as they happen plus a snapshot every few seconds. Stops when a
|
|
145
|
+
snapshot reports the lease done, on `timeout` seconds, or when the VM closes
|
|
146
|
+
the stream."""
|
|
147
|
+
deadline = time.time() + timeout
|
|
148
|
+
resp = self.request("GET", "/events", timeout=(10, 30), max_attempts=1, stream=True)
|
|
149
|
+
if resp.status_code != 200:
|
|
150
|
+
raise EndpointError(f"{self.microvm_id} answered {resp.status_code} on /events")
|
|
151
|
+
try:
|
|
152
|
+
for line in resp.iter_lines():
|
|
153
|
+
if time.time() > deadline:
|
|
154
|
+
return
|
|
155
|
+
if isinstance(line, bytes):
|
|
156
|
+
line = line.decode("utf-8", "replace")
|
|
157
|
+
if not line.startswith("data:"):
|
|
158
|
+
continue
|
|
159
|
+
try:
|
|
160
|
+
obj = json.loads(line[5:].strip())
|
|
161
|
+
except ValueError:
|
|
162
|
+
continue
|
|
163
|
+
yield obj
|
|
164
|
+
lease = obj.get("lease") if isinstance(obj, dict) else None
|
|
165
|
+
if isinstance(lease, dict) and lease.get("done"):
|
|
166
|
+
return
|
|
167
|
+
except requests.RequestException:
|
|
168
|
+
return
|
|
169
|
+
finally:
|
|
170
|
+
resp.close()
|
|
171
|
+
|
|
130
172
|
def shell_token(self, minutes: int = 15) -> dict[str, str]:
|
|
131
173
|
"""Token for interactive shell access (VM must run with SHELL_INGRESS)."""
|
|
132
174
|
return dict(
|
|
@@ -18,6 +18,7 @@ from typing import Callable
|
|
|
18
18
|
|
|
19
19
|
from microvm.client import image_arn, microvm_client
|
|
20
20
|
from microvm.config import TPS, PlaneConfig
|
|
21
|
+
from microvm.lease import Lease, LeasePolicy, client_token, encode_payload
|
|
21
22
|
from microvm.throttle import Throttled
|
|
22
23
|
|
|
23
24
|
ACTIVE_STATES = {"PENDING", "RUNNING", "SUSPENDING", "SUSPENDED"}
|
|
@@ -122,7 +123,7 @@ class FleetManager:
|
|
|
122
123
|
return self.quotas.get("MaxMemoryGb")
|
|
123
124
|
|
|
124
125
|
# -- single VM ---------------------------------------------------------------
|
|
125
|
-
def
|
|
126
|
+
def run_params(
|
|
126
127
|
self,
|
|
127
128
|
image: str,
|
|
128
129
|
*,
|
|
@@ -133,7 +134,14 @@ class FleetManager:
|
|
|
133
134
|
ingress: list[str] | None = None,
|
|
134
135
|
egress: list[str] | None = None,
|
|
135
136
|
execution_role: str | None = None,
|
|
136
|
-
|
|
137
|
+
client_token: str | None = None,
|
|
138
|
+
) -> dict:
|
|
139
|
+
"""The exact RunMicrovm request `run` would send, for inspection or dry runs.
|
|
140
|
+
|
|
141
|
+
`client_token` (1 to 128 chars) makes the launch idempotent on the service
|
|
142
|
+
side: a retried request with the same token returns the same microVM instead
|
|
143
|
+
of a second one. Use it whenever the caller may replay, such as a durable
|
|
144
|
+
function step."""
|
|
137
145
|
params: dict = {"imageIdentifier": image_arn(image, self.cfg.region, self.cfg.profile)}
|
|
138
146
|
if version:
|
|
139
147
|
params["imageVersion"] = version
|
|
@@ -149,8 +157,52 @@ class FleetManager:
|
|
|
149
157
|
role = execution_role or self.cfg.execution_role_arn
|
|
150
158
|
if role:
|
|
151
159
|
params["executionRoleArn"] = role
|
|
160
|
+
if client_token:
|
|
161
|
+
params["clientToken"] = client_token
|
|
162
|
+
return params
|
|
163
|
+
|
|
164
|
+
def run(
|
|
165
|
+
self,
|
|
166
|
+
image: str,
|
|
167
|
+
*,
|
|
168
|
+
version: str | None = None,
|
|
169
|
+
idle_policy: IdlePolicy | None = None,
|
|
170
|
+
run_payload: str | None = None,
|
|
171
|
+
max_duration: int | None = None,
|
|
172
|
+
ingress: list[str] | None = None,
|
|
173
|
+
egress: list[str] | None = None,
|
|
174
|
+
execution_role: str | None = None,
|
|
175
|
+
client_token: str | None = None,
|
|
176
|
+
) -> Microvm:
|
|
177
|
+
params = self.run_params(
|
|
178
|
+
image, version=version, idle_policy=idle_policy, run_payload=run_payload,
|
|
179
|
+
max_duration=max_duration, ingress=ingress, egress=egress, execution_role=execution_role,
|
|
180
|
+
client_token=client_token,
|
|
181
|
+
)
|
|
152
182
|
return Microvm.from_api(self._run(**params))
|
|
153
183
|
|
|
184
|
+
def lease(
|
|
185
|
+
self,
|
|
186
|
+
image: str,
|
|
187
|
+
lease: Lease,
|
|
188
|
+
task: dict,
|
|
189
|
+
policy: LeasePolicy | None = None,
|
|
190
|
+
*,
|
|
191
|
+
version: str | None = None,
|
|
192
|
+
execution_role: str | None = None,
|
|
193
|
+
ingress: list[str] | None = None,
|
|
194
|
+
egress: list[str] | None = None,
|
|
195
|
+
) -> Microvm:
|
|
196
|
+
"""RunMicrovm with the lease in runHookPayload, the policy's idle policy and duration cap,
|
|
197
|
+
and clientToken = client_token(lease). One call, idempotent on the token."""
|
|
198
|
+
policy = policy or LeasePolicy()
|
|
199
|
+
return self.run(
|
|
200
|
+
image, version=version, idle_policy=policy.idle_policy(),
|
|
201
|
+
run_payload=encode_payload(lease, task), max_duration=policy.max_duration(),
|
|
202
|
+
ingress=ingress, egress=egress, execution_role=execution_role,
|
|
203
|
+
client_token=client_token(lease),
|
|
204
|
+
)
|
|
205
|
+
|
|
154
206
|
def get(self, microvm_id: str) -> Microvm:
|
|
155
207
|
return Microvm.from_api(self.api.get_microvm(microvmIdentifier=microvm_id))
|
|
156
208
|
|