Skip to content

Commit ef6446b

Browse files
committed
feat: improve external infra support, ansible log parsing, and update dependencies
**Added:** - Documented external infrastructure workflows and requirements for using DreadGOAD with externally managed EC2 instances in `docs/providers/external-infrastructure.md` - Implemented `resolveReferenceInventory` in CLI to support flexible inventory reference resolution for environment generation - Added regex and logic to detect unreachable hosts in Ansible log parsing, ensuring both failed and unreachable hosts are reported **Changed:** - Enhanced Ansible log parsing to include unreachable hosts when extracting failed hosts, and updated corresponding tests for better reliability - Improved gMSA creation in Ansible role by ensuring KDS root key existence and verifying AD service account creation with retry logic - Refactored MSSQL sysadmin configuration to handle single-user mode bootstrapping if neither Windows nor `sa` authentication is available, increasing robustness for locked-down SQL Server environments - Updated SQL Server configuration templates to explicitly set `SECURITYMODE="SQL"` for both MSSQL 2019 and 2022, ensuring mixed-mode auth is enabled - Bumped Go version in CLI to 1.26.2 for compatibility - Updated Go module dependencies, including: - `github.com/cowdogmoo/warpgate/v3` to latest - Docker CLI to v29.4.0 - `go-containerregistry` to v0.21.4 - `mattn/go-isatty` to v0.0.21 - `golang.org/x/sys` to v0.43.0 **Removed:** - Deprecated hardcoded instance IDs from example inventory files, replacing them with `PENDING` placeholders to clarify expected workflow for external instance management and inventory sync - Removed Docker distribution indirect dependency from `go.mod`/`go.sum` as part of dependency cleanup
1 parent a70dd2d commit ef6446b

14 files changed

Lines changed: 502 additions & 54 deletions

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ dreadgoad
1717
ansible/roles/adcs_templates/files/ADCSTemplate.zip
1818
ansible/roles/vulns_adcs_templates/files/ADCSTemplate.zip
1919

