diff --git a/ansible/host_vars/ci-01.lab.alexpires.me.yml b/ansible/host_vars/ci-01.lab.alexpires.me.yml index 1cfcb39..6a95d69 100644 --- a/ansible/host_vars/ci-01.lab.alexpires.me.yml +++ b/ansible/host_vars/ci-01.lab.alexpires.me.yml @@ -10,7 +10,9 @@ ssh_users: admin: true keys: - "{{ lookup('file', 'keys/ansible_user.pub') }}" + - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN1YEqRDPq7lMd2n37OmzX1AEyeed17uf4LPgSgkb/Sb" +prometheus_install_dir: "D:\\Prometheus" grafana_prometheus_sources: - 10.19.4.136 @@ -33,7 +35,7 @@ windows_exporter_collectors: llama_server_windows_install_dir: "D:\\llama_server" llama_server_windows_user: "llama" llama_server_windows_working_dir: "D:\\llama_server" -llama_server_cuda_version: "13.3" +llama_server_windows_backend: "vulkan" llama_server_models: - name: "luau-qwen3-4b-fim-v0.1" @@ -83,4 +85,4 @@ llama_server_args_extra: "--mmap", ] -llama_server_windows_backend: "vulkan" +windows_containers_service_password: "{{ lookup('env', 'WIN_CI_01_PASSWORD') }}" diff --git a/ansible/host_vars/gpu-01.lab.alexpires.me.yml b/ansible/host_vars/gpu-01.lab.alexpires.me.yml index 2f929f2..0e78812 100644 --- a/ansible/host_vars/gpu-01.lab.alexpires.me.yml +++ b/ansible/host_vars/gpu-01.lab.alexpires.me.yml @@ -123,52 +123,118 @@ llama_server_models: model_id: "unsloth/gpt-oss-20b-GGUF" quant: "Q4_0" revision: "main" + - name: "gpt-oss-20b-q8-0" + model_id: "unsloth/gpt-oss-20b-GGUF" + quant: "Q8_0" + revision: "main" + - name: "gemma-4-12b-q8-0" + model_id: "unsloth/gemma-4-12b-it-GGUF" + quant: "Q8_0" + revision: "main" + preset: + ctx-size: 131072 - 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: 131072 - name: "gemma-4-31b-iq4-xs" model_id: "unsloth/gemma-4-31B-it-GGUF" quant: "IQ4_XS" revision: "main" - - name: "qwen3-6-35b-a3b-mtp-ud-iq4-xs" - model_id: "unsloth/Qwen3.6-35B-A3B-MTP-GGUF" - quant: "UD-IQ4_XS" - revision: "main" - - name: "qwen3-5-27b-mtp-iq4-xs" - model_id: "unsloth/Qwen3.5-27B-MTP-GGUF" - quant: "IQ4_XS" - revision: "main" - - name: "qwen3-5-35b-a3b-ud-iq4-xs" - model_id: "unsloth/Qwen3.5-35B-A3B-GGUF" - quant: "UD-IQ4_XS" - revision: "main" - - name: "ministral-3-14b-instruct-iq4-xs" - model_id: "unsloth/Ministral-3-14B-Instruct-2512-GGUF" - quant: "IQ4_XS" - revision: "main" - - name: "granite-4-1-30b-iq4-xs" - model_id: "unsloth/granite-4.1-30b-GGUF" - quant: "IQ4_XS" - revision: "main" + preset: + ctx-size: 131072 - name: "qwen3-5-9b-iq4-xs" model_id: "unsloth/Qwen3.5-9B-GGUF" quant: "IQ4_XS" revision: "main" + preset: + ctx-size: 131072 + - name: "qwen3-5-9b-q8-0" + model_id: "unsloth/Qwen3.5-9B-GGUF" + quant: "Q8_0" + revision: "main" + preset: + ctx-size: 131072 + - name: "qwen3-5-27b-mtp-iq4-xs" + model_id: "unsloth/Qwen3.5-27B-MTP-GGUF" + quant: "IQ4_XS" + revision: "main" + preset: + ctx-size: 131072 + - name: "qwen3-5-35b-a3b-ud-iq4-xs" + model_id: "unsloth/Qwen3.5-35B-A3B-GGUF" + quant: "UD-IQ4_XS" + revision: "main" + preset: + ctx-size: 131072 + - 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: 131072 + - name: "qwen3-6-27b-mtp-q4-0" + model_id: "unsloth/Qwen3.6-27B-MTP-GGUF" + quant: "Q4_0" + revision: "main" + preset: + ctx-size: 131072 + - name: "qwen3-6-27b-mtp-ud-q4-0-k-xl" + model_id: "unsloth/Qwen3.6-27B-MTP-GGUF" + quant: "UD-Q4_K_XL" + revision: "main" + preset: + ctx-size: 131072 + - name: "qwen3-6-27b-mtp-ud-q4-xl" + model_id: "unsloth/Qwen3.6-27B-MTP-GGUF" + quant: "UD-Q4_K_XL" + revision: "main" + preset: + ctx-size: 131072 + - name: "qwen3-6-35b-a3b-mtp-ud-iq4-xs" + model_id: "unsloth/Qwen3.6-35B-A3B-MTP-GGUF" + quant: "UD-IQ4_XS" + revision: "main" + preset: + ctx-size: 131072 + - 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: 131072 + - 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: 131072 - name: "qwen3-6-35b-a3b-claude47-distilled-iq4-xs" model_id: "lordx64/Qwen3.6-35B-A3B-Claude-4.7-Opus-Reasoning-Distilled-IQ4_XS-GGUF" quant: "IQ4_XS" revision: "main" + preset: + ctx-size: 131072 + - name: "ministral-3-14b-instruct-iq4-xs" + model_id: "unsloth/Ministral-3-14B-Instruct-2512-GGUF" + quant: "IQ4_XS" + revision: "main" + preset: + ctx-size: 131072 + - name: "granite-4-1-30b-iq4-xs" + model_id: "unsloth/granite-4.1-30b-GGUF" + quant: "IQ4_XS" + revision: "main" + preset: + ctx-size: 131072 - name: "devstral-small-2-24b-instruct-iq4-xs" model_id: "unsloth/Devstral-Small-2-24B-Instruct-2512-GGUF" quant: "IQ4_XS" revision: "main" - - name: "qwen3-6-27b-mtp-iq4-xs" - model_id: "unsloth/Qwen3.6-27B-MTP-GGUF" - quant: "IQ4_XS" - revision: "main" -llama_server_args_extra: +llama_server_argv_extra: [ "-t", 10, @@ -201,6 +267,6 @@ llama_server_args_extra: # # Optional global model preset options applied under [*] in models.ini. # # Keys map to llama.cpp CLI args without leading dashes, short args, or LLAMA_ARG_* env names. -# llama_preset_global: -# c: 131072 +# llama_server_preset_global: +# ctx-size: 131072 # n-gpu-layers: all diff --git a/ansible/host_vars/vh-01.lab.alexpires.me.yml b/ansible/host_vars/vh-01.lab.alexpires.me.yml index 5fc484d..baca601 100644 --- a/ansible/host_vars/vh-01.lab.alexpires.me.yml +++ b/ansible/host_vars/vh-01.lab.alexpires.me.yml @@ -296,6 +296,164 @@ virt_machines: slot: "0x00" function: "0x0" + - name: win2019server + title: "Microsoft Windows Server 2019" + uuid: "51d5d091-67cb-4113-bd8e-817e7317e1b6" + autostart: false + memory: 16 + cores: 5 + threads: 2 + physical_cpus: "3-11,4-12,5-13,6-14,7-15" + reserved_cpus: "0,8" + enable_hyperv: true + cdrom: true + console: false + audio: true + secure_boot: true + tpm: true + tablet: true + net_bridge: br0 + disk_file: "win2019server.qcow2" + mac_address: "52:54:00:cd:dd:2f" + block_devs: [] + guest_agent: true + host_devs: + # Micron/Crucial Technology T500 NVMe PCIe SSD + - type: "pci" + source: + address: + domain: "0x0000" + bus: "0x05" + slot: "0x00" + function: "0x0" + address: + type: "pci" + domain: "0x0000" + bus: "0x11" + slot: "0x00" + function: "0x0" + # Fresco Logic FL1100 USB 3.0 Host Controller + - type: "pci" + source: + address: + domain: "0x0000" + bus: "0x08" + slot: "0x00" + function: "0x0" + address: + type: "pci" + domain: "0x0000" + bus: "0x10" + slot: "0x00" + function: "0x0" + # NVIDIA Corporation M2000 [Maxwell GM107GL] + - type: "pci" + source: + address: + domain: "0x0000" + bus: "0x03" + slot: "0x00" + function: "0x0" + address: + type: "pci" + domain: "0x0000" + bus: "0x0b" + slot: "0x00" + function: "0x0" + # NVIDIA Corporation M2000 High Definition Audio Controller + - type: "pci" + source: + address: + domain: "0x0000" + bus: "0x03" + slot: "0x00" + function: "0x1" + address: + type: "pci" + domain: "0x0000" + bus: "0x0f" + slot: "0x00" + function: "0x0" + + - name: win2022server + title: "Microsoft Windows Server 2022" + uuid: "51d5d091-67cb-4113-bd8e-817e7317e1b7" + autostart: false + memory: 16 + cores: 5 + threads: 2 + physical_cpus: "3-11,4-12,5-13,6-14,7-15" + reserved_cpus: "0,8" + enable_hyperv: true + cdrom: true + console: false + audio: true + secure_boot: true + tpm: true + tablet: true + net_bridge: br0 + disk_file: "win2022server.qcow2" + mac_address: "52:54:00:cd:dd:2e" + block_devs: [] + guest_agent: true + host_devs: + # Micron/Crucial Technology T500 NVMe PCIe SSD + - type: "pci" + source: + address: + domain: "0x0000" + bus: "0x05" + slot: "0x00" + function: "0x0" + address: + type: "pci" + domain: "0x0000" + bus: "0x11" + slot: "0x00" + function: "0x0" + # Fresco Logic FL1100 USB 3.0 Host Controller + - type: "pci" + source: + address: + domain: "0x0000" + bus: "0x08" + slot: "0x00" + function: "0x0" + address: + type: "pci" + domain: "0x0000" + bus: "0x10" + slot: "0x00" + function: "0x0" + # NVIDIA Corporation M2000 [Maxwell GM107GL] + - type: "pci" + source: + address: + domain: "0x0000" + bus: "0x03" + slot: "0x00" + function: "0x0" + address: + type: "pci" + domain: "0x0000" + bus: "0x0b" + slot: "0x00" + function: "0x0" + # NVIDIA Corporation M2000 High Definition Audio Controller + - type: "pci" + source: + address: + domain: "0x0000" + bus: "0x03" + slot: "0x00" + function: "0x1" + address: + type: "pci" + domain: "0x0000" + bus: "0x0f" + slot: "0x00" + function: "0x0" + - name: dev-01 title: "Lab Kubernetes VM" uuid: "55feda16-51ae-446e-9c83-4c0eee1e5e00" diff --git a/ansible/playbook_windows_containers.yml b/ansible/playbook_windows_containers.yml new file mode 100644 index 0000000..77260f2 --- /dev/null +++ b/ansible/playbook_windows_containers.yml @@ -0,0 +1,6 @@ +--- +- name: Install Windows Containers + hosts: windows_containers + + roles: + - windows_containers diff --git a/ansible/roles/amd_rocm/tasks/main.yml b/ansible/roles/amd_rocm/tasks/main.yml index a671a6a..33ea415 100644 --- a/ansible/roles/amd_rocm/tasks/main.yml +++ b/ansible/roles/amd_rocm/tasks/main.yml @@ -1,6 +1,5 @@ --- - name: Add ROCm repository (yum) - when: ansible_pkg_mgr == 'yum' become: true ansible.builtin.yum_repository: name: rocm @@ -11,17 +10,6 @@ enabled: true state: present -- name: Add ROCm repository (dnf) - when: ansible_pkg_mgr == 'dnf' - become: true - ansible.builtin.dnf_repository: - name: rocm - description: ROCm Repository - baseurl: "{{ rocm_repo_baseurl }}" - gpgcheck: yes - gpgkey: "{{ rocm_repo_gpgkey }}" - enabled: yes - - name: Add AMD Graphics repository (yum) when: ansible_pkg_mgr == 'yum' become: true @@ -34,17 +22,6 @@ enabled: true state: present -- name: Add AMD Graphics repository (dnf) - when: ansible_pkg_mgr == 'dnf' - become: true - ansible.builtin.dnf_repository: - name: rocmgraphics - description: AMD Graphics Repository - baseurl: "{{ rocmgraphics_repo_baseurl }}" - gpgkey: "{{ rocm_repo_gpgkey }}" - gpgcheck: yes - enabled: yes - - name: Install rocm-dkms and dependencies become: true ansible.builtin.package: diff --git a/ansible/roles/llama_server/defaults/main.yml b/ansible/roles/llama_server/defaults/main.yml index b29f5e5..51f1416 100644 --- a/ansible/roles/llama_server/defaults/main.yml +++ b/ansible/roles/llama_server/defaults/main.yml @@ -34,9 +34,6 @@ llama_server_windows_working_dir: "C:\\llama_server" llama_server_windows_version: "b9442" llama_server_windows_backend: "cuda" llama_server_cuda_version: "13.3" -llama_server_windows_nssm_dir: "C:\\nssm" -llama_server_windows_nssm_version: "2.24" -llama_server_windows_nssm_url: "https://nssm.cc/release/nssm-{{ llama_server_windows_nssm_version }}.zip" # Llama-server binary defaults for Windows llama_server_windows_bin_name: "llama-server.exe" diff --git a/ansible/roles/llama_server/files/scripts/llama_hf_sync.py b/ansible/roles/llama_server/files/scripts/linux/llama_hf_sync.py similarity index 100% rename from ansible/roles/llama_server/files/scripts/llama_hf_sync.py rename to ansible/roles/llama_server/files/scripts/linux/llama_hf_sync.py diff --git a/ansible/roles/llama_server/files/scripts/llama_hf_sync_windows.py b/ansible/roles/llama_server/files/scripts/windows/llama_hf_sync.py similarity index 100% rename from ansible/roles/llama_server/files/scripts/llama_hf_sync_windows.py rename to ansible/roles/llama_server/files/scripts/windows/llama_hf_sync.py diff --git a/ansible/roles/llama_server/tasks/el/main.yml b/ansible/roles/llama_server/tasks/el/main.yml index 089d719..22836d9 100644 --- a/ansible/roles/llama_server/tasks/el/main.yml +++ b/ansible/roles/llama_server/tasks/el/main.yml @@ -55,7 +55,7 @@ loop: - models - .cache - - .cache/llama.cpp + - .cache/llamacpp - name: "Add SSH Authorized key" become: true @@ -130,7 +130,7 @@ - name: Create pods containers.podman.podman_container: - name: "llama.cpp" + name: "llamacpp" image: "{{ llama_server_image }}:{{ llama_server_tag }}" state: started device: "{{ llama_server_devices | default(omit) }}" @@ -159,7 +159,7 @@ become: true ansible.builtin.template: src: podman.service.j2 - dest: "/etc/systemd/system/podman-llama.cpp.service" + dest: "/etc/systemd/system/podman-llamacpp.service" owner: root group: root mode: "0644" @@ -172,7 +172,7 @@ - name: Enable containers at boot become: true ansible.builtin.systemd: - name: "podman-llama.cpp" + name: "podman-llamacpp" enabled: true state: "started" @@ -221,7 +221,7 @@ - name: Copy llama server Hugging Face sync helper become: true ansible.builtin.copy: - src: scripts/llama_hf_sync.py + src: scripts/linux/llama_hf_sync.py dest: "{{ llama_server_home }}/models/.router/llama_hf_sync.py" owner: "{{ llama_server_user }}" group: "{{ llama_server_group }}" diff --git a/ansible/roles/llama_server/tasks/windows/llama_exporter.yml b/ansible/roles/llama_server/tasks/windows/llama_exporter.yml index 641f5fb..323e500 100644 --- a/ansible/roles/llama_server/tasks/windows/llama_exporter.yml +++ b/ansible/roles/llama_server/tasks/windows/llama_exporter.yml @@ -65,7 +65,7 @@ - name: Install Windows Exporter service via NSSM ansible.windows.win_command: argv: - - "{{ __nssm_exe }}" + - "nssm.exe" - install - llama-exporter - "{{ __exporter_venv_python }}" @@ -78,7 +78,7 @@ - name: Set exporter AppDirectory via NSSM ansible.windows.win_command: argv: - - "{{ __nssm_exe }}" + - "nssm.exe" - set - llama-exporter - AppDirectory @@ -87,7 +87,7 @@ - name: Configure exporter stdout log via NSSM ansible.windows.win_command: argv: - - "{{ __nssm_exe }}" + - "nssm.exe" - set - llama-exporter - AppStdout @@ -96,7 +96,7 @@ - name: Configure exporter stderr log via NSSM ansible.windows.win_command: argv: - - "{{ __nssm_exe }}" + - "nssm.exe" - set - llama-exporter - AppStderr @@ -105,7 +105,7 @@ - name: Set exporter environment variables ansible.windows.win_command: argv: - - "{{ __nssm_exe }}" + - "nssm.exe" - set - llama-exporter - AppEnvironmentExtra @@ -116,7 +116,7 @@ - name: Set exporter Start mode to AUTO via NSSM ansible.windows.win_command: argv: - - "{{ __nssm_exe }}" + - "nssm.exe" - set - llama-exporter - Start diff --git a/ansible/roles/llama_server/tasks/windows/main.yml b/ansible/roles/llama_server/tasks/windows/main.yml index 6e568e8..6ad06d6 100644 --- a/ansible/roles/llama_server/tasks/windows/main.yml +++ b/ansible/roles/llama_server/tasks/windows/main.yml @@ -4,13 +4,11 @@ - name: Define Windows paths and versions ansible.builtin.set_fact: __llama_user: "{{ llama_server_windows_user }}" - __nssm_exe: "{{ llama_server_windows_nssm_dir }}\\nssm.exe" __llama_release_asset_name: "{{ 'llama-' ~ llama_server_windows_version ~ '-bin-win-' ~ llama_server_windows_backend ~ (llama_server_windows_backend == 'cuda') | ternary('-' ~ llama_server_cuda_version, '') ~ '-x64.zip' }}" __llama_release_zip_url: "https://github.com/ggml-org/llama.cpp/releases/download/{{ llama_server_windows_version }}/{{ 'llama-' ~ llama_server_windows_version ~ '-bin-win-' ~ llama_server_windows_backend ~ (llama_server_windows_backend == 'cuda') | ternary('-' ~ llama_server_cuda_version, '') ~ '-x64.zip' }}" __llama_release_zip_path: "{{ (ansible_env.TEMP | default('C:\\Windows\\Temp')) }}\\{{ 'llama-' ~ llama_server_windows_version ~ '-bin-win-' ~ llama_server_windows_backend ~ (llama_server_windows_backend == 'cuda') | ternary('-' ~ llama_server_cuda_version, '') ~ '-x64.zip' }}" __llama_release_marker: "{{ llama_server_windows_install_dir }}\\.release-{{ 'llama-' ~ llama_server_windows_version ~ '-bin-win-' ~ llama_server_windows_backend ~ (llama_server_windows_backend == 'cuda') | ternary('-' ~ llama_server_cuda_version, '') ~ '-x64.zip' }}" - __nssm_zip_path: "{{ (ansible_env.TEMP | default('C:\\Windows\\Temp')) }}\\nssm.zip" - __llama_sync_script: "{{ llama_server_windows_working_dir }}\\.router\\llama_hf_sync_windows.py" + __llama_sync_script: "{{ llama_server_windows_working_dir }}\\.router\\llama_hf_sync.py" __llama_models_json: "{{ llama_server_windows_working_dir }}\\.router\\desired-models.json" __llama_preset_global_json: "{{ llama_server_windows_working_dir }}\\.router\\preset-global.json" __llama_managed_dir: "{{ llama_server_windows_working_dir }}\\models\\managed" @@ -118,35 +116,13 @@ Check download/unzip and llama_server_windows_backend settings. when: not __llama_bin_stat.stat.exists -- name: Check if NSSM is installed - ansible.windows.win_stat: - path: "{{ llama_server_windows_nssm_dir }}\\nssm-{{ llama_server_windows_nssm_version }}\\win64\\nssm.exe" - register: __nssm_stat - -- name: Download NSSM if not installed - ansible.windows.win_get_url: - url: "{{ llama_server_windows_nssm_url }}" - dest: "{{ __nssm_zip_path }}" - force: false - when: not __nssm_stat.stat.exists - -- name: Extract NSSM if not installed - community.windows.win_unzip: - src: "{{ __nssm_zip_path }}" - dest: "{{ llama_server_windows_nssm_dir }}" - delete_archive: false - creates: "{{ llama_server_windows_nssm_dir }}\\nssm-{{ llama_server_windows_nssm_version }}\\win64\\nssm.exe" - when: not __nssm_stat.stat.exists - -- name: Copy NSSM executable - ansible.windows.win_copy: - src: "{{ llama_server_windows_nssm_dir }}\\nssm-{{ llama_server_windows_nssm_version }}\\win64\\nssm.exe" - dest: "{{ __nssm_exe }}" - remote_src: true +- name: Install NSSM + ansible.builtin.include_role: + name: nssm - name: Copy Windows-compatible Hugging Face sync script ansible.windows.win_copy: - src: scripts/llama_hf_sync_windows.py + src: scripts/windows/llama_hf_sync.py dest: "{{ __llama_sync_script }}" mode: "0750" @@ -244,7 +220,7 @@ argv: >- {{ [ - __nssm_exe, + 'nssm.exe', 'install', 'llama-server', (llama_server_windows_install_dir ~ '\\' ~ llama_server_windows_bin_name), @@ -268,7 +244,7 @@ - name: Set llama-server AppDirectory via NSSM ansible.windows.win_command: argv: - - "{{ __nssm_exe }}" + - nssm.exe - set - llama-server - AppDirectory @@ -277,7 +253,7 @@ - name: Configure llama-server stdout log via NSSM ansible.windows.win_command: argv: - - "{{ __nssm_exe }}" + - nssm.exe - set - llama-server - AppStdout @@ -286,7 +262,7 @@ - name: Configure llama-server stderr log via NSSM ansible.windows.win_command: argv: - - "{{ __nssm_exe }}" + - nssm.exe - set - llama-server - AppStderr @@ -295,7 +271,7 @@ - name: Set llama-server Start mode to AUTO via NSSM ansible.windows.win_command: argv: - - "{{ __nssm_exe }}" + - nssm.exe - set - llama-server - Start diff --git a/ansible/roles/llama_server/templates/podman.service.j2 b/ansible/roles/llama_server/templates/podman.service.j2 index 6a20fa2..a8727f4 100644 --- a/ansible/roles/llama_server/templates/podman.service.j2 +++ b/ansible/roles/llama_server/templates/podman.service.j2 @@ -7,8 +7,8 @@ Type=oneshot User={{ llama_server_user }} Group={{ llama_server_group }} RemainAfterExit=true -ExecStart={{ podman_binary }} start podman-llama.cpp -ExecStop={{ podman_binary }} stop podman-llama.cpp +ExecStart={{ podman_binary }} start llamacpp +ExecStop={{ podman_binary }} stop llamacpp StandardOutput=journal StandardError=journal diff --git a/ansible/roles/nssm/defaults/main.yml b/ansible/roles/nssm/defaults/main.yml new file mode 100644 index 0000000..c4e44ca --- /dev/null +++ b/ansible/roles/nssm/defaults/main.yml @@ -0,0 +1,7 @@ +--- +# NSSM role defaults +nssm_version: "2.24" +nssm_install_dir: "C:\\nssm" +nssm_zip_url: "https://nssm.cc/release/nssm-{{ nssm_version }}.zip" +nssm_zip_path: "{{ (ansible_env.TEMP | default('C:\\Windows\\Temp')) }}\\nssm.zip" +nssm_exe: "{{ nssm_install_dir }}\\nssm-{{ nssm_version }}\\win64\\nssm.exe" diff --git a/ansible/roles/nssm/meta/main.yml b/ansible/roles/nssm/meta/main.yml new file mode 100644 index 0000000..f5f2370 --- /dev/null +++ b/ansible/roles/nssm/meta/main.yml @@ -0,0 +1,11 @@ +--- +galaxy_info: + author: Alexandre Pires + description: Install NSSM (Non-Sucking Service Manager) on Windows + company: A13Labs + role_name: nssm + namespace: a13labs + license: MIT + min_ansible_version: "2.1" + platforms: + - name: Windows diff --git a/ansible/roles/nssm/tasks/main.yml b/ansible/roles/nssm/tasks/main.yml new file mode 100644 index 0000000..728f67c --- /dev/null +++ b/ansible/roles/nssm/tasks/main.yml @@ -0,0 +1,55 @@ +--- +# Install NSSM on Windows - reusable role + +- name: Create NSSM installation directory + ansible.windows.win_file: + path: "{{ nssm_install_dir }}" + state: directory + +- name: Check if NSSM is already installed + ansible.windows.win_stat: + path: "{{ nssm_exe }}" + register: __nssm_check + +- name: Download NSSM if not installed + ansible.windows.win_get_url: + url: "{{ nssm_zip_url }}" + dest: "{{ nssm_zip_path }}" + force: false + when: not __nssm_check.stat.exists + +- name: Extract NSSM if not installed + community.windows.win_unzip: + src: "{{ nssm_zip_path }}" + dest: "{{ nssm_install_dir }}" + delete_archive: false + creates: "{{ nssm_exe }}" + +- name: Verify NSSM executable is present + ansible.windows.win_stat: + path: "{{ nssm_exe }}" + register: __nssm_stat + +- name: Fail if NSSM executable is missing + ansible.builtin.fail: + msg: >- + nssm binary not found at {{ nssm_exe }}. + Check download/unzip and nssm_version. + when: not __nssm_stat.stat.exists + +# - name: Add NSSM to PATH (user level) +# ansible.windows.win_path: +# name: Path +# elements: +# - "{{ nssm_install_dir }}\\nssm-{{ nssm_version }}\\win64" +# state: present + +- name: Add NSSM to PATH (system level) + ansible.windows.win_path: + name: Path + elements: + - "{{ nssm_install_dir }}\\nssm-{{ nssm_version }}\\win64" + state: present + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM diff --git a/ansible/roles/windows_containers/defaults/main.yml b/ansible/roles/windows_containers/defaults/main.yml new file mode 100644 index 0000000..0928486 --- /dev/null +++ b/ansible/roles/windows_containers/defaults/main.yml @@ -0,0 +1,41 @@ +--- +# ── Mirantis Container Runtime (Windows Server) defaults ────────────────── + +windows_containers_mcr_install_script_url: "https://get.mirantis.com/install.ps1" +windows_containers_mcr_channel: "stable" +windows_containers_mcr_version: "" +windows_containers_disable_telemetry: false + +# --- Podman defaults (container behavior) --- +windows_containers_ubuntu_version: "latest" +windows_containers_install_dir: "C:\\Program Files\\RedHat\\Podman" +windows_containers_installer_url: "https://github.com/podman-container-tools/podman/releases/download/v5.8.2/podman-installer-windows-amd64.exe" +windows_containers_download_retries: 5 +windows_containers_download_delay: 10 +windows_containers_download_timeout: 120 +windows_containers_machine_name: "podman-machine-default" +windows_containers_image: "ubuntu:{{ windows_containers_ubuntu_version }}" +windows_containers_container_name: "ubuntu-container" +windows_containers_container_port: "2222" +windows_containers_ssh_enabled: false +windows_containers_ssh_users: + - user: wsluser + key: "" + +# --- WSL2 bootstrap defaults --- +windows_containers_wsl_enabled: true +windows_containers_wsl_features: + - Microsoft-Windows-Subsystem-Linux + - VirtualMachinePlatform +windows_containers_wsl_reboot: true +windows_containers_wsl_default_version: 2 +windows_containers_wsl_install_no_distribution: true + +# --- Dedicated service account --- +windows_containers_service_user: podman +windows_containers_service_password: "" +windows_containers_service_password_never_expires: true + +windows_containers_machine_cpus: 2 +windows_containers_machine_memory: 2048 +windows_containers_machine_disk_size: 10000 diff --git a/ansible/roles/windows_containers/handlers/main.yml b/ansible/roles/windows_containers/handlers/main.yml new file mode 100644 index 0000000..8de9e68 --- /dev/null +++ b/ansible/roles/windows_containers/handlers/main.yml @@ -0,0 +1,12 @@ +--- +- name: Restart podman machine + ansible.windows.win_shell: podman machine stop; podman machine start + failed_when: false + tags: + - roles::windows_containers + +- name: Restart Ubuntu container + ansible.windows.win_shell: podman restart {{ windows_containers_container_name }} + failed_when: false + tags: + - roles::windows_containers diff --git a/ansible/roles/windows_containers/meta/main.yml b/ansible/roles/windows_containers/meta/main.yml new file mode 100644 index 0000000..a9982dd --- /dev/null +++ b/ansible/roles/windows_containers/meta/main.yml @@ -0,0 +1,15 @@ +--- +galaxy_info: + author: Alexandre Pires + description: Install Mirantis Container Runtime on Windows Server and Podman on Windows Workstation + company: A13Labs + role_name: windows_containers + namespace: a13labs + license: MIT + min_ansible_version: "2.1" + platforms: + - name: Windows + categories: [] + +dependencies: + - role: nssm diff --git a/ansible/roles/windows_containers/tasks/bootstrap_wsl.yml b/ansible/roles/windows_containers/tasks/bootstrap_wsl.yml new file mode 100644 index 0000000..5549365 --- /dev/null +++ b/ansible/roles/windows_containers/tasks/bootstrap_wsl.yml @@ -0,0 +1,106 @@ +--- +# ── Phase 0: WSL2 prerequisite bootstrap ───────────────────────────────── + +- name: Enable Microsoft-Windows-Subsystem-Linux feature + ansible.windows.win_feature: + name: Microsoft-Windows-Subsystem-Linux + state: present + include_sub_features: true + include_management_tools: true + register: __wsl_feature + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + tags: + - roles::windows_containers::wsl_bootstrap + +- name: Enable Virtual Machine Platform feature + ansible.windows.win_feature: + name: VirtualMachinePlatform + state: present + include_sub_features: true + include_management_tools: true + register: __vmp_feature + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + tags: + - roles::windows_containers::wsl_bootstrap + when: ansible_facts.os_product_type == "workstation" + +- name: Enable Hyper-V feature + ansible.windows.win_feature: + name: Hyper-V + state: present + include_sub_features: true + include_management_tools: true + register: __hyperv_feature + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + tags: + - roles::windows_containers::wsl_bootstrap + when: ansible_facts.os_product_type == "server" + +- name: Enable Containers feature + ansible.windows.win_feature: + name: Containers + state: present + include_sub_features: true + include_management_tools: true + register: __containers_feature + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + tags: + - roles::windows_containers::wsl_bootstrap + when: ansible_facts.os_product_type == "server" + +- name: Reboot if WSL features were changed + ansible.windows.win_reboot: + reboot_timeout: 600 + pre_reboot_delay: 5 + post_reboot_delay: 15 + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + when: + - windows_containers_wsl_reboot | default(true) + - __wsl_feature.changed or __vmp_feature.changed or __hyperv_feature.changed or __containers_feature.changed + tags: + - roles::windows_containers::wsl_bootstrap + +- name: Download WSL2 kernel update package + ansible.windows.win_get_url: + url: "https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi" + dest: "{{ ansible_env.TEMP | default('C:\\Windows\\Temp') }}\\wsl_update_x64.msi" + force: false + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + tags: + - roles::windows_containers::wsl_bootstrap + +- name: Install WSL2 kernel update package + ansible.windows.win_shell: | + Start-Process "msiexec.exe" -ArgumentList "/i {{ ansible_env.TEMP | default('C:\\Windows\\Temp') }}\\wsl_update_x64.msi /quiet" -NoNewWindow -Wait + exit $LASTEXITCODE + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + register: __wsl_update_result + changed_when: __wsl_update_result.rc == 0 + tags: + - roles::windows_containers::wsl_bootstrap + +- name: Run wsl update to update WSL2 kernel + ansible.windows.win_shell: | + wsl --update + exit $LASTEXITCODE + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + register: __wsl_set_default_result + changed_when: __wsl_set_default_result.rc == 0 + tags: + - roles::windows_containers::wsl_bootstrap diff --git a/ansible/roles/windows_containers/tasks/containers_install.yml b/ansible/roles/windows_containers/tasks/containers_install.yml new file mode 100644 index 0000000..7373f42 --- /dev/null +++ b/ansible/roles/windows_containers/tasks/containers_install.yml @@ -0,0 +1,118 @@ +--- +# ── Phase 1: Mirantis Container Runtime (MCR) installation ──────────────── + +- name: Check if MCR (Docker) is already installed + ansible.windows.win_shell: | + $dockerCmd = Get-Command docker -ErrorAction SilentlyContinue + if ($dockerCmd) { + $version = (& docker --version 2>&1) + Write-Output "MCR_INSTALLED:$version" + exit 0 + } + exit 1 + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + register: __mcr_installed_check + changed_when: false + failed_when: false + tags: + - roles::windows_containers::install + +- name: Download Mirantis install script + ansible.windows.win_get_url: + url: "{{ windows_containers_mcr_install_script_url }}" + dest: "{{ ansible_env.TEMP | default('C:\\Windows\\Temp') }}\\mcr-install.ps1" + force: false + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + when: __mcr_installed_check.rc != 0 + tags: + - roles::windows_containers::install + +- name: Set execution policy for current process + ansible.windows.win_shell: | + Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Force -Scope Process + exit 0 + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + when: __mcr_installed_check.rc != 0 + changed_when: false + tags: + - roles::windows_containers::install + +- name: Install Mirantis Container Runtime + ansible.windows.win_shell: | + & "{{ ansible_env.TEMP | default('C:\\Windows\\Temp') }}\\mcr-install.ps1" \ + -Channel "{{ windows_containers_mcr_channel }}" \ + -DockerVersion "{{ windows_containers_mcr_version }}" + exit $LASTEXITCODE + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + register: __mcr_install_result + changed_when: __mcr_install_result.rc == 0 + when: __mcr_installed_check.rc != 0 + retries: 3 + delay: 30 + tags: + - roles::windows_containers::install + +- name: Clean up MCR install script + ansible.windows.win_file: + path: "{{ ansible_env.TEMP | default('C:\\Windows\\Temp') }}\\mcr-install.ps1" + state: absent + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + when: __mcr_installed_check.rc != 0 + tags: + - roles::windows_containers::install + +- name: Disable MCR telemetry (optional) + ansible.windows.win_copy: + content: '{"features":{"telemetry": false}}' + dest: "C:\\ProgramData\\docker\\config\\daemon.json" + mode: "0644" + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + notify: Restart Docker service + when: windows_containers_disable_telemetry | default(false) + tags: + - roles::windows_containers::install + +- name: Restart Docker service + ansible.windows.win_shell: | + Restart-Service -Name docker -Force -ErrorAction SilentlyContinue + exit 0 + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + when: __mcr_installed_check.rc != 0 + changed_when: false + tags: + - roles::windows_containers::install + +- name: Verify MCR (Docker) is running + ansible.windows.win_shell: | + $dockerdRunning = Get-Service -Name docker -ErrorAction SilentlyContinue + if ($dockerdRunning -and $dockerdRunning.Status -eq 'Running') { + $version = (& docker --version 2>&1) + Write-Output "MCR_SERVICE_RUNNING:$version" + exit 0 + } + Write-Error "Docker service is not running" + exit 1 + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + register: __mcr_service_verify + changed_when: false + until: __mcr_service_verify.rc == 0 + retries: 10 + delay: 10 + tags: + - roles::windows_containers::install diff --git a/ansible/roles/windows_containers/tasks/main.yml b/ansible/roles/windows_containers/tasks/main.yml new file mode 100644 index 0000000..f7ffad4 --- /dev/null +++ b/ansible/roles/windows_containers/tasks/main.yml @@ -0,0 +1,37 @@ +--- +# ── Phase 1: WSL2 prerequisites ───────────────────────────────────────── +- name: Bootstrap WSL2 prerequisites + ansible.builtin.include_tasks: bootstrap_wsl.yml + tags: + - roles::windows_containers + - roles::windows_containers::wsl_bootstrap + +# ── Phase 0: Docker Engine (Windows Server) ────────────────────────────── +- name: Install Docker Engine for Windows Server (Mirantis Container Runtime) + ansible.builtin.include_tasks: containers_install.yml + tags: + - roles::windows_containers + - roles::windows_containers::install + +# ── Phase 2: Podman install, user, NSSM service ───────────────────────── +- name: Install Podman + ansible.builtin.include_tasks: podman_install.yml + tags: + - roles::windows_containers + - roles::windows_containers::podman_install + +# ── Phase 2: Ubuntu container ──────────────────────────────────────────── +- name: Setup Ubuntu container + ansible.builtin.include_tasks: ubuntu_container.yml + tags: + - roles::windows_containers + - roles::windows_containers::ubuntu_container + when: ansible_facts.os_product_type == "workstation" + +# ── Phase 3: Verification ──────────────────────────────────────────────── +- name: Verify installation + ansible.builtin.include_tasks: verify.yml + tags: + - roles::windows_containers + - roles::windows_containers::verify + when: ansible_facts.os_product_type == "workstation" diff --git a/ansible/roles/windows_containers/tasks/podman_install.yml b/ansible/roles/windows_containers/tasks/podman_install.yml new file mode 100644 index 0000000..6c2a06b --- /dev/null +++ b/ansible/roles/windows_containers/tasks/podman_install.yml @@ -0,0 +1,389 @@ +--- +# ── Phase 1: Podman installer download & install ───────────────────────── + +- name: Check if Podman is already installed + ansible.windows.win_shell: | + if (Get-Command podman -ErrorAction SilentlyContinue) { + Write-Output "__PODMAN_INSTALLED__" + exit 0 + } + exit 1 + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + register: __podman_installed_check + changed_when: false + failed_when: false + tags: + - roles::windows_containers::podman_install + +- name: Download Podman installer on remote host + ansible.windows.win_get_url: + url: "{{ windows_containers_installer_url }}" + dest: "{{ ansible_env.TEMP | default('C:\\Windows\\Temp') }}\\podman-installer.exe" + force: false + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + when: __podman_installed_check.rc != 0 + tags: + - roles::windows_containers::podman_install + +- name: Install Podman silently + ansible.windows.win_shell: | + Start-Process "{{ ansible_env.TEMP | default('C:\\Windows\\Temp') }}\podman-installer.exe" -ArgumentList @("/S", "/D={{ windows_containers_install_dir }}") -Wait + exit $LASTEXITCODE + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + register: __podman_install_result + changed_when: __podman_install_result.rc == 0 + when: __podman_installed_check.rc != 0 + tags: + - roles::windows_containers::podman_install + +- name: Clean up Podman installer + ansible.windows.win_file: + path: "{{ ansible_env.TEMP | default('C:\\Windows\\Temp') }}\\podman-installer.exe" + state: absent + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + when: __podman_installed_check.rc != 0 + tags: + - roles::windows_containers::podman_install + +- name: Add Podman to PATH if not already present + ansible.windows.win_path: + name: Path + elements: + - "{{ windows_containers_install_dir }}" + state: present + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + when: __podman_installed_check.rc != 0 + tags: + - roles::windows_containers::podman_install + +# ── Phase 2: Dedicated service user ────────────────────────────────────── + +- name: Fail when podman service password is missing + ansible.builtin.fail: + msg: >- + windows_containers_service_password must be provided to configure service account + .\\{{ windows_containers_service_user }} for NSSM. + when: windows_containers_service_password | length == 0 + tags: + - roles::windows_containers::podman_install + +- name: Create dedicated podman service user + ansible.windows.win_user: + name: "{{ windows_containers_service_user }}" + password: "{{ windows_containers_service_password }}" + account_disabled: false + password_never_expires: true + user_cannot_change_password: true + state: present + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + when: + - windows_containers_service_password | length > 0 + no_log: true + tags: + - roles::windows_containers::podman_install + +- name: Get localized Users group name via PowerShell + ansible.windows.win_powershell: + script: | + $sid = New-Object System.Security.Principal.SecurityIdentifier("S-1-5-32-545") + $ntAccount = $sid.Translate([System.Security.Principal.NTAccount]) + # Handle both local and domain accounts - take the last part after backslash + $parts = $ntAccount.Value -split '\\' + $parts[-1] + register: __users_group_name + changed_when: false + tags: + - roles::windows_containers::podman_install + +- name: Debug localized Users group name + ansible.builtin.debug: + msg: "Localized Users group name is '{{ __users_group_name.output | first }}'" + tags: + - roles::windows_containers::podman_install + +- name: Add podman service user to Users group for interactive login + ansible.windows.win_group_membership: + name: "{{ __users_group_name.output | first }}" + members: + - ".\\{{ windows_containers_service_user }}" + state: present + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + when: + - windows_containers_service_password | length > 0 + tags: + - roles::windows_containers::podman_install + +- name: Grant "Log on as a service" right to podman service account + ansible.windows.win_user_right: + name: SeServiceLogonRight + users: + - ".\\{{ windows_containers_service_user }}" + action: add + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + when: + - windows_containers_service_password | length > 0 + tags: + - roles::windows_containers::podman_install + +- name: Grant "Log on locally" right to podman service account (required for Ansible become) + ansible.windows.win_user_right: + name: SeInteractiveLogonRight + users: + - ".\\{{ windows_containers_service_user }}" + action: add + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + when: + - windows_containers_service_password | length > 0 + tags: + - roles::windows_containers::podman_install + +- name: Grant "Log on as a batch job" right to podman service account + ansible.windows.win_user_right: + name: SeBatchLogonRight + users: + - ".\\{{ windows_containers_service_user }}" + action: add + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + when: + - windows_containers_service_password | length > 0 + tags: + - roles::windows_containers::podman_install + +# ── Phase 3: Podman machine setup ────────────────────────────────────── + +- name: Get Podman executable path + ansible.windows.win_shell: where.exe podman.exe 2>&1 + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + register: __podman_where + changed_when: false + failed_when: false + tags: + - roles::windows_containers::podman_install + +- name: Set Podman executable path from where output + ansible.builtin.set_fact: + __podman_exe: "{{ (__podman_where.stdout_lines | last).strip() }}" + when: + - __podman_where is not failed + - __podman_where.stdout | trim != "" + +- name: Default Podman executable path if not on PATH or where failed + ansible.builtin.set_fact: + __podman_exe: "{{ windows_containers_install_dir }}\\podman.exe" + when: __podman_where is failed or __podman_where.stdout | trim == "" + +# ── Phase 4: Podman machine init ──────────────────────────────────────── + +- name: Initialize Podman machine + ansible.windows.win_shell: | + $machinesJson = (podman machine ls --format json 2>$null | Out-String).Trim() + $machines = @() + if ($machinesJson) { + $machines = $machinesJson | ConvertFrom-Json + } + + $machine = $machines | Where-Object { $_.Name -eq "{{ windows_containers_machine_name }}" } | Select-Object -First 1 + if (-not $machine) { + podman machine init "{{ windows_containers_machine_name }}" --cpus {{ windows_containers_machine_cpus }} --memory {{ windows_containers_machine_memory }} --disk-size {{ windows_containers_machine_disk_size }} + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to initialize Podman machine {{ windows_containers_machine_name }}" + exit 1 + } + + $afterInitJson = (podman machine ls --format json 2>$null | Out-String).Trim() + $afterInitMachines = @() + if ($afterInitJson) { + $afterInitMachines = $afterInitJson | ConvertFrom-Json + } + + $createdMachine = $afterInitMachines | Where-Object { $_.Name -eq "{{ windows_containers_machine_name }}" } | Select-Object -First 1 + if (-not $createdMachine) { + Write-Error "Podman machine {{ windows_containers_machine_name }} is still missing after init" + exit 1 + } + + Write-Output "__PODMAN_MACHINE_CREATED__" + } + become: true + become_method: ansible.builtin.runas + become_user: "{{ windows_containers_service_user }}" + register: __podman_init + changed_when: "'__PODMAN_MACHINE_CREATED__' in (__podman_init.stdout + __podman_init.stderr)" + tags: + - roles::windows_containers::podman_install + +- name: Start Podman machine on boot via NSSM service only + ansible.windows.win_shell: | + # Podman machine will be started on boot via NSSM service (PodmanAutoStart) + # No need to start it during playbook execution + podman machine status "{{ windows_containers_machine_name }}" 2>$null | Out-Null + $status = $LASTEXITCODE + if ($status -eq 0) { + Write-Output "Podman machine {{ windows_containers_machine_name }} is already running" + exit 0 + } + Write-Output "Podman machine not running (will start on boot)" + exit 0 + become: true + become_method: ansible.builtin.runas + become_user: "{{ windows_containers_service_user }}" + register: __podman_status_check + changed_when: false + failed_when: false + tags: + - roles::windows_containers::podman_install + +- name: Deploy Podman auto-start PowerShell script + ansible.windows.win_template: + src: podman-auto-start.ps1.j2 + dest: "{{ windows_containers_install_dir }}\\auto-start-podman.ps1" + mode: "0644" + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + tags: + - roles::windows_containers::podman_install + +- name: Deploy batch wrapper to avoid path-with-spaces issues in NSSM + ansible.windows.win_template: + src: podman-auto-start.bat.j2 + dest: "{{ windows_containers_install_dir }}\\auto-start-podman.bat" + mode: "0644" + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + tags: + - roles::windows_containers::podman_install + +- name: Delete existing PodmanAutoStart service via NSSM + ansible.windows.win_shell: | + $existing = Get-CimInstance Win32_Service -Filter "Name='PodmanAutoStart'" -ErrorAction SilentlyContinue + if ($existing) { + Stop-Service -Name PodmanAutoStart -Force -ErrorAction SilentlyContinue + Start-Sleep -Seconds 2 + $null = nssm.exe remove PodmanAutoStart confirm + Start-Sleep -Seconds 2 + } + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + changed_when: true + failed_when: false + no_log: true + tags: + - roles::windows_containers::podman_install + +- name: Register Podman auto-start service via NSSM (batch wrapper for proper path quoting) + ansible.windows.win_shell: | + $installDir = "{{ windows_containers_install_dir }}" + $batchPath = Join-Path $installDir "auto-start-podman.bat" + nssm.exe install PodmanAutoStart "`"$batchPath`"" + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + changed_when: false + tags: + - roles::windows_containers::podman_install + +- name: Create log directory for Podman auto-start + ansible.windows.win_file: + path: C:\Logs\Podman + state: directory + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + tags: + - roles::windows_containers::podman_install + +- name: Grant podman service user full control over log directory + ansible.windows.win_acl: + path: C:\Logs\Podman + user: ".\\{{ windows_containers_service_user }}" + rights: FullControl + type: allow + state: present + inherit: ContainerInherit, ObjectInherit + propagation: None + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + tags: + - roles::windows_containers::podman_install + +- name: Configure Podman auto-start service via NSSM + ansible.windows.win_shell: | + $installDir = "{{ windows_containers_install_dir }}" + nssm.exe set PodmanAutoStart AppDirectory $installDir + nssm.exe set PodmanAutoStart DisplayName "Podman Auto Start" + nssm.exe set PodmanAutoStart Description "Starts Podman machine and containers on boot" + nssm.exe set PodmanAutoStart Start SERVICE_AUTO_START + nssm.exe set PodmanAutoStart AppRestartDelay 30000 + nssm.exe set PodmanAutoStart AppStdout "C:\Logs\Podman\auto-start.log" + nssm.exe set PodmanAutoStart AppStderr "C:\Logs\Podman\auto-start-error.log" + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + changed_when: false + tags: + - roles::windows_containers::podman_install + +- name: Set PodmanAutoStart service account via NSSM + ansible.windows.win_shell: | + nssm.exe set PodmanAutoStart ObjectName ".\{{ windows_containers_service_user }}" "{{ windows_containers_service_password }}" + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + changed_when: true + no_log: true + tags: + - roles::windows_containers::podman_install + +- name: Start PodmanAutoStart service + ansible.windows.win_shell: | + $svc = Get-CimInstance Win32_Service -Filter "Name='PodmanAutoStart'" -ErrorAction SilentlyContinue + if ($svc -and $svc.State -ne 'Running') { + nssm.exe start PodmanAutoStart + Start-Sleep -Seconds 5 + $svc = Get-CimInstance Win32_Service -Filter "Name='PodmanAutoStart'" + Write-Output "PodmanAutoStart service state: $($svc.State)" + } else { + Write-Output "PodmanAutoStart service already running or not found" + } + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + register: __podman_auto_start_service + changed_when: "'state: Running' in (__podman_auto_start_service.stdout)" + failed_when: false + tags: + - roles::windows_containers::podman_install +# ansible.windows.win_shell: podman pull "{{ windows_containers_image }}" +# become: true +# become_method: ansible.builtin.runas +# become_user: "{{ windows_containers_service_user }}" +# register: __podman_pull +# changed_when: false +# tags: +# - roles::windows_containers::podman_install diff --git a/ansible/roles/windows_containers/tasks/ubuntu_container.yml b/ansible/roles/windows_containers/tasks/ubuntu_container.yml new file mode 100644 index 0000000..a7c6400 --- /dev/null +++ b/ansible/roles/windows_containers/tasks/ubuntu_container.yml @@ -0,0 +1,114 @@ +--- +- name: Inspect Ubuntu container state + ansible.windows.win_shell: | + $containersJson = (podman ps -a --filter "name=^{{ windows_containers_container_name }}$" --format json 2>$null | Out-String).Trim() + if (-not $containersJson -or $containersJson -eq "[]") { + Write-Output "__CONTAINER_ABSENT__" + exit 0 + } + + $containers = $containersJson | ConvertFrom-Json + $container = $containers | Select-Object -First 1 + if ($container) { + Write-Output "__CONTAINER_PRESENT__" + if ($container.State -eq "running") { + Write-Output "__CONTAINER_RUNNING__" + } + } + register: container_state + changed_when: false + tags: + - roles::windows_containers::ubuntu_container + +- name: Create Ubuntu container + ansible.windows.win_shell: podman run -d --name {{ windows_containers_container_name }} -p {{ windows_containers_container_port }}:22 --restart unless-stopped "{{ windows_containers_image }}" + when: "'__CONTAINER_ABSENT__' in (container_state.stdout + container_state.stderr)" + tags: + - roles::windows_containers::ubuntu_container + +- name: Start Ubuntu container when stopped + ansible.windows.win_shell: podman start {{ windows_containers_container_name }} + when: + - "'__CONTAINER_PRESENT__' in (container_state.stdout + container_state.stderr)" + - "'__CONTAINER_RUNNING__' not in (container_state.stdout + container_state.stderr)" + tags: + - roles::windows_containers::ubuntu_container + +- name: Wait for container to be ready + ansible.windows.win_shell: | + $status = (podman inspect --format "{{ '{{.State.Running}}' }}" {{ windows_containers_container_name }} 2>$null | Out-String).Trim() + if ($LASTEXITCODE -eq 0 -and $status -eq "true") { + exit 0 + } + Write-Error "Container {{ windows_containers_container_name }} is not running yet" + exit 1 + register: container_ready + until: container_ready.rc == 0 + retries: 20 + delay: 5 + tags: + - roles::windows_containers::ubuntu_container + +- name: Set up SSH in Ubuntu container + ansible.windows.win_shell: | + podman exec {{ windows_containers_container_name }} sh -c 'mkdir -p /home/{{ windows_containers_user }}/.ssh' + podman exec {{ windows_containers_container_name }} sh -c 'touch /home/{{ windows_containers_user }}/.ssh/authorized_keys' + podman exec {{ windows_containers_container_name }} sh -c 'chown -R {{ windows_containers_user }}:$(id -g {{ windows_containers_user }}) /home/{{ windows_containers_user }}/.ssh' + podman exec {{ windows_containers_container_name }} sh -c 'chmod 700 /home/{{ windows_containers_user }}/.ssh && chmod 600 /home/{{ windows_containers_user }}/.ssh/authorized_keys' + register: ssh_dir_setup + changed_when: false + tags: + - roles::windows_containers::ubuntu_container + +- name: Deploy SSH authorized_keys + ansible.windows.win_shell: | + $keysJson = '{{ windows_containers_ssh_users | map(attribute="key") | list | to_json }}' + $keys = @() + if ($keysJson) { + $keys = $keysJson | ConvertFrom-Json + } + $keys = $keys | Where-Object { $_ -and $_.Trim() -ne "" } | ForEach-Object { $_.Trim() } | Sort-Object -Unique + + $desiredContent = "" + if ($keys.Count -gt 0) { + $desiredContent = ($keys -join "`n") + "`n" + } + + $currentContent = (podman exec {{ windows_containers_container_name }} sh -c "cat /home/{{ windows_containers_user }}/.ssh/authorized_keys 2>/dev/null || true" | Out-String) + if ($currentContent -ne $desiredContent) { + $encodedContent = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($desiredContent)) + podman exec {{ windows_containers_container_name }} sh -c "echo $encodedContent | base64 -d > /home/{{ windows_containers_user }}/.ssh/authorized_keys" + podman exec {{ windows_containers_container_name }} sh -c 'chown {{ windows_containers_user }}:$(id -g {{ windows_containers_user }}) /home/{{ windows_containers_user }}/.ssh/authorized_keys' + podman exec {{ windows_containers_container_name }} sh -c 'chmod 600 /home/{{ windows_containers_user }}/.ssh/authorized_keys' + Write-Output "__AUTHORIZED_KEYS_UPDATED__" + } + register: authorized_keys_result + changed_when: "'__AUTHORIZED_KEYS_UPDATED__' in (authorized_keys_result.stdout + authorized_keys_result.stderr)" + tags: + - roles::windows_containers::ubuntu_container + +- name: Enable and start sshd in Ubuntu container + ansible.windows.win_shell: | + $changed = $false + + podman exec {{ windows_containers_container_name }} sh -c "dpkg -s openssh-server >/dev/null 2>&1" + if ($LASTEXITCODE -ne 0) { + podman exec {{ windows_containers_container_name }} sh -c "apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y openssh-server" + $changed = $true + } + + podman exec {{ windows_containers_container_name }} sh -c "mkdir -p /run/sshd" + + podman exec {{ windows_containers_container_name }} sh -c "pgrep -x sshd >/dev/null 2>&1" + if ($LASTEXITCODE -ne 0) { + podman exec {{ windows_containers_container_name }} /usr/sbin/sshd + $changed = $true + } + + if ($changed) { + Write-Output "__SSHD_PROVISIONED__" + } + register: sshd_result + changed_when: "'__SSHD_PROVISIONED__' in (sshd_result.stdout + sshd_result.stderr)" + tags: + - roles::windows_containers::ubuntu_container diff --git a/ansible/roles/windows_containers/tasks/verify.yml b/ansible/roles/windows_containers/tasks/verify.yml new file mode 100644 index 0000000..276d109 --- /dev/null +++ b/ansible/roles/windows_containers/tasks/verify.yml @@ -0,0 +1,100 @@ +--- +# ── WSL verification ───────────────────────────────────────────────────── + +- name: Verify WSL is operational + ansible.windows.win_shell: wsl.exe --status --default + register: __wsl_verify + changed_when: false + failed_when: __wsl_verify.rc != 0 + tags: + - roles::windows_containers::verify + +# ── Podman verification ────────────────────────────────────────────────── + +- name: Verify Podman is installed + ansible.windows.win_shell: podman version + register: __podman_version + changed_when: false + tags: + - roles::windows_containers::verify + +- name: Verify Podman machine exists + ansible.windows.win_shell: | + $machinesJson = (podman machine ls --format json 2>$null | Out-String).Trim() + if (-not $machinesJson) { + Write-Error "No Podman machines found" + exit 1 + } + $machines = $machinesJson | ConvertFrom-Json + $machine = $machines | Where-Object { $_.Name -eq "{{ windows_containers_machine_name }}" } | Select-Object -First 1 + if (-not $machine) { + Write-Error "Podman machine {{ windows_containers_machine_name }} not found" + exit 1 + } + Write-Output "__PODMAN_MACHINE_EXISTS__" + register: __podman_machine_ls + changed_when: false + failed_when: "'__PODMAN_MACHINE_EXISTS__' not in (__podman_machine_ls.stdout + __podman_machine_ls.stderr)" + tags: + - roles::windows_containers::verify + +# ── NSSM service verification ──────────────────────────────────────────── + +- name: Verify NSSM Podman machine service is installed and running + ansible.windows.win_shell: | + $svc = (Get-Service -Name "{{ windows_containers_service_name }}" -ErrorAction SilentlyContinue) + if (-not $svc) { Write-Error "Service {{ windows_containers_service_name }} not found"; exit 1 } + if ($svc.StartType -ne 'Automatic') { Write-Error "Service {{ windows_containers_service_name }} is not Automatic"; exit 1 } + Write-Output "Service: $($svc.DisplayName) | Status: $($svc.Status) | StartType: $($svc.StartType)" + Write-Output "__NSSM_SERVICE_OK__" + register: __nssm_verify + changed_when: false + failed_when: "__NSSM_SERVICE_OK__ not in (__nssm_verify.stdout + __nssm_verify.stderr)" + tags: + - roles::windows_containers::verify + +- name: Verify NSSM service runs as dedicated podman user + ansible.windows.win_shell: | + $svc = Get-CimInstance -ClassName Win32_Service -Filter "Name='{{ windows_containers_service_name }}'" + $owner = $svc.StartName + if ($owner -eq ".\\{{ windows_containers_service_user }}" -or $owner -eq "{{ windows_containers_service_user }}") { + Write-Output "Service runs as $owner" + Write-Output "__NSSM_USER_OK__" + } else { + Write-Error "Expected service to run as .\\{{ windows_containers_service_user }}, got $owner" + exit 1 + } + register: __nssm_user_verify + changed_when: false + failed_when: "__NSSM_USER_OK__ not in (__nssm_user_verify.stdout + __nssm_user_verify.stderr)" + tags: + - roles::windows_containers::verify + +# ── Container verification ─────────────────────────────────────────────── + +- name: Verify Ubuntu container is running + ansible.windows.win_shell: podman inspect --format "{{ '{{.State.Running}}' }}" {{ windows_containers_container_name }} + register: __container_status + changed_when: false + failed_when: __container_status.rc != 0 or __container_status.stdout | trim != 'true' + tags: + - roles::windows_containers::verify + +- name: Verify SSH access to Ubuntu container + ansible.windows.win_shell: podman exec {{ windows_containers_container_name }} id {{ windows_containers_user }} + register: __user_check + changed_when: false + failed_when: __user_check.rc != 0 + tags: + - roles::windows_containers::verify + +- name: Report SSH connection details + ansible.windows.win_shell: | + Write-Output "Podman is installed" + Write-Output "WSL version: wsl.exe --version" + Write-Output "NSSM service: {{ windows_containers_service_name }}" + Write-Output "Ubuntu container: {{ windows_containers_container_name }}" + Write-Output "SSH access: ssh {{ windows_containers_user }}@localhost -p {{ windows_containers_container_port }}" + changed_when: false + tags: + - roles::windows_containers::verify diff --git a/ansible/roles/windows_containers/templates/podman-auto-start.bat.j2 b/ansible/roles/windows_containers/templates/podman-auto-start.bat.j2 new file mode 100644 index 0000000..d06ef91 --- /dev/null +++ b/ansible/roles/windows_containers/templates/podman-auto-start.bat.j2 @@ -0,0 +1,2 @@ +@echo off +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0auto-start-podman.ps1" diff --git a/ansible/roles/windows_containers/templates/podman-auto-start.ps1.j2 b/ansible/roles/windows_containers/templates/podman-auto-start.ps1.j2 new file mode 100644 index 0000000..5900745 --- /dev/null +++ b/ansible/roles/windows_containers/templates/podman-auto-start.ps1.j2 @@ -0,0 +1,71 @@ +# Podman auto-start on Windows boot +# Runs as {{ windows_containers_service_user }} (set via NSSM ObjectName) + +$ErrorActionPreference = "Stop" +$LogDir = "C:\Logs\Podman" +$LogTimestamp = Get-Date -Format "yyyy-MM-dd_HH-mm-ss" +$MachineName = "{{ windows_containers_machine_name }}" + +function Write-Log { + param([string]$LogName, [System.Collections.ArrayList]$Messages) + $Log = Join-Path $LogDir "$LogName-$LogTimestamp.log" + $lines = @() + foreach ($m in $Messages) { + $lines += "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') $m" + } + [System.IO.File]::WriteAllText($Log, ($lines -join "`r`n") + "`r`n", [System.Text.Encoding]::UTF8) +} + +try { + $bootMessages = [System.Collections.ArrayList]::new() + $bootMessages.Add("Starting Podman auto-start process") > $null + + # Check if machine is already running + $listOutput = & "{{ windows_containers_install_dir }}\podman.exe" @('machine', 'list', '--format', 'table {{ '{{' }}.Name{{ '}}' }}\t{{ '{{' }}.Running{{ '}}' }}') 2>&1 | Out-String + $listExit = $LASTEXITCODE + $bootMessages.Add("Machine list exit code: $listExit") > $null + $bootMessages.Add("Machine list output: $listOutput") > $null + + $running = ($listOutput -match "$MachineName\s+true") + if ($running) { + $bootMessages.Add("Machine is already running") > $null + } else { + $startOutput = & "{{ windows_containers_install_dir }}\podman.exe" @('machine', 'start', $MachineName) 2>&1 | Out-String + $bootMessages.Add("Machine start output: $startOutput") > $null + if ($LASTEXITCODE -ne 0) { + Write-Log "auto-start" $bootMessages + exit 1 + } + } + + # Wait for the machine to be ready (poll podman info) + $bootMessages.Add("Waiting for machine to be ready...") > $null + $ready = $false + for ($i = 0; $i -lt 90; $i++) { + $infoOutput = & "{{ windows_containers_install_dir }}\podman.exe" @('info', '--format', 'json') 2>&1 | Out-String + if ($LASTEXITCODE -eq 0 -and $infoOutput -and $infoOutput.Trim()) { + $ready = true + $bootMessages.Add("Machine is ready after $(($i + 1) * 2) seconds") > $null + break + } + Start-Sleep -Seconds 2 + } + + if (-not $ready) { + Write-Log "auto-start" $bootMessages + exit 1 + } + + # Start any stopped containers + $startContainersOutput = & "{{ windows_containers_install_dir }}\podman.exe" @('start', '-a') 2>&1 | Out-String + $bootMessages.Add("Container start output: $startContainersOutput") > $null + $bootMessages.Add("Podman auto-start complete") > $null + Write-Log "auto-start" $bootMessages + +} catch { + $errMessages = [System.Collections.ArrayList]::new() + $errMessages.Add("Podman auto-start failed: $_") > $null + $errMessages.Add("Stack trace: $($_.ScriptStackTrace)") > $null + Write-Log "auto-start-ERROR" $errMessages + exit 1 +} diff --git a/ansible/roles/windows_prometheus/defaults/main.yml b/ansible/roles/windows_prometheus/defaults/main.yml new file mode 100644 index 0000000..66d73b6 --- /dev/null +++ b/ansible/roles/windows_prometheus/defaults/main.yml @@ -0,0 +1,18 @@ +--- +prometheus_version: "3.6.0" +prometheus_install_dir: "C:\\Prometheus" +prometheus_listen_address: "0.0.0.0:9090" +windows_exporter_version: "0.31.3" +windows_exporter_collectors: + - cache + - cpu + - cpu_info + - disk_drive + - memory + - net + - os + - process + - service + - tcp + - time + - update diff --git a/ansible/roles/windows_prometheus/meta/main.yml b/ansible/roles/windows_prometheus/meta/main.yml index 76e0fd2..89c5252 100644 --- a/ansible/roles/windows_prometheus/meta/main.yml +++ b/ansible/roles/windows_prometheus/meta/main.yml @@ -8,4 +8,4 @@ galaxy_info: license: MIT min_ansible_version: "2.1" platforms: - - name: windows + - name: Windows diff --git a/ansible/roles/windows_prometheus/tasks/main.yml b/ansible/roles/windows_prometheus/tasks/main.yml index b0ae8a5..d759739 100644 --- a/ansible/roles/windows_prometheus/tasks/main.yml +++ b/ansible/roles/windows_prometheus/tasks/main.yml @@ -1,25 +1,16 @@ --- # Install Prometheus + Windows Exporter on Windows and open firewall for Grafana -- name: Define Prometheus Versions and install dir +- name: Define internal paths ansible.builtin.set_fact: - prometheus_version: "{{ prometheus_version | default('3.6.0') }}" - prometheus_install_dir: "{{ prometheus_install_dir | default('C:\\Prometheus') }}" - prometheus_listen_address: "{{ prometheus_listen_address | default('0.0.0.0:9090') }}" - nssm_dir: "{{ nssm_dir | default('C:\\nssm') }}" - nssm_version: "{{ nssm_version | default('2.24') }}" - windows_exporter_version: "{{ windows_exporter_version | default('0.31.3') }}" - windows_exporter_collectors: "{{ windows_exporter_collectors | default(['cache', 'cpu', 'cpu_info', 'diskdrive', 'memory', 'net', 'os', 'process', 'service', 'tcp', 'time', 'update']) }}" - -- name: Define Windows Exporter defaults - ansible.builtin.set_fact: - __nssm_url: "https://nssm.cc/release/nssm-{{ nssm_version }}.zip" - __prometheus_zip_url: "https://github.com/prometheus/prometheus/releases/download/v{{ prometheus_version }}/prometheus-{{ prometheus_version }}.windows-amd64.zip" + __prometheus_zip_url: >- + https://github.com/prometheus/prometheus/releases/download/v{{ + prometheus_version }}/prometheus-{{ prometheus_version }}.windows-amd64.zip __prometheus_zip_path: "{{ (ansible_env.TEMP | default('C:\\Windows\\Temp')) }}\\prometheus-{{ prometheus_version }}.zip" __prometheus_home: "{{ prometheus_install_dir }}\\prometheus-{{ prometheus_version }}.windows-amd64" - __windows_exporter_msi_url: "https://github.com/prometheus-community/windows_exporter/releases/download/v{{ windows_exporter_version }}/windows_exporter-{{ windows_exporter_version }}-amd64.msi" - __windows_exporter_msi_path: "{{ (ansible_env.TEMP | default('C:\\Windows\\Temp')) }}\\windows_exporter-{{ windows_exporter_version }}-amd64.msi" - __nssm_zip_path: "{{ (ansible_env.TEMP | default('C:\\Windows\\Temp')) }}\\nssm.zip" - __nssm_exe: "{{ nssm_dir }}\\nssm.exe" + __windows_exporter_msi_url: >- + https://github.com/prometheus-community/windows_exporter/releases/download/v{{ + windows_exporter_version }}/windows_exporter-{{ windows_exporter_version }}-amd64.msi + __windows_exporter_msi_path: "{{ (ansible_env.TEMP | default('C:\\Windows\\Temp')) }}\\windows_exporter-{{ windows_exporter_version }}.msi" - name: Create Prometheus installation directories ansible.windows.win_file: @@ -28,9 +19,8 @@ loop: - "{{ prometheus_install_dir }}" - "{{ prometheus_install_dir }}\\data" - - "{{ nssm_dir }}" -- name: Download required files (Prometheus, NSSM, Windows Exporter) +- name: Download required files (Prometheus, Windows Exporter) ansible.windows.win_get_url: url: "{{ item.url }}" dest: "{{ item.dest }}" @@ -38,8 +28,6 @@ loop: - url: "{{ __prometheus_zip_url }}" dest: "{{ __prometheus_zip_path }}" - - url: "{{ __nssm_url }}" - dest: "{{ __nssm_zip_path }}" - url: "{{ __windows_exporter_msi_url }}" dest: "{{ __windows_exporter_msi_path }}" @@ -53,8 +41,6 @@ - src: "{{ __prometheus_zip_path }}" dest: "{{ prometheus_install_dir }}" creates: "{{ __prometheus_home }}\\prometheus.exe" - - src: "{{ __nssm_zip_path }}" - dest: "{{ nssm_dir }}" - name: Verify Prometheus binary is present ansible.windows.win_stat: @@ -100,11 +86,9 @@ failed_when: promtool_check.rc != 0 when: promtool_bin.stat.exists -- name: Copy NSSM executable (win64) - ansible.windows.win_copy: - src: "{{ nssm_dir }}\\nssm-2.24\\win64\\nssm.exe" - dest: "{{ __nssm_exe }}" - remote_src: true +- name: Install NSSM + ansible.builtin.include_role: + name: nssm - name: Remove existing Prometheus service if present ansible.windows.win_service: @@ -114,7 +98,7 @@ - name: Install Prometheus service via NSSM ansible.windows.win_command: >- - "{{ __nssm_exe }}" + "nssm.exe" install Prometheus "{{ __prometheus_home }}\prometheus.exe" --config.file={{ prometheus_install_dir }}\prometheus.yml @@ -129,21 +113,21 @@ - name: Set Prometheus AppDirectory via NSSM ansible.windows.win_command: >- - "{{ __nssm_exe }}" set Prometheus AppDirectory "{{ __prometheus_home }}" + "nssm.exe" set Prometheus AppDirectory "{{ __prometheus_home }}" - name: Configure Prometheus stdout log via NSSM ansible.windows.win_command: >- - "{{ __nssm_exe }}" set Prometheus AppStdout + "nssm.exe" set Prometheus AppStdout "{{ prometheus_install_dir }}\prometheus-service.out.log" - name: Configure Prometheus stderr log via NSSM ansible.windows.win_command: >- - "{{ __nssm_exe }}" set Prometheus AppStderr + "nssm.exe" set Prometheus AppStderr "{{ prometheus_install_dir }}\prometheus-service.err.log" - name: Set Prometheus Start mode to AUTO via NSSM ansible.windows.win_command: >- - "{{ __nssm_exe }}" set Prometheus Start SERVICE_AUTO_START + "nssm.exe" set Prometheus Start SERVICE_AUTO_START - name: Ensure Prometheus service is running (skip in check mode) ansible.windows.win_service: diff --git a/ansible/roles/windows_ssh/meta/main.yml b/ansible/roles/windows_ssh/meta/main.yml index af13ce3..6525320 100644 --- a/ansible/roles/windows_ssh/meta/main.yml +++ b/ansible/roles/windows_ssh/meta/main.yml @@ -8,4 +8,4 @@ galaxy_info: license: MIT min_ansible_version: "2.1" platforms: - - name: windows + - name: Windows diff --git a/ansible/roles/windows_ssh/tasks/main.yml b/ansible/roles/windows_ssh/tasks/main.yml index cbd829b..a96c3cd 100644 --- a/ansible/roles/windows_ssh/tasks/main.yml +++ b/ansible/roles/windows_ssh/tasks/main.yml @@ -69,12 +69,14 @@ changed_when: false - name: Install OpenSSH Server capability when not installed + become: true + become_method: ansible.builtin.runas + become_user: "NT AUTHORITY\\SYSTEM" ansible.windows.win_shell: | $cap = Get-WindowsCapability -Online | Where-Object { $_.Name -like 'OpenSSH.Server*' } | Select-Object -First 1 if (-not $cap) { throw 'OpenSSH.Server capability not found on this host.' } - Add-WindowsCapability -Online -Name $cap.Name args: executable: powershell.exe @@ -149,8 +151,10 @@ if ($keys -and $keys.Count -gt 0) { foreach ($k in $keys) { $before = (Get-Acl -Path $k.FullName).Sddl - icacls.exe $k.FullName /inheritance:r - icacls.exe $k.FullName /grant:r "S-1-5-18:(F)" + icacls.exe $k.FullName /inheritance:r | Out-Null + if ($LASTEXITCODE -ne 0) { throw "icacls failed to disable inheritance on $($k.FullName)" } + icacls.exe $k.FullName /grant:r "*S-1-5-18:(F)" | Out-Null + if ($LASTEXITCODE -ne 0) { throw "icacls failed to grant SYSTEM on $($k.FullName)" } if ($removePrincipal -and $removePrincipal -notmatch '^(?i:NT AUTHORITY\\SYSTEM|SYSTEM)$') { icacls.exe $k.FullName /remove "$removePrincipal" 2>$null } @@ -181,8 +185,10 @@ if (Test-Path $p) { $before = (Get-Acl -Path $p).Sddl icacls.exe $p /inheritance:r | Out-Null - icacls.exe $p /grant:r "S-1-5-18:(OI)(CI)(F)" "S-1-5-32-544:(OI)(CI)(F)" | Out-Null - icacls.exe $p /remove "S-1-5-32-545" 2>$null + if ($LASTEXITCODE -ne 0) { throw "icacls failed to disable inheritance on $p" } + icacls.exe $p /grant:r "*S-1-5-18:(OI)(CI)(F)" "*S-1-5-32-544:(OI)(CI)(F)" | Out-Null + if ($LASTEXITCODE -ne 0) { throw "icacls failed to grant SYSTEM/Administrators on $p" } + icacls.exe $p /remove "*S-1-5-32-545" 2>$null $after = (Get-Acl -Path $p).Sddl if ($before -ne $after) { Write-Output 'ssh-dir-acl-changed' @@ -289,10 +295,34 @@ become_user: "NT AUTHORITY\\SYSTEM" - name: Deploy authorized_keys for each user - ansible.windows.win_copy: - dest: "C:\\Users\\{{ item.name }}\\.ssh\\authorized_keys" - content: "{{ (item['keys'] | default([])) | join('\r\n') }}\r\n" - force: true + ansible.windows.win_shell: | + $path = "C:\Users\{{ item.name }}\.ssh\authorized_keys" + $json = @' + {{ (item['keys'] | default([])) | to_json }} + '@ + $keys = $json | ConvertFrom-Json + if ($null -eq $keys) { + $keys = @() + } elseif ($keys -isnot [System.Array]) { + $keys = @($keys) + } + + $newContent = [string]::Join([Environment]::NewLine, $keys) + if ($keys.Count -gt 0) { + $newContent += [Environment]::NewLine + } + + $oldContent = if (Test-Path $path) { [System.IO.File]::ReadAllText($path) } else { '' } + if ($oldContent -ne $newContent) { + [System.IO.File]::WriteAllText($path, $newContent, [System.Text.Encoding]::UTF8) + Write-Output 'authorized-keys-updated' + } else { + Write-Output 'authorized-keys-unchanged' + } + args: + executable: powershell.exe + register: user_authorized_keys_result + changed_when: "'authorized-keys-updated' in user_authorized_keys_result.stdout" loop: "{{ __ssh_users__ | default([]) }}" loop_control: label: "{{ item.name }}" @@ -333,12 +363,34 @@ become_user: "NT AUTHORITY\\SYSTEM" - name: Create administrators_authorized_keys from admin users' keys (if any) - ansible.windows.win_copy: - dest: 'C:\ProgramData\ssh\administrators_authorized_keys' - content: >- - {{ (__ssh_users__ | default([]) | selectattr('admin', 'equalto', true) - | map(attribute='keys') | list | flatten | default([])) | join('\r\n') }}\r\n - force: true + ansible.windows.win_shell: | + $path = 'C:\ProgramData\ssh\administrators_authorized_keys' + $json = @' + {{ (__ssh_users__ | default([]) | selectattr('admin', 'equalto', true) | map(attribute='keys') | list | flatten | default([])) | to_json }} + '@ + $keys = $json | ConvertFrom-Json + if ($null -eq $keys) { + $keys = @() + } elseif ($keys -isnot [System.Array]) { + $keys = @($keys) + } + + $newContent = [string]::Join([Environment]::NewLine, $keys) + if ($keys.Count -gt 0) { + $newContent += [Environment]::NewLine + } + + $oldContent = if (Test-Path $path) { [System.IO.File]::ReadAllText($path) } else { '' } + if ($oldContent -ne $newContent) { + [System.IO.File]::WriteAllText($path, $newContent, [System.Text.Encoding]::UTF8) + Write-Output 'admin-authorized-keys-updated' + } else { + Write-Output 'admin-authorized-keys-unchanged' + } + args: + executable: powershell.exe + register: admin_authorized_keys_result + changed_when: "'admin-authorized-keys-updated' in admin_authorized_keys_result.stdout" when: (__ssh_users__ | default([]) | selectattr('admin','equalto',true) | list) | length > 0 become: true become_method: ansible.builtin.runas @@ -350,7 +402,9 @@ if (Test-Path $path) { $before = (Get-Acl -Path $path).Sddl icacls.exe $path /inheritance:r | Out-Null - icacls.exe $path /grant:r "S-1-5-18:(F)" "S-1-5-32-544:(F)" | Out-Null + if ($LASTEXITCODE -ne 0) { throw "icacls failed to disable inheritance on $path" } + icacls.exe $path /grant:r "*S-1-5-18:(F)" "*S-1-5-32-544:(F)" | Out-Null + if ($LASTEXITCODE -ne 0) { throw "icacls failed to grant SYSTEM/Administrators on $path" } $after = (Get-Acl -Path $path).Sddl if ($before -ne $after) { Write-Output 'admin-auth-keys-acl-changed' diff --git a/ansible/sectool.json b/ansible/sectool.json new file mode 120000 index 0000000..7284862 --- /dev/null +++ b/ansible/sectool.json @@ -0,0 +1 @@ +../sectool.json \ No newline at end of file diff --git a/inventory/hosts b/inventory/hosts index 1388fc9..b349ecb 100644 --- a/inventory/hosts +++ b/inventory/hosts @@ -129,3 +129,6 @@ gpu-01.lab.alexpires.me [gpu_monitoring] gpu-01.lab.alexpires.me +[windows_containers] +ci-01.lab.alexpires.me + diff --git a/terraform/apps/dev-01/resources/grafana/datasource.yml b/terraform/apps/dev-01/resources/grafana/datasource.yml index 1033637..0b1cd51 100644 --- a/terraform/apps/dev-01/resources/grafana/datasource.yml +++ b/terraform/apps/dev-01/resources/grafana/datasource.yml @@ -11,7 +11,7 @@ datasources: timeInterval: 15s - name: prometheus-ci-01 type: prometheus - url: http://ci-01.lab.alexpires.me:9090 + url: http://10.19.4.207:9090 isDefault: false editable: false jsonData: @@ -19,7 +19,7 @@ datasources: timeInterval: 15s - name: prometheus-gpu-01 type: prometheus - url: http://gpu-01.lab.alexpires.me:9091 + url: http://10.19.4.106:9091 isDefault: false editable: false jsonData: