diff --git a/changelogs/fragments/T6988-fix-user.yml b/changelogs/fragments/T6988-fix-user.yml new file mode 100644 index 00000000..26ea007f --- /dev/null +++ b/changelogs/fragments/T6988-fix-user.yml @@ -0,0 +1,15 @@ +--- +breaking_changes: + - removal of role/level as it was removed in 1.3 + +major_changes: + - add support for public-key authentication + - add support for encrypted password specification + +minor_changes: + - fix sending of `full-name` to use quotes + - fix parsing of `full-name` to ignore quotes + - fix integration tests for smoke + +known_issues: + - ssh login tests are brittle diff --git a/docs/vyos.vyos.vyos_user_module.rst b/docs/vyos.vyos.vyos_user_module.rst index 5f0ad831..f95200b6 100644 --- a/docs/vyos.vyos.vyos_user_module.rst +++ b/docs/vyos.vyos.vyos_user_module.rst @@ -1,361 +1,514 @@ .. _vyos.vyos.vyos_user_module: ******************* vyos.vyos.vyos_user ******************* **Manage the collection of local users on VyOS device** Version added: 1.0.0 .. contents:: :local: :depth: 1 Synopsis -------- - This module provides declarative management of the local usernames configured on network devices. It allows playbooks to manage either individual usernames or the collection of usernames in the current running config. It also supports purging usernames from the configuration that are not explicitly defined. Parameters ---------- .. raw:: html - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + - -
ParameterParameter Choices/Defaults Comments
+
aggregate
list / elements=dictionary
The set of username objects to be configured on the remote VyOS device. The list entries can either be the username or a hash of username and properties. This argument is mutually exclusive with the name argument.

aliases: users, collection
+
configured_password
string
The password to be configured on the VyOS device. The password needs to be provided in clear and it will be encrypted on the device.
+ +
+ encrypted_password + +
+ string +
+
+ +
The encrypted password of the user account on the remote device. Note that unlike the configured_password argument, this argument ignores the update_password and updates if the value is different from the one in the device running config.
+
full_name
string
The full_name argument provides the full name of the user account to be created on the remote device. This argument accepts any text string value.
+
+ name + +
+ string + / required +
+
+ +
The username to be configured on the VyOS device. This argument accepts a string value and is mutually exclusive with the aggregate argument.
+
+
+ public_keys + +
+ list + / elements=dictionary +
+
+ +
Public keys for authentiction over SSH.
+
- level + key
string + / required
-
The level argument configures the level of the user when logged into the system. This argument accepts string values admin or operator.
-

aliases: role
+
Public key string (base64 encoded)
name
string / required
-
The username to be configured on the VyOS device. This argument accepts a string value and is mutually exclusive with the aggregate argument.
+
Name of the key (usually in the form of user@hostname)
+
+ type + +
+ string + / required +
+
+
    Choices: +
  • ssh-dss
  • +
  • ssh-rsa
  • +
  • ecdsa-sha2-nistp256
  • +
  • ecdsa-sha2-nistp384
  • +
  • ssh-ed25519
  • +
  • ecdsa-sha2-nistp521
  • +
+
+
Type of the key
+
state
string
    Choices:
  • present
  • absent
Configures the state of the username definition as it relates to the device operational configuration. When set to present, the username(s) should be configured in the device active configuration and when set to absent the username(s) should not be in the device active configuration
+
update_password
string
    Choices:
  • on_create
  • always
Since passwords are encrypted in the device running config, this argument will instruct the module when to change the password. When set to always, the password will always be updated in the device and when set to on_create the password will be updated only if the username is created.
+
configured_password
string
The password to be configured on the VyOS device. The password needs to be provided in clear and it will be encrypted on the device.
+ +
+ encrypted_password + +
+ string +
+
+ +
The encrypted password of the user account on the remote device. Note that unlike the configured_password argument, this argument ignores the update_password and updates if the value is different from the one in the device running config.
+
full_name
string
The full_name argument provides the full name of the user account to be created on the remote device. This argument accepts any text string value.
+
+ name + +
+ string +
+
+ +
The username to be configured on the VyOS device. This argument accepts a string value and is mutually exclusive with the aggregate argument.
+
+
+ public_keys + +
+ list + / elements=dictionary +
+
+ +
Public keys for authentiction over SSH.
+
- level + key
string + / required
-
The level argument configures the level of the user when logged into the system. This argument accepts string values admin or operator.
-

aliases: role
+
Public key string (base64 encoded)
name
string + / required
-
The username to be configured on the VyOS device. This argument accepts a string value and is mutually exclusive with the aggregate argument.
+
Name of the key (usually in the form of user@hostname)
+
+ type + +
+ string + / required +
+
+
    Choices: +
  • ssh-dss
  • +
  • ssh-rsa
  • +
  • ecdsa-sha2-nistp256
  • +
  • ecdsa-sha2-nistp384
  • +
  • ssh-ed25519
  • +
  • ecdsa-sha2-nistp521
  • +
+
+
Type of the key
+
purge
boolean
    Choices:
  • no ←
  • yes
Instructs the module to consider the resource definition absolute. It will remove any previously configured usernames on the device with the exception of the `admin` user (the current defined set of users).
+
state
string
    Choices:
  • present ←
  • absent
Configures the state of the username definition as it relates to the device operational configuration. When set to present, the username(s) should be configured in the device active configuration and when set to absent the username(s) should not be in the device active configuration
+
update_password
string
    Choices:
  • on_create
  • always ←
Since passwords are encrypted in the device running config, this argument will instruct the module when to change the password. When set to always, the password will always be updated in the device and when set to on_create the password will be updated only if the username is created.

