##// END OF EJS Templates
automation: transition to Windows Server 2022...
Gregory Szorc -
r50329:f1dcddb7 stable
parent child Browse files
Show More
@@ -1,1325 +1,1330 b''
1 1 # aws.py - Automation code for Amazon Web Services
2 2 #
3 3 # Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 # no-check-code because Python 3 native.
9 9
10 10 import contextlib
11 11 import copy
12 12 import hashlib
13 13 import json
14 14 import os
15 15 import pathlib
16 16 import subprocess
17 17 import time
18 18
19 19 import boto3
20 20 import botocore.exceptions
21 21
22 22 from .linux import BOOTSTRAP_DEBIAN
23 23 from .ssh import (
24 24 exec_command as ssh_exec_command,
25 25 wait_for_ssh,
26 26 )
27 27 from .winrm import (
28 28 run_powershell,
29 29 wait_for_winrm,
30 30 )
31 31
32 32
33 33 SOURCE_ROOT = pathlib.Path(
34 34 os.path.abspath(__file__)
35 35 ).parent.parent.parent.parent
36 36
37 37 INSTALL_WINDOWS_DEPENDENCIES = (
38 38 SOURCE_ROOT / 'contrib' / 'install-windows-dependencies.ps1'
39 39 )
40 40
41 41
42 42 INSTANCE_TYPES_WITH_STORAGE = {
43 43 'c5d',
44 44 'd2',
45 45 'h1',
46 46 'i3',
47 47 'm5ad',
48 48 'm5d',
49 49 'r5d',
50 50 'r5ad',
51 51 'x1',
52 52 'z1d',
53 53 }
54 54
55 55
56 56 AMAZON_ACCOUNT_ID = '801119661308'
57 57 DEBIAN_ACCOUNT_ID = '379101102735'
58 58 DEBIAN_ACCOUNT_ID_2 = '136693071363'
59 59 UBUNTU_ACCOUNT_ID = '099720109477'
60 60
61 61
62 WINDOWS_BASE_IMAGE_NAME = 'Windows_Server-2019-English-Full-Base-*'
62 WINDOWS_BASE_IMAGE_NAME = 'Windows_Server-2022-English-Full-Base-*'
63 63
64 64
65 65 KEY_PAIRS = {
66 66 'automation',
67 67 }
68 68
69 69
70 70 SECURITY_GROUPS = {
71 71 'linux-dev-1': {
72 72 'description': 'Mercurial Linux instances that perform build/test automation',
73 73 'ingress': [
74 74 {
75 75 'FromPort': 22,
76 76 'ToPort': 22,
77 77 'IpProtocol': 'tcp',
78 78 'IpRanges': [
79 79 {
80 80 'CidrIp': '0.0.0.0/0',
81 81 'Description': 'SSH from entire Internet',
82 82 },
83 83 ],
84 84 },
85 85 ],
86 86 },
87 87 'windows-dev-1': {
88 88 'description': 'Mercurial Windows instances that perform build automation',
89 89 'ingress': [
90 90 {
91 91 'FromPort': 22,
92 92 'ToPort': 22,
93 93 'IpProtocol': 'tcp',
94 94 'IpRanges': [
95 95 {
96 96 'CidrIp': '0.0.0.0/0',
97 97 'Description': 'SSH from entire Internet',
98 98 },
99 99 ],
100 100 },
101 101 {
102 102 'FromPort': 3389,
103 103 'ToPort': 3389,
104 104 'IpProtocol': 'tcp',
105 105 'IpRanges': [
106 106 {
107 107 'CidrIp': '0.0.0.0/0',
108 108 'Description': 'RDP from entire Internet',
109 109 },
110 110 ],
111 111 },
112 112 {
113 113 'FromPort': 5985,
114 114 'ToPort': 5986,
115 115 'IpProtocol': 'tcp',
116 116 'IpRanges': [
117 117 {
118 118 'CidrIp': '0.0.0.0/0',
119 119 'Description': 'PowerShell Remoting (Windows Remote Management)',
120 120 },
121 121 ],
122 122 },
123 123 ],
124 124 },
125 125 }
126 126
127 127
128 128 IAM_ROLES = {
129 129 'ephemeral-ec2-role-1': {
130 130 'description': 'Mercurial temporary EC2 instances',
131 131 'policy_arns': [
132 132 'arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM',
133 133 ],
134 134 },
135 135 }
136 136
137 137
138 138 ASSUME_ROLE_POLICY_DOCUMENT = '''
139 139 {
140 140 "Version": "2012-10-17",
141 141 "Statement": [
142 142 {
143 143 "Effect": "Allow",
144 144 "Principal": {
145 145 "Service": "ec2.amazonaws.com"
146 146 },
147 147 "Action": "sts:AssumeRole"
148 148 }
149 149 ]
150 150 }
151 151 '''.strip()
152 152
153 153
154 154 IAM_INSTANCE_PROFILES = {
155 155 'ephemeral-ec2-1': {
156 156 'roles': [
157 157 'ephemeral-ec2-role-1',
158 158 ],
159 159 }
160 160 }
161 161
162 162
163 163 # User Data for Windows EC2 instance. Mainly used to set the password
164 164 # and configure WinRM.
165 165 # Inspired by the User Data script used by Packer
166 166 # (from https://www.packer.io/intro/getting-started/build-image.html).
167 167 WINDOWS_USER_DATA = r'''
168 168 <powershell>
169 169
170 170 # TODO enable this once we figure out what is failing.
171 171 #$ErrorActionPreference = "stop"
172 172
173 173 # Set administrator password
174 174 net user Administrator "%s"
175 175 wmic useraccount where "name='Administrator'" set PasswordExpires=FALSE
176 176
177 # And set it via EC2Launch so it persists across reboots.
178 $config = & $env:ProgramFiles\Amazon\EC2Launch\EC2Launch.exe get-agent-config --format json | ConvertFrom-Json
179 $config | ConvertTo-Json -Depth 6 | Out-File -encoding UTF8 $env:ProgramData/Amazon/EC2Launch/config/agent-config.yml
180 $setAdminAccount = @"
181 {
182 "task": "setAdminAccount",
183 "inputs": {
184 "password": {
185 "type": "static",
186 "data": "%s"
187 }
188 }
189 }
190 "@
191 $config.config | %%{if($_.stage -eq 'preReady'){$_.tasks += (ConvertFrom-Json -InputObject $setAdminAccount)}}
192 $config | ConvertTo-Json -Depth 6 | Out-File -encoding UTF8 $env:ProgramData/Amazon/EC2Launch/config/agent-config.yml
193
177 194 # First, make sure WinRM can't be connected to
178 195 netsh advfirewall firewall set rule name="Windows Remote Management (HTTP-In)" new enable=yes action=block
179 196
180 197 # Delete any existing WinRM listeners
181 198 winrm delete winrm/config/listener?Address=*+Transport=HTTP 2>$Null
182 199 winrm delete winrm/config/listener?Address=*+Transport=HTTPS 2>$Null
183 200
184 201 # Create a new WinRM listener and configure
185 202 winrm create winrm/config/listener?Address=*+Transport=HTTP
186 203 winrm set winrm/config/winrs '@{MaxMemoryPerShellMB="0"}'
187 204 winrm set winrm/config '@{MaxTimeoutms="7200000"}'
188 205 winrm set winrm/config/service '@{AllowUnencrypted="true"}'
189 206 winrm set winrm/config/service '@{MaxConcurrentOperationsPerUser="12000"}'
190 207 winrm set winrm/config/service/auth '@{Basic="true"}'
191 208 winrm set winrm/config/client/auth '@{Basic="true"}'
192 209
193 210 # Configure UAC to allow privilege elevation in remote shells
194 211 $Key = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
195 212 $Setting = 'LocalAccountTokenFilterPolicy'
196 213 Set-ItemProperty -Path $Key -Name $Setting -Value 1 -Force
197 214
198 215 # Avoid long usernames in the temp directory path because the '~' causes extra quoting in ssh output
199 216 [System.Environment]::SetEnvironmentVariable('TMP', 'C:\Temp', [System.EnvironmentVariableTarget]::User)
200 217 [System.Environment]::SetEnvironmentVariable('TEMP', 'C:\Temp', [System.EnvironmentVariableTarget]::User)
201 218
202 219 # Configure and restart the WinRM Service; Enable the required firewall exception
203 220 Stop-Service -Name WinRM
204 221 Set-Service -Name WinRM -StartupType Automatic
205 222 netsh advfirewall firewall set rule name="Windows Remote Management (HTTP-In)" new action=allow localip=any remoteip=any
206 223 Start-Service -Name WinRM
207 224
208 225 # Disable firewall on private network interfaces so prompts don't appear.
209 226 Set-NetFirewallProfile -Name private -Enabled false
210 227 </powershell>
211 228 '''.lstrip()
212 229
213 230
214 231 WINDOWS_BOOTSTRAP_POWERSHELL = '''
215 232 Write-Output "installing PowerShell dependencies"
216 233 Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force
217 234 Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
218 235 Install-Module -Name OpenSSHUtils -RequiredVersion 0.0.2.0
219 236
220 237 Write-Output "installing OpenSSL server"
221 238 Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
222 239 # Various tools will attempt to use older versions of .NET. So we enable
223 240 # the feature that provides them so it doesn't have to be auto-enabled
224 241 # later.
225 242 Write-Output "enabling .NET Framework feature"
226 243 Install-WindowsFeature -Name Net-Framework-Core
227 244 '''
228 245
229 246
230 247 class AWSConnection:
231 248 """Manages the state of a connection with AWS."""
232 249
233 250 def __init__(self, automation, region: str, ensure_ec2_state: bool = True):
234 251 self.automation = automation
235 252 self.local_state_path = automation.state_path
236 253
237 254 self.prefix = 'hg-'
238 255
239 256 self.session = boto3.session.Session(region_name=region)
240 257 self.ec2client = self.session.client('ec2')
241 258 self.ec2resource = self.session.resource('ec2')
242 259 self.iamclient = self.session.client('iam')
243 260 self.iamresource = self.session.resource('iam')
244 261 self.security_groups = {}
245 262
246 263 if ensure_ec2_state:
247 264 ensure_key_pairs(automation.state_path, self.ec2resource)
248 265 self.security_groups = ensure_security_groups(self.ec2resource)
249 266 ensure_iam_state(self.iamclient, self.iamresource)
250 267
251 268 def key_pair_path_private(self, name):
252 269 """Path to a key pair private key file."""
253 270 return self.local_state_path / 'keys' / ('keypair-%s' % name)
254 271
255 272 def key_pair_path_public(self, name):
256 273 return self.local_state_path / 'keys' / ('keypair-%s.pub' % name)
257 274
258 275
259 276 def rsa_key_fingerprint(p: pathlib.Path):
260 277 """Compute the fingerprint of an RSA private key."""
261 278
262 279 # TODO use rsa package.
263 280 res = subprocess.run(
264 281 [
265 282 'openssl',
266 283 'pkcs8',
267 284 '-in',
268 285 str(p),
269 286 '-nocrypt',
270 287 '-topk8',
271 288 '-outform',
272 289 'DER',
273 290 ],
274 291 capture_output=True,
275 292 check=True,
276 293 )
277 294
278 295 sha1 = hashlib.sha1(res.stdout).hexdigest()
279 296 return ':'.join(a + b for a, b in zip(sha1[::2], sha1[1::2]))
280 297
281 298
282 299 def ensure_key_pairs(state_path: pathlib.Path, ec2resource, prefix='hg-'):
283 300 remote_existing = {}
284 301
285 302 for kpi in ec2resource.key_pairs.all():
286 303 if kpi.name.startswith(prefix):
287 304 remote_existing[kpi.name[len(prefix) :]] = kpi.key_fingerprint
288 305
289 306 # Validate that we have these keys locally.
290 307 key_path = state_path / 'keys'
291 308 key_path.mkdir(exist_ok=True, mode=0o700)
292 309
293 310 def remove_remote(name):
294 311 print('deleting key pair %s' % name)
295 312 key = ec2resource.KeyPair(name)
296 313 key.delete()
297 314
298 315 def remove_local(name):
299 316 pub_full = key_path / ('keypair-%s.pub' % name)
300 317 priv_full = key_path / ('keypair-%s' % name)
301 318
302 319 print('removing %s' % pub_full)
303 320 pub_full.unlink()
304 321 print('removing %s' % priv_full)
305 322 priv_full.unlink()
306 323
307 324 local_existing = {}
308 325
309 326 for f in sorted(os.listdir(key_path)):
310 327 if not f.startswith('keypair-') or not f.endswith('.pub'):
311 328 continue
312 329
313 330 name = f[len('keypair-') : -len('.pub')]
314 331
315 332 pub_full = key_path / f
316 333 priv_full = key_path / ('keypair-%s' % name)
317 334
318 335 with open(pub_full, 'r', encoding='ascii') as fh:
319 336 data = fh.read()
320 337
321 338 if not data.startswith('ssh-rsa '):
322 339 print(
323 340 'unexpected format for key pair file: %s; removing' % pub_full
324 341 )
325 342 pub_full.unlink()
326 343 priv_full.unlink()
327 344 continue
328 345
329 346 local_existing[name] = rsa_key_fingerprint(priv_full)
330 347
331 348 for name in sorted(set(remote_existing) | set(local_existing)):
332 349 if name not in local_existing:
333 350 actual = '%s%s' % (prefix, name)
334 351 print('remote key %s does not exist locally' % name)
335 352 remove_remote(actual)
336 353 del remote_existing[name]
337 354
338 355 elif name not in remote_existing:
339 356 print('local key %s does not exist remotely' % name)
340 357 remove_local(name)
341 358 del local_existing[name]
342 359
343 360 elif remote_existing[name] != local_existing[name]:
344 361 print(
345 362 'key fingerprint mismatch for %s; '
346 363 'removing from local and remote' % name
347 364 )
348 365 remove_local(name)
349 366 remove_remote('%s%s' % (prefix, name))
350 367 del local_existing[name]
351 368 del remote_existing[name]
352 369
353 370 missing = KEY_PAIRS - set(remote_existing)
354 371
355 372 for name in sorted(missing):
356 373 actual = '%s%s' % (prefix, name)
357 374 print('creating key pair %s' % actual)
358 375
359 376 priv_full = key_path / ('keypair-%s' % name)
360 377 pub_full = key_path / ('keypair-%s.pub' % name)
361 378
362 379 kp = ec2resource.create_key_pair(KeyName=actual)
363 380
364 381 with priv_full.open('w', encoding='ascii') as fh:
365 382 fh.write(kp.key_material)
366 383 fh.write('\n')
367 384
368 385 priv_full.chmod(0o0600)
369 386
370 387 # SSH public key can be extracted via `ssh-keygen`.
371 388 with pub_full.open('w', encoding='ascii') as fh:
372 389 subprocess.run(
373 390 ['ssh-keygen', '-y', '-f', str(priv_full)],
374 391 stdout=fh,
375 392 check=True,
376 393 )
377 394
378 395 pub_full.chmod(0o0600)
379 396
380 397
381 398 def delete_instance_profile(profile):
382 399 for role in profile.roles:
383 400 print(
384 401 'removing role %s from instance profile %s'
385 402 % (role.name, profile.name)
386 403 )
387 404 profile.remove_role(RoleName=role.name)
388 405
389 406 print('deleting instance profile %s' % profile.name)
390 407 profile.delete()
391 408
392 409
393 410 def ensure_iam_state(iamclient, iamresource, prefix='hg-'):
394 411 """Ensure IAM state is in sync with our canonical definition."""
395 412
396 413 remote_profiles = {}
397 414
398 415 for profile in iamresource.instance_profiles.all():
399 416 if profile.name.startswith(prefix):
400 417 remote_profiles[profile.name[len(prefix) :]] = profile
401 418
402 419 for name in sorted(set(remote_profiles) - set(IAM_INSTANCE_PROFILES)):
403 420 delete_instance_profile(remote_profiles[name])
404 421 del remote_profiles[name]
405 422
406 423 remote_roles = {}
407 424
408 425 for role in iamresource.roles.all():
409 426 if role.name.startswith(prefix):
410 427 remote_roles[role.name[len(prefix) :]] = role
411 428
412 429 for name in sorted(set(remote_roles) - set(IAM_ROLES)):
413 430 role = remote_roles[name]
414 431
415 432 print('removing role %s' % role.name)
416 433 role.delete()
417 434 del remote_roles[name]
418 435
419 436 # We've purged remote state that doesn't belong. Create missing
420 437 # instance profiles and roles.
421 438 for name in sorted(set(IAM_INSTANCE_PROFILES) - set(remote_profiles)):
422 439 actual = '%s%s' % (prefix, name)
423 440 print('creating IAM instance profile %s' % actual)
424 441
425 442 profile = iamresource.create_instance_profile(
426 443 InstanceProfileName=actual
427 444 )
428 445 remote_profiles[name] = profile
429 446
430 447 waiter = iamclient.get_waiter('instance_profile_exists')
431 448 waiter.wait(InstanceProfileName=actual)
432 449 print('IAM instance profile %s is available' % actual)
433 450
434 451 for name in sorted(set(IAM_ROLES) - set(remote_roles)):
435 452 entry = IAM_ROLES[name]
436 453
437 454 actual = '%s%s' % (prefix, name)
438 455 print('creating IAM role %s' % actual)
439 456
440 457 role = iamresource.create_role(
441 458 RoleName=actual,
442 459 Description=entry['description'],
443 460 AssumeRolePolicyDocument=ASSUME_ROLE_POLICY_DOCUMENT,
444 461 )
445 462
446 463 waiter = iamclient.get_waiter('role_exists')
447 464 waiter.wait(RoleName=actual)
448 465 print('IAM role %s is available' % actual)
449 466
450 467 remote_roles[name] = role
451 468
452 469 for arn in entry['policy_arns']:
453 470 print('attaching policy %s to %s' % (arn, role.name))
454 471 role.attach_policy(PolicyArn=arn)
455 472
456 473 # Now reconcile state of profiles.
457 474 for name, meta in sorted(IAM_INSTANCE_PROFILES.items()):
458 475 profile = remote_profiles[name]
459 476 wanted = {'%s%s' % (prefix, role) for role in meta['roles']}
460 477 have = {role.name for role in profile.roles}
461 478
462 479 for role in sorted(have - wanted):
463 480 print('removing role %s from %s' % (role, profile.name))
464 481 profile.remove_role(RoleName=role)
465 482
466 483 for role in sorted(wanted - have):
467 484 print('adding role %s to %s' % (role, profile.name))
468 485 profile.add_role(RoleName=role)
469 486
470 487
471 488 def find_image(ec2resource, owner_id, name, reverse_sort_field=None):
472 489 """Find an AMI by its owner ID and name."""
473 490
474 491 images = ec2resource.images.filter(
475 492 Filters=[
476 493 {
477 494 'Name': 'owner-id',
478 495 'Values': [owner_id],
479 496 },
480 497 {
481 498 'Name': 'state',
482 499 'Values': ['available'],
483 500 },
484 501 {
485 502 'Name': 'image-type',
486 503 'Values': ['machine'],
487 504 },
488 505 {
489 506 'Name': 'name',
490 507 'Values': [name],
491 508 },
492 509 ]
493 510 )
494 511
495 512 if reverse_sort_field:
496 513 images = sorted(
497 514 images,
498 515 key=lambda image: getattr(image, reverse_sort_field),
499 516 reverse=True,
500 517 )
501 518
502 519 for image in images:
503 520 return image
504 521
505 522 raise Exception('unable to find image for %s' % name)
506 523
507 524
508 525 def ensure_security_groups(ec2resource, prefix='hg-'):
509 526 """Ensure all necessary Mercurial security groups are present.
510 527
511 528 All security groups are prefixed with ``hg-`` by default. Any security
512 529 groups having this prefix but aren't in our list are deleted.
513 530 """
514 531 existing = {}
515 532
516 533 for group in ec2resource.security_groups.all():
517 534 if group.group_name.startswith(prefix):
518 535 existing[group.group_name[len(prefix) :]] = group
519 536
520 537 purge = set(existing) - set(SECURITY_GROUPS)
521 538
522 539 for name in sorted(purge):
523 540 group = existing[name]
524 541 print('removing legacy security group: %s' % group.group_name)
525 542 group.delete()
526 543
527 544 security_groups = {}
528 545
529 546 for name, group in sorted(SECURITY_GROUPS.items()):
530 547 if name in existing:
531 548 security_groups[name] = existing[name]
532 549 continue
533 550
534 551 actual = '%s%s' % (prefix, name)
535 552 print('adding security group %s' % actual)
536 553
537 554 group_res = ec2resource.create_security_group(
538 555 Description=group['description'],
539 556 GroupName=actual,
540 557 )
541 558
542 559 group_res.authorize_ingress(
543 560 IpPermissions=group['ingress'],
544 561 )
545 562
546 563 security_groups[name] = group_res
547 564
548 565 return security_groups
549 566
550 567
551 568 def terminate_ec2_instances(ec2resource, prefix='hg-'):
552 569 """Terminate all EC2 instances managed by us."""
553 570 waiting = []
554 571
555 572 for instance in ec2resource.instances.all():
556 573 if instance.state['Name'] == 'terminated':
557 574 continue
558 575
559 576 for tag in instance.tags or []:
560 577 if tag['Key'] == 'Name' and tag['Value'].startswith(prefix):
561 578 print('terminating %s' % instance.id)
562 579 instance.terminate()
563 580 waiting.append(instance)
564 581
565 582 for instance in waiting:
566 583 instance.wait_until_terminated()
567 584
568 585
569 586 def remove_resources(c, prefix='hg-'):
570 587 """Purge all of our resources in this EC2 region."""
571 588 ec2resource = c.ec2resource
572 589 iamresource = c.iamresource
573 590
574 591 terminate_ec2_instances(ec2resource, prefix=prefix)
575 592
576 593 for image in ec2resource.images.filter(Owners=['self']):
577 594 if image.name.startswith(prefix):
578 595 remove_ami(ec2resource, image)
579 596
580 597 for group in ec2resource.security_groups.all():
581 598 if group.group_name.startswith(prefix):
582 599 print('removing security group %s' % group.group_name)
583 600 group.delete()
584 601
585 602 for profile in iamresource.instance_profiles.all():
586 603 if profile.name.startswith(prefix):
587 604 delete_instance_profile(profile)
588 605
589 606 for role in iamresource.roles.all():
590 607 if role.name.startswith(prefix):
591 608 for p in role.attached_policies.all():
592 609 print('detaching policy %s from %s' % (p.arn, role.name))
593 610 role.detach_policy(PolicyArn=p.arn)
594 611
595 612 print('removing role %s' % role.name)
596 613 role.delete()
597 614
598 615
599 616 def wait_for_ip_addresses(instances):
600 617 """Wait for the public IP addresses of an iterable of instances."""
601 618 for instance in instances:
602 619 while True:
603 620 if not instance.public_ip_address:
604 621 time.sleep(2)
605 622 instance.reload()
606 623 continue
607 624
608 625 print(
609 626 'public IP address for %s: %s'
610 627 % (instance.id, instance.public_ip_address)
611 628 )
612 629 break
613 630
614 631
615 632 def remove_ami(ec2resource, image):
616 633 """Remove an AMI and its underlying snapshots."""
617 634 snapshots = []
618 635
619 636 for device in image.block_device_mappings:
620 637 if 'Ebs' in device:
621 638 snapshots.append(ec2resource.Snapshot(device['Ebs']['SnapshotId']))
622 639
623 640 print('deregistering %s' % image.id)
624 641 image.deregister()
625 642
626 643 for snapshot in snapshots:
627 644 print('deleting snapshot %s' % snapshot.id)
628 645 snapshot.delete()
629 646
630 647
631 648 def wait_for_ssm(ssmclient, instances):
632 649 """Wait for SSM to come online for an iterable of instance IDs."""
633 650 while True:
634 651 res = ssmclient.describe_instance_information(
635 652 Filters=[
636 653 {
637 654 'Key': 'InstanceIds',
638 655 'Values': [i.id for i in instances],
639 656 },
640 657 ],
641 658 )
642 659
643 660 available = len(res['InstanceInformationList'])
644 661 wanted = len(instances)
645 662
646 663 print('%d/%d instances available in SSM' % (available, wanted))
647 664
648 665 if available == wanted:
649 666 return
650 667
651 668 time.sleep(2)
652 669
653 670
654 671 def run_ssm_command(ssmclient, instances, document_name, parameters):
655 672 """Run a PowerShell script on an EC2 instance."""
656 673
657 674 res = ssmclient.send_command(
658 675 InstanceIds=[i.id for i in instances],
659 676 DocumentName=document_name,
660 677 Parameters=parameters,
661 678 CloudWatchOutputConfig={
662 679 'CloudWatchOutputEnabled': True,
663 680 },
664 681 )
665 682
666 683 command_id = res['Command']['CommandId']
667 684
668 685 for instance in instances:
669 686 while True:
670 687 try:
671 688 res = ssmclient.get_command_invocation(
672 689 CommandId=command_id,
673 690 InstanceId=instance.id,
674 691 )
675 692 except botocore.exceptions.ClientError as e:
676 693 if e.response['Error']['Code'] == 'InvocationDoesNotExist':
677 694 print('could not find SSM command invocation; waiting')
678 695 time.sleep(1)
679 696 continue
680 697 else:
681 698 raise
682 699
683 700 if res['Status'] == 'Success':
684 701 break
685 702 elif res['Status'] in ('Pending', 'InProgress', 'Delayed'):
686 703 time.sleep(2)
687 704 else:
688 705 raise Exception(
689 706 'command failed on %s: %s' % (instance.id, res['Status'])
690 707 )
691 708
692 709
693 710 @contextlib.contextmanager
694 711 def temporary_ec2_instances(ec2resource, config):
695 712 """Create temporary EC2 instances.
696 713
697 714 This is a proxy to ``ec2client.run_instances(**config)`` that takes care of
698 715 managing the lifecycle of the instances.
699 716
700 717 When the context manager exits, the instances are terminated.
701 718
702 719 The context manager evaluates to the list of data structures
703 720 describing each created instance. The instances may not be available
704 721 for work immediately: it is up to the caller to wait for the instance
705 722 to start responding.
706 723 """
707 724
708 725 ids = None
709 726
710 727 try:
711 728 res = ec2resource.create_instances(**config)
712 729
713 730 ids = [i.id for i in res]
714 731 print('started instances: %s' % ' '.join(ids))
715 732
716 733 yield res
717 734 finally:
718 735 if ids:
719 736 print('terminating instances: %s' % ' '.join(ids))
720 737 for instance in res:
721 738 instance.terminate()
722 739 print('terminated %d instances' % len(ids))
723 740
724 741
725 742 @contextlib.contextmanager
726 743 def create_temp_windows_ec2_instances(
727 744 c: AWSConnection, config, bootstrap: bool = False
728 745 ):
729 746 """Create temporary Windows EC2 instances.
730 747
731 748 This is a higher-level wrapper around ``create_temp_ec2_instances()`` that
732 749 configures the Windows instance for Windows Remote Management. The emitted
733 750 instances will have a ``winrm_client`` attribute containing a
734 751 ``pypsrp.client.Client`` instance bound to the instance.
735 752 """
736 753 if 'IamInstanceProfile' in config:
737 754 raise ValueError('IamInstanceProfile cannot be provided in config')
738 755 if 'UserData' in config:
739 756 raise ValueError('UserData cannot be provided in config')
740 757
741 758 password = c.automation.default_password()
742 759
743 760 config = copy.deepcopy(config)
744 761 config['IamInstanceProfile'] = {
745 762 'Name': 'hg-ephemeral-ec2-1',
746 763 }
747 764 config.setdefault('TagSpecifications', []).append(
748 765 {
749 766 'ResourceType': 'instance',
750 767 'Tags': [{'Key': 'Name', 'Value': 'hg-temp-windows'}],
751 768 }
752 769 )
753 770
754 771 if bootstrap:
755 config['UserData'] = WINDOWS_USER_DATA % password
772 config['UserData'] = WINDOWS_USER_DATA % (password, password)
756 773
757 774 with temporary_ec2_instances(c.ec2resource, config) as instances:
758 775 wait_for_ip_addresses(instances)
759 776
760 777 print('waiting for Windows Remote Management service...')
761 778
762 779 for instance in instances:
763 780 client = wait_for_winrm(
764 781 instance.public_ip_address, 'Administrator', password
765 782 )
766 783 print('established WinRM connection to %s' % instance.id)
767 784 instance.winrm_client = client
768 785
769 786 yield instances
770 787
771 788
772 789 def resolve_fingerprint(fingerprint):
773 790 fingerprint = json.dumps(fingerprint, sort_keys=True)
774 791 return hashlib.sha256(fingerprint.encode('utf-8')).hexdigest()
775 792
776 793
777 794 def find_and_reconcile_image(ec2resource, name, fingerprint):
778 795 """Attempt to find an existing EC2 AMI with a name and fingerprint.
779 796
780 797 If an image with the specified fingerprint is found, it is returned.
781 798 Otherwise None is returned.
782 799
783 800 Existing images for the specified name that don't have the specified
784 801 fingerprint or are missing required metadata or deleted.
785 802 """
786 803 # Find existing AMIs with this name and delete the ones that are invalid.
787 804 # Store a reference to a good image so it can be returned one the
788 805 # image state is reconciled.
789 806 images = ec2resource.images.filter(
790 807 Filters=[{'Name': 'name', 'Values': [name]}]
791 808 )
792 809
793 810 existing_image = None
794 811
795 812 for image in images:
796 813 if image.tags is None:
797 814 print(
798 815 'image %s for %s lacks required tags; removing'
799 816 % (image.id, image.name)
800 817 )
801 818 remove_ami(ec2resource, image)
802 819 else:
803 820 tags = {t['Key']: t['Value'] for t in image.tags}
804 821
805 822 if tags.get('HGIMAGEFINGERPRINT') == fingerprint:
806 823 existing_image = image
807 824 else:
808 825 print(
809 826 'image %s for %s has wrong fingerprint; removing'
810 827 % (image.id, image.name)
811 828 )
812 829 remove_ami(ec2resource, image)
813 830
814 831 return existing_image
815 832
816 833
817 834 def create_ami_from_instance(
818 835 ec2client, instance, name, description, fingerprint
819 836 ):
820 837 """Create an AMI from a running instance.
821 838
822 839 Returns the ``ec2resource.Image`` representing the created AMI.
823 840 """
824 841 instance.stop()
825 842
826 843 ec2client.get_waiter('instance_stopped').wait(
827 844 InstanceIds=[instance.id],
828 845 WaiterConfig={
829 846 'Delay': 5,
830 847 },
831 848 )
832 849 print('%s is stopped' % instance.id)
833 850
834 851 image = instance.create_image(
835 852 Name=name,
836 853 Description=description,
837 854 )
838 855
839 856 image.create_tags(
840 857 Tags=[
841 858 {
842 859 'Key': 'HGIMAGEFINGERPRINT',
843 860 'Value': fingerprint,
844 861 },
845 862 ]
846 863 )
847 864
848 865 print('waiting for image %s' % image.id)
849 866
850 867 ec2client.get_waiter('image_available').wait(
851 868 ImageIds=[image.id],
852 869 )
853 870
854 871 print('image %s available as %s' % (image.id, image.name))
855 872
856 873 return image
857 874
858 875
859 876 def ensure_linux_dev_ami(c: AWSConnection, distro='debian10', prefix='hg-'):
860 877 """Ensures a Linux development AMI is available and up-to-date.
861 878
862 879 Returns an ``ec2.Image`` of either an existing AMI or a newly-built one.
863 880 """
864 881 ec2client = c.ec2client
865 882 ec2resource = c.ec2resource
866 883
867 884 name = '%s%s-%s' % (prefix, 'linux-dev', distro)
868 885
869 886 if distro == 'debian9':
870 887 image = find_image(
871 888 ec2resource,
872 889 DEBIAN_ACCOUNT_ID,
873 890 'debian-stretch-hvm-x86_64-gp2-2019-09-08-17994',
874 891 )
875 892 ssh_username = 'admin'
876 893 elif distro == 'debian10':
877 894 image = find_image(
878 895 ec2resource,
879 896 DEBIAN_ACCOUNT_ID_2,
880 897 'debian-10-amd64-20190909-10',
881 898 )
882 899 ssh_username = 'admin'
883 900 elif distro == 'ubuntu18.04':
884 901 image = find_image(
885 902 ec2resource,
886 903 UBUNTU_ACCOUNT_ID,
887 904 'ubuntu/images/hvm-ssd/ubuntu-bionic-18.04-amd64-server-20190918',
888 905 )
889 906 ssh_username = 'ubuntu'
890 907 elif distro == 'ubuntu19.04':
891 908 image = find_image(
892 909 ec2resource,
893 910 UBUNTU_ACCOUNT_ID,
894 911 'ubuntu/images/hvm-ssd/ubuntu-disco-19.04-amd64-server-20190918',
895 912 )
896 913 ssh_username = 'ubuntu'
897 914 else:
898 915 raise ValueError('unsupported Linux distro: %s' % distro)
899 916
900 917 config = {
901 918 'BlockDeviceMappings': [
902 919 {
903 920 'DeviceName': image.block_device_mappings[0]['DeviceName'],
904 921 'Ebs': {
905 922 'DeleteOnTermination': True,
906 923 'VolumeSize': 10,
907 924 'VolumeType': 'gp3',
908 925 },
909 926 },
910 927 ],
911 928 'EbsOptimized': True,
912 929 'ImageId': image.id,
913 930 'InstanceInitiatedShutdownBehavior': 'stop',
914 931 # 8 VCPUs for compiling Python.
915 932 'InstanceType': 't3.2xlarge',
916 933 'KeyName': '%sautomation' % prefix,
917 934 'MaxCount': 1,
918 935 'MinCount': 1,
919 936 'SecurityGroupIds': [c.security_groups['linux-dev-1'].id],
920 937 }
921 938
922 939 requirements3_path = (
923 940 pathlib.Path(__file__).parent.parent / 'linux-requirements-py3.txt'
924 941 )
925 942 requirements35_path = (
926 943 pathlib.Path(__file__).parent.parent / 'linux-requirements-py3.5.txt'
927 944 )
928 945 with requirements3_path.open('r', encoding='utf-8') as fh:
929 946 requirements3 = fh.read()
930 947 with requirements35_path.open('r', encoding='utf-8') as fh:
931 948 requirements35 = fh.read()
932 949
933 950 # Compute a deterministic fingerprint to determine whether image needs to
934 951 # be regenerated.
935 952 fingerprint = resolve_fingerprint(
936 953 {
937 954 'instance_config': config,
938 955 'bootstrap_script': BOOTSTRAP_DEBIAN,
939 956 'requirements_py3': requirements3,
940 957 'requirements_py35': requirements35,
941 958 }
942 959 )
943 960
944 961 existing_image = find_and_reconcile_image(ec2resource, name, fingerprint)
945 962
946 963 if existing_image:
947 964 return existing_image
948 965
949 966 print('no suitable %s image found; creating one...' % name)
950 967
951 968 with temporary_ec2_instances(ec2resource, config) as instances:
952 969 wait_for_ip_addresses(instances)
953 970
954 971 instance = instances[0]
955 972
956 973 client = wait_for_ssh(
957 974 instance.public_ip_address,
958 975 22,
959 976 username=ssh_username,
960 977 key_filename=str(c.key_pair_path_private('automation')),
961 978 )
962 979
963 980 home = '/home/%s' % ssh_username
964 981
965 982 with client:
966 983 print('connecting to SSH server')
967 984 sftp = client.open_sftp()
968 985
969 986 print('uploading bootstrap files')
970 987 with sftp.open('%s/bootstrap' % home, 'wb') as fh:
971 988 fh.write(BOOTSTRAP_DEBIAN)
972 989 fh.chmod(0o0700)
973 990
974 991 with sftp.open('%s/requirements-py3.txt' % home, 'wb') as fh:
975 992 fh.write(requirements3)
976 993 fh.chmod(0o0700)
977 994
978 995 with sftp.open('%s/requirements-py3.5.txt' % home, 'wb') as fh:
979 996 fh.write(requirements35)
980 997 fh.chmod(0o0700)
981 998
982 999 print('executing bootstrap')
983 1000 chan, stdin, stdout = ssh_exec_command(
984 1001 client, '%s/bootstrap' % home
985 1002 )
986 1003 stdin.close()
987 1004
988 1005 for line in stdout:
989 1006 print(line, end='')
990 1007
991 1008 res = chan.recv_exit_status()
992 1009 if res:
993 1010 raise Exception('non-0 exit from bootstrap: %d' % res)
994 1011
995 1012 print(
996 1013 'bootstrap completed; stopping %s to create %s'
997 1014 % (instance.id, name)
998 1015 )
999 1016
1000 1017 return create_ami_from_instance(
1001 1018 ec2client,
1002 1019 instance,
1003 1020 name,
1004 1021 'Mercurial Linux development environment',
1005 1022 fingerprint,
1006 1023 )
1007 1024
1008 1025
1009 1026 @contextlib.contextmanager
1010 1027 def temporary_linux_dev_instances(
1011 1028 c: AWSConnection,
1012 1029 image,
1013 1030 instance_type,
1014 1031 prefix='hg-',
1015 1032 ensure_extra_volume=False,
1016 1033 ):
1017 1034 """Create temporary Linux development EC2 instances.
1018 1035
1019 1036 Context manager resolves to a list of ``ec2.Instance`` that were created
1020 1037 and are running.
1021 1038
1022 1039 ``ensure_extra_volume`` can be set to ``True`` to require that instances
1023 1040 have a 2nd storage volume available other than the primary AMI volume.
1024 1041 For instance types with instance storage, this does nothing special.
1025 1042 But for instance types without instance storage, an additional EBS volume
1026 1043 will be added to the instance.
1027 1044
1028 1045 Instances have an ``ssh_client`` attribute containing a paramiko SSHClient
1029 1046 instance bound to the instance.
1030 1047
1031 1048 Instances have an ``ssh_private_key_path`` attributing containing the
1032 1049 str path to the SSH private key to connect to the instance.
1033 1050 """
1034 1051
1035 1052 block_device_mappings = [
1036 1053 {
1037 1054 'DeviceName': image.block_device_mappings[0]['DeviceName'],
1038 1055 'Ebs': {
1039 1056 'DeleteOnTermination': True,
1040 1057 'VolumeSize': 12,
1041 1058 'VolumeType': 'gp3',
1042 1059 },
1043 1060 }
1044 1061 ]
1045 1062
1046 1063 # This is not an exhaustive list of instance types having instance storage.
1047 1064 # But
1048 1065 if ensure_extra_volume and not instance_type.startswith(
1049 1066 tuple(INSTANCE_TYPES_WITH_STORAGE)
1050 1067 ):
1051 1068 main_device = block_device_mappings[0]['DeviceName']
1052 1069
1053 1070 if main_device == 'xvda':
1054 1071 second_device = 'xvdb'
1055 1072 elif main_device == '/dev/sda1':
1056 1073 second_device = '/dev/sdb'
1057 1074 else:
1058 1075 raise ValueError(
1059 1076 'unhandled primary EBS device name: %s' % main_device
1060 1077 )
1061 1078
1062 1079 block_device_mappings.append(
1063 1080 {
1064 1081 'DeviceName': second_device,
1065 1082 'Ebs': {
1066 1083 'DeleteOnTermination': True,
1067 1084 'VolumeSize': 8,
1068 1085 'VolumeType': 'gp3',
1069 1086 },
1070 1087 }
1071 1088 )
1072 1089
1073 1090 config = {
1074 1091 'BlockDeviceMappings': block_device_mappings,
1075 1092 'EbsOptimized': True,
1076 1093 'ImageId': image.id,
1077 1094 'InstanceInitiatedShutdownBehavior': 'terminate',
1078 1095 'InstanceType': instance_type,
1079 1096 'KeyName': '%sautomation' % prefix,
1080 1097 'MaxCount': 1,
1081 1098 'MinCount': 1,
1082 1099 'SecurityGroupIds': [c.security_groups['linux-dev-1'].id],
1083 1100 }
1084 1101
1085 1102 with temporary_ec2_instances(c.ec2resource, config) as instances:
1086 1103 wait_for_ip_addresses(instances)
1087 1104
1088 1105 ssh_private_key_path = str(c.key_pair_path_private('automation'))
1089 1106
1090 1107 for instance in instances:
1091 1108 client = wait_for_ssh(
1092 1109 instance.public_ip_address,
1093 1110 22,
1094 1111 username='hg',
1095 1112 key_filename=ssh_private_key_path,
1096 1113 )
1097 1114
1098 1115 instance.ssh_client = client
1099 1116 instance.ssh_private_key_path = ssh_private_key_path
1100 1117
1101 1118 try:
1102 1119 yield instances
1103 1120 finally:
1104 1121 for instance in instances:
1105 1122 instance.ssh_client.close()
1106 1123
1107 1124
1108 1125 def ensure_windows_dev_ami(
1109 1126 c: AWSConnection,
1110 1127 prefix='hg-',
1111 1128 base_image_name=WINDOWS_BASE_IMAGE_NAME,
1112 1129 ):
1113 1130 """Ensure Windows Development AMI is available and up-to-date.
1114 1131
1115 1132 If necessary, a modern AMI will be built by starting a temporary EC2
1116 1133 instance and bootstrapping it.
1117 1134
1118 1135 Obsolete AMIs will be deleted so there is only a single AMI having the
1119 1136 desired name.
1120 1137
1121 1138 Returns an ``ec2.Image`` of either an existing AMI or a newly-built
1122 1139 one.
1123 1140 """
1124 1141 ec2client = c.ec2client
1125 1142 ec2resource = c.ec2resource
1126 1143 ssmclient = c.session.client('ssm')
1127 1144
1128 1145 name = '%s%s' % (prefix, 'windows-dev')
1129 1146
1130 1147 image = find_image(
1131 1148 ec2resource,
1132 1149 AMAZON_ACCOUNT_ID,
1133 1150 base_image_name,
1134 1151 reverse_sort_field="name",
1135 1152 )
1136 1153
1137 1154 config = {
1138 1155 'BlockDeviceMappings': [
1139 1156 {
1140 1157 'DeviceName': '/dev/sda1',
1141 1158 'Ebs': {
1142 1159 'DeleteOnTermination': True,
1143 1160 'VolumeSize': 32,
1144 1161 'VolumeType': 'gp3',
1145 1162 },
1146 1163 }
1147 1164 ],
1148 1165 'ImageId': image.id,
1149 1166 'InstanceInitiatedShutdownBehavior': 'stop',
1150 1167 'InstanceType': 'm6i.large',
1151 1168 'KeyName': '%sautomation' % prefix,
1152 1169 'MaxCount': 1,
1153 1170 'MinCount': 1,
1154 1171 'SecurityGroupIds': [c.security_groups['windows-dev-1'].id],
1155 1172 }
1156 1173
1157 1174 commands = [
1158 1175 # Need to start the service so sshd_config is generated.
1159 1176 'Start-Service sshd',
1160 1177 'Write-Output "modifying sshd_config"',
1161 1178 r'$content = Get-Content C:\ProgramData\ssh\sshd_config',
1162 1179 '$content = $content -replace "Match Group administrators","" -replace "AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys",""',
1163 1180 r'$content | Set-Content C:\ProgramData\ssh\sshd_config',
1164 1181 'Import-Module OpenSSHUtils',
1165 1182 r'Repair-SshdConfigPermission C:\ProgramData\ssh\sshd_config -Confirm:$false',
1166 1183 'Restart-Service sshd',
1167 1184 'Write-Output "installing OpenSSL client"',
1168 1185 'Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0',
1169 1186 'Set-Service -Name sshd -StartupType "Automatic"',
1170 1187 'Write-Output "OpenSSH server running"',
1171 1188 ]
1172 1189
1173 1190 with INSTALL_WINDOWS_DEPENDENCIES.open('r', encoding='utf-8') as fh:
1174 1191 commands.extend(l.rstrip() for l in fh)
1175 1192
1176 # Schedule run of EC2Launch on next boot. This ensures that UserData
1177 # is executed.
1178 # We disable setComputerName because it forces a reboot.
1179 # We set an explicit admin password because this causes UserData to run
1180 # as Administrator instead of System.
1181 commands.extend(
1182 [
1183 r'''Set-Content -Path C:\ProgramData\Amazon\EC2-Windows\Launch\Config\LaunchConfig.json '''
1184 r'''-Value '{"setComputerName": false, "setWallpaper": true, "addDnsSuffixList": true, '''
1185 r'''"extendBootVolumeSize": true, "handleUserData": true, '''
1186 r'''"adminPasswordType": "Specify", "adminPassword": "%s"}' '''
1187 % c.automation.default_password(),
1188 r'C:\ProgramData\Amazon\EC2-Windows\Launch\Scripts\InitializeInstance.ps1 '
1189 r'–Schedule',
1190 ]
1191 )
1192
1193 1193 # Disable Windows Defender when bootstrapping because it just slows
1194 1194 # things down.
1195 1195 commands.insert(0, 'Set-MpPreference -DisableRealtimeMonitoring $true')
1196 1196 commands.append('Set-MpPreference -DisableRealtimeMonitoring $false')
1197 1197
1198 # Trigger shutdown to prepare for imaging.
1199 commands.append(
1200 'Stop-Computer -ComputerName localhost',
1201 )
1202
1198 1203 # Compute a deterministic fingerprint to determine whether image needs
1199 1204 # to be regenerated.
1200 1205 fingerprint = resolve_fingerprint(
1201 1206 {
1202 1207 'instance_config': config,
1203 1208 'user_data': WINDOWS_USER_DATA,
1204 1209 'initial_bootstrap': WINDOWS_BOOTSTRAP_POWERSHELL,
1205 1210 'bootstrap_commands': commands,
1206 1211 'base_image_name': base_image_name,
1207 1212 }
1208 1213 )
1209 1214
1210 1215 existing_image = find_and_reconcile_image(ec2resource, name, fingerprint)
1211 1216
1212 1217 if existing_image:
1213 1218 return existing_image
1214 1219
1215 1220 print('no suitable Windows development image found; creating one...')
1216 1221
1217 1222 with create_temp_windows_ec2_instances(
1218 1223 c, config, bootstrap=True
1219 1224 ) as instances:
1220 1225 assert len(instances) == 1
1221 1226 instance = instances[0]
1222 1227
1223 1228 wait_for_ssm(ssmclient, [instance])
1224 1229
1225 1230 # On first boot, install various Windows updates.
1226 1231 # We would ideally use PowerShell Remoting for this. However, there are
1227 1232 # trust issues that make it difficult to invoke Windows Update
1228 1233 # remotely. So we use SSM, which has a mechanism for running Windows
1229 1234 # Update.
1230 1235 print('installing Windows features...')
1231 1236 run_ssm_command(
1232 1237 ssmclient,
1233 1238 [instance],
1234 1239 'AWS-RunPowerShellScript',
1235 1240 {
1236 1241 'commands': WINDOWS_BOOTSTRAP_POWERSHELL.split('\n'),
1237 1242 },
1238 1243 )
1239 1244
1240 1245 # Reboot so all updates are fully applied.
1241 1246 #
1242 1247 # We don't use instance.reboot() here because it is asynchronous and
1243 1248 # we don't know when exactly the instance has rebooted. It could take
1244 1249 # a while to stop and we may start trying to interact with the instance
1245 1250 # before it has rebooted.
1246 1251 print('rebooting instance %s' % instance.id)
1247 1252 instance.stop()
1248 1253 ec2client.get_waiter('instance_stopped').wait(
1249 1254 InstanceIds=[instance.id],
1250 1255 WaiterConfig={
1251 1256 'Delay': 5,
1252 1257 },
1253 1258 )
1254 1259
1255 1260 instance.start()
1256 1261 wait_for_ip_addresses([instance])
1257 1262
1258 1263 # There is a race condition here between the User Data PS script running
1259 1264 # and us connecting to WinRM. This can manifest as
1260 1265 # "AuthorizationManager check failed" failures during run_powershell().
1261 1266 # TODO figure out a workaround.
1262 1267
1263 1268 print('waiting for Windows Remote Management to come back...')
1264 1269 client = wait_for_winrm(
1265 1270 instance.public_ip_address,
1266 1271 'Administrator',
1267 1272 c.automation.default_password(),
1268 1273 )
1269 1274 print('established WinRM connection to %s' % instance.id)
1270 1275 instance.winrm_client = client
1271 1276
1272 1277 print('bootstrapping instance...')
1273 1278 run_powershell(instance.winrm_client, '\n'.join(commands))
1274 1279
1275 1280 print('bootstrap completed; stopping %s to create image' % instance.id)
1276 1281 return create_ami_from_instance(
1277 1282 ec2client,
1278 1283 instance,
1279 1284 name,
1280 1285 'Mercurial Windows development environment',
1281 1286 fingerprint,
1282 1287 )
1283 1288
1284 1289
1285 1290 @contextlib.contextmanager
1286 1291 def temporary_windows_dev_instances(
1287 1292 c: AWSConnection,
1288 1293 image,
1289 1294 instance_type,
1290 1295 prefix='hg-',
1291 1296 disable_antivirus=False,
1292 1297 ):
1293 1298 """Create a temporary Windows development EC2 instance.
1294 1299
1295 1300 Context manager resolves to the list of ``EC2.Instance`` that were created.
1296 1301 """
1297 1302 config = {
1298 1303 'BlockDeviceMappings': [
1299 1304 {
1300 1305 'DeviceName': '/dev/sda1',
1301 1306 'Ebs': {
1302 1307 'DeleteOnTermination': True,
1303 1308 'VolumeSize': 32,
1304 1309 'VolumeType': 'gp3',
1305 1310 },
1306 1311 }
1307 1312 ],
1308 1313 'ImageId': image.id,
1309 1314 'InstanceInitiatedShutdownBehavior': 'stop',
1310 1315 'InstanceType': instance_type,
1311 1316 'KeyName': '%sautomation' % prefix,
1312 1317 'MaxCount': 1,
1313 1318 'MinCount': 1,
1314 1319 'SecurityGroupIds': [c.security_groups['windows-dev-1'].id],
1315 1320 }
1316 1321
1317 1322 with create_temp_windows_ec2_instances(c, config) as instances:
1318 1323 if disable_antivirus:
1319 1324 for instance in instances:
1320 1325 run_powershell(
1321 1326 instance.winrm_client,
1322 1327 'Set-MpPreference -DisableRealtimeMonitoring $true',
1323 1328 )
1324 1329
1325 1330 yield instances
General Comments 0
You need to be logged in to leave comments. Login now