ntx_ssvm_customize.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. #!/usr/bin/env python
  2. """
  3. Customization script for Self-Service VM Provisioning via Nutanix Prism
  4. Central. Invoked by cloud-init (Linux) and cloudbase-init (Windows)
  5. when a VM is provisioned by an end user.
  6. """
  7. __version__ = '2.0'
  8. __author__ = 'cybergavin'
  9. import logging
  10. import sys
  11. import configparser
  12. import json
  13. import platform
  14. import subprocess
  15. from pathlib import Path, PurePath
  16. from time import sleep
  17. import requests
  18. requests.packages.urllib3.disable_warnings()
  19. try:
  20. # Variables
  21. script_host = platform.node()
  22. script_os = platform.system()
  23. script_dir = Path((PurePath(sys.argv[0]).parent)).resolve(strict=True)
  24. script_name = PurePath(sys.argv[0]).name
  25. script_stem = PurePath(sys.argv[0]).stem
  26. log_file = script_dir / f'{script_stem}.log'
  27. cfg_file = script_dir / f'{script_stem}.cfg'
  28. cred_file = script_dir / f'{script_stem}.cred'
  29. # Set up Logger
  30. logger = logging.getLogger(__name__)
  31. logger.setLevel(logging.DEBUG)
  32. _FORMAT = '%(asctime)s.%(msecs)03d — %(module)s:%(name)s:%(lineno)d — %(levelname)s — %(message)s'
  33. formatter = logging.Formatter(_FORMAT, datefmt='%Y-%b-%d %H:%M:%S')
  34. console_handler = logging.StreamHandler(sys.stdout)
  35. file_handler = logging.FileHandler(log_file, mode='w')
  36. console_handler.setFormatter(formatter)
  37. file_handler.setFormatter(formatter)
  38. console_handler.setLevel(logging.INFO)
  39. file_handler.setLevel(logging.DEBUG)
  40. logger.addHandler(console_handler)
  41. logger.addHandler(file_handler)
  42. logger.debug('script_dir=%s|log_file=%s', script_dir, log_file)
  43. logger.debug('cfg_file=%s|cred_file=%s', cfg_file, cred_file)
  44. # Validation
  45. if not sys.version_info.major == 3 and sys.version_info.minor >= 5:
  46. logger.critical('This script requires Python version >= 3.5.')
  47. sys.exit()
  48. if not Path(cfg_file).exists():
  49. logger.critical('Missing required file %s.', cfg_file)
  50. sys.exit()
  51. if not Path(cred_file).exists():
  52. logger.critical('Missing required file %s.', cred_file)
  53. sys.exit()
  54. # Parsing configuration and credential files
  55. config = configparser.ConfigParser()
  56. config.read([cfg_file, cred_file])
  57. ntx_pc_url = f'https://{config["ntx_prism"]["ntx_pc_fqdn"]}:9440/api/nutanix/v3'
  58. ntx_pe_url = f'https://{config["ntx_prism"]["ntx_pe_fqdn"]}:9440/PrismGateway/services/rest/v1'
  59. prism_timeout = int(config['ntx_prism']['timeout'])
  60. ad_domain = config['ms_ad']['ad_domain']
  61. ad_fqdn = config['ms_ad']['ad_fqdn']
  62. linux_admins = config['ms_ad']['linux_admins']
  63. windows_admins = config['ms_ad']['windows_admins']
  64. ntx_pc_auth = config['ntx_prism']['ntx_pc_auth']
  65. ntx_pe_auth = config['ntx_prism']['ntx_pe_auth']
  66. ad_bind_user = config['ad_bind']['ad_bind_user']
  67. ad_bind_password = config['ad_bind']['ad_bind_password']
  68. logger.debug('Parsed files %s and %s. Available sections are %s',
  69. cfg_file, cred_file, config.sections())
  70. logger.debug('Nutanix Prism Central URL is %s', ntx_pc_url)
  71. logger.debug('Nutanix Prism Element URL is %s', ntx_pe_url)
  72. logger.info('Parsed configuration and credential files')
  73. logger.debug('Validation checks PASSED')
  74. # Get VM's Metadata from Nutanix Prism Central
  75. headers = {'Content-Type': 'application/json',
  76. 'Accept': 'application/json',
  77. 'Authorization': f'Basic {ntx_pc_auth}'}
  78. data = {'kind': 'vm',
  79. 'filter': f'vm_name=={script_host}'}
  80. md_response = requests.post(
  81. f'{ntx_pc_url}/vms/list',
  82. headers=headers,
  83. data=json.dumps(data),
  84. timeout=prism_timeout,
  85. verify=False)
  86. md_json = md_response.json()
  87. logger.debug('JSON Response from Prism Central. \n %s', md_json)
  88. md_response.raise_for_status()
  89. logger.info('Successfully called Prism Central API')
  90. ssvm_uuid = md_json["entities"][-1]["metadata"]["uuid"]
  91. try:
  92. ssvm_project = md_json["entities"][-1]["metadata"]["project_reference"]["name"]
  93. except KeyError:
  94. ssvm_project = ''
  95. try:
  96. ssvm_owner = md_json["entities"][-1]["metadata"]["owner_reference"]["name"]
  97. except KeyError:
  98. ssvm_owner = ''
  99. logger.debug('UUID=%s|PROJECT=%s|OWNER=%s', ssvm_uuid, ssvm_project, ssvm_owner)
  100. logger.info('Obtained metadata from Prism Central.')
  101. # Mount and Enable Nutanix Guest Tools via Prism Element
  102. headers = {'Content-Type': 'application/json',
  103. 'Accept': 'application/json',
  104. 'Authorization': f'Basic {ntx_pe_auth}'}
  105. ngt_mount_response = requests.post(
  106. f'{ntx_pe_url}/vms/{ssvm_uuid}/guest_tools/mount',
  107. headers=headers,
  108. timeout=prism_timeout,
  109. verify=False)
  110. logger.debug('JSON Response from Prism Element for mounting NGT \n %s',
  111. ngt_mount_response.json())
  112. ngt_mount_response.raise_for_status()
  113. logger.info('Successfully called Prism Element API to mount NGT')
  114. data = {
  115. 'vmUuid': f'{ssvm_uuid}',
  116. 'enabled': 'true',
  117. 'applications': {
  118. "file_level_restore": "true",
  119. "vss_snapshot": "true"}}
  120. ngt_enable_response = requests.post(
  121. f'{ntx_pe_url}/vms/{ssvm_uuid}/guest_tools/',
  122. headers=headers,
  123. data=json.dumps(data),
  124. timeout=prism_timeout,
  125. verify=False)
  126. logger.debug('JSON Response from Prism Element for enabling NGT \n %s',
  127. ngt_enable_response.json())
  128. ngt_enable_response.raise_for_status()
  129. logger.info('Successfully called Prism Element API to enable NGT')
  130. sleep(5)
  131. # Join VM to the Active Directory Domain
  132. if script_os.upper() == 'LINUX':
  133. ad_join_cmd = (
  134. f'echo "{ad_bind_password}" | realm join {ad_fqdn} '
  135. f'-U {ad_bind_user}'
  136. )
  137. ENABLE_SSSD_CMD = (
  138. 'systemctl enable sssd && '
  139. 'sed -i "s/use_fully_qualified_names = True/use_fully_qualified_names = False/" '
  140. '/etc/sssd/sssd.conf'
  141. )
  142. adminaccess_cmd = (
  143. f'realm deny --all && '
  144. f'realm permit -g "{ad_domain}\\{linux_admins}" && '
  145. f'echo "%{linux_admins} ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers'
  146. )
  147. if ssvm_project and ssvm_owner:
  148. useraccess_cmd = (
  149. f'realm permit -g "{ad_domain}\\{ssvm_project}" && '
  150. f'echo "{ssvm_owner[:ssvm_owner.rfind("@")]} ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers'
  151. )
  152. with open(log_file, 'a') as _lf:
  153. if not subprocess.run(
  154. f'{ad_join_cmd}',
  155. shell=True,
  156. stdout=_lf,
  157. stderr=_lf).returncode:
  158. logger.info('Joined VM %s to the %s AD Domain', script_host, ad_domain)
  159. else:
  160. logger.error('Failed to join VM %s to the %s AD Domain',
  161. script_host, config['active_directory']['ad_domain'])
  162. if not subprocess.run(
  163. f'{ENABLE_SSSD_CMD}',
  164. shell=True,
  165. stdout=_lf,
  166. stderr=_lf).returncode:
  167. logger.info('Configured and enabled SSSD')
  168. else:
  169. logger.error('Failed to configure/enable SSSD')
  170. if not subprocess.run(
  171. f'{adminaccess_cmd}',
  172. shell=True,
  173. stdout=_lf,
  174. stderr=_lf).returncode:
  175. logger.info('Provisioned Admin access to the VM.')
  176. else:
  177. logger.error('Failed to provision Admin access to the VM.')
  178. if ssvm_project and ssvm_owner:
  179. if not subprocess.run(
  180. f'{useraccess_cmd}',
  181. shell=True,
  182. stdout=_lf,
  183. stderr=_lf).returncode:
  184. logger.info('Provisioned SSH access to the VM for %s\\%s AD group '
  185. 'and sudo privileges for user %s', ad_domain,
  186. ssvm_project, ssvm_owner[:ssvm_owner.rfind("@")])
  187. else:
  188. logger.error('Failed to provision user access to the VM.')
  189. if not subprocess.run(
  190. 'systemctl restart sssd',
  191. shell=True,
  192. stdout=_lf,
  193. stderr=_lf).returncode:
  194. logger.info('Restarted the SSSD service.')
  195. else:
  196. logger.error('Failed to restart the SSSD service.')
  197. if not subprocess.run('systemctl restart ngt_guest_agent',
  198. shell=True,
  199. stdout=_lf,
  200. stderr=_lf).returncode:
  201. logger.info('Restarted the Nutanix Guest Agent service.')
  202. else:
  203. logger.error('Failed to restart the Nutanix Guest Agent service.')
  204. if script_os.upper() == 'WINDOWS':
  205. ad_join_cmd = (
  206. f'netdom join {script_host} '
  207. f'/domain:{ad_fqdn} '
  208. f'/ud:{ad_domain}\\{ad_bind_user} '
  209. f'/pd:"{ad_bind_password}"'
  210. )
  211. adminaccess_cmd = (
  212. f'net localgroup Administrators '
  213. f'{ad_domain}\\{windows_admins} /ADD'
  214. )
  215. if ssvm_project and ssvm_owner:
  216. useraccess_cmd = (
  217. f'net localgroup "Remote Desktop Users" '
  218. f'{ad_domain}\\{ssvm_project} /ADD && '
  219. f'net localgroup Administrators '
  220. f'{ad_domain}\\{ssvm_owner[:ssvm_owner.rfind("@")]} /ADD'
  221. )
  222. NGA_RESTART_CMD = (
  223. 'net stop "Nutanix Guest Agent" && '
  224. 'net start "Nutanix Guest Agent"'
  225. )
  226. with open(log_file, 'a') as _lf:
  227. if not subprocess.run(
  228. f'{ad_join_cmd}',
  229. shell=True,
  230. stdout=_lf,
  231. stderr=_lf).returncode:
  232. logger.info('Joined VM %s to the %s AD Domain',
  233. script_host, ad_domain)
  234. sleep(5)
  235. else:
  236. logger.error('Failed to join VM %s to the %s AD Domain',
  237. script_host, ad_domain)
  238. if not subprocess.run(
  239. f'{adminaccess_cmd}',
  240. shell=True,
  241. stdout=_lf,
  242. stderr=_lf).returncode:
  243. logger.info('Provisioned Admin access to the VM.')
  244. else:
  245. logger.error('Failed to provision Admin access to the VM.')
  246. if ssvm_project and ssvm_owner:
  247. if not subprocess.run(
  248. f'{useraccess_cmd}',
  249. shell=True,
  250. stdout=_lf,
  251. stderr=_lf).returncode:
  252. logger.info('Provisioned standard RDP access to the VM '
  253. 'for %s\\%s AD group and Administrator privileges for user %s',
  254. ad_domain, ssvm_project, ssvm_owner[:ssvm_owner.rfind("@")])
  255. else:
  256. logger.error('Failed to provision user access to the VM.')
  257. if not subprocess.run(
  258. f'{NGA_RESTART_CMD}',
  259. shell=True,
  260. stdout=_lf,
  261. stderr=_lf).returncode:
  262. logger.info('Restarted the Nutanix Guest Agent service.')
  263. else:
  264. logger.error('Failed to restart the Nutanix Guest Agent service.')
  265. if not subprocess.run('shutdown /r /t 5',
  266. shell=True,
  267. stdout=_lf,
  268. stderr=_lf).returncode:
  269. logger.info('Restarting host %s.', script_host)
  270. else:
  271. logger.error('Failed to trigger restart for %s.', script_host)
  272. except Exception:
  273. logger.exception('Encountered unhandled exception')
  274. finally:
  275. # Cleanup
  276. if Path(cred_file).exists():
  277. Path(cred_file).unlink()
  278. # Cloudbase-init exit for Windows to prevent re-execution upon boot.
  279. if script_os.upper() == 'WINDOWS':
  280. sys.exit(1001)