Notes ----- .. note:: - Tested against VyOS 1.1.8 (helium). - This module works with connection ``ansible.netcommon.network_cli``. See `the VyOS OS Platform Options <../network/user_guide/platform_vyos.html>`_. - For more information on using Ansible to manage network devices see the :ref:`Ansible Network Guide ` Examples -------- .. code-block:: yaml - name: create a new user vyos.vyos.vyos_user: name: ansible configured_password: password state: present - name: remove all users except admin vyos.vyos.vyos_user: purge: true - name: set multiple users to level operator vyos.vyos.vyos_user: aggregate: - name: netop - name: netend - level: operator state: present - name: Change Password for User netop vyos.vyos.vyos_user: name: netop configured_password: '{{ new_password }}' update_password: always state: present Return Values ------------- Common return values are documented `here `_, the following are the fields unique to this module: .. raw:: html
Key Returned Description
commands
list
always
The list of configuration mode commands to send to the device

Sample:
-
['set system login user test level operator', 'set system login user authentication plaintext-password password']
+
['set system login user authentication plaintext-password password']


Status ------ Authors ~~~~~~~ - Trishna Guha (@trishnaguha) diff --git a/plugins/modules/vyos_user.py b/plugins/modules/vyos_user.py index 53c45c20..5aebf943 100644 --- a/plugins/modules/vyos_user.py +++ b/plugins/modules/vyos_user.py @@ -1,393 +1,514 @@ #!/usr/bin/python # -*- coding: utf-8 -*- from __future__ import absolute_import, division, print_function __metaclass__ = type # (c) 2017, Ansible by Red Hat, inc # # This file is part of Ansible by Red Hat # # Ansible is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Ansible is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # DOCUMENTATION = """ module: vyos_user author: Trishna Guha (@trishnaguha) short_description: Manage the collection of local users on VyOS device description: - This module provides declarative management of the local usernames configured on network devices. It allows playbooks to manage either individual usernames or the collection of usernames in the current running config. It also supports purging usernames from the configuration that are not explicitly defined. version_added: 1.0.0 extends_documentation_fragment: - vyos.vyos.vyos notes: - Tested against VyOS 1.1.8 (helium). - This module works with connection C(ansible.netcommon.network_cli). See L(the VyOS OS Platform Options,../network/user_guide/platform_vyos.html). options: aggregate: description: - The set of username objects to be configured on the remote VyOS device. The list entries can either be the username or a hash of username and properties. This argument is mutually exclusive with the C(name) argument. aliases: - users - collection type: list elements: dict suboptions: name: description: - The username to be configured on the VyOS device. This argument accepts a string value and is mutually exclusive with the C(aggregate) argument. required: True type: str full_name: description: - The C(full_name) argument provides the full name of the user account to be created on the remote device. This argument accepts any text string value. type: str + encrypted_password: + description: + - The encrypted password of the user account on the remote device. Note that unlike + the C(configured_password) argument, this argument ignores the C(update_password) + and updates if the value is different from the one in the device running config. + type: str configured_password: description: - The password to be configured on the VyOS device. The password needs to be provided in clear and it will be encrypted on the device. type: str update_password: description: - Since passwords are encrypted in the device running config, this argument will instruct the module when to change the password. When set to C(always), the password will always be updated in the device and when set to C(on_create) the password will be updated only if the username is created. type: str choices: - on_create - always - level: - description: - - The C(level) argument configures the level of the user when logged into the - system. This argument accepts string values admin or operator. - type: str - aliases: - - role state: description: - Configures the state of the username definition as it relates to the device operational configuration. When set to I(present), the username(s) should be configured in the device active configuration and when set to I(absent) the username(s) should not be in the device active configuration type: str choices: - present - absent + public_keys: &public_keys + description: + - Public keys for authentiction over SSH. + type: list + elements: dict + suboptions: + name: + description: Name of the key (usually in the form of user@hostname) + required: true + type: str + key: + description: Public key string (base64 encoded) + required: true + type: str + type: + description: Type of the key + required: true + type: str + choices: + - ssh-dss + - ssh-rsa + - ecdsa-sha2-nistp256 + - ecdsa-sha2-nistp384 + - ssh-ed25519 + - ecdsa-sha2-nistp521 + name: description: - The username to be configured on the VyOS device. This argument accepts a string value and is mutually exclusive with the C(aggregate) argument. type: str full_name: description: - The C(full_name) argument provides the full name of the user account to be created on the remote device. This argument accepts any text string value. type: str + encrypted_password: + description: + - The encrypted password of the user account on the remote device. Note that unlike + the C(configured_password) argument, this argument ignores the C(update_password) + and updates if the value is different from the one in the device running config. + type: str configured_password: description: - The password to be configured on the VyOS device. The password needs to be provided in clear and it will be encrypted on the device. type: str update_password: description: - Since passwords are encrypted in the device running config, this argument will instruct the module when to change the password. When set to C(always), the password will always be updated in the device and when set to C(on_create) the password will be updated only if the username is created. default: always type: str choices: - on_create - always - level: - description: - - The C(level) argument configures the level of the user when logged into the - system. This argument accepts string values admin or operator. - type: str - aliases: - - role + public_keys: *public_keys purge: description: - Instructs the module to consider the resource definition absolute. It will remove any previously configured usernames on the device with the exception of the `admin` user (the current defined set of users). type: bool default: false state: description: - Configures the state of the username definition as it relates to the device operational configuration. When set to I(present), the username(s) should be configured in the device active configuration and when set to I(absent) the username(s) should not be in the device active configuration type: str default: present choices: - present - absent """ EXAMPLES = """ - name: create a new user vyos.vyos.vyos_user: name: ansible configured_password: password state: present - name: remove all users except admin vyos.vyos.vyos_user: purge: true - name: set multiple users to level operator vyos.vyos.vyos_user: aggregate: - name: netop - name: netend - level: operator state: present - name: Change Password for User netop vyos.vyos.vyos_user: name: netop configured_password: '{{ new_password }}' update_password: always state: present """ RETURN = """ commands: description: The list of configuration mode commands to send to the device returned: always type: list sample: - - set system login user test level operator - set system login user authentication plaintext-password password """ import re from copy import deepcopy from functools import partial from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( remove_default_spec, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import ( get_config, load_config, ) -def validate_level(value, module): - if value not in ("admin", "operator"): - module.fail_json(msg="level must be either admin or operator, got %s" % value) - - def spec_to_commands(updates, module): commands = list() update_password = module.params["update_password"] def needs_update(want, have, x): return want.get(x) and (want.get(x) != have.get(x)) def add(command, want, x): command.append("set system login user %s %s" % (want["name"], x)) for update in updates: want, have = update if want["state"] == "absent": commands.append("delete system login user %s" % want["name"]) continue - if needs_update(want, have, "level"): - add(commands, want, "level %s" % want["level"]) - if needs_update(want, have, "full_name"): - add(commands, want, "full-name %s" % want["full_name"]) + add(commands, want, "full-name '%s'" % want["full_name"]) + + # look both ways for public_keys to handle replacement + want_keys = want.get("public_keys") or dict() + have_keys = have.get("public_keys") or dict() + for key_name in want_keys: + key = want_keys[key_name] + if key_name not in have_keys or key != have_keys[key_name]: + add( + commands, + want, + "authentication public-keys %s key '%s'" % (key["name"], key["key"]), + ) + add( + commands, + want, + "authentication public-keys %s type '%s'" % (key["name"], key["type"]), + ) + + for key_name in have_keys: + if key_name not in want_keys: + commands.append( + "delete system login user %s authentication public-keys %s" + % (want["name"], key_name), + ) + + if needs_update(want, have, "encrypted_password"): + add( + commands, + want, + "authentication encrypted-password '%s'" % want["encrypted_password"], + ) if needs_update(want, have, "configured_password"): if update_password == "always" or not have: add( commands, want, "authentication plaintext-password %s" % want["configured_password"], ) return commands -def parse_level(data): - match = re.search(r"level (\S+)", data, re.M) - if match: - level = match.group(1)[1:-1] - return level - - def parse_full_name(data): - match = re.search(r"full-name (\S+)", data, re.M) + match = re.search(r"full-name '(\S+)'", data, re.M) if match: full_name = match.group(1)[1:-1] return full_name +def parse_key(data): + match = re.search(r"key '(\S+)'", data, re.M) + if match: + key = match.group(1) + return key + + +def parse_key_type(data): + match = re.search(r"type '(\S+)'", data, re.M) + if match: + key_type = match.group(1) + return key_type + + +def parse_public_keys(data): + """ + Parse public keys from the configuration + returning dictionary of dictionaries indexed by key name + """ + match = re.findall(r"public-keys (\S+)", data, re.M) + if not match: + return dict() + + keys = dict() + for key in set(match): + regex = r" %s .+$" % key + cfg = re.findall(regex, data, re.M) + cfg = "\n".join(cfg) + obj = { + "name": key, + "key": parse_key(cfg), + "type": parse_key_type(cfg), + } + keys[key] = obj + return keys + + +def parse_encrypted_password(data): + match = re.search(r"authentication encrypted-password '(\S+)'", data, re.M) + if match: + encrypted_password = match.group(1) + return encrypted_password + + def config_to_dict(module): data = get_config(module) match = re.findall(r"^set system login user (\S+)", data, re.M) if not match: return list() instances = list() for user in set(match): regex = r" %s .+$" % user cfg = re.findall(regex, data, re.M) cfg = "\n".join(cfg) obj = { "name": user, "state": "present", "configured_password": None, - "level": parse_level(cfg), "full_name": parse_full_name(cfg), + "encrypted_password": parse_encrypted_password(cfg), + "public_keys": parse_public_keys(cfg), } instances.append(obj) return instances def get_param_value(key, item, module): # if key doesn't exist in the item, get it from module.params if not item.get(key): value = module.params[key] # validate the param value (if validator func exists) validator = globals().get("validate_%s" % key) if all((value, validator)): validator(value, module) return value +def map_key_params_to_dict(keys): + """ + Map the list of keys to a dictionary of dictionaries + indexed by key name + """ + all_keys = dict() + if keys is None: + return all_keys + + for key in keys: + key_name = key["name"] + all_keys[key_name] = key + return all_keys + + def map_params_to_obj(module): aggregate = module.params["aggregate"] if not aggregate: if not module.params["name"] and module.params["purge"]: return list() else: users = [{"name": module.params["name"]}] else: users = list() for item in aggregate: if not isinstance(item, dict): users.append({"name": item}) else: users.append(item) objects = list() for item in users: get_value = partial(get_param_value, item=item, module=module) item["configured_password"] = get_value("configured_password") + item["encrypted_password"] = get_value("encrypted_password") item["full_name"] = get_value("full_name") - item["level"] = get_value("level") item["state"] = get_value("state") + item["public_keys"] = map_key_params_to_dict(get_value("public_keys")) objects.append(item) return objects def update_objects(want, have): updates = list() for entry in want: item = next((i for i in have if i["name"] == entry["name"]), None) if item is None: updates.append((entry, {})) elif item: for key, value in iteritems(entry): if value and value != item[key]: updates.append((entry, item)) return updates def main(): """main entry point for module execution""" + public_key_spec = dict( + name=dict(required=True, type="str"), + key=dict(required=True, type="str", no_log=False), + type=dict( + required=True, + type="str", + choices=[ + "ssh-dss", + "ssh-rsa", + "ecdsa-sha2-nistp256", + "ecdsa-sha2-nistp384", + "ssh-ed25519", + "ecdsa-sha2-nistp521", + ], + ), + ) element_spec = dict( name=dict(), full_name=dict(), - level=dict(aliases=["role"]), configured_password=dict(no_log=True), + encrypted_password=dict(no_log=False), update_password=dict(default="always", choices=["on_create", "always"]), state=dict(default="present", choices=["present", "absent"]), + public_keys=dict(type="list", elements="dict", options=public_key_spec), ) aggregate_spec = deepcopy(element_spec) aggregate_spec["name"] = dict(required=True) # remove default in aggregate spec, to handle common arguments remove_default_spec(aggregate_spec) argument_spec = dict( aggregate=dict( type="list", elements="dict", options=aggregate_spec, aliases=["users", "collection"], ), purge=dict(type="bool", default=False), ) argument_spec.update(element_spec) - mutually_exclusive = [("name", "aggregate")] + mutually_exclusive = [ + ("name", "aggregate"), + ("encrypted_password", "configured_password"), + ] + module = AnsibleModule( argument_spec=argument_spec, mutually_exclusive=mutually_exclusive, supports_check_mode=True, ) warnings = list() result = {"changed": False, "warnings": warnings} want = map_params_to_obj(module) have = config_to_dict(module) commands = spec_to_commands(update_objects(want, have), module) if module.params["purge"]: want_users = [x["name"] for x in want] have_users = [x["name"] for x in have] for item in set(have_users).difference(want_users): commands.append("delete system login user %s" % item) result["commands"] = commands if commands: commit = not module.check_mode load_config(module, commands, commit=commit) result["changed"] = True module.exit_json(**result) if __name__ == "__main__": main() diff --git a/tests/integration/targets/vyos_smoke/tests/cli/caching.yaml b/tests/integration/targets/vyos_smoke/tests/cli/caching.yaml index 9afea2e0..b7e7e1fd 100644 --- a/tests/integration/targets/vyos_smoke/tests/cli/caching.yaml +++ b/tests/integration/targets/vyos_smoke/tests/cli/caching.yaml @@ -1,85 +1,81 @@ --- - block: - debug: msg="START connection={{ ansible_connection }} cli/caching.yaml" - set_fact: interface_cmds: - set interfaces ethernet eth1 description 'Configured by Ansible - Interface 1' - set interfaces ethernet eth1 mtu '1500' - - set interfaces ethernet eth1 duplex 'auto' - - set interfaces ethernet eth1 speed 'auto' - set interfaces ethernet eth1 vif 101 description 'Eth1 - VIF 101' - set interfaces ethernet eth2 description 'Configured by Ansible - Interface 2 (ADMIN DOWN)' - - set interfaces ethernet eth2 mtu '600' + - set interfaces ethernet eth2 mtu '1280' l3_interface_cmds: - set interfaces ethernet eth1 address '192.0.2.10/24' - set interfaces ethernet eth1 address '2001:db8::10/32' - set interfaces ethernet eth2 address '198.51.100.10/24' - name: Remove interfaces from config before actual testing ignore_errors: true vyos.vyos.vyos_config: &rem lines: - delete interfaces ethernet eth1 - delete interfaces ethernet eth2 match: none - name: Merge base interfaces configuration register: result vyos.vyos.vyos_interfaces: &merged config: - name: eth1 description: Configured by Ansible - Interface 1 mtu: 1500 - speed: auto - duplex: auto vifs: - vlan_id: 101 description: Eth1 - VIF 101 - name: eth2 description: Configured by Ansible - Interface 2 (ADMIN DOWN) - mtu: 600 + mtu: 1280 state: merged - assert: that: - "{{ interface_cmds | symmetric_difference(result['commands']) |length == 0 }}" - name: Merge base interfaces configuration (IDEMPOTENT) register: result vyos.vyos.vyos_interfaces: *merged - assert: that: - result.changed == False - name: Merge L3 interfaces configuration register: result vyos.vyos.vyos_l3_interfaces: &mergedl3 config: - name: eth1 ipv4: - address: 192.0.2.10/24 ipv6: - address: 2001:db8::10/32 - name: eth2 ipv4: - address: 198.51.100.10/24 state: merged - assert: that: - "{{ l3_interface_cmds | symmetric_difference(result['commands']) |length == 0 }}" - name: Merge L3 interfaces configuration (IDEMPOTENT) register: result vyos.vyos.vyos_l3_interfaces: *mergedl3 - assert: that: - result.changed == False always: - name: cleanup vyos.vyos.vyos_config: *rem when: ansible_connection == "ansible.netcommon.network_cli" and ansible_network_single_user_mode|d(False) diff --git a/tests/integration/targets/vyos_user/tests/cli/auth.yaml b/tests/integration/targets/vyos_user/tests/cli/auth.yaml index a3178bf0..98a3d170 100644 --- a/tests/integration/targets/vyos_user/tests/cli/auth.yaml +++ b/tests/integration/targets/vyos_user/tests/cli/auth.yaml @@ -1,39 +1,38 @@ --- - block: - name: Create user with password vyos.vyos.vyos_user: name: auth_user - role: admin state: present configured_password: pass123 - name: test login via ssh with new user expect: command: >- - ssh auth_user@{{ ansible_ssh_host }} -p {{ ansible_port | default(22) }} \ - -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no '/opt/vyatta/sbin/vyatta-cfg-cmd-wrapper - show version' + ssh auth_user@{{ ansible_ssh_host }} -p {{ ansible_port | default(22) }} + -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no + '/opt/vyatta/sbin/vyatta-cfg-cmd-wrapper show version' responses: (?i)password: pass123 - name: test login via ssh with invalid password (should fail) expect: command: >- - ssh auth_user@{{ ansible_ssh_host }} -p {{ ansible_port | default(22) }} \ - -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no '/opt/vyatta/sbin/vyatta-cfg-cmd-wrapper - show version' + ssh auth_user@{{ ansible_ssh_host }} -p {{ ansible_port | default(22) }} + -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no + '/opt/vyatta/sbin/vyatta-cfg-cmd-wrapper show version' responses: (?i)password: badpass ignore_errors: true register: results - name: check that attempt failed assert: that: - results.failed always: - name: delete user register: result vyos.vyos.vyos_user: name: auth_user state: absent diff --git a/tests/integration/targets/vyos_user/tests/cli/basic.yaml b/tests/integration/targets/vyos_user/tests/cli/basic.yaml index 096edc0b..381f0db1 100644 --- a/tests/integration/targets/vyos_user/tests/cli/basic.yaml +++ b/tests/integration/targets/vyos_user/tests/cli/basic.yaml @@ -1,81 +1,79 @@ --- - debug: msg="START cli/basic.yaml on connection={{ ansible_connection }}" - name: Setup vyos.vyos.vyos_config: lines: - delete system login user ansibletest1 - delete system login user ansibletest2 - delete system login user ansibletest3 - name: Create user register: result vyos.vyos.vyos_user: name: ansibletest1 configured_password: test state: present - assert: that: - result.changed == true - '"set system login user" in result.commands[0]' - '"authentication plaintext-password" in result.commands[0]' - name: Collection of users (SetUp) register: result vyos.vyos.vyos_user: aggregate: - name: ansibletest2 - - name: ansibletest3 - level: operator + full_name: "test user" state: present - assert: that: - result.changed == true - - result.commands == ["set system login user ansibletest2 level operator", "set system login user ansibletest3 level operator"] + - result.commands == ["set system login user ansibletest2 full-name 'test user'", "set system login user ansibletest3 full-name 'test user'"] - name: Add user again (Idempotent) register: result vyos.vyos.vyos_user: name: ansibletest1 configured_password: test state: present update_password: on_create - assert: that: - result.changed == false - result.commands | length == 0 - name: Add collection of users (Idempotent) register: result vyos.vyos.vyos_user: aggregate: - name: ansibletest2 - name: ansibletest3 - level: operator state: present - assert: that: - result.changed == false - result.commands | length == 0 - name: tearDown register: result vyos.vyos.vyos_user: users: - name: ansibletest1 - name: ansibletest2 - name: ansibletest3 state: absent - assert: that: - result.changed == true - result.commands == ["delete system login user ansibletest1", "delete system login user ansibletest2", "delete system login user ansibletest3"] diff --git a/tests/integration/targets/vyos_user/tests/cli/basic.yaml b/tests/integration/targets/vyos_user/tests/cli/encrypted.yaml similarity index 56% copy from tests/integration/targets/vyos_user/tests/cli/basic.yaml copy to tests/integration/targets/vyos_user/tests/cli/encrypted.yaml index 096edc0b..39fbf61f 100644 --- a/tests/integration/targets/vyos_user/tests/cli/basic.yaml +++ b/tests/integration/targets/vyos_user/tests/cli/encrypted.yaml @@ -1,81 +1,97 @@ --- - debug: msg="START cli/basic.yaml on connection={{ ansible_connection }}" - name: Setup vyos.vyos.vyos_config: lines: - delete system login user ansibletest1 - delete system login user ansibletest2 - delete system login user ansibletest3 - name: Create user register: result vyos.vyos.vyos_user: name: ansibletest1 - configured_password: test + encrypted_password: "{{ encrypted_password }}" state: present - assert: that: - result.changed == true - - '"set system login user" in result.commands[0]' - - '"authentication plaintext-password" in result.commands[0]' + - "{{ encrypted_add['commands'] | symmetric_difference(result['commands']) |length == 0 }}" - name: Collection of users (SetUp) register: result vyos.vyos.vyos_user: aggregate: - name: ansibletest2 - - name: ansibletest3 - level: operator + full_name: "test user" + encrypted_password: "{{ encrypted_password }}" state: present - assert: that: - result.changed == true - - result.commands == ["set system login user ansibletest2 level operator", "set system login user ansibletest3 level operator"] + - "{{ encrypted_aggregate_add['commands'] | symmetric_difference(result['commands']) |length == 0 }}" - name: Add user again (Idempotent) register: result vyos.vyos.vyos_user: name: ansibletest1 - configured_password: test + encrypted_password: "{{ encrypted_password }}" state: present - update_password: on_create - assert: that: - result.changed == false - result.commands | length == 0 - name: Add collection of users (Idempotent) register: result vyos.vyos.vyos_user: aggregate: - name: ansibletest2 + - name: ansibletest3 + encrypted_password: "{{ encrypted_password }}" + state: present +- name: Change user password + register: result + vyos.vyos.vyos_user: + name: ansibletest1 + encrypted_password: "{{ encrypted_password_2 }}" + state: present + +- assert: + that: + - result.changed == true + - "{{ encrypted_change['commands'] | symmetric_difference(result['commands']) |length == 0 }}" + +- name: Change collection of users + register: result + vyos.vyos.vyos_user: + aggregate: + - name: ansibletest2 - name: ansibletest3 - level: operator + encrypted_password: "{{ encrypted_password_2 }}" state: present - assert: that: - - result.changed == false - - result.commands | length == 0 + - result.changed == true + - "{{ encrypted_aggregate_change['commands'] | symmetric_difference(result['commands']) |length == 0 }}" - name: tearDown register: result vyos.vyos.vyos_user: users: - name: ansibletest1 - - name: ansibletest2 - - name: ansibletest3 state: absent - assert: that: - result.changed == true - result.commands == ["delete system login user ansibletest1", "delete system login user ansibletest2", "delete system login user ansibletest3"] diff --git a/tests/integration/targets/vyos_user/tests/cli/public_keys.yaml b/tests/integration/targets/vyos_user/tests/cli/public_keys.yaml new file mode 100644 index 00000000..9ffa41eb --- /dev/null +++ b/tests/integration/targets/vyos_user/tests/cli/public_keys.yaml @@ -0,0 +1,129 @@ +--- +- debug: msg="START cli/basic.yaml on connection={{ ansible_connection }}" + +- name: Setup + vyos.vyos.vyos_config: + lines: + - delete system login user ssh_test_1 + - delete system login user ssh_test_2 + - delete system login user ssh_test_3 + +- name: Create first user + register: result + vyos.vyos.vyos_user: + name: ssh_test_1 + public_keys: + - name: test_key + key: "AAAAC3NzaC1lZDI1NTE5AAAAIFIR0jrMvBdmvTJNY5EDhOD+eixvbOinhY1eBU2uyuhu" + type: ssh-ed25519 + state: present + +- debug: + var: result +- debug: + var: ssh_add['commands'] + +- assert: + that: + - result.changed == true + - "{{ ssh_add['commands'] | symmetric_difference(result['commands']) |length == 0 }}" + +- name: Collection of users (SetUp) + register: result + vyos.vyos.vyos_user: + aggregate: + - name: ssh_test_2 + - name: ssh_test_3 + full_name: "test user" + public_keys: + - name: test_key_2 + key: "AAAAC3NzaC1lZDI1NTE5AAAAIFIR0jrMvBdmvTJNY5EDhOD+eixvbOinhY1eBU2uyuhu" + type: ssh-ed25519 + state: present + +- debug: + var: result +- debug: + var: ssh_aggregate_add['commands'] + +- assert: + that: + - result.changed == true + - "{{ ssh_aggregate_add['commands'] | symmetric_difference(result['commands']) |length == 0 }}" + +- name: Add user again (Idempotent) + register: result + vyos.vyos.vyos_user: + name: ssh_test_1 + public_keys: + - name: test_key + key: "AAAAC3NzaC1lZDI1NTE5AAAAIFIR0jrMvBdmvTJNY5EDhOD+eixvbOinhY1eBU2uyuhu" + type: ssh-ed25519 + state: present + +- assert: + that: + - result.changed == false + - result.commands | length == 0 + +- name: Add collection of users (Idempotent) + register: result + vyos.vyos.vyos_user: + aggregate: + - name: ssh_test_2 + - name: ssh_test_3 + public_keys: + - name: test_key_2 + key: "AAAAC3NzaC1lZDI1NTE5AAAAIFIR0jrMvBdmvTJNY5EDhOD+eixvbOinhY1eBU2uyuhu" + type: ssh-ed25519 + state: present + +- assert: + that: + - result.changed == false + - result.commands | length == 0 + +- name: Change user key + register: result + vyos.vyos.vyos_user: + name: ssh_test_1 + public_keys: + - name: test_key_3 + key: "AAAAC3NzaC1lZDI1NTE5AAAAIFIR0jrMvBdmvTJNY5EDhOD+eixvbOinhY1eBU2uyuhu" + type: ssh-ed25519 + state: present + +- assert: + that: + - result.changed == True + - "{{ ssh_change_key['commands'] | symmetric_difference(result['commands']) |length == 0 }}" + +- name: change collection of users keys + register: result + vyos.vyos.vyos_user: + aggregate: + - name: ssh_test_2 + - name: ssh_test_3 + public_keys: + - name: test_key_4 + key: "AAAAC3NzaC1lZDI1NTE5AAAAIFIR0jrMvBdmvTJNY5EDhOD+eixvbOinhY1eBU2uyuhu" + type: ssh-ed25519 + state: present +- assert: + that: + - result.changed == True + - "{{ ssh_aggregate_change['commands'] | symmetric_difference(result['commands']) |length == 0 }}" + +- name: tearDown + register: result + vyos.vyos.vyos_user: + users: + - name: ssh_test_1 + - name: ssh_test_2 + - name: ssh_test_3 + state: absent + +- assert: + that: + - result.changed == true + - result.commands == ["delete system login user ssh_test_1", "delete system login user ssh_test_2", "delete system login user ssh_test_3"] diff --git a/tests/integration/targets/vyos_user/vars/main.yaml b/tests/integration/targets/vyos_user/vars/main.yaml new file mode 100644 index 00000000..89163faf --- /dev/null +++ b/tests/integration/targets/vyos_user/vars/main.yaml @@ -0,0 +1,64 @@ +--- +ssh_add: + commands: + - set system login user ssh_test_1 authentication public-keys test_key key 'AAAAC3NzaC1lZDI1NTE5AAAAIFIR0jrMvBdmvTJNY5EDhOD+eixvbOinhY1eBU2uyuhu' + - set system login user ssh_test_1 authentication public-keys test_key type 'ssh-ed25519' + +ssh_aggregate_add: + commands: + - set system login user ssh_test_2 full-name 'test user' + - set system login user ssh_test_2 authentication public-keys test_key_2 key 'AAAAC3NzaC1lZDI1NTE5AAAAIFIR0jrMvBdmvTJNY5EDhOD+eixvbOinhY1eBU2uyuhu' + - set system login user ssh_test_2 authentication public-keys test_key_2 type 'ssh-ed25519' + - set system login user ssh_test_3 full-name 'test user' + - set system login user ssh_test_3 authentication public-keys test_key_2 key 'AAAAC3NzaC1lZDI1NTE5AAAAIFIR0jrMvBdmvTJNY5EDhOD+eixvbOinhY1eBU2uyuhu' + - set system login user ssh_test_3 authentication public-keys test_key_2 type 'ssh-ed25519' + +ssh_change_key: + commands: + - delete system login user ssh_test_1 authentication public-keys test_key + - set system login user ssh_test_1 authentication public-keys test_key_3 key 'AAAAC3NzaC1lZDI1NTE5AAAAIFIR0jrMvBdmvTJNY5EDhOD+eixvbOinhY1eBU2uyuhu' + - set system login user ssh_test_1 authentication public-keys test_key_3 type 'ssh-ed25519' + +ssh_aggregate_change: + commands: + - delete system login user ssh_test_2 authentication public-keys test_key_2 + - set system login user ssh_test_2 authentication public-keys test_key_4 key 'AAAAC3NzaC1lZDI1NTE5AAAAIFIR0jrMvBdmvTJNY5EDhOD+eixvbOinhY1eBU2uyuhu' + - set system login user ssh_test_2 authentication public-keys test_key_4 type 'ssh-ed25519' + - delete system login user ssh_test_3 authentication public-keys test_key_2 + - set system login user ssh_test_3 authentication public-keys test_key_4 key 'AAAAC3NzaC1lZDI1NTE5AAAAIFIR0jrMvBdmvTJNY5EDhOD+eixvbOinhY1eBU2uyuhu' + - set system login user ssh_test_3 authentication public-keys test_key_4 type 'ssh-ed25519' + +encrypted_password: "$6$x6SQ/zSxNwIEuqnL$hHmU/NXfAK/pFWXoCi91kKPAiQtf/cyckOlBUDUIL44QOUZHnqipHtz2znwYHQVM0Lqm6aFnm7Qs9WFlRf4mW/" + +encrypted_add: + commands: + - >- + set system login user ansibletest1 authentication encrypted-password + '$6$x6SQ/zSxNwIEuqnL$hHmU/NXfAK/pFWXoCi91kKPAiQtf/cyckOlBUDUIL44QOUZHnqipHtz2znwYHQVM0Lqm6aFnm7Qs9WFlRf4mW/' +encrypted_aggregate_add: + commands: + - set system login user ansibletest2 full-name 'test user' + - >- + set system login user ansibletest2 authentication encrypted-password + '$6$x6SQ/zSxNwIEuqnL$hHmU/NXfAK/pFWXoCi91kKPAiQtf/cyckOlBUDUIL44QOUZHnqipHtz2znwYHQVM0Lqm6aFnm7Qs9WFlRf4mW/' + - set system login user ansibletest3 full-name 'test user' + - >- + set system login user ansibletest3 authentication encrypted-password + '$6$x6SQ/zSxNwIEuqnL$hHmU/NXfAK/pFWXoCi91kKPAiQtf/cyckOlBUDUIL44QOUZHnqipHtz2znwYHQVM0Lqm6aFnm7Qs9WFlRf4mW/' + +encrypted_password_2: "$6$drNuMGFEgJ6Vremv$ukdc1trPwatKTUFVA9J1rAJsoWU.9ssgyZBoM7/ReK/yVAcxGbwx7.7VrKwF.Bag.thXXoXSduLtTzTlcJnU6." + +encrypted_change: + commands: + - >- + set system login user ansibletest1 authentication encrypted-password + '$6$drNuMGFEgJ6Vremv$ukdc1trPwatKTUFVA9J1rAJsoWU.9ssgyZBoM7/ReK/yVAcxGbwx7.7VrKwF.Bag.thXXoXSduLtTzTlcJnU6.' + +encrypted_aggregate_change: + commands: + - >- + set system login user ansibletest2 authentication encrypted-password + '$6$drNuMGFEgJ6Vremv$ukdc1trPwatKTUFVA9J1rAJsoWU.9ssgyZBoM7/ReK/yVAcxGbwx7.7VrKwF.Bag.thXXoXSduLtTzTlcJnU6.' + - >- + set system login user ansibletest3 authentication encrypted-password + '$6$drNuMGFEgJ6Vremv$ukdc1trPwatKTUFVA9J1rAJsoWU.9ssgyZBoM7/ReK/yVAcxGbwx7.7VrKwF.Bag.thXXoXSduLtTzTlcJnU6.' diff --git a/tests/unit/modules/network/vyos/fixtures/vyos_user_config.cfg b/tests/unit/modules/network/vyos/fixtures/vyos_user_config.cfg index 81cd1a48..9b73106e 100644 --- a/tests/unit/modules/network/vyos/fixtures/vyos_user_config.cfg +++ b/tests/unit/modules/network/vyos/fixtures/vyos_user_config.cfg @@ -1,2 +1,4 @@ -set system login user admin level operator authentication encrypted-password '$6$V5oWW3JM9NFAwOG$P2L4raFvIrZjjs3g0qmH4Ns5ti7flRpSs6aEqy4TrGZYXGeBiYzwi2A6jy' -set system login user ansible level operator authentication encrypted-password '$6$ZfvSv6A50W6yNPYX$4HP5eg2sywcXYxTqhApQ7zvUvx0HsQHrI9xuJoFLy2gM/' +set system login user admin authentication encrypted-password '$6$V5oWW3JM9NFAwOG$P2L4raFvIrZjjs3g0qmH4Ns5ti7flRpSs6aEqy4TrGZYXGeBiYzwi2A6jy' +set system login user ansible authentication encrypted-password '$6$ZfvSv6A50W6yNPYX$4HP5eg2sywcXYxTqhApQ7zvUvx0HsQHrI9xuJoFLy2gM/' +set system login user ssh authentication public-keys user@host key 'AAAAB3NzaC1yc2EAAAADAQABAAABAQD' +set system login user ssh authentication public-keys user@host type 'ssh-rsa' diff --git a/tests/unit/modules/network/vyos/test_vyos_user.py b/tests/unit/modules/network/vyos/test_vyos_user.py index 70297207..e8c50783 100644 --- a/tests/unit/modules/network/vyos/test_vyos_user.py +++ b/tests/unit/modules/network/vyos/test_vyos_user.py @@ -1,131 +1,239 @@ # (c) 2016 Red Hat Inc. # # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Ansible is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # Make coding more python3-ish from __future__ import absolute_import, division, print_function __metaclass__ = type from unittest.mock import patch from ansible_collections.vyos.vyos.plugins.modules import vyos_user from ansible_collections.vyos.vyos.tests.unit.modules.utils import set_module_args from .vyos_module import TestVyosModule, load_fixture class TestVyosUserModule(TestVyosModule): module = vyos_user def setUp(self): super(TestVyosUserModule, self).setUp() self.mock_get_config = patch( "ansible_collections.vyos.vyos.plugins.modules.vyos_user.get_config", ) self.get_config = self.mock_get_config.start() self.mock_load_config = patch( "ansible_collections.vyos.vyos.plugins.modules.vyos_user.load_config", ) self.load_config = self.mock_load_config.start() def tearDown(self): super(TestVyosUserModule, self).tearDown() self.mock_get_config.stop() self.mock_load_config.stop() def load_fixtures(self, commands=None, filename=None): self.get_config.return_value = load_fixture("vyos_user_config.cfg") self.load_config.return_value = dict(diff=None, session="session") def test_vyos_user_password(self): set_module_args(dict(name="ansible", configured_password="test")) result = self.execute_module(changed=True) self.assertEqual( result["commands"], ["set system login user ansible authentication plaintext-password test"], ) def test_vyos_user_delete(self): set_module_args(dict(name="ansible", state="absent")) result = self.execute_module(changed=True) self.assertEqual(result["commands"], ["delete system login user ansible"]) - def test_vyos_user_level(self): - set_module_args(dict(name="ansible", level="operator")) - result = self.execute_module(changed=True) - self.assertEqual( - result["commands"], - ["set system login user ansible level operator"], - ) - - def test_vyos_user_level_invalid(self): - set_module_args(dict(name="ansible", level="sysadmin")) - self.execute_module(failed=True) - def test_vyos_user_purge(self): set_module_args(dict(purge=True)) result = self.execute_module(changed=True) self.assertEqual( sorted(result["commands"]), sorted( [ "delete system login user ansible", "delete system login user admin", + "delete system login user ssh", ], ), ) def test_vyos_user_update_password_changed(self): set_module_args( dict( name="test", configured_password="test", update_password="on_create", ), ) result = self.execute_module(changed=True) self.assertEqual( result["commands"], ["set system login user test authentication plaintext-password test"], ) def test_vyos_user_update_password_on_create_ok(self): set_module_args( dict( name="ansible", configured_password="test", update_password="on_create", ), ) self.execute_module() def test_vyos_user_update_password_always(self): set_module_args( dict( name="ansible", configured_password="test", update_password="always", ), ) result = self.execute_module(changed=True) self.assertEqual( result["commands"], ["set system login user ansible authentication plaintext-password test"], ) + + def test_vyos_user_set_ssh_key(self): + set_module_args( + dict( + name="ansible", + public_keys=[ + dict( + name="user@host", + key="AAAAC3NzaC1lZDI1NTE5AAAAIFIR0jrMvBdmvTJNY5EDhOD+eixvbOinhY1eBU2uyuhu", + type="ssh-ed25519", + ), + ], + ), + ) + result = self.execute_module(changed=True) + self.assertEqual( + result["commands"], + [ + "set system login user ansible authentication public-keys user@host key 'AAAAC3NzaC1lZDI1NTE5AAAAIFIR0jrMvBdmvTJNY5EDhOD+eixvbOinhY1eBU2uyuhu'", + "set system login user ansible authentication public-keys user@host type 'ssh-ed25519'", + ], + ) + + def test_vyos_user_set_ssh_key_idempotent(self): + set_module_args( + dict( + name="ssh", + public_keys=[ + dict( + name="user@host", + key="AAAAB3NzaC1yc2EAAAADAQABAAABAQD", + type="ssh-rsa", + ), + ], + ), + ) + self.load_fixtures() + result = self.execute_module(changed=False) + self.assertEqual(result["commands"], []) + + def test_vyos_user_set_ssh_key_change(self): + set_module_args( + dict( + name="ssh", + public_keys=[ + dict( + name="user@host", + key="AAAAC3NzaC1lZDI1NTE5AAAAIFIR0jrMvBdmvTJNY5EDhOD+eixvbOinhY1eBU2uyuhu", + type="ssh-ed25519", + ), + ], + ), + ) + self.load_fixtures() + result = self.execute_module( + changed=True, + commands=[ + "set system login user ssh authentication public-keys user@host key 'AAAAC3NzaC1lZDI1NTE5AAAAIFIR0jrMvBdmvTJNY5EDhOD+eixvbOinhY1eBU2uyuhu'", + "set system login user ssh authentication public-keys user@host type 'ssh-ed25519'", + ], + ) + + def test_vyos_user_set_ssh_key_add_and_remove(self): + set_module_args( + dict( + name="ssh", + public_keys=[ + dict( + name="noone@nowhere", + key="AAAAC3NzaC1lZDI1NTE5AAAAIFIR0jrMvBdmvTJNY5EDhOD+eixvbOinhY1eBU2uyuhu", + type="ssh-ed25519", + ), + ], + ), + ) + self.load_fixtures() + result = self.execute_module( + changed=True, + commands=[ + "delete system login user ssh authentication public-keys user@host", + "set system login user ssh authentication public-keys noone@nowhere key 'AAAAC3NzaC1lZDI1NTE5AAAAIFIR0jrMvBdmvTJNY5EDhOD+eixvbOinhY1eBU2uyuhu'", + "set system login user ssh authentication public-keys noone@nowhere type 'ssh-ed25519'", + ], + ) + + def test_vyos_user_set_ssh_key_empty(self): + # empty public_keys has no effect (for setting passwords, user names, etc.) + set_module_args( + dict( + name="ssh", + public_keys=[], + ), + ) + self.load_fixtures() + result = self.execute_module(changed=False) + + def test_vyos_user_set_encrypted_password(self): + set_module_args( + dict( + name="ansible", + encrypted_password="$6$rounds=656000$SALT$HASH", + ), + ) + result = self.execute_module(changed=True) + self.assertEqual( + result["commands"], + [ + "set system login user ansible authentication encrypted-password '$6$rounds=656000$SALT$HASH'", + ], + ) + + def test_vyos_user_set_encrypted_password_idem(self): + set_module_args( + dict( + name="ansible", + encrypted_password="$6$ZfvSv6A50W6yNPYX$4HP5eg2sywcXYxTqhApQ7zvUvx0HsQHrI9xuJoFLy2gM/", + ), + ) + result = self.execute_module(changed=False)