diff options
Diffstat (limited to 'plugins/modules/vyos_user.py')
| -rw-r--r-- | plugins/modules/vyos_user.py | 302 |
1 files changed, 302 insertions, 0 deletions
diff --git a/plugins/modules/vyos_user.py b/plugins/modules/vyos_user.py new file mode 100644 index 0000000..8ccb148 --- /dev/null +++ b/plugins/modules/vyos_user.py @@ -0,0 +1,302 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# GNU General Public License v3.0+ +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = r""" +--- +module: vyos_user +short_description: Manage user accounts on VyOS devices using REST API +description: + - Manages local user accounts on VyOS devices via the REST API. + - Uses REST API (C(connection=httpapi)) instead of CLI. + - Passwords are write-only. Once set, they cannot be read back in plaintext. + - Use C(update_password=on_create) to avoid resetting passwords on every run. +version_added: "1.0.0" +author: + - VyOS Community (@vyos) +options: + users: + description: List of user definitions. + type: list + elements: dict + suboptions: + name: + description: Username. + type: str + required: true + full_name: + description: Full name of the user. + type: str + password: + description: Plaintext password. Write-only — hashed on device immediately. + type: str + update_password: + description: + - Control when password is updated. + - C(always) updates the password on every run (default). + - C(on_create) only sets the password when the user is first created. + type: str + choices: [always, on_create] + default: always + public_keys: + description: SSH public keys for the user. + type: list + elements: dict + suboptions: + name: + description: Key identifier/name. + type: str + required: true + key: + description: Base64-encoded public key. + type: str + required: true + type: + description: Key type. + type: str + choices: [ssh-dss, ssh-rsa, ecdsa-sha2-nistp256, ecdsa-sha2-nistp384, + ecdsa-sha2-nistp521, ssh-ed25519] + required: true + state: + description: + - C(present) ensures users exist with the specified configuration. + - C(absent) removes specified users. + - C(gathered) returns current user configuration as structured data. + type: str + choices: [present, absent, gathered] + default: present +notes: + - Requires C(ansible_connection=httpapi) with the VyOS httpapi plugin. + - C(ansible_network_os) must be set to C(vyos.rest.vyos). + - The C(vyos) user cannot be deleted as it is required for API access. + - Passwords are hashed immediately by VyOS and cannot be read back. +""" + +EXAMPLES = r""" +- name: Create user + vyos.rest.vyos_user: + users: + - name: alice + full_name: Alice Smith + password: securepassword + update_password: on_create + state: present + +- name: Add SSH public key + vyos.rest.vyos_user: + users: + - name: alice + public_keys: + - name: alice-laptop + type: ssh-rsa + key: AAAAB3NzaC1yc2EAAAADAQABAAAB... + state: present + +- name: Delete user + vyos.rest.vyos_user: + users: + - name: alice + state: absent + +- name: Gather all users + vyos.rest.vyos_user: + state: gathered +""" + +RETURN = r""" +before: + description: User configuration before this module ran. + returned: always + type: list +after: + description: User configuration after this module ran. + returned: when changed + type: list +commands: + description: List of API command tuples sent to the device. + returned: always + type: list +gathered: + description: Current user configuration as structured data. + returned: when state is gathered + type: list +saved: + description: Whether the config was saved after changes. + returned: when changes are applied + type: bool +response: + description: Raw API response. + returned: when changes are applied + type: dict +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vyos.rest.plugins.module_utils.vyos import VyOSModule + + +_BASE = ["system", "login", "user"] + + +def get_running_config(vyos): + raw = vyos.get_config(_BASE) + if not raw or not isinstance(raw, dict): + return [] + raw = raw.get("user", raw) + result = [] + for username, data in sorted(raw.items()): + user = {"name": username} + data = data or {} + if data.get("full-name"): + user["full_name"] = data["full-name"] + auth = data.get("authentication", {}) or {} + if auth.get("encrypted-password"): + user["encrypted_password"] = auth["encrypted-password"] + pub_keys = auth.get("public-keys", {}) or {} + if pub_keys and isinstance(pub_keys, dict): + keys = [] + for key_name, key_data in sorted(pub_keys.items()): + key_data = key_data or {} + k = {"name": key_name} + if key_data.get("key"): + k["key"] = key_data["key"] + if key_data.get("type"): + k["type"] = key_data["type"] + keys.append(k) + if keys: + user["public_keys"] = keys + result.append(user) + return result + + +def build_commands(users, have_list, state): + cmds = [] + have_map = {u["name"]: u for u in have_list} + + if state == "absent": + for user in users: + name = user["name"] + if name in have_map: + cmds.append(("delete", _BASE + [name])) + return cmds + + # state == "present" + for user in users: + name = user["name"] + have = have_map.get(name, {}) + ubase = _BASE + [name] + is_new = name not in have_map + + # full_name + if user.get("full_name") and user["full_name"] != have.get("full_name"): + cmds.append(("set", ubase + ["full-name", user["full_name"]])) + + # password + if user.get("password"): + update_pw = user.get("update_password", "always") + if update_pw == "always" or is_new: + cmds.append( + ( + "set", + ubase + + [ + "authentication", + "plaintext-password", + user["password"], + ], + ), + ) + + # public_keys + want_keys = {k["name"]: k for k in (user.get("public_keys") or [])} + have_keys = {k["name"]: k for k in (have.get("public_keys") or [])} + for key_name, key_data in want_keys.items(): + have_key = have_keys.get(key_name, {}) + kbase = ubase + ["authentication", "public-keys", key_name] + if key_data.get("key") and key_data["key"] != have_key.get("key"): + cmds.append(("set", kbase + ["key", key_data["key"]])) + if key_data.get("type") and key_data["type"] != have_key.get("type"): + cmds.append(("set", kbase + ["type", key_data["type"]])) + + return cmds + + +ARGUMENT_SPEC = dict( + users=dict( + type="list", + elements="dict", + options=dict( + name=dict(type="str", required=True), + full_name=dict(type="str"), + password=dict(type="str", no_log=True), + update_password=dict( + type="str", + choices=["always", "on_create"], + default="always", + ), + public_keys=dict( + type="list", + elements="dict", + options=dict( + name=dict(type="str", required=True), + key=dict(type="str", required=True, no_log=True), + type=dict( + type="str", + required=True, + choices=[ + "ssh-dss", + "ssh-rsa", + "ecdsa-sha2-nistp256", + "ecdsa-sha2-nistp384", + "ecdsa-sha2-nistp521", + "ssh-ed25519", + ], + ), + ), + ), + ), + ), + state=dict( + default="present", + choices=["present", "absent", "gathered"], + ), +) + + +def main(): + module = AnsibleModule(ARGUMENT_SPEC, supports_check_mode=True) + vyos = VyOSModule(module) + + state = module.params["state"] + users = module.params.get("users") or [] + + have = get_running_config(vyos) + + if state == "gathered": + module.exit_json(changed=False, gathered=have) + + commands = build_commands(users, have, state) + + if module.check_mode: + module.exit_json(changed=bool(commands), commands=commands, before=have) + + if commands: + response = vyos.apply_commands(commands) + saved = vyos.save_config() + module.exit_json( + changed=True, + before=have, + after=get_running_config(vyos), + commands=commands, + saved=saved, + response=response, + ) + + module.exit_json(changed=False, before=have, after=have, commands=[]) + + +if __name__ == "__main__": + main() |
