123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300 |
- #!/usr/bin/env python
- """
- 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.
- """
- __version__ = '2.0'
- __author__ = 'cybergavin'
- import logging
- import sys
- import configparser
- import json
- import platform
- import subprocess
- from pathlib import Path, PurePath
- from time import sleep
- import requests
- requests.packages.urllib3.disable_warnings()
- try:
- # Variables
- script_host = platform.node()
- script_os = platform.system()
- script_dir = Path((PurePath(sys.argv[0]).parent)).resolve(strict=True)
- script_name = PurePath(sys.argv[0]).name
- script_stem = PurePath(sys.argv[0]).stem
- log_file = script_dir / f'{script_stem}.log'
- cfg_file = script_dir / f'{script_stem}.cfg'
- cred_file = script_dir / f'{script_stem}.cred'
- # Set up Logger
- logger = logging.getLogger(__name__)
- logger.setLevel(logging.DEBUG)
- _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')
- console_handler.setFormatter(formatter)
- file_handler.setFormatter(formatter)
- console_handler.setLevel(logging.INFO)
- file_handler.setLevel(logging.DEBUG)
- logger.addHandler(console_handler)
- logger.addHandler(file_handler)
- logger.debug('script_dir=%s|log_file=%s', script_dir, log_file)
- logger.debug('cfg_file=%s|cred_file=%s', cfg_file, cred_file)
- # Validation
- if not sys.version_info.major == 3 and sys.version_info.minor >= 5:
- logger.critical('This script requires Python version >= 3.5.')
- sys.exit()
- if not Path(cfg_file).exists():
- logger.critical('Missing required file %s.', cfg_file)
- sys.exit()
- 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.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']
- 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']
- 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.')
- 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.')
- if script_os.upper() == 'WINDOWS':
- ad_join_cmd = (
- f'netdom join {script_host} '
- f'/domain:{ad_fqdn} '
- f'/ud:{ad_domain}\\{ad_bind_user} '
- f'/pd:"{ad_bind_password}"'
- )
- adminaccess_cmd = (
- f'net localgroup Administrators '
- f'{ad_domain}\\{windows_admins} /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}',
- shell=True,
- stdout=_lf,
- stderr=_lf).returncode:
- logger.info('Joined VM %s to the %s AD Domain',
- script_host, ad_domain)
- sleep(5)
- else:
- logger.error('Failed to join VM %s to the %s AD Domain',
- script_host, ad_domain)
- 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 standard RDP access to the VM '
- 'for %s\\%s AD group and Administrator 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(
- f'{NGA_RESTART_CMD}',
- 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 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
- if Path(cred_file).exists():
- Path(cred_file).unlink()
- # Cloudbase-init exit for Windows to prevent re-execution upon boot.
- if script_os.upper() == 'WINDOWS':
- sys.exit(1001)
|