Mercurial > public > mercurial-scm > hg-stable
comparison contrib/automation/hgautomation/aws.py @ 43076:2372284d9457
formatting: blacken the codebase
This is using my patch to black
(https://github.com/psf/black/pull/826) so we don't un-wrap collection
literals.
Done with:
hg files 'set:**.py - mercurial/thirdparty/** - "contrib/python-zstandard/**"' | xargs black -S
# skip-blame mass-reformatting only
# no-check-commit reformats foo_bar functions
Differential Revision: https://phab.mercurial-scm.org/D6971
author | Augie Fackler <augie@google.com> |
---|---|
date | Sun, 06 Oct 2019 09:45:02 -0400 |
parents | d1d919f679f7 |
children | c09e8ac3f61f |
comparison
equal
deleted
inserted
replaced
43075:57875cf423c9 | 43076:2372284d9457 |
---|---|
17 import time | 17 import time |
18 | 18 |
19 import boto3 | 19 import boto3 |
20 import botocore.exceptions | 20 import botocore.exceptions |
21 | 21 |
22 from .linux import ( | 22 from .linux import BOOTSTRAP_DEBIAN |
23 BOOTSTRAP_DEBIAN, | |
24 ) | |
25 from .ssh import ( | 23 from .ssh import ( |
26 exec_command as ssh_exec_command, | 24 exec_command as ssh_exec_command, |
27 wait_for_ssh, | 25 wait_for_ssh, |
28 ) | 26 ) |
29 from .winrm import ( | 27 from .winrm import ( |
30 run_powershell, | 28 run_powershell, |
31 wait_for_winrm, | 29 wait_for_winrm, |
32 ) | 30 ) |
33 | 31 |
34 | 32 |
35 SOURCE_ROOT = pathlib.Path(os.path.abspath(__file__)).parent.parent.parent.parent | 33 SOURCE_ROOT = pathlib.Path( |
36 | 34 os.path.abspath(__file__) |
37 INSTALL_WINDOWS_DEPENDENCIES = (SOURCE_ROOT / 'contrib' / | 35 ).parent.parent.parent.parent |
38 'install-windows-dependencies.ps1') | 36 |
37 INSTALL_WINDOWS_DEPENDENCIES = ( | |
38 SOURCE_ROOT / 'contrib' / 'install-windows-dependencies.ps1' | |
39 ) | |
39 | 40 |
40 | 41 |
41 INSTANCE_TYPES_WITH_STORAGE = { | 42 INSTANCE_TYPES_WITH_STORAGE = { |
42 'c5d', | 43 'c5d', |
43 'd2', | 44 'd2', |
105 { | 106 { |
106 'CidrIp': '0.0.0.0/0', | 107 'CidrIp': '0.0.0.0/0', |
107 'Description': 'RDP from entire Internet', | 108 'Description': 'RDP from entire Internet', |
108 }, | 109 }, |
109 ], | 110 ], |
110 | |
111 }, | 111 }, |
112 { | 112 { |
113 'FromPort': 5985, | 113 'FromPort': 5985, |
114 'ToPort': 5986, | 114 'ToPort': 5986, |
115 'IpProtocol': 'tcp', | 115 'IpProtocol': 'tcp', |
117 { | 117 { |
118 'CidrIp': '0.0.0.0/0', | 118 'CidrIp': '0.0.0.0/0', |
119 'Description': 'PowerShell Remoting (Windows Remote Management)', | 119 'Description': 'PowerShell Remoting (Windows Remote Management)', |
120 }, | 120 }, |
121 ], | 121 ], |
122 } | 122 }, |
123 ], | 123 ], |
124 }, | 124 }, |
125 } | 125 } |
126 | 126 |
127 | 127 |
150 } | 150 } |
151 '''.strip() | 151 '''.strip() |
152 | 152 |
153 | 153 |
154 IAM_INSTANCE_PROFILES = { | 154 IAM_INSTANCE_PROFILES = { |
155 'ephemeral-ec2-1': { | 155 'ephemeral-ec2-1': {'roles': ['ephemeral-ec2-role-1',],} |
156 'roles': [ | |
157 'ephemeral-ec2-role-1', | |
158 ], | |
159 } | |
160 } | 156 } |
161 | 157 |
162 | 158 |
163 # User Data for Windows EC2 instance. Mainly used to set the password | 159 # User Data for Windows EC2 instance. Mainly used to set the password |
164 # and configure WinRM. | 160 # and configure WinRM. |
224 | 220 |
225 | 221 |
226 class AWSConnection: | 222 class AWSConnection: |
227 """Manages the state of a connection with AWS.""" | 223 """Manages the state of a connection with AWS.""" |
228 | 224 |
229 def __init__(self, automation, region: str, ensure_ec2_state: bool=True): | 225 def __init__(self, automation, region: str, ensure_ec2_state: bool = True): |
230 self.automation = automation | 226 self.automation = automation |
231 self.local_state_path = automation.state_path | 227 self.local_state_path = automation.state_path |
232 | 228 |
233 self.prefix = 'hg-' | 229 self.prefix = 'hg-' |
234 | 230 |
255 def rsa_key_fingerprint(p: pathlib.Path): | 251 def rsa_key_fingerprint(p: pathlib.Path): |
256 """Compute the fingerprint of an RSA private key.""" | 252 """Compute the fingerprint of an RSA private key.""" |
257 | 253 |
258 # TODO use rsa package. | 254 # TODO use rsa package. |
259 res = subprocess.run( | 255 res = subprocess.run( |
260 ['openssl', 'pkcs8', '-in', str(p), '-nocrypt', '-topk8', | 256 [ |
261 '-outform', 'DER'], | 257 'openssl', |
258 'pkcs8', | |
259 '-in', | |
260 str(p), | |
261 '-nocrypt', | |
262 '-topk8', | |
263 '-outform', | |
264 'DER', | |
265 ], | |
262 capture_output=True, | 266 capture_output=True, |
263 check=True) | 267 check=True, |
268 ) | |
264 | 269 |
265 sha1 = hashlib.sha1(res.stdout).hexdigest() | 270 sha1 = hashlib.sha1(res.stdout).hexdigest() |
266 return ':'.join(a + b for a, b in zip(sha1[::2], sha1[1::2])) | 271 return ':'.join(a + b for a, b in zip(sha1[::2], sha1[1::2])) |
267 | 272 |
268 | 273 |
269 def ensure_key_pairs(state_path: pathlib.Path, ec2resource, prefix='hg-'): | 274 def ensure_key_pairs(state_path: pathlib.Path, ec2resource, prefix='hg-'): |
270 remote_existing = {} | 275 remote_existing = {} |
271 | 276 |
272 for kpi in ec2resource.key_pairs.all(): | 277 for kpi in ec2resource.key_pairs.all(): |
273 if kpi.name.startswith(prefix): | 278 if kpi.name.startswith(prefix): |
274 remote_existing[kpi.name[len(prefix):]] = kpi.key_fingerprint | 279 remote_existing[kpi.name[len(prefix) :]] = kpi.key_fingerprint |
275 | 280 |
276 # Validate that we have these keys locally. | 281 # Validate that we have these keys locally. |
277 key_path = state_path / 'keys' | 282 key_path = state_path / 'keys' |
278 key_path.mkdir(exist_ok=True, mode=0o700) | 283 key_path.mkdir(exist_ok=True, mode=0o700) |
279 | 284 |
295 | 300 |
296 for f in sorted(os.listdir(key_path)): | 301 for f in sorted(os.listdir(key_path)): |
297 if not f.startswith('keypair-') or not f.endswith('.pub'): | 302 if not f.startswith('keypair-') or not f.endswith('.pub'): |
298 continue | 303 continue |
299 | 304 |
300 name = f[len('keypair-'):-len('.pub')] | 305 name = f[len('keypair-') : -len('.pub')] |
301 | 306 |
302 pub_full = key_path / f | 307 pub_full = key_path / f |
303 priv_full = key_path / ('keypair-%s' % name) | 308 priv_full = key_path / ('keypair-%s' % name) |
304 | 309 |
305 with open(pub_full, 'r', encoding='ascii') as fh: | 310 with open(pub_full, 'r', encoding='ascii') as fh: |
306 data = fh.read() | 311 data = fh.read() |
307 | 312 |
308 if not data.startswith('ssh-rsa '): | 313 if not data.startswith('ssh-rsa '): |
309 print('unexpected format for key pair file: %s; removing' % | 314 print( |
310 pub_full) | 315 'unexpected format for key pair file: %s; removing' % pub_full |
316 ) | |
311 pub_full.unlink() | 317 pub_full.unlink() |
312 priv_full.unlink() | 318 priv_full.unlink() |
313 continue | 319 continue |
314 | 320 |
315 local_existing[name] = rsa_key_fingerprint(priv_full) | 321 local_existing[name] = rsa_key_fingerprint(priv_full) |
325 print('local key %s does not exist remotely' % name) | 331 print('local key %s does not exist remotely' % name) |
326 remove_local(name) | 332 remove_local(name) |
327 del local_existing[name] | 333 del local_existing[name] |
328 | 334 |
329 elif remote_existing[name] != local_existing[name]: | 335 elif remote_existing[name] != local_existing[name]: |
330 print('key fingerprint mismatch for %s; ' | 336 print( |
331 'removing from local and remote' % name) | 337 'key fingerprint mismatch for %s; ' |
338 'removing from local and remote' % name | |
339 ) | |
332 remove_local(name) | 340 remove_local(name) |
333 remove_remote('%s%s' % (prefix, name)) | 341 remove_remote('%s%s' % (prefix, name)) |
334 del local_existing[name] | 342 del local_existing[name] |
335 del remote_existing[name] | 343 del remote_existing[name] |
336 | 344 |
354 # SSH public key can be extracted via `ssh-keygen`. | 362 # SSH public key can be extracted via `ssh-keygen`. |
355 with pub_full.open('w', encoding='ascii') as fh: | 363 with pub_full.open('w', encoding='ascii') as fh: |
356 subprocess.run( | 364 subprocess.run( |
357 ['ssh-keygen', '-y', '-f', str(priv_full)], | 365 ['ssh-keygen', '-y', '-f', str(priv_full)], |
358 stdout=fh, | 366 stdout=fh, |
359 check=True) | 367 check=True, |
368 ) | |
360 | 369 |
361 pub_full.chmod(0o0600) | 370 pub_full.chmod(0o0600) |
362 | 371 |
363 | 372 |
364 def delete_instance_profile(profile): | 373 def delete_instance_profile(profile): |
365 for role in profile.roles: | 374 for role in profile.roles: |
366 print('removing role %s from instance profile %s' % (role.name, | 375 print( |
367 profile.name)) | 376 'removing role %s from instance profile %s' |
377 % (role.name, profile.name) | |
378 ) | |
368 profile.remove_role(RoleName=role.name) | 379 profile.remove_role(RoleName=role.name) |
369 | 380 |
370 print('deleting instance profile %s' % profile.name) | 381 print('deleting instance profile %s' % profile.name) |
371 profile.delete() | 382 profile.delete() |
372 | 383 |
376 | 387 |
377 remote_profiles = {} | 388 remote_profiles = {} |
378 | 389 |
379 for profile in iamresource.instance_profiles.all(): | 390 for profile in iamresource.instance_profiles.all(): |
380 if profile.name.startswith(prefix): | 391 if profile.name.startswith(prefix): |
381 remote_profiles[profile.name[len(prefix):]] = profile | 392 remote_profiles[profile.name[len(prefix) :]] = profile |
382 | 393 |
383 for name in sorted(set(remote_profiles) - set(IAM_INSTANCE_PROFILES)): | 394 for name in sorted(set(remote_profiles) - set(IAM_INSTANCE_PROFILES)): |
384 delete_instance_profile(remote_profiles[name]) | 395 delete_instance_profile(remote_profiles[name]) |
385 del remote_profiles[name] | 396 del remote_profiles[name] |
386 | 397 |
387 remote_roles = {} | 398 remote_roles = {} |
388 | 399 |
389 for role in iamresource.roles.all(): | 400 for role in iamresource.roles.all(): |
390 if role.name.startswith(prefix): | 401 if role.name.startswith(prefix): |
391 remote_roles[role.name[len(prefix):]] = role | 402 remote_roles[role.name[len(prefix) :]] = role |
392 | 403 |
393 for name in sorted(set(remote_roles) - set(IAM_ROLES)): | 404 for name in sorted(set(remote_roles) - set(IAM_ROLES)): |
394 role = remote_roles[name] | 405 role = remote_roles[name] |
395 | 406 |
396 print('removing role %s' % role.name) | 407 print('removing role %s' % role.name) |
402 for name in sorted(set(IAM_INSTANCE_PROFILES) - set(remote_profiles)): | 413 for name in sorted(set(IAM_INSTANCE_PROFILES) - set(remote_profiles)): |
403 actual = '%s%s' % (prefix, name) | 414 actual = '%s%s' % (prefix, name) |
404 print('creating IAM instance profile %s' % actual) | 415 print('creating IAM instance profile %s' % actual) |
405 | 416 |
406 profile = iamresource.create_instance_profile( | 417 profile = iamresource.create_instance_profile( |
407 InstanceProfileName=actual) | 418 InstanceProfileName=actual |
419 ) | |
408 remote_profiles[name] = profile | 420 remote_profiles[name] = profile |
409 | 421 |
410 waiter = iamclient.get_waiter('instance_profile_exists') | 422 waiter = iamclient.get_waiter('instance_profile_exists') |
411 waiter.wait(InstanceProfileName=actual) | 423 waiter.wait(InstanceProfileName=actual) |
412 print('IAM instance profile %s is available' % actual) | 424 print('IAM instance profile %s is available' % actual) |
451 def find_image(ec2resource, owner_id, name): | 463 def find_image(ec2resource, owner_id, name): |
452 """Find an AMI by its owner ID and name.""" | 464 """Find an AMI by its owner ID and name.""" |
453 | 465 |
454 images = ec2resource.images.filter( | 466 images = ec2resource.images.filter( |
455 Filters=[ | 467 Filters=[ |
456 { | 468 {'Name': 'owner-id', 'Values': [owner_id],}, |
457 'Name': 'owner-id', | 469 {'Name': 'state', 'Values': ['available'],}, |
458 'Values': [owner_id], | 470 {'Name': 'image-type', 'Values': ['machine'],}, |
459 }, | 471 {'Name': 'name', 'Values': [name],}, |
460 { | 472 ] |
461 'Name': 'state', | 473 ) |
462 'Values': ['available'], | |
463 }, | |
464 { | |
465 'Name': 'image-type', | |
466 'Values': ['machine'], | |
467 }, | |
468 { | |
469 'Name': 'name', | |
470 'Values': [name], | |
471 }, | |
472 ]) | |
473 | 474 |
474 for image in images: | 475 for image in images: |
475 return image | 476 return image |
476 | 477 |
477 raise Exception('unable to find image for %s' % name) | 478 raise Exception('unable to find image for %s' % name) |
485 """ | 486 """ |
486 existing = {} | 487 existing = {} |
487 | 488 |
488 for group in ec2resource.security_groups.all(): | 489 for group in ec2resource.security_groups.all(): |
489 if group.group_name.startswith(prefix): | 490 if group.group_name.startswith(prefix): |
490 existing[group.group_name[len(prefix):]] = group | 491 existing[group.group_name[len(prefix) :]] = group |
491 | 492 |
492 purge = set(existing) - set(SECURITY_GROUPS) | 493 purge = set(existing) - set(SECURITY_GROUPS) |
493 | 494 |
494 for name in sorted(purge): | 495 for name in sorted(purge): |
495 group = existing[name] | 496 group = existing[name] |
505 | 506 |
506 actual = '%s%s' % (prefix, name) | 507 actual = '%s%s' % (prefix, name) |
507 print('adding security group %s' % actual) | 508 print('adding security group %s' % actual) |
508 | 509 |
509 group_res = ec2resource.create_security_group( | 510 group_res = ec2resource.create_security_group( |
510 Description=group['description'], | 511 Description=group['description'], GroupName=actual, |
511 GroupName=actual, | 512 ) |
512 ) | 513 |
513 | 514 group_res.authorize_ingress(IpPermissions=group['ingress'],) |
514 group_res.authorize_ingress( | |
515 IpPermissions=group['ingress'], | |
516 ) | |
517 | 515 |
518 security_groups[name] = group_res | 516 security_groups[name] = group_res |
519 | 517 |
520 return security_groups | 518 return security_groups |
521 | 519 |
575 if not instance.public_ip_address: | 573 if not instance.public_ip_address: |
576 time.sleep(2) | 574 time.sleep(2) |
577 instance.reload() | 575 instance.reload() |
578 continue | 576 continue |
579 | 577 |
580 print('public IP address for %s: %s' % ( | 578 print( |
581 instance.id, instance.public_ip_address)) | 579 'public IP address for %s: %s' |
580 % (instance.id, instance.public_ip_address) | |
581 ) | |
582 break | 582 break |
583 | 583 |
584 | 584 |
585 def remove_ami(ec2resource, image): | 585 def remove_ami(ec2resource, image): |
586 """Remove an AMI and its underlying snapshots.""" | 586 """Remove an AMI and its underlying snapshots.""" |
601 def wait_for_ssm(ssmclient, instances): | 601 def wait_for_ssm(ssmclient, instances): |
602 """Wait for SSM to come online for an iterable of instance IDs.""" | 602 """Wait for SSM to come online for an iterable of instance IDs.""" |
603 while True: | 603 while True: |
604 res = ssmclient.describe_instance_information( | 604 res = ssmclient.describe_instance_information( |
605 Filters=[ | 605 Filters=[ |
606 { | 606 {'Key': 'InstanceIds', 'Values': [i.id for i in instances],}, |
607 'Key': 'InstanceIds', | |
608 'Values': [i.id for i in instances], | |
609 }, | |
610 ], | 607 ], |
611 ) | 608 ) |
612 | 609 |
613 available = len(res['InstanceInformationList']) | 610 available = len(res['InstanceInformationList']) |
614 wanted = len(instances) | 611 wanted = len(instances) |
626 | 623 |
627 res = ssmclient.send_command( | 624 res = ssmclient.send_command( |
628 InstanceIds=[i.id for i in instances], | 625 InstanceIds=[i.id for i in instances], |
629 DocumentName=document_name, | 626 DocumentName=document_name, |
630 Parameters=parameters, | 627 Parameters=parameters, |
631 CloudWatchOutputConfig={ | 628 CloudWatchOutputConfig={'CloudWatchOutputEnabled': True,}, |
632 'CloudWatchOutputEnabled': True, | |
633 }, | |
634 ) | 629 ) |
635 | 630 |
636 command_id = res['Command']['CommandId'] | 631 command_id = res['Command']['CommandId'] |
637 | 632 |
638 for instance in instances: | 633 for instance in instances: |
639 while True: | 634 while True: |
640 try: | 635 try: |
641 res = ssmclient.get_command_invocation( | 636 res = ssmclient.get_command_invocation( |
642 CommandId=command_id, | 637 CommandId=command_id, InstanceId=instance.id, |
643 InstanceId=instance.id, | |
644 ) | 638 ) |
645 except botocore.exceptions.ClientError as e: | 639 except botocore.exceptions.ClientError as e: |
646 if e.response['Error']['Code'] == 'InvocationDoesNotExist': | 640 if e.response['Error']['Code'] == 'InvocationDoesNotExist': |
647 print('could not find SSM command invocation; waiting') | 641 print('could not find SSM command invocation; waiting') |
648 time.sleep(1) | 642 time.sleep(1) |
653 if res['Status'] == 'Success': | 647 if res['Status'] == 'Success': |
654 break | 648 break |
655 elif res['Status'] in ('Pending', 'InProgress', 'Delayed'): | 649 elif res['Status'] in ('Pending', 'InProgress', 'Delayed'): |
656 time.sleep(2) | 650 time.sleep(2) |
657 else: | 651 else: |
658 raise Exception('command failed on %s: %s' % ( | 652 raise Exception( |
659 instance.id, res['Status'])) | 653 'command failed on %s: %s' % (instance.id, res['Status']) |
654 ) | |
660 | 655 |
661 | 656 |
662 @contextlib.contextmanager | 657 @contextlib.contextmanager |
663 def temporary_ec2_instances(ec2resource, config): | 658 def temporary_ec2_instances(ec2resource, config): |
664 """Create temporary EC2 instances. | 659 """Create temporary EC2 instances. |
709 | 704 |
710 config = copy.deepcopy(config) | 705 config = copy.deepcopy(config) |
711 config['IamInstanceProfile'] = { | 706 config['IamInstanceProfile'] = { |
712 'Name': 'hg-ephemeral-ec2-1', | 707 'Name': 'hg-ephemeral-ec2-1', |
713 } | 708 } |
714 config.setdefault('TagSpecifications', []).append({ | 709 config.setdefault('TagSpecifications', []).append( |
715 'ResourceType': 'instance', | 710 { |
716 'Tags': [{'Key': 'Name', 'Value': 'hg-temp-windows'}], | 711 'ResourceType': 'instance', |
717 }) | 712 'Tags': [{'Key': 'Name', 'Value': 'hg-temp-windows'}], |
713 } | |
714 ) | |
718 config['UserData'] = WINDOWS_USER_DATA % password | 715 config['UserData'] = WINDOWS_USER_DATA % password |
719 | 716 |
720 with temporary_ec2_instances(c.ec2resource, config) as instances: | 717 with temporary_ec2_instances(c.ec2resource, config) as instances: |
721 wait_for_ip_addresses(instances) | 718 wait_for_ip_addresses(instances) |
722 | 719 |
723 print('waiting for Windows Remote Management service...') | 720 print('waiting for Windows Remote Management service...') |
724 | 721 |
725 for instance in instances: | 722 for instance in instances: |
726 client = wait_for_winrm(instance.public_ip_address, 'Administrator', password) | 723 client = wait_for_winrm( |
724 instance.public_ip_address, 'Administrator', password | |
725 ) | |
727 print('established WinRM connection to %s' % instance.id) | 726 print('established WinRM connection to %s' % instance.id) |
728 instance.winrm_client = client | 727 instance.winrm_client = client |
729 | 728 |
730 yield instances | 729 yield instances |
731 | 730 |
746 """ | 745 """ |
747 # Find existing AMIs with this name and delete the ones that are invalid. | 746 # Find existing AMIs with this name and delete the ones that are invalid. |
748 # Store a reference to a good image so it can be returned one the | 747 # Store a reference to a good image so it can be returned one the |
749 # image state is reconciled. | 748 # image state is reconciled. |
750 images = ec2resource.images.filter( | 749 images = ec2resource.images.filter( |
751 Filters=[{'Name': 'name', 'Values': [name]}]) | 750 Filters=[{'Name': 'name', 'Values': [name]}] |
751 ) | |
752 | 752 |
753 existing_image = None | 753 existing_image = None |
754 | 754 |
755 for image in images: | 755 for image in images: |
756 if image.tags is None: | 756 if image.tags is None: |
757 print('image %s for %s lacks required tags; removing' % ( | 757 print( |
758 image.id, image.name)) | 758 'image %s for %s lacks required tags; removing' |
759 % (image.id, image.name) | |
760 ) | |
759 remove_ami(ec2resource, image) | 761 remove_ami(ec2resource, image) |
760 else: | 762 else: |
761 tags = {t['Key']: t['Value'] for t in image.tags} | 763 tags = {t['Key']: t['Value'] for t in image.tags} |
762 | 764 |
763 if tags.get('HGIMAGEFINGERPRINT') == fingerprint: | 765 if tags.get('HGIMAGEFINGERPRINT') == fingerprint: |
764 existing_image = image | 766 existing_image = image |
765 else: | 767 else: |
766 print('image %s for %s has wrong fingerprint; removing' % ( | 768 print( |
767 image.id, image.name)) | 769 'image %s for %s has wrong fingerprint; removing' |
770 % (image.id, image.name) | |
771 ) | |
768 remove_ami(ec2resource, image) | 772 remove_ami(ec2resource, image) |
769 | 773 |
770 return existing_image | 774 return existing_image |
771 | 775 |
772 | 776 |
773 def create_ami_from_instance(ec2client, instance, name, description, | 777 def create_ami_from_instance( |
774 fingerprint): | 778 ec2client, instance, name, description, fingerprint |
779 ): | |
775 """Create an AMI from a running instance. | 780 """Create an AMI from a running instance. |
776 | 781 |
777 Returns the ``ec2resource.Image`` representing the created AMI. | 782 Returns the ``ec2resource.Image`` representing the created AMI. |
778 """ | 783 """ |
779 instance.stop() | 784 instance.stop() |
780 | 785 |
781 ec2client.get_waiter('instance_stopped').wait( | 786 ec2client.get_waiter('instance_stopped').wait( |
782 InstanceIds=[instance.id], | 787 InstanceIds=[instance.id], WaiterConfig={'Delay': 5,} |
783 WaiterConfig={ | 788 ) |
784 'Delay': 5, | |
785 }) | |
786 print('%s is stopped' % instance.id) | 789 print('%s is stopped' % instance.id) |
787 | 790 |
788 image = instance.create_image( | 791 image = instance.create_image(Name=name, Description=description,) |
789 Name=name, | 792 |
790 Description=description, | 793 image.create_tags( |
794 Tags=[{'Key': 'HGIMAGEFINGERPRINT', 'Value': fingerprint,},] | |
791 ) | 795 ) |
792 | 796 |
793 image.create_tags(Tags=[ | |
794 { | |
795 'Key': 'HGIMAGEFINGERPRINT', | |
796 'Value': fingerprint, | |
797 }, | |
798 ]) | |
799 | |
800 print('waiting for image %s' % image.id) | 797 print('waiting for image %s' % image.id) |
801 | 798 |
802 ec2client.get_waiter('image_available').wait( | 799 ec2client.get_waiter('image_available').wait(ImageIds=[image.id],) |
803 ImageIds=[image.id], | |
804 ) | |
805 | 800 |
806 print('image %s available as %s' % (image.id, image.name)) | 801 print('image %s available as %s' % (image.id, image.name)) |
807 | 802 |
808 return image | 803 return image |
809 | 804 |
825 'debian-stretch-hvm-x86_64-gp2-2019-09-08-17994', | 820 'debian-stretch-hvm-x86_64-gp2-2019-09-08-17994', |
826 ) | 821 ) |
827 ssh_username = 'admin' | 822 ssh_username = 'admin' |
828 elif distro == 'debian10': | 823 elif distro == 'debian10': |
829 image = find_image( | 824 image = find_image( |
830 ec2resource, | 825 ec2resource, DEBIAN_ACCOUNT_ID_2, 'debian-10-amd64-20190909-10', |
831 DEBIAN_ACCOUNT_ID_2, | |
832 'debian-10-amd64-20190909-10', | |
833 ) | 826 ) |
834 ssh_username = 'admin' | 827 ssh_username = 'admin' |
835 elif distro == 'ubuntu18.04': | 828 elif distro == 'ubuntu18.04': |
836 image = find_image( | 829 image = find_image( |
837 ec2resource, | 830 ec2resource, |
869 'MaxCount': 1, | 862 'MaxCount': 1, |
870 'MinCount': 1, | 863 'MinCount': 1, |
871 'SecurityGroupIds': [c.security_groups['linux-dev-1'].id], | 864 'SecurityGroupIds': [c.security_groups['linux-dev-1'].id], |
872 } | 865 } |
873 | 866 |
874 requirements2_path = (pathlib.Path(__file__).parent.parent / | 867 requirements2_path = ( |
875 'linux-requirements-py2.txt') | 868 pathlib.Path(__file__).parent.parent / 'linux-requirements-py2.txt' |
876 requirements3_path = (pathlib.Path(__file__).parent.parent / | 869 ) |
877 'linux-requirements-py3.txt') | 870 requirements3_path = ( |
871 pathlib.Path(__file__).parent.parent / 'linux-requirements-py3.txt' | |
872 ) | |
878 with requirements2_path.open('r', encoding='utf-8') as fh: | 873 with requirements2_path.open('r', encoding='utf-8') as fh: |
879 requirements2 = fh.read() | 874 requirements2 = fh.read() |
880 with requirements3_path.open('r', encoding='utf-8') as fh: | 875 with requirements3_path.open('r', encoding='utf-8') as fh: |
881 requirements3 = fh.read() | 876 requirements3 = fh.read() |
882 | 877 |
883 # Compute a deterministic fingerprint to determine whether image needs to | 878 # Compute a deterministic fingerprint to determine whether image needs to |
884 # be regenerated. | 879 # be regenerated. |
885 fingerprint = resolve_fingerprint({ | 880 fingerprint = resolve_fingerprint( |
886 'instance_config': config, | 881 { |
887 'bootstrap_script': BOOTSTRAP_DEBIAN, | 882 'instance_config': config, |
888 'requirements_py2': requirements2, | 883 'bootstrap_script': BOOTSTRAP_DEBIAN, |
889 'requirements_py3': requirements3, | 884 'requirements_py2': requirements2, |
890 }) | 885 'requirements_py3': requirements3, |
886 } | |
887 ) | |
891 | 888 |
892 existing_image = find_and_reconcile_image(ec2resource, name, fingerprint) | 889 existing_image = find_and_reconcile_image(ec2resource, name, fingerprint) |
893 | 890 |
894 if existing_image: | 891 if existing_image: |
895 return existing_image | 892 return existing_image |
900 wait_for_ip_addresses(instances) | 897 wait_for_ip_addresses(instances) |
901 | 898 |
902 instance = instances[0] | 899 instance = instances[0] |
903 | 900 |
904 client = wait_for_ssh( | 901 client = wait_for_ssh( |
905 instance.public_ip_address, 22, | 902 instance.public_ip_address, |
903 22, | |
906 username=ssh_username, | 904 username=ssh_username, |
907 key_filename=str(c.key_pair_path_private('automation'))) | 905 key_filename=str(c.key_pair_path_private('automation')), |
906 ) | |
908 | 907 |
909 home = '/home/%s' % ssh_username | 908 home = '/home/%s' % ssh_username |
910 | 909 |
911 with client: | 910 with client: |
912 print('connecting to SSH server') | 911 print('connecting to SSH server') |
924 with sftp.open('%s/requirements-py3.txt' % home, 'wb') as fh: | 923 with sftp.open('%s/requirements-py3.txt' % home, 'wb') as fh: |
925 fh.write(requirements3) | 924 fh.write(requirements3) |
926 fh.chmod(0o0700) | 925 fh.chmod(0o0700) |
927 | 926 |
928 print('executing bootstrap') | 927 print('executing bootstrap') |
929 chan, stdin, stdout = ssh_exec_command(client, | 928 chan, stdin, stdout = ssh_exec_command( |
930 '%s/bootstrap' % home) | 929 client, '%s/bootstrap' % home |
930 ) | |
931 stdin.close() | 931 stdin.close() |
932 | 932 |
933 for line in stdout: | 933 for line in stdout: |
934 print(line, end='') | 934 print(line, end='') |
935 | 935 |
936 res = chan.recv_exit_status() | 936 res = chan.recv_exit_status() |
937 if res: | 937 if res: |
938 raise Exception('non-0 exit from bootstrap: %d' % res) | 938 raise Exception('non-0 exit from bootstrap: %d' % res) |
939 | 939 |
940 print('bootstrap completed; stopping %s to create %s' % ( | 940 print( |
941 instance.id, name)) | 941 'bootstrap completed; stopping %s to create %s' |
942 | 942 % (instance.id, name) |
943 return create_ami_from_instance(ec2client, instance, name, | 943 ) |
944 'Mercurial Linux development environment', | 944 |
945 fingerprint) | 945 return create_ami_from_instance( |
946 ec2client, | |
947 instance, | |
948 name, | |
949 'Mercurial Linux development environment', | |
950 fingerprint, | |
951 ) | |
946 | 952 |
947 | 953 |
948 @contextlib.contextmanager | 954 @contextlib.contextmanager |
949 def temporary_linux_dev_instances(c: AWSConnection, image, instance_type, | 955 def temporary_linux_dev_instances( |
950 prefix='hg-', ensure_extra_volume=False): | 956 c: AWSConnection, |
957 image, | |
958 instance_type, | |
959 prefix='hg-', | |
960 ensure_extra_volume=False, | |
961 ): | |
951 """Create temporary Linux development EC2 instances. | 962 """Create temporary Linux development EC2 instances. |
952 | 963 |
953 Context manager resolves to a list of ``ec2.Instance`` that were created | 964 Context manager resolves to a list of ``ec2.Instance`` that were created |
954 and are running. | 965 and are running. |
955 | 966 |
977 } | 988 } |
978 ] | 989 ] |
979 | 990 |
980 # This is not an exhaustive list of instance types having instance storage. | 991 # This is not an exhaustive list of instance types having instance storage. |
981 # But | 992 # But |
982 if (ensure_extra_volume | 993 if ensure_extra_volume and not instance_type.startswith( |
983 and not instance_type.startswith(tuple(INSTANCE_TYPES_WITH_STORAGE))): | 994 tuple(INSTANCE_TYPES_WITH_STORAGE) |
995 ): | |
984 main_device = block_device_mappings[0]['DeviceName'] | 996 main_device = block_device_mappings[0]['DeviceName'] |
985 | 997 |
986 if main_device == 'xvda': | 998 if main_device == 'xvda': |
987 second_device = 'xvdb' | 999 second_device = 'xvdb' |
988 elif main_device == '/dev/sda1': | 1000 elif main_device == '/dev/sda1': |
989 second_device = '/dev/sdb' | 1001 second_device = '/dev/sdb' |
990 else: | 1002 else: |
991 raise ValueError('unhandled primary EBS device name: %s' % | 1003 raise ValueError( |
992 main_device) | 1004 'unhandled primary EBS device name: %s' % main_device |
993 | 1005 ) |
994 block_device_mappings.append({ | 1006 |
995 'DeviceName': second_device, | 1007 block_device_mappings.append( |
996 'Ebs': { | 1008 { |
997 'DeleteOnTermination': True, | 1009 'DeviceName': second_device, |
998 'VolumeSize': 8, | 1010 'Ebs': { |
999 'VolumeType': 'gp2', | 1011 'DeleteOnTermination': True, |
1012 'VolumeSize': 8, | |
1013 'VolumeType': 'gp2', | |
1014 }, | |
1000 } | 1015 } |
1001 }) | 1016 ) |
1002 | 1017 |
1003 config = { | 1018 config = { |
1004 'BlockDeviceMappings': block_device_mappings, | 1019 'BlockDeviceMappings': block_device_mappings, |
1005 'EbsOptimized': True, | 1020 'EbsOptimized': True, |
1006 'ImageId': image.id, | 1021 'ImageId': image.id, |
1017 | 1032 |
1018 ssh_private_key_path = str(c.key_pair_path_private('automation')) | 1033 ssh_private_key_path = str(c.key_pair_path_private('automation')) |
1019 | 1034 |
1020 for instance in instances: | 1035 for instance in instances: |
1021 client = wait_for_ssh( | 1036 client = wait_for_ssh( |
1022 instance.public_ip_address, 22, | 1037 instance.public_ip_address, |
1038 22, | |
1023 username='hg', | 1039 username='hg', |
1024 key_filename=ssh_private_key_path) | 1040 key_filename=ssh_private_key_path, |
1041 ) | |
1025 | 1042 |
1026 instance.ssh_client = client | 1043 instance.ssh_client = client |
1027 instance.ssh_private_key_path = ssh_private_key_path | 1044 instance.ssh_private_key_path = ssh_private_key_path |
1028 | 1045 |
1029 try: | 1046 try: |
1031 finally: | 1048 finally: |
1032 for instance in instances: | 1049 for instance in instances: |
1033 instance.ssh_client.close() | 1050 instance.ssh_client.close() |
1034 | 1051 |
1035 | 1052 |
1036 def ensure_windows_dev_ami(c: AWSConnection, prefix='hg-', | 1053 def ensure_windows_dev_ami( |
1037 base_image_name=WINDOWS_BASE_IMAGE_NAME): | 1054 c: AWSConnection, prefix='hg-', base_image_name=WINDOWS_BASE_IMAGE_NAME |
1055 ): | |
1038 """Ensure Windows Development AMI is available and up-to-date. | 1056 """Ensure Windows Development AMI is available and up-to-date. |
1039 | 1057 |
1040 If necessary, a modern AMI will be built by starting a temporary EC2 | 1058 If necessary, a modern AMI will be built by starting a temporary EC2 |
1041 instance and bootstrapping it. | 1059 instance and bootstrapping it. |
1042 | 1060 |
1098 commands.insert(0, 'Set-MpPreference -DisableRealtimeMonitoring $true') | 1116 commands.insert(0, 'Set-MpPreference -DisableRealtimeMonitoring $true') |
1099 commands.append('Set-MpPreference -DisableRealtimeMonitoring $false') | 1117 commands.append('Set-MpPreference -DisableRealtimeMonitoring $false') |
1100 | 1118 |
1101 # Compute a deterministic fingerprint to determine whether image needs | 1119 # Compute a deterministic fingerprint to determine whether image needs |
1102 # to be regenerated. | 1120 # to be regenerated. |
1103 fingerprint = resolve_fingerprint({ | 1121 fingerprint = resolve_fingerprint( |
1104 'instance_config': config, | 1122 { |
1105 'user_data': WINDOWS_USER_DATA, | 1123 'instance_config': config, |
1106 'initial_bootstrap': WINDOWS_BOOTSTRAP_POWERSHELL, | 1124 'user_data': WINDOWS_USER_DATA, |
1107 'bootstrap_commands': commands, | 1125 'initial_bootstrap': WINDOWS_BOOTSTRAP_POWERSHELL, |
1108 'base_image_name': base_image_name, | 1126 'bootstrap_commands': commands, |
1109 }) | 1127 'base_image_name': base_image_name, |
1128 } | |
1129 ) | |
1110 | 1130 |
1111 existing_image = find_and_reconcile_image(ec2resource, name, fingerprint) | 1131 existing_image = find_and_reconcile_image(ec2resource, name, fingerprint) |
1112 | 1132 |
1113 if existing_image: | 1133 if existing_image: |
1114 return existing_image | 1134 return existing_image |
1129 print('installing Windows features...') | 1149 print('installing Windows features...') |
1130 run_ssm_command( | 1150 run_ssm_command( |
1131 ssmclient, | 1151 ssmclient, |
1132 [instance], | 1152 [instance], |
1133 'AWS-RunPowerShellScript', | 1153 'AWS-RunPowerShellScript', |
1134 { | 1154 {'commands': WINDOWS_BOOTSTRAP_POWERSHELL.split('\n'),}, |
1135 'commands': WINDOWS_BOOTSTRAP_POWERSHELL.split('\n'), | |
1136 }, | |
1137 ) | 1155 ) |
1138 | 1156 |
1139 # Reboot so all updates are fully applied. | 1157 # Reboot so all updates are fully applied. |
1140 # | 1158 # |
1141 # We don't use instance.reboot() here because it is asynchronous and | 1159 # We don't use instance.reboot() here because it is asynchronous and |
1143 # a while to stop and we may start trying to interact with the instance | 1161 # a while to stop and we may start trying to interact with the instance |
1144 # before it has rebooted. | 1162 # before it has rebooted. |
1145 print('rebooting instance %s' % instance.id) | 1163 print('rebooting instance %s' % instance.id) |
1146 instance.stop() | 1164 instance.stop() |
1147 ec2client.get_waiter('instance_stopped').wait( | 1165 ec2client.get_waiter('instance_stopped').wait( |
1148 InstanceIds=[instance.id], | 1166 InstanceIds=[instance.id], WaiterConfig={'Delay': 5,} |
1149 WaiterConfig={ | 1167 ) |
1150 'Delay': 5, | |
1151 }) | |
1152 | 1168 |
1153 instance.start() | 1169 instance.start() |
1154 wait_for_ip_addresses([instance]) | 1170 wait_for_ip_addresses([instance]) |
1155 | 1171 |
1156 # There is a race condition here between the User Data PS script running | 1172 # There is a race condition here between the User Data PS script running |
1157 # and us connecting to WinRM. This can manifest as | 1173 # and us connecting to WinRM. This can manifest as |
1158 # "AuthorizationManager check failed" failures during run_powershell(). | 1174 # "AuthorizationManager check failed" failures during run_powershell(). |
1159 # TODO figure out a workaround. | 1175 # TODO figure out a workaround. |
1160 | 1176 |
1161 print('waiting for Windows Remote Management to come back...') | 1177 print('waiting for Windows Remote Management to come back...') |
1162 client = wait_for_winrm(instance.public_ip_address, 'Administrator', | 1178 client = wait_for_winrm( |
1163 c.automation.default_password()) | 1179 instance.public_ip_address, |
1180 'Administrator', | |
1181 c.automation.default_password(), | |
1182 ) | |
1164 print('established WinRM connection to %s' % instance.id) | 1183 print('established WinRM connection to %s' % instance.id) |
1165 instance.winrm_client = client | 1184 instance.winrm_client = client |
1166 | 1185 |
1167 print('bootstrapping instance...') | 1186 print('bootstrapping instance...') |
1168 run_powershell(instance.winrm_client, '\n'.join(commands)) | 1187 run_powershell(instance.winrm_client, '\n'.join(commands)) |
1169 | 1188 |
1170 print('bootstrap completed; stopping %s to create image' % instance.id) | 1189 print('bootstrap completed; stopping %s to create image' % instance.id) |
1171 return create_ami_from_instance(ec2client, instance, name, | 1190 return create_ami_from_instance( |
1172 'Mercurial Windows development environment', | 1191 ec2client, |
1173 fingerprint) | 1192 instance, |
1193 name, | |
1194 'Mercurial Windows development environment', | |
1195 fingerprint, | |
1196 ) | |
1174 | 1197 |
1175 | 1198 |
1176 @contextlib.contextmanager | 1199 @contextlib.contextmanager |
1177 def temporary_windows_dev_instances(c: AWSConnection, image, instance_type, | 1200 def temporary_windows_dev_instances( |
1178 prefix='hg-', disable_antivirus=False): | 1201 c: AWSConnection, |
1202 image, | |
1203 instance_type, | |
1204 prefix='hg-', | |
1205 disable_antivirus=False, | |
1206 ): | |
1179 """Create a temporary Windows development EC2 instance. | 1207 """Create a temporary Windows development EC2 instance. |
1180 | 1208 |
1181 Context manager resolves to the list of ``EC2.Instance`` that were created. | 1209 Context manager resolves to the list of ``EC2.Instance`` that were created. |
1182 """ | 1210 """ |
1183 config = { | 1211 config = { |
1203 with create_temp_windows_ec2_instances(c, config) as instances: | 1231 with create_temp_windows_ec2_instances(c, config) as instances: |
1204 if disable_antivirus: | 1232 if disable_antivirus: |
1205 for instance in instances: | 1233 for instance in instances: |
1206 run_powershell( | 1234 run_powershell( |
1207 instance.winrm_client, | 1235 instance.winrm_client, |
1208 'Set-MpPreference -DisableRealtimeMonitoring $true') | 1236 'Set-MpPreference -DisableRealtimeMonitoring $true', |
1237 ) | |
1209 | 1238 |
1210 yield instances | 1239 yield instances |