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