mirror of
https://github.com/fscotto/infra.git
synced 2026-05-30 15:39:58 +00:00
273 lines
11 KiB
YAML
273 lines
11 KiB
YAML
---
|
|
- name: Enable required Windows features for WSL
|
|
tags: [packages, services, wsl]
|
|
ansible.windows.win_optional_feature:
|
|
name:
|
|
- Microsoft-Windows-Subsystem-Linux
|
|
- VirtualMachinePlatform
|
|
state: present
|
|
include_parent: true
|
|
|
|
- name: Gather Windows host version details
|
|
tags: [packages]
|
|
ansible.windows.win_powershell:
|
|
script: |
|
|
$os = Get-ComputerInfo
|
|
$buildNumber = [int]$os.OsBuildNumber
|
|
$Ansible.Result = @{
|
|
product_name = $os.WindowsProductName
|
|
build_number = $buildNumber
|
|
is_windows_11 = $buildNumber -ge 22000
|
|
}
|
|
$Ansible.Changed = $false
|
|
register: windows_host_version_state
|
|
|
|
- name: Fail when Windows host is not Windows 11
|
|
tags: [packages]
|
|
ansible.builtin.fail:
|
|
msg: >-
|
|
workstation_host_windows is supported only on Windows 11. Detected {{ windows_host_version_state.result.product_name }}
|
|
build {{ windows_host_version_state.result.build_number }}.
|
|
when: not (windows_host_version_state.result.is_windows_11 | default(false))
|
|
|
|
- name: Enable dark mode for Windows apps
|
|
tags: [packages]
|
|
ansible.windows.win_regedit:
|
|
path: HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize
|
|
name: AppsUseLightTheme
|
|
data: 0
|
|
type: dword
|
|
register: windows_apps_dark_mode_state
|
|
when: windows_enable_dark_theme | default(false)
|
|
|
|
- name: Enable dark mode for Windows system surfaces
|
|
tags: [packages]
|
|
ansible.windows.win_regedit:
|
|
path: HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize
|
|
name: SystemUsesLightTheme
|
|
data: 0
|
|
type: dword
|
|
register: windows_system_dark_mode_state
|
|
when: windows_enable_dark_theme | default(false)
|
|
|
|
- name: Hide Windows taskbar search box
|
|
tags: [packages]
|
|
ansible.windows.win_regedit:
|
|
path: HKCU:\Software\Microsoft\Windows\CurrentVersion\Search
|
|
name: SearchboxTaskbarMode
|
|
data: 0
|
|
type: dword
|
|
register: windows_taskbar_search_state
|
|
when: windows_hide_taskbar_search | default(false)
|
|
|
|
- name: Hide Windows 11 taskbar widgets
|
|
tags: [packages]
|
|
ansible.windows.win_regedit:
|
|
path: HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced
|
|
name: TaskbarDa
|
|
data: 0
|
|
type: dword
|
|
register: windows_11_widgets_state
|
|
when: windows_hide_taskbar_widgets | default(false)
|
|
|
|
- name: Disable Windows 11 widgets via policy
|
|
tags: [packages]
|
|
ansible.windows.win_regedit:
|
|
path: HKLM:\SOFTWARE\Policies\Microsoft\Dsh
|
|
name: AllowNewsAndInterests
|
|
data: 0
|
|
type: dword
|
|
register: windows_11_widgets_policy_state
|
|
when: windows_hide_taskbar_widgets | default(false)
|
|
|
|
- name: Note when Windows shell settings may require sign out
|
|
tags: [packages]
|
|
ansible.builtin.debug:
|
|
msg: >-
|
|
Windows shell appearance settings changed. Sign out and back in, or restart explorer.exe,
|
|
if taskbar or theme updates do not appear immediately. Windows 11 widget policy changes can require
|
|
signing out before the widget button disappears from the taskbar.
|
|
changed_when: false
|
|
when: >-
|
|
(windows_apps_dark_mode_state is changed)
|
|
or (windows_system_dark_mode_state is changed)
|
|
or (windows_taskbar_search_state is changed)
|
|
or (windows_11_widgets_state is changed)
|
|
or (windows_11_widgets_policy_state is changed)
|
|
|
|
- name: Ensure winget is available on Windows host
|
|
tags: [packages]
|
|
ansible.windows.win_powershell:
|
|
script: |
|
|
$winget = Get-Command winget.exe -ErrorAction SilentlyContinue
|
|
if ($null -eq $winget) {
|
|
throw 'winget.exe is not available on the Windows host. Install App Installer or rerun the bootstrap script.'
|
|
}
|
|
$Ansible.Changed = $false
|
|
|
|
- name: Ensure WSL 2 is the default backend
|
|
tags: [packages, services, wsl]
|
|
ansible.windows.win_powershell:
|
|
script: |
|
|
$status = & wsl --status 2>$null
|
|
if ($LASTEXITCODE -eq 0 -and $status -match 'Default Version:\s*2') {
|
|
$Ansible.Changed = $false
|
|
return
|
|
}
|
|
|
|
& wsl --set-default-version 2
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw 'Failed to set WSL default version to 2.'
|
|
}
|
|
|
|
$Ansible.Changed = $true
|
|
|
|
- name: Install Windows workstation applications with winget
|
|
tags: [packages]
|
|
ansible.windows.win_powershell:
|
|
script: |
|
|
$packageId = '{{ item.id }}'
|
|
$packageName = '{{ item.name | default(item.id) }}'
|
|
$packageScope = '{{ item.scope | default('' ) }}'
|
|
$packageIdRegex = [regex]::Escape($packageId)
|
|
$installArgs = @(
|
|
'install'
|
|
'--id', $packageId
|
|
'--exact'
|
|
'--silent'
|
|
'--accept-package-agreements'
|
|
'--accept-source-agreements'
|
|
'--disable-interactivity'
|
|
)
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($packageScope)) {
|
|
$installArgs += @('--scope', $packageScope)
|
|
}
|
|
|
|
if ($packageScope -ne 'user') {
|
|
$installed = & winget list --id $packageId --exact --accept-source-agreements --disable-interactivity 2>$null
|
|
if ($LASTEXITCODE -eq 0 -and $installed -match $packageIdRegex) {
|
|
$Ansible.Changed = $false
|
|
return
|
|
}
|
|
}
|
|
|
|
if ($packageScope -eq 'user') {
|
|
$interactiveUser = (Get-CimInstance Win32_ComputerSystem).UserName
|
|
if ([string]::IsNullOrWhiteSpace($interactiveUser)) {
|
|
throw "Failed to install $packageName with winget: no interactive Windows user session is available for a user-scoped install."
|
|
}
|
|
|
|
$taskName = "AnsibleWinget-$($packageId -replace '[^A-Za-z0-9.-]', '-')"
|
|
$taskScriptPath = Join-Path $env:TEMP "$taskName.ps1"
|
|
$stdoutPath = Join-Path $env:TEMP "$taskName.stdout.log"
|
|
$stderrPath = Join-Path $env:TEMP "$taskName.stderr.log"
|
|
$resultPath = Join-Path $env:TEMP "$taskName.result.json"
|
|
$quotedArgs = ($installArgs | ForEach-Object { '"' + ($_ -replace '"', '""') + '"' }) -join ', '
|
|
$taskScript = @(
|
|
"`$ErrorActionPreference = 'Stop'",
|
|
"function Test-PackageInstalled {",
|
|
" `$installed = & winget.exe list --id '$packageId' --exact --accept-source-agreements --disable-interactivity 2>`$null",
|
|
" return (`$LASTEXITCODE -eq 0 -and `$installed -match '$packageIdRegex')",
|
|
"}",
|
|
"`$result = @{ changed = `$false; installed = `$false }",
|
|
"if (-not (Test-PackageInstalled)) {",
|
|
"`$installArgs = @($quotedArgs)",
|
|
"`$process = Start-Process -FilePath 'winget.exe' -ArgumentList `$installArgs -Wait -PassThru -NoNewWindow -RedirectStandardOutput '$stdoutPath' -RedirectStandardError '$stderrPath'",
|
|
" if (`$process.ExitCode -ne 0) { exit `$process.ExitCode }",
|
|
" `$deadline = (Get-Date).AddMinutes(2)",
|
|
" do {",
|
|
" Start-Sleep -Seconds 2",
|
|
" `$result.installed = Test-PackageInstalled",
|
|
" } while ((Get-Date) -lt `$deadline -and -not `$result.installed)",
|
|
" if (-not `$result.installed) { throw 'Package was not detected after the user-scoped install completed.' }",
|
|
" `$result.changed = `$true",
|
|
"}",
|
|
"else {",
|
|
" `$result.installed = `$true",
|
|
"}",
|
|
"`$result | ConvertTo-Json -Compress | Set-Content -Path '$resultPath' -Encoding Ascii"
|
|
) -join [Environment]::NewLine
|
|
|
|
Set-Content -Path $taskScriptPath -Value $taskScript -Encoding Ascii
|
|
|
|
try {
|
|
$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$taskScriptPath`""
|
|
$principal = New-ScheduledTaskPrincipal -UserId $interactiveUser -LogonType InteractiveToken -RunLevel Limited
|
|
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable
|
|
|
|
Register-ScheduledTask -TaskName $taskName -Action $action -Principal $principal -Settings $settings -Force | Out-Null
|
|
Start-ScheduledTask -TaskName $taskName
|
|
|
|
$deadline = (Get-Date).AddMinutes(10)
|
|
do {
|
|
Start-Sleep -Seconds 2
|
|
$task = Get-ScheduledTask -TaskName $taskName
|
|
$taskInfo = Get-ScheduledTaskInfo -TaskName $taskName
|
|
} while ((Get-Date) -lt $deadline -and ($task.State -ne 'Ready' -or $taskInfo.LastRunTime -eq [datetime]::MinValue))
|
|
|
|
if ($task.State -ne 'Ready' -or $taskInfo.LastRunTime -eq [datetime]::MinValue) {
|
|
throw "Failed to install $packageName with winget: timed out waiting for the user-scoped scheduled task to finish."
|
|
}
|
|
|
|
if ($taskInfo.LastTaskResult -ne 0) {
|
|
$stderr = if (Test-Path $stderrPath) { Get-Content -Path $stderrPath -Raw } else { '' }
|
|
$stdout = if (Test-Path $stdoutPath) { Get-Content -Path $stdoutPath -Raw } else { '' }
|
|
throw "Failed to install $packageName with winget. Exit code: $($taskInfo.LastTaskResult). Stdout: $stdout Stderr: $stderr"
|
|
}
|
|
|
|
if (-not (Test-Path $resultPath)) {
|
|
throw "Failed to install $packageName with winget: no result file was produced by the user-scoped scheduled task."
|
|
}
|
|
|
|
$result = Get-Content -Path $resultPath -Raw | ConvertFrom-Json
|
|
if (-not $result.installed) {
|
|
throw "Failed to install $packageName with winget: the package is still not detected after the user-scoped install task finished."
|
|
}
|
|
|
|
$Ansible.Changed = [bool]$result.changed
|
|
return
|
|
}
|
|
finally {
|
|
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
|
|
Remove-Item -Path $taskScriptPath, $stdoutPath, $stderrPath, $resultPath -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
else {
|
|
& winget @installArgs
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Failed to install $packageName with winget"
|
|
}
|
|
}
|
|
|
|
$Ansible.Changed = $true
|
|
loop: "{{ windows_winget_packages | default([]) }}"
|
|
loop_control:
|
|
label: "{{ item.id }}"
|
|
|
|
- name: Install VS Code WSL extensions on Windows host
|
|
tags: [packages, vscode]
|
|
ansible.windows.win_powershell:
|
|
script: |
|
|
$extensionId = '{{ item }}'
|
|
$code = Get-Command code.cmd -ErrorAction SilentlyContinue
|
|
if ($null -eq $code) {
|
|
throw 'code.cmd is not available. Ensure Visual Studio Code is installed before managing extensions.'
|
|
}
|
|
|
|
$installedExtensions = & $code.Source --list-extensions
|
|
if ($installedExtensions -contains $extensionId) {
|
|
$Ansible.Changed = $false
|
|
return
|
|
}
|
|
|
|
& $code.Source --install-extension $extensionId --force
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Failed to install VS Code extension $extensionId"
|
|
}
|
|
|
|
$Ansible.Changed = $true
|
|
loop: "{{ windows_vscode_extensions | default([]) }}"
|
|
loop_control:
|
|
label: "{{ item }}"
|