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:
@@ -1,5 +1,6 @@
|
|||||||
[defaults]
|
[defaults]
|
||||||
inventory = ../inventory/hosts
|
inventory = ../inventory/hosts
|
||||||
|
allow_world_readable_tmpfiles = True
|
||||||
|
|
||||||
[ssh_connection]
|
[ssh_connection]
|
||||||
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
|
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)
|
||||||
@@ -118,25 +118,25 @@ llama_server_devices:
|
|||||||
- "/dev/dri:/dev/dri:rw"
|
- "/dev/dri:/dev/dri:rw"
|
||||||
|
|
||||||
llama_server_models:
|
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"
|
- name: "gemma-4-12b-q8-0"
|
||||||
model_id: "unsloth/gemma-4-12b-it-GGUF"
|
model_id: "unsloth/gemma-4-12b-it-GGUF"
|
||||||
quant: "Q8_0"
|
quant: "Q8_0"
|
||||||
revision: "main"
|
revision: "main"
|
||||||
preset:
|
# preset:
|
||||||
ctx-size: 98304
|
# ctx-size: 98304
|
||||||
- name: "gemma-4-26b-a4b-ud-iq4-xs"
|
- name: "gemma-4-26b-a4b-ud-iq4-xs"
|
||||||
model_id: "unsloth/gemma-4-26B-A4B-it-GGUF"
|
model_id: "unsloth/gemma-4-26B-A4B-it-GGUF"
|
||||||
quant: "UD-IQ4_XS"
|
quant: "UD-IQ4_XS"
|
||||||
revision: "main"
|
revision: "main"
|
||||||
preset:
|
# preset:
|
||||||
ctx-size: 98304
|
# ctx-size: 98304
|
||||||
- name: "gemma-4-31b-iq4-xs"
|
- name: "gemma-4-31b-iq4-xs"
|
||||||
model_id: "unsloth/gemma-4-31B-it-GGUF"
|
model_id: "unsloth/gemma-4-31B-it-GGUF"
|
||||||
quant: "IQ4_XS"
|
quant: "IQ4_XS"
|
||||||
revision: "main"
|
revision: "main"
|
||||||
preset:
|
# preset:
|
||||||
ctx-size: 98304
|
# ctx-size: 98304
|
||||||
- name: "gpt-oss-20b-q4-0"
|
- name: "gpt-oss-20b-q4-0"
|
||||||
model_id: "unsloth/gpt-oss-20b-GGUF"
|
model_id: "unsloth/gpt-oss-20b-GGUF"
|
||||||
quant: "Q4_0"
|
quant: "Q4_0"
|
||||||
@@ -149,44 +149,56 @@ llama_server_models:
|
|||||||
model_id: "unsloth/Qwen3.5-27B-GGUF"
|
model_id: "unsloth/Qwen3.5-27B-GGUF"
|
||||||
quant: "Q4_K_M"
|
quant: "Q4_K_M"
|
||||||
revision: "main"
|
revision: "main"
|
||||||
preset:
|
# preset:
|
||||||
ctx-size: 98304
|
# ctx-size: 98304
|
||||||
- name: "qwen3-5-35b-a3b-q4-k-m"
|
- name: "qwen3-5-35b-a3b-q4-k-m"
|
||||||
model_id: "unsloth/Qwen3.5-35B-A3B-GGUF"
|
model_id: "unsloth/Qwen3.5-35B-A3B-GGUF"
|
||||||
quant: "Q4_K_M"
|
quant: "Q4_K_M"
|
||||||
revision: "main"
|
revision: "main"
|
||||||
preset:
|
# preset:
|
||||||
ctx-size: 98304
|
# ctx-size: 98304
|
||||||
- name: "qwen3-5-9b-q8-0"
|
- name: "qwen3-5-9b-q8-0"
|
||||||
model_id: "unsloth/Qwen3.5-9B-GGUF"
|
model_id: "unsloth/Qwen3.5-9B-GGUF"
|
||||||
quant: "Q8_0"
|
quant: "Q8_0"
|
||||||
revision: "main"
|
revision: "main"
|
||||||
preset:
|
# preset:
|
||||||
ctx-size: 98304
|
# ctx-size: 98304
|
||||||
- name: "qwen3-6-27b-q4-k-m"
|
- name: "qwen3-6-27b-q4-k-m"
|
||||||
model_id: "unsloth/Qwen3.6-27B-GGUF"
|
model_id: "unsloth/Qwen3.6-27B-GGUF"
|
||||||
quant: "Q4_K_M"
|
quant: "Q4_K_M"
|
||||||
revision: "main"
|
revision: "main"
|
||||||
preset:
|
# preset:
|
||||||
ctx-size: 98304
|
# 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"
|
- name: "qwen3-6-27b-mtp-q4-0"
|
||||||
model_id: "unsloth/Qwen3.6-27B-MTP-GGUF"
|
model_id: "unsloth/Qwen3.6-27B-MTP-GGUF"
|
||||||
quant: "Q4_0"
|
quant: "Q4_0"
|
||||||
revision: "main"
|
revision: "main"
|
||||||
preset:
|
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"
|
- name: "qwen3-6-35b-a3b-mtp-ud-q4-m"
|
||||||
model_id: "unsloth/Qwen3.6-35B-A3B-MTP-GGUF"
|
model_id: "unsloth/Qwen3.6-35B-A3B-MTP-GGUF"
|
||||||
quant: "UD-Q4_K_M"
|
quant: "UD-Q4_K_M"
|
||||||
revision: "main"
|
revision: "main"
|
||||||
preset:
|
preset:
|
||||||
ctx-size: 98304
|
temperature: 0.6
|
||||||
- name: "qwen3-6-35b-a3b-ud-q4-k-m"
|
top_p: 0.95
|
||||||
model_id: "unsloth/Qwen3.6-35B-A3B-GGUF"
|
top_k: 20
|
||||||
quant: "UD-Q4_K_M"
|
min_p: 0.0
|
||||||
revision: "main"
|
presence_penalty: 0.0
|
||||||
preset:
|
repeat_penalty: 1.0
|
||||||
ctx-size: 98304
|
# ctx-size: 98304
|
||||||
|
|
||||||
llama_server_argv_extra:
|
llama_server_argv_extra:
|
||||||
[
|
[
|
||||||
@@ -195,15 +207,15 @@ llama_server_argv_extra:
|
|||||||
"-np",
|
"-np",
|
||||||
-1,
|
-1,
|
||||||
"-b",
|
"-b",
|
||||||
1024,
|
2048,
|
||||||
"-ub",
|
"-ub",
|
||||||
512,
|
512,
|
||||||
"-fa",
|
"-fa",
|
||||||
"on",
|
"on",
|
||||||
"-ctk",
|
"-ctk",
|
||||||
"q4_0",
|
|
||||||
"-ctv",
|
|
||||||
"q8_0",
|
"q8_0",
|
||||||
|
"-ctv",
|
||||||
|
"q4_0",
|
||||||
"-cram",
|
"-cram",
|
||||||
-1,
|
-1,
|
||||||
"-sm",
|
"-sm",
|
||||||
@@ -216,11 +228,14 @@ llama_server_argv_extra:
|
|||||||
"off",
|
"off",
|
||||||
"-mg",
|
"-mg",
|
||||||
1,
|
1,
|
||||||
|
"--timeout",
|
||||||
|
1800,
|
||||||
"--mmap",
|
"--mmap",
|
||||||
"--spec-type",
|
"--spec-type",
|
||||||
"draft-mtp",
|
"draft-mtp",
|
||||||
"--spec-draft-n-max",
|
"--spec-draft-n-max",
|
||||||
2,
|
2,
|
||||||
|
"--swa-full",
|
||||||
]
|
]
|
||||||
|
|
||||||
auto_suspend_enabled: true
|
auto_suspend_enabled: true
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ macos_dev_station_opencode_config:
|
|||||||
options:
|
options:
|
||||||
baseURL: "https://open-webui.lab.alexpires.me/api"
|
baseURL: "https://open-webui.lab.alexpires.me/api"
|
||||||
apiKey: "{{ lookup('env', 'OPEN_WEBUI_API_KEY', default='') }}"
|
apiKey: "{{ lookup('env', 'OPEN_WEBUI_API_KEY', default='') }}"
|
||||||
|
timeout: 3000000
|
||||||
|
chunkTimeout: 3000000
|
||||||
models:
|
models:
|
||||||
gemma-4-12b-q8-0:
|
gemma-4-12b-q8-0:
|
||||||
name: "gemma-4-12b-q8-0"
|
name: "gemma-4-12b-q8-0"
|
||||||
|
|||||||
@@ -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: {}
|
||||||
@@ -11,5 +11,4 @@
|
|||||||
notify:
|
notify:
|
||||||
- Reload systemd
|
- Reload systemd
|
||||||
tags:
|
tags:
|
||||||
- systemd
|
- roles::cis::logind
|
||||||
- logind
|
|
||||||
|
|||||||
@@ -6,3 +6,6 @@ KillExcludeUsers=root
|
|||||||
IdleAction=lock
|
IdleAction=lock
|
||||||
IdleActionSec=15min
|
IdleActionSec=15min
|
||||||
RemoveIPC=yes
|
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
@@ -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] = {}
|
normalized: dict[str, str] = {}
|
||||||
for key, value in options.items():
|
for key, value in options.items():
|
||||||
key_str = str(key).strip()
|
key_str = str(key).strip()
|
||||||
|
# convert _ to - for compatibility with llama.cpp router preset format
|
||||||
|
key_str = key_str.replace("_", "-")
|
||||||
if not key_str:
|
if not key_str:
|
||||||
raise RuntimeError(f"{scope} preset option keys must be non-empty")
|
raise RuntimeError(f"{scope} preset option keys must be non-empty")
|
||||||
if value is None:
|
if value is None:
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ prod-01.alexpires.me
|
|||||||
dev-01.lab.alexpires.me
|
dev-01.lab.alexpires.me
|
||||||
vh-01.lab.alexpires.me
|
vh-01.lab.alexpires.me
|
||||||
gpu-01.lab.alexpires.me
|
gpu-01.lab.alexpires.me
|
||||||
|
dev-02.lab.alexpires.me
|
||||||
|
|
||||||
[hardening]
|
[hardening]
|
||||||
prod-01.alexpires.me
|
prod-01.alexpires.me
|
||||||
dev-01.lab.alexpires.me
|
dev-01.lab.alexpires.me
|
||||||
vh-01.lab.alexpires.me
|
vh-01.lab.alexpires.me
|
||||||
gpu-01.lab.alexpires.me
|
gpu-01.lab.alexpires.me
|
||||||
|
dev-02.lab.alexpires.me
|
||||||
|
|
||||||
[ubuntu]
|
[ubuntu]
|
||||||
prod-01.alexpires.me
|
prod-01.alexpires.me
|
||||||
@@ -17,6 +19,7 @@ dev-01.lab.alexpires.me
|
|||||||
[fedora]
|
[fedora]
|
||||||
vh-01.lab.alexpires.me
|
vh-01.lab.alexpires.me
|
||||||
gpu-01.lab.alexpires.me
|
gpu-01.lab.alexpires.me
|
||||||
|
dev-02.lab.alexpires.me
|
||||||
|
|
||||||
[certbot]
|
[certbot]
|
||||||
vh-01.lab.alexpires.me
|
vh-01.lab.alexpires.me
|
||||||
@@ -29,6 +32,7 @@ prod-01.alexpires.me
|
|||||||
dev-01.lab.alexpires.me
|
dev-01.lab.alexpires.me
|
||||||
vh-01.lab.alexpires.me
|
vh-01.lab.alexpires.me
|
||||||
gpu-01.lab.alexpires.me
|
gpu-01.lab.alexpires.me
|
||||||
|
dev-02.lab.alexpires.me
|
||||||
|
|
||||||
[gitea]
|
[gitea]
|
||||||
prod-01.alexpires.me
|
prod-01.alexpires.me
|
||||||
@@ -141,3 +145,7 @@ ci-01.lab.alexpires.me
|
|||||||
[darwin_dev]
|
[darwin_dev]
|
||||||
mac-01.lab.alexpires.me
|
mac-01.lab.alexpires.me
|
||||||
|
|
||||||
|
[linux_dev]
|
||||||
|
dev-02.lab.alexpires.me
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -111,6 +111,11 @@ locals {
|
|||||||
type = "A"
|
type = "A"
|
||||||
content = "10.19.4.207"
|
content = "10.19.4.207"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name = "dev-02"
|
||||||
|
type = "A"
|
||||||
|
content = "10.19.4.210"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name = "vaultwarden"
|
name = "vaultwarden"
|
||||||
type = "CNAME"
|
type = "CNAME"
|
||||||
|
|||||||
Reference in New Issue
Block a user