aws-bootstrap-g4dn 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aws_bootstrap/__init__.py +1 -0
- aws_bootstrap/cli.py +438 -0
- aws_bootstrap/config.py +24 -0
- aws_bootstrap/ec2.py +341 -0
- aws_bootstrap/resources/__init__.py +0 -0
- aws_bootstrap/resources/gpu_benchmark.py +839 -0
- aws_bootstrap/resources/gpu_smoke_test.ipynb +340 -0
- aws_bootstrap/resources/remote_setup.sh +188 -0
- aws_bootstrap/resources/requirements.txt +8 -0
- aws_bootstrap/ssh.py +513 -0
- aws_bootstrap/tests/__init__.py +0 -0
- aws_bootstrap/tests/test_cli.py +528 -0
- aws_bootstrap/tests/test_config.py +35 -0
- aws_bootstrap/tests/test_ec2.py +313 -0
- aws_bootstrap/tests/test_ssh_config.py +297 -0
- aws_bootstrap/tests/test_ssh_gpu.py +138 -0
- aws_bootstrap_g4dn-0.1.0.dist-info/METADATA +308 -0
- aws_bootstrap_g4dn-0.1.0.dist-info/RECORD +22 -0
- aws_bootstrap_g4dn-0.1.0.dist-info/WHEEL +5 -0
- aws_bootstrap_g4dn-0.1.0.dist-info/entry_points.txt +2 -0
- aws_bootstrap_g4dn-0.1.0.dist-info/licenses/LICENSE +21 -0
- aws_bootstrap_g4dn-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
"""Tests for CLI entry point and help output."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from datetime import UTC, datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from unittest.mock import patch
|
|
7
|
+
|
|
8
|
+
from click.testing import CliRunner
|
|
9
|
+
|
|
10
|
+
from aws_bootstrap.cli import main
|
|
11
|
+
from aws_bootstrap.ssh import GpuInfo, SSHHostDetails
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_help():
|
|
15
|
+
runner = CliRunner()
|
|
16
|
+
result = runner.invoke(main, ["--help"])
|
|
17
|
+
assert result.exit_code == 0
|
|
18
|
+
assert "Bootstrap AWS EC2 GPU instances" in result.output
|
|
19
|
+
assert "launch" in result.output
|
|
20
|
+
assert "status" in result.output
|
|
21
|
+
assert "terminate" in result.output
|
|
22
|
+
assert "list" in result.output
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_version():
|
|
26
|
+
runner = CliRunner()
|
|
27
|
+
result = runner.invoke(main, ["--version"])
|
|
28
|
+
assert result.exit_code == 0
|
|
29
|
+
assert "version" in result.output
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_launch_help():
|
|
33
|
+
runner = CliRunner()
|
|
34
|
+
result = runner.invoke(main, ["launch", "--help"])
|
|
35
|
+
assert result.exit_code == 0
|
|
36
|
+
assert "--instance-type" in result.output
|
|
37
|
+
assert "--spot" in result.output
|
|
38
|
+
assert "--dry-run" in result.output
|
|
39
|
+
assert "--key-path" in result.output
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_launch_missing_key():
|
|
43
|
+
runner = CliRunner()
|
|
44
|
+
result = runner.invoke(main, ["launch", "--key-path", "/nonexistent/key.pub"])
|
|
45
|
+
assert result.exit_code != 0
|
|
46
|
+
assert "SSH public key not found" in result.output
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_status_help():
|
|
50
|
+
runner = CliRunner()
|
|
51
|
+
result = runner.invoke(main, ["status", "--help"])
|
|
52
|
+
assert result.exit_code == 0
|
|
53
|
+
assert "--region" in result.output
|
|
54
|
+
assert "--profile" in result.output
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_terminate_help():
|
|
58
|
+
runner = CliRunner()
|
|
59
|
+
result = runner.invoke(main, ["terminate", "--help"])
|
|
60
|
+
assert result.exit_code == 0
|
|
61
|
+
assert "--region" in result.output
|
|
62
|
+
assert "--yes" in result.output
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
66
|
+
@patch("aws_bootstrap.cli.find_tagged_instances")
|
|
67
|
+
def test_status_no_instances(mock_find, mock_session):
|
|
68
|
+
mock_find.return_value = []
|
|
69
|
+
runner = CliRunner()
|
|
70
|
+
result = runner.invoke(main, ["status"])
|
|
71
|
+
assert result.exit_code == 0
|
|
72
|
+
assert "No active" in result.output
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@patch("aws_bootstrap.cli.list_ssh_hosts", return_value={})
|
|
76
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
77
|
+
@patch("aws_bootstrap.cli.get_spot_price")
|
|
78
|
+
@patch("aws_bootstrap.cli.find_tagged_instances")
|
|
79
|
+
def test_status_shows_instances(mock_find, mock_spot_price, mock_session, mock_ssh_hosts):
|
|
80
|
+
mock_find.return_value = [
|
|
81
|
+
{
|
|
82
|
+
"InstanceId": "i-abc123",
|
|
83
|
+
"Name": "aws-bootstrap-g4dn.xlarge",
|
|
84
|
+
"State": "running",
|
|
85
|
+
"InstanceType": "g4dn.xlarge",
|
|
86
|
+
"PublicIp": "1.2.3.4",
|
|
87
|
+
"LaunchTime": datetime(2025, 1, 1, tzinfo=UTC),
|
|
88
|
+
"Lifecycle": "spot",
|
|
89
|
+
"AvailabilityZone": "us-west-2a",
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
mock_spot_price.return_value = 0.1578
|
|
93
|
+
runner = CliRunner()
|
|
94
|
+
result = runner.invoke(main, ["status"])
|
|
95
|
+
assert result.exit_code == 0
|
|
96
|
+
assert "i-abc123" in result.output
|
|
97
|
+
assert "1.2.3.4" in result.output
|
|
98
|
+
assert "spot ($0.1578/hr)" in result.output
|
|
99
|
+
assert "Uptime" in result.output
|
|
100
|
+
assert "Est. cost" in result.output
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@patch("aws_bootstrap.cli.list_ssh_hosts", return_value={})
|
|
104
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
105
|
+
@patch("aws_bootstrap.cli.get_spot_price")
|
|
106
|
+
@patch("aws_bootstrap.cli.find_tagged_instances")
|
|
107
|
+
def test_status_on_demand_no_cost(mock_find, mock_spot_price, mock_session, mock_ssh_hosts):
|
|
108
|
+
mock_find.return_value = [
|
|
109
|
+
{
|
|
110
|
+
"InstanceId": "i-ondemand",
|
|
111
|
+
"Name": "aws-bootstrap-g4dn.xlarge",
|
|
112
|
+
"State": "running",
|
|
113
|
+
"InstanceType": "g4dn.xlarge",
|
|
114
|
+
"PublicIp": "5.6.7.8",
|
|
115
|
+
"LaunchTime": datetime(2025, 1, 1, tzinfo=UTC),
|
|
116
|
+
"Lifecycle": "on-demand",
|
|
117
|
+
"AvailabilityZone": "us-west-2a",
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
runner = CliRunner()
|
|
121
|
+
result = runner.invoke(main, ["status"])
|
|
122
|
+
assert result.exit_code == 0
|
|
123
|
+
assert "on-demand" in result.output
|
|
124
|
+
assert "Uptime" not in result.output
|
|
125
|
+
assert "Est. cost" not in result.output
|
|
126
|
+
mock_spot_price.assert_not_called()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
130
|
+
@patch("aws_bootstrap.cli.find_tagged_instances")
|
|
131
|
+
def test_terminate_no_instances(mock_find, mock_session):
|
|
132
|
+
mock_find.return_value = []
|
|
133
|
+
runner = CliRunner()
|
|
134
|
+
result = runner.invoke(main, ["terminate"])
|
|
135
|
+
assert result.exit_code == 0
|
|
136
|
+
assert "No active" in result.output
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@patch("aws_bootstrap.cli.remove_ssh_host", return_value=None)
|
|
140
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
141
|
+
@patch("aws_bootstrap.cli.find_tagged_instances")
|
|
142
|
+
@patch("aws_bootstrap.cli.terminate_tagged_instances")
|
|
143
|
+
def test_terminate_with_confirm(mock_terminate, mock_find, mock_session, mock_remove_ssh):
|
|
144
|
+
mock_find.return_value = [
|
|
145
|
+
{
|
|
146
|
+
"InstanceId": "i-abc123",
|
|
147
|
+
"Name": "test",
|
|
148
|
+
"State": "running",
|
|
149
|
+
"InstanceType": "g4dn.xlarge",
|
|
150
|
+
"PublicIp": "1.2.3.4",
|
|
151
|
+
"LaunchTime": datetime(2025, 1, 1, tzinfo=UTC),
|
|
152
|
+
}
|
|
153
|
+
]
|
|
154
|
+
mock_terminate.return_value = [
|
|
155
|
+
{
|
|
156
|
+
"InstanceId": "i-abc123",
|
|
157
|
+
"PreviousState": {"Name": "running"},
|
|
158
|
+
"CurrentState": {"Name": "shutting-down"},
|
|
159
|
+
}
|
|
160
|
+
]
|
|
161
|
+
runner = CliRunner()
|
|
162
|
+
result = runner.invoke(main, ["terminate", "--yes"])
|
|
163
|
+
assert result.exit_code == 0
|
|
164
|
+
assert "Terminated 1" in result.output
|
|
165
|
+
mock_terminate.assert_called_once()
|
|
166
|
+
assert mock_terminate.call_args[0][1] == ["i-abc123"]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
170
|
+
@patch("aws_bootstrap.cli.find_tagged_instances")
|
|
171
|
+
def test_terminate_cancelled(mock_find, mock_session):
|
|
172
|
+
mock_find.return_value = [
|
|
173
|
+
{
|
|
174
|
+
"InstanceId": "i-abc123",
|
|
175
|
+
"Name": "test",
|
|
176
|
+
"State": "running",
|
|
177
|
+
"InstanceType": "g4dn.xlarge",
|
|
178
|
+
"PublicIp": "",
|
|
179
|
+
"LaunchTime": datetime(2025, 1, 1, tzinfo=UTC),
|
|
180
|
+
}
|
|
181
|
+
]
|
|
182
|
+
runner = CliRunner()
|
|
183
|
+
result = runner.invoke(main, ["terminate"], input="n\n")
|
|
184
|
+
assert result.exit_code == 0
|
|
185
|
+
assert "Cancelled" in result.output
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# ---------------------------------------------------------------------------
|
|
189
|
+
# list subcommand
|
|
190
|
+
# ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def test_list_help():
|
|
194
|
+
runner = CliRunner()
|
|
195
|
+
result = runner.invoke(main, ["list", "--help"])
|
|
196
|
+
assert result.exit_code == 0
|
|
197
|
+
assert "instance-types" in result.output
|
|
198
|
+
assert "amis" in result.output
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def test_list_instance_types_help():
|
|
202
|
+
runner = CliRunner()
|
|
203
|
+
result = runner.invoke(main, ["list", "instance-types", "--help"])
|
|
204
|
+
assert result.exit_code == 0
|
|
205
|
+
assert "--prefix" in result.output
|
|
206
|
+
assert "--region" in result.output
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def test_list_amis_help():
|
|
210
|
+
runner = CliRunner()
|
|
211
|
+
result = runner.invoke(main, ["list", "amis", "--help"])
|
|
212
|
+
assert result.exit_code == 0
|
|
213
|
+
assert "--filter" in result.output
|
|
214
|
+
assert "--region" in result.output
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
218
|
+
@patch("aws_bootstrap.cli.list_instance_types")
|
|
219
|
+
def test_list_instance_types_output(mock_list, mock_session):
|
|
220
|
+
mock_list.return_value = [
|
|
221
|
+
{
|
|
222
|
+
"InstanceType": "g4dn.xlarge",
|
|
223
|
+
"VCpuCount": 4,
|
|
224
|
+
"MemoryMiB": 16384,
|
|
225
|
+
"GpuSummary": "1x T4 (16384 MiB)",
|
|
226
|
+
},
|
|
227
|
+
]
|
|
228
|
+
runner = CliRunner()
|
|
229
|
+
result = runner.invoke(main, ["list", "instance-types"])
|
|
230
|
+
assert result.exit_code == 0
|
|
231
|
+
assert "g4dn.xlarge" in result.output
|
|
232
|
+
assert "16384" in result.output
|
|
233
|
+
assert "T4" in result.output
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
237
|
+
@patch("aws_bootstrap.cli.list_instance_types")
|
|
238
|
+
def test_list_instance_types_empty(mock_list, mock_session):
|
|
239
|
+
mock_list.return_value = []
|
|
240
|
+
runner = CliRunner()
|
|
241
|
+
result = runner.invoke(main, ["list", "instance-types", "--prefix", "zzz"])
|
|
242
|
+
assert result.exit_code == 0
|
|
243
|
+
assert "No instance types found" in result.output
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
247
|
+
@patch("aws_bootstrap.cli.list_amis")
|
|
248
|
+
def test_list_amis_output(mock_list, mock_session):
|
|
249
|
+
mock_list.return_value = [
|
|
250
|
+
{
|
|
251
|
+
"ImageId": "ami-abc123",
|
|
252
|
+
"Name": "Deep Learning AMI v42",
|
|
253
|
+
"CreationDate": "2025-06-01T00:00:00Z",
|
|
254
|
+
"Architecture": "x86_64",
|
|
255
|
+
},
|
|
256
|
+
]
|
|
257
|
+
runner = CliRunner()
|
|
258
|
+
result = runner.invoke(main, ["list", "amis"])
|
|
259
|
+
assert result.exit_code == 0
|
|
260
|
+
assert "ami-abc123" in result.output
|
|
261
|
+
assert "Deep Learning AMI v42" in result.output
|
|
262
|
+
assert "2025-06-01" in result.output
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
266
|
+
@patch("aws_bootstrap.cli.list_amis")
|
|
267
|
+
def test_list_amis_empty(mock_list, mock_session):
|
|
268
|
+
mock_list.return_value = []
|
|
269
|
+
runner = CliRunner()
|
|
270
|
+
result = runner.invoke(main, ["list", "amis", "--filter", "nonexistent*"])
|
|
271
|
+
assert result.exit_code == 0
|
|
272
|
+
assert "No AMIs found" in result.output
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# ---------------------------------------------------------------------------
|
|
276
|
+
# SSH config integration tests
|
|
277
|
+
# ---------------------------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@patch("aws_bootstrap.cli.add_ssh_host", return_value="aws-gpu1")
|
|
281
|
+
@patch("aws_bootstrap.cli.run_remote_setup", return_value=True)
|
|
282
|
+
@patch("aws_bootstrap.cli.wait_for_ssh", return_value=True)
|
|
283
|
+
@patch("aws_bootstrap.cli.wait_instance_ready")
|
|
284
|
+
@patch("aws_bootstrap.cli.launch_instance")
|
|
285
|
+
@patch("aws_bootstrap.cli.ensure_security_group", return_value="sg-123")
|
|
286
|
+
@patch("aws_bootstrap.cli.import_key_pair", return_value="aws-bootstrap-key")
|
|
287
|
+
@patch("aws_bootstrap.cli.get_latest_ami")
|
|
288
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
289
|
+
def test_launch_output_shows_ssh_alias(
|
|
290
|
+
mock_session, mock_ami, mock_import, mock_sg, mock_launch, mock_wait, mock_ssh, mock_setup, mock_add_ssh, tmp_path
|
|
291
|
+
):
|
|
292
|
+
mock_ami.return_value = {"ImageId": "ami-123", "Name": "TestAMI"}
|
|
293
|
+
mock_launch.return_value = {"InstanceId": "i-test123"}
|
|
294
|
+
mock_wait.return_value = {"PublicIpAddress": "1.2.3.4"}
|
|
295
|
+
|
|
296
|
+
key_path = tmp_path / "id_ed25519.pub"
|
|
297
|
+
key_path.write_text("ssh-ed25519 AAAA test@host")
|
|
298
|
+
|
|
299
|
+
runner = CliRunner()
|
|
300
|
+
result = runner.invoke(main, ["launch", "--key-path", str(key_path), "--no-setup"])
|
|
301
|
+
assert result.exit_code == 0
|
|
302
|
+
assert "ssh aws-gpu1" in result.output
|
|
303
|
+
assert "SSH alias: aws-gpu1" in result.output
|
|
304
|
+
mock_add_ssh.assert_called_once()
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
308
|
+
@patch("aws_bootstrap.cli.get_latest_ami")
|
|
309
|
+
@patch("aws_bootstrap.cli.import_key_pair", return_value="aws-bootstrap-key")
|
|
310
|
+
@patch("aws_bootstrap.cli.ensure_security_group", return_value="sg-123")
|
|
311
|
+
@patch("aws_bootstrap.cli.add_ssh_host")
|
|
312
|
+
def test_launch_dry_run_no_ssh_config(mock_add_ssh, mock_sg, mock_import, mock_ami, mock_session, tmp_path):
|
|
313
|
+
mock_ami.return_value = {"ImageId": "ami-123", "Name": "TestAMI"}
|
|
314
|
+
|
|
315
|
+
key_path = tmp_path / "id_ed25519.pub"
|
|
316
|
+
key_path.write_text("ssh-ed25519 AAAA test@host")
|
|
317
|
+
|
|
318
|
+
runner = CliRunner()
|
|
319
|
+
result = runner.invoke(main, ["launch", "--key-path", str(key_path), "--dry-run"])
|
|
320
|
+
assert result.exit_code == 0
|
|
321
|
+
mock_add_ssh.assert_not_called()
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
@patch("aws_bootstrap.cli.remove_ssh_host", return_value="aws-gpu1")
|
|
325
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
326
|
+
@patch("aws_bootstrap.cli.find_tagged_instances")
|
|
327
|
+
@patch("aws_bootstrap.cli.terminate_tagged_instances")
|
|
328
|
+
def test_terminate_removes_ssh_config(mock_terminate, mock_find, mock_session, mock_remove_ssh):
|
|
329
|
+
mock_find.return_value = [
|
|
330
|
+
{
|
|
331
|
+
"InstanceId": "i-abc123",
|
|
332
|
+
"Name": "test",
|
|
333
|
+
"State": "running",
|
|
334
|
+
"InstanceType": "g4dn.xlarge",
|
|
335
|
+
"PublicIp": "1.2.3.4",
|
|
336
|
+
"LaunchTime": datetime(2025, 1, 1, tzinfo=UTC),
|
|
337
|
+
}
|
|
338
|
+
]
|
|
339
|
+
mock_terminate.return_value = [
|
|
340
|
+
{
|
|
341
|
+
"InstanceId": "i-abc123",
|
|
342
|
+
"PreviousState": {"Name": "running"},
|
|
343
|
+
"CurrentState": {"Name": "shutting-down"},
|
|
344
|
+
}
|
|
345
|
+
]
|
|
346
|
+
runner = CliRunner()
|
|
347
|
+
result = runner.invoke(main, ["terminate", "--yes"])
|
|
348
|
+
assert result.exit_code == 0
|
|
349
|
+
assert "Removed SSH config alias: aws-gpu1" in result.output
|
|
350
|
+
mock_remove_ssh.assert_called_once_with("i-abc123")
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
@patch("aws_bootstrap.cli.list_ssh_hosts")
|
|
354
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
355
|
+
@patch("aws_bootstrap.cli.get_spot_price")
|
|
356
|
+
@patch("aws_bootstrap.cli.find_tagged_instances")
|
|
357
|
+
def test_status_shows_alias(mock_find, mock_spot_price, mock_session, mock_ssh_hosts):
|
|
358
|
+
mock_find.return_value = [
|
|
359
|
+
{
|
|
360
|
+
"InstanceId": "i-abc123",
|
|
361
|
+
"Name": "aws-bootstrap-g4dn.xlarge",
|
|
362
|
+
"State": "running",
|
|
363
|
+
"InstanceType": "g4dn.xlarge",
|
|
364
|
+
"PublicIp": "1.2.3.4",
|
|
365
|
+
"LaunchTime": datetime(2025, 1, 1, tzinfo=UTC),
|
|
366
|
+
"Lifecycle": "spot",
|
|
367
|
+
"AvailabilityZone": "us-west-2a",
|
|
368
|
+
}
|
|
369
|
+
]
|
|
370
|
+
mock_spot_price.return_value = 0.15
|
|
371
|
+
mock_ssh_hosts.return_value = {"i-abc123": "aws-gpu1"}
|
|
372
|
+
runner = CliRunner()
|
|
373
|
+
result = runner.invoke(main, ["status"])
|
|
374
|
+
assert result.exit_code == 0
|
|
375
|
+
assert "aws-gpu1" in result.output
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
@patch("aws_bootstrap.cli.list_ssh_hosts", return_value={})
|
|
379
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
380
|
+
@patch("aws_bootstrap.cli.get_spot_price")
|
|
381
|
+
@patch("aws_bootstrap.cli.find_tagged_instances")
|
|
382
|
+
def test_status_no_alias_graceful(mock_find, mock_spot_price, mock_session, mock_ssh_hosts):
|
|
383
|
+
mock_find.return_value = [
|
|
384
|
+
{
|
|
385
|
+
"InstanceId": "i-old999",
|
|
386
|
+
"Name": "aws-bootstrap-g4dn.xlarge",
|
|
387
|
+
"State": "running",
|
|
388
|
+
"InstanceType": "g4dn.xlarge",
|
|
389
|
+
"PublicIp": "9.8.7.6",
|
|
390
|
+
"LaunchTime": datetime(2025, 1, 1, tzinfo=UTC),
|
|
391
|
+
"Lifecycle": "spot",
|
|
392
|
+
"AvailabilityZone": "us-west-2a",
|
|
393
|
+
}
|
|
394
|
+
]
|
|
395
|
+
mock_spot_price.return_value = 0.15
|
|
396
|
+
runner = CliRunner()
|
|
397
|
+
result = runner.invoke(main, ["status"])
|
|
398
|
+
assert result.exit_code == 0
|
|
399
|
+
assert "i-old999" in result.output
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
# ---------------------------------------------------------------------------
|
|
403
|
+
# --gpu flag tests
|
|
404
|
+
# ---------------------------------------------------------------------------
|
|
405
|
+
|
|
406
|
+
_RUNNING_INSTANCE = {
|
|
407
|
+
"InstanceId": "i-abc123",
|
|
408
|
+
"Name": "aws-bootstrap-g4dn.xlarge",
|
|
409
|
+
"State": "running",
|
|
410
|
+
"InstanceType": "g4dn.xlarge",
|
|
411
|
+
"PublicIp": "1.2.3.4",
|
|
412
|
+
"LaunchTime": datetime(2025, 1, 1, tzinfo=UTC),
|
|
413
|
+
"Lifecycle": "spot",
|
|
414
|
+
"AvailabilityZone": "us-west-2a",
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
_SAMPLE_GPU_INFO = GpuInfo(
|
|
418
|
+
driver_version="560.35.03",
|
|
419
|
+
cuda_driver_version="13.0",
|
|
420
|
+
cuda_toolkit_version="12.8",
|
|
421
|
+
gpu_name="Tesla T4",
|
|
422
|
+
compute_capability="7.5",
|
|
423
|
+
architecture="Turing",
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def test_status_help_shows_gpu_flag():
|
|
428
|
+
runner = CliRunner()
|
|
429
|
+
result = runner.invoke(main, ["status", "--help"])
|
|
430
|
+
assert result.exit_code == 0
|
|
431
|
+
assert "--gpu" in result.output
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
@patch("aws_bootstrap.cli.query_gpu_info", return_value=_SAMPLE_GPU_INFO)
|
|
435
|
+
@patch("aws_bootstrap.cli.get_ssh_host_details")
|
|
436
|
+
@patch("aws_bootstrap.cli.list_ssh_hosts", return_value={"i-abc123": "aws-gpu1"})
|
|
437
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
438
|
+
@patch("aws_bootstrap.cli.get_spot_price", return_value=0.15)
|
|
439
|
+
@patch("aws_bootstrap.cli.find_tagged_instances")
|
|
440
|
+
def test_status_gpu_shows_info(mock_find, mock_spot, mock_session, mock_ssh_hosts, mock_details, mock_gpu):
|
|
441
|
+
mock_find.return_value = [_RUNNING_INSTANCE]
|
|
442
|
+
mock_details.return_value = SSHHostDetails(
|
|
443
|
+
hostname="1.2.3.4", user="ubuntu", identity_file=Path("/home/user/.ssh/id_ed25519")
|
|
444
|
+
)
|
|
445
|
+
runner = CliRunner()
|
|
446
|
+
result = runner.invoke(main, ["status", "--gpu"])
|
|
447
|
+
assert result.exit_code == 0
|
|
448
|
+
assert "Tesla T4 (Turing)" in result.output
|
|
449
|
+
assert "12.8" in result.output
|
|
450
|
+
assert "driver supports up to 13.0" in result.output
|
|
451
|
+
assert "560.35.03" in result.output
|
|
452
|
+
mock_gpu.assert_called_once()
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
@patch("aws_bootstrap.cli.query_gpu_info", return_value=None)
|
|
456
|
+
@patch("aws_bootstrap.cli.get_ssh_host_details")
|
|
457
|
+
@patch("aws_bootstrap.cli.list_ssh_hosts", return_value={})
|
|
458
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
459
|
+
@patch("aws_bootstrap.cli.get_spot_price", return_value=0.15)
|
|
460
|
+
@patch("aws_bootstrap.cli.find_tagged_instances")
|
|
461
|
+
def test_status_gpu_ssh_fails_gracefully(mock_find, mock_spot, mock_session, mock_ssh_hosts, mock_details, mock_gpu):
|
|
462
|
+
mock_find.return_value = [_RUNNING_INSTANCE]
|
|
463
|
+
mock_details.return_value = SSHHostDetails(
|
|
464
|
+
hostname="1.2.3.4", user="ubuntu", identity_file=Path("/home/user/.ssh/id_ed25519")
|
|
465
|
+
)
|
|
466
|
+
runner = CliRunner()
|
|
467
|
+
result = runner.invoke(main, ["status", "--gpu"])
|
|
468
|
+
assert result.exit_code == 0
|
|
469
|
+
assert "unavailable" in result.output
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
@patch("aws_bootstrap.cli.query_gpu_info", return_value=_SAMPLE_GPU_INFO)
|
|
473
|
+
@patch("aws_bootstrap.cli.get_ssh_host_details", return_value=None)
|
|
474
|
+
@patch("aws_bootstrap.cli.list_ssh_hosts", return_value={})
|
|
475
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
476
|
+
@patch("aws_bootstrap.cli.get_spot_price", return_value=0.15)
|
|
477
|
+
@patch("aws_bootstrap.cli.find_tagged_instances")
|
|
478
|
+
def test_status_gpu_no_ssh_config_uses_defaults(
|
|
479
|
+
mock_find, mock_spot, mock_session, mock_ssh_hosts, mock_details, mock_gpu
|
|
480
|
+
):
|
|
481
|
+
mock_find.return_value = [_RUNNING_INSTANCE]
|
|
482
|
+
runner = CliRunner()
|
|
483
|
+
result = runner.invoke(main, ["status", "--gpu"])
|
|
484
|
+
assert result.exit_code == 0
|
|
485
|
+
# Should have been called with the instance IP and default user/key
|
|
486
|
+
mock_gpu.assert_called_once()
|
|
487
|
+
call_args = mock_gpu.call_args
|
|
488
|
+
assert call_args[0][0] == "1.2.3.4"
|
|
489
|
+
assert call_args[0][1] == "ubuntu"
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
@patch("aws_bootstrap.cli.query_gpu_info")
|
|
493
|
+
@patch("aws_bootstrap.cli.get_ssh_host_details")
|
|
494
|
+
@patch("aws_bootstrap.cli.list_ssh_hosts", return_value={})
|
|
495
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
496
|
+
@patch("aws_bootstrap.cli.find_tagged_instances")
|
|
497
|
+
def test_status_gpu_skips_non_running(mock_find, mock_session, mock_ssh_hosts, mock_details, mock_gpu):
|
|
498
|
+
mock_find.return_value = [
|
|
499
|
+
{
|
|
500
|
+
"InstanceId": "i-stopped",
|
|
501
|
+
"Name": "aws-bootstrap-g4dn.xlarge",
|
|
502
|
+
"State": "stopped",
|
|
503
|
+
"InstanceType": "g4dn.xlarge",
|
|
504
|
+
"PublicIp": "",
|
|
505
|
+
"LaunchTime": datetime(2025, 1, 1, tzinfo=UTC),
|
|
506
|
+
"Lifecycle": "on-demand",
|
|
507
|
+
"AvailabilityZone": "us-west-2a",
|
|
508
|
+
}
|
|
509
|
+
]
|
|
510
|
+
runner = CliRunner()
|
|
511
|
+
result = runner.invoke(main, ["status", "--gpu"])
|
|
512
|
+
assert result.exit_code == 0
|
|
513
|
+
mock_gpu.assert_not_called()
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
@patch("aws_bootstrap.cli.query_gpu_info")
|
|
517
|
+
@patch("aws_bootstrap.cli.get_ssh_host_details")
|
|
518
|
+
@patch("aws_bootstrap.cli.list_ssh_hosts", return_value={"i-abc123": "aws-gpu1"})
|
|
519
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
520
|
+
@patch("aws_bootstrap.cli.get_spot_price", return_value=0.15)
|
|
521
|
+
@patch("aws_bootstrap.cli.find_tagged_instances")
|
|
522
|
+
def test_status_without_gpu_flag_no_ssh(mock_find, mock_spot, mock_session, mock_ssh_hosts, mock_details, mock_gpu):
|
|
523
|
+
mock_find.return_value = [_RUNNING_INSTANCE]
|
|
524
|
+
runner = CliRunner()
|
|
525
|
+
result = runner.invoke(main, ["status"])
|
|
526
|
+
assert result.exit_code == 0
|
|
527
|
+
mock_gpu.assert_not_called()
|
|
528
|
+
mock_details.assert_not_called()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Tests for LaunchConfig defaults and overrides."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from aws_bootstrap.config import LaunchConfig
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_defaults():
|
|
10
|
+
config = LaunchConfig()
|
|
11
|
+
assert config.instance_type == "g4dn.xlarge"
|
|
12
|
+
assert config.region == "us-west-2"
|
|
13
|
+
assert config.spot is True
|
|
14
|
+
assert config.volume_size == 100
|
|
15
|
+
assert config.ssh_user == "ubuntu"
|
|
16
|
+
assert config.key_name == "aws-bootstrap-key"
|
|
17
|
+
assert config.security_group == "aws-bootstrap-ssh"
|
|
18
|
+
assert config.tag_value == "aws-bootstrap-g4dn"
|
|
19
|
+
assert config.run_setup is True
|
|
20
|
+
assert config.dry_run is False
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_overrides():
|
|
24
|
+
config = LaunchConfig(
|
|
25
|
+
instance_type="g5.xlarge",
|
|
26
|
+
region="us-east-1",
|
|
27
|
+
spot=False,
|
|
28
|
+
volume_size=200,
|
|
29
|
+
key_path=Path("/tmp/test.pub"),
|
|
30
|
+
)
|
|
31
|
+
assert config.instance_type == "g5.xlarge"
|
|
32
|
+
assert config.region == "us-east-1"
|
|
33
|
+
assert config.spot is False
|
|
34
|
+
assert config.volume_size == 200
|
|
35
|
+
assert config.key_path == Path("/tmp/test.pub")
|