Repository: ansible-collections/community.vmware

Issue:
vmware_host_lockdown: Add possibility to configure lockdown exception users
<!--- Verify first that your feature was not already discussed on GitHub -->
<!--- Complete *all* sections as described, this form is processed automatically -->

##### SUMMARY
<!--- Describe the new feature/improvement briefly below -->
Hello community,
I would like to see the possibility to configure lockdown exception users (these are users who are able to login to the host, even with activated lockdown mode). This is usefull for automation accounts.

##### ISSUE TYPE
- Feature Idea

##### COMPONENT NAME
<!--- Write the short name of the module, plugin, task or feature below, use your best guess if unsure -->
vmware_host_lockdown

##### ADDITIONAL INFORMATION
<!--- Describe how the feature would be used, why it is needed and what it would solve -->
By adding this feature it would be possible to natively use a dedicated ansible-bot-user on vmware hosts, without removing the lockdown mode from the host - performing the desired action - activating the lockdown mode again (and maybe adding a block with repair section - to lockdown even if something failed)
I have two additional parameters for this module in mind:

- lockdown_exception_user: username
- lockdown_exception_user_state: present/absent

I hope this is some kind of an easytask, unfortunally I'm not able to do it on my own.

<!--- Paste example playbooks or commands between quotes below -->
```yaml

```

<!--- HINT: You can also paste gist.github.com links for larger files -->



Discussion:
Maybe usefull methods of the api:
https://code.vmware.com/apis/704/vsphere/vim.host.HostAccessManager.html#queryLockdownExceptions
https://code.vmware.com/apis/704/vsphere/vim.host.HostAccessManager.html#updateLockdownExceptions
@Kloppi313 Thanks for reporting this feature. I am not able to access `hostAccessManager` but I am not quite sure why. 

```
(Pdb) content.hostAccessManager
*** AttributeError: 'vim.ServiceInstanceContent' object has no attribute 'hostAccessManager'
```

@Akasurde Oh, by searching a bit I found, that the module might use a deprecated function: https://code.vmware.com/apis/704/vsphere/vim.HostSystem.html#enterLockdownMode (Deprecated. As of vSphere API 6.0, use ChangeLockdownMode.) as well as
https://code.vmware.com/apis/704/vsphere/vim.HostSystem.html#exitLockdownMode
(maybe because of backwarts compatibility?)

They were replaced by
https://code.vmware.com/apis/704/vsphere/vim.host.HostAccessManager.html#changeLockdownMode -> it also needs HostAccessManager
So is it possible that you are developing with some older version of esxi?
This could be case sensitive: HostAccessManager instead of hostAccessManager
cc @Tomorrow9 @goneri @lparkes @pgbidkar @warthog9
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
I am having the same issue, how do I update the lockdown exception users using ansible? Is this feature going to be added to the REST API?
Hi @Akasurde , you added the "waiting_on_contributer" label. Where are you waiting for in this case? I see that @Kloppi313 and @mainboarder replied, have you seen that as well? I'm not able to communicate to the API on my own, but I'm willing to participate where possible to help creating this module.

Most recent reference guide:
https://vdc-download.vmware.com/vmwb-repository/dcr-public/bf660c0a-f060-46e8-a94d-4b5e6ffc77ad/208bc706-e281-49b6-a0ce-b402ec19ef82/SDK/vsphere-ws/docs/ReferenceGuide/vim.host.HostAccessManager.html#updateLockdownExceptions
@denknl `waiting_on_contributer` means we want this functionality but have no time or resources, so someone from the community can take a lead on this and work on it. You might want to start drafting WIP PR so that we can guide you along the way. Thanks.

