From 91de26af574db9fdd36897fe0d2d040db489cf84 Mon Sep 17 00:00:00 2001 From: Timotej Lazar Date: Fri, 9 May 2025 17:03:53 +0200 Subject: [PATCH] Add windows role MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Set up network interfaces and SSH for Windows hosts. We can’t gather facts before we know which remote shell to use, so first run a win_ping to determine if a given host is running Windows. --- roles/facts/tasks/main.yml | 66 +++++++++++++++-------- roles/windows/README.md | 3 ++ roles/windows/handlers/main.yml | 5 ++ roles/windows/tasks/interface.yml | 12 +++++ roles/windows/tasks/interface_address.yml | 22 ++++++++ roles/windows/tasks/main.yml | 35 ++++++++++++ setup.yml | 1 + 7 files changed, 123 insertions(+), 21 deletions(-) create mode 100644 roles/windows/README.md create mode 100644 roles/windows/handlers/main.yml create mode 100644 roles/windows/tasks/interface.yml create mode 100644 roles/windows/tasks/interface_address.yml create mode 100644 roles/windows/tasks/main.yml diff --git a/roles/facts/tasks/main.yml b/roles/facts/tasks/main.yml index 60b5529..5dff60c 100644 --- a/roles/facts/tasks/main.yml +++ b/roles/facts/tasks/main.yml @@ -1,3 +1,30 @@ +# Read secrets and keys. +- name: Get SSH keys + delegate_to: localhost + check_mode: false + run_once: true + block: + - name: Get GPG key IDs + shell: cat ${PASSWORD_STORE_DIR:-~/.password-store}/.gpg-id + changed_when: false + register: gpg_ids + + - name: Export public SSH keys + shell: echo "$(gpg --export-ssh-key {{ item }} | cut -d ' ' -f 1,2) $(gpg --list-keys --with-colons {{ item }} | sed -n 's@uid:.*<\(.*\)>.*@\1@p')" + loop: '{{ gpg_ids.stdout_lines }}' + changed_when: false + register: ssh_export + + - name: Set SSH keys to deploy on servers + set_fact: + ssh_keys: '{{ ssh_export.results | map(attribute="stdout") }}' + failed_when: not ssh_keys # something must be terribly wrong so let’s not lock everyone out + +- name: Get passwords + delegate_to: localhost + set_fact: + password: '{{ lookup("passwordstore", ("vm/" if is_virtual else "host/")~inventory_hostname, returnall=true, missing="empty") | from_yaml }}' + # Make expensive lookups to NetBox once for later reference by any host. - when: lookup("env", "NETBOX_API") != "" delegate_to: localhost @@ -22,28 +49,25 @@ cluster_services: '{{ (cluster_services|default([])) + query("netbox.netbox.nb_lookup", "services", raw_data=true, api_filter="id="+item) }}' loop: '{{ cluster.custom_fields.services | map(attribute="id") | map("string") }}' -- name: Fetch passwords +# Set host-specific connection parameters. +- name: Set SSH connection username delegate_to: localhost set_fact: - password: '{{ lookup("passwordstore", ("vm/" if is_virtual else "host/")~inventory_hostname, returnall=true, missing="empty") | from_yaml }}' + ansible_ssh_user: "{{ password.user }}" + when: password.user is defined -- name: Get SSH keys - delegate_to: localhost - check_mode: false - run_once: true - block: - - name: Get GPG key IDs - shell: cat ${PASSWORD_STORE_DIR:-~/.password-store}/.gpg-id - changed_when: false - register: gpg_ids +- name: Check if the host is running Windows + win_ping: + vars: + ansible_shell_type: powershell + failed_when: false + ignore_errors: true + ignore_unreachable: true + register: result - - name: Export public SSH keys - shell: echo "$(gpg --export-ssh-key {{ item }} | cut -d ' ' -f 1,2) $(gpg --list-keys --with-colons {{ item }} | sed -n 's@uid:.*<\(.*\)>.*@\1@p')" - loop: '{{ gpg_ids.stdout_lines }}' - changed_when: false - register: ssh_export - - - name: Set SSH keys to deploy on servers - set_fact: - ssh_keys: '{{ ssh_export.results | map(attribute="stdout") }}' - failed_when: not ssh_keys # something must be terribly wrong so let’s not lock everyone out +- name: Set connection parameters for Windows + set_fact: + ansible_shell_type: powershell + ansible_become_method: runas + ansible_become_flags: "" + when: result.ping|default("") == "pong" diff --git a/roles/windows/README.md b/roles/windows/README.md new file mode 100644 index 0000000..84ebc28 --- /dev/null +++ b/roles/windows/README.md @@ -0,0 +1,3 @@ +Set up a generic Windows host. + +Rename and configure network interfaces. Configure the SSH server. diff --git a/roles/windows/handlers/main.yml b/roles/windows/handlers/main.yml new file mode 100644 index 0000000..3e97365 --- /dev/null +++ b/roles/windows/handlers/main.yml @@ -0,0 +1,5 @@ +- name: restart sshd + win_service: + name: sshd + state: restarted + when: "'handler' not in ansible_skip_tags" diff --git a/roles/windows/tasks/interface.yml b/roles/windows/tasks/interface.yml new file mode 100644 index 0000000..aa0540d --- /dev/null +++ b/roles/windows/tasks/interface.yml @@ -0,0 +1,12 @@ +- name: Rename interface + win_shell: > + Get-NetAdapter + | Where-Object -Property MacAddress -eq "{{ interface.mac_address | replace(':', '-') }}" + | Rename-NetAdapter -NewName "{{ interface.name }}" + changed_when: false # not really but we don’t care + +- include_tasks: interface_address.yml + loop: "{{ interface.ip_addresses }}" + loop_control: + label: "{{ interface.name }}" + loop_var: address diff --git a/roles/windows/tasks/interface_address.yml b/roles/windows/tasks/interface_address.yml new file mode 100644 index 0000000..1e509d8 --- /dev/null +++ b/roles/windows/tasks/interface_address.yml @@ -0,0 +1,22 @@ +- name: Add IP address + win_shell: > + New-NetIPAddress -InterfaceAlias {{ interface.name }} + -AddressFamily IPv{{ address.family.value }} + -IPAddress "{{ address.address | ipaddr("address") }}" -PrefixLength {{ address.address | ipaddr("prefix") }} + register: result + changed_when: "not result.stderr or 'Instance MSFT_NetIPAddress already exists' not in result.stderr" + failed_when: false + +- set_fact: + prefix: "{{ prefixes | selectattr('prefix', '==', address.address|ipaddr('subnet')) | first }}" + +- name: Set gateway + when: address.family.value == 4 and prefix.custom_fields.gateway + win_shell: > + New-NetRoute -InterfaceAlias {{ interface.name }} + -AddressFamily IPv{{ address.family.value }} + -DestinationPrefix {{ "0.0.0.0/0" if address.family.value == 4 else "::/0" }} + -NextHop {{ prefix.custom_fields.gateway.address | ipaddr("address") }} + register: result + changed_when: "not result.stderr or 'Instance MSFT_NetRoute already exists' not in result.stderr" + failed_when: false diff --git a/roles/windows/tasks/main.yml b/roles/windows/tasks/main.yml new file mode 100644 index 0000000..4c8e06a --- /dev/null +++ b/roles/windows/tasks/main.yml @@ -0,0 +1,35 @@ +- include_tasks: interface.yml + loop: "{{ interfaces }}" + loop_control: + label: "{{ interface.name }}" + loop_var: interface + +- name: Disable SSH password authentication + win_lineinfile: + path: c:\ProgramData\ssh\sshd_config + regexp: '^#?{{ item.key }}' + line: "{{ item.key }} {{ item.value }}" + loop: + - key: "PasswordAuthentication" + value: "no" + - key: "PermitRootLogin" + value: "prohibit-password" + notify: restart sshd + +- name: Set default shell to powershell + win_regedit: + path: HKLM:\SOFTWARE\OpenSSH + name: DefaultShell + data: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe + notify: restart sshd + +- name: Set authorized SSH keys + win_copy: + dest: C:\ProgramData\ssh\administrators_authorized_keys + content: "{{ ssh_keys | join('\n') }}" + +- name: Enable ssh + win_service: + name: sshd + start_mode: auto + state: started diff --git a/setup.yml b/setup.yml index 5f36aac..946cd8d 100644 --- a/setup.yml +++ b/setup.yml @@ -8,6 +8,7 @@ roles: - { role: alpine, when: ansible_distribution == 'Alpine' } - { role: debian, when: ansible_distribution == 'Debian' } + - { role: windows, when: ansible_os_family == 'Windows' } # hosts - hosts: mgmt-gw