feat(playbooks): add cloud desktop bootstrap flow
This commit is contained in:
parent
19e1f4ef1d
commit
e7d9140b86
33
README.md
33
README.md
@ -1,5 +1,14 @@
|
|||||||
# playbooks
|
# playbooks
|
||||||
|
|
||||||
|
## Cloud Dev Desktop
|
||||||
|
|
||||||
|
The cloud dev desktop flow lives here as two playbooks:
|
||||||
|
|
||||||
|
1. `bootstrap_cloud_dev_desktop.yml`
|
||||||
|
2. `destroy_cloud_dev_desktop.yml`
|
||||||
|
|
||||||
|
`bootstrap_cloud_dev_desktop.yml` now includes the create/bootstrap/verify sequence in one entry point. The control-plane repo calls these playbooks from `../playbooks`.
|
||||||
|
|
||||||
## Traffic Billing Stack
|
## Traffic Billing Stack
|
||||||
|
|
||||||
The traffic billing stack now has a single aggregate playbook:
|
The traffic billing stack now has a single aggregate playbook:
|
||||||
@ -8,12 +17,14 @@ The traffic billing stack now has a single aggregate playbook:
|
|||||||
|
|
||||||
It orchestrates these existing playbooks in dependency order:
|
It orchestrates these existing playbooks in dependency order:
|
||||||
|
|
||||||
1. `deploy_xray_exporter.yml`
|
1. `deploy_billing_service.yml`
|
||||||
2. `deploy_billing_service.yml`
|
2. `deploy_xworkmate_bridge_vhosts.yml`
|
||||||
3. `deploy_accounts_svc_plus.yml`
|
3. `deploy_xray_exporter.yml`
|
||||||
4. `deploy_console_svc_plus.yml`
|
4. `deploy_agent_svc_plus.yml`
|
||||||
5. `deploy_agent_svc_plus.yml`
|
5. `deploy_accounts_svc_plus.yml`
|
||||||
6. `deploy_xworkmate_bridge_vhosts.yml`
|
6. `deploy_stunnel-client.yml`
|
||||||
|
7. `deploy_apisix.yml`
|
||||||
|
8. `deploy_console_svc_plus.yml`
|
||||||
|
|
||||||
### Full stack deploy
|
### Full stack deploy
|
||||||
|
|
||||||
@ -47,12 +58,14 @@ ansible-playbook -i inventory.ini -l jp_xhttp_contabo_host deploy_svc_plus_core_
|
|||||||
|
|
||||||
Use `STACK_SERVICES` with a comma-separated list:
|
Use `STACK_SERVICES` with a comma-separated list:
|
||||||
|
|
||||||
- `xray-exporter`
|
|
||||||
- `billing-service`
|
- `billing-service`
|
||||||
- `accounts`
|
|
||||||
- `console`
|
|
||||||
- `agent`
|
|
||||||
- `xworkmate-bridge`
|
- `xworkmate-bridge`
|
||||||
|
- `xray-exporter`
|
||||||
|
- `agent`
|
||||||
|
- `accounts`
|
||||||
|
- `stunnel-client`
|
||||||
|
- `apisix`
|
||||||
|
- `console`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks
|
cd /Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks
|
||||||
|
|||||||
289
bootstrap_cloud_dev_desktop.yml
Normal file
289
bootstrap_cloud_dev_desktop.yml
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
- name: Normalize cloud dev desktop request
|
||||||
|
hosts: localhost
|
||||||
|
connection: local
|
||||||
|
gather_facts: true
|
||||||
|
roles:
|
||||||
|
- role: cloud_vm_request_validate
|
||||||
|
|
||||||
|
- name: Create cloud dev desktop infrastructure
|
||||||
|
hosts: localhost
|
||||||
|
connection: local
|
||||||
|
gather_facts: true
|
||||||
|
roles:
|
||||||
|
- role: "{{ (provider == 'azure') | ternary('azure_dev_desktop_lifecycle', 'gcp_dev_desktop_lifecycle') }}"
|
||||||
|
vars:
|
||||||
|
cloud_lifecycle_action: create
|
||||||
|
- role: cloud_vm_inventory_emit
|
||||||
|
|
||||||
|
- name: Bootstrap remote cloud dev desktop
|
||||||
|
hosts: cloud_desktop
|
||||||
|
gather_facts: true
|
||||||
|
become: "{{ os_family != 'windows' }}"
|
||||||
|
roles:
|
||||||
|
- role: dev_desktop_common
|
||||||
|
when: os_family != "windows"
|
||||||
|
- role: dev_desktop_windows
|
||||||
|
when: os_family == "windows"
|
||||||
|
- role: dev_desktop_fedora_gnome
|
||||||
|
when: os_family == "fedora-gnome"
|
||||||
|
- role: dev_desktop_debian_kde
|
||||||
|
when: os_family == "debian-kde"
|
||||||
|
|
||||||
|
- name: Verify remote cloud dev desktop
|
||||||
|
hosts: cloud_desktop
|
||||||
|
gather_facts: true
|
||||||
|
become: "{{ os_family != 'windows' }}"
|
||||||
|
tasks:
|
||||||
|
- name: Verify common Linux workspace marker
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: /opt/cloud-dev-desktop/profile.env
|
||||||
|
register: common_profile_marker
|
||||||
|
when: os_family != "windows"
|
||||||
|
|
||||||
|
- name: Assert Linux profile marker exists
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- common_profile_marker.stat.exists
|
||||||
|
fail_msg: "Missing /opt/cloud-dev-desktop/profile.env marker on Linux host."
|
||||||
|
when: os_family != "windows"
|
||||||
|
|
||||||
|
- name: Verify Fedora GNOME desktop packages
|
||||||
|
ansible.builtin.command: rpm -q gnome-shell gtk3-devel gtk4-devel glib2-devel clang cmake ninja-build
|
||||||
|
changed_when: false
|
||||||
|
when: os_family == "fedora-gnome"
|
||||||
|
|
||||||
|
- name: Verify Debian KDE desktop packages
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
set -euo pipefail
|
||||||
|
dpkg-query -W plasma-desktop clang cmake ninja-build >/dev/null
|
||||||
|
if dpkg-query -W qt6-base-dev >/dev/null 2>&1; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
dpkg-query -W qtbase5-dev >/dev/null
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
changed_when: false
|
||||||
|
when: os_family == "debian-kde"
|
||||||
|
|
||||||
|
- name: Verify Node.js 22+ on Linux
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
set -euo pipefail
|
||||||
|
test "$(node --version | sed 's/^v//' | cut -d. -f1)" -ge 22
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
become_user: "{{ admin_username }}"
|
||||||
|
changed_when: false
|
||||||
|
when: os_family != "windows"
|
||||||
|
|
||||||
|
- name: Verify Go toolchain on Linux
|
||||||
|
ansible.builtin.command: /usr/local/go/bin/go version
|
||||||
|
become_user: "{{ admin_username }}"
|
||||||
|
changed_when: false
|
||||||
|
when: os_family != "windows"
|
||||||
|
|
||||||
|
- name: Verify Codex CLI on Linux
|
||||||
|
ansible.builtin.command: codex --version
|
||||||
|
become_user: "{{ admin_username }}"
|
||||||
|
changed_when: false
|
||||||
|
when:
|
||||||
|
- os_family != "windows"
|
||||||
|
- toolchains.codex | bool
|
||||||
|
|
||||||
|
- name: Verify Flutter is installed on Linux
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
set -euo pipefail
|
||||||
|
test -x {{ cloud_dev_desktop_flutter_install_root | default('/opt/flutter') }}/bin/flutter
|
||||||
|
{{ cloud_dev_desktop_flutter_install_root | default('/opt/flutter') }}/bin/flutter --version
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
become_user: "{{ admin_username }}"
|
||||||
|
environment:
|
||||||
|
HOME: "/home/{{ admin_username }}"
|
||||||
|
PUB_CACHE: "/home/{{ admin_username }}/.pub-cache"
|
||||||
|
changed_when: false
|
||||||
|
when:
|
||||||
|
- os_family != "windows"
|
||||||
|
- toolchains.flutter | bool
|
||||||
|
|
||||||
|
- name: Verify Codex CLI on Windows
|
||||||
|
ansible.windows.win_shell: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine')
|
||||||
|
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
|
||||||
|
$npmUserBin = if ($env:APPDATA) { Join-Path $env:APPDATA 'npm' } else { '' }
|
||||||
|
$extraPaths = @(
|
||||||
|
$npmUserBin,
|
||||||
|
'C:\Program Files\nodejs',
|
||||||
|
'C:\tools\flutter\bin',
|
||||||
|
'C:\Program Files\Microsoft VS Code\bin',
|
||||||
|
'C:\ProgramData\chocolatey\bin'
|
||||||
|
) | Where-Object { $_ }
|
||||||
|
$env:Path = (($machinePath, $userPath, ($extraPaths -join ';')) -join ';')
|
||||||
|
$codexCmd = Join-Path $env:APPDATA 'npm\codex.cmd'
|
||||||
|
if (-not (Test-Path $codexCmd)) {
|
||||||
|
throw "Missing Codex CLI launcher at $codexCmd"
|
||||||
|
}
|
||||||
|
$codexCmdLine = '"' + $codexCmd + '" --version'
|
||||||
|
cmd.exe /d /c $codexCmdLine | Out-Null
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Codex CLI version probe failed with exit code $LASTEXITCODE"
|
||||||
|
}
|
||||||
|
changed_when: false
|
||||||
|
when: os_family == "windows"
|
||||||
|
|
||||||
|
- name: Verify Node.js 22+ on Windows
|
||||||
|
ansible.windows.win_shell: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine')
|
||||||
|
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
|
||||||
|
$npmUserBin = if ($env:APPDATA) { Join-Path $env:APPDATA 'npm' } else { '' }
|
||||||
|
$extraPaths = @(
|
||||||
|
$npmUserBin,
|
||||||
|
'C:\Program Files\nodejs',
|
||||||
|
'C:\tools\flutter\bin',
|
||||||
|
'C:\Program Files\Microsoft VS Code\bin',
|
||||||
|
'C:\ProgramData\chocolatey\bin'
|
||||||
|
) | Where-Object { $_ }
|
||||||
|
$env:Path = (($machinePath, $userPath, ($extraPaths -join ';')) -join ';')
|
||||||
|
$nodeMajor = [int]((node --version).Trim().TrimStart('v').Split('.')[0])
|
||||||
|
if ($nodeMajor -lt 22) {
|
||||||
|
throw "Node.js 22+ is required, found $(node --version)"
|
||||||
|
}
|
||||||
|
changed_when: false
|
||||||
|
when: os_family == "windows"
|
||||||
|
|
||||||
|
- name: Verify Flutter on Windows
|
||||||
|
ansible.windows.win_shell: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine')
|
||||||
|
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
|
||||||
|
$npmUserBin = if ($env:APPDATA) { Join-Path $env:APPDATA 'npm' } else { '' }
|
||||||
|
$extraPaths = @(
|
||||||
|
$npmUserBin,
|
||||||
|
'C:\Program Files\nodejs',
|
||||||
|
'C:\tools\flutter\bin',
|
||||||
|
'C:\Program Files\Microsoft VS Code\bin',
|
||||||
|
'C:\ProgramData\chocolatey\bin'
|
||||||
|
) | Where-Object { $_ }
|
||||||
|
$env:Path = (($machinePath, $userPath, ($extraPaths -join ';')) -join ';')
|
||||||
|
flutter --version | Out-Null
|
||||||
|
changed_when: false
|
||||||
|
when:
|
||||||
|
- os_family == "windows"
|
||||||
|
- toolchains.flutter | bool
|
||||||
|
|
||||||
|
- name: Verify VS Code on Windows
|
||||||
|
ansible.windows.win_shell: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine')
|
||||||
|
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
|
||||||
|
$npmUserBin = if ($env:APPDATA) { Join-Path $env:APPDATA 'npm' } else { '' }
|
||||||
|
$extraPaths = @(
|
||||||
|
$npmUserBin,
|
||||||
|
'C:\Program Files\nodejs',
|
||||||
|
'C:\tools\flutter\bin',
|
||||||
|
'C:\Program Files\Microsoft VS Code\bin',
|
||||||
|
'C:\ProgramData\chocolatey\bin'
|
||||||
|
) | Where-Object { $_ }
|
||||||
|
$env:Path = (($machinePath, $userPath, ($extraPaths -join ';')) -join ';')
|
||||||
|
Get-Command code | Out-Null
|
||||||
|
changed_when: false
|
||||||
|
when:
|
||||||
|
- os_family == "windows"
|
||||||
|
- toolchains.vscode | bool
|
||||||
|
|
||||||
|
- name: Verify Git on Windows
|
||||||
|
ansible.windows.win_shell: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine')
|
||||||
|
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
|
||||||
|
$npmUserBin = if ($env:APPDATA) { Join-Path $env:APPDATA 'npm' } else { '' }
|
||||||
|
$extraPaths = @(
|
||||||
|
$npmUserBin,
|
||||||
|
'C:\Program Files\Git\cmd',
|
||||||
|
'C:\Program Files\nodejs',
|
||||||
|
'C:\tools\flutter\bin',
|
||||||
|
'C:\Program Files\Microsoft VS Code\bin',
|
||||||
|
'C:\ProgramData\chocolatey\bin'
|
||||||
|
) | Where-Object { $_ }
|
||||||
|
$env:Path = (($machinePath, $userPath, ($extraPaths -join ';')) -join ';')
|
||||||
|
git --version | Out-Null
|
||||||
|
changed_when: false
|
||||||
|
when: os_family == "windows"
|
||||||
|
|
||||||
|
- name: Verify Android Studio on Windows
|
||||||
|
ansible.windows.win_shell: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
if (-not (Test-Path 'C:\Program Files\Android\Android Studio\bin\studio64.exe')) {
|
||||||
|
throw 'Missing Android Studio executable at C:\Program Files\Android\Android Studio\bin\studio64.exe'
|
||||||
|
}
|
||||||
|
changed_when: false
|
||||||
|
when:
|
||||||
|
- os_family == "windows"
|
||||||
|
- toolchains.android_studio | bool
|
||||||
|
|
||||||
|
- name: Verify Visual Studio desktop C++ toolchain on Windows
|
||||||
|
ansible.windows.win_shell: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'
|
||||||
|
if (-not (Test-Path $vswhere)) {
|
||||||
|
throw "Missing vswhere at $vswhere"
|
||||||
|
}
|
||||||
|
$installPath = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath
|
||||||
|
if (-not $installPath) {
|
||||||
|
throw 'Visual Studio Build Tools with Desktop C++ workload is not installed'
|
||||||
|
}
|
||||||
|
changed_when: false
|
||||||
|
when: os_family == "windows"
|
||||||
|
|
||||||
|
- name: Verify Android SDK and emulator on Windows
|
||||||
|
ansible.windows.win_shell: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$androidSdkRoot = Join-Path $env:LOCALAPPDATA 'Android\Sdk'
|
||||||
|
$requiredPaths = @(
|
||||||
|
(Join-Path $androidSdkRoot 'platform-tools\adb.exe'),
|
||||||
|
(Join-Path $androidSdkRoot 'emulator\emulator.exe'),
|
||||||
|
(Join-Path $androidSdkRoot 'cmdline-tools\latest\bin\sdkmanager.bat')
|
||||||
|
)
|
||||||
|
foreach ($required in $requiredPaths) {
|
||||||
|
if (-not (Test-Path $required)) {
|
||||||
|
throw "Missing Android SDK component: $required"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
changed_when: false
|
||||||
|
when:
|
||||||
|
- os_family == "windows"
|
||||||
|
- toolchains.android_studio | bool
|
||||||
|
|
||||||
|
- name: Verify Windows Android virtualization features
|
||||||
|
ansible.windows.win_shell: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$featureNames = @(
|
||||||
|
'Microsoft-Hyper-V-All',
|
||||||
|
'HypervisorPlatform',
|
||||||
|
'VirtualMachinePlatform'
|
||||||
|
)
|
||||||
|
foreach ($featureName in $featureNames) {
|
||||||
|
$feature = Get-WindowsOptionalFeature -Online -FeatureName $featureName
|
||||||
|
if ($feature.State -ne 'Enabled') {
|
||||||
|
throw "Windows optional feature $featureName is not enabled"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
& (Join-Path $env:LOCALAPPDATA 'Android\Sdk\emulator\emulator-check.exe') accel | Out-Null
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Android emulator acceleration probe failed with exit code $LASTEXITCODE"
|
||||||
|
}
|
||||||
|
changed_when: false
|
||||||
|
when:
|
||||||
|
- os_family == "windows"
|
||||||
|
- toolchains.android_studio | bool
|
||||||
|
|
||||||
|
- name: Verify Windows SSHD service
|
||||||
|
ansible.windows.win_shell: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$sshd = Get-Service sshd
|
||||||
|
if ($sshd.Status -ne 'Running') {
|
||||||
|
throw "SSHD service is not running"
|
||||||
|
}
|
||||||
|
changed_when: false
|
||||||
|
when: os_family == "windows"
|
||||||
@ -9,6 +9,19 @@
|
|||||||
agent_svc_plus_repo_version: >-
|
agent_svc_plus_repo_version: >-
|
||||||
{{ lookup('ansible.builtin.env', 'AGENT_REPO_VERSION')
|
{{ lookup('ansible.builtin.env', 'AGENT_REPO_VERSION')
|
||||||
| default('main', true) }}
|
| default('main', true) }}
|
||||||
|
agent_svc_plus_release_tag: >-
|
||||||
|
{{ lookup('ansible.builtin.env', 'AGENT_RELEASE_TAG')
|
||||||
|
| default(
|
||||||
|
(lookup('ansible.builtin.env', 'AGENT_REPO_VERSION')
|
||||||
|
| default('main', true))
|
||||||
|
if ((lookup('ansible.builtin.env', 'AGENT_REPO_VERSION')
|
||||||
|
| default('main', true)) is match('^v.+'))
|
||||||
|
else '',
|
||||||
|
true
|
||||||
|
) }}
|
||||||
|
agent_svc_plus_binary_src: >-
|
||||||
|
{{ lookup('ansible.builtin.env', 'AGENT_BINARY_SRC')
|
||||||
|
| default('', true) }}
|
||||||
agent_svc_plus_app_dir: >-
|
agent_svc_plus_app_dir: >-
|
||||||
{{ lookup('ansible.builtin.env', 'AGENT_APP_DIR')
|
{{ lookup('ansible.builtin.env', 'AGENT_APP_DIR')
|
||||||
| default('/opt/agent.svc.plus', true) }}
|
| default('/opt/agent.svc.plus', true) }}
|
||||||
|
|||||||
2
deploy_apisix.yml
Normal file
2
deploy_apisix.yml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
---
|
||||||
|
- import_playbook: deploy_apisix_svc.plus.yaml
|
||||||
7
deploy_stunnel-client.yml
Normal file
7
deploy_stunnel-client.yml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
- name: Deploy stunnel-client release
|
||||||
|
hosts: server
|
||||||
|
become: true
|
||||||
|
gather_facts: true
|
||||||
|
roles:
|
||||||
|
- role: ../github-org-cloud-neutral-toolkit/ansible/roles/stunnel_client_deploy
|
||||||
@ -1,20 +1,60 @@
|
|||||||
- import_playbook: deploy_xray_exporter.yml
|
- name: Load stack environment values from local .env file
|
||||||
|
hosts: all
|
||||||
|
gather_facts: false
|
||||||
vars:
|
vars:
|
||||||
stack_env_file: >-
|
stack_env_file: >-
|
||||||
{{ lookup('ansible.builtin.env', 'STACK_ENV_FILE')
|
{{ lookup('ansible.builtin.env', 'STACK_ENV_FILE')
|
||||||
| default(playbook_dir ~ '/.env', true) }}
|
| default(playbook_dir ~ '/.env', true) }}
|
||||||
xray_exporter_hosts: >-
|
tasks:
|
||||||
{{ lookup('ansible.builtin.env', 'STACK_TARGET_HOST')
|
- name: Parse stack .env file into a dictionary
|
||||||
| default(lookup('ansible.builtin.env', 'XRAY_EXPORTER_HOSTS')
|
run_once: true
|
||||||
| default('xray_exporter', true), true) }}
|
delegate_to: localhost
|
||||||
xray_exporter_internal_service_token: >-
|
ansible.builtin.command:
|
||||||
{{ lookup('ansible.builtin.env', 'INTERNAL_SERVICE_TOKEN')
|
argv:
|
||||||
| default(lookup('ansible.builtin.ini', 'INTERNAL_SERVICE_TOKEN type=properties file=' ~ stack_env_file, errors='ignore')
|
- python3
|
||||||
| default('', true), true) }}
|
- -c
|
||||||
stack_services: >-
|
- |
|
||||||
{{ lookup('ansible.builtin.env', 'STACK_SERVICES')
|
import json
|
||||||
| default('xray-exporter,billing-service,accounts,console,agent,xworkmate-bridge', true) }}
|
import os
|
||||||
when: "'xray-exporter' in (stack_services.split(',') | map('trim') | list)"
|
import re
|
||||||
|
import shlex
|
||||||
|
import sys
|
||||||
|
|
||||||
|
path = sys.argv[1]
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
if os.path.exists(path):
|
||||||
|
with open(path, encoding="utf-8", errors="ignore") as handle:
|
||||||
|
for raw_line in handle:
|
||||||
|
line = raw_line.strip()
|
||||||
|
if not line or line.startswith("#"):
|
||||||
|
continue
|
||||||
|
match = re.match(r"^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$", line)
|
||||||
|
if not match:
|
||||||
|
continue
|
||||||
|
key, value = match.groups()
|
||||||
|
value = value.strip()
|
||||||
|
if value:
|
||||||
|
try:
|
||||||
|
parts = shlex.split(value, comments=False, posix=True)
|
||||||
|
value = parts[0] if parts else ""
|
||||||
|
except ValueError:
|
||||||
|
if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'):
|
||||||
|
value = value[1:-1]
|
||||||
|
data[key] = value
|
||||||
|
|
||||||
|
print(json.dumps(data))
|
||||||
|
- "{{ stack_env_file }}"
|
||||||
|
register: stack_env_parse_result
|
||||||
|
changed_when: false
|
||||||
|
check_mode: false
|
||||||
|
|
||||||
|
- name: Store parsed stack environment values
|
||||||
|
run_once: true
|
||||||
|
delegate_to: localhost
|
||||||
|
delegate_facts: true
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
stack_env_map: "{{ stack_env_parse_result.stdout | default('{}', true) | from_json }}"
|
||||||
|
|
||||||
- import_playbook: deploy_billing_service.yml
|
- import_playbook: deploy_billing_service.yml
|
||||||
vars:
|
vars:
|
||||||
@ -26,18 +66,91 @@
|
|||||||
| default(lookup('ansible.builtin.env', 'BILLING_SERVICE_HOSTS')
|
| default(lookup('ansible.builtin.env', 'BILLING_SERVICE_HOSTS')
|
||||||
| default('billing_service', true), true) }}
|
| default('billing_service', true), true) }}
|
||||||
billing_service_database_url: >-
|
billing_service_database_url: >-
|
||||||
{{ lookup('ansible.builtin.env', 'DATABASE_URL')
|
{{
|
||||||
| default(lookup('ansible.builtin.ini', 'DATABASE_URL type=properties file=' ~ stack_env_file, errors='ignore')
|
lookup('ansible.builtin.env', 'DATABASE_URL')
|
||||||
| default('', true), true) }}
|
| default(hostvars['localhost'].stack_env_map.DATABASE_URL
|
||||||
|
| default('', true), true)
|
||||||
|
or (
|
||||||
|
'postgres://%s:%s@%s:%s/%s?sslmode=disable'
|
||||||
|
| format(
|
||||||
|
lookup('ansible.builtin.env', 'POSTGRES_USER')
|
||||||
|
| default(hostvars['localhost'].stack_env_map.POSTGRES_USER
|
||||||
|
| default('svcplus_vps', true), true),
|
||||||
|
lookup('ansible.builtin.env', 'POSTGRES_PASSWORD')
|
||||||
|
| default(hostvars['localhost'].stack_env_map.POSTGRES_PASSWORD
|
||||||
|
| default('', true), true),
|
||||||
|
lookup('ansible.builtin.env', 'BILLING_DB_HOST')
|
||||||
|
| default(hostvars['localhost'].stack_env_map.BILLING_DB_HOST
|
||||||
|
| default('stunnel-client', true), true),
|
||||||
|
lookup('ansible.builtin.env', 'BILLING_DB_PORT')
|
||||||
|
| default(hostvars['localhost'].stack_env_map.BILLING_DB_PORT
|
||||||
|
| default('15432', true), true),
|
||||||
|
lookup('ansible.builtin.env', 'BILLING_DB_NAME')
|
||||||
|
| default(hostvars['localhost'].stack_env_map.BILLING_DB_NAME
|
||||||
|
| default('account', true), true)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}}
|
||||||
billing_service_exporter_base_url: >-
|
billing_service_exporter_base_url: >-
|
||||||
{{ lookup('ansible.builtin.env', 'EXPORTER_BASE_URL')
|
{{ lookup('ansible.builtin.env', 'EXPORTER_BASE_URL')
|
||||||
| default(lookup('ansible.builtin.ini', 'EXPORTER_BASE_URL type=properties file=' ~ stack_env_file, errors='ignore')
|
| default(hostvars['localhost'].stack_env_map.EXPORTER_BASE_URL
|
||||||
| default('http://127.0.0.1:8080', true), true) }}
|
| default('http://127.0.0.1:8080', true), true) }}
|
||||||
stack_services: >-
|
stack_services: >-
|
||||||
{{ lookup('ansible.builtin.env', 'STACK_SERVICES')
|
{{ lookup('ansible.builtin.env', 'STACK_SERVICES')
|
||||||
| default('xray-exporter,billing-service,accounts,console,agent,xworkmate-bridge', true) }}
|
| default('billing-service,xworkmate-bridge,xray-exporter,agent,accounts,stunnel-client,apisix,console', true) }}
|
||||||
when: "'billing-service' in (stack_services.split(',') | map('trim') | list)"
|
when: "'billing-service' in (stack_services.split(',') | map('trim') | list)"
|
||||||
|
|
||||||
|
- import_playbook: deploy_xworkmate_bridge_vhosts.yml
|
||||||
|
vars:
|
||||||
|
xworkmate_bridge_hosts: >-
|
||||||
|
{{ lookup('ansible.builtin.env', 'STACK_TARGET_HOST')
|
||||||
|
| default(lookup('ansible.builtin.env', 'XWORKMATE_BRIDGE_HOSTS')
|
||||||
|
| default('all', true), true) }}
|
||||||
|
stack_services: >-
|
||||||
|
{{ lookup('ansible.builtin.env', 'STACK_SERVICES')
|
||||||
|
| default('billing-service,xworkmate-bridge,xray-exporter,agent,accounts,stunnel-client,apisix,console', true) }}
|
||||||
|
when: "'xworkmate-bridge' in (stack_services.split(',') | map('trim') | list)"
|
||||||
|
|
||||||
|
- import_playbook: deploy_xray_exporter.yml
|
||||||
|
vars:
|
||||||
|
stack_env_file: >-
|
||||||
|
{{ lookup('ansible.builtin.env', 'STACK_ENV_FILE')
|
||||||
|
| default(playbook_dir ~ '/.env', true) }}
|
||||||
|
xray_exporter_hosts: >-
|
||||||
|
{{ lookup('ansible.builtin.env', 'STACK_TARGET_HOST')
|
||||||
|
| default(lookup('ansible.builtin.env', 'XRAY_EXPORTER_HOSTS')
|
||||||
|
| default('xray_exporter', true), true) }}
|
||||||
|
xray_exporter_internal_service_token: >-
|
||||||
|
{{ lookup('ansible.builtin.env', 'INTERNAL_SERVICE_TOKEN')
|
||||||
|
| default(hostvars['localhost'].stack_env_map.INTERNAL_SERVICE_TOKEN
|
||||||
|
| default('', true), true) }}
|
||||||
|
stack_services: >-
|
||||||
|
{{ lookup('ansible.builtin.env', 'STACK_SERVICES')
|
||||||
|
| default('billing-service,xworkmate-bridge,xray-exporter,agent,accounts,stunnel-client,apisix,console', true) }}
|
||||||
|
when: "'xray-exporter' in (stack_services.split(',') | map('trim') | list)"
|
||||||
|
|
||||||
|
- import_playbook: deploy_agent_svc_plus.yml
|
||||||
|
vars:
|
||||||
|
stack_env_file: >-
|
||||||
|
{{ lookup('ansible.builtin.env', 'STACK_ENV_FILE')
|
||||||
|
| default(playbook_dir ~ '/.env', true) }}
|
||||||
|
agent_service_hosts: >-
|
||||||
|
{{ lookup('ansible.builtin.env', 'STACK_TARGET_HOST')
|
||||||
|
| default(lookup('ansible.builtin.env', 'AGENT_SERVICE_HOSTS')
|
||||||
|
| default('agent_svc_plus', true), true) }}
|
||||||
|
agent_api_token: >-
|
||||||
|
{{ lookup('ansible.builtin.env', 'INTERNAL_SERVICE_TOKEN')
|
||||||
|
| default(hostvars['localhost'].stack_env_map.INTERNAL_SERVICE_TOKEN
|
||||||
|
| default('', true), true) }}
|
||||||
|
agent_billing_base_url: >-
|
||||||
|
{{ lookup('ansible.builtin.env', 'BILLING_SERVICE_BASE_URL')
|
||||||
|
| default(hostvars['localhost'].stack_env_map.BILLING_SERVICE_BASE_URL
|
||||||
|
| default('http://127.0.0.1:8081', true), true) }}
|
||||||
|
stack_services: >-
|
||||||
|
{{ lookup('ansible.builtin.env', 'STACK_SERVICES')
|
||||||
|
| default('billing-service,xworkmate-bridge,xray-exporter,agent,accounts,stunnel-client,apisix,console', true) }}
|
||||||
|
when: "'agent' in (stack_services.split(',') | map('trim') | list)"
|
||||||
|
|
||||||
- import_playbook: deploy_accounts_svc_plus.yml
|
- import_playbook: deploy_accounts_svc_plus.yml
|
||||||
vars:
|
vars:
|
||||||
stack_env_file: >-
|
stack_env_file: >-
|
||||||
@ -49,17 +162,31 @@
|
|||||||
| default('accounts', true), true) }}
|
| default('accounts', true), true) }}
|
||||||
accounts_service_image_repo: >-
|
accounts_service_image_repo: >-
|
||||||
{{ lookup('ansible.builtin.env', 'ACCOUNTS_IMAGE_REPO')
|
{{ lookup('ansible.builtin.env', 'ACCOUNTS_IMAGE_REPO')
|
||||||
| default(lookup('ansible.builtin.ini', 'ACCOUNTS_IMAGE_REPO type=properties file=' ~ stack_env_file, errors='ignore')
|
| default(hostvars['localhost'].stack_env_map.ACCOUNTS_IMAGE_REPO
|
||||||
| default('ghcr.io/x-evor/accounts', true), true) }}
|
| default('ghcr.io/x-evor/accounts', true), true) }}
|
||||||
accounts_service_image_tag: >-
|
accounts_service_image_tag: >-
|
||||||
{{ lookup('ansible.builtin.env', 'ACCOUNTS_IMAGE_TAG')
|
{{ lookup('ansible.builtin.env', 'ACCOUNTS_IMAGE_TAG')
|
||||||
| default(lookup('ansible.builtin.ini', 'ACCOUNTS_IMAGE_TAG type=properties file=' ~ stack_env_file, errors='ignore')
|
| default(hostvars['localhost'].stack_env_map.ACCOUNTS_IMAGE_TAG
|
||||||
| default('latest', true), true) }}
|
| default('latest', true), true) }}
|
||||||
stack_services: >-
|
stack_services: >-
|
||||||
{{ lookup('ansible.builtin.env', 'STACK_SERVICES')
|
{{ lookup('ansible.builtin.env', 'STACK_SERVICES')
|
||||||
| default('xray-exporter,billing-service,accounts,console,agent,xworkmate-bridge', true) }}
|
| default('billing-service,xworkmate-bridge,xray-exporter,agent,accounts,stunnel-client,apisix,console', true) }}
|
||||||
when: "'accounts' in (stack_services.split(',') | map('trim') | list)"
|
when: "'accounts' in (stack_services.split(',') | map('trim') | list)"
|
||||||
|
|
||||||
|
- import_playbook: deploy_stunnel-client.yml
|
||||||
|
vars:
|
||||||
|
stack_services: >-
|
||||||
|
{{ lookup('ansible.builtin.env', 'STACK_SERVICES')
|
||||||
|
| default('billing-service,xworkmate-bridge,xray-exporter,agent,accounts,stunnel-client,apisix,console', true) }}
|
||||||
|
when: "'stunnel-client' in (stack_services.split(',') | map('trim') | list)"
|
||||||
|
|
||||||
|
- import_playbook: deploy_apisix.yml
|
||||||
|
vars:
|
||||||
|
stack_services: >-
|
||||||
|
{{ lookup('ansible.builtin.env', 'STACK_SERVICES')
|
||||||
|
| default('billing-service,xworkmate-bridge,xray-exporter,agent,accounts,stunnel-client,apisix,console', true) }}
|
||||||
|
when: "'apisix' in (stack_services.split(',') | map('trim') | list)"
|
||||||
|
|
||||||
- import_playbook: deploy_console_svc_plus.yml
|
- import_playbook: deploy_console_svc_plus.yml
|
||||||
vars:
|
vars:
|
||||||
stack_env_file: >-
|
stack_env_file: >-
|
||||||
@ -75,50 +202,17 @@
|
|||||||
| default(false, true), true) | bool }}
|
| default(false, true), true) | bool }}
|
||||||
console_service_frontend_image: >-
|
console_service_frontend_image: >-
|
||||||
{{ lookup('ansible.builtin.env', 'FRONTEND_IMAGE')
|
{{ lookup('ansible.builtin.env', 'FRONTEND_IMAGE')
|
||||||
| default(lookup('ansible.builtin.ini', 'FRONTEND_IMAGE type=properties file=' ~ stack_env_file, errors='ignore')
|
| default(hostvars['localhost'].stack_env_map.FRONTEND_IMAGE
|
||||||
| default('', true), true) }}
|
| default('', true), true) }}
|
||||||
console_service_registry_username: >-
|
console_service_registry_username: >-
|
||||||
{{ lookup('ansible.builtin.env', 'GHCR_USERNAME')
|
{{ lookup('ansible.builtin.env', 'GHCR_USERNAME')
|
||||||
| default(lookup('ansible.builtin.ini', 'GHCR_USERNAME type=properties file=' ~ stack_env_file, errors='ignore')
|
| default(hostvars['localhost'].stack_env_map.GHCR_USERNAME
|
||||||
| default('', true), true) }}
|
| default('', true), true) }}
|
||||||
console_service_registry_password: >-
|
console_service_registry_password: >-
|
||||||
{{ lookup('ansible.builtin.env', 'GHCR_PASSWORD')
|
{{ lookup('ansible.builtin.env', 'GHCR_PASSWORD')
|
||||||
| default(lookup('ansible.builtin.ini', 'GHCR_PASSWORD type=properties file=' ~ stack_env_file, errors='ignore')
|
| default(hostvars['localhost'].stack_env_map.GHCR_PASSWORD
|
||||||
| default('', true), true) }}
|
| default('', true), true) }}
|
||||||
stack_services: >-
|
stack_services: >-
|
||||||
{{ lookup('ansible.builtin.env', 'STACK_SERVICES')
|
{{ lookup('ansible.builtin.env', 'STACK_SERVICES')
|
||||||
| default('xray-exporter,billing-service,accounts,console,agent,xworkmate-bridge', true) }}
|
| default('billing-service,xworkmate-bridge,xray-exporter,agent,accounts,stunnel-client,apisix,console', true) }}
|
||||||
when: "'console' in (stack_services.split(',') | map('trim') | list)"
|
when: "'console' in (stack_services.split(',') | map('trim') | list)"
|
||||||
|
|
||||||
- import_playbook: deploy_agent_svc_plus.yml
|
|
||||||
vars:
|
|
||||||
stack_env_file: >-
|
|
||||||
{{ lookup('ansible.builtin.env', 'STACK_ENV_FILE')
|
|
||||||
| default(playbook_dir ~ '/.env', true) }}
|
|
||||||
agent_service_hosts: >-
|
|
||||||
{{ lookup('ansible.builtin.env', 'STACK_TARGET_HOST')
|
|
||||||
| default(lookup('ansible.builtin.env', 'AGENT_SERVICE_HOSTS')
|
|
||||||
| default('agent_svc_plus', true), true) }}
|
|
||||||
agent_api_token: >-
|
|
||||||
{{ lookup('ansible.builtin.env', 'INTERNAL_SERVICE_TOKEN')
|
|
||||||
| default(lookup('ansible.builtin.ini', 'INTERNAL_SERVICE_TOKEN type=properties file=' ~ stack_env_file, errors='ignore')
|
|
||||||
| default('', true), true) }}
|
|
||||||
agent_billing_base_url: >-
|
|
||||||
{{ lookup('ansible.builtin.env', 'BILLING_SERVICE_BASE_URL')
|
|
||||||
| default(lookup('ansible.builtin.ini', 'BILLING_SERVICE_BASE_URL type=properties file=' ~ stack_env_file, errors='ignore')
|
|
||||||
| default('http://127.0.0.1:8081', true), true) }}
|
|
||||||
stack_services: >-
|
|
||||||
{{ lookup('ansible.builtin.env', 'STACK_SERVICES')
|
|
||||||
| default('xray-exporter,billing-service,accounts,console,agent,xworkmate-bridge', true) }}
|
|
||||||
when: "'agent' in (stack_services.split(',') | map('trim') | list)"
|
|
||||||
|
|
||||||
- import_playbook: deploy_xworkmate_bridge_vhosts.yml
|
|
||||||
vars:
|
|
||||||
xworkmate_bridge_hosts: >-
|
|
||||||
{{ lookup('ansible.builtin.env', 'STACK_TARGET_HOST')
|
|
||||||
| default(lookup('ansible.builtin.env', 'XWORKMATE_BRIDGE_HOSTS')
|
|
||||||
| default('all', true), true) }}
|
|
||||||
stack_services: >-
|
|
||||||
{{ lookup('ansible.builtin.env', 'STACK_SERVICES')
|
|
||||||
| default('xray-exporter,billing-service,accounts,console,agent,xworkmate-bridge', true) }}
|
|
||||||
when: "'xworkmate-bridge' in (stack_services.split(',') | map('trim') | list)"
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
| default(playbook_dir ~ '/../xray-exporter', true) }}
|
| default(playbook_dir ~ '/../xray-exporter', true) }}
|
||||||
xray_exporter_node_id: >-
|
xray_exporter_node_id: >-
|
||||||
{{ lookup('ansible.builtin.env', 'EXPORTER_NODE_ID')
|
{{ lookup('ansible.builtin.env', 'EXPORTER_NODE_ID')
|
||||||
| default('node-xhttp.svc.plus', true) }}
|
| default(xray_exporter_node_id_custom | default(inventory_hostname, true), true) }}
|
||||||
xray_exporter_env_name: >-
|
xray_exporter_env_name: >-
|
||||||
{{ lookup('ansible.builtin.env', 'EXPORTER_ENV')
|
{{ lookup('ansible.builtin.env', 'EXPORTER_ENV')
|
||||||
| default('prod', true) }}
|
| default('prod', true) }}
|
||||||
|
|||||||
9
destroy_cloud_dev_desktop.yml
Normal file
9
destroy_cloud_dev_desktop.yml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
- name: Destroy cloud dev desktop infrastructure
|
||||||
|
hosts: localhost
|
||||||
|
connection: local
|
||||||
|
gather_facts: true
|
||||||
|
roles:
|
||||||
|
- role: cloud_vm_request_validate
|
||||||
|
- role: "{{ (provider == 'azure') | ternary('azure_dev_desktop_lifecycle', 'gcp_dev_desktop_lifecycle') }}"
|
||||||
|
vars:
|
||||||
|
cloud_lifecycle_action: destroy
|
||||||
@ -5,7 +5,7 @@ cn-front.svc.plus ansible_host=47.120.61.35 ansible_user=root ansible_ssh_user=r
|
|||||||
|
|
||||||
[jp_xhttp_contabo_host]
|
[jp_xhttp_contabo_host]
|
||||||
# services: api.svc.plus, console.svc.plus, accounts.svc.plus, acp-server.svc.plus, xworkmate-bridge.svc.plus, vault.svc.plus, openclaw.svc.plus, postgresql.svc.plus
|
# services: api.svc.plus, console.svc.plus, accounts.svc.plus, acp-server.svc.plus, xworkmate-bridge.svc.plus, vault.svc.plus, openclaw.svc.plus, postgresql.svc.plus
|
||||||
jp-xhttp-contabo.svc.plus ansible_host=46.250.251.132 ansible_user=root ansible_ssh_user=root service_domains=api.svc.plus,console.svc.plus,accounts.svc.plus,acp-server.svc.plus,xworkmate-bridge.svc.plus,vault.svc.plus,openclaw.svc.plus,postgresql.svc.plus
|
jp-xhttp-contabo.svc.plus ansible_host=46.250.251.132 ansible_user=root ansible_ssh_user=root service_domains=api.svc.plus,console.svc.plus,accounts.svc.plus,acp-server.svc.plus,xworkmate-bridge.svc.plus,vault.svc.plus,openclaw.svc.plus,postgresql.svc.plus xray_exporter_node_id_custom=jp-xhttp-contabo.svc.plus
|
||||||
|
|
||||||
[tky_proxy_host]
|
[tky_proxy_host]
|
||||||
# services: tky-proxy.svc.plus
|
# services: tky-proxy.svc.plus
|
||||||
|
|||||||
232
roles/azure_dev_desktop_lifecycle/tasks/create.yml
Normal file
232
roles/azure_dev_desktop_lifecycle/tasks/create.yml
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
- name: Preview Azure create request
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg:
|
||||||
|
- "azure_resource_group={{ azure_resource_group }}"
|
||||||
|
- "azure_vm_name={{ azure_vm_name }}"
|
||||||
|
- "azure_computer_name={{ azure_computer_name }}"
|
||||||
|
- "azure_location={{ azure_location }}"
|
||||||
|
- "allowed_cidrs={{ allowed_cidrs | join(',') }}"
|
||||||
|
- "allowed_tcp_ports={{ allowed_tcp_ports | join(',') }}"
|
||||||
|
- "state_file={{ cloud_vm_state_file }}"
|
||||||
|
|
||||||
|
- name: Ensure Azure resource group exists
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv:
|
||||||
|
- az
|
||||||
|
- group
|
||||||
|
- create
|
||||||
|
- --subscription
|
||||||
|
- "{{ azure_subscription_id }}"
|
||||||
|
- --name
|
||||||
|
- "{{ azure_resource_group }}"
|
||||||
|
- --location
|
||||||
|
- "{{ azure_location }}"
|
||||||
|
- --tags
|
||||||
|
- "{{ tags | dict2items | map('join', '=') | join(' ') }}"
|
||||||
|
changed_when: true
|
||||||
|
when: not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Ensure Azure virtual network exists
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv:
|
||||||
|
- az
|
||||||
|
- network
|
||||||
|
- vnet
|
||||||
|
- create
|
||||||
|
- --subscription
|
||||||
|
- "{{ azure_subscription_id }}"
|
||||||
|
- --resource-group
|
||||||
|
- "{{ azure_resource_group }}"
|
||||||
|
- --name
|
||||||
|
- "{{ azure_virtual_network }}"
|
||||||
|
- --address-prefixes
|
||||||
|
- "{{ azure_vnet_cidr | default('10.42.0.0/16') }}"
|
||||||
|
- --subnet-name
|
||||||
|
- "{{ azure_subnet }}"
|
||||||
|
- --subnet-prefixes
|
||||||
|
- "{{ azure_subnet_cidr | default('10.42.1.0/24') }}"
|
||||||
|
changed_when: true
|
||||||
|
when: not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Ensure Azure network security group exists
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv:
|
||||||
|
- az
|
||||||
|
- network
|
||||||
|
- nsg
|
||||||
|
- create
|
||||||
|
- --subscription
|
||||||
|
- "{{ azure_subscription_id }}"
|
||||||
|
- --resource-group
|
||||||
|
- "{{ azure_resource_group }}"
|
||||||
|
- --name
|
||||||
|
- "{{ azure_network_security_group }}"
|
||||||
|
- --location
|
||||||
|
- "{{ azure_location }}"
|
||||||
|
changed_when: true
|
||||||
|
when: not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Create Azure allowlist rules
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv:
|
||||||
|
- az
|
||||||
|
- network
|
||||||
|
- nsg
|
||||||
|
- rule
|
||||||
|
- create
|
||||||
|
- --subscription
|
||||||
|
- "{{ azure_subscription_id }}"
|
||||||
|
- --resource-group
|
||||||
|
- "{{ azure_resource_group }}"
|
||||||
|
- --nsg-name
|
||||||
|
- "{{ azure_network_security_group }}"
|
||||||
|
- --name
|
||||||
|
- "allow-tcp-{{ port }}-{{ '%03d' | format((index | int) + ((port_index | int) * 100) + 100) }}"
|
||||||
|
- --priority
|
||||||
|
- "{{ '%d' | format((index | int) + ((port_index | int) * 100) + 100) }}"
|
||||||
|
- --access
|
||||||
|
- Allow
|
||||||
|
- --protocol
|
||||||
|
- Tcp
|
||||||
|
- --direction
|
||||||
|
- Inbound
|
||||||
|
- --source-address-prefixes
|
||||||
|
- "{{ cidr }}"
|
||||||
|
- --source-port-ranges
|
||||||
|
- "*"
|
||||||
|
- --destination-port-ranges
|
||||||
|
- "{{ port | string }}"
|
||||||
|
loop: "{{ allowed_cidrs | product(allowed_tcp_ports) | list }}"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.0 }} -> {{ item.1 }}"
|
||||||
|
index_var: combo_index
|
||||||
|
changed_when: true
|
||||||
|
when: not ansible_check_mode
|
||||||
|
vars:
|
||||||
|
cidr: "{{ item.0 }}"
|
||||||
|
port: "{{ item.1 }}"
|
||||||
|
index: "{{ combo_index % (allowed_cidrs | length) }}"
|
||||||
|
port_index: "{{ combo_index // (allowed_cidrs | length) }}"
|
||||||
|
|
||||||
|
- name: Build Azure VM create command
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
azure_vm_create_command: >-
|
||||||
|
az vm create
|
||||||
|
--subscription {{ azure_subscription_id | quote }}
|
||||||
|
--resource-group {{ azure_resource_group | quote }}
|
||||||
|
--name {{ azure_vm_name | quote }}
|
||||||
|
--computer-name {{ azure_computer_name | quote }}
|
||||||
|
--image {{ (azure_image_publisher ~ ':' ~ azure_image_offer ~ ':' ~ azure_image_sku ~ ':' ~ azure_image_version) | quote }}
|
||||||
|
--size {{ vm_size | quote }}
|
||||||
|
--admin-username {{ admin_username | quote }}
|
||||||
|
--vnet-name {{ azure_virtual_network | quote }}
|
||||||
|
--subnet {{ azure_subnet | quote }}
|
||||||
|
--nsg {{ azure_network_security_group | quote }}
|
||||||
|
--public-ip-sku Standard
|
||||||
|
--public-ip-address {{ azure_public_ip_name | quote }}
|
||||||
|
--storage-sku Premium_LRS
|
||||||
|
--os-disk-size-gb {{ disk_gb | int }}
|
||||||
|
--tags {{ tags | dict2items | map('join', '=') | join(' ') | quote }}
|
||||||
|
{% if os_family == 'windows' %}
|
||||||
|
--admin-password {{ azure_admin_password | quote }}
|
||||||
|
{% else %}
|
||||||
|
--ssh-key-values {{ ssh_public_key_path | quote }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
- name: Create Azure VM
|
||||||
|
ansible.builtin.shell: "{{ azure_vm_create_command }}"
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
changed_when: true
|
||||||
|
when: not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Fetch Azure VM networking facts
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv:
|
||||||
|
- az
|
||||||
|
- vm
|
||||||
|
- show
|
||||||
|
- --subscription
|
||||||
|
- "{{ azure_subscription_id }}"
|
||||||
|
- --resource-group
|
||||||
|
- "{{ azure_resource_group }}"
|
||||||
|
- --name
|
||||||
|
- "{{ azure_vm_name }}"
|
||||||
|
- --show-details
|
||||||
|
- --query
|
||||||
|
- "{publicIp:publicIps,privateIp:privateIps}"
|
||||||
|
- -o
|
||||||
|
- json
|
||||||
|
register: azure_vm_network_json
|
||||||
|
changed_when: false
|
||||||
|
when: not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Set Azure VM connection facts
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
cloud_vm_public_ip: "{{ (azure_vm_network_json.stdout | from_json).publicIp | default('127.0.0.1') }}"
|
||||||
|
cloud_vm_private_ip: "{{ (azure_vm_network_json.stdout | from_json).privateIp | default('127.0.0.1') }}"
|
||||||
|
cloud_vm_admin_user: "{{ admin_username }}"
|
||||||
|
when: not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Set Azure dry-run connection facts
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
cloud_vm_public_ip: "{{ cloud_vm_public_ip | default('198.51.100.10') }}"
|
||||||
|
cloud_vm_private_ip: "{{ cloud_vm_private_ip | default('10.42.1.10') }}"
|
||||||
|
cloud_vm_admin_user: "{{ admin_username }}"
|
||||||
|
when: ansible_check_mode
|
||||||
|
|
||||||
|
- name: Prepare Azure Windows VM for initial WinRM bootstrap
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv:
|
||||||
|
- az
|
||||||
|
- vm
|
||||||
|
- run-command
|
||||||
|
- invoke
|
||||||
|
- --subscription
|
||||||
|
- "{{ azure_subscription_id }}"
|
||||||
|
- --resource-group
|
||||||
|
- "{{ azure_resource_group }}"
|
||||||
|
- --name
|
||||||
|
- "{{ azure_vm_name }}"
|
||||||
|
- --command-id
|
||||||
|
- RunPowerShellScript
|
||||||
|
- --scripts
|
||||||
|
- |
|
||||||
|
$ProgressPreference = 'SilentlyContinue'
|
||||||
|
$profiles = Get-NetConnectionProfile
|
||||||
|
foreach ($profile in $profiles) {
|
||||||
|
if ($profile.NetworkCategory -ne 'Private') {
|
||||||
|
Set-NetConnectionProfile -InterfaceIndex $profile.InterfaceIndex -NetworkCategory Private
|
||||||
|
}
|
||||||
|
}
|
||||||
|
winrm quickconfig -quiet
|
||||||
|
Enable-PSRemoting -Force
|
||||||
|
Set-Service -Name WinRM -StartupType Automatic
|
||||||
|
Start-Service -Name WinRM
|
||||||
|
Set-Item -Path WSMan:\localhost\Service\Auth\Basic -Value $true
|
||||||
|
Set-Item -Path WSMan:\localhost\Service\AllowUnencrypted -Value $true
|
||||||
|
netsh advfirewall firewall add rule name="Allow WinRM 5985" dir=in action=allow protocol=TCP localport=5985 | Out-Null
|
||||||
|
Get-Service -Name WinRM | Select-Object Status, StartType, Name
|
||||||
|
register: azure_windows_winrm_prep
|
||||||
|
changed_when: true
|
||||||
|
when:
|
||||||
|
- not ansible_check_mode
|
||||||
|
- os_family == "windows"
|
||||||
|
|
||||||
|
- name: Show Azure Windows WinRM prep output
|
||||||
|
ansible.builtin.debug:
|
||||||
|
var: azure_windows_winrm_prep.stdout
|
||||||
|
when:
|
||||||
|
- not ansible_check_mode
|
||||||
|
- os_family == "windows"
|
||||||
|
|
||||||
|
- name: Wait for Azure Windows WinRM endpoint
|
||||||
|
ansible.builtin.wait_for:
|
||||||
|
host: "{{ cloud_vm_public_ip }}"
|
||||||
|
port: 5985
|
||||||
|
delay: 10
|
||||||
|
timeout: 300
|
||||||
|
state: started
|
||||||
|
when:
|
||||||
|
- not ansible_check_mode
|
||||||
|
- os_family == "windows"
|
||||||
96
roles/azure_dev_desktop_lifecycle/tasks/destroy.yml
Normal file
96
roles/azure_dev_desktop_lifecycle/tasks/destroy.yml
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
- name: Preview Azure destroy/cleanup request
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg:
|
||||||
|
- "azure_resource_group={{ azure_resource_group | default('n/a') }}"
|
||||||
|
- "azure_vm_name={{ azure_vm_name | default(profile_name | default('n/a')) }}"
|
||||||
|
- "azure_cleanup_mode={{ azure_cleanup_mode | default(false) }}"
|
||||||
|
- "cloud_vm_destroy_mode={{ cloud_vm_destroy_mode | default('destroy') }}"
|
||||||
|
|
||||||
|
- name: Build Azure cleanup query
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
azure_cleanup_query: "[?tags.toolkit_scope=='cloud-dev-desktop' && tags.managed_by=='ansible'].[resourceGroup,name]"
|
||||||
|
when: azure_cleanup_mode | default(false)
|
||||||
|
|
||||||
|
- name: List Azure expired VMs
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv:
|
||||||
|
- az
|
||||||
|
- vm
|
||||||
|
- list
|
||||||
|
- --subscription
|
||||||
|
- "{{ azure_subscription_id }}"
|
||||||
|
- --show-details
|
||||||
|
- --query
|
||||||
|
- "{{ azure_cleanup_query }}"
|
||||||
|
- -o
|
||||||
|
- json
|
||||||
|
register: azure_expired_vm_list
|
||||||
|
changed_when: false
|
||||||
|
when:
|
||||||
|
- azure_cleanup_mode | default(false)
|
||||||
|
- not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Park Azure VM in lowest-consumption mode
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv:
|
||||||
|
- az
|
||||||
|
- vm
|
||||||
|
- deallocate
|
||||||
|
- --subscription
|
||||||
|
- "{{ azure_subscription_id }}"
|
||||||
|
- --resource-group
|
||||||
|
- "{{ azure_resource_group }}"
|
||||||
|
- --name
|
||||||
|
- "{{ azure_vm_name }}"
|
||||||
|
changed_when: true
|
||||||
|
when:
|
||||||
|
- not azure_cleanup_mode | default(false)
|
||||||
|
- (cloud_vm_destroy_mode | default('destroy')) == 'park'
|
||||||
|
- not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Delete Azure VM directly
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv:
|
||||||
|
- az
|
||||||
|
- vm
|
||||||
|
- delete
|
||||||
|
- --subscription
|
||||||
|
- "{{ azure_subscription_id }}"
|
||||||
|
- --resource-group
|
||||||
|
- "{{ azure_resource_group }}"
|
||||||
|
- --name
|
||||||
|
- "{{ azure_vm_name }}"
|
||||||
|
- --yes
|
||||||
|
changed_when: true
|
||||||
|
when:
|
||||||
|
- not azure_cleanup_mode | default(false)
|
||||||
|
- (cloud_vm_destroy_mode | default('destroy')) == 'destroy'
|
||||||
|
- not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Delete Azure expired VMs
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv:
|
||||||
|
- az
|
||||||
|
- vm
|
||||||
|
- delete
|
||||||
|
- --subscription
|
||||||
|
- "{{ azure_subscription_id }}"
|
||||||
|
- --resource-group
|
||||||
|
- "{{ item[0] }}"
|
||||||
|
- --name
|
||||||
|
- "{{ item[1] }}"
|
||||||
|
- --yes
|
||||||
|
loop: "{{ (azure_expired_vm_list.stdout | default('[]')) | from_json }}"
|
||||||
|
changed_when: true
|
||||||
|
when:
|
||||||
|
- azure_cleanup_mode | default(false)
|
||||||
|
- not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Remove Azure state file after destroy
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ cloud_vm_state_file }}"
|
||||||
|
state: absent
|
||||||
|
when:
|
||||||
|
- cloud_vm_state_file is defined
|
||||||
|
- not azure_cleanup_mode | default(false)
|
||||||
|
- (cloud_vm_destroy_mode | default('destroy')) == 'destroy'
|
||||||
64
roles/azure_dev_desktop_lifecycle/tasks/facts.yml
Normal file
64
roles/azure_dev_desktop_lifecycle/tasks/facts.yml
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
- name: Normalize Azure desktop resource names
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
azure_subscription_id: "{{ azure_subscription_id | default(lookup('env', 'AZURE_SUBSCRIPTION_ID')) }}"
|
||||||
|
azure_resource_group: "{{ azure_resource_group | default('rg-devdesktop-' ~ cloud_vm_owner_slug) }}"
|
||||||
|
azure_location: "{{ region | default(azure_location | default('japaneast')) }}"
|
||||||
|
azure_network_security_group: "{{ azure_network_security_group | default('nsg-' ~ cloud_vm_profile_slug) }}"
|
||||||
|
azure_virtual_network: "{{ azure_virtual_network | default('vnet-devdesktop-' ~ cloud_vm_owner_slug) }}"
|
||||||
|
azure_subnet: "{{ azure_subnet | default('snet-devdesktop') }}"
|
||||||
|
azure_public_ip_name: "{{ azure_public_ip_name | default('pip-' ~ cloud_vm_profile_slug) }}"
|
||||||
|
azure_nic_name: "{{ azure_nic_name | default('nic-' ~ cloud_vm_profile_slug) }}"
|
||||||
|
azure_vm_name: "{{ azure_vm_name | default('vm-' ~ cloud_vm_profile_slug) }}"
|
||||||
|
azure_computer_name: >-
|
||||||
|
{{
|
||||||
|
azure_computer_name
|
||||||
|
| default(
|
||||||
|
('vm' ~ (cloud_vm_profile_slug | regex_replace('[^A-Za-z0-9]', '')))[:15]
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
|
||||||
|
- name: Assert Azure subscription is available when requested
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- azure_subscription_id | length > 0
|
||||||
|
fail_msg: "AZURE_SUBSCRIPTION_ID or azure_subscription_id is required for Azure operations."
|
||||||
|
when: not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Select Azure image defaults by OS family
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
azure_image_publisher: >-
|
||||||
|
{{
|
||||||
|
azure_image_publisher
|
||||||
|
| default(
|
||||||
|
{
|
||||||
|
'windows': 'MicrosoftWindowsDesktop',
|
||||||
|
'fedora-gnome': 'Fedora',
|
||||||
|
'debian-kde': 'Debian'
|
||||||
|
}[os_family]
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
azure_image_offer: >-
|
||||||
|
{{
|
||||||
|
image_offer
|
||||||
|
| default(
|
||||||
|
{
|
||||||
|
'windows': 'windows-11',
|
||||||
|
'fedora-gnome': 'fedora-x86_64',
|
||||||
|
'debian-kde': 'debian-13'
|
||||||
|
}[os_family]
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
azure_image_sku: >-
|
||||||
|
{{
|
||||||
|
image_sku
|
||||||
|
| default(
|
||||||
|
{
|
||||||
|
'windows': 'win11-24h2-pro',
|
||||||
|
'fedora-gnome': '43-gen2',
|
||||||
|
'debian-kde': '13-gen2'
|
||||||
|
}[os_family]
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
azure_image_version: "{{ image_version | default('latest') }}"
|
||||||
|
azure_admin_password: "{{ azure_admin_password | default(lookup('env', 'AZURE_WINDOWS_ADMIN_PASSWORD')) }}"
|
||||||
|
cloud_vm_platform: "azure"
|
||||||
16
roles/azure_dev_desktop_lifecycle/tasks/main.yml
Normal file
16
roles/azure_dev_desktop_lifecycle/tasks/main.yml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
- name: Gather Azure lifecycle facts
|
||||||
|
ansible.builtin.include_tasks: facts.yml
|
||||||
|
|
||||||
|
- name: Run Azure create flow
|
||||||
|
ansible.builtin.include_tasks: create.yml
|
||||||
|
when: cloud_lifecycle_action == "create"
|
||||||
|
|
||||||
|
- name: Run Azure destroy flow
|
||||||
|
ansible.builtin.include_tasks: destroy.yml
|
||||||
|
when: cloud_lifecycle_action == "destroy"
|
||||||
|
|
||||||
|
- name: Run Azure cleanup flow
|
||||||
|
ansible.builtin.include_tasks: destroy.yml
|
||||||
|
vars:
|
||||||
|
azure_cleanup_mode: true
|
||||||
|
when: cloud_lifecycle_action == "cleanup"
|
||||||
10
roles/cloud_cli_prereqs/defaults/main.yml
Normal file
10
roles/cloud_cli_prereqs/defaults/main.yml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
cloud_cli_prereqs_install_azure_cli: true
|
||||||
|
cloud_cli_prereqs_install_gcloud_cli: true
|
||||||
|
cloud_cli_prereqs_gcloud_version: 527.0.0
|
||||||
|
cloud_cli_prereqs_gcloud_install_root: /opt
|
||||||
|
cloud_cli_prereqs_gcloud_archive_map:
|
||||||
|
x86_64: google-cloud-cli-linux-x86_64.tar.gz
|
||||||
|
amd64: google-cloud-cli-linux-x86_64.tar.gz
|
||||||
|
aarch64: google-cloud-cli-linux-arm.tar.gz
|
||||||
|
arm64: google-cloud-cli-linux-arm.tar.gz
|
||||||
|
|
||||||
101
roles/cloud_cli_prereqs/tasks/linux.yml
Normal file
101
roles/cloud_cli_prereqs/tasks/linux.yml
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
- name: Install Azure CLI on Debian family
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
set -euo pipefail
|
||||||
|
curl -sL https://aka.ms/InstallAzureCLIDeb | bash
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
when:
|
||||||
|
- cloud_cli_prereqs_install_azure_cli | bool
|
||||||
|
- ansible_os_family == "Debian"
|
||||||
|
- not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Install Azure CLI dependencies on RedHat family
|
||||||
|
ansible.builtin.package:
|
||||||
|
name:
|
||||||
|
- ca-certificates
|
||||||
|
- curl
|
||||||
|
- gnupg2
|
||||||
|
state: present
|
||||||
|
when:
|
||||||
|
- cloud_cli_prereqs_install_azure_cli | bool
|
||||||
|
- ansible_os_family == "RedHat"
|
||||||
|
|
||||||
|
- name: Add Azure CLI yum repository on RedHat family
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: /etc/yum.repos.d/azure-cli.repo
|
||||||
|
mode: "0644"
|
||||||
|
content: |
|
||||||
|
[azure-cli]
|
||||||
|
name=Azure CLI
|
||||||
|
baseurl=https://packages.microsoft.com/yumrepos/azure-cli
|
||||||
|
enabled=1
|
||||||
|
gpgcheck=1
|
||||||
|
gpgkey=https://packages.microsoft.com/keys/microsoft.asc
|
||||||
|
when:
|
||||||
|
- cloud_cli_prereqs_install_azure_cli | bool
|
||||||
|
- ansible_os_family == "RedHat"
|
||||||
|
|
||||||
|
- name: Install Azure CLI on RedHat family
|
||||||
|
ansible.builtin.package:
|
||||||
|
name: azure-cli
|
||||||
|
state: present
|
||||||
|
when:
|
||||||
|
- cloud_cli_prereqs_install_azure_cli | bool
|
||||||
|
- ansible_os_family == "RedHat"
|
||||||
|
|
||||||
|
- name: Select Google Cloud CLI archive for Linux
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
cloud_cli_prereqs_gcloud_archive_name: >-
|
||||||
|
{{
|
||||||
|
cloud_cli_prereqs_gcloud_archive_map[ansible_architecture]
|
||||||
|
| default('google-cloud-cli-linux-x86_64.tar.gz')
|
||||||
|
}}
|
||||||
|
when: cloud_cli_prereqs_install_gcloud_cli | bool
|
||||||
|
|
||||||
|
- name: Build Google Cloud CLI download URL for Linux
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
cloud_cli_prereqs_gcloud_url: >-
|
||||||
|
https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/{{ cloud_cli_prereqs_gcloud_archive_name }}
|
||||||
|
when: cloud_cli_prereqs_install_gcloud_cli | bool
|
||||||
|
|
||||||
|
- name: Download Google Cloud CLI archive on Linux
|
||||||
|
ansible.builtin.get_url:
|
||||||
|
url: "{{ cloud_cli_prereqs_gcloud_url }}"
|
||||||
|
dest: "/tmp/{{ cloud_cli_prereqs_gcloud_archive_name }}"
|
||||||
|
mode: "0644"
|
||||||
|
when:
|
||||||
|
- cloud_cli_prereqs_install_gcloud_cli | bool
|
||||||
|
- not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Extract Google Cloud CLI on Linux
|
||||||
|
ansible.builtin.unarchive:
|
||||||
|
src: "/tmp/{{ cloud_cli_prereqs_gcloud_archive_name }}"
|
||||||
|
dest: "{{ cloud_cli_prereqs_gcloud_install_root }}"
|
||||||
|
remote_src: true
|
||||||
|
creates: "{{ cloud_cli_prereqs_gcloud_install_root }}/google-cloud-sdk/bin/gcloud"
|
||||||
|
when:
|
||||||
|
- cloud_cli_prereqs_install_gcloud_cli | bool
|
||||||
|
- not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Ensure gcloud symlink exists on Linux
|
||||||
|
ansible.builtin.file:
|
||||||
|
src: "{{ cloud_cli_prereqs_gcloud_install_root }}/google-cloud-sdk/bin/gcloud"
|
||||||
|
dest: /usr/local/bin/gcloud
|
||||||
|
state: link
|
||||||
|
when:
|
||||||
|
- cloud_cli_prereqs_install_gcloud_cli | bool
|
||||||
|
- not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Verify Azure CLI on Linux
|
||||||
|
ansible.builtin.command: az version
|
||||||
|
changed_when: false
|
||||||
|
when:
|
||||||
|
- cloud_cli_prereqs_install_azure_cli | bool
|
||||||
|
- not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Verify Google Cloud CLI on Linux
|
||||||
|
ansible.builtin.command: gcloud version
|
||||||
|
changed_when: false
|
||||||
|
when:
|
||||||
|
- cloud_cli_prereqs_install_gcloud_cli | bool
|
||||||
|
- not ansible_check_mode
|
||||||
13
roles/cloud_cli_prereqs/tasks/macos.yml
Normal file
13
roles/cloud_cli_prereqs/tasks/macos.yml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
- name: Ensure Homebrew is installed on macOS
|
||||||
|
ansible.builtin.command: brew --version
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Install Azure CLI on macOS
|
||||||
|
ansible.builtin.command: brew install azure-cli
|
||||||
|
changed_when: true
|
||||||
|
when: cloud_cli_prereqs_install_azure_cli | bool
|
||||||
|
|
||||||
|
- name: Install Google Cloud CLI on macOS
|
||||||
|
ansible.builtin.command: brew install --cask google-cloud-sdk
|
||||||
|
changed_when: true
|
||||||
|
when: cloud_cli_prereqs_install_gcloud_cli | bool
|
||||||
14
roles/cloud_cli_prereqs/tasks/main.yml
Normal file
14
roles/cloud_cli_prereqs/tasks/main.yml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
- name: Install macOS cloud CLIs
|
||||||
|
ansible.builtin.include_tasks: macos.yml
|
||||||
|
when: ansible_system == "Darwin"
|
||||||
|
|
||||||
|
- name: Install Windows cloud CLIs
|
||||||
|
ansible.builtin.include_tasks: windows.yml
|
||||||
|
when: ansible_os_family == "Windows"
|
||||||
|
|
||||||
|
- name: Install Linux cloud CLIs
|
||||||
|
ansible.builtin.include_tasks: linux.yml
|
||||||
|
when:
|
||||||
|
- ansible_system == "Linux"
|
||||||
|
- ansible_os_family != "Windows"
|
||||||
|
|
||||||
84
roles/cloud_cli_prereqs/tasks/windows.yml
Normal file
84
roles/cloud_cli_prereqs/tasks/windows.yml
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
- name: Detect winget on Windows
|
||||||
|
ansible.builtin.raw: |
|
||||||
|
$ErrorActionPreference = "SilentlyContinue"
|
||||||
|
if (Get-Command winget -ErrorAction SilentlyContinue) {
|
||||||
|
Write-Output "present"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
Write-Output "missing"
|
||||||
|
exit 0
|
||||||
|
changed_when: false
|
||||||
|
register: cloud_cli_prereqs_winget_check
|
||||||
|
|
||||||
|
- name: Detect Chocolatey on Windows
|
||||||
|
ansible.builtin.raw: |
|
||||||
|
$ErrorActionPreference = "SilentlyContinue"
|
||||||
|
if (Get-Command choco -ErrorAction SilentlyContinue) {
|
||||||
|
Write-Output "present"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
Write-Output "missing"
|
||||||
|
exit 0
|
||||||
|
changed_when: false
|
||||||
|
register: cloud_cli_prereqs_choco_check
|
||||||
|
|
||||||
|
- name: Install Chocolatey fallback on Windows
|
||||||
|
ansible.builtin.raw: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
Set-ExecutionPolicy Bypass -Scope Process -Force
|
||||||
|
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
|
||||||
|
Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
|
||||||
|
changed_when: true
|
||||||
|
when:
|
||||||
|
- "'present' not in cloud_cli_prereqs_winget_check.stdout"
|
||||||
|
- "'present' not in cloud_cli_prereqs_choco_check.stdout"
|
||||||
|
|
||||||
|
- name: Install Azure CLI on Windows with winget
|
||||||
|
ansible.builtin.raw: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
winget install --id Microsoft.AzureCLI --exact --accept-package-agreements --accept-source-agreements --silent
|
||||||
|
changed_when: true
|
||||||
|
when:
|
||||||
|
- cloud_cli_prereqs_install_azure_cli | bool
|
||||||
|
- "'present' in cloud_cli_prereqs_winget_check.stdout"
|
||||||
|
|
||||||
|
- name: Install Azure CLI on Windows with Chocolatey
|
||||||
|
ansible.builtin.raw: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
choco install azure-cli -y --no-progress
|
||||||
|
changed_when: true
|
||||||
|
when:
|
||||||
|
- cloud_cli_prereqs_install_azure_cli | bool
|
||||||
|
- "'present' not in cloud_cli_prereqs_winget_check.stdout"
|
||||||
|
|
||||||
|
- name: Install Google Cloud CLI on Windows with winget
|
||||||
|
ansible.builtin.raw: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
winget install --id Google.CloudSDK --exact --accept-package-agreements --accept-source-agreements --silent
|
||||||
|
changed_when: true
|
||||||
|
when:
|
||||||
|
- cloud_cli_prereqs_install_gcloud_cli | bool
|
||||||
|
- "'present' in cloud_cli_prereqs_winget_check.stdout"
|
||||||
|
|
||||||
|
- name: Install Google Cloud CLI on Windows with Chocolatey
|
||||||
|
ansible.builtin.raw: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
choco install gcloudsdk -y --no-progress
|
||||||
|
changed_when: true
|
||||||
|
when:
|
||||||
|
- cloud_cli_prereqs_install_gcloud_cli | bool
|
||||||
|
- "'present' not in cloud_cli_prereqs_winget_check.stdout"
|
||||||
|
|
||||||
|
- name: Verify Azure CLI on Windows
|
||||||
|
ansible.builtin.raw: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
Get-Command az | Out-Null
|
||||||
|
changed_when: false
|
||||||
|
when: cloud_cli_prereqs_install_azure_cli | bool
|
||||||
|
|
||||||
|
- name: Verify Google Cloud CLI on Windows
|
||||||
|
ansible.builtin.raw: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
Get-Command gcloud | Out-Null
|
||||||
|
changed_when: false
|
||||||
|
when: cloud_cli_prereqs_install_gcloud_cli | bool
|
||||||
35
roles/cloud_vm_inventory_emit/tasks/main.yml
Normal file
35
roles/cloud_vm_inventory_emit/tasks/main.yml
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
- name: Ensure cloud desktop state directory exists
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ cloud_vm_state_root }}"
|
||||||
|
state: directory
|
||||||
|
mode: "0700"
|
||||||
|
when: cloud_vm_state_root is defined
|
||||||
|
|
||||||
|
- name: Persist cloud desktop state file
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: "{{ cloud_vm_state_file }}"
|
||||||
|
mode: "0600"
|
||||||
|
content: |
|
||||||
|
{
|
||||||
|
"provider": {{ provider | to_json }},
|
||||||
|
"profile_name": {{ profile_name | to_json }},
|
||||||
|
"os_family": {{ os_family | to_json }},
|
||||||
|
"admin_username": {{ cloud_vm_admin_user | default(admin_username) | to_json }},
|
||||||
|
"public_ip": {{ cloud_vm_public_ip | default('') | to_json }},
|
||||||
|
"private_ip": {{ cloud_vm_private_ip | default('') | to_json }},
|
||||||
|
"platform": {{ cloud_vm_platform | default(os_family) | to_json }},
|
||||||
|
"desktop_access": {{ desktop_access | default({}) | to_json }},
|
||||||
|
"tags": {{ tags | default({}) | to_json }},
|
||||||
|
"created_at": {{ cloud_vm_created_at | default('') | to_json }},
|
||||||
|
"expires_at": {{ cloud_vm_expires_at | default('') | to_json }}
|
||||||
|
}
|
||||||
|
when:
|
||||||
|
- cloud_vm_state_file is defined
|
||||||
|
- cloud_vm_public_ip is defined
|
||||||
|
- not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Show resulting cloud desktop state path
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg:
|
||||||
|
- "cloud_vm_state_file={{ cloud_vm_state_file | default('unset') }}"
|
||||||
|
- "cloud_vm_public_ip={{ cloud_vm_public_ip | default('pending') }}"
|
||||||
119
roles/cloud_vm_request_validate/tasks/main.yml
Normal file
119
roles/cloud_vm_request_validate/tasks/main.yml
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
- name: Ensure request validation mode is set
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
cloud_vm_request_validation_mode: "{{ cloud_vm_request_validation_mode | default('standard') }}"
|
||||||
|
|
||||||
|
- name: Capture provider defaults
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
cloud_dev_desktop_required_common_keys:
|
||||||
|
- provider
|
||||||
|
- profile_name
|
||||||
|
- os_family
|
||||||
|
- admin_username
|
||||||
|
- allowed_cidrs
|
||||||
|
- ttl_hours
|
||||||
|
- owner
|
||||||
|
- purpose
|
||||||
|
|
||||||
|
- name: Assert provider is supported
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- provider is defined
|
||||||
|
- provider in ['azure', 'gcp']
|
||||||
|
fail_msg: "provider must be one of: azure, gcp"
|
||||||
|
|
||||||
|
- name: Assert os_family is supported
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- os_family is defined
|
||||||
|
- os_family in ['windows', 'fedora-gnome', 'debian-kde']
|
||||||
|
fail_msg: "os_family must be one of: windows, fedora-gnome, debian-kde"
|
||||||
|
when: cloud_vm_request_validation_mode != "cleanup"
|
||||||
|
|
||||||
|
- name: Assert required common fields are present
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that: "{{ cloud_dev_desktop_required_common_keys | map('extract', vars) | list is not none }}"
|
||||||
|
fail_msg: "cloud dev desktop request is missing one or more required keys."
|
||||||
|
when: cloud_vm_request_validation_mode != "cleanup"
|
||||||
|
|
||||||
|
- name: Assert allowed CIDRs were supplied
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- allowed_cidrs is sequence
|
||||||
|
- allowed_cidrs | length > 0
|
||||||
|
fail_msg: "allowed_cidrs must be a non-empty list."
|
||||||
|
when: cloud_vm_request_validation_mode != "cleanup"
|
||||||
|
|
||||||
|
- name: Assert provider-specific location fields exist for standard mode
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- "(provider == 'azure' and region is defined) or (provider == 'gcp' and zone is defined)"
|
||||||
|
fail_msg: "azure requests need region; gcp requests need zone."
|
||||||
|
when: cloud_vm_request_validation_mode != "cleanup"
|
||||||
|
|
||||||
|
- name: Normalize toolchain defaults
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
toolchains: "{{ {'codex': true, 'android_studio': false, 'vscode': true, 'flutter': false, 'dart': false} | combine(toolchains | default({}), recursive=True) }}"
|
||||||
|
|
||||||
|
- name: Normalize SSH public key default
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
ssh_public_key_path: "{{ ssh_public_key_path | default('~/.ssh/id_rsa.pub') }}"
|
||||||
|
when:
|
||||||
|
- cloud_vm_request_validation_mode != "cleanup"
|
||||||
|
- os_family != "windows"
|
||||||
|
|
||||||
|
- name: Normalize allowed TCP ports
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
allowed_tcp_ports: >-
|
||||||
|
{{
|
||||||
|
allowed_tcp_ports
|
||||||
|
| default(
|
||||||
|
(os_family == 'windows')
|
||||||
|
| ternary([22, 3389, 5985], [22, 3389])
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
when: cloud_vm_request_validation_mode != "cleanup"
|
||||||
|
|
||||||
|
- name: Normalize desktop access defaults
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
desktop_access: "{{ {'protocol': (os_family == 'windows') | ternary('rdp', 'native'), 'port': (os_family == 'windows') | ternary(3389, 22)} | combine(desktop_access | default({}), recursive=True) }}"
|
||||||
|
when: cloud_vm_request_validation_mode != "cleanup"
|
||||||
|
|
||||||
|
- name: Derive cloud desktop timestamps and names
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
cloud_vm_profile_slug: "{{ (profile_name | default('cleanup')) | lower | regex_replace('[^a-z0-9]+', '-') | regex_replace('(^-|-$)', '') }}"
|
||||||
|
cloud_vm_owner_slug: "{{ (owner | default('cleanup')) | lower | regex_replace('[^a-z0-9]+', '-') | regex_replace('(^-|-$)', '') }}"
|
||||||
|
cloud_vm_state_root: "{{ cloud_vm_state_root | default(playbook_dir ~ '/../.cloud-dev-desktop-state') }}"
|
||||||
|
cloud_vm_created_at: "{{ ansible_date_time.iso8601 }}"
|
||||||
|
cloud_vm_expires_at: "{{ lookup('pipe', 'python3 -c \"from datetime import datetime, timedelta, timezone; print((datetime.now(timezone.utc)+timedelta(hours=' ~ (ttl_hours | int) ~ ')).isoformat())\"') }}"
|
||||||
|
when:
|
||||||
|
- ttl_hours is defined
|
||||||
|
- cloud_vm_request_validation_mode != "cleanup"
|
||||||
|
|
||||||
|
- name: Derive cloud desktop cleanup names
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
cloud_vm_profile_slug: "{{ (profile_name | default('cleanup')) | lower | regex_replace('[^a-z0-9]+', '-') | regex_replace('(^-|-$)', '') }}"
|
||||||
|
cloud_vm_owner_slug: "{{ (owner | default('cleanup')) | lower | regex_replace('[^a-z0-9]+', '-') | regex_replace('(^-|-$)', '') }}"
|
||||||
|
when: cloud_vm_request_validation_mode == "cleanup"
|
||||||
|
|
||||||
|
- name: Derive cloud desktop state file path
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
cloud_vm_state_file: "{{ cloud_vm_state_file | default(cloud_vm_state_root ~ '/' ~ provider ~ '-' ~ cloud_vm_profile_slug ~ '.json') }}"
|
||||||
|
when: cloud_vm_request_validation_mode != "cleanup"
|
||||||
|
|
||||||
|
- name: Build default tags and labels
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
cloud_vm_default_tags:
|
||||||
|
managed_by: ansible
|
||||||
|
toolkit_scope: cloud-dev-desktop
|
||||||
|
provider: "{{ provider }}"
|
||||||
|
profile_name: "{{ profile_name }}"
|
||||||
|
owner: "{{ owner }}"
|
||||||
|
purpose: "{{ purpose }}"
|
||||||
|
os_family: "{{ os_family }}"
|
||||||
|
expires_at: "{{ cloud_vm_expires_at | default('') }}"
|
||||||
|
when: cloud_vm_request_validation_mode != "cleanup"
|
||||||
|
|
||||||
|
- name: Normalize tags and labels
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
tags: "{{ cloud_vm_default_tags | combine(tags | default({}), recursive=True) }}"
|
||||||
|
when: cloud_vm_request_validation_mode != "cleanup"
|
||||||
11
roles/dev_desktop_common/defaults/main.yml
Normal file
11
roles/dev_desktop_common/defaults/main.yml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
cloud_dev_desktop_workspace_root: /opt/cloud-dev-desktop
|
||||||
|
cloud_dev_desktop_timezone: Etc/UTC
|
||||||
|
cloud_dev_desktop_locale: en_US.UTF-8
|
||||||
|
cloud_dev_desktop_user_shell: /bin/bash
|
||||||
|
cloud_dev_desktop_extra_authorized_keys: []
|
||||||
|
cloud_dev_desktop_toolchain_profile_path: /etc/profile.d/cloud-dev-desktop-toolchain.sh
|
||||||
|
cloud_dev_desktop_node_major: "22"
|
||||||
|
cloud_dev_desktop_go_release_index_url: https://go.dev/dl/?mode=json
|
||||||
|
cloud_dev_desktop_flutter_install_root: /opt/flutter
|
||||||
|
cloud_dev_desktop_flutter_version: "3.41.5"
|
||||||
|
cloud_dev_desktop_flutter_linux_sdk_url: "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_{{ cloud_dev_desktop_flutter_version }}-stable.tar.xz"
|
||||||
14
roles/dev_desktop_common/tasks/main.yml
Normal file
14
roles/dev_desktop_common/tasks/main.yml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
- name: Expand Linux root filesystem when the cloud image did not consume the full boot disk
|
||||||
|
ansible.builtin.import_tasks: storage.yml
|
||||||
|
|
||||||
|
- name: Prepare common Linux users
|
||||||
|
ansible.builtin.import_tasks: users.yml
|
||||||
|
|
||||||
|
- name: Install common Linux developer toolchains
|
||||||
|
ansible.builtin.import_tasks: toolchains.yml
|
||||||
|
|
||||||
|
- name: Prepare common Linux network policy
|
||||||
|
ansible.builtin.import_tasks: network.yml
|
||||||
|
|
||||||
|
- name: Persist TTL and profile metadata
|
||||||
|
ansible.builtin.import_tasks: ttl.yml
|
||||||
9
roles/dev_desktop_common/tasks/network.yml
Normal file
9
roles/dev_desktop_common/tasks/network.yml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
- name: Record allowlisted desktop access policy
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: "{{ cloud_dev_desktop_workspace_root }}/allowed-cidrs.txt"
|
||||||
|
mode: "0644"
|
||||||
|
content: |
|
||||||
|
{% for cidr in allowed_cidrs %}
|
||||||
|
{{ cidr }}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
87
roles/dev_desktop_common/tasks/storage.yml
Normal file
87
roles/dev_desktop_common/tasks/storage.yml
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
- name: Install Linux filesystem growth helpers on Debian family
|
||||||
|
ansible.builtin.package:
|
||||||
|
name:
|
||||||
|
- cloud-guest-utils
|
||||||
|
- gdisk
|
||||||
|
- btrfs-progs
|
||||||
|
- xfsprogs
|
||||||
|
- e2fsprogs
|
||||||
|
state: present
|
||||||
|
when:
|
||||||
|
- os_family != "windows"
|
||||||
|
- ansible_os_family == "Debian"
|
||||||
|
- not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Install Linux filesystem growth helpers on RedHat family
|
||||||
|
ansible.builtin.package:
|
||||||
|
name:
|
||||||
|
- cloud-utils-growpart
|
||||||
|
- gdisk
|
||||||
|
- btrfs-progs
|
||||||
|
- xfsprogs
|
||||||
|
- e2fsprogs
|
||||||
|
state: present
|
||||||
|
when:
|
||||||
|
- os_family != "windows"
|
||||||
|
- ansible_os_family == "RedHat"
|
||||||
|
- not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Expand Linux root partition and filesystem when the image layout is undersized
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
root_source="$(findmnt -no SOURCE /)"
|
||||||
|
root_block_device="${root_source%%[*}"
|
||||||
|
root_fstype="$(findmnt -no FSTYPE /)"
|
||||||
|
|
||||||
|
if [[ -z "${root_block_device}" || "${root_block_device}" != /dev/* ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
root_pkname="$(lsblk -no PKNAME "${root_block_device}")"
|
||||||
|
root_partnum="$(basename "${root_block_device}" | sed -E 's/^.*[^0-9]([0-9]+)$/\1/')"
|
||||||
|
|
||||||
|
if [[ -z "${root_pkname}" || -z "${root_partnum}" ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
disk_device="/dev/${root_pkname}"
|
||||||
|
root_device="/dev/$(basename "${root_block_device}")"
|
||||||
|
|
||||||
|
disk_size_bytes="$(blockdev --getsize64 "${disk_device}")"
|
||||||
|
part_size_bytes="$(blockdev --getsize64 "${root_device}")"
|
||||||
|
slack_bytes="$((disk_size_bytes - part_size_bytes))"
|
||||||
|
|
||||||
|
# Skip resize when the partition already occupies nearly the full boot disk.
|
||||||
|
if (( slack_bytes < 536870912 )); then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
sgdisk -e "${disk_device}" >/dev/null 2>&1 || true
|
||||||
|
growpart_output="$(growpart "${disk_device}" "${root_partnum}" 2>&1 || true)"
|
||||||
|
if [[ -n "${growpart_output}" ]]; then
|
||||||
|
echo "${growpart_output}"
|
||||||
|
fi
|
||||||
|
if grep -q 'NOCHANGE:' <<<"${growpart_output}"; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "${root_fstype}" in
|
||||||
|
btrfs)
|
||||||
|
btrfs filesystem resize max /
|
||||||
|
;;
|
||||||
|
ext2|ext3|ext4)
|
||||||
|
resize2fs "${root_device}"
|
||||||
|
;;
|
||||||
|
xfs)
|
||||||
|
xfs_growfs /
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
when:
|
||||||
|
- os_family != "windows"
|
||||||
|
- not ansible_check_mode
|
||||||
124
roles/dev_desktop_common/tasks/toolchains.yml
Normal file
124
roles/dev_desktop_common/tasks/toolchains.yml
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
- name: Detect current Linux Node.js version
|
||||||
|
ansible.builtin.command: node --version
|
||||||
|
register: cloud_dev_desktop_node_version
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Determine whether Linux Node.js 22+ installation is required
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
cloud_dev_desktop_node_major_installed: "{{ (cloud_dev_desktop_node_version.stdout | default('v0.0.0') | regex_replace('^v([0-9]+).*$','\\1')) | int }}"
|
||||||
|
cloud_dev_desktop_needs_node_install: "{{ ((cloud_dev_desktop_node_version.stdout | default('v0.0.0') | regex_replace('^v([0-9]+).*$','\\1')) | int) < (cloud_dev_desktop_node_major | int) }}"
|
||||||
|
|
||||||
|
- name: Install Node.js 22.x from NodeSource on Debian family
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
set -euo pipefail
|
||||||
|
curl -fsSL "https://deb.nodesource.com/setup_{{ cloud_dev_desktop_node_major }}.x" | bash -
|
||||||
|
apt-get install -y nodejs
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
when:
|
||||||
|
- ansible_os_family == "Debian"
|
||||||
|
- cloud_dev_desktop_needs_node_install
|
||||||
|
- not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Install Node.js 22.x from NodeSource on Fedora family
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
set -euo pipefail
|
||||||
|
curl -fsSL "https://rpm.nodesource.com/setup_{{ cloud_dev_desktop_node_major }}.x" | bash -
|
||||||
|
dnf install -y nodejs
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
when:
|
||||||
|
- ansible_os_family == "RedHat"
|
||||||
|
- cloud_dev_desktop_needs_node_install
|
||||||
|
- not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Detect current Linux Go toolchain
|
||||||
|
ansible.builtin.command: /usr/local/go/bin/go version
|
||||||
|
register: cloud_dev_desktop_go_version
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Fetch latest stable Go release index
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ cloud_dev_desktop_go_release_index_url }}"
|
||||||
|
return_content: true
|
||||||
|
register: cloud_dev_desktop_go_release_index
|
||||||
|
when: not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Select latest stable Go archive for linux amd64
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
cloud_dev_desktop_go_release: "{{ (cloud_dev_desktop_go_release_index.json | selectattr('stable', 'equalto', true) | list | first) }}"
|
||||||
|
cloud_dev_desktop_go_archive: >-
|
||||||
|
{{
|
||||||
|
(
|
||||||
|
(cloud_dev_desktop_go_release_index.json | selectattr('stable', 'equalto', true) | list | first).files
|
||||||
|
| selectattr('os', 'equalto', 'linux')
|
||||||
|
| selectattr('arch', 'equalto', 'amd64')
|
||||||
|
| selectattr('kind', 'equalto', 'archive')
|
||||||
|
| list
|
||||||
|
| first
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
when: not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Determine whether Linux Go installation is required
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
cloud_dev_desktop_go_needs_install: "{{ (cloud_dev_desktop_go_release.version | default('')) not in (cloud_dev_desktop_go_version.stdout | default('')) }}"
|
||||||
|
when: not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Remove outdated Go toolchain
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: /usr/local/go
|
||||||
|
state: absent
|
||||||
|
when:
|
||||||
|
- not ansible_check_mode
|
||||||
|
- cloud_dev_desktop_go_needs_install | default(false)
|
||||||
|
|
||||||
|
- name: Install latest stable Go toolchain
|
||||||
|
ansible.builtin.unarchive:
|
||||||
|
src: "https://go.dev/dl/{{ cloud_dev_desktop_go_archive.filename }}"
|
||||||
|
dest: /usr/local
|
||||||
|
remote_src: true
|
||||||
|
when:
|
||||||
|
- not ansible_check_mode
|
||||||
|
- cloud_dev_desktop_go_needs_install | default(false)
|
||||||
|
|
||||||
|
- name: Install Codex CLI globally on Linux
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: npm install -g @openai/codex
|
||||||
|
when:
|
||||||
|
- toolchains.codex | bool
|
||||||
|
- not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Persist common Linux toolchain PATH
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: "{{ cloud_dev_desktop_toolchain_profile_path }}"
|
||||||
|
mode: "0644"
|
||||||
|
content: |
|
||||||
|
export PATH="/opt/flutter/bin:/usr/local/go/bin:$PATH"
|
||||||
|
|
||||||
|
- name: Verify Linux Node.js version
|
||||||
|
ansible.builtin.command: node --version
|
||||||
|
register: cloud_dev_desktop_node_verify
|
||||||
|
changed_when: false
|
||||||
|
when: not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Assert Linux Node.js major version is 22 or newer
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- (cloud_dev_desktop_node_verify.stdout | regex_replace('^v([0-9]+).*$','\\1') | int) >= (cloud_dev_desktop_node_major | int)
|
||||||
|
fail_msg: "Node.js 22+ is required on Linux cloud desktops."
|
||||||
|
when: not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Verify Linux Go toolchain
|
||||||
|
ansible.builtin.command: /usr/local/go/bin/go version
|
||||||
|
changed_when: false
|
||||||
|
when: not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Verify Linux Codex CLI
|
||||||
|
ansible.builtin.command: codex --version
|
||||||
|
changed_when: false
|
||||||
|
when:
|
||||||
|
- toolchains.codex | bool
|
||||||
|
- not ansible_check_mode
|
||||||
13
roles/dev_desktop_common/tasks/ttl.yml
Normal file
13
roles/dev_desktop_common/tasks/ttl.yml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
- name: Persist cloud desktop profile metadata
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: "{{ cloud_dev_desktop_workspace_root }}/profile.env"
|
||||||
|
mode: "0644"
|
||||||
|
content: |
|
||||||
|
PROFILE_NAME={{ profile_name }}
|
||||||
|
PROVIDER={{ provider }}
|
||||||
|
OS_FAMILY={{ os_family }}
|
||||||
|
OWNER={{ owner }}
|
||||||
|
PURPOSE={{ purpose }}
|
||||||
|
CREATED_AT={{ vars.get('cloud_vm_created_at', '') }}
|
||||||
|
EXPIRES_AT={{ vars.get('cloud_vm_expires_at', '') }}
|
||||||
|
TTL_HOURS={{ ttl_hours }}
|
||||||
40
roles/dev_desktop_common/tasks/users.yml
Normal file
40
roles/dev_desktop_common/tasks/users.yml
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
- name: Ensure common Linux packages are installed
|
||||||
|
ansible.builtin.package:
|
||||||
|
name:
|
||||||
|
- git
|
||||||
|
- curl
|
||||||
|
- wget
|
||||||
|
- unzip
|
||||||
|
- tar
|
||||||
|
- jq
|
||||||
|
- ca-certificates
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Ensure admin user exists on Linux
|
||||||
|
ansible.builtin.user:
|
||||||
|
name: "{{ admin_username }}"
|
||||||
|
shell: "{{ cloud_dev_desktop_user_shell }}"
|
||||||
|
create_home: true
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Ensure admin SSH directory exists on Linux
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "/home/{{ admin_username }}/.ssh"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ admin_username }}"
|
||||||
|
group: "{{ admin_username }}"
|
||||||
|
mode: "0700"
|
||||||
|
|
||||||
|
- name: Authorize additional SSH public keys for admin user
|
||||||
|
ansible.posix.authorized_key:
|
||||||
|
user: "{{ admin_username }}"
|
||||||
|
key: "{{ item }}"
|
||||||
|
state: present
|
||||||
|
loop: "{{ cloud_dev_desktop_extra_authorized_keys | default([]) }}"
|
||||||
|
when: (cloud_dev_desktop_extra_authorized_keys | default([])) | length > 0
|
||||||
|
|
||||||
|
- name: Ensure workspace root exists
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ cloud_dev_desktop_workspace_root }}"
|
||||||
|
state: directory
|
||||||
|
mode: "0755"
|
||||||
17
roles/dev_desktop_debian_kde/tasks/desktop.yml
Normal file
17
roles/dev_desktop_debian_kde/tasks/desktop.yml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
- name: Install KDE desktop packages on Debian family
|
||||||
|
ansible.builtin.apt:
|
||||||
|
name:
|
||||||
|
- plasma-desktop
|
||||||
|
- sddm
|
||||||
|
- xrdp
|
||||||
|
- dbus-x11
|
||||||
|
state: present
|
||||||
|
update_cache: true
|
||||||
|
|
||||||
|
- name: Ensure SDDM is enabled
|
||||||
|
ansible.builtin.service:
|
||||||
|
name: sddm
|
||||||
|
state: started
|
||||||
|
enabled: true
|
||||||
|
when: not ansible_check_mode
|
||||||
|
|
||||||
90
roles/dev_desktop_debian_kde/tasks/flutter_qt.yml
Normal file
90
roles/dev_desktop_debian_kde/tasks/flutter_qt.yml
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
- name: Install Debian/Ubuntu Flutter Qt dependencies with Qt 6
|
||||||
|
block:
|
||||||
|
- name: Install Debian/Ubuntu Flutter Qt 6 dependencies
|
||||||
|
ansible.builtin.apt:
|
||||||
|
name:
|
||||||
|
- build-essential
|
||||||
|
- clang
|
||||||
|
- lld
|
||||||
|
- cmake
|
||||||
|
- ninja-build
|
||||||
|
- pkg-config
|
||||||
|
- libgtk-3-dev
|
||||||
|
- libsecret-1-dev
|
||||||
|
- xvfb
|
||||||
|
- qt6-base-dev
|
||||||
|
- qt6-base-dev-tools
|
||||||
|
- qt6-tools-dev-tools
|
||||||
|
- libgl1-mesa-dev
|
||||||
|
- unzip
|
||||||
|
state: present
|
||||||
|
update_cache: true
|
||||||
|
rescue:
|
||||||
|
- name: Install Debian/Ubuntu Flutter Qt 5 fallback dependencies
|
||||||
|
ansible.builtin.apt:
|
||||||
|
name:
|
||||||
|
- build-essential
|
||||||
|
- clang
|
||||||
|
- lld
|
||||||
|
- cmake
|
||||||
|
- ninja-build
|
||||||
|
- pkg-config
|
||||||
|
- libgtk-3-dev
|
||||||
|
- libsecret-1-dev
|
||||||
|
- xvfb
|
||||||
|
- qtbase5-dev
|
||||||
|
- qtchooser
|
||||||
|
- qt5-qmake
|
||||||
|
- libgl1-mesa-dev
|
||||||
|
- unzip
|
||||||
|
state: present
|
||||||
|
update_cache: true
|
||||||
|
|
||||||
|
- name: Probe installed Flutter SDK on Debian family
|
||||||
|
ansible.builtin.command: "{{ cloud_dev_desktop_flutter_install_root }}/bin/flutter --version"
|
||||||
|
register: debian_flutter_version_probe
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Remove stale Flutter SDK on Debian family
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ cloud_dev_desktop_flutter_install_root }}"
|
||||||
|
state: absent
|
||||||
|
when:
|
||||||
|
- not ansible_check_mode
|
||||||
|
- debian_flutter_version_probe.rc == 0
|
||||||
|
- ("Flutter " ~ cloud_dev_desktop_flutter_version ~ " ") not in debian_flutter_version_probe.stdout
|
||||||
|
|
||||||
|
- name: Install Flutter SDK on Debian family
|
||||||
|
ansible.builtin.unarchive:
|
||||||
|
src: "{{ flutter_sdk_url | default(cloud_dev_desktop_flutter_linux_sdk_url) }}"
|
||||||
|
dest: /opt
|
||||||
|
remote_src: true
|
||||||
|
creates: "{{ cloud_dev_desktop_flutter_install_root }}/bin/flutter"
|
||||||
|
|
||||||
|
- name: Ensure Debian/Ubuntu Flutter SDK is writable by the desktop user
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ cloud_dev_desktop_flutter_install_root }}"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ admin_username }}"
|
||||||
|
group: "{{ admin_username }}"
|
||||||
|
recurse: true
|
||||||
|
when: not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Enable Flutter Linux desktop on Debian family
|
||||||
|
ansible.builtin.command: "{{ cloud_dev_desktop_flutter_install_root }}/bin/flutter config --enable-linux-desktop"
|
||||||
|
become_user: "{{ admin_username }}"
|
||||||
|
environment:
|
||||||
|
HOME: "/home/{{ admin_username }}"
|
||||||
|
PUB_CACHE: "/home/{{ admin_username }}/.pub-cache"
|
||||||
|
changed_when: false
|
||||||
|
when: not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Verify Flutter doctor on Debian family
|
||||||
|
ansible.builtin.command: "{{ cloud_dev_desktop_flutter_install_root }}/bin/flutter doctor -v"
|
||||||
|
become_user: "{{ admin_username }}"
|
||||||
|
environment:
|
||||||
|
HOME: "/home/{{ admin_username }}"
|
||||||
|
PUB_CACHE: "/home/{{ admin_username }}/.pub-cache"
|
||||||
|
changed_when: false
|
||||||
|
when: not ansible_check_mode
|
||||||
12
roles/dev_desktop_debian_kde/tasks/main.yml
Normal file
12
roles/dev_desktop_debian_kde/tasks/main.yml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
- name: Install Azure CLI and Google Cloud CLI on Debian/Ubuntu
|
||||||
|
ansible.builtin.import_role:
|
||||||
|
name: cloud_cli_prereqs
|
||||||
|
|
||||||
|
- name: Install Debian/Ubuntu KDE desktop
|
||||||
|
ansible.builtin.import_tasks: desktop.yml
|
||||||
|
|
||||||
|
- name: Install Debian/Ubuntu Flutter Qt toolchain
|
||||||
|
ansible.builtin.import_tasks: flutter_qt.yml
|
||||||
|
|
||||||
|
- name: Configure Debian/Ubuntu native remote desktop
|
||||||
|
ansible.builtin.import_tasks: remote_access.yml
|
||||||
7
roles/dev_desktop_debian_kde/tasks/remote_access.yml
Normal file
7
roles/dev_desktop_debian_kde/tasks/remote_access.yml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
- name: Ensure xrdp service is enabled for KDE hosts
|
||||||
|
ansible.builtin.service:
|
||||||
|
name: xrdp
|
||||||
|
state: started
|
||||||
|
enabled: true
|
||||||
|
when: not ansible_check_mode
|
||||||
|
|
||||||
15
roles/dev_desktop_fedora_gnome/tasks/desktop.yml
Normal file
15
roles/dev_desktop_fedora_gnome/tasks/desktop.yml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
- name: Install Fedora GNOME desktop packages
|
||||||
|
ansible.builtin.dnf:
|
||||||
|
name:
|
||||||
|
- "@workstation-product-environment"
|
||||||
|
- gnome-remote-desktop
|
||||||
|
- gdm
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Ensure GDM is enabled
|
||||||
|
ansible.builtin.service:
|
||||||
|
name: gdm
|
||||||
|
state: started
|
||||||
|
enabled: true
|
||||||
|
when: not ansible_check_mode
|
||||||
|
|
||||||
69
roles/dev_desktop_fedora_gnome/tasks/flutter_gtk.yml
Normal file
69
roles/dev_desktop_fedora_gnome/tasks/flutter_gtk.yml
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
- name: Install Fedora Flutter GTK dependencies
|
||||||
|
ansible.builtin.dnf:
|
||||||
|
name:
|
||||||
|
- gcc-c++
|
||||||
|
- clang
|
||||||
|
- cmake
|
||||||
|
- ninja-build
|
||||||
|
- pkgconf-pkg-config
|
||||||
|
- gtk3-devel
|
||||||
|
- gtk4-devel
|
||||||
|
- glib2-devel
|
||||||
|
- gobject-introspection-devel
|
||||||
|
- libsecret-devel
|
||||||
|
- libblkid-devel
|
||||||
|
- xorg-x11-server-Xvfb
|
||||||
|
- xz-devel
|
||||||
|
- make
|
||||||
|
- patch
|
||||||
|
- unzip
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Probe installed Flutter SDK on Fedora
|
||||||
|
ansible.builtin.command: "{{ cloud_dev_desktop_flutter_install_root }}/bin/flutter --version"
|
||||||
|
register: fedora_flutter_version_probe
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Remove stale Flutter SDK on Fedora
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ cloud_dev_desktop_flutter_install_root }}"
|
||||||
|
state: absent
|
||||||
|
when:
|
||||||
|
- not ansible_check_mode
|
||||||
|
- fedora_flutter_version_probe.rc == 0
|
||||||
|
- ("Flutter " ~ cloud_dev_desktop_flutter_version ~ " ") not in fedora_flutter_version_probe.stdout
|
||||||
|
|
||||||
|
- name: Install Flutter SDK on Fedora
|
||||||
|
ansible.builtin.unarchive:
|
||||||
|
src: "{{ flutter_sdk_url | default(cloud_dev_desktop_flutter_linux_sdk_url) }}"
|
||||||
|
dest: /opt
|
||||||
|
remote_src: true
|
||||||
|
creates: "{{ cloud_dev_desktop_flutter_install_root }}/bin/flutter"
|
||||||
|
|
||||||
|
- name: Ensure Fedora Flutter SDK is writable by the desktop user
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ cloud_dev_desktop_flutter_install_root }}"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ admin_username }}"
|
||||||
|
group: "{{ admin_username }}"
|
||||||
|
recurse: true
|
||||||
|
when: not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Enable Flutter Linux desktop on Fedora
|
||||||
|
ansible.builtin.command: "{{ cloud_dev_desktop_flutter_install_root }}/bin/flutter config --enable-linux-desktop"
|
||||||
|
become_user: "{{ admin_username }}"
|
||||||
|
environment:
|
||||||
|
HOME: "/home/{{ admin_username }}"
|
||||||
|
PUB_CACHE: "/home/{{ admin_username }}/.pub-cache"
|
||||||
|
changed_when: false
|
||||||
|
when: not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Verify Flutter doctor on Fedora
|
||||||
|
ansible.builtin.command: "{{ cloud_dev_desktop_flutter_install_root }}/bin/flutter doctor -v"
|
||||||
|
become_user: "{{ admin_username }}"
|
||||||
|
environment:
|
||||||
|
HOME: "/home/{{ admin_username }}"
|
||||||
|
PUB_CACHE: "/home/{{ admin_username }}/.pub-cache"
|
||||||
|
changed_when: false
|
||||||
|
when: not ansible_check_mode
|
||||||
12
roles/dev_desktop_fedora_gnome/tasks/main.yml
Normal file
12
roles/dev_desktop_fedora_gnome/tasks/main.yml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
- name: Install Azure CLI and Google Cloud CLI on Fedora
|
||||||
|
ansible.builtin.import_role:
|
||||||
|
name: cloud_cli_prereqs
|
||||||
|
|
||||||
|
- name: Install Fedora GNOME desktop
|
||||||
|
ansible.builtin.import_tasks: desktop.yml
|
||||||
|
|
||||||
|
- name: Install Fedora Flutter GTK toolchain
|
||||||
|
ansible.builtin.import_tasks: flutter_gtk.yml
|
||||||
|
|
||||||
|
- name: Configure Fedora native remote desktop
|
||||||
|
ansible.builtin.import_tasks: remote_access.yml
|
||||||
8
roles/dev_desktop_fedora_gnome/tasks/remote_access.yml
Normal file
8
roles/dev_desktop_fedora_gnome/tasks/remote_access.yml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
- name: Enable GNOME Remote Desktop services
|
||||||
|
ansible.builtin.service:
|
||||||
|
name: gnome-remote-desktop
|
||||||
|
enabled: true
|
||||||
|
state: started
|
||||||
|
failed_when: false
|
||||||
|
when: not ansible_check_mode
|
||||||
|
|
||||||
16
roles/dev_desktop_windows/defaults/main.yml
Normal file
16
roles/dev_desktop_windows/defaults/main.yml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
cloud_dev_desktop_windows_android_cmdline_tools_url: https://dl.google.com/android/repository/commandlinetools-win-13114758_latest.zip
|
||||||
|
cloud_dev_desktop_windows_android_sdk_api_level: "36"
|
||||||
|
cloud_dev_desktop_windows_android_build_tools_version: "36.0.0"
|
||||||
|
cloud_dev_desktop_windows_android_system_image: system-images;android-36;google_apis;x86_64
|
||||||
|
cloud_dev_desktop_windows_android_avd_name: cloud-dev-desktop-api36
|
||||||
|
cloud_dev_desktop_windows_android_optional_features:
|
||||||
|
- Microsoft-Hyper-V-All
|
||||||
|
- HypervisorPlatform
|
||||||
|
- VirtualMachinePlatform
|
||||||
|
cloud_dev_desktop_windows_flutter_version: "3.41.5"
|
||||||
|
cloud_dev_desktop_windows_flutter_install_root: C:\tools\flutter
|
||||||
|
cloud_dev_desktop_windows_flutter_sdk_url: "https://storage.googleapis.com/flutter_infra_release/releases/stable/windows/flutter_windows_{{ cloud_dev_desktop_windows_flutter_version }}-stable.zip"
|
||||||
|
cloud_dev_desktop_windows_visualstudio_packages:
|
||||||
|
- visualstudio2022buildtools
|
||||||
|
- visualstudio2022-workload-vctools
|
||||||
|
- visualstudio2022-workload-nativedesktop
|
||||||
13
roles/dev_desktop_windows/tasks/main.yml
Normal file
13
roles/dev_desktop_windows/tasks/main.yml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
- name: Install Azure CLI and Google Cloud CLI on Windows
|
||||||
|
ansible.builtin.import_role:
|
||||||
|
name: cloud_cli_prereqs
|
||||||
|
|
||||||
|
- name: Install Windows desktop toolchain
|
||||||
|
ansible.builtin.import_tasks: tools.yml
|
||||||
|
|
||||||
|
- name: Configure Windows SSH client for peer Linux desktops
|
||||||
|
ansible.builtin.import_tasks: ssh_client.yml
|
||||||
|
when: windows_ssh_private_key_b64 is defined and windows_ssh_config_b64 is defined
|
||||||
|
|
||||||
|
- name: Configure Windows remote desktop access
|
||||||
|
ansible.builtin.import_tasks: remote_access.yml
|
||||||
45
roles/dev_desktop_windows/tasks/remote_access.yml
Normal file
45
roles/dev_desktop_windows/tasks/remote_access.yml
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
- name: Enable Windows remote desktop
|
||||||
|
ansible.builtin.raw: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$sshdCapability = Get-WindowsCapability -Online | Where-Object Name -like 'OpenSSH.Server*'
|
||||||
|
if ($sshdCapability.State -ne 'Installed') {
|
||||||
|
Add-WindowsCapability -Online -Name $sshdCapability.Name | Out-Null
|
||||||
|
}
|
||||||
|
Set-Service -Name sshd -StartupType Automatic
|
||||||
|
if ((Get-Service sshd).Status -ne 'Running') {
|
||||||
|
Start-Service sshd
|
||||||
|
}
|
||||||
|
if (-not (Get-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -ErrorAction SilentlyContinue)) {
|
||||||
|
New-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22 | Out-Null
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Enable-NetFirewallRule -Name 'OpenSSH-Server-In-TCP'
|
||||||
|
}
|
||||||
|
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name 'fDenyTSConnections' -Value 0
|
||||||
|
Enable-NetFirewallRule -DisplayGroup 'Remote Desktop'
|
||||||
|
$profiles = Get-NetConnectionProfile
|
||||||
|
foreach ($profile in $profiles) {
|
||||||
|
if ($profile.NetworkCategory -ne 'Private') {
|
||||||
|
Set-NetConnectionProfile -InterfaceIndex $profile.InterfaceIndex -NetworkCategory Private
|
||||||
|
}
|
||||||
|
}
|
||||||
|
winrm quickconfig -q
|
||||||
|
Set-Item -Path WSMan:\localhost\Service\Auth\Basic -Value $true
|
||||||
|
Set-Item -Path WSMan:\localhost\Service\AllowUnencrypted -Value $true
|
||||||
|
changed_when: true
|
||||||
|
|
||||||
|
- name: Authorize administrator SSH public keys on Windows
|
||||||
|
ansible.windows.win_shell: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$authorizedKeysPath = 'C:\ProgramData\ssh\administrators_authorized_keys'
|
||||||
|
New-Item -ItemType Directory -Force -Path 'C:\ProgramData\ssh' | Out-Null
|
||||||
|
$authorizedKeys = @'
|
||||||
|
{{ (cloud_dev_desktop_extra_authorized_keys | default([])) | join('\r\n') }}
|
||||||
|
'@
|
||||||
|
$authorizedKeys = $authorizedKeys.Trim()
|
||||||
|
Set-Content -Path $authorizedKeysPath -Encoding ascii -Value $authorizedKeys
|
||||||
|
icacls $authorizedKeysPath /inheritance:r | Out-Null
|
||||||
|
icacls $authorizedKeysPath /grant 'Administrators:F' | Out-Null
|
||||||
|
icacls $authorizedKeysPath /grant 'SYSTEM:F' | Out-Null
|
||||||
|
changed_when: true
|
||||||
|
when: (cloud_dev_desktop_extra_authorized_keys | default([])) | length > 0
|
||||||
28
roles/dev_desktop_windows/tasks/ssh_client.yml
Normal file
28
roles/dev_desktop_windows/tasks/ssh_client.yml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
- name: Ensure OpenSSH client is installed on Windows
|
||||||
|
ansible.builtin.raw: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$capability = Get-WindowsCapability -Online -Name OpenSSH.Client*
|
||||||
|
if ($capability.State -ne 'Installed') {
|
||||||
|
Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0 | Out-Null
|
||||||
|
}
|
||||||
|
changed_when: true
|
||||||
|
|
||||||
|
- name: Materialize Windows SSH key and config for peer Linux desktops
|
||||||
|
ansible.builtin.raw: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$sshDir = Join-Path $env:USERPROFILE '.ssh'
|
||||||
|
$keyPath = Join-Path $sshDir '{{ windows_ssh_identity_filename | default("cloud-dev-desktop-fleet") }}'
|
||||||
|
$pubPath = "${keyPath}.pub"
|
||||||
|
$configPath = Join-Path $sshDir 'config'
|
||||||
|
|
||||||
|
New-Item -ItemType Directory -Path $sshDir -Force | Out-Null
|
||||||
|
|
||||||
|
[System.IO.File]::WriteAllBytes($keyPath, [System.Convert]::FromBase64String('{{ windows_ssh_private_key_b64 }}'))
|
||||||
|
[System.IO.File]::WriteAllBytes($pubPath, [System.Convert]::FromBase64String('{{ windows_ssh_public_key_b64 }}'))
|
||||||
|
[System.IO.File]::WriteAllText($configPath, [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('{{ windows_ssh_config_b64 }}')), [System.Text.Encoding]::ASCII)
|
||||||
|
|
||||||
|
icacls $sshDir /inheritance:r /grant:r "${env:USERNAME}:(OI)(CI)F" | Out-Null
|
||||||
|
icacls $keyPath /inheritance:r /grant:r "${env:USERNAME}:F" | Out-Null
|
||||||
|
icacls $pubPath /inheritance:r /grant:r "${env:USERNAME}:F" | Out-Null
|
||||||
|
icacls $configPath /inheritance:r /grant:r "${env:USERNAME}:F" | Out-Null
|
||||||
|
changed_when: true
|
||||||
249
roles/dev_desktop_windows/tasks/tools.yml
Normal file
249
roles/dev_desktop_windows/tasks/tools.yml
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
- name: Install Chocolatey
|
||||||
|
ansible.builtin.raw: |
|
||||||
|
Set-ExecutionPolicy Bypass -Scope Process -Force
|
||||||
|
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
|
||||||
|
if (-not (Get-Command choco -ErrorAction SilentlyContinue)) {
|
||||||
|
Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
|
||||||
|
}
|
||||||
|
changed_when: true
|
||||||
|
|
||||||
|
- name: Install Windows toolchain packages
|
||||||
|
ansible.windows.win_shell: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$packages = @()
|
||||||
|
$packages += 'git'
|
||||||
|
if ({{ toolchains.vscode | bool | ternary('$true', '$false') }}) { $packages += 'vscode' }
|
||||||
|
if ({{ toolchains.android_studio | bool | ternary('$true', '$false') }}) { $packages += 'androidstudio' }
|
||||||
|
if ({{ toolchains.codex | bool | ternary('$true', '$false') }}) {
|
||||||
|
$packages += 'vcredist140'
|
||||||
|
$packages += 'nodejs-lts'
|
||||||
|
}
|
||||||
|
if ({{ toolchains.flutter | bool | ternary('$true', '$false') }}) {
|
||||||
|
$packages += @({{ cloud_dev_desktop_windows_visualstudio_packages | map('to_json') | join(', ') }})
|
||||||
|
}
|
||||||
|
if (({{ toolchains.dart | bool | ternary('$true', '$false') }}) -and (-not {{ toolchains.flutter | bool | ternary('$true', '$false') }})) {
|
||||||
|
$packages += 'dart-sdk'
|
||||||
|
}
|
||||||
|
foreach ($pkg in ($packages | Select-Object -Unique)) {
|
||||||
|
choco install $pkg -y --no-progress
|
||||||
|
}
|
||||||
|
changed_when: true
|
||||||
|
|
||||||
|
- name: Install Codex CLI globally on Windows
|
||||||
|
ansible.windows.win_shell: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine')
|
||||||
|
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
|
||||||
|
$npmUserBin = if ($env:APPDATA) { Join-Path $env:APPDATA 'npm' } else { '' }
|
||||||
|
$extraPaths = @(
|
||||||
|
$npmUserBin,
|
||||||
|
'C:\Program Files\nodejs',
|
||||||
|
'C:\ProgramData\chocolatey\bin'
|
||||||
|
) | Where-Object { $_ }
|
||||||
|
$env:Path = (($machinePath, $userPath, ($extraPaths -join ';')) -join ';')
|
||||||
|
npm install -g @openai/codex
|
||||||
|
changed_when: true
|
||||||
|
when: toolchains.codex | bool
|
||||||
|
|
||||||
|
- name: Enable Windows virtualization features required for Android emulators
|
||||||
|
ansible.windows.win_optional_feature:
|
||||||
|
name: "{{ cloud_dev_desktop_windows_android_optional_features }}"
|
||||||
|
state: present
|
||||||
|
include_parent: true
|
||||||
|
register: windows_android_virtualization_features
|
||||||
|
when: toolchains.android_studio | bool
|
||||||
|
|
||||||
|
- name: Reboot Windows after enabling Android virtualization features
|
||||||
|
ansible.windows.win_reboot:
|
||||||
|
reboot_timeout: 3600
|
||||||
|
connect_timeout: 30
|
||||||
|
post_reboot_delay: 60
|
||||||
|
when:
|
||||||
|
- toolchains.android_studio | bool
|
||||||
|
- windows_android_virtualization_features.reboot_required | default(false)
|
||||||
|
|
||||||
|
- name: Install Android SDK command-line tools on Windows
|
||||||
|
ansible.windows.win_shell: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$androidSdkRoot = Join-Path $env:LOCALAPPDATA 'Android\Sdk'
|
||||||
|
$cmdlineToolsRoot = Join-Path $androidSdkRoot 'cmdline-tools'
|
||||||
|
$cmdlineToolsLatest = Join-Path $cmdlineToolsRoot 'latest'
|
||||||
|
$cmdlineToolsBin = Join-Path $cmdlineToolsLatest 'bin'
|
||||||
|
$javaHome = 'C:\Program Files\Android\Android Studio\jbr'
|
||||||
|
New-Item -ItemType Directory -Force -Path $androidSdkRoot, $cmdlineToolsRoot | Out-Null
|
||||||
|
if (-not (Test-Path (Join-Path $cmdlineToolsBin 'sdkmanager.bat'))) {
|
||||||
|
$zipPath = Join-Path $env:TEMP 'android-commandlinetools.zip'
|
||||||
|
$extractDir = Join-Path $env:TEMP 'android-commandlinetools'
|
||||||
|
if (Test-Path $extractDir) {
|
||||||
|
Remove-Item -Recurse -Force $extractDir
|
||||||
|
}
|
||||||
|
Invoke-WebRequest -UseBasicParsing -Uri '{{ cloud_dev_desktop_windows_android_cmdline_tools_url }}' -OutFile $zipPath
|
||||||
|
Expand-Archive -LiteralPath $zipPath -DestinationPath $extractDir -Force
|
||||||
|
if (Test-Path $cmdlineToolsLatest) {
|
||||||
|
Remove-Item -Recurse -Force $cmdlineToolsLatest
|
||||||
|
}
|
||||||
|
Move-Item -Path (Join-Path $extractDir 'cmdline-tools') -Destination $cmdlineToolsLatest
|
||||||
|
}
|
||||||
|
if (Test-Path $javaHome) {
|
||||||
|
[Environment]::SetEnvironmentVariable('JAVA_HOME', $javaHome, 'User')
|
||||||
|
$env:JAVA_HOME = $javaHome
|
||||||
|
}
|
||||||
|
[Environment]::SetEnvironmentVariable('ANDROID_HOME', $androidSdkRoot, 'User')
|
||||||
|
[Environment]::SetEnvironmentVariable('ANDROID_SDK_ROOT', $androidSdkRoot, 'User')
|
||||||
|
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
|
||||||
|
$androidPathEntries = @(
|
||||||
|
$androidSdkRoot,
|
||||||
|
(Join-Path $androidSdkRoot 'platform-tools'),
|
||||||
|
(Join-Path $androidSdkRoot 'emulator'),
|
||||||
|
$cmdlineToolsBin
|
||||||
|
)
|
||||||
|
if ($env:JAVA_HOME) {
|
||||||
|
$androidPathEntries += (Join-Path $env:JAVA_HOME 'bin')
|
||||||
|
}
|
||||||
|
$userPathParts = @($userPath -split ';') | Where-Object { $_ }
|
||||||
|
foreach ($entry in $androidPathEntries) {
|
||||||
|
if (-not ($userPathParts -contains $entry)) {
|
||||||
|
$userPathParts += $entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$newUserPath = ($userPathParts | Select-Object -Unique) -join ';'
|
||||||
|
[Environment]::SetEnvironmentVariable('Path', $newUserPath, 'User')
|
||||||
|
$env:ANDROID_HOME = $androidSdkRoot
|
||||||
|
$env:ANDROID_SDK_ROOT = $androidSdkRoot
|
||||||
|
$env:Path = ([Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + $newUserPath + ';C:\ProgramData\chocolatey\bin')
|
||||||
|
$sdkmanager = Join-Path $cmdlineToolsBin 'sdkmanager.bat'
|
||||||
|
$sdkPackages = @(
|
||||||
|
'platform-tools',
|
||||||
|
'platforms;android-{{ cloud_dev_desktop_windows_android_sdk_api_level }}',
|
||||||
|
'build-tools;{{ cloud_dev_desktop_windows_android_build_tools_version }}',
|
||||||
|
'emulator',
|
||||||
|
'{{ cloud_dev_desktop_windows_android_system_image }}'
|
||||||
|
)
|
||||||
|
1..32 | ForEach-Object { 'y' } | & $sdkmanager --sdk_root="$androidSdkRoot" --licenses | Out-Null
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Android SDK license acceptance failed with exit code $LASTEXITCODE"
|
||||||
|
}
|
||||||
|
& $sdkmanager --sdk_root="$androidSdkRoot" @sdkPackages
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Android SDK package installation failed with exit code $LASTEXITCODE"
|
||||||
|
}
|
||||||
|
$avdmanager = Join-Path $cmdlineToolsBin 'avdmanager.bat'
|
||||||
|
$avdName = '{{ cloud_dev_desktop_windows_android_avd_name }}'
|
||||||
|
$avdPath = Join-Path $env:USERPROFILE ".android\avd\$avdName.avd"
|
||||||
|
if (-not (Test-Path $avdPath)) {
|
||||||
|
'no' | & $avdmanager create avd --force -n $avdName -k '{{ cloud_dev_desktop_windows_android_system_image }}' | Out-Null
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Android AVD creation failed with exit code $LASTEXITCODE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
changed_when: true
|
||||||
|
when: toolchains.android_studio | bool
|
||||||
|
|
||||||
|
- name: Probe installed Flutter SDK on Windows
|
||||||
|
ansible.windows.win_shell: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$flutterBat = Join-Path '{{ cloud_dev_desktop_windows_flutter_install_root }}' 'bin\flutter.bat'
|
||||||
|
if (Test-Path $flutterBat) {
|
||||||
|
& $flutterBat --version
|
||||||
|
}
|
||||||
|
register: windows_flutter_version_probe
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
when: toolchains.flutter | bool
|
||||||
|
|
||||||
|
- name: Install Flutter SDK on Windows
|
||||||
|
ansible.windows.win_shell: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$flutterRoot = '{{ cloud_dev_desktop_windows_flutter_install_root }}'
|
||||||
|
$installRoot = Split-Path -Parent $flutterRoot
|
||||||
|
$zipPath = Join-Path $env:TEMP 'flutter-windows-sdk.zip'
|
||||||
|
New-Item -ItemType Directory -Force -Path $installRoot | Out-Null
|
||||||
|
if (Test-Path $flutterRoot) {
|
||||||
|
Remove-Item -Recurse -Force $flutterRoot
|
||||||
|
}
|
||||||
|
Invoke-WebRequest -UseBasicParsing -Uri '{{ cloud_dev_desktop_windows_flutter_sdk_url }}' -OutFile $zipPath
|
||||||
|
Expand-Archive -LiteralPath $zipPath -DestinationPath $installRoot -Force
|
||||||
|
if (-not (Test-Path (Join-Path $flutterRoot 'bin\flutter.bat'))) {
|
||||||
|
throw "Flutter SDK install failed: missing $(Join-Path $flutterRoot 'bin\flutter.bat')"
|
||||||
|
}
|
||||||
|
changed_when: true
|
||||||
|
when:
|
||||||
|
- toolchains.flutter | bool
|
||||||
|
- windows_flutter_version_probe.rc != 0 or ("Flutter " ~ cloud_dev_desktop_windows_flutter_version ~ " ") not in windows_flutter_version_probe.stdout
|
||||||
|
|
||||||
|
- name: Configure Flutter on Windows
|
||||||
|
ansible.windows.win_shell: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine')
|
||||||
|
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
|
||||||
|
$npmUserBin = if ($env:APPDATA) { Join-Path $env:APPDATA 'npm' } else { '' }
|
||||||
|
$extraPaths = @(
|
||||||
|
$npmUserBin,
|
||||||
|
'C:\Program Files\nodejs',
|
||||||
|
'{{ cloud_dev_desktop_windows_flutter_install_root }}\bin',
|
||||||
|
'C:\Program Files\Microsoft VS Code\bin',
|
||||||
|
'C:\ProgramData\chocolatey\bin'
|
||||||
|
) | Where-Object { $_ }
|
||||||
|
$env:Path = (($machinePath, $userPath, ($extraPaths -join ';')) -join ';')
|
||||||
|
if ({{ toolchains.android_studio | bool | ternary('$true', '$false') }}) {
|
||||||
|
flutter config --android-sdk (Join-Path $env:LOCALAPPDATA 'Android\Sdk')
|
||||||
|
}
|
||||||
|
flutter config --enable-windows-desktop
|
||||||
|
flutter doctor
|
||||||
|
changed_when: true
|
||||||
|
when: toolchains.flutter | bool
|
||||||
|
|
||||||
|
- name: Verify Windows desktop toolchain versions
|
||||||
|
ansible.builtin.raw: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine')
|
||||||
|
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
|
||||||
|
$npmUserBin = if ($env:APPDATA) { Join-Path $env:APPDATA 'npm' } else { '' }
|
||||||
|
$extraPaths = @(
|
||||||
|
$npmUserBin,
|
||||||
|
'C:\Program Files\nodejs',
|
||||||
|
'{{ cloud_dev_desktop_windows_flutter_install_root }}\bin',
|
||||||
|
'C:\Program Files\Microsoft VS Code\bin',
|
||||||
|
'C:\ProgramData\chocolatey\bin'
|
||||||
|
) | Where-Object { $_ }
|
||||||
|
$env:Path = (($machinePath, $userPath, ($extraPaths -join ';')) -join ';')
|
||||||
|
$nodeMajor = [int]((node --version).Trim().TrimStart('v').Split('.')[0])
|
||||||
|
if ($nodeMajor -lt 22) {
|
||||||
|
throw "Node.js 22+ is required, found $(node --version)"
|
||||||
|
}
|
||||||
|
if ({{ toolchains.codex | bool | ternary('$true', '$false') }}) {
|
||||||
|
$codexCmd = Join-Path $env:APPDATA 'npm\codex.cmd'
|
||||||
|
if (-not (Test-Path $codexCmd)) {
|
||||||
|
throw "Missing Codex CLI launcher at $codexCmd"
|
||||||
|
}
|
||||||
|
$codexCmdLine = '"' + $codexCmd + '" --version'
|
||||||
|
cmd.exe /d /c $codexCmdLine | Out-Null
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Codex CLI version probe failed with exit code $LASTEXITCODE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ({{ toolchains.vscode | bool | ternary('$true', '$false') }}) {
|
||||||
|
Get-Command code | Out-Null
|
||||||
|
}
|
||||||
|
if ({{ toolchains.android_studio | bool | ternary('$true', '$false') }}) {
|
||||||
|
if (-not (Test-Path 'C:\Program Files\Android\Android Studio\bin\studio64.exe')) {
|
||||||
|
throw 'Missing Android Studio executable at C:\Program Files\Android\Android Studio\bin\studio64.exe'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ({{ toolchains.flutter | bool | ternary('$true', '$false') }}) {
|
||||||
|
flutter --version | Out-Null
|
||||||
|
}
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Create Windows desktop metadata directory
|
||||||
|
ansible.builtin.raw: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
New-Item -ItemType Directory -Force -Path 'C:\cloud-dev-desktop' | Out-Null
|
||||||
|
@"
|
||||||
|
PROFILE_NAME={{ profile_name }}
|
||||||
|
PROVIDER={{ provider }}
|
||||||
|
OWNER={{ owner }}
|
||||||
|
PURPOSE={{ purpose }}
|
||||||
|
EXPIRES_AT={{ vars.get('cloud_vm_expires_at', '') }}
|
||||||
|
"@ | Set-Content -Encoding ASCII 'C:\cloud-dev-desktop\profile.env'
|
||||||
|
changed_when: true
|
||||||
149
roles/gcp_dev_desktop_lifecycle/tasks/create.yml
Normal file
149
roles/gcp_dev_desktop_lifecycle/tasks/create.yml
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
- name: Preview GCP create request
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg:
|
||||||
|
- "gcp_project_id={{ gcp_project_id }}"
|
||||||
|
- "gcp_zone={{ gcp_zone }}"
|
||||||
|
- "gcp_vm_name={{ gcp_vm_name }}"
|
||||||
|
- "allowed_cidrs={{ allowed_cidrs | join(',') }}"
|
||||||
|
- "allowed_tcp_ports={{ allowed_tcp_ports | join(',') }}"
|
||||||
|
|
||||||
|
- name: Ensure GCP network exists
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv:
|
||||||
|
- gcloud
|
||||||
|
- compute
|
||||||
|
- networks
|
||||||
|
- create
|
||||||
|
- "{{ gcp_network }}"
|
||||||
|
- --project
|
||||||
|
- "{{ gcp_project_id }}"
|
||||||
|
- --subnet-mode
|
||||||
|
- custom
|
||||||
|
changed_when: true
|
||||||
|
failed_when: false
|
||||||
|
when: not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Ensure GCP subnet exists
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv:
|
||||||
|
- gcloud
|
||||||
|
- compute
|
||||||
|
- networks
|
||||||
|
- subnets
|
||||||
|
- create
|
||||||
|
- "{{ gcp_subnet }}"
|
||||||
|
- --project
|
||||||
|
- "{{ gcp_project_id }}"
|
||||||
|
- --network
|
||||||
|
- "{{ gcp_network }}"
|
||||||
|
- --region
|
||||||
|
- "{{ gcp_region }}"
|
||||||
|
- --range
|
||||||
|
- "{{ gcp_subnet_cidr | default('10.52.1.0/24') }}"
|
||||||
|
changed_when: true
|
||||||
|
failed_when: false
|
||||||
|
when: not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Ensure GCP firewall rules exist
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv:
|
||||||
|
- gcloud
|
||||||
|
- compute
|
||||||
|
- firewall-rules
|
||||||
|
- create
|
||||||
|
- "{{ gcp_firewall_rule }}-{{ port }}-{{ index }}"
|
||||||
|
- --project
|
||||||
|
- "{{ gcp_project_id }}"
|
||||||
|
- --network
|
||||||
|
- "{{ gcp_network }}"
|
||||||
|
- --allow
|
||||||
|
- "tcp:{{ port }}"
|
||||||
|
- --source-ranges
|
||||||
|
- "{{ cidr }}"
|
||||||
|
- --target-tags
|
||||||
|
- "{{ cloud_vm_profile_slug }}"
|
||||||
|
loop: "{{ allowed_cidrs | product(allowed_tcp_ports) | list }}"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.0 }} -> {{ item.1 }}"
|
||||||
|
index_var: index
|
||||||
|
changed_when: true
|
||||||
|
failed_when: false
|
||||||
|
when: not ansible_check_mode
|
||||||
|
vars:
|
||||||
|
cidr: "{{ item.0 }}"
|
||||||
|
port: "{{ item.1 }}"
|
||||||
|
|
||||||
|
- name: Build GCP create command
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
gcp_vm_create_command: >-
|
||||||
|
gcloud compute instances create {{ gcp_vm_name | quote }}
|
||||||
|
--project={{ gcp_project_id | quote }}
|
||||||
|
--zone={{ gcp_zone | quote }}
|
||||||
|
--machine-type={{ vm_size | quote }}
|
||||||
|
--boot-disk-size={{ (disk_gb | string) ~ 'GB' | quote }}
|
||||||
|
--network={{ gcp_network | quote }}
|
||||||
|
--subnet={{ gcp_subnet | quote }}
|
||||||
|
--tags={{ cloud_vm_profile_slug | quote }}
|
||||||
|
--labels={{ gcp_labels | dict2items | map('join', '=') | join(',') | quote }}
|
||||||
|
--image-family={{ gcp_image_family | quote }}
|
||||||
|
--image-project={{ gcp_image_project | quote }}
|
||||||
|
{% if os_family != 'windows' %}
|
||||||
|
--metadata=ssh-keys='{{ admin_username }}:{{ lookup("file", ssh_public_key_path) | trim }}'
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
- name: Create GCP VM
|
||||||
|
ansible.builtin.shell: "{{ gcp_vm_create_command }}"
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
changed_when: true
|
||||||
|
when: not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Reset GCP Windows password when needed
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv:
|
||||||
|
- gcloud
|
||||||
|
- compute
|
||||||
|
- reset-windows-password
|
||||||
|
- "{{ gcp_vm_name }}"
|
||||||
|
- --project
|
||||||
|
- "{{ gcp_project_id }}"
|
||||||
|
- --zone
|
||||||
|
- "{{ gcp_zone }}"
|
||||||
|
- --user
|
||||||
|
- "{{ admin_username }}"
|
||||||
|
- --quiet
|
||||||
|
changed_when: true
|
||||||
|
when:
|
||||||
|
- os_family == "windows"
|
||||||
|
- not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Fetch GCP VM networking facts
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv:
|
||||||
|
- gcloud
|
||||||
|
- compute
|
||||||
|
- instances
|
||||||
|
- describe
|
||||||
|
- "{{ gcp_vm_name }}"
|
||||||
|
- --project
|
||||||
|
- "{{ gcp_project_id }}"
|
||||||
|
- --zone
|
||||||
|
- "{{ gcp_zone }}"
|
||||||
|
- --format=json
|
||||||
|
register: gcp_vm_json
|
||||||
|
changed_when: false
|
||||||
|
when: not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Set GCP VM connection facts
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
cloud_vm_public_ip: "{{ ((gcp_vm_json.stdout | from_json).networkInterfaces[0].accessConfigs | default([]) | first).natIP | default('198.51.100.11') }}"
|
||||||
|
cloud_vm_private_ip: "{{ (gcp_vm_json.stdout | from_json).networkInterfaces[0].networkIP | default('10.52.1.10') }}"
|
||||||
|
cloud_vm_admin_user: "{{ admin_username }}"
|
||||||
|
when: not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Set GCP dry-run connection facts
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
cloud_vm_public_ip: "{{ cloud_vm_public_ip | default('198.51.100.11') }}"
|
||||||
|
cloud_vm_private_ip: "{{ cloud_vm_private_ip | default('10.52.1.10') }}"
|
||||||
|
cloud_vm_admin_user: "{{ admin_username }}"
|
||||||
|
when: ansible_check_mode
|
||||||
91
roles/gcp_dev_desktop_lifecycle/tasks/destroy.yml
Normal file
91
roles/gcp_dev_desktop_lifecycle/tasks/destroy.yml
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
- name: Preview GCP destroy/cleanup request
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg:
|
||||||
|
- "gcp_project_id={{ gcp_project_id | default('n/a') }}"
|
||||||
|
- "gcp_vm_name={{ gcp_vm_name | default(profile_name | default('n/a')) }}"
|
||||||
|
- "gcp_cleanup_mode={{ gcp_cleanup_mode | default(false) }}"
|
||||||
|
- "cloud_vm_destroy_mode={{ cloud_vm_destroy_mode | default('destroy') }}"
|
||||||
|
|
||||||
|
- name: List GCP managed instances
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv:
|
||||||
|
- gcloud
|
||||||
|
- compute
|
||||||
|
- instances
|
||||||
|
- list
|
||||||
|
- --project
|
||||||
|
- "{{ gcp_project_id }}"
|
||||||
|
- --filter
|
||||||
|
- "labels.toolkit_scope=cloud-dev-desktop AND labels.managed_by=ansible"
|
||||||
|
- --format=value(name,zone.basename())
|
||||||
|
register: gcp_managed_instances
|
||||||
|
changed_when: false
|
||||||
|
when:
|
||||||
|
- gcp_cleanup_mode | default(false)
|
||||||
|
- not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Park GCP VM in lowest-consumption mode
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv:
|
||||||
|
- gcloud
|
||||||
|
- compute
|
||||||
|
- instances
|
||||||
|
- stop
|
||||||
|
- "{{ gcp_vm_name }}"
|
||||||
|
- --project
|
||||||
|
- "{{ gcp_project_id }}"
|
||||||
|
- --zone
|
||||||
|
- "{{ gcp_zone }}"
|
||||||
|
- --quiet
|
||||||
|
changed_when: true
|
||||||
|
when:
|
||||||
|
- not gcp_cleanup_mode | default(false)
|
||||||
|
- (cloud_vm_destroy_mode | default('destroy')) == 'park'
|
||||||
|
- not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Delete GCP VM directly
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv:
|
||||||
|
- gcloud
|
||||||
|
- compute
|
||||||
|
- instances
|
||||||
|
- delete
|
||||||
|
- "{{ gcp_vm_name }}"
|
||||||
|
- --project
|
||||||
|
- "{{ gcp_project_id }}"
|
||||||
|
- --zone
|
||||||
|
- "{{ gcp_zone }}"
|
||||||
|
- --quiet
|
||||||
|
changed_when: true
|
||||||
|
when:
|
||||||
|
- not gcp_cleanup_mode | default(false)
|
||||||
|
- (cloud_vm_destroy_mode | default('destroy')) == 'destroy'
|
||||||
|
- not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Delete GCP managed instances
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv:
|
||||||
|
- gcloud
|
||||||
|
- compute
|
||||||
|
- instances
|
||||||
|
- delete
|
||||||
|
- "{{ item.split()[0] }}"
|
||||||
|
- --project
|
||||||
|
- "{{ gcp_project_id }}"
|
||||||
|
- --zone
|
||||||
|
- "{{ item.split()[1] }}"
|
||||||
|
- --quiet
|
||||||
|
loop: "{{ gcp_managed_instances.stdout_lines | default([]) }}"
|
||||||
|
changed_when: true
|
||||||
|
when:
|
||||||
|
- gcp_cleanup_mode | default(false)
|
||||||
|
- not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Remove GCP state file after destroy
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ cloud_vm_state_file }}"
|
||||||
|
state: absent
|
||||||
|
when:
|
||||||
|
- cloud_vm_state_file is defined
|
||||||
|
- not gcp_cleanup_mode | default(false)
|
||||||
|
- (cloud_vm_destroy_mode | default('destroy')) == 'destroy'
|
||||||
50
roles/gcp_dev_desktop_lifecycle/tasks/facts.yml
Normal file
50
roles/gcp_dev_desktop_lifecycle/tasks/facts.yml
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
- name: Normalize GCP desktop resource names
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
gcp_project_id: "{{ gcp_project_id | default(lookup('env', 'GCP_PROJECT_ID')) }}"
|
||||||
|
gcp_zone: "{{ zone | default(gcp_zone | default('asia-northeast1-a')) }}"
|
||||||
|
gcp_network: "{{ gcp_network | default('devdesktop-' ~ cloud_vm_owner_slug) }}"
|
||||||
|
gcp_subnet: "{{ gcp_subnet | default('devdesktop-' ~ cloud_vm_owner_slug ~ '-subnet') }}"
|
||||||
|
gcp_firewall_rule: "{{ gcp_firewall_rule | default('allow-' ~ cloud_vm_profile_slug) }}"
|
||||||
|
gcp_vm_name: "{{ gcp_vm_name | default('vm-' ~ cloud_vm_profile_slug) }}"
|
||||||
|
gcp_labels: "{{ tags | default({}) }}"
|
||||||
|
cloud_vm_platform: "gcp"
|
||||||
|
|
||||||
|
- name: Derive GCP region from zone
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
gcp_region: "{{ gcp_zone.rsplit('-', 1)[0] }}"
|
||||||
|
|
||||||
|
- name: Assert GCP project id is available when requested
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- gcp_project_id | length > 0
|
||||||
|
fail_msg: "GCP_PROJECT_ID or gcp_project_id is required for GCP operations."
|
||||||
|
when: not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Select GCP image defaults by OS family
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
gcp_image_project: >-
|
||||||
|
{{
|
||||||
|
image_project
|
||||||
|
| default(
|
||||||
|
gcp_image_project
|
||||||
|
| default(
|
||||||
|
{
|
||||||
|
'windows': 'windows-cloud',
|
||||||
|
'fedora-gnome': 'fedora-cloud',
|
||||||
|
'debian-kde': 'debian-cloud'
|
||||||
|
}[os_family]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
gcp_image_family: >-
|
||||||
|
{{
|
||||||
|
image_family
|
||||||
|
| default(
|
||||||
|
{
|
||||||
|
'windows': 'windows-11',
|
||||||
|
'fedora-gnome': 'fedora-cloud-43',
|
||||||
|
'debian-kde': 'debian-13'
|
||||||
|
}[os_family]
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
gcp_windows_password: "{{ gcp_windows_password | default(lookup('env', 'GCP_WINDOWS_ADMIN_PASSWORD')) }}"
|
||||||
16
roles/gcp_dev_desktop_lifecycle/tasks/main.yml
Normal file
16
roles/gcp_dev_desktop_lifecycle/tasks/main.yml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
- name: Gather GCP lifecycle facts
|
||||||
|
ansible.builtin.include_tasks: facts.yml
|
||||||
|
|
||||||
|
- name: Run GCP create flow
|
||||||
|
ansible.builtin.include_tasks: create.yml
|
||||||
|
when: cloud_lifecycle_action == "create"
|
||||||
|
|
||||||
|
- name: Run GCP destroy flow
|
||||||
|
ansible.builtin.include_tasks: destroy.yml
|
||||||
|
when: cloud_lifecycle_action == "destroy"
|
||||||
|
|
||||||
|
- name: Run GCP cleanup flow
|
||||||
|
ansible.builtin.include_tasks: destroy.yml
|
||||||
|
vars:
|
||||||
|
gcp_cleanup_mode: true
|
||||||
|
when: cloud_lifecycle_action == "cleanup"
|
||||||
@ -4,14 +4,22 @@
|
|||||||
agent_svc_plus_repo_url: "https://github.com/x-evor/agent.svc.plus.git"
|
agent_svc_plus_repo_url: "https://github.com/x-evor/agent.svc.plus.git"
|
||||||
agent_svc_plus_repo_version: "main"
|
agent_svc_plus_repo_version: "main"
|
||||||
agent_svc_plus_app_dir: "/opt/agent.svc.plus"
|
agent_svc_plus_app_dir: "/opt/agent.svc.plus"
|
||||||
|
agent_svc_plus_manage_source_checkout: false
|
||||||
|
agent_svc_plus_binary_src: ""
|
||||||
|
agent_svc_plus_release_repo: "x-evor/agent.svc.plus"
|
||||||
|
agent_svc_plus_release_tag: ""
|
||||||
|
agent_svc_plus_release_base_url: "https://github.com/{{ agent_svc_plus_release_repo }}/releases/download"
|
||||||
|
|
||||||
agent_svc_plus_go_version: "1.25.1"
|
agent_svc_plus_go_version: "1.25.1"
|
||||||
agent_svc_plus_go_root: "/usr/local/go"
|
agent_svc_plus_go_root: "/usr/local/go"
|
||||||
agent_svc_plus_go_bin: "{{ agent_svc_plus_go_root }}/bin/go"
|
agent_svc_plus_go_bin: "{{ agent_svc_plus_go_root }}/bin/go"
|
||||||
|
agent_svc_plus_build_on_target: false
|
||||||
|
|
||||||
agent_svc_plus_binary_name: "agent-svc-plus"
|
agent_svc_plus_binary_name: "agent-svc-plus"
|
||||||
agent_svc_plus_binary_path: "/usr/local/bin/{{ agent_svc_plus_binary_name }}"
|
agent_svc_plus_binary_path: "/usr/local/bin/{{ agent_svc_plus_binary_name }}"
|
||||||
agent_svc_plus_build_target: "./cmd/agent"
|
agent_svc_plus_build_target: "./cmd/agent"
|
||||||
|
agent_svc_plus_release_asset_name: "{{ agent_svc_plus_binary_name }}-linux-{{ agent_svc_plus_release_arch }}"
|
||||||
|
agent_svc_plus_release_asset_url: "{{ agent_svc_plus_release_base_url }}/{{ agent_svc_plus_release_tag }}/{{ agent_svc_plus_release_asset_name }}"
|
||||||
|
|
||||||
agent_svc_plus_service_name: "agent-svc-plus"
|
agent_svc_plus_service_name: "agent-svc-plus"
|
||||||
agent_svc_plus_service_description: "agent.svc.plus service"
|
agent_svc_plus_service_description: "agent.svc.plus service"
|
||||||
|
|||||||
@ -3,6 +3,15 @@
|
|||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
agent_svc_plus_go_arch: >-
|
agent_svc_plus_go_arch: >-
|
||||||
{{ 'arm64' if ansible_architecture in ['aarch64', 'arm64'] else 'amd64' }}
|
{{ 'arm64' if ansible_architecture in ['aarch64', 'arm64'] else 'amd64' }}
|
||||||
|
agent_svc_plus_release_arch: >-
|
||||||
|
{{ 'arm64' if ansible_architecture in ['aarch64', 'arm64'] else 'amd64' }}
|
||||||
|
when: agent_svc_plus_build_on_target | bool
|
||||||
|
|
||||||
|
- name: Map release artifact architecture when build is disabled
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
agent_svc_plus_release_arch: >-
|
||||||
|
{{ 'arm64' if ansible_architecture in ['aarch64', 'arm64'] else 'amd64' }}
|
||||||
|
when: not (agent_svc_plus_build_on_target | bool)
|
||||||
|
|
||||||
- name: Ensure agent build prerequisites are installed
|
- name: Ensure agent build prerequisites are installed
|
||||||
ansible.builtin.apt:
|
ansible.builtin.apt:
|
||||||
@ -13,32 +22,40 @@
|
|||||||
- tar
|
- tar
|
||||||
state: present
|
state: present
|
||||||
update_cache: true
|
update_cache: true
|
||||||
|
when: agent_svc_plus_build_on_target | bool
|
||||||
|
|
||||||
- name: Check installed Go version
|
- name: Check installed Go version
|
||||||
ansible.builtin.command: "{{ agent_svc_plus_go_bin }} version"
|
ansible.builtin.command: "{{ agent_svc_plus_go_bin }} version"
|
||||||
register: agent_svc_plus_go_version_check
|
register: agent_svc_plus_go_version_check
|
||||||
changed_when: false
|
changed_when: false
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
when: agent_svc_plus_build_on_target | bool
|
||||||
|
|
||||||
- name: Download Go toolchain archive
|
- name: Download Go toolchain archive
|
||||||
ansible.builtin.get_url:
|
ansible.builtin.get_url:
|
||||||
url: "https://go.dev/dl/go{{ agent_svc_plus_go_version }}.linux-{{ agent_svc_plus_go_arch }}.tar.gz"
|
url: "https://go.dev/dl/go{{ agent_svc_plus_go_version }}.linux-{{ agent_svc_plus_go_arch }}.tar.gz"
|
||||||
dest: "/tmp/go{{ agent_svc_plus_go_version }}.linux-{{ agent_svc_plus_go_arch }}.tar.gz"
|
dest: "/tmp/go{{ agent_svc_plus_go_version }}.linux-{{ agent_svc_plus_go_arch }}.tar.gz"
|
||||||
mode: "0644"
|
mode: "0644"
|
||||||
when: agent_svc_plus_go_version_check.rc != 0 or ('go' ~ agent_svc_plus_go_version) not in agent_svc_plus_go_version_check.stdout
|
when:
|
||||||
|
- agent_svc_plus_build_on_target | bool
|
||||||
|
- agent_svc_plus_go_version_check.rc != 0 or ('go' ~ agent_svc_plus_go_version) not in agent_svc_plus_go_version_check.stdout
|
||||||
|
|
||||||
- name: Remove previous Go installation
|
- name: Remove previous Go installation
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ agent_svc_plus_go_root }}"
|
path: "{{ agent_svc_plus_go_root }}"
|
||||||
state: absent
|
state: absent
|
||||||
when: agent_svc_plus_go_version_check.rc != 0 or ('go' ~ agent_svc_plus_go_version) not in agent_svc_plus_go_version_check.stdout
|
when:
|
||||||
|
- agent_svc_plus_build_on_target | bool
|
||||||
|
- agent_svc_plus_go_version_check.rc != 0 or ('go' ~ agent_svc_plus_go_version) not in agent_svc_plus_go_version_check.stdout
|
||||||
|
|
||||||
- name: Install Go toolchain
|
- name: Install Go toolchain
|
||||||
ansible.builtin.unarchive:
|
ansible.builtin.unarchive:
|
||||||
src: "/tmp/go{{ agent_svc_plus_go_version }}.linux-{{ agent_svc_plus_go_arch }}.tar.gz"
|
src: "/tmp/go{{ agent_svc_plus_go_version }}.linux-{{ agent_svc_plus_go_arch }}.tar.gz"
|
||||||
dest: "/usr/local"
|
dest: "/usr/local"
|
||||||
remote_src: true
|
remote_src: true
|
||||||
when: agent_svc_plus_go_version_check.rc != 0 or ('go' ~ agent_svc_plus_go_version) not in agent_svc_plus_go_version_check.stdout
|
when:
|
||||||
|
- agent_svc_plus_build_on_target | bool
|
||||||
|
- agent_svc_plus_go_version_check.rc != 0 or ('go' ~ agent_svc_plus_go_version) not in agent_svc_plus_go_version_check.stdout
|
||||||
|
|
||||||
- name: Create agent directories
|
- name: Create agent directories
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
@ -60,6 +77,7 @@
|
|||||||
version: "{{ agent_svc_plus_repo_version }}"
|
version: "{{ agent_svc_plus_repo_version }}"
|
||||||
update: true
|
update: true
|
||||||
notify: Restart agent-svc-plus
|
notify: Restart agent-svc-plus
|
||||||
|
when: agent_svc_plus_manage_source_checkout | bool
|
||||||
|
|
||||||
- name: Build agent.svc.plus binary
|
- name: Build agent.svc.plus binary
|
||||||
ansible.builtin.command: >-
|
ansible.builtin.command: >-
|
||||||
@ -71,6 +89,50 @@
|
|||||||
PATH: "{{ agent_svc_plus_go_root }}/bin:{{ ansible_env.PATH }}"
|
PATH: "{{ agent_svc_plus_go_root }}/bin:{{ ansible_env.PATH }}"
|
||||||
GOTOOLCHAIN: local
|
GOTOOLCHAIN: local
|
||||||
notify: Restart agent-svc-plus
|
notify: Restart agent-svc-plus
|
||||||
|
when: agent_svc_plus_build_on_target | bool
|
||||||
|
|
||||||
|
- name: Deploy prebuilt agent binary from control machine
|
||||||
|
ansible.builtin.copy:
|
||||||
|
src: "{{ agent_svc_plus_binary_src }}"
|
||||||
|
dest: "{{ agent_svc_plus_binary_path }}"
|
||||||
|
owner: "{{ agent_svc_plus_user }}"
|
||||||
|
group: "{{ agent_svc_plus_group }}"
|
||||||
|
mode: "0755"
|
||||||
|
notify: Restart agent-svc-plus
|
||||||
|
when:
|
||||||
|
- not (agent_svc_plus_build_on_target | bool)
|
||||||
|
- agent_svc_plus_binary_src | length > 0
|
||||||
|
|
||||||
|
- name: Download prebuilt agent binary from GitHub Release
|
||||||
|
ansible.builtin.get_url:
|
||||||
|
url: "{{ agent_svc_plus_release_asset_url }}"
|
||||||
|
dest: "{{ agent_svc_plus_binary_path }}"
|
||||||
|
owner: "{{ agent_svc_plus_user }}"
|
||||||
|
group: "{{ agent_svc_plus_group }}"
|
||||||
|
mode: "0755"
|
||||||
|
notify: Restart agent-svc-plus
|
||||||
|
when:
|
||||||
|
- not (agent_svc_plus_build_on_target | bool)
|
||||||
|
- agent_svc_plus_binary_src | length == 0
|
||||||
|
- agent_svc_plus_release_tag | length > 0
|
||||||
|
|
||||||
|
- name: Check existing agent binary on target host
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: "{{ agent_svc_plus_binary_path }}"
|
||||||
|
register: agent_svc_plus_binary_stat
|
||||||
|
|
||||||
|
- name: Assert target host already has agent binary when build is disabled
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- agent_svc_plus_binary_stat.stat.exists
|
||||||
|
- not agent_svc_plus_binary_stat.stat.isdir
|
||||||
|
fail_msg: >-
|
||||||
|
{{ agent_svc_plus_binary_path }} does not exist on the target host.
|
||||||
|
This role is configured to avoid clone/build on target. Provide
|
||||||
|
agent_svc_plus_binary_src, or set agent_svc_plus_release_tag so the role
|
||||||
|
can download {{ agent_svc_plus_release_asset_name }}, or enable
|
||||||
|
agent_svc_plus_build_on_target.
|
||||||
|
when: not (agent_svc_plus_build_on_target | bool)
|
||||||
|
|
||||||
- name: Ensure agent binary permissions
|
- name: Ensure agent binary permissions
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
|
|||||||
@ -15,7 +15,7 @@ xray_exporter_env_path: "{{ xray_exporter_env_dir }}/xray-exporter"
|
|||||||
|
|
||||||
xray_exporter_listen_addr: "127.0.0.1:8080"
|
xray_exporter_listen_addr: "127.0.0.1:8080"
|
||||||
xray_exporter_scrape_interval: "1m"
|
xray_exporter_scrape_interval: "1m"
|
||||||
xray_exporter_node_id: "node-xhttp.svc.plus"
|
xray_exporter_node_id: "{{ xray_exporter_node_id_custom | default(inventory_hostname, true) }}"
|
||||||
xray_exporter_env_name: "prod"
|
xray_exporter_env_name: "prod"
|
||||||
xray_exporter_stats_url: "http://127.0.0.1:49227/debug/vars"
|
xray_exporter_stats_url: "http://127.0.0.1:49227/debug/vars"
|
||||||
xray_exporter_stats_token: ""
|
xray_exporter_stats_token: ""
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user