20+
# Root environment inventories are local runtime state.
21+
/*-inventory
22+
/*-inventory.bak.*
23+
2024
# Scenario data (keep only tracked environments)
2125
ad/PURPLE
2226
ad/REDLAB

ansible/roles/gmsa/tasks/main.yml

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,14 @@
2424
try {
2525
Import-Module ActiveDirectory
2626
Set-Location AD:
27-
Add-KDSRootKey -EffectiveTime (Get-Date).AddHours(-10) -ErrorAction SilentlyContinue
27+
28+
# Ensure KDS root key exists and is effective
29+
$kdsKey = Get-KdsRootKey -ErrorAction SilentlyContinue
30+
if (-not $kdsKey) {
31+
Add-KDSRootKey -EffectiveTime (Get-Date).AddHours(-10)
32+
Start-Sleep -Seconds 5
33+
Write-Output "Created KDS root key"
34+
}
2835
2936
$existing = Get-ADServiceAccount -Identity $gMSA_Name -ErrorAction SilentlyContinue
3037
if ($existing) {
@@ -37,8 +44,22 @@
3744
3845
$gMSA_HostsGroup = $gMSA_HostNames | ForEach-Object { Get-ADComputer -Identity $_ }
3946
New-ADServiceAccount -Name $gMSA_Name -DNSHostName $gMSA_FQDN -PrincipalsAllowedToRetrieveManagedPassword $gMSA_HostsGroup -ServicePrincipalNames $gMSA_SPNs
47+
48+
# Verify the account was actually created in AD
49+
Start-Sleep -Seconds 3
50+
$verify = Get-ADServiceAccount -Identity $gMSA_Name -ErrorAction SilentlyContinue
51+
if (-not $verify) {
52+
# Retry once after waiting for AD replication
53+
Write-Output "GMSA not found after creation, waiting for AD replication..."
54+
Start-Sleep -Seconds 10
55+
$verify = Get-ADServiceAccount -Identity $gMSA_Name -ErrorAction SilentlyContinue
56+
if (-not $verify) {
57+
throw "GMSA account $gMSA_Name was not found after creation - possible KDS root key or replication issue"
58+
}
59+
}
60+
4061
$Ansible.Changed = $true
41-
Write-Output "Created GMSA account $gMSA_Name successfully"
62+
Write-Output "Created and verified GMSA account $gMSA_Name"
4263
}
4364
catch {
4465
Write-Error "Failed to create GMSA account: $_"

ansible/roles/mssql/files/sql_conf.ini.MSSQL_2019.j2

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ SQLSVCACCOUNT="{{ SQLSVCACCOUNT }}"
4545
{% if SQLSVCPASSWORD != "" %}
4646
SQLSVCPASSWORD="{{ SQLSVCPASSWORD }}"
4747
{% endif %}
48+
SECURITYMODE="SQL"
4849
SAPWD="{{ sa_password }}"
4950
SQLSYSADMINACCOUNTS="{{ SQLSYSADMIN }}"
5051
ADDCURRENTUSERASSQLADMIN="True"

ansible/roles/mssql/files/sql_conf.ini.MSSQL_2022.j2

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ SQLSVCACCOUNT="{{ SQLSVCACCOUNT }}"
4545
{% if SQLSVCPASSWORD != "" %}
4646
SQLSVCPASSWORD="{{ SQLSVCPASSWORD }}"
4747
{% endif %}
48+
SECURITYMODE="SQL"
4849
SAPWD="{{ sa_password }}"
4950
SQLSYSADMINACCOUNTS="{{ SQLSYSADMIN }}"
5051
;ADDCURRENTUSERASSQLADMIN="True"

ansible/roles/mssql/tasks/config.yml

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,54 @@
1818
- name: Ensure BUILTIN\Administrators has SQL sysadmin
1919
ansible.windows.win_shell: |
2020
$ErrorActionPreference = "Continue"
21+
$instanceName = "{{ sql_instance_name }}"
22+
$serviceName = "{{ mssql_service_name }}"
23+
$saPassword = "{{ sa_password }}"
2124
2225
# Test if current session already has SQL sysadmin via Windows auth
2326
$test = SqlCmd {{ connection_type }} -Q "SET NOCOUNT ON; SELECT CASE WHEN IS_SRVROLEMEMBER('sysadmin')=1 THEN 'HAS_SYSADMIN' ELSE 'NO_SYSADMIN' END" 2>&1
2427
$hasSysadmin = ($LASTEXITCODE -eq 0) -and ($test -match 'HAS_SYSADMIN')
2528
2629
if (-not $hasSysadmin) {
27-
# Current session lacks sysadmin - bootstrap via sa auth (enabled by prior provision).
28-
# Grant sysadmin to BUILTIN\Administrators so any admin user (ssm-user, ansible, etc.) works.
29-
$saArgs = @("-b", "-U", "sa", "-P", "{{ sa_password }}", "-S", "localhost\{{ sql_instance_name }}")
30-
$r1 = & SqlCmd @saArgs -Q "IF NOT EXISTS (SELECT 1 FROM sys.server_principals WHERE name = 'BUILTIN\Administrators') CREATE LOGIN [BUILTIN\Administrators] FROM WINDOWS WITH DEFAULT_DATABASE=[master]" 2>&1
31-
if ($LASTEXITCODE -ne 0) { Write-Error "CREATE LOGIN BUILTIN\Administrators failed: $r1"; exit 1 }
32-
$r2 = & SqlCmd @saArgs -Q "IF IS_SRVROLEMEMBER('sysadmin', 'BUILTIN\Administrators') = 0 EXEC sp_addsrvrolemember 'BUILTIN\Administrators', 'sysadmin'" 2>&1
33-
if ($LASTEXITCODE -ne 0) { Write-Error "sp_addsrvrolemember failed: $r2"; exit 1 }
30+
# Try sa auth first (available after prior provision or if SECURITYMODE=SQL was used at install)
31+
$saArgs = @("-b", "-U", "sa", "-P", $saPassword, "-S", "localhost\$instanceName")
32+
$r1 = & SqlCmd @saArgs -Q "SELECT 1" 2>&1
33+
$saWorks = ($LASTEXITCODE -eq 0)
34+
35+
if (-not $saWorks) {
36+
# Neither Windows auth nor sa auth works - bootstrap via single-user mode
37+
Write-Output "Bootstrapping SQL Server via single-user mode..."
38+
Stop-Service $serviceName -Force
39+
Start-Sleep -Seconds 3
40+
41+
# Find sqlservr.exe path
42+
$sqlVer = if ($instanceName -eq "SQLEXPRESS") { (Get-ChildItem "C:\Program Files\Microsoft SQL Server" -Directory | Where-Object { $_.Name -match "^MSSQL\d+" } | Sort-Object Name -Descending | Select-Object -First 1).Name } else { "MSSQL15.$instanceName" }
43+
$sqlServerExe = "C:\Program Files\Microsoft SQL Server\$sqlVer\MSSQL\Binn\sqlservr.exe"
44+
45+
$proc = Start-Process -FilePath $sqlServerExe -ArgumentList "-s$instanceName", "-m" -PassThru -NoNewWindow
46+
Start-Sleep -Seconds 12
47+
48+
# In single-user mode, current user gets sysadmin
49+
& SqlCmd -E -S "localhost\$instanceName" -Q "EXEC xp_instance_regwrite N'HKEY_LOCAL_MACHINE', N'SOFTWARE\Microsoft\Microsoft SQL Server\$sqlVer\MSSQLServer', N'LoginMode', REG_DWORD, 2" 2>&1 | Out-Null
50+
& SqlCmd -E -S "localhost\$instanceName" -Q "ALTER LOGIN sa ENABLE; ALTER LOGIN sa WITH PASSWORD = N'$saPassword', CHECK_POLICY=OFF" 2>&1 | Out-Null
51+
& SqlCmd -E -S "localhost\$instanceName" -Q "IF NOT EXISTS (SELECT 1 FROM sys.server_principals WHERE name = 'BUILTIN\Administrators') CREATE LOGIN [BUILTIN\Administrators] FROM WINDOWS; IF IS_SRVROLEMEMBER('sysadmin', 'BUILTIN\Administrators') = 0 EXEC sp_addsrvrolemember 'BUILTIN\Administrators', 'sysadmin'" 2>&1 | Out-Null
52+
53+
Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
54+
Start-Sleep -Seconds 5
55+
Start-Service $serviceName
56+
Start-Sleep -Seconds 5
57+
Write-Output "SQL Server bootstrapped via single-user mode"
58+
} else {
59+
# sa auth works - use it to grant BUILTIN\Administrators sysadmin
60+
$r2 = & SqlCmd @saArgs -Q "IF NOT EXISTS (SELECT 1 FROM sys.server_principals WHERE name = 'BUILTIN\Administrators') CREATE LOGIN [BUILTIN\Administrators] FROM WINDOWS WITH DEFAULT_DATABASE=[master]" 2>&1
61+
if ($LASTEXITCODE -ne 0) { Write-Error "CREATE LOGIN BUILTIN\Administrators failed: $r2"; exit 1 }
62+
$r3 = & SqlCmd @saArgs -Q "IF IS_SRVROLEMEMBER('sysadmin', 'BUILTIN\Administrators') = 0 EXEC sp_addsrvrolemember 'BUILTIN\Administrators', 'sysadmin'" 2>&1
63+
if ($LASTEXITCODE -ne 0) { Write-Error "sp_addsrvrolemember failed: $r3"; exit 1 }
64+
Write-Output "SQL sysadmin bootstrapped via sa auth"
65+
}
66+
} else {
67+
Write-Output "SQL sysadmin access already available via Windows auth"
3468
}
35-
Write-Output "SQL sysadmin access verified (had_sysadmin=$hasSysadmin)"
3669
3770
- name: Add MSSQL admin
3871
ansible.windows.win_shell: |

cli/cmd/env_cmd.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,10 @@ func copyBaseConfig(projectRoot, envName string) error {
316316
}
317317

318318
func generateInventory(projectRoot, envName, region, reference string) error {
319-
refInvPath := filepath.Join(projectRoot, reference+"-inventory")
319+
refInvPath, err := resolveReferenceInventory(projectRoot, reference)
320+
if err != nil {
321+
return err
322+
}
320323
dstInvPath := filepath.Join(projectRoot, envName+"-inventory")
321324

322325
data, err := os.ReadFile(refInvPath)
@@ -365,6 +368,26 @@ func generateInventory(projectRoot, envName, region, reference string) error {
365368
return os.WriteFile(dstInvPath, []byte(content), 0o644)
366369
}
367370

371+
func resolveReferenceInventory(projectRoot, reference string) (string, error) {
372+
candidates := []string{
373+
filepath.Join(projectRoot, reference+"-inventory"),
374+
filepath.Join(projectRoot, reference+"-inventory.example"),
375+
}
376+
377+
for _, candidate := range candidates {
378+
if _, err := os.Stat(candidate); err == nil {
379+
return candidate, nil
380+
}
381+
}
382+
383+
return "", fmt.Errorf(
384+
"reference inventory %q not found; expected %s or %s",
385+
reference,
386+
filepath.Base(candidates[0]),
387+
filepath.Base(candidates[1]),
388+
)
389+
}
390+
368391
func generateVariantConfig(projectRoot, envName string) error {
369392
source := filepath.Join(projectRoot, "ad", "GOAD")
370393
target := filepath.Join(projectRoot, "ad", "GOAD-"+envName)

cli/go.mod

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
module github.com/dreadnode/dreadgoad
22

3-
go 1.26.1
3+
go 1.26.2
44

55
require (
66
github.com/aws/aws-sdk-go-v2 v1.41.5
77
github.com/aws/aws-sdk-go-v2/config v1.32.14
88
github.com/aws/aws-sdk-go-v2/service/ec2 v1.297.0
99
github.com/aws/aws-sdk-go-v2/service/ssm v1.68.4
1010
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10
11-
github.com/cowdogmoo/warpgate/v3 v3.2.1-0.20260407202430-8248494ac086
11+
github.com/cowdogmoo/warpgate/v3 v3.2.1-0.20260409025702-2b5a62b7131a
1212
github.com/fatih/color v1.19.0
1313
github.com/spf13/cobra v1.10.2
1414
github.com/spf13/viper v1.21.0
@@ -33,16 +33,15 @@ require (
3333
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 // indirect
3434
github.com/aws/smithy-go v1.24.3 // indirect
3535
github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect
36-
github.com/docker/cli v29.3.1+incompatible // indirect
37-
github.com/docker/distribution v2.8.3+incompatible // indirect
36+
github.com/docker/cli v29.4.0+incompatible // indirect
3837
github.com/docker/docker-credential-helpers v0.9.5 // indirect
3938
github.com/fsnotify/fsnotify v1.9.0 // indirect
4039
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
41-
github.com/google/go-containerregistry v0.21.3 // indirect
40+
github.com/google/go-containerregistry v0.21.4 // indirect
4241
github.com/inconshreveable/mousetrap v1.1.0 // indirect
4342
github.com/klauspost/compress v1.18.5 // indirect
4443
github.com/mattn/go-colorable v0.1.14 // indirect
45-
github.com/mattn/go-isatty v0.0.20 // indirect
44+
github.com/mattn/go-isatty v0.0.21 // indirect
4645
github.com/mitchellh/go-homedir v1.1.0 // indirect
4746
github.com/opencontainers/go-digest v1.0.0 // indirect
4847
github.com/opencontainers/image-spec v1.1.1 // indirect
@@ -55,7 +54,7 @@ require (
5554
github.com/subosito/gotenv v1.6.0 // indirect
5655
github.com/vbatts/tar-split v0.12.2 // indirect
5756
golang.org/x/sync v0.20.0 // indirect
58-
golang.org/x/sys v0.42.0 // indirect
57+
golang.org/x/sys v0.43.0 // indirect
5958
golang.org/x/term v0.41.0 // indirect
6059
golang.org/x/text v0.35.0 // indirect
6160
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect

cli/go.sum

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,13 @@ github.com/aws/smithy-go v1.24.3 h1:XgOAaUgx+HhVBoP4v8n6HCQoTRDhoMghKqw4LNHsDNg=
4040
github.com/aws/smithy-go v1.24.3/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
4141
github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw=
4242
github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY=
43-
github.com/cowdogmoo/warpgate/v3 v3.2.1-0.20260407202430-8248494ac086 h1:FnZ1mwfkEK44bk9ijXR4SfpEyejp5205xfFbJNaSegE=
44-
github.com/cowdogmoo/warpgate/v3 v3.2.1-0.20260407202430-8248494ac086/go.mod h1:a6SUrGZAU4RWqVttY0ZZkD8E8GFwUco4JlFv/HM9Vl0=
43+
github.com/cowdogmoo/warpgate/v3 v3.2.1-0.20260409025702-2b5a62b7131a h1:c2Pa8BlXoxejvUyNA8XuNjjWXtMec0dnCvv4TujehQ0=
44+
github.com/cowdogmoo/warpgate/v3 v3.2.1-0.20260409025702-2b5a62b7131a/go.mod h1:1WLaPoaqiTGyvvIJAtLWKPyxtTenmxXIWXSDlJympXM=
4545
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
4646
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
4747
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
48-
github.com/docker/cli v29.3.1+incompatible h1:M04FDj2TRehDacrosh7Vlkgc7AuQoWloQkf1PA5hmoI=
49-
github.com/docker/cli v29.3.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
50-
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
51-
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
48+
github.com/docker/cli v29.4.0+incompatible h1:+IjXULMetlvWJiuSI0Nbor36lcJ5BTcVpUmB21KBoVM=
49+
github.com/docker/cli v29.4.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
5250
github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=
5351
github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
5452
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
@@ -61,8 +59,8 @@ github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPE
6159
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
6260
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
6361
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
64-
github.com/google/go-containerregistry v0.21.3 h1:Xr+yt3VvwOOn/5nJzd7UoOhwPGiPkYW0zWDLLUXqAi4=
65-
github.com/google/go-containerregistry v0.21.3/go.mod h1:D5ZrJF1e6dMzvInpBPuMCX0FxURz7GLq2rV3Us9aPkc=
62+
github.com/google/go-containerregistry v0.21.4 h1:VrhlIQtdhE6riZW//MjPrcJ1snAjPoCCpPHqGOygrv8=
63+
github.com/google/go-containerregistry v0.21.4/go.mod h1:kxgc23zQ2qMY/hAKt0wCbB/7tkeovAP2mE2ienynJUw=
6664
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
6765
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
6866
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
@@ -73,8 +71,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
7371
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
7472
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
7573
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
76-
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
77-
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
74+
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
75+
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
7876
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
7977
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
8078
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@@ -115,9 +113,8 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
115113
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
116114
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
117115
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
118-
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
119-
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
120-
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
116+
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
117+
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
121118
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
122119
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
123120
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=

cli/internal/ansible/logparser.go

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ import (
66
)
77

88
var (
9-
failedRe = regexp.MustCompile(`failed=[1-9][0-9]*`)
10-
unreachableRe = regexp.MustCompile(`unreachable=[1-9][0-9]*`)
11-
failedHostRe = regexp.MustCompile(`(?m)^([a-zA-Z0-9_-]+)\s+:.*failed=[1-9]`)
9+
failedRe = regexp.MustCompile(`failed=[1-9][0-9]*`)
10+
unreachableRe = regexp.MustCompile(`unreachable=[1-9][0-9]*`)
11+
failedHostRe = regexp.MustCompile(`(?m)^([a-zA-Z0-9_-]+)\s+:.*failed=[1-9]`)
12+
unreachableHostRe = regexp.MustCompile(`(?m)^([a-zA-Z0-9_-]+)\s+:.*unreachable=[1-9]`)
1213
)
1314

1415
// CheckAnsibleSuccess analyzes Ansible output to determine if the run succeeded.
@@ -46,16 +47,18 @@ func CheckAnsibleSuccess(output string) bool {
4647
return true
4748
}
4849

49-
// ExtractFailedHosts parses PLAY RECAP to find hosts with failures.
50+
// ExtractFailedHosts parses PLAY RECAP to find hosts with failures or unreachable status.
5051
func ExtractFailedHosts(output string) []string {
51-
matches := failedHostRe.FindAllStringSubmatch(output, -1)
52-
var hosts []string
5352
seen := make(map[string]bool)
54-
for _, m := range matches {
55-
host := m[1]
56-
if !seen[host] {
57-
seen[host] = true
58-
hosts = append(hosts, host)
53+
var hosts []string
54+
55+
for _, re := range []*regexp.Regexp{failedHostRe, unreachableHostRe} {
56+
for _, m := range re.FindAllStringSubmatch(output, -1) {
57+
host := m[1]
58+
if !seen[host] {
59+
seen[host] = true
60+
hosts = append(hosts, host)
61+
}
5962
}
6063
}
6164
return hosts

cli/internal/ansible/logparser_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,20 @@ DC01 : ok=10 changed=2 unreachable=0 failed=0 s
124124
DC01 : ok=5 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0`,
125125
want: []string{"DC01"},
126126
},
127+
{
128+
name: "unreachable host included",
129+
output: `PLAY RECAP *********************************************************************
130+
dc01 : ok=4 changed=2 unreachable=0 failed=1 skipped=3 rescued=0 ignored=0
131+
dc02 : ok=3 changed=1 unreachable=1 failed=0 skipped=3 rescued=0 ignored=0
132+
dc03 : ok=10 changed=6 unreachable=0 failed=0 skipped=4 rescued=0 ignored=0`,
133+
want: []string{"dc01", "dc02"},
134+
},
135+
{
136+
name: "only unreachable no failed",
137+
output: `PLAY RECAP *********************************************************************
138+
dc02 : ok=0 changed=0 unreachable=1 failed=0 skipped=0 rescued=0 ignored=0`,
139+
want: []string{"dc02"},
140+
},
127141
}
128142

129143
for _, tt := range tests {

0 commit comments

Comments
 (0)