Browse Source

Version 3.0
-----------
--> Remove dependency on Prism Element with REST API calls only to Prism Central
--> Allow joining VMs to specific OUs in AD
--> Modify hostnames to provide uniqueness across projects
--> Update VM's category to allow the application of Policies. Category "Self-Service" must exist with values the same as that of the Project names.
--------------------------------------------------------------------------------

cybergavin 4 years ago
parent
commit
8d95f6b42f

+ 14 - 11
self-service/README.md

@@ -1,4 +1,8 @@
-# Nutanix Self-Service VM Provisioning Customization
+- Nutanix Self-Service VM Provisioning Customization when using Active Directory as the IdP for access to the VM
+- Supports environments with a single Prism Central managing a single Nutanix cluster
+- Tested Environment: 
+  - Nutanix: AOS 5.15.4/AHV 20190916.360/Prism Central pc.2021.1.0.1 - Guest VMs: Oracle Linux 8, Red Hat Enterprise Linux 8 and Windows Server 2019
+
 
 ## Application Listing
 
@@ -11,11 +15,13 @@
 ### ntx_ssvm_customize.py
 This script customizes a Linux or Windows VM upon self-service provisioning
 by doing the following:
-1. Obtaining metadata (e.g. project, owner) about the VM.
-2. Mounting and Enabling Nutanix Guest Tools on the VM.
-3. Joining the VM to a specified Active Directory Domain.
-4. Provisioning standard SSH/RDP access (unprivileged) to the user's Project team.
-5. Provisioning privileged access (sudo/Administrator) to the user/owner.
+1. Obtains metadata (e.g. project, owner) about the VM.
+2. Updates the VM's category (for pre-provisioned Policies)
+3. Renames the VM to ensure uniqueness across (but, not within) projects.
+4. Mounts and enables Nutanix Guest Tools on the VM.
+5. Joins the VM to the specified Active Directory Domain.
+6. Provisions standard SSH/RDP access (unprivileged) to the user's Project team.
+7. Provisions privileged access (sudo/Administrator) to the user/owner.
 
 When done, the user/owner may directly access the VM via SSH or RDP using 
 their Active Directory domain account.
@@ -29,11 +35,8 @@ Replace the values within <> and including <> with actual values.
 The script deletes the credential file upon completion.
 
 **DO NOT commit this file to source code management if you add credentials to it.**
-**It is recommended to fetch these credentials from a vault at runtime.** . You will have to 
-modify the python script.
+**Upon standardization of an Enterprise Vault, it is recommended to fetch these credentials from the vault at runtime**
 
 
 All the above files must be burned into a VM template and configured to be executed
-by cloud-init (Linux) or cloudbase-init(Windows). Refer to the workflow in
-ntx-ssvm-vmtemplate-customization.png and "cloud-init" and "cloudbase-init" directories
-for sample configuration files.
+by cloud-init (Linux) or cloudbase-init(Windows).

+ 12 - 7
self-service/ntx_ssvm_customize.cfg

@@ -1,18 +1,23 @@
+#############################################
+# Configuration file
+# DO NOT use any quotes for variable values
+#############################################
 # Nutanix Prism
 [ntx_prism]
 # Example FQDNs => prismcentral.contoso.com, prismelement.contoso.com
-ntx_pc_fqdn =
-ntx_pe_fqdn =
+ntx_pc_fqdn = 
 # Timeout for REST API calls, in seconds
-timeout =
+timeout = 
 
 # Microsoft Active Directory
 [ms_ad]
 # NetBIOS AD domain name. Example: CONTOSO
-ad_domain =
+ad_domain = 
 # AD Domain FQDN. Example: CONTOSO.COM
-ad_fqdn =
+ad_fqdn = 
+# AD OU for the servers to join
+ad_ou = 
 # AD Group name for Linux VM administrators
-linux_admins =
+linux_admins = 
 # AD Group name for Windows VM administrators
-windows_admins =
+windows_admins = 

+ 6 - 4
self-service/ntx_ssvm_customize.cred

