summaryrefslogtreecommitdiff
path: root/plugins/modules/vyos_user.py
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/modules/vyos_user.py')
-rw-r--r--plugins/modules/vyos_user.py302
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()