##// END OF EJS Templates
automation: wait for instance profiles and roles...
Gregory Szorc -
r42464:8dc22a20 default
parent child Browse files
Show More
@@ -1,884 +1,892 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 .winrm import (
23 23 run_powershell,
24 24 wait_for_winrm,
25 25 )
26 26
27 27
28 28 SOURCE_ROOT = pathlib.Path(os.path.abspath(__file__)).parent.parent.parent.parent
29 29
30 30 INSTALL_WINDOWS_DEPENDENCIES = (SOURCE_ROOT / 'contrib' /
31 31 'install-windows-dependencies.ps1')
32 32
33 33
34 34 KEY_PAIRS = {
35 35 'automation',
36 36 }
37 37
38 38
39 39 SECURITY_GROUPS = {
40 40 'windows-dev-1': {
41 41 'description': 'Mercurial Windows instances that perform build automation',
42 42 'ingress': [
43 43 {
44 44 'FromPort': 22,
45 45 'ToPort': 22,
46 46 'IpProtocol': 'tcp',
47 47 'IpRanges': [
48 48 {
49 49 'CidrIp': '0.0.0.0/0',
50 50 'Description': 'SSH from entire Internet',
51 51 },
52 52 ],
53 53 },
54 54 {
55 55 'FromPort': 3389,
56 56 'ToPort': 3389,
57 57 'IpProtocol': 'tcp',
58 58 'IpRanges': [
59 59 {
60 60 'CidrIp': '0.0.0.0/0',
61 61 'Description': 'RDP from entire Internet',
62 62 },
63 63 ],
64 64
65 65 },
66 66 {
67 67 'FromPort': 5985,
68 68 'ToPort': 5986,
69 69 'IpProtocol': 'tcp',
70 70 'IpRanges': [
71 71 {
72 72 'CidrIp': '0.0.0.0/0',
73 73 'Description': 'PowerShell Remoting (Windows Remote Management)',
74 74 },
75 75 ],
76 76 }
77 77 ],
78 78 },
79 79 }
80 80
81 81
82 82 IAM_ROLES = {
83 83 'ephemeral-ec2-role-1': {
84 84 'description': 'Mercurial temporary EC2 instances',
85 85 'policy_arns': [
86 86 'arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM',
87 87 ],
88 88 },
89 89 }
90 90
91 91
92 92 ASSUME_ROLE_POLICY_DOCUMENT = '''
93 93 {
94 94 "Version": "2012-10-17",
95 95 "Statement": [
96 96 {
97 97 "Effect": "Allow",
98 98 "Principal": {
99 99 "Service": "ec2.amazonaws.com"
100 100 },
101 101 "Action": "sts:AssumeRole"
102 102 }
103 103 ]
104 104 }
105 105 '''.strip()
106 106
107 107
108 108 IAM_INSTANCE_PROFILES = {
109 109 'ephemeral-ec2-1': {
110 110 'roles': [
111 111 'ephemeral-ec2-role-1',
112 112 ],
113 113 }
114 114 }
115 115
116 116
117 117 # User Data for Windows EC2 instance. Mainly used to set the password
118 118 # and configure WinRM.
119 119 # Inspired by the User Data script used by Packer
120 120 # (from https://www.packer.io/intro/getting-started/build-image.html).
121 121 WINDOWS_USER_DATA = r'''
122 122 <powershell>
123 123
124 124 # TODO enable this once we figure out what is failing.
125 125 #$ErrorActionPreference = "stop"
126 126
127 127 # Set administrator password
128 128 net user Administrator "%s"
129 129 wmic useraccount where "name='Administrator'" set PasswordExpires=FALSE
130 130
131 131 # First, make sure WinRM can't be connected to
132 132 netsh advfirewall firewall set rule name="Windows Remote Management (HTTP-In)" new enable=yes action=block
133 133
134 134 # Delete any existing WinRM listeners
135 135 winrm delete winrm/config/listener?Address=*+Transport=HTTP 2>$Null
136 136 winrm delete winrm/config/listener?Address=*+Transport=HTTPS 2>$Null
137 137
138 138 # Create a new WinRM listener and configure
139 139 winrm create winrm/config/listener?Address=*+Transport=HTTP
140 140 winrm set winrm/config/winrs '@{MaxMemoryPerShellMB="0"}'
141 141 winrm set winrm/config '@{MaxTimeoutms="7200000"}'
142 142 winrm set winrm/config/service '@{AllowUnencrypted="true"}'
143 143 winrm set winrm/config/service '@{MaxConcurrentOperationsPerUser="12000"}'
144 144 winrm set winrm/config/service/auth '@{Basic="true"}'
145 145 winrm set winrm/config/client/auth '@{Basic="true"}'
146 146
147 147 # Configure UAC to allow privilege elevation in remote shells
148 148 $Key = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
149 149 $Setting = 'LocalAccountTokenFilterPolicy'
150 150 Set-ItemProperty -Path $Key -Name $Setting -Value 1 -Force
151 151
152 152 # Configure and restart the WinRM Service; Enable the required firewall exception
153 153 Stop-Service -Name WinRM
154 154 Set-Service -Name WinRM -StartupType Automatic
155 155 netsh advfirewall firewall set rule name="Windows Remote Management (HTTP-In)" new action=allow localip=any remoteip=any
156 156 Start-Service -Name WinRM
157 157
158 158 # Disable firewall on private network interfaces so prompts don't appear.
159 159 Set-NetFirewallProfile -Name private -Enabled false
160 160 </powershell>
161 161 '''.lstrip()
162 162
163 163
164 164 WINDOWS_BOOTSTRAP_POWERSHELL = '''
165 165 Write-Output "installing PowerShell dependencies"
166 166 Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force
167 167 Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
168 168 Install-Module -Name OpenSSHUtils -RequiredVersion 0.0.2.0
169 169
170 170 Write-Output "installing OpenSSL server"
171 171 Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
172 172 # Various tools will attempt to use older versions of .NET. So we enable
173 173 # the feature that provides them so it doesn't have to be auto-enabled
174 174 # later.
175 175 Write-Output "enabling .NET Framework feature"
176 176 Install-WindowsFeature -Name Net-Framework-Core
177 177 '''
178 178
179 179
180 180 class AWSConnection:
181 181 """Manages the state of a connection with AWS."""
182 182
183 183 def __init__(self, automation, region: str, ensure_ec2_state: bool=True):
184 184 self.automation = automation
185 185 self.local_state_path = automation.state_path
186 186
187 187 self.prefix = 'hg-'
188 188
189 189 self.session = boto3.session.Session(region_name=region)
190 190 self.ec2client = self.session.client('ec2')
191 191 self.ec2resource = self.session.resource('ec2')
192 192 self.iamclient = self.session.client('iam')
193 193 self.iamresource = self.session.resource('iam')
194 194 self.security_groups = {}
195 195
196 196 if ensure_ec2_state:
197 197 ensure_key_pairs(automation.state_path, self.ec2resource)
198 198 self.security_groups = ensure_security_groups(self.ec2resource)
199 ensure_iam_state(self.iamresource)
199 ensure_iam_state(self.iamclient, self.iamresource)
200 200
201 201 def key_pair_path_private(self, name):
202 202 """Path to a key pair private key file."""
203 203 return self.local_state_path / 'keys' / ('keypair-%s' % name)
204 204
205 205 def key_pair_path_public(self, name):
206 206 return self.local_state_path / 'keys' / ('keypair-%s.pub' % name)
207 207
208 208
209 209 def rsa_key_fingerprint(p: pathlib.Path):
210 210 """Compute the fingerprint of an RSA private key."""
211 211
212 212 # TODO use rsa package.
213 213 res = subprocess.run(
214 214 ['openssl', 'pkcs8', '-in', str(p), '-nocrypt', '-topk8',
215 215 '-outform', 'DER'],
216 216 capture_output=True,
217 217 check=True)
218 218
219 219 sha1 = hashlib.sha1(res.stdout).hexdigest()
220 220 return ':'.join(a + b for a, b in zip(sha1[::2], sha1[1::2]))
221 221
222 222
223 223 def ensure_key_pairs(state_path: pathlib.Path, ec2resource, prefix='hg-'):
224 224 remote_existing = {}
225 225
226 226 for kpi in ec2resource.key_pairs.all():
227 227 if kpi.name.startswith(prefix):
228 228 remote_existing[kpi.name[len(prefix):]] = kpi.key_fingerprint
229 229
230 230 # Validate that we have these keys locally.
231 231 key_path = state_path / 'keys'
232 232 key_path.mkdir(exist_ok=True, mode=0o700)
233 233
234 234 def remove_remote(name):
235 235 print('deleting key pair %s' % name)
236 236 key = ec2resource.KeyPair(name)
237 237 key.delete()
238 238
239 239 def remove_local(name):
240 240 pub_full = key_path / ('keypair-%s.pub' % name)
241 241 priv_full = key_path / ('keypair-%s' % name)
242 242
243 243 print('removing %s' % pub_full)
244 244 pub_full.unlink()
245 245 print('removing %s' % priv_full)
246 246 priv_full.unlink()
247 247
248 248 local_existing = {}
249 249
250 250 for f in sorted(os.listdir(key_path)):
251 251 if not f.startswith('keypair-') or not f.endswith('.pub'):
252 252 continue
253 253
254 254 name = f[len('keypair-'):-len('.pub')]
255 255
256 256 pub_full = key_path / f
257 257 priv_full = key_path / ('keypair-%s' % name)
258 258
259 259 with open(pub_full, 'r', encoding='ascii') as fh:
260 260 data = fh.read()
261 261
262 262 if not data.startswith('ssh-rsa '):
263 263 print('unexpected format for key pair file: %s; removing' %
264 264 pub_full)
265 265 pub_full.unlink()
266 266 priv_full.unlink()
267 267 continue
268 268
269 269 local_existing[name] = rsa_key_fingerprint(priv_full)
270 270
271 271 for name in sorted(set(remote_existing) | set(local_existing)):
272 272 if name not in local_existing:
273 273 actual = '%s%s' % (prefix, name)
274 274 print('remote key %s does not exist locally' % name)
275 275 remove_remote(actual)
276 276 del remote_existing[name]
277 277
278 278 elif name not in remote_existing:
279 279 print('local key %s does not exist remotely' % name)
280 280 remove_local(name)
281 281 del local_existing[name]
282 282
283 283 elif remote_existing[name] != local_existing[name]:
284 284 print('key fingerprint mismatch for %s; '
285 285 'removing from local and remote' % name)
286 286 remove_local(name)
287 287 remove_remote('%s%s' % (prefix, name))
288 288 del local_existing[name]
289 289 del remote_existing[name]
290 290
291 291 missing = KEY_PAIRS - set(remote_existing)
292 292
293 293 for name in sorted(missing):
294 294 actual = '%s%s' % (prefix, name)
295 295 print('creating key pair %s' % actual)
296 296
297 297 priv_full = key_path / ('keypair-%s' % name)
298 298 pub_full = key_path / ('keypair-%s.pub' % name)
299 299
300 300 kp = ec2resource.create_key_pair(KeyName=actual)
301 301
302 302 with priv_full.open('w', encoding='ascii') as fh:
303 303 fh.write(kp.key_material)
304 304 fh.write('\n')
305 305
306 306 priv_full.chmod(0o0600)
307 307
308 308 # SSH public key can be extracted via `ssh-keygen`.
309 309 with pub_full.open('w', encoding='ascii') as fh:
310 310 subprocess.run(
311 311 ['ssh-keygen', '-y', '-f', str(priv_full)],
312 312 stdout=fh,
313 313 check=True)
314 314
315 315 pub_full.chmod(0o0600)
316 316
317 317
318 318 def delete_instance_profile(profile):
319 319 for role in profile.roles:
320 320 print('removing role %s from instance profile %s' % (role.name,
321 321 profile.name))
322 322 profile.remove_role(RoleName=role.name)
323 323
324 324 print('deleting instance profile %s' % profile.name)
325 325 profile.delete()
326 326
327 327
328 def ensure_iam_state(iamresource, prefix='hg-'):
328 def ensure_iam_state(iamclient, iamresource, prefix='hg-'):
329 329 """Ensure IAM state is in sync with our canonical definition."""
330 330
331 331 remote_profiles = {}
332 332
333 333 for profile in iamresource.instance_profiles.all():
334 334 if profile.name.startswith(prefix):
335 335 remote_profiles[profile.name[len(prefix):]] = profile
336 336
337 337 for name in sorted(set(remote_profiles) - set(IAM_INSTANCE_PROFILES)):
338 338 delete_instance_profile(remote_profiles[name])
339 339 del remote_profiles[name]
340 340
341 341 remote_roles = {}
342 342
343 343 for role in iamresource.roles.all():
344 344 if role.name.startswith(prefix):
345 345 remote_roles[role.name[len(prefix):]] = role
346 346
347 347 for name in sorted(set(remote_roles) - set(IAM_ROLES)):
348 348 role = remote_roles[name]
349 349
350 350 print('removing role %s' % role.name)
351 351 role.delete()
352 352 del remote_roles[name]
353 353
354 354 # We've purged remote state that doesn't belong. Create missing
355 355 # instance profiles and roles.
356 356 for name in sorted(set(IAM_INSTANCE_PROFILES) - set(remote_profiles)):
357 357 actual = '%s%s' % (prefix, name)
358 358 print('creating IAM instance profile %s' % actual)
359 359
360 360 profile = iamresource.create_instance_profile(
361 361 InstanceProfileName=actual)
362 362 remote_profiles[name] = profile
363 363
364 waiter = iamclient.get_waiter('instance_profile_exists')
365 waiter.wait(InstanceProfileName=actual)
366 print('IAM instance profile %s is available' % actual)
367
364 368 for name in sorted(set(IAM_ROLES) - set(remote_roles)):
365 369 entry = IAM_ROLES[name]
366 370
367 371 actual = '%s%s' % (prefix, name)
368 372 print('creating IAM role %s' % actual)
369 373
370 374 role = iamresource.create_role(
371 375 RoleName=actual,
372 376 Description=entry['description'],
373 377 AssumeRolePolicyDocument=ASSUME_ROLE_POLICY_DOCUMENT,
374 378 )
375 379
380 waiter = iamclient.get_waiter('role_exists')
381 waiter.wait(RoleName=actual)
382 print('IAM role %s is available' % actual)
383
376 384 remote_roles[name] = role
377 385
378 386 for arn in entry['policy_arns']:
379 387 print('attaching policy %s to %s' % (arn, role.name))
380 388 role.attach_policy(PolicyArn=arn)
381 389
382 390 # Now reconcile state of profiles.
383 391 for name, meta in sorted(IAM_INSTANCE_PROFILES.items()):
384 392 profile = remote_profiles[name]
385 393 wanted = {'%s%s' % (prefix, role) for role in meta['roles']}
386 394 have = {role.name for role in profile.roles}
387 395
388 396 for role in sorted(have - wanted):
389 397 print('removing role %s from %s' % (role, profile.name))
390 398 profile.remove_role(RoleName=role)
391 399
392 400 for role in sorted(wanted - have):
393 401 print('adding role %s to %s' % (role, profile.name))
394 402 profile.add_role(RoleName=role)
395 403
396 404
397 405 def find_windows_server_2019_image(ec2resource):
398 406 """Find the Amazon published Windows Server 2019 base image."""
399 407
400 408 images = ec2resource.images.filter(
401 409 Filters=[
402 410 {
403 411 'Name': 'owner-alias',
404 412 'Values': ['amazon'],
405 413 },
406 414 {
407 415 'Name': 'state',
408 416 'Values': ['available'],
409 417 },
410 418 {
411 419 'Name': 'image-type',
412 420 'Values': ['machine'],
413 421 },
414 422 {
415 423 'Name': 'name',
416 424 'Values': ['Windows_Server-2019-English-Full-Base-2019.02.13'],
417 425 },
418 426 ])
419 427
420 428 for image in images:
421 429 return image
422 430
423 431 raise Exception('unable to find Windows Server 2019 image')
424 432
425 433
426 434 def ensure_security_groups(ec2resource, prefix='hg-'):
427 435 """Ensure all necessary Mercurial security groups are present.
428 436
429 437 All security groups are prefixed with ``hg-`` by default. Any security
430 438 groups having this prefix but aren't in our list are deleted.
431 439 """
432 440 existing = {}
433 441
434 442 for group in ec2resource.security_groups.all():
435 443 if group.group_name.startswith(prefix):
436 444 existing[group.group_name[len(prefix):]] = group
437 445
438 446 purge = set(existing) - set(SECURITY_GROUPS)
439 447
440 448 for name in sorted(purge):
441 449 group = existing[name]
442 450 print('removing legacy security group: %s' % group.group_name)
443 451 group.delete()
444 452
445 453 security_groups = {}
446 454
447 455 for name, group in sorted(SECURITY_GROUPS.items()):
448 456 if name in existing:
449 457 security_groups[name] = existing[name]
450 458 continue
451 459
452 460 actual = '%s%s' % (prefix, name)
453 461 print('adding security group %s' % actual)
454 462
455 463 group_res = ec2resource.create_security_group(
456 464 Description=group['description'],
457 465 GroupName=actual,
458 466 )
459 467
460 468 group_res.authorize_ingress(
461 469 IpPermissions=group['ingress'],
462 470 )
463 471
464 472 security_groups[name] = group_res
465 473
466 474 return security_groups
467 475
468 476
469 477 def terminate_ec2_instances(ec2resource, prefix='hg-'):
470 478 """Terminate all EC2 instances managed by us."""
471 479 waiting = []
472 480
473 481 for instance in ec2resource.instances.all():
474 482 if instance.state['Name'] == 'terminated':
475 483 continue
476 484
477 485 for tag in instance.tags or []:
478 486 if tag['Key'] == 'Name' and tag['Value'].startswith(prefix):
479 487 print('terminating %s' % instance.id)
480 488 instance.terminate()
481 489 waiting.append(instance)
482 490
483 491 for instance in waiting:
484 492 instance.wait_until_terminated()
485 493
486 494
487 495 def remove_resources(c, prefix='hg-'):
488 496 """Purge all of our resources in this EC2 region."""
489 497 ec2resource = c.ec2resource
490 498 iamresource = c.iamresource
491 499
492 500 terminate_ec2_instances(ec2resource, prefix=prefix)
493 501
494 502 for image in ec2resource.images.filter(Owners=['self']):
495 503 if image.name.startswith(prefix):
496 504 remove_ami(ec2resource, image)
497 505
498 506 for group in ec2resource.security_groups.all():
499 507 if group.group_name.startswith(prefix):
500 508 print('removing security group %s' % group.group_name)
501 509 group.delete()
502 510
503 511 for profile in iamresource.instance_profiles.all():
504 512 if profile.name.startswith(prefix):
505 513 delete_instance_profile(profile)
506 514
507 515 for role in iamresource.roles.all():
508 516 if role.name.startswith(prefix):
509 517 for p in role.attached_policies.all():
510 518 print('detaching policy %s from %s' % (p.arn, role.name))
511 519 role.detach_policy(PolicyArn=p.arn)
512 520
513 521 print('removing role %s' % role.name)
514 522 role.delete()
515 523
516 524
517 525 def wait_for_ip_addresses(instances):
518 526 """Wait for the public IP addresses of an iterable of instances."""
519 527 for instance in instances:
520 528 while True:
521 529 if not instance.public_ip_address:
522 530 time.sleep(2)
523 531 instance.reload()
524 532 continue
525 533
526 534 print('public IP address for %s: %s' % (
527 535 instance.id, instance.public_ip_address))
528 536 break
529 537
530 538
531 539 def remove_ami(ec2resource, image):
532 540 """Remove an AMI and its underlying snapshots."""
533 541 snapshots = []
534 542
535 543 for device in image.block_device_mappings:
536 544 if 'Ebs' in device:
537 545 snapshots.append(ec2resource.Snapshot(device['Ebs']['SnapshotId']))
538 546
539 547 print('deregistering %s' % image.id)
540 548 image.deregister()
541 549
542 550 for snapshot in snapshots:
543 551 print('deleting snapshot %s' % snapshot.id)
544 552 snapshot.delete()
545 553
546 554
547 555 def wait_for_ssm(ssmclient, instances):
548 556 """Wait for SSM to come online for an iterable of instance IDs."""
549 557 while True:
550 558 res = ssmclient.describe_instance_information(
551 559 Filters=[
552 560 {
553 561 'Key': 'InstanceIds',
554 562 'Values': [i.id for i in instances],
555 563 },
556 564 ],
557 565 )
558 566
559 567 available = len(res['InstanceInformationList'])
560 568 wanted = len(instances)
561 569
562 570 print('%d/%d instances available in SSM' % (available, wanted))
563 571
564 572 if available == wanted:
565 573 return
566 574
567 575 time.sleep(2)
568 576
569 577
570 578 def run_ssm_command(ssmclient, instances, document_name, parameters):
571 579 """Run a PowerShell script on an EC2 instance."""
572 580
573 581 res = ssmclient.send_command(
574 582 InstanceIds=[i.id for i in instances],
575 583 DocumentName=document_name,
576 584 Parameters=parameters,
577 585 CloudWatchOutputConfig={
578 586 'CloudWatchOutputEnabled': True,
579 587 },
580 588 )
581 589
582 590 command_id = res['Command']['CommandId']
583 591
584 592 for instance in instances:
585 593 while True:
586 594 try:
587 595 res = ssmclient.get_command_invocation(
588 596 CommandId=command_id,
589 597 InstanceId=instance.id,
590 598 )
591 599 except botocore.exceptions.ClientError as e:
592 600 if e.response['Error']['Code'] == 'InvocationDoesNotExist':
593 601 print('could not find SSM command invocation; waiting')
594 602 time.sleep(1)
595 603 continue
596 604 else:
597 605 raise
598 606
599 607 if res['Status'] == 'Success':
600 608 break
601 609 elif res['Status'] in ('Pending', 'InProgress', 'Delayed'):
602 610 time.sleep(2)
603 611 else:
604 612 raise Exception('command failed on %s: %s' % (
605 613 instance.id, res['Status']))
606 614
607 615
608 616 @contextlib.contextmanager
609 617 def temporary_ec2_instances(ec2resource, config):
610 618 """Create temporary EC2 instances.
611 619
612 620 This is a proxy to ``ec2client.run_instances(**config)`` that takes care of
613 621 managing the lifecycle of the instances.
614 622
615 623 When the context manager exits, the instances are terminated.
616 624
617 625 The context manager evaluates to the list of data structures
618 626 describing each created instance. The instances may not be available
619 627 for work immediately: it is up to the caller to wait for the instance
620 628 to start responding.
621 629 """
622 630
623 631 ids = None
624 632
625 633 try:
626 634 res = ec2resource.create_instances(**config)
627 635
628 636 ids = [i.id for i in res]
629 637 print('started instances: %s' % ' '.join(ids))
630 638
631 639 yield res
632 640 finally:
633 641 if ids:
634 642 print('terminating instances: %s' % ' '.join(ids))
635 643 for instance in res:
636 644 instance.terminate()
637 645 print('terminated %d instances' % len(ids))
638 646
639 647
640 648 @contextlib.contextmanager
641 649 def create_temp_windows_ec2_instances(c: AWSConnection, config):
642 650 """Create temporary Windows EC2 instances.
643 651
644 652 This is a higher-level wrapper around ``create_temp_ec2_instances()`` that
645 653 configures the Windows instance for Windows Remote Management. The emitted
646 654 instances will have a ``winrm_client`` attribute containing a
647 655 ``pypsrp.client.Client`` instance bound to the instance.
648 656 """
649 657 if 'IamInstanceProfile' in config:
650 658 raise ValueError('IamInstanceProfile cannot be provided in config')
651 659 if 'UserData' in config:
652 660 raise ValueError('UserData cannot be provided in config')
653 661
654 662 password = c.automation.default_password()
655 663
656 664 config = copy.deepcopy(config)
657 665 config['IamInstanceProfile'] = {
658 666 'Name': 'hg-ephemeral-ec2-1',
659 667 }
660 668 config.setdefault('TagSpecifications', []).append({
661 669 'ResourceType': 'instance',
662 670 'Tags': [{'Key': 'Name', 'Value': 'hg-temp-windows'}],
663 671 })
664 672 config['UserData'] = WINDOWS_USER_DATA % password
665 673
666 674 with temporary_ec2_instances(c.ec2resource, config) as instances:
667 675 wait_for_ip_addresses(instances)
668 676
669 677 print('waiting for Windows Remote Management service...')
670 678
671 679 for instance in instances:
672 680 client = wait_for_winrm(instance.public_ip_address, 'Administrator', password)
673 681 print('established WinRM connection to %s' % instance.id)
674 682 instance.winrm_client = client
675 683
676 684 yield instances
677 685
678 686
679 687 def ensure_windows_dev_ami(c: AWSConnection, prefix='hg-'):
680 688 """Ensure Windows Development AMI is available and up-to-date.
681 689
682 690 If necessary, a modern AMI will be built by starting a temporary EC2
683 691 instance and bootstrapping it.
684 692
685 693 Obsolete AMIs will be deleted so there is only a single AMI having the
686 694 desired name.
687 695
688 696 Returns an ``ec2.Image`` of either an existing AMI or a newly-built
689 697 one.
690 698 """
691 699 ec2client = c.ec2client
692 700 ec2resource = c.ec2resource
693 701 ssmclient = c.session.client('ssm')
694 702
695 703 name = '%s%s' % (prefix, 'windows-dev')
696 704
697 705 config = {
698 706 'BlockDeviceMappings': [
699 707 {
700 708 'DeviceName': '/dev/sda1',
701 709 'Ebs': {
702 710 'DeleteOnTermination': True,
703 711 'VolumeSize': 32,
704 712 'VolumeType': 'gp2',
705 713 },
706 714 }
707 715 ],
708 716 'ImageId': find_windows_server_2019_image(ec2resource).id,
709 717 'InstanceInitiatedShutdownBehavior': 'stop',
710 718 'InstanceType': 't3.medium',
711 719 'KeyName': '%sautomation' % prefix,
712 720 'MaxCount': 1,
713 721 'MinCount': 1,
714 722 'SecurityGroupIds': [c.security_groups['windows-dev-1'].id],
715 723 }
716 724
717 725 commands = [
718 726 # Need to start the service so sshd_config is generated.
719 727 'Start-Service sshd',
720 728 'Write-Output "modifying sshd_config"',
721 729 r'$content = Get-Content C:\ProgramData\ssh\sshd_config',
722 730 '$content = $content -replace "Match Group administrators","" -replace "AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys",""',
723 731 r'$content | Set-Content C:\ProgramData\ssh\sshd_config',
724 732 'Import-Module OpenSSHUtils',
725 733 r'Repair-SshdConfigPermission C:\ProgramData\ssh\sshd_config -Confirm:$false',
726 734 'Restart-Service sshd',
727 735 'Write-Output "installing OpenSSL client"',
728 736 'Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0',
729 737 'Set-Service -Name sshd -StartupType "Automatic"',
730 738 'Write-Output "OpenSSH server running"',
731 739 ]
732 740
733 741 with INSTALL_WINDOWS_DEPENDENCIES.open('r', encoding='utf-8') as fh:
734 742 commands.extend(l.rstrip() for l in fh)
735 743
736 744 # Disable Windows Defender when bootstrapping because it just slows
737 745 # things down.
738 746 commands.insert(0, 'Set-MpPreference -DisableRealtimeMonitoring $true')
739 747 commands.append('Set-MpPreference -DisableRealtimeMonitoring $false')
740 748
741 749 # Compute a deterministic fingerprint to determine whether image needs
742 750 # to be regenerated.
743 751 fingerprint = {
744 752 'instance_config': config,
745 753 'user_data': WINDOWS_USER_DATA,
746 754 'initial_bootstrap': WINDOWS_BOOTSTRAP_POWERSHELL,
747 755 'bootstrap_commands': commands,
748 756 }
749 757
750 758 fingerprint = json.dumps(fingerprint, sort_keys=True)
751 759 fingerprint = hashlib.sha256(fingerprint.encode('utf-8')).hexdigest()
752 760
753 761 # Find existing AMIs with this name and delete the ones that are invalid.
754 762 # Store a reference to a good image so it can be returned one the
755 763 # image state is reconciled.
756 764 images = ec2resource.images.filter(
757 765 Filters=[{'Name': 'name', 'Values': [name]}])
758 766
759 767 existing_image = None
760 768
761 769 for image in images:
762 770 if image.tags is None:
763 771 print('image %s for %s lacks required tags; removing' % (
764 772 image.id, image.name))
765 773 remove_ami(ec2resource, image)
766 774 else:
767 775 tags = {t['Key']: t['Value'] for t in image.tags}
768 776
769 777 if tags.get('HGIMAGEFINGERPRINT') == fingerprint:
770 778 existing_image = image
771 779 else:
772 780 print('image %s for %s has wrong fingerprint; removing' % (
773 781 image.id, image.name))
774 782 remove_ami(ec2resource, image)
775 783
776 784 if existing_image:
777 785 return existing_image
778 786
779 787 print('no suitable Windows development image found; creating one...')
780 788
781 789 with create_temp_windows_ec2_instances(c, config) as instances:
782 790 assert len(instances) == 1
783 791 instance = instances[0]
784 792
785 793 wait_for_ssm(ssmclient, [instance])
786 794
787 795 # On first boot, install various Windows updates.
788 796 # We would ideally use PowerShell Remoting for this. However, there are
789 797 # trust issues that make it difficult to invoke Windows Update
790 798 # remotely. So we use SSM, which has a mechanism for running Windows
791 799 # Update.
792 800 print('installing Windows features...')
793 801 run_ssm_command(
794 802 ssmclient,
795 803 [instance],
796 804 'AWS-RunPowerShellScript',
797 805 {
798 806 'commands': WINDOWS_BOOTSTRAP_POWERSHELL.split('\n'),
799 807 },
800 808 )
801 809
802 810 # Reboot so all updates are fully applied.
803 811 print('rebooting instance %s' % instance.id)
804 812 ec2client.reboot_instances(InstanceIds=[instance.id])
805 813
806 814 time.sleep(15)
807 815
808 816 print('waiting for Windows Remote Management to come back...')
809 817 client = wait_for_winrm(instance.public_ip_address, 'Administrator',
810 818 c.automation.default_password())
811 819 print('established WinRM connection to %s' % instance.id)
812 820 instance.winrm_client = client
813 821
814 822 print('bootstrapping instance...')
815 823 run_powershell(instance.winrm_client, '\n'.join(commands))
816 824
817 825 print('bootstrap completed; stopping %s to create image' % instance.id)
818 826 instance.stop()
819 827
820 828 ec2client.get_waiter('instance_stopped').wait(
821 829 InstanceIds=[instance.id],
822 830 WaiterConfig={
823 831 'Delay': 5,
824 832 })
825 833 print('%s is stopped' % instance.id)
826 834
827 835 image = instance.create_image(
828 836 Name=name,
829 837 Description='Mercurial Windows development environment',
830 838 )
831 839
832 840 image.create_tags(Tags=[
833 841 {
834 842 'Key': 'HGIMAGEFINGERPRINT',
835 843 'Value': fingerprint,
836 844 },
837 845 ])
838 846
839 847 print('waiting for image %s' % image.id)
840 848
841 849 ec2client.get_waiter('image_available').wait(
842 850 ImageIds=[image.id],
843 851 )
844 852
845 853 print('image %s available as %s' % (image.id, image.name))
846 854
847 855 return image
848 856
849 857
850 858 @contextlib.contextmanager
851 859 def temporary_windows_dev_instances(c: AWSConnection, image, instance_type,
852 860 prefix='hg-', disable_antivirus=False):
853 861 """Create a temporary Windows development EC2 instance.
854 862
855 863 Context manager resolves to the list of ``EC2.Instance`` that were created.
856 864 """
857 865 config = {
858 866 'BlockDeviceMappings': [
859 867 {
860 868 'DeviceName': '/dev/sda1',
861 869 'Ebs': {
862 870 'DeleteOnTermination': True,
863 871 'VolumeSize': 32,
864 872 'VolumeType': 'gp2',
865 873 },
866 874 }
867 875 ],
868 876 'ImageId': image.id,
869 877 'InstanceInitiatedShutdownBehavior': 'stop',
870 878 'InstanceType': instance_type,
871 879 'KeyName': '%sautomation' % prefix,
872 880 'MaxCount': 1,
873 881 'MinCount': 1,
874 882 'SecurityGroupIds': [c.security_groups['windows-dev-1'].id],
875 883 }
876 884
877 885 with create_temp_windows_ec2_instances(c, config) as instances:
878 886 if disable_antivirus:
879 887 for instance in instances:
880 888 run_powershell(
881 889 instance.winrm_client,
882 890 'Set-MpPreference -DisableRealtimeMonitoring $true')
883 891
884 892 yield instances
@@ -1,119 +1,119 b''
1 1 #
2 2 # This file is autogenerated by pip-compile
3 3 # To update, run:
4 4 #
5 5 # pip-compile -U --generate-hashes --output-file contrib/automation/requirements.txt contrib/automation/requirements.txt.in
6 6 #
7 7 asn1crypto==0.24.0 \
8 8 --hash=sha256:2f1adbb7546ed199e3c90ef23ec95c5cf3585bac7d11fb7eb562a3fe89c64e87 \
9 9 --hash=sha256:9d5c20441baf0cb60a4ac34cc447c6c189024b6b4c6cd7877034f4965c464e49 \
10 10 # via cryptography
11 boto3==1.9.111 \
12 --hash=sha256:06414c75d1f62af7d04fd652b38d1e4fd3cfd6b35bad978466af88e2aaecd00d \
13 --hash=sha256:f3b77dff382374773d02411fa47ee408f4f503aeebd837fd9dc9ed8635bc5e8e
14 botocore==1.12.111 \
15 --hash=sha256:6af473c52d5e3e7ff82de5334e9fee96b2d5ec2df5d78bc00cd9937e2573a7a8 \
16 --hash=sha256:9f5123c7be704b17aeacae99b5842ab17bda1f799dd29134de8c70e0a50a45d7 \
11 boto3==1.9.137 \
12 --hash=sha256:882cc4869b47b51dae4b4a900769e72171ff00e0b6bca644b2d7a7ad7378f324 \
13 --hash=sha256:cd503a7e7a04f1c14d2801f9727159dfa88c393b4004e98940fa4aa205d920c8
14 botocore==1.12.137 \
15 --hash=sha256:0d95794f6b1239c75e2c5f966221bcd4b68020fddb5676f757531eedbb612ed8 \
16 --hash=sha256:3213cf48cf2ceee10fc3b93221f2cd1c38521cca7584f547d5c086213cc60f35 \
17 17 # via boto3, s3transfer
18 18 certifi==2019.3.9 \
19 19 --hash=sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5 \
20 20 --hash=sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae \
21 21 # via requests
22 cffi==1.12.2 \
23 --hash=sha256:00b97afa72c233495560a0793cdc86c2571721b4271c0667addc83c417f3d90f \
24 --hash=sha256:0ba1b0c90f2124459f6966a10c03794082a2f3985cd699d7d63c4a8dae113e11 \
25 --hash=sha256:0bffb69da295a4fc3349f2ec7cbe16b8ba057b0a593a92cbe8396e535244ee9d \
26 --hash=sha256:21469a2b1082088d11ccd79dd84157ba42d940064abbfa59cf5f024c19cf4891 \
27 --hash=sha256:2e4812f7fa984bf1ab253a40f1f4391b604f7fc424a3e21f7de542a7f8f7aedf \
28 --hash=sha256:2eac2cdd07b9049dd4e68449b90d3ef1adc7c759463af5beb53a84f1db62e36c \
29 --hash=sha256:2f9089979d7456c74d21303c7851f158833d48fb265876923edcb2d0194104ed \
30 --hash=sha256:3dd13feff00bddb0bd2d650cdb7338f815c1789a91a6f68fdc00e5c5ed40329b \
31 --hash=sha256:4065c32b52f4b142f417af6f33a5024edc1336aa845b9d5a8d86071f6fcaac5a \
32 --hash=sha256:51a4ba1256e9003a3acf508e3b4f4661bebd015b8180cc31849da222426ef585 \
33 --hash=sha256:59888faac06403767c0cf8cfb3f4a777b2939b1fbd9f729299b5384f097f05ea \
34 --hash=sha256:59c87886640574d8b14910840327f5cd15954e26ed0bbd4e7cef95fa5aef218f \
35 --hash=sha256:610fc7d6db6c56a244c2701575f6851461753c60f73f2de89c79bbf1cc807f33 \
36 --hash=sha256:70aeadeecb281ea901bf4230c6222af0248c41044d6f57401a614ea59d96d145 \
37 --hash=sha256:71e1296d5e66c59cd2c0f2d72dc476d42afe02aeddc833d8e05630a0551dad7a \
38 --hash=sha256:8fc7a49b440ea752cfdf1d51a586fd08d395ff7a5d555dc69e84b1939f7ddee3 \
39 --hash=sha256:9b5c2afd2d6e3771d516045a6cfa11a8da9a60e3d128746a7fe9ab36dfe7221f \
40 --hash=sha256:9c759051ebcb244d9d55ee791259ddd158188d15adee3c152502d3b69005e6bd \
41 --hash=sha256:b4d1011fec5ec12aa7cc10c05a2f2f12dfa0adfe958e56ae38dc140614035804 \
42 --hash=sha256:b4f1d6332339ecc61275bebd1f7b674098a66fea11a00c84d1c58851e618dc0d \
43 --hash=sha256:c030cda3dc8e62b814831faa4eb93dd9a46498af8cd1d5c178c2de856972fd92 \
44 --hash=sha256:c2e1f2012e56d61390c0e668c20c4fb0ae667c44d6f6a2eeea5d7148dcd3df9f \
45 --hash=sha256:c37c77d6562074452120fc6c02ad86ec928f5710fbc435a181d69334b4de1d84 \
46 --hash=sha256:c8149780c60f8fd02752d0429246088c6c04e234b895c4a42e1ea9b4de8d27fb \
47 --hash=sha256:cbeeef1dc3c4299bd746b774f019de9e4672f7cc666c777cd5b409f0b746dac7 \
48 --hash=sha256:e113878a446c6228669144ae8a56e268c91b7f1fafae927adc4879d9849e0ea7 \
49 --hash=sha256:e21162bf941b85c0cda08224dade5def9360f53b09f9f259adb85fc7dd0e7b35 \
50 --hash=sha256:fb6934ef4744becbda3143d30c6604718871495a5e36c408431bf33d9c146889 \
22 cffi==1.12.3 \
23 --hash=sha256:041c81822e9f84b1d9c401182e174996f0bae9991f33725d059b771744290774 \
24 --hash=sha256:046ef9a22f5d3eed06334d01b1e836977eeef500d9b78e9ef693f9380ad0b83d \
25 --hash=sha256:066bc4c7895c91812eff46f4b1c285220947d4aa46fa0a2651ff85f2afae9c90 \
26 --hash=sha256:066c7ff148ae33040c01058662d6752fd73fbc8e64787229ea8498c7d7f4041b \
27 --hash=sha256:2444d0c61f03dcd26dbf7600cf64354376ee579acad77aef459e34efcb438c63 \
28 --hash=sha256:300832850b8f7967e278870c5d51e3819b9aad8f0a2c8dbe39ab11f119237f45 \
29 --hash=sha256:34c77afe85b6b9e967bd8154e3855e847b70ca42043db6ad17f26899a3df1b25 \
30 --hash=sha256:46de5fa00f7ac09f020729148ff632819649b3e05a007d286242c4882f7b1dc3 \
31 --hash=sha256:4aa8ee7ba27c472d429b980c51e714a24f47ca296d53f4d7868075b175866f4b \
32 --hash=sha256:4d0004eb4351e35ed950c14c11e734182591465a33e960a4ab5e8d4f04d72647 \
33 --hash=sha256:4e3d3f31a1e202b0f5a35ba3bc4eb41e2fc2b11c1eff38b362de710bcffb5016 \
34 --hash=sha256:50bec6d35e6b1aaeb17f7c4e2b9374ebf95a8975d57863546fa83e8d31bdb8c4 \
35 --hash=sha256:55cad9a6df1e2a1d62063f79d0881a414a906a6962bc160ac968cc03ed3efcfb \
36 --hash=sha256:5662ad4e4e84f1eaa8efce5da695c5d2e229c563f9d5ce5b0113f71321bcf753 \
37 --hash=sha256:59b4dc008f98fc6ee2bb4fd7fc786a8d70000d058c2bbe2698275bc53a8d3fa7 \
38 --hash=sha256:73e1ffefe05e4ccd7bcea61af76f36077b914f92b76f95ccf00b0c1b9186f3f9 \
39 --hash=sha256:a1f0fd46eba2d71ce1589f7e50a9e2ffaeb739fb2c11e8192aa2b45d5f6cc41f \
40 --hash=sha256:a2e85dc204556657661051ff4bab75a84e968669765c8a2cd425918699c3d0e8 \
41 --hash=sha256:a5457d47dfff24882a21492e5815f891c0ca35fefae8aa742c6c263dac16ef1f \
42 --hash=sha256:a8dccd61d52a8dae4a825cdbb7735da530179fea472903eb871a5513b5abbfdc \
43 --hash=sha256:ae61af521ed676cf16ae94f30fe202781a38d7178b6b4ab622e4eec8cefaff42 \
44 --hash=sha256:b012a5edb48288f77a63dba0840c92d0504aa215612da4541b7b42d849bc83a3 \
45 --hash=sha256:d2c5cfa536227f57f97c92ac30c8109688ace8fa4ac086d19d0af47d134e2909 \
46 --hash=sha256:d42b5796e20aacc9d15e66befb7a345454eef794fdb0737d1af593447c6c8f45 \
47 --hash=sha256:dee54f5d30d775f525894d67b1495625dd9322945e7fee00731952e0368ff42d \
48 --hash=sha256:e070535507bd6aa07124258171be2ee8dfc19119c28ca94c9dfb7efd23564512 \
49 --hash=sha256:e1ff2748c84d97b065cc95429814cdba39bcbd77c9c85c89344b317dc0d9cbff \
50 --hash=sha256:ed851c75d1e0e043cbf5ca9a8e1b13c4c90f3fbd863dacb01c0808e2b5204201 \
51 51 # via cryptography
52 52 chardet==3.0.4 \
53 53 --hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae \
54 54 --hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691 \
55 55 # via requests
56 56 cryptography==2.6.1 \
57 57 --hash=sha256:066f815f1fe46020877c5983a7e747ae140f517f1b09030ec098503575265ce1 \
58 58 --hash=sha256:210210d9df0afba9e000636e97810117dc55b7157c903a55716bb73e3ae07705 \
59 59 --hash=sha256:26c821cbeb683facb966045e2064303029d572a87ee69ca5a1bf54bf55f93ca6 \
60 60 --hash=sha256:2afb83308dc5c5255149ff7d3fb9964f7c9ee3d59b603ec18ccf5b0a8852e2b1 \
61 61 --hash=sha256:2db34e5c45988f36f7a08a7ab2b69638994a8923853dec2d4af121f689c66dc8 \
62 62 --hash=sha256:409c4653e0f719fa78febcb71ac417076ae5e20160aec7270c91d009837b9151 \
63 63 --hash=sha256:45a4f4cf4f4e6a55c8128f8b76b4c057027b27d4c67e3fe157fa02f27e37830d \
64 64 --hash=sha256:48eab46ef38faf1031e58dfcc9c3e71756a1108f4c9c966150b605d4a1a7f659 \
65 65 --hash=sha256:6b9e0ae298ab20d371fc26e2129fd683cfc0cfde4d157c6341722de645146537 \
66 66 --hash=sha256:6c4778afe50f413707f604828c1ad1ff81fadf6c110cb669579dea7e2e98a75e \
67 67 --hash=sha256:8c33fb99025d353c9520141f8bc989c2134a1f76bac6369cea060812f5b5c2bb \
68 68 --hash=sha256:9873a1760a274b620a135054b756f9f218fa61ca030e42df31b409f0fb738b6c \
69 69 --hash=sha256:9b069768c627f3f5623b1cbd3248c5e7e92aec62f4c98827059eed7053138cc9 \
70 70 --hash=sha256:9e4ce27a507e4886efbd3c32d120db5089b906979a4debf1d5939ec01b9dd6c5 \
71 71 --hash=sha256:acb424eaca214cb08735f1a744eceb97d014de6530c1ea23beb86d9c6f13c2ad \
72 72 --hash=sha256:c8181c7d77388fe26ab8418bb088b1a1ef5fde058c6926790c8a0a3d94075a4a \
73 73 --hash=sha256:d4afbb0840f489b60f5a580a41a1b9c3622e08ecb5eec8614d4fb4cd914c4460 \
74 74 --hash=sha256:d9ed28030797c00f4bc43c86bf819266c76a5ea61d006cd4078a93ebf7da6bfd \
75 75 --hash=sha256:e603aa7bb52e4e8ed4119a58a03b60323918467ef209e6ff9db3ac382e5cf2c6 \
76 76 # via pypsrp
77 77 docutils==0.14 \
78 78 --hash=sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6 \
79 79 --hash=sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274 \
80 80 --hash=sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6 \
81 81 # via botocore
82 82 idna==2.8 \
83 83 --hash=sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407 \
84 84 --hash=sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c \
85 85 # via requests
86 86 jmespath==0.9.4 \
87 87 --hash=sha256:3720a4b1bd659dd2eecad0666459b9788813e032b83e7ba58578e48254e0a0e6 \
88 88 --hash=sha256:bde2aef6f44302dfb30320115b17d030798de8c4110e28d5cf6cf91a7a31074c \
89 89 # via boto3, botocore
90 ntlm-auth==1.2.0 \
91 --hash=sha256:7bc02a3fbdfee7275d3dc20fce8028ed8eb6d32364637f28be9e9ae9160c6d5c \
92 --hash=sha256:9b13eaf88f16a831637d75236a93d60c0049536715aafbf8190ba58a590b023e \
90 ntlm-auth==1.3.0 \
91 --hash=sha256:bb2fd03c665f0f62c5f65695b62dcdb07fb7a45df6ebc86c770be2054d6902dd \
92 --hash=sha256:ce5b4483ed761f341a538a426a71a52e5a9cf5fd834ebef1d2090f9eef14b3f8 \
93 93 # via pypsrp
94 94 pycparser==2.19 \
95 95 --hash=sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3 \
96 96 # via cffi
97 97 pypsrp==0.3.1 \
98 98 --hash=sha256:309853380fe086090a03cc6662a778ee69b1cae355ae4a932859034fd76e9d0b \
99 99 --hash=sha256:90f946254f547dc3493cea8493c819ab87e152a755797c93aa2668678ba8ae85
100 100 python-dateutil==2.8.0 \
101 101 --hash=sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb \
102 102 --hash=sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e \
103 103 # via botocore
104 104 requests==2.21.0 \
105 105 --hash=sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e \
106 106 --hash=sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b \
107 107 # via pypsrp
108 108 s3transfer==0.2.0 \
109 109 --hash=sha256:7b9ad3213bff7d357f888e0fab5101b56fa1a0548ee77d121c3a3dbfbef4cb2e \
110 110 --hash=sha256:f23d5cb7d862b104401d9021fc82e5fa0e0cf57b7660a1331425aab0c691d021 \
111 111 # via boto3
112 112 six==1.12.0 \
113 113 --hash=sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c \
114 114 --hash=sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73 \
115 115 # via cryptography, pypsrp, python-dateutil
116 urllib3==1.24.1 \
117 --hash=sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39 \
118 --hash=sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22 \
116 urllib3==1.24.2 \
117 --hash=sha256:4c291ca23bbb55c76518905869ef34bdd5f0e46af7afe6861e8375643ffee1a0 \
118 --hash=sha256:9a247273df709c4fedb38c711e44292304f73f39ab01beda9f6b9fc375669ac3 \
119 119 # via botocore, requests
General Comments 0
You need to be logged in to leave comments. Login now