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', |
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) |
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 |
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 |
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 |
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 = { |