Add Linux development station role with systemd services for code-server and OpenCode

- Implemented handlers for restarting code-server and OpenCode services.
- Created main tasks for setting up a Linux development station, including user management, package installation, and configuration.
- Added validation for OpenCode server credentials and code-server password.
- Introduced tool package management for npm, uv, and go installers.
- Developed templates for systemd service units for code-server and OpenCode.
- Included scripts for starting code-server and OpenCode with SSH agent support.
- Updated inventory and Terraform configuration to include new development host.
This commit is contained in:
2026-07-10 15:44:18 +02:00
parent e1ebe7d139
commit 59b2c10c43
24 changed files with 1523 additions and 28 deletions
+1
View File
@@ -1,5 +1,6 @@
[defaults]
inventory = ../inventory/hosts
allow_world_readable_tmpfiles = True
[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
@@ -0,0 +1,167 @@
hostname: dev-02.lab.alexpires.me
ansible_user: "{{ lookup('env', 'ANSIBLE_USER') }}"
ansible_host: "10.19.4.210"
linux_dev_station_service_user_enabled: true
linux_dev_station_service_user: "devstation"
linux_dev_station_service_home: "/home/devstation"
linux_dev_station_workspace_dir: "/home/devstation/projects"
linux_dev_station_scripts_dir: "/home/devstation/scripts"
linux_dev_station_logs_dir: "/home/devstation/.local/state/logs"
linux_dev_station_code_server_host: "0.0.0.0"
linux_dev_station_code_server_port: 3000
linux_dev_station_code_server_auth: "password"
linux_dev_station_opencode_server_host: "0.0.0.0"
linux_dev_station_opencode_server_port: 4096
linux_dev_station_manage_systemd: true
linux_dev_station_oh_my_zsh_enabled: true
linux_dev_station_code_server_extensions_enabled: true
linux_dev_station_code_server_extensions:
- ms-python.python
- ms-python.vscode-pylance
- ms-python.debugpy
- golang.go
- ms-azuretools.vscode-docker
- hashicorp.terraform
- opentofu.vscode-opentofu
- redhat.ansible
- redhat.vscode-yaml
- eamodio.gitlens
linux_dev_station_ssh_agent_enabled: true
linux_dev_station_mcp_bootstrap_commands: []
linux_dev_station_mcp_installers_enabled: true
linux_dev_station_mcp_installers:
- name: "chrome-devtools-mcp"
kind: "npm"
package: "chrome-devtools-mcp"
version: "latest"
- name: "playwright-mcp"
kind: "npm"
package: "@playwright/mcp"
version: "latest"
- name: "aws-documentation-mcp"
kind: "uv"
package: "awslabs.aws-documentation-mcp-server"
version: "latest"
binary: "awslabs.aws-documentation-mcp-server"
- name: "gitea-mcp"
kind: "go"
module: "gitea.com/gitea/gitea-mcp"
version: "latest"
binary: "gitea-mcp"
linux_dev_station_opencode_extra_environment:
GITEA_MCP_HOST: "https://code.alexpires.me"
GITEA_APP_TOKEN: "{{ lookup('env', 'GITEA_APP_TOKEN', default='') }}"
linux_dev_station_opencode_config:
"$schema": "https://opencode.ai/config.json"
provider:
alexpires-me:
name: "alexpires.me"
npm: "@ai-sdk/openai-compatible"
options:
baseURL: "https://open-webui.lab.alexpires.me/api"
apiKey: "{{ lookup('env', 'OPEN_WEBUI_API_KEY', default='') }}"
timeout: 3000000
chunkTimeout: 3000000
models:
gemma-4-12b-q8-0:
name: "gemma-4-12b-q8-0"
gemma-4-26b-a4b-ud-iq4-xs:
name: "gemma-4-26b-a4b-ud-iq4-xs"
gemma-4-31b-iq4-xs:
name: "gemma-4-31b-iq4-xs"
gpt-oss-20b-q4-0:
name: "gpt-oss-20b-q4-0"
gpt-oss-20b-q8-0:
name: "gpt-oss-20b-q8-0"
qwen3-5-35b-a3b-q4-k-m:
name: "qwen3-5-35b-a3b-q4-k-m"
qwen3-5-9b-q8-0:
name: "qwen3-5-9b-q8-0"
qwen3-6-27b-mtp-q4-0:
name: "qwen3-6-27b-mtp-q4-0"
qwen3-6-27b-q4-k-m:
name: "qwen3-6-27b-q4-k-m"
qwen3-6-35b-a3b-mtp-ud-q4-m:
name: "qwen3-6-35b-a3b-mtp-ud-q4-m"
qwen3-6-35b-a3b-ud-q4-k-m:
name: "qwen3-6-35b-a3b-ud-q4-k-m"
mcp:
deepwiki:
type: "remote"
url: "https://mcp.deepwiki.com/mcp"
enabled: true
terraform:
type: "remote"
url: "http://terraform-mcp.lab.alexpires.me/mcp"
enabled: true
opentofu:
type: "remote"
url: "https://mcp.opentofu.org/mcp"
enabled: true
microsoft_learn:
type: "remote"
url: "https://learn.microsoft.com/api/mcp"
enabled: true
mozilla_developer:
type: "remote"
url: "https://mcp.mdn.mozilla.net/"
enabled: true
chrome_devtools:
type: "local"
command:
- "npx"
- "-y"
- "chrome-devtools-mcp@latest"
- "--headless"
- "--isolated"
enabled: true
playwright:
type: "local"
command:
- "npx"
- "-y"
- "@playwright/mcp@latest"
- "--headless"
- "--isolated"
enabled: true
aws_documentation:
type: "local"
command:
- "uvx"
- "awslabs.aws-documentation-mcp-server@latest"
enabled: true
gitea:
type: "local"
command:
- "gitea-mcp"
- "-t"
- "stdio"
- "-H"
- "{env:GITEA_MCP_HOST}"
- "-T"
- "{env:GITEA_APP_TOKEN}"
enabled: true
compaction:
auto: true
prune: true
reserved: 32768
linux_dev_station_service_user_ssh_enabled: true
linux_dev_station_service_user_ssh_authorized_keys: []
# Firewall ports
ufw_enable: true
fw_allowed_ports:
- { rule: "allow", port: "22", proto: "tcp", from: "10.5.5.5/32" } # SSH Access (VPN device)
- { rule: "allow", port: "22", proto: "tcp", from: "10.19.4.0/24" } # SSH Access (local network)
- { rule: "allow", port: "3000", proto: "tcp", from: "10.19.4.0/24" } # code-server (local network only)
- { rule: "allow", port: "4096", proto: "tcp", from: "10.19.4.0/24" } # opencode (local network only)
- { rule: "allow", port: "9090", proto: "tcp", from: "10.19.4.0/24" } # opencode (local network only)
+41 -26
View File
@@ -118,25 +118,25 @@ llama_server_devices:
- "/dev/dri:/dev/dri:rw"
llama_server_models:
# TODO: replace revision values with pinned commit SHAs for strict reproducibility.
# # TODO: replace revision values with pinned commit SHAs for strict reproducibility.
- name: "gemma-4-12b-q8-0"
model_id: "unsloth/gemma-4-12b-it-GGUF"
quant: "Q8_0"
revision: "main"
preset:
ctx-size: 98304
# preset:
# ctx-size: 98304
- name: "gemma-4-26b-a4b-ud-iq4-xs"
model_id: "unsloth/gemma-4-26B-A4B-it-GGUF"
quant: "UD-IQ4_XS"
revision: "main"
preset:
ctx-size: 98304
# preset:
# ctx-size: 98304
- name: "gemma-4-31b-iq4-xs"
model_id: "unsloth/gemma-4-31B-it-GGUF"
quant: "IQ4_XS"
revision: "main"
preset:
ctx-size: 98304
# preset:
# ctx-size: 98304
- name: "gpt-oss-20b-q4-0"
model_id: "unsloth/gpt-oss-20b-GGUF"
quant: "Q4_0"
@@ -149,44 +149,56 @@ llama_server_models:
model_id: "unsloth/Qwen3.5-27B-GGUF"
quant: "Q4_K_M"
revision: "main"
preset:
ctx-size: 98304
# preset:
# ctx-size: 98304
- name: "qwen3-5-35b-a3b-q4-k-m"
model_id: "unsloth/Qwen3.5-35B-A3B-GGUF"
quant: "Q4_K_M"
revision: "main"
preset:
ctx-size: 98304
# preset:
# ctx-size: 98304
- name: "qwen3-5-9b-q8-0"
model_id: "unsloth/Qwen3.5-9B-GGUF"
quant: "Q8_0"
revision: "main"
preset:
ctx-size: 98304
# preset:
# ctx-size: 98304
- name: "qwen3-6-27b-q4-k-m"
model_id: "unsloth/Qwen3.6-27B-GGUF"
quant: "Q4_K_M"
revision: "main"
preset:
ctx-size: 98304
# preset:
# ctx-size: 98304
- name: "qwen3-6-35b-a3b-ud-q4-k-m"
model_id: "unsloth/Qwen3.6-35B-A3B-GGUF"
quant: "UD-Q4_K_M"
revision: "main"
# preset:
# ctx-size: 98304
- name: "qwen3-6-27b-mtp-q4-0"
model_id: "unsloth/Qwen3.6-27B-MTP-GGUF"
quant: "Q4_0"
revision: "main"
preset:
ctx-size: 98304
temperature: 0.6
top_p: 0.95
top_k: 20
min_p: 0.0
presence_penalty: 0.0
repeat_penalty: 1.0
# ctx-size: 98304
- name: "qwen3-6-35b-a3b-mtp-ud-q4-m"
model_id: "unsloth/Qwen3.6-35B-A3B-MTP-GGUF"
quant: "UD-Q4_K_M"
revision: "main"
preset:
ctx-size: 98304
- name: "qwen3-6-35b-a3b-ud-q4-k-m"
model_id: "unsloth/Qwen3.6-35B-A3B-GGUF"
quant: "UD-Q4_K_M"
revision: "main"
preset:
ctx-size: 98304
temperature: 0.6
top_p: 0.95
top_k: 20
min_p: 0.0
presence_penalty: 0.0
repeat_penalty: 1.0
# ctx-size: 98304
llama_server_argv_extra:
[
@@ -195,15 +207,15 @@ llama_server_argv_extra:
"-np",
-1,
"-b",
1024,
2048,
"-ub",
512,
"-fa",
"on",
"-ctk",
"q4_0",
"-ctv",
"q8_0",
"-ctv",
"q4_0",
"-cram",
-1,
"-sm",
@@ -216,11 +228,14 @@ llama_server_argv_extra:
"off",
"-mg",
1,
"--timeout",
1800,
"--mmap",
"--spec-type",
"draft-mtp",
"--spec-draft-n-max",
2,
"--swa-full",
]
auto_suspend_enabled: true
@@ -76,6 +76,8 @@ macos_dev_station_opencode_config:
options:
baseURL: "https://open-webui.lab.alexpires.me/api"
apiKey: "{{ lookup('env', 'OPEN_WEBUI_API_KEY', default='') }}"
timeout: 3000000
chunkTimeout: 3000000
models:
gemma-4-12b-q8-0:
name: "gemma-4-12b-q8-0"
+14
View File
@@ -0,0 +1,14 @@
---
- name: Configure Linux dev station
hosts: linux_dev
gather_facts: true
pre_tasks:
- name: Ensure target is Fedora
ansible.builtin.assert:
that:
- ansible_facts['distribution'] == "Fedora"
fail_msg: "This playbook supports Fedora hosts only"
roles:
- linux_dev_station
@@ -0,0 +1 @@
cis_logind_conf: {}
+1 -2
View File
@@ -11,5 +11,4 @@
notify:
- Reload systemd
tags:
- systemd
- logind
- roles::cis::logind
@@ -6,3 +6,6 @@ KillExcludeUsers=root
IdleAction=lock
IdleActionSec=15min
RemoveIPC=yes
{% for key, value in cis_logind_conf.items() %}
{{ key }}={{ value }}
{% endfor %}
@@ -0,0 +1,103 @@
---
linux_dev_station_service_user_enabled: true
linux_dev_station_service_user: "devstation"
linux_dev_station_service_shell: "/usr/sbin/nologin"
linux_dev_station_service_user_ssh_enabled: false
linux_dev_station_service_user_ssh_shell: "/bin/zsh"
linux_dev_station_service_user_ssh_authorized_keys: []
linux_dev_station_service_home: "/home/{{ linux_dev_station_service_user }}"
linux_dev_station_workspace_dir: "{{ linux_dev_station_service_home }}/projects"
linux_dev_station_scripts_dir: "{{ linux_dev_station_service_home }}/scripts"
linux_dev_station_logs_dir: "{{ linux_dev_station_service_home }}/.local/state/logs"
linux_dev_station_systemd_unit_dir: "/etc/systemd/system"
linux_dev_station_path: >-
{{ linux_dev_station_service_home }}/.local/bin:{{ linux_dev_station_service_home }}/go/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/usr/local/bin
linux_dev_station_dnf_packages:
- git
- tmux
- wget
- curl
- jq
- openssl
- zsh
- golang
- ripgrep
- fzf
linux_dev_station_tool_packages:
- name: uv
install_script_url: "https://astral.sh/uv/install.sh"
- name: node
manager: fnm
- name: rust
manager: rustup
- name: code-server
version: "4.127.0"
sha256: ""
- name: opencode
version: "1.17.18"
sha256: ""
linux_dev_station_code_server_host: "0.0.0.0"
linux_dev_station_code_server_port: 3000
linux_dev_station_code_server_auth: "password"
linux_dev_station_code_server_password: "{{ lookup('env', 'CODE_SERVER_PASSWORD', default='') }}"
linux_dev_station_code_server_workspace: "{{ linux_dev_station_workspace_dir }}"
linux_dev_station_code_server_extensions_enabled: true
linux_dev_station_code_server_extensions_fail_on_error: false
linux_dev_station_code_server_extensions:
- ms-python.python
- ms-python.vscode-pylance
- ms-azuretools.vscode-docker
- hashicorp.terraform
- redhat.ansible
- eamodio.gitlens
linux_dev_station_copilot_enabled: false
linux_dev_station_copilot_extensions:
- GitHub.copilot
- GitHub.copilot-chat
linux_dev_station_copilot_fail_on_error: false
linux_dev_station_oh_my_zsh_enabled: true
linux_dev_station_oh_my_zsh_install_url: "https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh"
linux_dev_station_npm_packages: []
linux_dev_station_git_init_default_branch: "main"
linux_dev_station_git_pull_rebase: false
linux_dev_station_ssh_key_path: "{{ ansible_facts['env']['HOME'] }}/.ssh/id_ed25519"
linux_dev_station_ssh_key_comment: "dev-02"
linux_dev_station_code_server_systemd_enabled: true
linux_dev_station_opencode_systemd_enabled: true
linux_dev_station_opencode_server_host: "0.0.0.0"
linux_dev_station_opencode_server_port: 4096
linux_dev_station_opencode_server_username: "{{ lookup('env', 'OPENCODE_SERVER_USERNAME', default='') }}"
linux_dev_station_opencode_server_password: "{{ lookup('env', 'OPENCODE_SERVER_PASSWORD', default='') }}"
linux_dev_station_opencode_extra_environment: {}
linux_dev_station_opencode_command: "opencode serve --hostname {{ linux_dev_station_opencode_server_host }} --port {{ linux_dev_station_opencode_server_port }}"
linux_dev_station_opencode_config: {}
linux_dev_station_opencode_config_path: "{{ linux_dev_station_service_home }}/.config/opencode/opencode.json"
# MCP installer definitions
# Supported kinds:
# - npm: package + optional version
# - uv: package + optional version + binary
# - go: module + optional version + binary
linux_dev_station_mcp_installers_enabled: true
linux_dev_station_mcp_installers: []
# Legacy ad-hoc hook kept for backward compatibility.
linux_dev_station_mcp_bootstrap_commands: []
linux_dev_station_manage_systemd: true
# SSH agent
linux_dev_station_ssh_agent_enabled: false
linux_dev_station_ssh_agent_keys:
- "{{ linux_dev_station_service_home }}/.ssh/id_ed25519"
linux_dev_station_ssh_agent_passphrase: "{{ lookup('env', 'SSH_KEY_PASSPHRASE', default='') }}"
linux_dev_station_ssh_agent_socket: "{{ linux_dev_station_service_home }}/.ssh/ssh-agent.sock"
+164
View File
@@ -0,0 +1,164 @@
#!/usr/bin/env bash
# ansible: linux_dev_station install-copilot.sh
# Installs GitHub Copilot and Copilot Chat extensions into code-server
# with engine-compatible versions resolved from the VS Marketplace API.
set -euo pipefail
get_engine_version() {
local version_output
version_output=$(code-server --version | head -n1)
if echo "$version_output" | grep -q "with Code"; then
echo "$version_output" | sed -n 's/.*with Code \([0-9.]*\).*/\1/p'
else
echo "$version_output" | awk '{print $1}'
fi
}
get_user_data_dir() {
local process_info
process_info=$(ps aux 2>/dev/null | grep -v grep | grep "code-server" | head -n 1) ||
process_info=$(ps -ef 2>/dev/null | grep -v grep | grep "code-server" | head -n 1)
if [ -n "$process_info" ]; then
local dir
dir=$(echo "$process_info" | grep -o -- '--user-data-dir=[^ ]*' | sed 's/--user-data-dir=//')
if [ -n "$dir" ]; then
echo "$dir"; return
fi
dir=$(echo "$process_info" | grep -o -- '--user-data-dir [^ ]*' | sed 's/--user-data-dir //')
if [ -n "$dir" ]; then
echo "$dir"; return
fi
fi
}
find_compatible_version() {
local extension_id="$1"
local engine_version="$2"
local response
response=$(curl -s -X POST "https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery" \
-H "Content-Type: application/json" \
-H "Accept: application/json;api-version=3.0-preview.1" \
-d "{
\"filters\": [{
\"criteria\": [
{\"filterType\": 7, \"value\": \"$extension_id\"},
{\"filterType\": 12, \"value\": \"4096\"}
],
\"pageSize\": 50
}],
\"flags\": 4112
}")
echo "$response" | jq -r --arg engine_version "$engine_version" '
.results[0].extensions[0].versions[] |
select(.version | test("^[0-9]+\\.[0-9]+\\.[0-9]*$")) |
select(all(.properties[]; .key != "Microsoft.VisualStudio.Code.PreRelease" or .value != "true")) |
{
version: .version,
engine: (.properties[] | select(.key == "Microsoft.VisualStudio.Code.Engine") | .value)
} |
select(.engine | ltrimstr("^") | split(".") |
map(split("-")[0] | tonumber? // 0) as $engine_parts |
($engine_version | split(".") | map(split("-")[0] | tonumber? // 0)) as $server_parts |
(
($engine_parts[0] // 0) < $server_parts[0] or
(($engine_parts[0] // 0) == $server_parts[0] and ($engine_parts[1] // 0) < $server_parts[1]) or
(($engine_parts[0] // 0) == $server_parts[0] and ($engine_parts[1] // 0) == $server_parts[1] and ($engine_parts[2] // 0) <= $server_parts[2])
)
) |
.version' | head -n 1
}
install_extension() {
local extension_id="$1"
local version="$2"
local user_data_dir="$3"
local extension_name
extension_name=$(echo "$extension_id" | cut -d'.' -f2)
local temp_dir="/tmp/code-extensions"
echo "Installing $extension_id v$version..."
mkdir -p "$temp_dir"
echo " Downloading..."
curl -L "https://marketplace.visualstudio.com/_apis/public/gallery/publishers/GitHub/vsextensions/$extension_name/$version/vspackage" \
-o "$temp_dir/$extension_name.vsix.gz"
if [ ! -f "$temp_dir/$extension_name.vsix.gz" ]; then
echo " [SKIP] Download failed for $extension_id"
return 1
fi
if command -v gunzip >/dev/null 2>&1; then
gunzip -f "$temp_dir/$extension_name.vsix.gz"
else
gzip -df "$temp_dir/$extension_name.vsix.gz"
fi
if [ -n "$user_data_dir" ]; then
code-server --user-data-dir="$user_data_dir" --force --install-extension "$temp_dir/$extension_name.vsix"
else
code-server --force --install-extension "$temp_dir/$extension_name.vsix"
fi
rm -f "$temp_dir/$extension_name.vsix"
echo " [OK] $extension_id installed successfully!"
return 0
}
check_dependencies() {
local missing_deps=()
for cmd in curl jq code-server; do
if ! command -v "$cmd" >/dev/null 2>&1; then
missing_deps+=("$cmd")
fi
done
if ! command -v gunzip >/dev/null 2>&1 && ! command -v gzip >/dev/null 2>&1; then
missing_deps+=("gunzip/gzip")
fi
if [ "${#missing_deps[@]}" -gt 0 ]; then
echo "Error: Missing required dependencies: ${missing_deps[*]}"
echo "Please install the missing dependencies and try again."
exit 1
fi
}
echo "========================================"
echo " GitHub Copilot Extensions Installer"
echo "========================================"
echo ""
check_dependencies
ENGINE_VERSION="$(get_engine_version)"
if [ -z "$ENGINE_VERSION" ]; then
echo "Error: Could not extract engine version from code-server"
exit 1
fi
echo "Detected code-server engine version: $ENGINE_VERSION"
USER_DATA_DIR="$(get_user_data_dir)"
if [ -n "$USER_DATA_DIR" ]; then
echo "Detected user-data-dir: $USER_DATA_DIR"
fi
echo ""
EXTENSIONS="GitHub.copilot GitHub.copilot-chat"
FAILED=0
for ext in $EXTENSIONS; do
echo "Processing $ext..."
version="$(find_compatible_version "$ext" "$ENGINE_VERSION")"
if [ -z "$version" ]; then
echo " [SKIP] No compatible version found for $ext"
FAILED="$((FAILED + 1))"
else
echo " Found compatible version: $version"
if ! install_extension "$ext" "$version" "$USER_DATA_DIR"; then
FAILED="$((FAILED + 1))"
fi
fi
echo ""
done
echo "========================================"
if [ $FAILED -eq 0 ]; then
echo "[OK] All extensions installed successfully!"
rm -rf /tmp/code-extensions
else
echo "[WARN] Completed with $FAILED error(s)"
exit 1
fi
@@ -0,0 +1,24 @@
---
- name: Reload and restart code-server
become: true
ansible.builtin.systemd:
name: code-server
state: restarted
daemon_reload: true
when:
- linux_dev_station_manage_systemd | bool
- linux_dev_station_code_server_systemd_enabled | bool
listen: Restart code-server
- name: Reload and restart opencode
become: true
ansible.builtin.systemd:
name: opencode
state: restarted
daemon_reload: true
when:
- linux_dev_station_manage_systemd | bool
- linux_dev_station_opencode_systemd_enabled | bool
listen: Restart opencode
@@ -0,0 +1,452 @@
---
- name: Ensure host is Fedora
ansible.builtin.assert:
that:
- ansible_facts['distribution'] == "Fedora"
fail_msg: "Role linux_dev_station supports Fedora hosts only"
- name: Ensure OpenCode server credentials are provided
ansible.builtin.assert:
that:
- linux_dev_station_opencode_server_username | length > 0
- linux_dev_station_opencode_server_password | length > 0
fail_msg: >-
Set OPENCODE_SERVER_USERNAME and OPENCODE_SERVER_PASSWORD
in environment (recommended via sectool)
before running the playbook.
when: linux_dev_station_opencode_systemd_enabled | bool
- name: Ensure code-server password is provided
ansible.builtin.assert:
that:
- linux_dev_station_code_server_password | length > 0
fail_msg: >-
Set CODE_SERVER_PASSWORD in environment
(recommended via sectool) before running the playbook.
when:
- linux_dev_station_code_server_systemd_enabled | bool
- linux_dev_station_code_server_auth == "password"
- name: Install dnf packages
become: true
ansible.builtin.dnf:
name: "{{ linux_dev_station_dnf_packages }}"
state: present
- name: Ensure dedicated service user exists
become: true
ansible.builtin.user:
name: "{{ linux_dev_station_service_user }}"
shell: >-
{{ (linux_dev_station_service_user_ssh_enabled | bool) |
ternary(linux_dev_station_service_user_ssh_shell, linux_dev_station_service_shell) }}
home: "{{ linux_dev_station_service_home }}"
create_home: true
state: present
when: linux_dev_station_service_user_enabled | bool
- name: Ensure service user cache directory exists for tool installs
become: true
ansible.builtin.file:
path: "{{ linux_dev_station_service_home }}/.cache"
state: directory
mode: "0750"
owner: "{{ linux_dev_station_service_user }}"
group: "{{ linux_dev_station_service_user }}"
when: linux_dev_station_service_user_enabled | bool
- name: Install tool-managed packages
ansible.builtin.include_tasks: tool_packages.yml
- name: Discover npm executable path
become: true
become_user: "{{ linux_dev_station_service_user }}"
ansible.builtin.command: /bin/sh -lc "command -v npm"
register: __linux_dev_station_npm_path
changed_when: false
failed_when: false
environment:
PATH: "{{ linux_dev_station_path }}"
when:
- linux_dev_station_service_user_enabled | bool
- linux_dev_station_npm_packages | length > 0
- name: Install global npm packages
become: true
become_user: "{{ linux_dev_station_service_user }}"
community.general.npm:
name: "{{ item.name }}"
version: "{{ item.version }}"
executable: "{{ __linux_dev_station_npm_path.stdout | trim }}"
global: true
environment:
PATH: "{{ linux_dev_station_path }}"
loop: "{{ linux_dev_station_npm_packages }}"
when:
- linux_dev_station_service_user_enabled | bool
- linux_dev_station_npm_packages | length > 0
- __linux_dev_station_npm_path.rc | default(1) == 0
- name: Ensure required user directories exist
become: true
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: "0750"
owner: "{{ linux_dev_station_service_user }}"
group: "{{ linux_dev_station_service_user }}"
loop:
- "{{ linux_dev_station_service_home }}"
- "{{ linux_dev_station_workspace_dir }}"
- "{{ linux_dev_station_scripts_dir }}"
- "{{ linux_dev_station_logs_dir }}"
- name: Ensure service home permissions are restrictive
become: true
ansible.builtin.file:
path: "{{ linux_dev_station_service_home }}"
state: directory
owner: "{{ linux_dev_station_service_user }}"
group: "{{ linux_dev_station_service_user }}"
mode: "0700"
- name: Ensure service user config directories exist
become: true
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: "0750"
owner: "{{ linux_dev_station_service_user }}"
group: "{{ linux_dev_station_service_user }}"
loop:
- "{{ linux_dev_station_service_home }}/.local"
- "{{ linux_dev_station_service_home }}/.local/bin"
- "{{ linux_dev_station_service_home }}/.local/lib"
- "{{ linux_dev_station_service_home }}/.local/lib/node_modules"
- "{{ linux_dev_station_service_home }}/.config"
- "{{ linux_dev_station_service_home }}/.config/opencode"
when: linux_dev_station_service_user_enabled | bool
- name: Ensure OpenCode config object is valid when provided
ansible.builtin.assert:
that:
- linux_dev_station_opencode_config is mapping
fail_msg: "linux_dev_station_opencode_config must be a mapping (dictionary)."
- name: Render OpenCode config from host vars
become: true
ansible.builtin.template:
src: opencode.json.j2
dest: "{{ linux_dev_station_opencode_config_path }}"
owner: "{{ linux_dev_station_service_user }}"
group: "{{ linux_dev_station_service_user }}"
mode: "0600"
notify: Restart opencode
when:
- linux_dev_station_service_user_enabled | bool
- linux_dev_station_opencode_config | length > 0
- name: Download Oh My Zsh installer
become: true
ansible.builtin.get_url:
url: "{{ linux_dev_station_oh_my_zsh_install_url }}"
dest: "{{ linux_dev_station_service_home }}/.cache/ohmyzsh-install.sh"
owner: "{{ linux_dev_station_service_user }}"
group: "{{ linux_dev_station_service_user }}"
mode: "0755"
when:
- linux_dev_station_service_user_enabled | bool
- linux_dev_station_oh_my_zsh_enabled | bool
- name: Install Oh My Zsh for service user
become: true
become_user: "{{ linux_dev_station_service_user }}"
ansible.builtin.command: >-
/bin/sh {{ linux_dev_station_service_home }}/.cache/ohmyzsh-install.sh --unattended
args:
creates: "{{ linux_dev_station_service_home }}/.oh-my-zsh"
environment:
HOME: "{{ linux_dev_station_service_home }}"
USER: "{{ linux_dev_station_service_user }}"
RUNZSH: "no"
CHSH: "no"
KEEP_ZSHRC: "yes"
when:
- linux_dev_station_service_user_enabled | bool
- linux_dev_station_oh_my_zsh_enabled | bool
- name: Ensure .zshrc has ssh-agent startup for devstation
become: true
ansible.builtin.blockinfile:
path: "{{ linux_dev_station_service_home }}/.zshrc"
block: |
# ansible: linux_dev_station ssh-agent
export SSH_AUTH_SOCK="{{ linux_dev_station_ssh_agent_socket }}"
if [ -S "$SSH_AUTH_SOCK" ] && ssh-add -l &>/dev/null; then
# Agent socket exists and has keys — nothing to do
true
else
if [ -S "$SSH_AUTH_SOCK" ]; then
# Socket exists but agent is dead or has no keys — restart
rm -f "$SSH_AUTH_SOCK"
eval "$(ssh-agent -a "$SSH_AUTH_SOCK" 2>/dev/null)" || true
else
# No socket — start fresh agent
eval "$(ssh-agent -a "$SSH_AUTH_SOCK" 2>/dev/null)" || true
fi
if [ -f "{{ linux_dev_station_scripts_dir }}/ssh-agent-start.sh" ]; then
source "{{ linux_dev_station_scripts_dir }}/ssh-agent-start.sh"
fi
fi
marker: "# {mark} ansible: linux_dev_station ssh-agent"
create: true
mode: "0644"
owner: "{{ linux_dev_station_service_user }}"
group: "{{ linux_dev_station_service_user }}"
when:
- linux_dev_station_service_user_enabled | bool
- linux_dev_station_ssh_agent_enabled | bool
- name: Install managed MCP server runtimes
ansible.builtin.include_tasks: mcp_installers.yml
when:
- linux_dev_station_service_user_enabled | bool
- linux_dev_station_mcp_installers_enabled | bool
- linux_dev_station_mcp_installers | length > 0
- name: Run MCP bootstrap commands
become: true
become_user: "{{ linux_dev_station_service_user }}"
ansible.builtin.shell: "{{ item }}"
register: __linux_dev_station_mcp_bootstrap_result
changed_when: __linux_dev_station_mcp_bootstrap_result.rc == 0
args:
executable: /bin/sh
environment:
HOME: "{{ linux_dev_station_service_home }}"
USER: "{{ linux_dev_station_service_user }}"
PATH: "{{ linux_dev_station_path }}"
loop: "{{ linux_dev_station_mcp_bootstrap_commands }}"
when:
- linux_dev_station_service_user_enabled | bool
- linux_dev_station_mcp_bootstrap_commands | length > 0
- name: List installed code-server extensions for service user
become: true
become_user: "{{ linux_dev_station_service_user }}"
ansible.builtin.command: code-server --list-extensions
register: __linux_dev_station_code_server_extensions_installed
changed_when: false
environment:
HOME: "{{ linux_dev_station_service_home }}"
USER: "{{ linux_dev_station_service_user }}"
PATH: "{{ linux_dev_station_path }}"
when:
- linux_dev_station_service_user_enabled | bool
- linux_dev_station_code_server_extensions_enabled | bool
- linux_dev_station_code_server_extensions | length > 0
- name: Install base code-server extensions for service user
become: true
become_user: "{{ linux_dev_station_service_user }}"
ansible.builtin.command: >-
code-server --install-extension {{ item }}
register: __linux_dev_station_code_server_extension_install
changed_when: __linux_dev_station_code_server_extension_install.rc == 0
failed_when: >-
(linux_dev_station_code_server_extensions_fail_on_error | bool)
and (__linux_dev_station_code_server_extension_install.rc != 0)
environment:
HOME: "{{ linux_dev_station_service_home }}"
USER: "{{ linux_dev_station_service_user }}"
PATH: "{{ linux_dev_station_path }}"
notify: Restart code-server
loop: "{{ linux_dev_station_code_server_extensions }}"
when:
- linux_dev_station_service_user_enabled | bool
- linux_dev_station_code_server_extensions_enabled | bool
- item not in (__linux_dev_station_code_server_extensions_installed.stdout_lines | default([]))
- name: Report skipped code-server extensions that are unavailable
ansible.builtin.debug:
msg: >-
Skipping unavailable code-server extension '{{ item.item }}':
{{ (item.stderr_lines | default([])) | join(' ') }}
loop: "{{ __linux_dev_station_code_server_extension_install.results | default([]) }}"
when:
- linux_dev_station_service_user_enabled | bool
- linux_dev_station_code_server_extensions_enabled | bool
- not (linux_dev_station_code_server_extensions_fail_on_error | bool)
- item.rc is defined
- item.rc != 0
- name: Copy Copilot installer script
become: true
ansible.builtin.copy:
src: install-copilot.sh
dest: "{{ linux_dev_station_service_home }}/.cache/install-copilot.sh"
owner: "{{ linux_dev_station_service_user }}"
group: "{{ linux_dev_station_service_user }}"
mode: "0755"
when:
- linux_dev_station_service_user_enabled | bool
- linux_dev_station_copilot_enabled | bool
- name: Install GitHub Copilot extensions via compatible VSIX
become: true
become_user: "{{ linux_dev_station_service_user }}"
ansible.builtin.command: "{{ linux_dev_station_service_home }}/.cache/install-copilot.sh"
register: __linux_dev_station_copilot_install_result
changed_when: __linux_dev_station_copilot_install_result.rc == 0
failed_when: >-
(linux_dev_station_copilot_fail_on_error | bool)
and (__linux_dev_station_copilot_install_result.rc != 0)
environment:
HOME: "{{ linux_dev_station_service_home }}"
USER: "{{ linux_dev_station_service_user }}"
PATH: "{{ linux_dev_station_path }}"
when:
- linux_dev_station_service_user_enabled | bool
- linux_dev_station_copilot_enabled | bool
- name: Ensure devstation SSH directory exists when SSH access is enabled
become: true
ansible.builtin.file:
path: "{{ linux_dev_station_service_home }}/.ssh"
state: directory
owner: "{{ linux_dev_station_service_user }}"
group: "{{ linux_dev_station_service_user }}"
mode: "0700"
when:
- linux_dev_station_service_user_enabled | bool
- linux_dev_station_service_user_ssh_enabled | bool
- name: Ensure devstation authorized keys are present when SSH access is enabled
become: true
ansible.posix.authorized_key:
user: "{{ linux_dev_station_service_user }}"
key: "{{ item }}"
state: present
loop: "{{ linux_dev_station_service_user_ssh_authorized_keys }}"
when:
- linux_dev_station_service_user_enabled | bool
- linux_dev_station_service_user_ssh_enabled | bool
- name: Render ssh-agent starter script
become: true
ansible.builtin.template:
src: ssh-agent-start.sh.j2
dest: "{{ linux_dev_station_scripts_dir }}/ssh-agent-start.sh"
mode: "0755"
owner: "{{ linux_dev_station_service_user }}"
group: "{{ linux_dev_station_service_user }}"
when:
- linux_dev_station_service_user_enabled | bool
- linux_dev_station_ssh_agent_enabled | bool
- name: Ensure ssh_config has ssh-agent settings for devstation
become: true
ansible.builtin.blockinfile:
path: "{{ linux_dev_station_service_home }}/.ssh/config"
block: |
Host *
AddKeysToAgent yes
ServerAliveInterval 60
ServerAliveCountMax 3
marker: "# {mark} ansible: linux_dev_station ssh-agent"
create: true
mode: "0600"
owner: "{{ linux_dev_station_service_user }}"
group: "{{ linux_dev_station_service_user }}"
when:
- linux_dev_station_service_user_enabled | bool
- linux_dev_station_ssh_agent_enabled | bool
- name: Generate SSH key if missing
ansible.builtin.command: >-
ssh-keygen -t ed25519 -C {{ linux_dev_station_ssh_key_comment }}
-f {{ linux_dev_station_ssh_key_path }} -N ''
args:
creates: "{{ linux_dev_station_ssh_key_path }}"
- name: Configure Git init default branch
community.general.git_config:
name: init.defaultBranch
scope: global
value: "{{ linux_dev_station_git_init_default_branch }}"
- name: Configure Git pull.rebase
community.general.git_config:
name: pull.rebase
scope: global
value: "{{ linux_dev_station_git_pull_rebase | ternary('true', 'false') }}"
- name: Render code-server launcher script
become: true
ansible.builtin.template:
src: start-ide.sh.j2
dest: "{{ linux_dev_station_scripts_dir }}/start-ide.sh"
mode: "0755"
owner: "{{ linux_dev_station_service_user }}"
group: "{{ linux_dev_station_service_user }}"
- name: Render opencode launcher script
become: true
ansible.builtin.template:
src: start-opencode.sh.j2
dest: "{{ linux_dev_station_scripts_dir }}/start-opencode.sh"
mode: "0755"
owner: "{{ linux_dev_station_service_user }}"
group: "{{ linux_dev_station_service_user }}"
- name: Render tmux configuration
ansible.builtin.copy:
dest: "{{ ansible_facts['env']['HOME'] }}/.tmux.conf"
mode: "0644"
content: |
set -g mouse on
set -g history-limit 100000
- name: Render code-server systemd unit
become: true
ansible.builtin.template:
src: code-server.service.j2
dest: "{{ linux_dev_station_systemd_unit_dir }}/code-server.service"
mode: "0644"
when: linux_dev_station_code_server_systemd_enabled | bool
notify: Restart code-server
- name: Render opencode systemd unit
become: true
ansible.builtin.template:
src: opencode.service.j2
dest: "{{ linux_dev_station_systemd_unit_dir }}/opencode.service"
mode: "0644"
when: linux_dev_station_opencode_systemd_enabled | bool
notify: Restart opencode
- name: Ensure code-server service is started and enabled
become: true
ansible.builtin.systemd:
name: code-server
state: started
enabled: true
daemon_reload: true
when:
- linux_dev_station_manage_systemd | bool
- linux_dev_station_code_server_systemd_enabled | bool
- name: Ensure opencode service is started and enabled
become: true
ansible.builtin.systemd:
name: opencode
state: started
enabled: true
daemon_reload: true
when:
- linux_dev_station_manage_systemd | bool
- linux_dev_station_opencode_systemd_enabled | bool
- name: Ensure systemd services are started
ansible.builtin.meta: flush_handlers
when: linux_dev_station_manage_systemd | bool
@@ -0,0 +1,138 @@
---
- name: Ensure MCP installer definitions list is valid
ansible.builtin.assert:
that:
- linux_dev_station_mcp_installers is iterable
fail_msg: "linux_dev_station_mcp_installers must be a list of installer definitions."
- name: Validate MCP installer kind values
ansible.builtin.assert:
that:
- item.kind is defined
- item.kind in ['npm', 'uv', 'go']
fail_msg: >-
MCP installer '{{ item.name | default('unnamed') }}'
has unsupported kind '{{ item.kind | default('undefined') }}'.
Supported: npm, uv, go.
loop: "{{ linux_dev_station_mcp_installers }}"
when: item.enabled | default(true) | bool
- name: Validate npm MCP installers
ansible.builtin.assert:
that:
- item.package is defined
- item.package | length > 0
fail_msg: "npm MCP installer '{{ item.name | default('unnamed') }}' requires 'package'."
loop: "{{ linux_dev_station_mcp_installers }}"
when:
- item.enabled | default(true) | bool
- item.kind == 'npm'
- name: Validate uv MCP installers
ansible.builtin.assert:
that:
- item.package is defined
- item.package | length > 0
- item.binary is defined
- item.binary | length > 0
fail_msg: "uv MCP installer '{{ item.name | default('unnamed') }}' requires 'package' and 'binary'."
loop: "{{ linux_dev_station_mcp_installers }}"
when:
- item.enabled | default(true) | bool
- item.kind == 'uv'
- name: Validate go MCP installers
ansible.builtin.assert:
that:
- item.module is defined
- item.module | length > 0
- item.binary is defined
- item.binary | length > 0
fail_msg: "go MCP installer '{{ item.name | default('unnamed') }}' requires 'module' and 'binary'."
loop: "{{ linux_dev_station_mcp_installers }}"
when:
- item.enabled | default(true) | bool
- item.kind == 'go'
- name: Discover npm executable path for MCP installers
become: true
become_user: "{{ linux_dev_station_service_user }}"
ansible.builtin.command: /bin/sh -lc "command -v npm"
register: __linux_dev_station_mcp_npm_path
changed_when: false
failed_when: false
environment:
PATH: "{{ linux_dev_station_path }}"
when: (linux_dev_station_mcp_installers | selectattr('kind', 'equalto', 'npm') | list | length) > 0
- name: Install npm MCP packages for service user
become: true
become_user: "{{ linux_dev_station_service_user }}"
ansible.builtin.command:
argv:
- "{{ __linux_dev_station_mcp_npm_path.stdout | trim }}"
- install
- --prefix
- "{{ linux_dev_station_service_home }}/.local"
- --global
- "{{ item.package }}@{{ item.version | default('latest') }}"
args:
creates: "{{ linux_dev_station_service_home }}/.local/lib/node_modules/{{ item.package }}"
register: __linux_dev_station_mcp_npm_install
changed_when: __linux_dev_station_mcp_npm_install.rc == 0
environment:
HOME: "{{ linux_dev_station_service_home }}"
USER: "{{ linux_dev_station_service_user }}"
PATH: "{{ linux_dev_station_path }}"
notify: Restart opencode
loop: "{{ linux_dev_station_mcp_installers }}"
when:
- item.enabled | default(true) | bool
- item.kind == 'npm'
- __linux_dev_station_mcp_npm_path.rc | default(1) == 0
- name: Install uv MCP tools for service user
become: true
become_user: "{{ linux_dev_station_service_user }}"
ansible.builtin.command:
argv:
- uv
- tool
- install
- "{{ item.package }}@{{ item.version | default('latest') }}"
args:
creates: "{{ linux_dev_station_service_home }}/.local/bin/{{ item.binary }}"
register: __linux_dev_station_mcp_uv_install
changed_when: __linux_dev_station_mcp_uv_install.rc == 0
environment:
HOME: "{{ linux_dev_station_service_home }}"
USER: "{{ linux_dev_station_service_user }}"
PATH: "{{ linux_dev_station_path }}"
notify: Restart opencode
loop: "{{ linux_dev_station_mcp_installers }}"
when:
- item.enabled | default(true) | bool
- item.kind == 'uv'
- name: Install go MCP binaries for service user
become: true
become_user: "{{ linux_dev_station_service_user }}"
ansible.builtin.command:
argv:
- go
- install
- "{{ item.module }}@{{ item.version | default('latest') }}"
args:
creates: "{{ linux_dev_station_service_home }}/.local/bin/{{ item.binary }}"
register: __linux_dev_station_mcp_go_install
changed_when: __linux_dev_station_mcp_go_install.rc == 0
environment:
HOME: "{{ linux_dev_station_service_home }}"
USER: "{{ linux_dev_station_service_user }}"
PATH: "{{ linux_dev_station_path }}"
GOBIN: "{{ linux_dev_station_service_home }}/.local/bin"
notify: Restart opencode
loop: "{{ linux_dev_station_mcp_installers }}"
when:
- item.enabled | default(true) | bool
- item.kind == 'go'
@@ -0,0 +1,268 @@
---
- name: Ensure tool packages list is valid
ansible.builtin.assert:
that:
- linux_dev_station_tool_packages is iterable
fail_msg: "linux_dev_station_tool_packages must be a list of tool definitions."
- name: Download uv installer
become: true
ansible.builtin.get_url:
url: https://astral.sh/uv/install.sh
dest: "{{ linux_dev_station_service_home }}/.cache/uv-install.sh"
owner: "{{ linux_dev_station_service_user }}"
group: "{{ linux_dev_station_service_user }}"
mode: "0755"
when:
- linux_dev_station_service_user_enabled | bool
- linux_dev_station_tool_packages | selectattr('name', 'equalto', 'uv') | list | length > 0
- name: Install uv
become: true
become_user: "{{ linux_dev_station_service_user }}"
ansible.builtin.command: >-
/bin/sh {{ linux_dev_station_service_home }}/.cache/uv-install.sh
args:
creates: "{{ linux_dev_station_service_home }}/.local/bin/uv"
environment:
HOME: "{{ linux_dev_station_service_home }}"
USER: "{{ linux_dev_station_service_user }}"
PATH: "{{ linux_dev_station_path }}"
UV_UNINSTALL_MISSING_PACKAGES: "1"
when:
- linux_dev_station_service_user_enabled | bool
- linux_dev_station_tool_packages | selectattr('name', 'equalto', 'uv') | list | length > 0
- name: Download rustup installer
become: true
ansible.builtin.get_url:
url: https://sh.rustup.rs
dest: "{{ linux_dev_station_service_home }}/.cache/rustup-install.sh"
owner: "{{ linux_dev_station_service_user }}"
group: "{{ linux_dev_station_service_user }}"
mode: "0755"
when:
- linux_dev_station_service_user_enabled | bool
- linux_dev_station_tool_packages | selectattr('name', 'equalto', 'rust') | list | length > 0
- name: Install rustup
become: true
become_user: "{{ linux_dev_station_service_user }}"
ansible.builtin.command: >-
/bin/sh {{ linux_dev_station_service_home }}/.cache/rustup-install.sh -y --default-toolchain stable
args:
creates: "{{ linux_dev_station_service_home }}/.cargo/bin/rustc"
environment:
HOME: "{{ linux_dev_station_service_home }}"
USER: "{{ linux_dev_station_service_user }}"
PATH: "{{ linux_dev_station_path }}:{{ linux_dev_station_service_home }}/.cargo/bin"
when:
- linux_dev_station_service_user_enabled | bool
- linux_dev_station_tool_packages | selectattr('name', 'equalto', 'rust') | list | length > 0
- name: Download fnm (Fast Node Manager) binary
become: true
ansible.builtin.get_url:
url: "https://github.com/Schniz/fnm/releases/download/{{ fnm_version }}/fnm-linux.zip"
dest: "{{ linux_dev_station_service_home }}/.cache/fnm.zip"
owner: "{{ linux_dev_station_service_user }}"
group: "{{ linux_dev_station_service_user }}"
mode: "0644"
vars:
fnm_version: v1.38.1
when:
- linux_dev_station_service_user_enabled | bool
- linux_dev_station_tool_packages | selectattr('name', 'equalto', 'node') | list | length > 0
- name: Create .fnm directory
become: true
become_user: "{{ linux_dev_station_service_user }}"
ansible.builtin.file:
path: "{{ linux_dev_station_service_home }}/.fnm"
state: directory
owner: "{{ linux_dev_station_service_user }}"
group: "{{ linux_dev_station_service_user }}"
mode: "0755"
when:
- linux_dev_station_service_user_enabled | bool
- linux_dev_station_tool_packages | selectattr('name', 'equalto', 'node') | list | length > 0
- name: Extract fnm to .fnm directory
become: true
become_user: "{{ linux_dev_station_service_user }}"
ansible.builtin.unarchive:
src: "{{ linux_dev_station_service_home }}/.cache/fnm.zip"
dest: "{{ linux_dev_station_service_home }}/.fnm"
remote_src: true
when:
- linux_dev_station_service_user_enabled | bool
- linux_dev_station_tool_packages | selectattr('name', 'equalto', 'node') | list | length > 0
- name: Ensure fnm binary is executable
become: true
ansible.builtin.file:
path: "{{ linux_dev_station_service_home }}/.fnm/fnm"
mode: "0755"
owner: "{{ linux_dev_station_service_user }}"
group: "{{ linux_dev_station_service_user }}"
when:
- linux_dev_station_service_user_enabled | bool
- linux_dev_station_tool_packages | selectattr('name', 'equalto', 'node') | list | length > 0
- name: Install LTS Node.js version via fnm
become: true
become_user: "{{ linux_dev_station_service_user }}"
ansible.builtin.command: >-
{{ linux_dev_station_service_home }}/.fnm/fnm install --lts
args:
creates: "{{ linux_dev_station_service_home }}/.fnm/node-versions"
environment:
HOME: "{{ linux_dev_station_service_home }}"
USER: "{{ linux_dev_station_service_user }}"
PATH: "{{ linux_dev_station_path }}:{{ linux_dev_station_service_home }}/.fnm"
FNM_DIR: "{{ linux_dev_station_service_home }}/.fnm"
when:
- linux_dev_station_service_user_enabled | bool
- linux_dev_station_tool_packages | selectattr('name', 'equalto', 'node') | list | length > 0
- name: Get installed Node.js version from fnm node-versions directory
become: true
become_user: "{{ linux_dev_station_service_user }}"
ansible.builtin.command: ls "{{ linux_dev_station_service_home }}/.fnm/node-versions"
register: fnm_versions_list
changed_when: false
environment:
FNM_DIR: "{{ linux_dev_station_service_home }}/.fnm"
when:
- linux_dev_station_service_user_enabled | bool
- linux_dev_station_tool_packages | selectattr('name', 'equalto', 'node') | list | length > 0
- name: Set default Node.js version via fnm
become: true
become_user: "{{ linux_dev_station_service_user }}"
ansible.builtin.command: >-
{{ linux_dev_station_service_home }}/.fnm/fnm default {{ node_version }}
environment:
HOME: "{{ linux_dev_station_service_home }}"
USER: "{{ linux_dev_station_service_user }}"
PATH: "{{ linux_dev_station_path }}:{{ linux_dev_station_service_home }}/.fnm"
FNM_DIR: "{{ linux_dev_station_service_home }}/.fnm"
when:
- linux_dev_station_service_user_enabled | bool
- linux_dev_station_tool_packages | selectattr('name', 'equalto', 'node') | list | length > 0
- fnm_versions_list is defined
- fnm_versions_list.stdout | trim | length > 0
vars:
node_version: "{{ fnm_versions_list.stdout | trim }}"
- name: Download code-server tarball
become: true
ansible.builtin.get_url:
url: "https://github.com/coder/code-server/releases/download/v{{ tool.version }}/code-server-{{ tool.version }}-linux-amd64.tar.gz"
dest: "{{ linux_dev_station_service_home }}/.cache/code-server.tar.gz"
owner: "{{ linux_dev_station_service_user }}"
group: "{{ linux_dev_station_service_user }}"
mode: "0644"
vars:
tool: "{{ linux_dev_station_tool_packages | selectattr('name', 'equalto', 'code-server') | first }}"
when:
- linux_dev_station_service_user_enabled | bool
- linux_dev_station_tool_packages | selectattr('name', 'equalto', 'code-server') | list | length > 0
- name: Create code-server installation directories
become: true
become_user: "{{ linux_dev_station_service_user }}"
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: "0755"
loop:
- "{{ linux_dev_station_service_home }}/.local/lib"
- "{{ linux_dev_station_service_home }}/.local/bin"
when:
- linux_dev_station_service_user_enabled | bool
- linux_dev_station_tool_packages | selectattr('name', 'equalto', 'code-server') | list | length > 0
- name: Extract code-server tarball to lib directory
become: true
become_user: "{{ linux_dev_station_service_user }}"
ansible.builtin.unarchive:
src: "{{ linux_dev_station_service_home }}/.cache/code-server.tar.gz"
dest: "{{ linux_dev_station_service_home }}/.local/lib"
remote_src: true
environment:
HOME: "{{ linux_dev_station_service_home }}"
USER: "{{ linux_dev_station_service_user }}"
when:
- linux_dev_station_service_user_enabled | bool
- linux_dev_station_tool_packages | selectattr('name', 'equalto', 'code-server') | list | length > 0
- name: Create symlink for code-server binary
become: true
become_user: "{{ linux_dev_station_service_user }}"
ansible.builtin.file:
src: "{{ linux_dev_station_service_home }}/.local/lib/code-server-{{ tool.version }}-linux-amd64/bin/code-server"
dest: "{{ linux_dev_station_service_home }}/.local/bin/code-server"
state: link
vars:
tool: "{{ linux_dev_station_tool_packages | selectattr('name', 'equalto', 'code-server') | first }}"
when:
- linux_dev_station_service_user_enabled | bool
- linux_dev_station_tool_packages | selectattr('name', 'equalto', 'code-server') | list | length > 0
- name: Create opencode installation directories
become: true
become_user: "{{ linux_dev_station_service_user }}"
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: "0755"
loop:
- "{{ linux_dev_station_service_home }}/.local/lib"
- "{{ linux_dev_station_service_home }}/.local/lib/opencode-{{ tool.version }}"
- "{{ linux_dev_station_service_home }}/.local/bin"
vars:
tool: "{{ linux_dev_station_tool_packages | selectattr('name', 'equalto', 'opencode') | first }}"
when:
- linux_dev_station_service_user_enabled | bool
- linux_dev_station_tool_packages | selectattr('name', 'equalto', 'opencode') | list | length > 0
- name: Download opencode tarball
become: true
ansible.builtin.get_url:
url: "https://github.com/anomalyco/opencode/releases/download/v{{ tool.version }}/opencode-linux-x64.tar.gz"
dest: "{{ linux_dev_station_service_home }}/.cache/opencode-{{ tool.version }}.tar.gz"
owner: "{{ linux_dev_station_service_user }}"
group: "{{ linux_dev_station_service_user }}"
mode: "0644"
vars:
tool: "{{ linux_dev_station_tool_packages | selectattr('name', 'equalto', 'opencode') | first }}"
when:
- linux_dev_station_service_user_enabled | bool
- linux_dev_station_tool_packages | selectattr('name', 'equalto', 'opencode') | list | length > 0
- name: Extract opencode tarball to versioned directory
become: true
become_user: "{{ linux_dev_station_service_user }}"
ansible.builtin.unarchive:
src: "{{ linux_dev_station_service_home }}/.cache/opencode-{{ tool.version }}.tar.gz"
dest: "{{ linux_dev_station_service_home }}/.local/lib/opencode-{{ tool.version }}"
remote_src: true
vars:
tool: "{{ linux_dev_station_tool_packages | selectattr('name', 'equalto', 'opencode') | first }}"
when:
- linux_dev_station_service_user_enabled | bool
- linux_dev_station_tool_packages | selectattr('name', 'equalto', 'opencode') | list | length > 0
- name: Create symlink for opencode binary
become: true
become_user: "{{ linux_dev_station_service_user }}"
ansible.builtin.file:
src: "{{ linux_dev_station_service_home }}/.local/lib/opencode-{{ tool.version }}/opencode"
dest: "{{ linux_dev_station_service_home }}/.local/bin/opencode"
state: link
vars:
tool: "{{ linux_dev_station_tool_packages | selectattr('name', 'equalto', 'opencode') | first }}"
when:
- linux_dev_station_service_user_enabled | bool
- linux_dev_station_tool_packages | selectattr('name', 'equalto', 'opencode') | list | length > 0
@@ -0,0 +1,24 @@
[Unit]
Description=code-server - VS Code on remote Linux
After=network.target
[Service]
Type=simple
User={{ linux_dev_station_service_user }}
Group={{ linux_dev_station_service_user }}
WorkingDirectory={{ linux_dev_station_workspace_dir }}
ExecStart=/bin/bash {{ linux_dev_station_scripts_dir }}/start-ide.sh
Restart=always
RestartSec=5
StandardOutput=append:{{ linux_dev_station_logs_dir }}/code-server.log
StandardError=append:{{ linux_dev_station_logs_dir }}/code-server.err.log
Environment="HOME={{ linux_dev_station_service_home }}"
Environment="PATH={{ linux_dev_station_path }}"
Environment="PASSWORD={{ linux_dev_station_code_server_password }}"
{% if linux_dev_station_ssh_agent_enabled | bool %}
Environment="SSH_AUTH_SOCK={{ linux_dev_station_ssh_agent_socket }}"
{% endif %}
[Install]
WantedBy=multi-user.target
@@ -0,0 +1 @@
{{ linux_dev_station_opencode_config | to_nice_json }}
@@ -0,0 +1,28 @@
[Unit]
Description=OpenCode - AI-powered terminal IDE
After=network.target
[Service]
Type=simple
User={{ linux_dev_station_service_user }}
Group={{ linux_dev_station_service_user }}
WorkingDirectory={{ linux_dev_station_workspace_dir }}
ExecStart=/bin/bash {{ linux_dev_station_scripts_dir }}/start-opencode.sh
Restart=always
RestartSec=5
StandardOutput=append:{{ linux_dev_station_logs_dir }}/opencode.log
StandardError=append:{{ linux_dev_station_logs_dir }}/opencode.err.log
Environment="HOME={{ linux_dev_station_service_home }}"
Environment="PATH={{ linux_dev_station_path }}"
Environment="OPENCODE_SERVER_USERNAME={{ linux_dev_station_opencode_server_username }}"
Environment="OPENCODE_SERVER_PASSWORD={{ linux_dev_station_opencode_server_password }}"
{% for env_key, env_value in linux_dev_station_opencode_extra_environment.items() %}
Environment="{{ env_key }}={{ env_value }}"
{% endfor %}
{% if linux_dev_station_ssh_agent_enabled | bool %}
Environment="SSH_AUTH_SOCK={{ linux_dev_station_ssh_agent_socket }}"
{% endif %}
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# SSH agent starter — idempotent, safe to source from concurrent sessions.
# Starts ssh-agent if not already running, then adds keys.
export SSH_AUTH_SOCK="{{ linux_dev_station_ssh_agent_socket }}"
# Start ssh-agent if socket doesn't exist
if [ ! -S "$SSH_AUTH_SOCK" ]; then
eval "$(/usr/bin/ssh-agent -a "{{ linux_dev_station_ssh_agent_socket }}" -s)" > /dev/null 2>&1
fi
# Wait for the agent socket to appear
__wait=0
while [ ! -S "$SSH_AUTH_SOCK" ] && [ "$__wait" -lt 15 ]; do
sleep 1
__wait=$(( __wait + 1 ))
done
# Add keys if the agent is running but has none loaded
if [ -S "$SSH_AUTH_SOCK" ]; then
if ssh-add -l 2>/dev/null | grep -q "The agent has no identities"; then
for _key in {{ linux_dev_station_ssh_agent_keys | map('quote') | join(' ') }}; do
if [ -f "$_key" ]; then
if [ -n "{{ linux_dev_station_ssh_agent_passphrase }}" ]; then
printf '%s\n' "{{ linux_dev_station_ssh_agent_passphrase }}" | ssh-add "$_key" 2>/dev/null || true
else
ssh-add "$_key" 2>/dev/null || true
fi
fi
done
fi
fi
@@ -0,0 +1,17 @@
[Unit]
Description=SSH Agent for devstation
After=network.target
[Service]
Type=simple
User={{ linux_dev_station_service_user }}
Group={{ linux_dev_station_service_user }}
ExecStart=/usr/bin/ssh-agent -a {{ linux_dev_station_ssh_agent_socket }}
Restart=always
RestartSec=5
StandardOutput=append:{{ linux_dev_station_logs_dir }}/ssh-agent.log
StandardError=append:{{ linux_dev_station_logs_dir }}/ssh-agent.err.log
Environment="HOME={{ linux_dev_station_service_home }}"
Environment="PATH={{ linux_dev_station_path }}"
Environment="SSH_AUTH_SOCK={{ linux_dev_station_ssh_agent_socket }}"
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -euo pipefail
{% if linux_dev_station_ssh_agent_enabled | bool %}
source "{{ linux_dev_station_scripts_dir }}/ssh-agent-start.sh"
{% endif %}
cd {{ linux_dev_station_workspace_dir }}
exec code-server \
--bind-addr {{ linux_dev_station_code_server_host }}:{{ linux_dev_station_code_server_port }} \
--auth {{ linux_dev_station_code_server_auth }} \
--disable-telemetry \
{{ linux_dev_station_code_server_workspace }}
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
{% if linux_dev_station_ssh_agent_enabled | bool %}
source "{{ linux_dev_station_scripts_dir }}/ssh-agent-start.sh"
{% endif %}
cd {{ linux_dev_station_workspace_dir }}
exec {{ linux_dev_station_opencode_command }}
@@ -120,6 +120,8 @@ def _normalize_preset_options(options: dict[str, Any], scope: str) -> dict[str,
normalized: dict[str, str] = {}
for key, value in options.items():
key_str = str(key).strip()
# convert _ to - for compatibility with llama.cpp router preset format
key_str = key_str.replace("_", "-")
if not key_str:
raise RuntimeError(f"{scope} preset option keys must be non-empty")
if value is None:
+8
View File
@@ -3,12 +3,14 @@ prod-01.alexpires.me
dev-01.lab.alexpires.me
vh-01.lab.alexpires.me
gpu-01.lab.alexpires.me
dev-02.lab.alexpires.me
[hardening]
prod-01.alexpires.me
dev-01.lab.alexpires.me
vh-01.lab.alexpires.me
gpu-01.lab.alexpires.me
dev-02.lab.alexpires.me
[ubuntu]
prod-01.alexpires.me
@@ -17,6 +19,7 @@ dev-01.lab.alexpires.me
[fedora]
vh-01.lab.alexpires.me
gpu-01.lab.alexpires.me
dev-02.lab.alexpires.me
[certbot]
vh-01.lab.alexpires.me
@@ -29,6 +32,7 @@ prod-01.alexpires.me
dev-01.lab.alexpires.me
vh-01.lab.alexpires.me
gpu-01.lab.alexpires.me
dev-02.lab.alexpires.me
[gitea]
prod-01.alexpires.me
@@ -141,3 +145,7 @@ ci-01.lab.alexpires.me
[darwin_dev]
mac-01.lab.alexpires.me
[linux_dev]
dev-02.lab.alexpires.me
+5
View File
@@ -111,6 +111,11 @@ locals {
type = "A"
content = "10.19.4.207"
},
{
name = "dev-02"
type = "A"
content = "10.19.4.210"
},
{
name = "vaultwarden"
type = "CNAME"