mirror of
https://github.com/fscotto/infra.git
synced 2026-05-30 15:39:58 +00:00
412 lines
15 KiB
YAML
412 lines
15 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 executable on Windows host through PSRP
|
|
tags: [packages]
|
|
when: windows_package_backend == 'winget_psrp'
|
|
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.'
|
|
}
|
|
|
|
& $winget.Source --info *> $null
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw 'winget.exe is not executable through the current PSRP session. Set windows_package_backend to winget_wsl_local on WSL-managed hosts.'
|
|
}
|
|
|
|
$Ansible.Changed = $false
|
|
|
|
- name: Ensure winget is executable through local Windows PowerShell from WSL
|
|
tags: [packages]
|
|
when: windows_package_backend == 'winget_wsl_local'
|
|
block:
|
|
- name: Ensure winget_wsl_local backend is running inside WSL
|
|
ansible.builtin.assert:
|
|
that:
|
|
- lookup('ansible.builtin.env', 'WSL_DISTRO_NAME') | length > 0
|
|
fail_msg: >-
|
|
winget_wsl_local requires running the playbook from inside the
|
|
target machine's WSL environment.
|
|
|
|
- name: Get local Windows host name through WSL interop
|
|
ansible.builtin.shell: |
|
|
powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command '$env:COMPUTERNAME'
|
|
args:
|
|
executable: /bin/bash
|
|
delegate_to: localhost
|
|
register: local_windows_host_name_state
|
|
changed_when: false
|
|
|
|
- name: Get local Windows host IP addresses through WSL interop
|
|
ansible.builtin.shell: |
|
|
powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command '
|
|
Get-NetIPAddress -AddressFamily IPv4, IPv6 -ErrorAction SilentlyContinue |
|
|
Where-Object {
|
|
$_.IPAddress -notin @("127.0.0.1", "::1") -and
|
|
$_.IPAddress -notlike "169.254.*" -and
|
|
$_.IPAddress -notlike "fe80:*"
|
|
} |
|
|
Select-Object -ExpandProperty IPAddress
|
|
'
|
|
args:
|
|
executable: /bin/bash
|
|
delegate_to: localhost
|
|
register: local_windows_host_ip_state
|
|
changed_when: false
|
|
|
|
- name: Ensure winget_wsl_local backend targets the local Windows host
|
|
ansible.builtin.assert:
|
|
that:
|
|
- >-
|
|
(ansible_host | lower) in ['localhost', '127.0.0.1', '::1']
|
|
or (local_windows_host_name_state.stdout | trim | upper)
|
|
== ((ansible_host | regex_replace('\\..*$', '')) | upper)
|
|
or (ansible_host | lower)
|
|
in (local_windows_host_ip_state.stdout_lines | map('trim') | map('lower') | list)
|
|
fail_msg: >-
|
|
winget_wsl_local can only target the local Windows host reached
|
|
through WSL interop. Local Windows host '{{ local_windows_host_name_state.stdout | trim }}'
|
|
with addresses {{ local_windows_host_ip_state.stdout_lines | map('trim') | list }}
|
|
does not match ansible_host '{{ ansible_host }}'.
|
|
|
|
- name: Ensure winget is executable through local Windows PowerShell
|
|
ansible.builtin.shell: |
|
|
powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command '
|
|
$ErrorActionPreference = "Stop"
|
|
$winget = Get-Command winget.exe -ErrorAction SilentlyContinue
|
|
if ($null -eq $winget) {
|
|
throw "winget.exe is not available on the local Windows host. Install App Installer or rerun the bootstrap script."
|
|
}
|
|
|
|
& $winget.Source --info *> $null
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "winget.exe is not executable through local Windows PowerShell interop."
|
|
}
|
|
'
|
|
args:
|
|
executable: /bin/bash
|
|
delegate_to: localhost
|
|
changed_when: 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 through PSRP
|
|
tags: [packages]
|
|
when: windows_package_backend == 'winget_psrp'
|
|
ansible.windows.win_powershell:
|
|
script: |
|
|
$packageId = '{{ item.id }}'
|
|
$packageName = '{{ item.name | default(item.id) }}'
|
|
$packageSource = '{{ item.source | default('') }}'
|
|
$listArgs = @(
|
|
'list'
|
|
'--id', $packageId
|
|
'--exact'
|
|
'--accept-source-agreements'
|
|
'--disable-interactivity'
|
|
)
|
|
$installArgs = @(
|
|
'install'
|
|
'--id', $packageId
|
|
'--exact'
|
|
'--silent'
|
|
'--accept-package-agreements'
|
|
'--accept-source-agreements'
|
|
'--disable-interactivity'
|
|
)
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($packageSource)) {
|
|
$listArgs += @('--source', $packageSource)
|
|
$installArgs += @('--source', $packageSource)
|
|
}
|
|
|
|
$installed = & winget @listArgs 2>$null
|
|
if ($LASTEXITCODE -eq 0 -and $installed -match [regex]::Escape($packageId)) {
|
|
$Ansible.Changed = $false
|
|
return
|
|
}
|
|
|
|
& 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 Windows workstation applications with winget through WSL local backend
|
|
tags: [packages]
|
|
when: windows_package_backend == 'winget_wsl_local'
|
|
ansible.builtin.shell: |
|
|
powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command '
|
|
$ErrorActionPreference = "Stop"
|
|
$packageId = "{{ item.id }}"
|
|
$packageName = "{{ item.name | default(item.id) }}"
|
|
$packageSource = "{{ item.source | default('') }}"
|
|
$winget = Get-Command winget.exe -ErrorAction Stop
|
|
$listArgs = @(
|
|
"list"
|
|
"--id", $packageId
|
|
"--exact"
|
|
"--accept-source-agreements"
|
|
"--disable-interactivity"
|
|
)
|
|
$installArgs = @(
|
|
"install"
|
|
"--id", $packageId
|
|
"--exact"
|
|
"--silent"
|
|
"--accept-package-agreements"
|
|
"--accept-source-agreements"
|
|
"--disable-interactivity"
|
|
)
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($packageSource)) {
|
|
$listArgs += @("--source", $packageSource)
|
|
$installArgs += @("--source", $packageSource)
|
|
}
|
|
|
|
$installed = & $winget.Source @listArgs 2>$null
|
|
if ($LASTEXITCODE -eq 0 -and $installed -match [regex]::Escape($packageId)) {
|
|
[Console]::Out.WriteLine("changed=false")
|
|
exit 0
|
|
}
|
|
|
|
& $winget.Source @installArgs
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Failed to install $packageName with winget"
|
|
}
|
|
|
|
[Console]::Out.WriteLine("changed=true")
|
|
'
|
|
args:
|
|
executable: /bin/bash
|
|
delegate_to: localhost
|
|
register: windows_wsl_local_winget_install_state
|
|
changed_when: "'changed=true' in windows_wsl_local_winget_install_state.stdout"
|
|
loop: "{{ windows_winget_packages | default([]) }}"
|
|
loop_control:
|
|
label: "{{ item.id }}"
|
|
|
|
- name: Configure Windows taskbar pin layout
|
|
tags: [packages]
|
|
when: (windows_taskbar_pins | default([])) | length > 0
|
|
block:
|
|
- name: Ensure Windows taskbar layout directory exists
|
|
ansible.windows.win_file:
|
|
path: "{{ windows_taskbar_layout_directory }}"
|
|
state: directory
|
|
|
|
- name: Render Windows taskbar layout policy file
|
|
ansible.windows.win_template:
|
|
src: taskbar-layout.xml.j2
|
|
dest: "{{ windows_taskbar_layout_path }}"
|
|
register: windows_taskbar_layout_file_state
|
|
|
|
- name: Enable Windows taskbar layout policy
|
|
ansible.windows.win_regedit:
|
|
path: HKCU:\Software\Policies\Microsoft\Windows\Explorer
|
|
name: LockedStartLayout
|
|
data: 1
|
|
type: dword
|
|
register: windows_taskbar_layout_policy_state
|
|
|
|
- name: Set Windows taskbar layout policy file path
|
|
ansible.windows.win_regedit:
|
|
path: HKCU:\Software\Policies\Microsoft\Windows\Explorer
|
|
name: StartLayoutFile
|
|
data: "{{ windows_taskbar_layout_path }}"
|
|
type: expandstring
|
|
register: windows_taskbar_layout_policy_path_state
|
|
|
|
- name: Set Windows taskbar layout policy refresh behavior
|
|
ansible.windows.win_regedit:
|
|
path: HKCU:\Software\Policies\Microsoft\Windows\Explorer
|
|
name: ReapplyStartLayoutEveryLogon
|
|
data: "{{ (windows_taskbar_reapply_every_logon | default(false)) | ternary(1, 0) }}"
|
|
type: dword
|
|
register: windows_taskbar_layout_policy_reapply_state
|
|
|
|
- name: Note when Windows taskbar pin layout may require sign out
|
|
ansible.builtin.debug:
|
|
msg: >-
|
|
Windows taskbar pin policy changed. Sign out and back in if the updated pin order does not appear immediately.
|
|
changed_when: false
|
|
when: >-
|
|
(windows_taskbar_layout_file_state is changed)
|
|
or (windows_taskbar_layout_policy_state is changed)
|
|
or (windows_taskbar_layout_policy_path_state is changed)
|
|
or (windows_taskbar_layout_policy_reapply_state is changed)
|
|
|
|
- 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 }}"
|
|
|
|
- name: Set Windows Terminal default profile to Ubuntu
|
|
tags: [packages, wsl]
|
|
ansible.windows.win_powershell:
|
|
script: |
|
|
$terminalSettingsPath = Join-Path $env:LOCALAPPDATA 'Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json'
|
|
if (-not (Test-Path $terminalSettingsPath)) {
|
|
throw 'Windows Terminal settings.json was not found. Launch Windows Terminal once before applying the default profile configuration.'
|
|
}
|
|
|
|
$settings = Get-Content -Path $terminalSettingsPath -Raw | ConvertFrom-Json
|
|
if ($null -eq $settings.profiles -or $null -eq $settings.profiles.list) {
|
|
throw 'Windows Terminal settings.json does not contain a profiles.list section.'
|
|
}
|
|
|
|
$targetProfile = $settings.profiles.list |
|
|
Where-Object {
|
|
$_.name -eq '{{ windows_terminal_default_profile_name }}' -and -not ($_.PSObject.Properties.Name -contains 'hidden' -and $_.hidden)
|
|
} |
|
|
Select-Object -First 1
|
|
|
|
if ($null -eq $targetProfile) {
|
|
throw "Windows Terminal visible profile '{{ windows_terminal_default_profile_name }}' was not found. Ensure the Ubuntu WSL profile exists and is not hidden in Windows Terminal first."
|
|
}
|
|
|
|
if ($settings.defaultProfile -eq $targetProfile.guid) {
|
|
$Ansible.Changed = $false
|
|
return
|
|
}
|
|
|
|
$settings.defaultProfile = $targetProfile.guid
|
|
$settings | ConvertTo-Json -Depth 100 | Set-Content -Path $terminalSettingsPath -Encoding utf8
|
|
$Ansible.Changed = $true
|