diff options
author | Gaige B Paulsen <gaige@cluetrust.com> | 2025-01-02 14:06:58 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-01-02 19:06:58 +0000 |
commit | 9e159990f949652ec1b22f9a9a6e72828bdd1e80 (patch) | |
tree | f3f580083415d4ea48cf81b86e02f08df8f9f26a | |
parent | dbd87e3ab89b7839e41df76c2fa7712855853fd3 (diff) | |
download | vyos.vyos-9e159990f949652ec1b22f9a9a6e72828bdd1e80.tar.gz vyos.vyos-9e159990f949652ec1b22f9a9a6e72828bdd1e80.zip |
T6988: fix: remove role/level, fix tests (#371)
* T6988: fix: remove role/level, fix tests
* feature: add support for SSH keys
* tests: add integration tests for public_keys
* feat: add encrypted password support
* tests: add unit for encrypted
* tests: fix wrapping in YAML
* tests: fix smoke tests
-rw-r--r-- | changelogs/fragments/T6988-fix-user.yml | 15 | ||||
-rw-r--r-- | docs/vyos.vyos.vyos_user_module.rst | 191 | ||||
-rw-r--r-- | plugins/modules/vyos_user.py | 195 | ||||
-rw-r--r-- | tests/integration/targets/vyos_smoke/tests/cli/caching.yaml | 8 | ||||
-rw-r--r-- | tests/integration/targets/vyos_user/tests/cli/auth.yaml | 13 | ||||
-rw-r--r-- | tests/integration/targets/vyos_user/tests/cli/basic.yaml | 6 | ||||
-rw-r--r-- | tests/integration/targets/vyos_user/tests/cli/encrypted.yaml | 97 | ||||
-rw-r--r-- | tests/integration/targets/vyos_user/tests/cli/public_keys.yaml | 129 | ||||
-rw-r--r-- | tests/integration/targets/vyos_user/vars/main.yaml | 64 | ||||
-rw-r--r-- | tests/unit/modules/network/vyos/fixtures/vyos_user_config.cfg | 6 | ||||
-rw-r--r-- | tests/unit/modules/network/vyos/test_vyos_user.py | 132 |
11 files changed, 769 insertions, 87 deletions
diff --git a/changelogs/fragments/T6988-fix-user.yml b/changelogs/fragments/T6988-fix-user.yml new file mode 100644 index 0000000..26ea007 --- /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 5f0ad83..f95200b 100644 --- a/docs/vyos.vyos.vyos_user_module.rst +++ b/docs/vyos.vyos.vyos_user_module.rst @@ -29,12 +29,12 @@ Parameters <table border=0 cellpadding=0 class="documentation-table"> <tr> - <th colspan="2">Parameter</th> + <th colspan="3">Parameter</th> <th>Choices/<font color="blue">Defaults</font></th> <th width="100%">Comments</th> </tr> <tr> - <td colspan="2"> + <td colspan="3"> <div class="ansibleOptionAnchor" id="parameter-"></div> <b>aggregate</b> <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> @@ -52,7 +52,7 @@ Parameters </tr> <tr> <td class="elbow-placeholder"></td> - <td colspan="1"> + <td colspan="2"> <div class="ansibleOptionAnchor" id="parameter-"></div> <b>configured_password</b> <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> @@ -68,7 +68,23 @@ Parameters </tr> <tr> <td class="elbow-placeholder"></td> - <td colspan="1"> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>encrypted_password</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>The encrypted password of the user account on the remote device. Note that unlike the <code>configured_password</code> argument, this argument ignores the <code>update_password</code> and updates if the value is different from the one in the device running config.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td colspan="2"> <div class="ansibleOptionAnchor" id="parameter-"></div> <b>full_name</b> <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> @@ -84,23 +100,59 @@ Parameters </tr> <tr> <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>name</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + / <span style="color: red">required</span> + </div> + </td> + <td> + </td> + <td> + <div>The username to be configured on the VyOS device. This argument accepts a string value and is mutually exclusive with the <code>aggregate</code> argument.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>public_keys</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">list</span> + / <span style="color: purple">elements=dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Public keys for authentiction over SSH.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> <td colspan="1"> <div class="ansibleOptionAnchor" id="parameter-"></div> - <b>level</b> + <b>key</b> <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> <div style="font-size: small"> <span style="color: purple">string</span> + / <span style="color: red">required</span> </div> </td> <td> </td> <td> - <div>The <code>level</code> argument configures the level of the user when logged into the system. This argument accepts string values admin or operator.</div> - <div style="font-size: small; color: darkgreen"><br/>aliases: role</div> + <div>Public key string (base64 encoded)</div> </td> </tr> <tr> <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> <td colspan="1"> <div class="ansibleOptionAnchor" id="parameter-"></div> <b>name</b> @@ -113,13 +165,40 @@ Parameters <td> </td> <td> - <div>The username to be configured on the VyOS device. This argument accepts a string value and is mutually exclusive with the <code>aggregate</code> argument.</div> + <div>Name of the key (usually in the form of user@hostname)</div> </td> </tr> <tr> <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> <td colspan="1"> <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>type</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + / <span style="color: red">required</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>ssh-dss</li> + <li>ssh-rsa</li> + <li>ecdsa-sha2-nistp256</li> + <li>ecdsa-sha2-nistp384</li> + <li>ssh-ed25519</li> + <li>ecdsa-sha2-nistp521</li> + </ul> + </td> + <td> + <div>Type of the key</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> <b>state</b> <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> <div style="font-size: small"> @@ -138,7 +217,7 @@ Parameters </tr> <tr> <td class="elbow-placeholder"></td> - <td colspan="1"> + <td colspan="2"> <div class="ansibleOptionAnchor" id="parameter-"></div> <b>update_password</b> <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> @@ -158,7 +237,7 @@ Parameters </tr> <tr> - <td colspan="2"> + <td colspan="3"> <div class="ansibleOptionAnchor" id="parameter-"></div> <b>configured_password</b> <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> @@ -173,7 +252,22 @@ Parameters </td> </tr> <tr> - <td colspan="2"> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>encrypted_password</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>The encrypted password of the user account on the remote device. Note that unlike the <code>configured_password</code> argument, this argument ignores the <code>update_password</code> and updates if the value is different from the one in the device running config.</div> + </td> + </tr> + <tr> + <td colspan="3"> <div class="ansibleOptionAnchor" id="parameter-"></div> <b>full_name</b> <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> @@ -188,39 +282,99 @@ Parameters </td> </tr> <tr> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>name</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>The username to be configured on the VyOS device. This argument accepts a string value and is mutually exclusive with the <code>aggregate</code> argument.</div> + </td> + </tr> + <tr> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>public_keys</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">list</span> + / <span style="color: purple">elements=dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Public keys for authentiction over SSH.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> <td colspan="2"> <div class="ansibleOptionAnchor" id="parameter-"></div> - <b>level</b> + <b>key</b> <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> <div style="font-size: small"> <span style="color: purple">string</span> + / <span style="color: red">required</span> </div> </td> <td> </td> <td> - <div>The <code>level</code> argument configures the level of the user when logged into the system. This argument accepts string values admin or operator.</div> - <div style="font-size: small; color: darkgreen"><br/>aliases: role</div> + <div>Public key string (base64 encoded)</div> </td> </tr> <tr> + <td class="elbow-placeholder"></td> <td colspan="2"> <div class="ansibleOptionAnchor" id="parameter-"></div> <b>name</b> <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> <div style="font-size: small"> <span style="color: purple">string</span> + / <span style="color: red">required</span> </div> </td> <td> </td> <td> - <div>The username to be configured on the VyOS device. This argument accepts a string value and is mutually exclusive with the <code>aggregate</code> argument.</div> + <div>Name of the key (usually in the form of user@hostname)</div> </td> </tr> <tr> + <td class="elbow-placeholder"></td> <td colspan="2"> <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>type</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + / <span style="color: red">required</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>ssh-dss</li> + <li>ssh-rsa</li> + <li>ecdsa-sha2-nistp256</li> + <li>ecdsa-sha2-nistp384</li> + <li>ssh-ed25519</li> + <li>ecdsa-sha2-nistp521</li> + </ul> + </td> + <td> + <div>Type of the key</div> + </td> + </tr> + + <tr> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> <b>purge</b> <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> <div style="font-size: small"> @@ -238,7 +392,7 @@ Parameters </td> </tr> <tr> - <td colspan="2"> + <td colspan="3"> <div class="ansibleOptionAnchor" id="parameter-"></div> <b>state</b> <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> @@ -257,7 +411,7 @@ Parameters </td> </tr> <tr> - <td colspan="2"> + <td colspan="3"> <div class="ansibleOptionAnchor" id="parameter-"></div> <b>update_password</b> <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> @@ -307,7 +461,6 @@ Examples aggregate: - name: netop - name: netend - level: operator state: present - name: Change Password for User netop vyos.vyos.vyos_user: @@ -344,7 +497,7 @@ Common return values are documented `here <https://docs.ansible.com/ansible/late <div>The list of configuration mode commands to send to the device</div> <br/> <div style="font-size: smaller"><b>Sample:</b></div> - <div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">['set system login user test level operator', 'set system login user authentication plaintext-password password']</div> + <div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">['set system login user authentication plaintext-password password']</div> </td> </tr> </table> diff --git a/plugins/modules/vyos_user.py b/plugins/modules/vyos_user.py index 53c45c2..5aebf94 100644 --- a/plugins/modules/vyos_user.py +++ b/plugins/modules/vyos_user.py @@ -62,6 +62,12 @@ options: - 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 @@ -77,13 +83,6 @@ options: 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 @@ -94,6 +93,32 @@ options: 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 @@ -104,6 +129,12 @@ options: - 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 @@ -120,13 +151,7 @@ options: 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 @@ -161,7 +186,6 @@ EXAMPLES = """ aggregate: - name: netop - name: netend - level: operator state: present - name: Change Password for User netop vyos.vyos.vyos_user: @@ -177,7 +201,6 @@ commands: returned: always type: list sample: - - set system login user test level operator - set system login user authentication plaintext-password password """ @@ -198,11 +221,6 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import ) -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"] @@ -220,11 +238,39 @@ def spec_to_commands(updates, module): 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: @@ -237,20 +283,57 @@ def spec_to_commands(updates, module): 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) @@ -268,8 +351,9 @@ def config_to_dict(module): "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) @@ -289,6 +373,21 @@ def get_param_value(key, item, 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: @@ -309,9 +408,10 @@ def map_params_to_obj(module): 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 @@ -332,13 +432,30 @@ def update_objects(want, have): 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) @@ -359,7 +476,11 @@ def main(): 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, diff --git a/tests/integration/targets/vyos_smoke/tests/cli/caching.yaml b/tests/integration/targets/vyos_smoke/tests/cli/caching.yaml index 9afea2e..b7e7e1f 100644 --- a/tests/integration/targets/vyos_smoke/tests/cli/caching.yaml +++ b/tests/integration/targets/vyos_smoke/tests/cli/caching.yaml @@ -6,11 +6,9 @@ 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' @@ -31,15 +29,13 @@ - 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: diff --git a/tests/integration/targets/vyos_user/tests/cli/auth.yaml b/tests/integration/targets/vyos_user/tests/cli/auth.yaml index a3178bf..98a3d17 100644 --- a/tests/integration/targets/vyos_user/tests/cli/auth.yaml +++ b/tests/integration/targets/vyos_user/tests/cli/auth.yaml @@ -3,25 +3,24 @@ - 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 diff --git a/tests/integration/targets/vyos_user/tests/cli/basic.yaml b/tests/integration/targets/vyos_user/tests/cli/basic.yaml index 096edc0..381f0db 100644 --- a/tests/integration/targets/vyos_user/tests/cli/basic.yaml +++ b/tests/integration/targets/vyos_user/tests/cli/basic.yaml @@ -26,15 +26,14 @@ 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 @@ -56,7 +55,6 @@ - name: ansibletest2 - name: ansibletest3 - level: operator state: present - assert: diff --git a/tests/integration/targets/vyos_user/tests/cli/encrypted.yaml b/tests/integration/targets/vyos_user/tests/cli/encrypted.yaml new file mode 100644 index 0000000..39fbf61 --- /dev/null +++ b/tests/integration/targets/vyos_user/tests/cli/encrypted.yaml @@ -0,0 +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 + encrypted_password: "{{ encrypted_password }}" + state: present + +- assert: + that: + - result.changed == true + - "{{ 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 + full_name: "test user" + encrypted_password: "{{ encrypted_password }}" + state: present + +- assert: + that: + - result.changed == true + - "{{ encrypted_aggregate_add['commands'] | symmetric_difference(result['commands']) |length == 0 }}" + +- name: Add user again (Idempotent) + register: result + vyos.vyos.vyos_user: + name: ansibletest1 + encrypted_password: "{{ encrypted_password }}" + 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: 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 + encrypted_password: "{{ encrypted_password_2 }}" + state: present + +- assert: + that: + - 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 0000000..9ffa41e --- /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 0000000..89163fa --- /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 81cd1a4..9b73106 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 7029720..e8c5078 100644 --- a/tests/unit/modules/network/vyos/test_vyos_user.py +++ b/tests/unit/modules/network/vyos/test_vyos_user.py @@ -67,18 +67,6 @@ class TestVyosUserModule(TestVyosModule): 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) @@ -88,6 +76,7 @@ class TestVyosUserModule(TestVyosModule): [ "delete system login user ansible", "delete system login user admin", + "delete system login user ssh", ], ), ) @@ -129,3 +118,122 @@ class TestVyosUserModule(TestVyosModule): 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) |