Resolving patch:
diff --git a/plugins/modules/vmware_host_lockdown_exceptions.py b/plugins/modules/vmware_host_lockdown_exceptions.py
new file mode 100644
--- /dev/null
+++ b/plugins/modules/vmware_host_lockdown_exceptions.py
@@ -0,0 +1,189 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2022, Mario Lenz <m@riolenz.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: vmware_host_lockdown_exceptions
+version_added: '3.1.0'
+short_description: Manage Lockdown Mode Exception Users
+description:
+- This module can be used to manage Lockdown Mode Exception Users.
+- All parameters and VMware objects values are case sensitive.
+- Please specify C(hostname) as vCenter IP or hostname only, as lockdown operations are not possible from standalone ESXi server.
+author:
+- Mario Lenz (@mariolenz)
+options:
+  cluster_name:
+    description:
+    - Name of cluster.
+    - All host systems from given cluster used to manage exception users.
+    - Required parameter, if C(esxi_hostname) is not set.
+    type: str
+  esxi_hostname:
+    description:
+    - List of ESXi hostname to manage exception users.
+    - Required parameter, if C(cluster_name) is not set.
+    type: list
+    elements: str
+  state:
+    description:
+    - If C(present), make sure the given users are defined as Lockdown Mode Exception Users.
+    - If C(absent), make sure the given users are NO Lockdown Mode Exception Users.
+    - If C(set), will replace Lockdown Mode Exception Users defined list of users.
+    default: present
+    choices: [ present, absent , set ]
+    type: str
+  exception_users:
+    description:
+    - List of Lockdown Mode Exception Users.
+    - To remove all Exception Users, I(state=set) the empty list.
+    type: list
+    elements: str
+    required: True
+extends_documentation_fragment:
+- community.vmware.vmware.documentation
+
+'''
+
+EXAMPLES = r'''
+- name: Remove all Lockdown Mode Exception Users on a host
+  community.vmware.vmware_host_lockdown:
+    hostname: '{{ vcenter_hostname }}'
+    username: '{{ vcenter_username }}'
+    password: '{{ vcenter_password }}'
+    esxi_hostname: '{{ esxi_hostname }}'
+    exception_users: []
+    state: set
+  delegate_to: localhost
+'''
+
+RETURN = r'''
+results:
+    description: metadata about exception users of Host systems
+    returned: always
+    type: dict
+    sample: {
+                "host_lockdown_exceptions": {
+                    "DC0_C0": {
+                        "current_exception_users": [],
+                        "desired_exception_users": [],
+                        "previous_exception_users": [
+                            "root"
+                        ]
+                    },
+                }
+            }
+'''
+
+try:
+    from pyVmomi import vim
+    HAS_PYVMOMI = True
+except ImportError:
+    HAS_PYVMOMI = False
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.vmware.plugins.module_utils.vmware import vmware_argument_spec, PyVmomi
+from ansible.module_utils._text import to_native
+
+
+class VmwareLockdownManager(PyVmomi):
+    def __init__(self, module):
+        super(VmwareLockdownManager, self).__init__(module)
+        if not self.is_vcenter():
+            self.module.fail_json(msg="Lockdown operations are performed from vCenter only. "
+                                      "hostname %s is an ESXi server. Please specify hostname "
+                                      "as vCenter server." % self.module.params['hostname'])
+        cluster_name = self.params.get('cluster_name', None)
+        esxi_host_name = self.params.get('esxi_hostname', None)
+        self.hosts = self.get_all_host_objs(cluster_name=cluster_name, esxi_host_name=esxi_host_name)
+
+    def ensure(self):
+        """
+        Function to manage internal state management
+        """
+        results = dict(changed=False, host_lockdown_exceptions=dict())
+        change_list = []
+        desired_state = self.params.get('state')
+        exception_users = self.params.get('exception_users')
+        for host in self.hosts:
+            current_exception_users = host.configManager.hostAccessManager.QueryLockdownExceptions()
+            current_exception_users.sort()
+            new_exception_users = current_exception_users.copy()
+            results['host_lockdown_exceptions'][host.name] = dict(previous_exception_users=current_exception_users)
+            changed = False
+
+            if desired_state == 'present':
+                for user in exception_users:
+                    if user not in current_exception_users:
+                        new_exception_users.append(user)
+                        changed = True
+            elif desired_state == 'absent':
+                for user in exception_users:
+                    if user in current_exception_users:
+                        new_exception_users.remove(user)
+                        changed = True
+            elif desired_state == 'set':
+                if set(current_exception_users) != set(exception_users):
+                    new_exception_users = exception_users
+                    changed = True
+
+            new_exception_users.sort()
+            results['host_lockdown_exceptions'][host.name]['desired_exception_users'] = new_exception_users
+            results['host_lockdown_exceptions'][host.name]['current_exception_users'] = new_exception_users
+
+            if not self.module.check_mode:
+                try:
+                    host.configManager.hostAccessManager.UpdateLockdownExceptions(new_exception_users)
+
+                except vim.fault.HostConfigFault as host_config_fault:
+                    self.module.fail_json(msg="Failed to manage lockdown mode for esxi"
+                                              " hostname %s : %s" % (host.name, to_native(host_config_fault.msg)))
+                except vim.fault.AdminDisabled as admin_disabled:
+                    self.module.fail_json(msg="Failed to manage lockdown mode as administrator "
+                                              "permission has been disabled for "
+                                              "esxi hostname %s : %s" % (host.name, to_native(admin_disabled.msg)))
+                except Exception as generic_exception:
+                    self.module.fail_json(msg="Failed to manage lockdown mode due to generic exception for esxi "
+                                              "hostname %s : %s" % (host.name, to_native(generic_exception)))
+            change_list.append(changed)
+
+        if any(change_list):
+            results['changed'] = True
+
+        self.module.exit_json(**results)
+
+
+def main():
+    argument_spec = vmware_argument_spec()
+    argument_spec.update(
+        cluster_name=dict(type='str', required=False),
+        esxi_hostname=dict(type='list', required=False, elements='str'),
+        exception_users=dict(type='list', required=True, elements='str'),
+        state=dict(type='str', default='present', choices=['present', 'absent', 'set'], required=False),
+    )
+
+    module = AnsibleModule(
+        argument_spec=argument_spec,
+        supports_check_mode=True,
+        required_one_of=[
+            ['cluster_name', 'esxi_hostname'],
+        ]
+    )
+
+    if not HAS_PYVMOMI:
+        module.fail_json(msg='pyvmomi required for this module')
+
+    vmware_lockdown_mgr = VmwareLockdownManager(module)
+    vmware_lockdown_mgr.ensure()
+
+
+if __name__ == "__main__":
+    main()


Test patch:
diff --git a/tests/integration/targets/vmware_host_lockdown_exceptions/aliases b/tests/integration/targets/vmware_host_lockdown_exceptions/aliases
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/vmware_host_lockdown_exceptions/aliases
@@ -0,0 +1,3 @@
+cloud/vcenter
+needs/target/prepare_vmware_tests
+zuul/vmware/vcenter_1esxi
diff --git a/tests/integration/targets/vmware_host_lockdown_exceptions/tasks/main.yml b/tests/integration/targets/vmware_host_lockdown_exceptions/tasks/main.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/vmware_host_lockdown_exceptions/tasks/main.yml
@@ -0,0 +1,195 @@
+# Test code for the vmware_host_lockdown_exceptions module.
+# # Copyright: (c) 2022, Mario Lenz <m@riolenz.de>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+- import_role:
+    name: prepare_vmware_tests
+  vars:
+    setup_attach_host: true
+
+- name: Allow root login in lockdown mode (check mode)
+  community.vmware.vmware_host_lockdown_exceptions: &allow_root
+    hostname: "{{ vcenter_hostname }}"
+    username: "{{ vcenter_username }}"
+    password: "{{ vcenter_password }}"
+    esxi_hostname: '{{ esxi1 }}'
+    validate_certs: false
+    exception_users:
+      - "root"
+    state: present
+  check_mode: true
+  register: result_allow_root
+
+- debug:
+    var: result_allow_root
+
+- name: Assert that root would be allowed in lockdown mode
+  assert:
+    that:
+      - result_allow_root is changed
+
+- name: Allow root login in lockdown mode
+  community.vmware.vmware_host_lockdown_exceptions:
+    <<: *allow_root
+  register: result_allow_root
+
+- debug:
+    var: result_allow_root
+
+- name: Assert that root is allowed in lockdown mode
+  assert:
+    that:
+      - result_allow_root is changed
+
+- name: Allow root login in lockdown mode again (idempotency)
+  community.vmware.vmware_host_lockdown_exceptions:
+    <<: *allow_root
+  register: result_allow_root
+
+- debug:
+    var: result_allow_root
+
+- name: Assert no changes (idempotency)
+  assert:
+    that:
+      - result_allow_root is not changed
+
+- name: Disallow root login in lockdown mode (check mode)
+  community.vmware.vmware_host_lockdown_exceptions: &disallow_root
+    hostname: "{{ vcenter_hostname }}"
+    username: "{{ vcenter_username }}"
+    password: "{{ vcenter_password }}"
+    esxi_hostname: '{{ esxi1 }}'
+    validate_certs: false
+    exception_users:
+      - "root"
+    state: absent
+  check_mode: true
+  register: result_disallow_root
+
+- debug:
+    var: result_disallow_root
+
+- name: Assert that root would be disallowed in lockdown mode
+  assert:
+    that:
+      - result_disallow_root is changed
+
+- name: Disallow root login in lockdown mode
+  community.vmware.vmware_host_lockdown_exceptions:
+    <<: *disallow_root
+  register: result_disallow_root
+
+- debug:
+    var: result_disallow_root
+
+- name: Assert that root is disallowed in lockdown mode
+  assert:
+    that:
+      - result_disallow_root is changed
+
+- name: Disllow root login in lockdown mode again (idempotency)
+  community.vmware.vmware_host_lockdown_exceptions:
+    <<: *disallow_root
+  register: result_disallow_root
+
+- debug:
+    var: result_disallow_root
+
+- name: Assert no changes (idempotency)
+  assert:
+    that:
+      - result_disallow_root is not changed
+
+- name: Set the list of exception users to exactly root (check mode)
+  community.vmware.vmware_host_lockdown_exceptions: &set_exception_users
+    hostname: "{{ vcenter_hostname }}"
+    username: "{{ vcenter_username }}"
+    password: "{{ vcenter_password }}"
+    esxi_hostname: '{{ esxi1 }}'
+    validate_certs: false
+    exception_users:
+      - "root"
+    state: set
+  check_mode: true
+  register: result_set_exception_users
+
+- debug:
+    var: result_set_exception_users
+
+- name: Assert that root would be allowed in lockdown mode
+  assert:
+    that:
+      - result_set_exception_users is changed
+
+- name: Set the list of exception users to exactly root
+  community.vmware.vmware_host_lockdown_exceptions:
+    <<: *set_exception_users
+  register: result_set_exception_users
+
+- debug:
+    var: result_set_exception_users
+
+- name: Assert that root is allowed in lockdown mode
+  assert:
+    that:
+      - result_set_exception_users is changed
+
+- name: Set the list of exception users to exactly root again (idempotency)
+  community.vmware.vmware_host_lockdown_exceptions:
+    <<: *set_exception_users
+  register: result_set_exception_users
+
+- debug:
+    var: result_set_exception_users
+
+- name: Assert no changes (idempotency)
+  assert:
+    that:
+      - result_set_exception_users is not changed
+
+- name: Remove all exception users (check mode)
+  community.vmware.vmware_host_lockdown_exceptions: &remove_all_exception_users
+    hostname: "{{ vcenter_hostname }}"
+    username: "{{ vcenter_username }}"
+    password: "{{ vcenter_password }}"
+    esxi_hostname: '{{ esxi1 }}'
+    validate_certs: false
+    exception_users: []
+    state: set
+  check_mode: true
+  register: result_remove_all_exception_users
+
+- debug:
+    var: result_remove_all_exception_users
+
+- name: Assert that all exception users would be removed
+  assert:
+    that:
+      - result_remove_all_exception_users is changed
+
+- name: Remove all exception users
+  community.vmware.vmware_host_lockdown_exceptions:
+    <<: *remove_all_exception_users
+  register: result_remove_all_exception_users
+
+- debug:
+    var: result_remove_all_exception_users
+
+- name: Assert that all exception users are removed
+  assert:
+    that:
+      - result_remove_all_exception_users is changed
+
+- name: Remove all exception users again (idempotency)
+  community.vmware.vmware_host_lockdown_exceptions:
+    <<: *remove_all_exception_users
+  register: result_remove_all_exception_users
+
+- debug:
+    var: result_remove_all_exception_users
+
+- name: Assert no changes (idempotency)
+  assert:
+    that:
+      - result_remove_all_exception_users is not changed