diff --git a/.editorconfig b/.editorconfig index 1963b65..ae42879 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,36 +1,52 @@ +# https://editorconfig.org + root = true +# Global defaults for all project files [*] charset = utf-8 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true indent_style = space -indent_size = 4 +indent_size = 2 +# Ansible Playbooks, Tasks, Roles, Vars, and Handlers [*.{yml,yaml}] +indent_style = space indent_size = 2 +max_line_length = 120 -[*.json] +# Jinja2 Templates (used for config templating in Ansible) +[*.{j2,jinja2}] +indent_style = space indent_size = 2 -[*.md] -trim_trailing_whitespace = false - +# Shell scripts (hooks, dynamic inventories, wrapper scripts) [*.sh] -indent_size = 4 +indent_style = space +indent_size = 2 -[install-ansible] +# Python files (custom plugins, filter plugins, modules) +[*.py] +indent_style = space indent_size = 4 +max_line_length = 88 -[install-requirements] -indent_size = 4 +# Markdown files (project documentation) +[*.md] +indent_style = space +indent_size = 2 +trim_trailing_whitespace = false -[run-ansible] -indent_size = 4 +# GitLab CI pipeline configuration +[.gitlab-ci.yml] +indent_style = space +indent_size = 2 -[run-lint] -indent_size = 4 +# Makefiles (if your project uses make for automation) +[Makefile] +indent_style = tab [nvidia/*] indent_size = 4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e76d789..08aff80 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,11 +23,11 @@ jobs: - name: Install dependencies run: | - ./install-requirements - ./install-ansible + ./scripts/install-requirements + ./scripts/install-ansible - name: Run linters - run: ./run-lint --github-format + run: ./scripts/ansible-lint --github-format - name: Publish ansible-lint results uses: github/codeql-action/upload-sarif@v4 @@ -35,5 +35,5 @@ jobs: always() && !(github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork) with: - sarif_file: ansible-lint.sarif + sarif_file: scripts/ansible-lint.sarif category: ansible-lint diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 1732900..8f960db 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -4,17 +4,13 @@ { "label": "ansible: install", "type": "shell", - "command": "${workspaceFolder}/install-requirements && ${workspaceFolder}/install-ansible", - "presentation": { - "reveal": "always", - "panel": "dedicated", - "focus": true }, + "command": "${workspaceFolder}/scripts/install-requirements && ${workspaceFolder}/scripts/install-ansible", "problemMatcher": [] }, { "label": "ansible: lint", "type": "shell", - "command": "${workspaceFolder}/run-lint", + "command": "${workspaceFolder}/scripts/ansible-lint", "problemMatcher": [ { "owner": "yamllint", @@ -50,7 +46,7 @@ { "label": "ansible: run all roles", "type": "shell", - "command": "${workspaceFolder}/run-ansible", + "command": "${workspaceFolder}/scripts/deploy --host ${input:remoteHost}", "presentation": { "reveal": "always", "panel": "dedicated", @@ -61,7 +57,7 @@ { "label": "ansible: run by tag", "type": "shell", - "command": "${workspaceFolder}/run-ansible ${input:roleTag}", + "command": "${workspaceFolder}/scripts/deploy --host ${input:remoteHost} --tag ${input:roleTag}", "presentation": { "reveal": "always", "panel": "dedicated", @@ -78,12 +74,19 @@ "options": [ "common", "shell", + "scripts", "systemd", "podman", "vscode", "git", "yakuake" ] + }, + { + "id": "remoteHost", + "type": "promptString", + "description": "Remote SSH host or SSH config alias", + "default": "localhost" } ] } diff --git a/README.md b/README.md index 2230c15..1c0204c 100644 --- a/README.md +++ b/README.md @@ -1,92 +1,131 @@ -# linux-setup-scripts +# Linux Setup Scripts -Ansible-based workstation setup for Debian systems. +An Ansible-based workstation configuration for Debian and Ubuntu systems. It +installs a consistent developer environment locally or on a remote machine over +SSH. -## What it sets up +## Included Configuration -| Role | What it does | -|---|---| -| `common` | installs base packages (curl, wget, git, Node.js LTS, …) | -| `shell_config` | fish shell, starship prompt, direnv, fzf, lsd, pyenv, Hack Nerd Font, Konsole profile | -| `podman` | rootless podman, podman-compose, user socket + linger, containers.conf | -| `vscode` | VS Code apt repo, installation, settings and extensions | -| `git_config` | global git configuration (`.gitconfig`) | +| Tag | Role | Configuration | +| --- | --- | --- | +| `common` | `common` | Base development packages and Node.js 20 | +| `systemd` | `systemd` | `systemd-resolved` and NetworkManager DNS integration | +| `podman` | `podman` | Rootless Podman and Podman Compose configuration | +| `shell` | `shell_config` | Fish, Starship, direnv, fzf, pyenv, fonts, and Konsole profile | +| `scripts` | `system_scripts` | Helper scripts in `/usr/local/bin` | +| `vscode` | `vscode` | VS Code repository, application, settings, and extensions | +| `git` | `git_config` | Global Git configuration | +| `yakuake` | `yakuake` | Yakuake configuration and user systemd service | ## Requirements -- Debian / Ubuntu based system -- `sudo` access for package installation +- Debian or Ubuntu system +- `sudo` access on the target host +- Python 3 on the control machine and remote target +- For remote deployments: SSH access with a configured key or SSH agent -## First-time setup +## First-Time Setup + +Install the Python tools, Ansible, linting tools, and required collections: ```bash -# 1. Install Python and pipx dependencies -./install-requirements +./scripts/install-requirements +./scripts/install-ansible +``` -# 2. Install ansible-core and Ansible collections -./install-ansible +The installation scripts put Ansible in the user-local `pipx` environment. Open +a new shell after installation if its executable directory was not already on +your `PATH`. -# 3. Run the playbook -./run-ansible -``` +## Deploy Locally -## Day-to-day usage +Apply all roles to the local workstation: ```bash -./run-ansible +./scripts/deploy ``` -To run only a specific role use tags: +The command prompts for the local `sudo` password when privileged tasks need +it. `localhost`, `127.0.0.1`, and `::1` passed with `--host` also use the local +Ansible connection. + +## Deploy Remotely + +Pass an SSH host name, address, or alias to deploy to a remote workstation: ```bash -cd ansible -ansible-playbook -i inventories/production/hosts.yml playbooks/workstation.yml \ - --ask-become-pass --tags shell +./scripts/deploy --host mars +./scripts/deploy --host 192.168.1.50 ``` -Available tags: `common`, `shell`, `podman`, `vscode`, `git` +The inventory contains a `remote` placeholder with `ansible_connection: ssh`. +At runtime, `--host` supplies its `ansible_host`, so remote machines do not need +to be committed to the inventory. The remote SSH user defaults to the current +control user's name; configure it in `ansible/inventories/production/group_vars/all.yml` +when the target uses a different account. Ansible displays the resolved +deployment target before it applies roles. + +## Run Selected Roles -## Repository layout +Use `--tag` (or its short form `-t`) to apply only one role: +```bash +./scripts/deploy --tag shell +./scripts/deploy -H mars -t vscode ``` -ansible/ - ansible.cfg - requirements.yml - inventories/production/ - hosts.yml - group_vars/all.yml - playbooks/ - workstation.yml - roles/ - common/ - shell_config/ - podman/ - vscode/ - git_config/ -install-requirements # installs python3, pip, venv -install-ansible # installs ansible-core + collections -run-ansible # runs the workstation playbook -requirements.txt # pip packages (pipx, podman-compose) -requirements.yml # Ansible collections + +For compatibility, a bare tag is also accepted: + +```bash +./scripts/deploy podman +``` + +Run `./scripts/deploy --help` for all command-line options. + +## VS Code Tasks + +The workspace provides tasks for installing dependencies, linting, local +deployment, and remote deployment. Run them from **Tasks: Run Task**: + +- `ansible: install` +- `ansible: lint` +- `ansible: run all roles` +- `ansible: run by tag` +- `ansible: run all roles remote` +- `ansible: run by tag remote` + +The remote tasks prompt for an SSH host; their default is `localhost`. + +## Linting + +Run YAML and Ansible linting with: + +```bash +./scripts/ansible-lint ``` -## NVIDIA (manual scripts) +## Repository Layout -The `nvidia/` folder contains standalone shell scripts for setting up the NVIDIA driver stack on Debian 12. These are **not** part of the Ansible playbook and must be run manually. +```text +ansible/ + inventories/production/hosts.yml Local and remote deployment entries + playbooks/workstation.yml Workstation playbook + roles/ Individual workstation roles +scripts/ + deploy Local and remote deployment launcher + install-requirements Installs Python package requirements + install-ansible Installs Ansible and collections + ansible-lint Runs yamllint and ansible-lint +nvidia/ Standalone NVIDIA driver helper scripts +``` -| Script | Purpose | -|---|---| -| `install_nvidia_keyring` | downloads and installs the CUDA apt keyring from NVIDIA | -| `install_nvidia_packages` | installs the full NVIDIA driver stack at a pinned version | -| `list_installed_nvidia_packages` | lists currently installed NVIDIA packages and their versions | -| `installed_nvidia_packages_version_580` | reference list of all packages installed for driver version 580 | +## NVIDIA Driver Scripts -### Usage +The scripts in `nvidia/` are standalone helpers for the NVIDIA driver stack on +Debian 12. They are intentionally not managed by the Ansible workstation +playbook. ```bash -# Step 1 — add the NVIDIA apt repository keyring sudo ./nvidia/install_nvidia_keyring - -# Step 2 — install the driver stack (edit VERSION in the script first if needed) sudo ./nvidia/install_nvidia_packages ``` diff --git a/ansible/inventories/production/hosts.yml b/ansible/inventories/production/hosts.yml index 418a805..9e2ce2b 100644 --- a/ansible/inventories/production/hosts.yml +++ b/ansible/inventories/production/hosts.yml @@ -7,3 +7,6 @@ all: ansible_host: localhost ansible_connection: local ansible_python_interpreter: /usr/bin/python3.13 + remote: + ansible_connection: ssh + ansible_python_interpreter: /usr/bin/python3.13 diff --git a/ansible/playbooks/workstation.yml b/ansible/playbooks/workstation.yml index aae1f69..c142a38 100644 --- a/ansible/playbooks/workstation.yml +++ b/ansible/playbooks/workstation.yml @@ -1,8 +1,14 @@ --- - name: Configure workstation - hosts: workstations + hosts: all become: false + pre_tasks: + - name: Show deployment target + ansible.builtin.debug: + msg: "Deploying to {{ inventory_hostname }} ({{ ansible_host | default(inventory_hostname) }})" + tags: [always] + roles: - role: common tags: [common] @@ -12,6 +18,8 @@ tags: [podman] - role: shell_config tags: [shell] + - role: system_scripts + tags: [scripts] - role: vscode tags: [vscode] - role: git_config diff --git a/ansible/roles/system_scripts/defaults/main.yml b/ansible/roles/system_scripts/defaults/main.yml new file mode 100644 index 0000000..c5d3712 --- /dev/null +++ b/ansible/roles/system_scripts/defaults/main.yml @@ -0,0 +1,2 @@ +system_scripts_install: + - su-session diff --git a/ansible/roles/system_scripts/files/su-session b/ansible/roles/system_scripts/files/su-session new file mode 100644 index 0000000..0adaab0 --- /dev/null +++ b/ansible/roles/system_scripts/files/su-session @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +if [[ $# -ne 1 ]]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +user="$1" + +exec sudo systemd-run \ + --system \ + --scope \ + su - "$user" diff --git a/ansible/roles/system_scripts/tasks/main.yml b/ansible/roles/system_scripts/tasks/main.yml new file mode 100644 index 0000000..a4034b8 --- /dev/null +++ b/ansible/roles/system_scripts/tasks/main.yml @@ -0,0 +1,17 @@ +--- +- name: Ensure /usr/local/bin exists + become: true + ansible.builtin.file: + path: /usr/local/bin + state: directory + mode: "0755" + +- name: Install helper scripts + become: true + ansible.builtin.copy: + src: "{{ item }}" + dest: "/usr/local/bin/{{ item }}" + owner: root + group: root + mode: "0755" + loop: "{{ system_scripts_install }}" diff --git a/install-requirements b/install-requirements deleted file mode 100755 index 2011e06..0000000 --- a/install-requirements +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REQ_FILE="${SCRIPT_DIR}/requirements.txt" - -# Ensure python3, pip, and venv are installed -PYTHON_PACKAGES=( - python3 - python3-pip - python3-venv -) - -if ! dpkg -s "${PYTHON_PACKAGES[@]}" &>/dev/null; then - echo "Installing ${PYTHON_PACKAGES[*]}..." - sudo apt-get update -qq - sudo apt-get install -y "${PYTHON_PACKAGES[@]}" -fi - -USER_BIN="$(python3 -m site --user-base)/bin" -export PATH="${USER_BIN}:${PATH}" - -# Install pip if missing -python3 -m ensurepip --upgrade >/dev/null 2>&1 || true -python3 -m pip install --user --break-system-packages --upgrade -r "${REQ_FILE}" - diff --git a/requirements.txt b/requirements.txt index aa9d17a..c0dd2c4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -pipx==1.12.0 -podman-compose==1.5.0 +pipx>=1.8 +podman-compose==1.6.0 diff --git a/run-ansible b/run-ansible deleted file mode 100755 index ee9808e..0000000 --- a/run-ansible +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" - -TAG="${1:-}" - -( - cd "${SCRIPT_DIR}/ansible" - if [[ -n "${TAG}" ]]; then - ansible-playbook -i inventories/production/hosts.yml playbooks/workstation.yml --ask-become-pass --tags "${TAG}" - else - ansible-playbook -i inventories/production/hosts.yml playbooks/workstation.yml --ask-become-pass - fi -) diff --git a/run-lint b/run-lint deleted file mode 100755 index 13d4d73..0000000 --- a/run-lint +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env bash -set -Eeuo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -cd "${SCRIPT_DIR}/ansible" - -export PATH="${PATH}:$(python3 -m site --user-base)/bin" -export PYTHONUNBUFFERED=1 - -GITHUB_FORMAT=false -SARIF_FILE="${SCRIPT_DIR}/ansible-lint.sarif" - -for arg in "$@"; do - case "$arg" in - --github-format) - GITHUB_FORMAT=true - ;; - esac -done - -EXIT_CODE=0 - -run_yamllint() { - if [[ "${GITHUB_FORMAT}" == "true" ]]; then - yamllint --format github . || return $? - else - yamllint . || return $? - fi -} - -run_ansible_lint() { - if [[ "${GITHUB_FORMAT}" == "true" ]]; then - ansible-lint -f sarif > "${SARIF_FILE}" || return $? - else - ansible-lint || return $? - fi -} - -run_yamllint || EXIT_CODE=$? -run_ansible_lint || EXIT_CODE=$? - -exit "${EXIT_CODE}" diff --git a/scripts/ansible-lint b/scripts/ansible-lint new file mode 100755 index 0000000..9b4f786 --- /dev/null +++ b/scripts/ansible-lint @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "${SCRIPT_DIR}/../ansible" + +USER_BIN="$(python3 -m site --user-base)/bin" +export PATH="${USER_BIN}:${PATH}" + +export PYTHONUNBUFFERED=1 +export ANSIBLE_CONFIG="${SCRIPT_DIR}/../ansible.cfg" +export ANSIBLE_COLLECTIONS_PATH="${HOME}/.ansible/collections" + +SARIF_FILE="${SCRIPT_DIR}/ansible-lint.sarif" +LINT_FILE="${SCRIPT_DIR}/ansible-lint.out" + +GITHUB_FORMAT=false +REVIEWDOG=false + +for arg in "$@"; do + case "$arg" in + --github-format) + GITHUB_FORMAT=true + ;; + --reviewdog) + REVIEWDOG=true + ;; + esac +done + +EXIT_CODE=0 + +run_yamllint() { + if [[ "${REVIEWDOG}" == "true" ]]; then + yamllint --format parsable . + elif [[ "${GITHUB_FORMAT}" == "true" ]]; then + yamllint --format github . + else + yamllint . + fi +} + +run_ansible_lint() { + if [[ "${REVIEWDOG}" == "true" ]]; then + ansible-lint --format=pep8 > "${LINT_FILE}" || true + else + ansible-lint + fi +} + +generate_sarif() { + if [[ "${GITHUB_FORMAT}" == "true" ]]; then + ansible-lint -f sarif > "${SARIF_FILE}" || true + fi +} + +run_yamllint || EXIT_CODE=$? +run_ansible_lint || EXIT_CODE=$? + +generate_sarif + +exit "${EXIT_CODE}" \ No newline at end of file diff --git a/scripts/deploy b/scripts/deploy new file mode 100755 index 0000000..ecbe318 --- /dev/null +++ b/scripts/deploy @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +HOST="" +TAG="" + +usage() { + cat </dev/null || pipx install ansible-core +pipx upgrade ansible-core 2>/dev/null || pipx install --force ansible-core pipx inject --include-apps --force ansible-core ansible-lint -pipx upgrade yamllint 2>/dev/null || pipx install yamllint +pipx upgrade yamllint 2>/dev/null || pipx install --force yamllint -ansible-galaxy collection install -r "${SCRIPT_DIR}/requirements.yml" --upgrade +ansible-galaxy collection install -r "${SCRIPT_DIR}/../requirements.yml" --upgrade diff --git a/scripts/install-requirements b/scripts/install-requirements new file mode 100755 index 0000000..2af04ad --- /dev/null +++ b/scripts/install-requirements @@ -0,0 +1,12 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REQ_FILE="${SCRIPT_DIR}/../requirements.txt" + +# Ensure user-local bin is on PATH for pip-installed CLIs +export PATH="${PATH}:$(python3 -m site --user-base)/bin" + +# Install pip if missing +python3 -m ensurepip --upgrade >/dev/null 2>&1 || true +python3 -m pip install --user --break-system-packages --upgrade -r "${REQ_FILE}" \ No newline at end of file