--- # Operator accounts and their SSH keys. # # `switch_operators` (host_vars) is the source of truth: one entry per person/seat, # each with the group it belongs to and the public keys that may log in as it. The # role only holds mechanism. # # Idempotency: the device is queried FIRST and only genuinely missing keys are # uploaded — an unconditional `net_put` would report changed on every run. RouterOS # reports a key's comment field as `key-owner`, which is what we match on. # # Passwords: a RouterOS user with no password can still be reached over WinBox/console # without one, so operators are created with a vaulted password even though SSH is # key-only (`/ip/ssh always-allow-password-login=no`). See CLAUDE.md. - name: Assert every operator has a vaulted password ansible.builtin.assert: that: switch_operators | map(attribute='name') | difference(vault_operator_passwords | default({}) | list) | length == 0 fail_msg: >- Every entry in switch_operators needs a vault_operator_passwords. entry — a passwordless RouterOS user is reachable over WinBox/console without one. Missing: {{ switch_operators | map(attribute='name') | difference(vault_operator_passwords | default({}) | list) | join(', ') }} quiet: true - name: Read the SSH keys currently installed on the device community.routeros.command: commands: - /user/ssh-keys/print detail register: switch_ssh_keys_raw changed_when: false - name: Build the set of user/key-owner pairs already present ansible.builtin.set_fact: switch_ssh_keys_present: >- {{ switch_ssh_keys_raw.stdout[0] | regex_findall('user=(\S+).*?key-owner="([^"]*)"') | map('join', '|') | list }} - name: Ensure each operator account exists in the right group community.routeros.command: commands: - >- :if ([:len [/user find name="{{ item.name }}"]] = 0) do={ /user add name="{{ item.name }}" group="{{ item.group }}" password="{{ vault_operator_passwords[item.name] }}" } else={ :if ([/user get [find name="{{ item.name }}"] group] != "{{ item.group }}") do={ /user set [find name="{{ item.name }}"] group="{{ item.group }}" } } loop: "{{ switch_operators }}" loop_control: label: "{{ item.name }}" no_log: true changed_when: false - name: Upload the operator public keys that are not on the device yet ansible.netcommon.net_put: src: "{{ item.1 }}" dest: "{{ item.1 | basename }}" loop: "{{ switch_operators | subelements('pubkey_files') }}" loop_control: label: "{{ item.0.name }} <- {{ item.1 | basename }}" when: item.0.name ~ '|' ~ (lookup('file', item.1).split() | last) not in switch_ssh_keys_present - name: Import the newly uploaded operator public keys community.routeros.command: commands: - >- /user/ssh-keys/import public-key-file="{{ item.1 | basename }}" user="{{ item.0.name }}" loop: "{{ switch_operators | subelements('pubkey_files') }}" loop_control: label: "{{ item.0.name }} <- {{ item.1 | basename }}" when: item.0.name ~ '|' ~ (lookup('file', item.1).split() | last) not in switch_ssh_keys_present changed_when: true - name: Remove the uploaded key files from the switch flash community.routeros.command: commands: - >- :if ([:len [/file find name="{{ item.1 | basename }}"]] > 0) do={ /file remove [find name="{{ item.1 | basename }}"] } loop: "{{ switch_operators | subelements('pubkey_files') }}" loop_control: label: "{{ item.1 | basename }}" changed_when: false - name: Disable the default admin user community.routeros.command: commands: - >- :if ([:len [/user find name="admin"]] > 0) do={ /user/set admin disabled=yes } when: switch_disable_default_admin | bool changed_when: false