@@ -1,9 +1,11 @@
-# Nutanix Prism Central/Element credentials
-# Replace <auth token> by the base64 encoded value of user:password.
+##############################################
+# Nutanix Prism Central credentials
+# DO NOT use quotes in credential values
+##############################################
+# Replace <auth hash> by the base64 encoded value of user:password.
 # 
 [ntx_prism]
-ntx_pc_auth = <auth token>
-ntx_pe_auth = <auth token>
+ntx_pc_auth = <auth hash>
 
 # Active Directory Bind account credentials
 [ad_bind]

+ 285 - 156
self-service/ntx_ssvm_customize.py

@@ -4,12 +4,14 @@
 Customization script for Self-Service VM Provisioning via Nutanix Prism
 Central. Invoked by cloud-init (Linux) and cloudbase-init (Windows)
 when a VM is provisioned by an end user.
+Performs a single reboot for Linux and 3 reboots for Windows.
 
 """
 
 
-__version__ = '2.0'
+__version__ = '3.0'
 __author__ = 'cybergavin'
+__email__ = 'cybergavin@gmail.com'
 
 
 import logging
@@ -35,6 +37,8 @@ try:
     log_file = script_dir / f'{script_stem}.log'
     cfg_file = script_dir / f'{script_stem}.cfg'
     cred_file = script_dir / f'{script_stem}.cred'
+    run_file = script_dir / f'{script_stem}.running'
+   
 
     # Set up Logger
     logger = logging.getLogger(__name__)
@@ -42,7 +46,7 @@ try:
     _FORMAT = '%(asctime)s.%(msecs)03d — %(module)s:%(name)s:%(lineno)d — %(levelname)s — %(message)s'
     formatter = logging.Formatter(_FORMAT, datefmt='%Y-%b-%d %H:%M:%S')
     console_handler = logging.StreamHandler(sys.stdout)
-    file_handler = logging.FileHandler(log_file, mode='w')
+    file_handler = logging.FileHandler(log_file, mode='a')
     console_handler.setFormatter(formatter)
     file_handler.setFormatter(formatter)
     console_handler.setLevel(logging.INFO)
@@ -63,185 +67,311 @@ try:
     if not Path(cred_file).exists():
         logger.critical('Missing required file %s.', cred_file)
         sys.exit()
- 
+
     # Parsing configuration and credential files
-    config = configparser.ConfigParser()
+    config = configparser.ConfigParser(interpolation=None)
     config.read([cfg_file, cred_file])
     ntx_pc_url = f'https://{config["ntx_prism"]["ntx_pc_fqdn"]}:9440/api/nutanix/v3'
-    ntx_pe_url = f'https://{config["ntx_prism"]["ntx_pe_fqdn"]}:9440/PrismGateway/services/rest/v1'
     prism_timeout = int(config['ntx_prism']['timeout'])
     ad_domain = config['ms_ad']['ad_domain']
     ad_fqdn = config['ms_ad']['ad_fqdn']
+    ad_ou = config['ms_ad']['ad_ou']
     linux_admins = config['ms_ad']['linux_admins']
     windows_admins = config['ms_ad']['windows_admins']
     ntx_pc_auth = config['ntx_prism']['ntx_pc_auth']
-    ntx_pe_auth = config['ntx_prism']['ntx_pe_auth']
     ad_bind_user = config['ad_bind']['ad_bind_user']
     ad_bind_password = config['ad_bind']['ad_bind_password']
+    
+    # Variables for REST API calls to Prism Central
+    headers = {'Content-Type': 'application/json',
+    'Accept': 'application/json',
+    'Authorization': f'Basic {ntx_pc_auth}'}
+    vm_filter = {'kind': 'vm','filter': f'vm_name=={script_host}'}
+    vm_ngt = {"nutanix_guest_tools": {"iso_mount_state":"MOUNTED","enabled_capability_list": ["SELF_SERVICE_RESTORE","VSS_SNAPSHOT"],"state":"ENABLED"}}
 
     logger.debug('Parsed files %s and %s. Available sections are %s',
     cfg_file, cred_file, config.sections())
     logger.debug('Nutanix Prism Central URL is %s', ntx_pc_url)
-    logger.debug('Nutanix Prism Element URL is %s', ntx_pe_url)
     logger.info('Parsed configuration and credential files')
 
     logger.debug('Validation checks PASSED')
 
-    # Get VM's Metadata from Nutanix Prism Central
-    headers = {'Content-Type': 'application/json',
-    'Accept': 'application/json',
-    'Authorization': f'Basic {ntx_pc_auth}'}
-    data = {'kind': 'vm',
-    'filter': f'vm_name=={script_host}'}
-    md_response = requests.post(
-        f'{ntx_pc_url}/vms/list',
-        headers=headers,
-        data=json.dumps(data),
-        timeout=prism_timeout,
-        verify=False)
-    md_json = md_response.json()
-    logger.debug('JSON Response from Prism Central. \n %s', md_json)
-    md_response.raise_for_status()
-    logger.info('Successfully called Prism Central API')
-    ssvm_uuid = md_json["entities"][-1]["metadata"]["uuid"]
-    try:
-        ssvm_project = md_json["entities"][-1]["metadata"]["project_reference"]["name"]
-    except KeyError:
-        ssvm_project = ''
-    try:
-        ssvm_owner = md_json["entities"][-1]["metadata"]["owner_reference"]["name"]
-    except KeyError:
-        ssvm_owner = ''
-
-    logger.debug('UUID=%s|PROJECT=%s|OWNER=%s', ssvm_uuid, ssvm_project, ssvm_owner)
-    logger.info('Obtained metadata from Prism Central.')
-
-    # Mount and Enable Nutanix Guest Tools via Prism Element
-    headers = {'Content-Type': 'application/json',
-    'Accept': 'application/json',
-    'Authorization': f'Basic {ntx_pe_auth}'}
-    ngt_mount_response = requests.post(
-        f'{ntx_pe_url}/vms/{ssvm_uuid}/guest_tools/mount',
-        headers=headers,
-        timeout=prism_timeout,
-        verify=False)
-    logger.debug('JSON Response from Prism Element for mounting NGT \n %s',
-    ngt_mount_response.json())
-    ngt_mount_response.raise_for_status()
-    logger.info('Successfully called Prism Element API to mount NGT')
-    data = {
-        'vmUuid': f'{ssvm_uuid}',
-        'enabled': 'true',
-        'applications': {
-            "file_level_restore": "true",
-            "vss_snapshot": "true"}}
-    ngt_enable_response = requests.post(
-        f'{ntx_pe_url}/vms/{ssvm_uuid}/guest_tools/',
-        headers=headers,
-        data=json.dumps(data),
-        timeout=prism_timeout,
-        verify=False)
-    logger.debug('JSON Response from Prism Element for enabling NGT \n %s',
-    ngt_enable_response.json())
-    ngt_enable_response.raise_for_status()
-    logger.info('Successfully called Prism Element API to enable NGT')
-    sleep(5)
-
-    # Join VM to the Active Directory Domain
-    if script_os.upper() == 'LINUX':
-        ad_join_cmd = (
-            f'echo "{ad_bind_password}" | realm join {ad_fqdn} '
-            f'-U {ad_bind_user}'
-        )
-        ENABLE_SSSD_CMD = (
-            'systemctl enable sssd && '
-            'sed -i "s/use_fully_qualified_names = True/use_fully_qualified_names = False/" '
-            '/etc/sssd/sssd.conf'
-        )
-        adminaccess_cmd = (
-            f'realm deny --all && '
-            f'realm permit -g "{ad_domain}\\{linux_admins}" && '
-            f'echo "%{linux_admins} ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers'
-        )
-        if ssvm_project and ssvm_owner:
-            useraccess_cmd = (
-                f'realm permit -g "{ad_domain}\\{ssvm_project}" && '
-                f'echo "{ssvm_owner[:ssvm_owner.rfind("@")]} ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers'
-                )
-        with open(log_file, 'a') as _lf:
-            if not subprocess.run(
-                f'{ad_join_cmd}',
-                shell=True,
-                stdout=_lf,
-                stderr=_lf).returncode:
-                logger.info('Joined VM %s to the %s AD Domain', script_host, ad_domain)
-            else:
-                logger.error('Failed to join VM %s to the %s AD Domain',
-                script_host, ad_domain)
-            if not subprocess.run(
-                f'{ENABLE_SSSD_CMD}',
-                shell=True,
-                stdout=_lf,
-                stderr=_lf).returncode:
-                logger.info('Configured and enabled SSSD')
-            else:
-                logger.error('Failed to configure/enable SSSD')
-            if not subprocess.run(
-                f'{adminaccess_cmd}',
-                shell=True,
-                stdout=_lf,
-                stderr=_lf).returncode:
-                logger.info('Provisioned Admin access to the VM.')
-            else:
-                logger.error('Failed to provision Admin access to the VM.')
+    '''
+    Use run_file to pass values between Windows VM reboots.
+    run_file is not used for Linux VM customization
+    '''
+    if not Path(run_file).exists():
+        # Get VM's Metadata from Nutanix Prism Central
+        try:
+            md_response = requests.post(
+                f'{ntx_pc_url}/vms/list',
+                headers=headers,
+                data=json.dumps(vm_filter),
+                timeout=prism_timeout,
+                verify=False)
+            md_json = md_response.json()
+            md_response.raise_for_status()
+        except requests.exceptions.HTTPError as err_http:
+            logger.error(err_http)
+            sys.exit()
+        except requests.exceptions.ConnectionError as err_conn:
+            logger.error(err_conn)
+            sys.exit()
+        except requests.exceptions.Timeout as err_timeout:
+            logger.error(err_timeout)
+            sys.exit()
+        except requests.exceptions.RequestException as err:
+            logger.error(err)
+            sys.exit()
+
+        logger.info('Successfully called Prism Central API for VM spec')
+        logger.debug('VM spec JSON from Prism Central. \n %s', json.dumps(md_json,indent=4))
+
+        try:
+            ssvm_uuid = md_json["entities"][-1]["metadata"]["uuid"]
+            ssvm_project = md_json["entities"][-1]["metadata"]["project_reference"]["name"]
+            ssvm_owner = md_json["entities"][-1]["metadata"]["owner_reference"]["name"]
+        except KeyError as ke:
+            logger.error('Encountered KeyError while obtaining VM Metadata \n %s',ke)
+            sys.exit()            
+        
+        logger.debug('UUID=%s|PROJECT=%s|OWNER=%s', ssvm_uuid, ssvm_project, ssvm_owner)
+        logger.info('Obtained the required metadata from Prism Central.')
+
+        '''
+        Generate a new hostname for other IT systems (AD/Patching/etc.)
+        Modified hostname -> upto 12 characters of provided name combined with
+        '-NN', where NN are the last 2 characters of the project name. This
+        allows for unique names across (but not within) Nutanix projects
+        '''
+        mon_host = f'{script_host[:12]}-{ssvm_project[-2:]}'
+
+        '''
+        Get the VM's latest spec (due to a current bug in different spec_version returned
+        in previous POST while creating VM and subsequent GET) for update.
+        '''
+        try:
+            vm_spec_res = requests.get(
+                f'{ntx_pc_url}/vms/{ssvm_uuid}',
+                headers=headers,
+                timeout=prism_timeout,
+                verify=False)
+            vm_spec = vm_spec_res.json()
+            vm_spec_res.raise_for_status()
+        except requests.exceptions.HTTPError as err_http:
+            logger.error(err_http)
+            sys.exit()            
+        except requests.exceptions.ConnectionError as err_conn:
+            logger.error(err_conn)
+            sys.exit()            
+        except requests.exceptions.Timeout as err_timeout:
+            logger.error(err_timeout)
+            sys.exit()            
+        except requests.exceptions.RequestException as err:
+            logger.error(err)
+            sys.exit()            
+
+        logger.info('Successfully called Prism Central API for latest VM spec')
+        logger.debug('Latest VM spec JSON from Prism Central. \n %s', json.dumps(vm_spec,indent=4))
+        
+        # Prepare VM spec for VM Update
+        new_vm_spec = {}
+        new_vm_spec["api_version"] = vm_spec["api_version"]
+        new_vm_spec["spec"] = vm_spec["spec"]
+        new_vm_spec["spec"]["name"] = mon_host        
+        new_vm_spec["spec"]["resources"]["guest_tools"] = vm_ngt
+        new_vm_spec["metadata"] = vm_spec["metadata"]
+        new_vm_spec["metadata"]["categories_mapping"] = { "Self-Service": [f"{ssvm_project}"]}
+        new_vm_spec["metadata"]["categories"] = { "Self-Service": f"{ssvm_project}"}
+
+        # Update VM - change hostname, mount Nutanix Guest Tools and update VM's categories
+        try:
+            update_vm_res = requests.put(
+                f'{ntx_pc_url}/vms/{ssvm_uuid}',
+                headers=headers,
+                data=json.dumps(new_vm_spec),
+                timeout=prism_timeout,
+                verify=False)
+            update_vm_json = update_vm_res.json()
+            update_vm_res.raise_for_status()
+        except requests.exceptions.HTTPError as err_http:
+            logger.error(err_http)
+            sys.exit()            
+        except requests.exceptions.ConnectionError as err_conn:
+            logger.error(err_conn)
+            sys.exit()            
+        except requests.exceptions.Timeout as err_timeout:
+            logger.error(err_timeout)
+            sys.exit()            
+        except requests.exceptions.RequestException as err:
+            logger.error(err)
+            sys.exit()            
+
+        logger.debug('JSON Response from Prism Central for updating VM \n %s',json.dumps(update_vm_json,indent=4))
+        logger.info('Successfully called Prism Central to update the VM')
+        sleep(10)
+
+        # Customize VM
+        if script_os.upper() == 'LINUX':
+            hostname_cmd = (
+                f'hostnamectl set-hostname {mon_host}'
+            )
+            ad_join_cmd = (
+                f'echo "{ad_bind_password}" | realm join "{ad_fqdn}" '
+                f'--computer-ou="{ad_ou}" '
+                f'-U "{ad_bind_user}"'
+            )
+            ENABLE_SSSD_CMD = (
+                'systemctl enable sssd && '
+                'sed -i "s/use_fully_qualified_names = True/use_fully_qualified_names = False/" '
+                '/etc/sssd/sssd.conf'
+            )
+            adminaccess_cmd = (
+                f'realm deny --all && '
+                f'realm permit -g "{ad_domain}\{linux_admins}" && '
+                f'echo \'"%{linux_admins}" ALL=(ALL) NOPASSWD: ALL\''
+                f'>> /etc/sudoers'
+            )
             if ssvm_project and ssvm_owner:
+                useraccess_cmd = (
+                    f'realm permit -g "{ad_domain}\{ssvm_project}" && '
+                    f'echo \'{ssvm_owner[:ssvm_owner.rfind("@")]} ALL=(ALL) NOPASSWD: ALL\''
+                    f'>> /etc/sudoers'
+                    )
+            with open(log_file, 'a') as _lf:
                 if not subprocess.run(
-                    f'{useraccess_cmd}',
+                    f'{hostname_cmd}',
                     shell=True,
                     stdout=_lf,
                     stderr=_lf).returncode:
-                    logger.info('Provisioned SSH access to the VM for %s\\%s AD group '
-                    'and sudo privileges for user %s', ad_domain,
-                    ssvm_project, ssvm_owner[:ssvm_owner.rfind("@")])
+                    logger.info('Renamed VM %s to %s before joining %s AD Domain',
+                    script_host, mon_host, ad_domain)
                 else:
-                    logger.error('Failed to provision user access to the VM.')
-            if not subprocess.run(
-                'systemctl restart sssd',
-                shell=True,
-                stdout=_lf,
-                stderr=_lf).returncode:
-                logger.info('Restarted the SSSD service.')
-            else:
-                logger.error('Failed to restart the SSSD service.')
-            if not subprocess.run('systemctl restart ngt_guest_agent',
-                shell=True,
-                stdout=_lf,
-                stderr=_lf).returncode:
-                logger.info('Restarted the Nutanix Guest Agent service.')
-            else:
-                logger.error('Failed to restart the Nutanix Guest Agent service.')
-    if script_os.upper() == 'WINDOWS':
+                    logger.error('Failed to rename VM %s before joining the %s AD Domain',
+                    script_host, ad_domain)
+                if not subprocess.run(
+                    f'{ad_join_cmd}',
+                    shell=True,
+                    stdout=_lf,
+                    stderr=_lf).returncode:
+                    logger.info('Joined VM %s to the %s AD Domain', mon_host, ad_domain)
+                else:
+                    logger.error('Failed to join VM %s to the %s AD Domain',
+                    mon_host, ad_domain)
+                if not subprocess.run(
+                    f'{ENABLE_SSSD_CMD}',
+                    shell=True,
+                    stdout=_lf,
+                    stderr=_lf).returncode:
+                    logger.info('Configured and enabled SSSD')
+                else:
+                    logger.error('Failed to configure/enable SSSD')
+                if not subprocess.run(
+                    f'{adminaccess_cmd}',
+                    shell=True,
+                    stdout=_lf,
+                    stderr=_lf).returncode:
+                    logger.info('Provisioned Admin access to the VM.')
+                else:
+                    logger.error('Failed to provision Admin access to the VM.')
+                if ssvm_project and ssvm_owner:
+                    if not subprocess.run(
+                        f'{useraccess_cmd}',
+                        shell=True,
+                        stdout=_lf,
+                        stderr=_lf).returncode:
+                        logger.info('Provisioned SSH access to the VM for %s\\%s AD group '
+                        'and sudo privileges for user %s', ad_domain,
+                        ssvm_project, ssvm_owner[:ssvm_owner.rfind("@")])
+                    else:
+                        logger.error('Failed to provision user access to the VM.')
+                if not subprocess.run(
+                    'systemctl restart sssd',
+                    shell=True,
+                    stdout=_lf,
+                    stderr=_lf).returncode:
+                    logger.info('Restarted the SSSD service.')
+                else:
+                    logger.error('Failed to restart the SSSD service.')
+                if not subprocess.run('systemctl restart ngt_guest_agent',
+                    shell=True,
+                    stdout=_lf,
+                    stderr=_lf).returncode:
+                    logger.info('Restarted the Nutanix Guest Agent service.')
+                else:
+                    logger.error('Failed to restart the Nutanix Guest Agent service.')                                     
+
+                # Disable cloud-init to prevent reverting the hostname
+                Path("/etc/cloud/cloud-init.disabled").touch()
+                if Path("/etc/cloud/cloud-init.disabled").exists():
+                    logger.info('Disabled the cloud-init service')
+                else:
+                    logger.error('Failed to disable the cloud-init service')
+
+        if script_os.upper() == 'WINDOWS':
+            # Log data in run_file for next run after Windows VM reboot
+            # Change hostname and reboot with exit code 1003 to signal to
+            # Cloudbase-init to rerun this program after reboot
+            with open(run_file, "a") as text_file:
+                print(f'ssvm_project={ssvm_project}', file=text_file)
+                print(f'ssvm_owner={ssvm_owner}', file=text_file)
+            hostname_cmd = (
+                f'netdom renamecomputer {script_host} '
+                f'/newname:{mon_host} '
+                f'/force'
+            )
+            with open(log_file, 'a') as _lf:
+                if not subprocess.run(
+                    f'{hostname_cmd}',
+                    shell=True,
+                    stdout=_lf,
+                    stderr=_lf).returncode:
+                    logger.info('Renamed VM %s to %s',script_host, mon_host)
+                    sys.exit(1003)
+                else:
+                    logger.error('Failed to rename VM %s to %s',
+                    script_host, mon_host)
+                    sys.exit(911)
+    else:
+        # Entering Windows reboot re-run block for further customization
+        ntx_fetched = {}
+        with open(run_file) as f:
+            for line in f.readlines():
+                key, value = line.rstrip("\n").split("=")
+                ntx_fetched[key] = value
+        ssvm_project = ntx_fetched["ssvm_project"]
+        ssvm_owner = ntx_fetched["ssvm_owner"]
+        if not ssvm_project and ssvm_owner:
+            log.error('Could not determine project and owner from run file. Exiting.')
+            sys.exit()
+            
+        logger.debug('ntx_fetched values are ssvm_project = %s and '
+        'ssvm_owner = %s', ssvm_project,ssvm_owner)
+
         ad_join_cmd = (
             f'netdom join {script_host} '
+            f'/ou:"{ad_ou}" '
             f'/domain:{ad_fqdn} '
             f'/ud:{ad_domain}\\{ad_bind_user} '
             f'/pd:"{ad_bind_password}"'
-        )
+            )
+
         adminaccess_cmd = (
+            f'net localgroup Administrators "{windows_admins}" /ADD'
+            )
+
+        useraccess_cmd = (
+            f'net localgroup "Remote Desktop Users" '
+            f'"{ssvm_project}" /ADD && '
             f'net localgroup Administrators '
-            f'{ad_domain}\\{windows_admins} /ADD'
+            f'{ssvm_owner[:ssvm_owner.rfind("@")]} /ADD'
             )
-        if ssvm_project and ssvm_owner:
-            useraccess_cmd = (
-                f'net localgroup "Remote Desktop Users" '
-                f'{ad_domain}\\{ssvm_project} /ADD && '
-                f'net localgroup Administrators '
-                f'{ad_domain}\\{ssvm_owner[:ssvm_owner.rfind("@")]} /ADD'
-                )
+
         NGA_RESTART_CMD = (
             'net stop "Nutanix Guest Agent" && '
             'net start "Nutanix Guest Agent"'
-        )
+            )
+
         with open(log_file, 'a') as _lf:
             if not subprocess.run(
                 f'{ad_join_cmd}',
@@ -262,6 +392,7 @@ try:
                 logger.info('Provisioned Admin access to the VM.')
             else:
                 logger.error('Failed to provision Admin access to the VM.')
+
             if ssvm_project and ssvm_owner:
                 if not subprocess.run(
                     f'{useraccess_cmd}',
@@ -273,6 +404,7 @@ try:
                     ad_domain, ssvm_project, ssvm_owner[:ssvm_owner.rfind("@")])
                 else:
                     logger.error('Failed to provision user access to the VM.')
+
             if not subprocess.run(
                 f'{NGA_RESTART_CMD}',
                 shell=True,
@@ -281,20 +413,17 @@ try:
                 logger.info('Restarted the Nutanix Guest Agent service.')
             else:
                 logger.error('Failed to restart the Nutanix Guest Agent service.')
-            if not subprocess.run('shutdown /r /t 5',
-                shell=True,
-                stdout=_lf,
-                stderr=_lf).returncode:
-                logger.info('Restarting host %s.', script_host)
-            else:
-                logger.error('Failed to trigger restart for %s.', script_host)
-except Exception:
-    logger.exception('Encountered unhandled exception')
-finally:
-    # Cleanup
+except Exception as ex:
+    logger.exception('Encountered unhandled exception\n %s',ex)
     if Path(cred_file).exists():
         Path(cred_file).unlink()
 
+# Cleanup
+if Path(cred_file).exists():
+    Path(cred_file).unlink()
+if Path(run_file).exists():
+    Path(run_file).unlink()    
+
 # Cloudbase-init exit for Windows to prevent re-execution upon boot.
 if script_os.upper() == 'WINDOWS':
     sys.exit(1001)