win test: add http tester container to Windows tests (#46606)

(cherry picked from commit 6e2897647c)
This commit is contained in:
Jordan Borean 2018-10-13 08:20:00 +10:00 committed by Toshio Kuratomi
parent 377540348c
commit ff5073c10b
8 changed files with 353 additions and 7 deletions

View file

@ -3,6 +3,11 @@
- set_fact:
has_httptester: "{{ lookup('env', 'HTTPTESTER') != '' }}"
- name: make sure we have the ansible_os_family and ansible_distribution_version facts
setup:
gather_subset: distribution
when: ansible_facts == {}
# If we are running with access to a httptester container, grab it's cacert and install it
- block:
# Override hostname defaults with httptester linked names
@ -22,6 +27,16 @@
get_url:
url: "http://ansible.http.tests/{{ item }}"
dest: "{{ output_dir }}/{{ item }}"
when: ansible_os_family != 'Windows'
with_items:
- client.pem
- client.key
- name: Windows - Get client cert/key
win_get_url:
url: http://ansible.http.tests/{{ item }}
dest: '{{ win_output_dir }}\{{ item }}'
when: ansible_os_family == 'Windows'
with_items:
- client.pem
- client.key
@ -38,6 +53,12 @@
dest: "/usr/local/share/ca-certificates/ansible.crt"
when: ansible_os_family == 'Debian'
- name: Windows - Retrieve test cacert
win_get_url:
url: http://ansible.http.tests/cacert.pem
dest: '{{ win_output_dir }}\cacert.pem'
when: ansible_os_family == 'Windows'
- name: Redhat - Update ca trust
command: update-ca-trust extract
when: ansible_os_family == 'RedHat'
@ -46,6 +67,14 @@
command: update-ca-certificates
when: ansible_os_family == 'Debian' or ansible_os_family == 'Suse'
- name: Windows - Update ca trust
win_certificate_store:
path: '{{ win_output_dir }}\cacert.pem'
state: present
store_location: LocalMachine
store_name: Root
when: ansible_os_family == 'Windows'
- name: FreeBSD - Retrieve test cacert
get_url:
url: "http://ansible.http.tests/cacert.pem"
@ -68,4 +97,7 @@
command: /usr/local/opt/openssl/bin/c_rehash
when: ansible_os_family == 'Darwin'
when: has_httptester|bool
when:
- has_httptester|bool
# skip the setup if running on Windows Server 2008 as httptester is not available
- ansible_os_family != 'Windows' or (ansible_os_family == 'Windows' and not ansible_distribution_version.startswith("6.0."))

View file

@ -1,3 +1,3 @@
shippable/windows/group3
unstable
needs/httptester
skip/windows/2008 # httptester requires SSH which doesn't work with 2008

View file

@ -1,3 +1,2 @@
---
test_uri_path: C:\ansible\win_uri
httpbin_host: httpbin.org
test_uri_path: '{{ win_output_dir }}\win_uri'

View file

@ -0,0 +1,3 @@
dependencies:
- prepare_win_tests
- prepare_http_tests

View file

@ -333,6 +333,7 @@ def parse_args():
config=WindowsIntegrationConfig)
add_extra_docker_options(windows_integration, integration=False)
add_httptester_options(windows_integration, argparse)
windows_integration.add_argument('--windows',
metavar='VERSION',

View file

@ -60,6 +60,8 @@ from lib.util import (
from lib.docker_util import (
docker_pull,
docker_run,
docker_available,
docker_rm,
get_docker_container_id,
get_docker_container_ip,
)
@ -507,6 +509,8 @@ def command_windows_integration(args):
all_targets = tuple(walk_windows_integration_targets(include_hidden=True))
internal_targets = command_integration_filter(args, all_targets, init_callback=windows_init)
instances = [] # type: list [lib.thread.WrappedThread]
use_httptester = False
httptester_id = None
if args.windows:
get_coverage_path(args) # initialize before starting threads
@ -533,12 +537,58 @@ def command_windows_integration(args):
with open(filename, 'w') as inventory_fd:
inventory_fd.write(inventory)
use_httptester = args.httptester and any('needs/httptester/' in t.aliases for t in internal_targets)
# if running under Docker delegation, the httptester may have already been started
docker_httptester = bool(os.environ.get("HTTPTESTER", False))
if use_httptester and not docker_available() and not docker_httptester:
display.warning('Assuming --disable-httptester since `docker` is not available.')
use_httptester = False
if use_httptester:
if docker_httptester:
# we are running in a Docker container that is linked to the httptester container, we just need to
# forward these requests to the linked hostname
first_host = HTTPTESTER_HOSTS[0]
ssh_options = ["-R", "8080:%s:80" % first_host, "-R", "8443:%s:443" % first_host]
else:
# we are running directly and need to start the httptester container ourselves and forward the port
# from there manually set so HTTPTESTER env var is set during the run
args.inject_httptester = True
httptester_id, ssh_options = start_httptester(args)
# to get this SSH command to run in the background we need to set to run in background (-f) and disable
# the pty allocation (-T)
ssh_options.insert(0, "-fT")
# create a script that will continue to run in the background until the script is deleted, this will
# cleanup and close the connection
watcher_path = "ansible-test-http-watcher-%s.ps1" % time.time()
for remote in [r for r in remotes if r.version != '2008']:
manage = ManageWindowsCI(remote)
manage.upload("test/runner/setup/windows-httptester.ps1", watcher_path)
# need to use -Command as we cannot pass an array of values with -File
script = "powershell.exe -NoProfile -Command .\\%s -Hosts %s" % (watcher_path, ", ".join(HTTPTESTER_HOSTS))
if args.verbosity > 3:
script += " -Verbose"
manage.ssh(script, options=ssh_options, force_pty=False)
success = False
try:
command_integration_filtered(args, internal_targets, all_targets)
success = True
finally:
if use_httptester:
if httptester_id:
docker_rm(args, httptester_id)
for remote in [r for r in remotes if r.version != '2008']:
# delete the tmp file that keeps the http-tester alive
manage = ManageWindowsCI(remote)
manage.ssh("del %s /F /Q" % watcher_path)
if args.remote_terminate == 'always' or (args.remote_terminate == 'success' and success):
for instance in instances:
instance.result.stop()
@ -736,7 +786,8 @@ def command_integration_filtered(args, targets, all_targets):
display.warning('SSH service not responding. Waiting %d second(s) before checking again.' % seconds)
time.sleep(seconds)
if args.inject_httptester:
# Windows is different as Ansible execution is done locally but the host is remote
if args.inject_httptester and not isinstance(args, WindowsIntegrationConfig):
inject_httptester(args)
start_at_task = args.start_at_task

View file

@ -75,24 +75,57 @@ class ManageWindowsCI(object):
raise ApplicationError('Timeout waiting for %s/%s instance %s.' %
(self.core_ci.platform, self.core_ci.version, self.core_ci.instance_id))
def ssh(self, command, options=None):
def download(self, remote, local):
"""
:type remote: str
:type local: str
"""
self.scp('%s@%s:%s' % (self.core_ci.connection.username, self.core_ci.connection.hostname, remote), local)
def upload(self, local, remote):
"""
:type local: str
:type remote: str
"""
self.scp(local, '%s@%s:%s' % (self.core_ci.connection.username, self.core_ci.connection.hostname, remote))
def ssh(self, command, options=None, force_pty=True):
"""
:type command: str | list[str]
:type options: list[str] | None
:type force_pty: bool
"""
if not options:
options = []
if force_pty:
options.append('-tt')
if isinstance(command, list):
command = ' '.join(pipes.quote(c) for c in command)
run_command(self.core_ci.args,
['ssh', '-tt', '-q'] + self.ssh_args +
['ssh', '-q'] + self.ssh_args +
options +
['-p', '22',
'%s@%s' % (self.core_ci.connection.username, self.core_ci.connection.hostname)] +
[command])
def scp(self, src, dst):
"""
:type src: str
:type dst: str
"""
for dummy in range(1, 10):
try:
run_command(self.core_ci.args,
['scp'] + self.ssh_args +
['-P', '22', '-q', '-r', src, dst])
return
except SubprocessError:
time.sleep(10)
raise ApplicationError('Failed transfer: %s -> %s' % (src, dst))
class ManageNetworkCI(object):
"""Manage access to a network instance provided by Ansible Core CI."""

View file

@ -0,0 +1,227 @@
<#
.SYNOPSIS
Designed to set a Windows host to connect to the httptester container running
on the Ansible host. This will setup the Windows host file and forward the
local ports to use this connection. This will continue to run in the background
until the script is deleted.
Run this with SSH with the -R arguments to foward ports 8080 and 8443 to the
httptester container.
.PARAMETER Hosts
A list of hostnames to add to the Windows hosts file for the httptester
container.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory=$true, Position=0)][String[]]$Hosts
)
$ProgressPreference = "SilentlyContinue"
$ErrorActionPreference = "Stop"
$os_version = [Version](Get-Item -Path "$env:SystemRoot\System32\kernel32.dll").VersionInfo.ProductVersion
Write-Verbose -Message "Configuring HTTP Tester on Windows $os_version for '$($Hosts -join "', '")'"
Function Get-PmapperRuleBytes {
<#
.SYNOPSIS
Create the byte values that configures a rule in the PMapper configuration
file. This isn't really documented but because PMapper is only used for
Server 2008 R2 we will stick to 1 version and just live with the legacy
work for now.
.PARAMETER ListenPort
The port to listen on localhost, this will be forwarded to the host defined
by ConnectAddress and ConnectPort.
.PARAMETER ConnectAddress
The hostname or IP to map the traffic to.
.PARAMETER ConnectPort
This port of ConnectAddress to map the traffic to.
#>
param(
[Parameter(Mandatory=$true)][UInt16]$ListenPort,
[Parameter(Mandatory=$true)][String]$ConnectAddress,
[Parameter(Mandatory=$true)][Int]$ConnectPort
)
$connect_field = "$($ConnectAddress):$ConnectPort"
$connect_bytes = [System.Text.Encoding]::ASCII.GetBytes($connect_field)
$data_length = [byte]($connect_bytes.Length + 6) # size of payload minus header, length, and footer
$port_bytes = [System.BitConverter]::GetBytes($ListenPort)
$payload = [System.Collections.Generic.List`1[Byte]]@()
$payload.Add([byte]16) > $null # header is \x10, means Configure Mapping rule
$payload.Add($data_length) > $null
$payload.AddRange($connect_bytes)
$payload.AddRange($port_bytes)
$payload.AddRange([byte[]]@(0, 0)) # 2 extra bytes of padding
$payload.Add([byte]0) > $null # 0 is TCP, 1 is UDP
$payload.Add([byte]0) > $null # 0 is Any, 1 is Internet
$payload.Add([byte]31) > $null # footer is \x1f, means end of Configure Mapping rule
return ,$payload.ToArray()
}
Write-Verbose -Message "Adding host file entries"
$hosts_file = "$env:SystemRoot\System32\drivers\etc\hosts"
$hosts_file_lines = [System.IO.File]::ReadAllLines($hosts_file)
$changed = $false
foreach ($httptester_host in $Hosts) {
$host_line = "127.0.0.1 $httptester_host # ansible-test httptester"
if ($host_line -notin $hosts_file_lines) {
$hosts_file_lines += $host_line
$changed = $true
}
}
if ($changed) {
Write-Verbose -Message "Host file is missing entries, adding missing entries"
[System.IO.File]::WriteAllLines($hosts_file, $hosts_file_lines)
}
# forward ports
$forwarded_ports = @{
80 = 8080
443 = 8443
}
if ($os_version -ge [Version]"6.2") {
Write-Verbose -Message "Using netsh to configure forwarded ports"
foreach ($forwarded_port in $forwarded_ports.GetEnumerator()) {
$port_set = netsh interface portproxy show v4tov4 | `
Where-Object { $_ -match "127.0.0.1\s*$($forwarded_port.Key)\s*127.0.0.1\s*$($forwarded_port.Value)" }
if (-not $port_set) {
Write-Verbose -Message "Adding netsh portproxy rule for $($forwarded_port.Key) -> $($forwarded_port.Value)"
$add_args = @(
"interface",
"portproxy",
"add",
"v4tov4",
"listenaddress=127.0.0.1",
"listenport=$($forwarded_port.Key)",
"connectaddress=127.0.0.1",
"connectport=$($forwarded_port.Value)"
)
$null = netsh $add_args 2>&1
}
}
} else {
Write-Verbose -Message "Using Port Mapper to configure forwarded ports"
# netsh interface portproxy doesn't work on local addresses in older
# versions of Windows. Use custom application Port Mapper to acheive the
# same outcome
# http://www.analogx.com/contents/download/Network/pmapper/Freeware.htm
$s3_url = "https://s3.amazonaws.com/ansible-ci-files/ansible-test/pmapper-1.04.exe"
# download the Port Mapper executable to a temporary directory
$pmapper_folder = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath ([System.IO.Path]::GetRandomFileName())
$pmapper_exe = Join-Path -Path $pmapper_folder -ChildPath pmapper.exe
$pmapper_config = Join-Path -Path $pmapper_folder -ChildPath pmapper.dat
New-Item -Path $pmapper_folder -ItemType Directory > $null
$stop = $false
do {
try {
Write-Verbose -Message "Attempting download of '$s3_url'"
(New-Object -TypeName System.Net.WebClient).DownloadFile($s3_url, $pmapper_exe)
$stop = $true
} catch { Start-Sleep -Second 5 }
} until ($stop)
# create the Port Mapper rule file that contains our forwarded ports
$fs = [System.IO.File]::Create($pmapper_config)
try {
foreach ($forwarded_port in $forwarded_ports.GetEnumerator()) {
Write-Verbose -Message "Creating forwarded port rule for $($forwarded_port.Key) -> $($forwarded_port.Value)"
$pmapper_rule = Get-PmapperRuleBytes -ListenPort $forwarded_port.Key -ConnectAddress 127.0.0.1 -ConnectPort $forwarded_port.Value
$fs.Write($pmapper_rule, 0, $pmapper_rule.Length)
}
} finally {
$fs.Close()
}
Write-Verbose -Message "Starting Port Mapper '$pmapper_exe' in the background"
$start_args = @{
CommandLine = $pmapper_exe
CurrentDirectory = $pmapper_folder
}
$res = Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments $start_args
if ($res.ReturnValue -ne 0) {
$error_msg = switch($res.ReturnValue) {
2 { "Access denied" }
3 { "Insufficient privilege" }
8 { "Unknown failure" }
9 { "Path not found" }
21 { "Invalid parameter" }
default { "Undefined Error: $($res.ReturnValue)" }
}
Write-Error -Message "Failed to start pmapper: $error_msg"
}
$pmapper_pid = $res.ProcessId
Write-Verbose -Message "Port Mapper PID: $pmapper_pid"
}
Write-Verbose -Message "Wait for current script at '$PSCommandPath' to be deleted before running cleanup"
$fsw = New-Object -TypeName System.IO.FileSystemWatcher
$fsw.Path = Split-Path -Path $PSCommandPath -Parent
$fsw.Filter = Split-Path -Path $PSCommandPath -Leaf
$fsw.WaitForChanged([System.IO.WatcherChangeTypes]::Deleted, 3600000) > $null
Write-Verbose -Message "Script delete or timeout reached, cleaning up Windows httptester artifacts"
Write-Verbose -Message "Cleanup host file entries"
$hosts_file_lines = [System.IO.File]::ReadAllLines($hosts_file)
$new_lines = [System.Collections.ArrayList]@()
$changed = $false
foreach ($host_line in $hosts_file_lines) {
if ($host_line.EndsWith("# ansible-test httptester")) {
$changed = $true
continue
}
$new_lines.Add($host_line) > $null
}
if ($changed) {
Write-Verbose -Message "Host file has extra entries, removing extra entries"
[System.IO.File]::WriteAllLines($hosts_file, $new_lines)
}
if ($os_version -ge [Version]"6.2") {
Write-Verbose -Message "Cleanup of forwarded port configured in netsh"
foreach ($forwarded_port in $forwarded_ports.GetEnumerator()) {
$port_set = netsh interface portproxy show v4tov4 | `
Where-Object { $_ -match "127.0.0.1\s*$($forwarded_port.Key)\s*127.0.0.1\s*$($forwarded_port.Value)" }
if ($port_set) {
Write-Verbose -Message "Removing netsh portproxy rule for $($forwarded_port.Key) -> $($forwarded_port.Value)"
$delete_args = @(
"interface",
"portproxy",
"delete",
"v4tov4",
"listenaddress=127.0.0.1",
"listenport=$($forwarded_port.Key)"
)
$null = netsh $delete_args 2>&1
}
}
} else {
Write-Verbose -Message "Stopping Port Mapper executable based on pid $pmapper_pid"
Stop-Process -Id $pmapper_pid -Force
# the process may not stop straight away, try multiple times to delete the Port Mapper folder
$attempts = 1
do {
try {
Write-Verbose -Message "Cleanup temporary files for Port Mapper at '$pmapper_folder' - Attempt: $attempts"
Remove-Item -Path $pmapper_folder -Force -Recurse
break
} catch {
Write-Verbose -Message "Cleanup temporary files for Port Mapper failed, waiting 5 seconds before trying again:$($_ | Out-String)"
if ($attempts -ge 5) {
break
}
$attempts += 1
Start-Sleep -Second 5
}
} until ($true)
}