notify_dc_users.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. #!/usr/bin/env python
  2. from __future__ import print_function
  3. import pickle
  4. import os.path
  5. import os
  6. from googleapiclient.discovery import build
  7. from elemental_utils import ElementalNetbox
  8. from pynetbox.models.ipam import IpAddresses
  9. import smtplib
  10. from email.message import EmailMessage
  11. import sys
  12. import re
  13. import subprocess
  14. import ipaddress
  15. import CLEUCreds # noqa
  16. from cleu.config import Config as C # noqa
  17. FROM = "Joe Clarke <jclarke@cisco.com>"
  18. CC = "Anthony Jesani <anjesani@cisco.com>, Jara Osterfeld <josterfe@cisco.com>"
  19. JUMP_HOSTS = ["10.100.252.26", "10.100.252.27", "10.100.252.28", "10.100.252.29"]
  20. DC_MAP = {"DC1": "dc1_datastore_1", "DC2": "dc2_datastore_1", "HyperFlex-DC1": "DC1-HX-DS-01", "HyperFlex-DC2": "DC2-HX-DS-01"}
  21. DEFAULT_CLUSTER = "FlexPod"
  22. HX_DCs = {"HyperFlex-DC1": 1, "HyperFlex-DC2": 1}
  23. IP4_SUBNET = "10.100."
  24. IP6_PREFIX = "2a11:d940:2:"
  25. NETWORK_MAP = {
  26. "Stretched_VMs": {
  27. "subnet": "{}252.0/24".format(IP4_SUBNET),
  28. "gw": "{}252.254".format(IP4_SUBNET),
  29. "prefix": "{}64fc::".format(IP6_PREFIX),
  30. "gw6": "{}64fc::fe".format(IP6_PREFIX),
  31. },
  32. "VMs-DC1": {
  33. "subnet": "{}253.0/24".format(IP4_SUBNET),
  34. "gw": "{}253.254".format(IP4_SUBNET),
  35. "prefix": "{}64fd::".format(IP6_PREFIX),
  36. "gw6": "{}64fd::fe".format(IP6_PREFIX),
  37. },
  38. "VMs-DC2": {
  39. "subnet": "{}254.0/24".format(IP4_SUBNET),
  40. "gw": "{}254.254".format(IP4_SUBNET),
  41. "prefix": "{}64fe::".format(IP6_PREFIX),
  42. "gw6": "{}64fe::fe".format(IP6_PREFIX),
  43. },
  44. }
  45. OSTYPE_LIST = [
  46. (r"(?i)ubuntu ?22.04", "ubuntu64Guest", "ubuntu22.04"),
  47. (r"(?i)ubuntu", "ubuntu64Guest", "linux"),
  48. (r"(?i)windows 10", "windows9_64Guest", "windows"),
  49. (r"(?i)windows 2012", "windows8Server64Guest", "windows"),
  50. (r"(?i)windows ?2019", "windows9Server64Guest", "windows2019"),
  51. (r"(?i)windows 201(6|9)", "windows9Server64Guest", "windows"),
  52. (r"(?i)debian 8", "debian8_64Guest", "linux"),
  53. (r"(?i)debian", "debian9_64Guest", "linux"),
  54. (r"(?i)centos 7", "centos7_64Guest", "linux"),
  55. (r"(?i)centos", "centos8_64Guest", "linux"),
  56. (r"(?i)red hat", "rhel7_64Guest", "linux"),
  57. (r"(?i)linux", "other3xLinux64Guest", "linux"),
  58. (r"(?i)freebsd ?13.1", "freebsd12_64Guest", "freebsd13.1"),
  59. (r"(?i)freebsd", "freebsd12_64Guest", "other"),
  60. ]
  61. INTERFACE_MAP = {
  62. "freebsd13.1": "vmx0",
  63. "linux": "eth0",
  64. "ubuntu22.04": "eth0",
  65. "windows": "Ethernet 1",
  66. "windows2019": "Ethernet 1",
  67. "other": "Ethernet 1",
  68. }
  69. DNS1 = "10.100.253.6"
  70. DNS2 = "10.100.254.6"
  71. NTP1 = "10.128.0.1"
  72. NTP2 = "10.128.0.2"
  73. VCENTER = "https://" + C.VCENTER
  74. DOMAIN = C.DNS_DOMAIN
  75. AD_DOMAIN = C.AD_DOMAIN
  76. SMTP_SERVER = C.SMTP_SERVER
  77. SYSLOG = SMTP_SERVER
  78. ISO_DS = "dc1_datastore_1"
  79. ISO_DS_HX1 = "DC1-HX-DS-01"
  80. ISO_DS_HX2 = "DC2-HX-DS-01"
  81. VPN_SERVER_IP = C.VPN_SERVER_IP
  82. ANSIBLE_PATH = "/home/jclarke/src/git/ciscolive/automation/cleu-ansible-n9k"
  83. DATACENTER = "CiscoLive"
  84. CISCOLIVE_YEAR = C.CISCOLIVE_YEAR
  85. PW_RESET_URL = C.PW_RESET_URL
  86. TENANT_NAME = "Infrastructure"
  87. VRF_NAME = "default"
  88. SPREADSHEET_ID = "15sC26okPX1lHzMFDJFnoujDKLNclh4NQhBPmV175slY"
  89. SHEET_HOSTNAME = 0
  90. SHEET_OS = 1
  91. SHEET_OVA = 2
  92. SHEET_CONTACT = 5
  93. SHEET_CPU = 6
  94. SHEET_RAM = 7
  95. SHEET_DISK = 8
  96. SHEET_NICS = 9
  97. SHEET_CLUSTER = 12
  98. SHEET_DC = 13
  99. SHEET_VLAN = 14
  100. FIRST_IP = 30
  101. def get_next_ip(enb: ElementalNetbox, prefix: str) -> IpAddresses:
  102. """
  103. Get the next available IP for a prefix.
  104. """
  105. global FIRST_IP, TENANT_NAME, VRF_NAME
  106. prefix_obj = enb.ipam.prefixes.get(prefix=prefix)
  107. available_ips = prefix_obj.available_ips.list()
  108. for addr in available_ips:
  109. ip_obj = ipaddress.ip_address(addr.address.split("/")[0])
  110. if int(ip_obj.packed[-1]) > FIRST_IP:
  111. tenant = enb.tenancy.tenants.get(name=TENANT_NAME)
  112. vrf = enb.ipam.vrfs.get(name=VRF_NAME)
  113. return enb.ipam.ip_addresses.create(address=addr.address, tenant=tenant.id, vrf=vrf.id)
  114. return None
  115. def main():
  116. if len(sys.argv) != 2:
  117. print(f"usage: {sys.argv[0]} ROW_RANGE")
  118. sys.exit(1)
  119. if not os.path.exists("gs_token.pickle"):
  120. print("ERROR: Google Sheets token does not exist! Please re-auth the app first.")
  121. sys.exit(1)
  122. creds = None
  123. with open("gs_token.pickle", "rb") as token:
  124. creds = pickle.load(token)
  125. if "VMWARE_USER" not in os.environ or "VMWARE_PASSWORD" not in os.environ:
  126. print("ERROR: VMWARE_USER and VMWARE_PASSWORD environment variables must be set prior to running!")
  127. sys.exit(1)
  128. gs_service = build("sheets", "v4", credentials=creds)
  129. vm_sheet = gs_service.spreadsheets()
  130. vm_result = vm_sheet.values().get(spreadsheetId=SPREADSHEET_ID, range=sys.argv[1]).execute()
  131. vm_values = vm_result.get("values", [])
  132. if not vm_values:
  133. print("ERROR: Did not read anything from Google Sheets!")
  134. sys.exit(1)
  135. enb = ElementalNetbox()
  136. (rstart, _) = sys.argv[1].split(":")
  137. i = int(rstart) - 1
  138. users = {}
  139. for row in vm_values:
  140. i += 1
  141. try:
  142. owners = row[SHEET_CONTACT].strip().split(",")
  143. name = row[SHEET_HOSTNAME].strip()
  144. opsys = row[SHEET_OS].strip()
  145. is_ova = row[SHEET_OVA].strip()
  146. cpu = int(row[SHEET_CPU].strip())
  147. mem = int(row[SHEET_RAM].strip()) * 1024
  148. disk = int(row[SHEET_DISK].strip())
  149. dc = row[SHEET_DC].strip()
  150. cluster = row[SHEET_CLUSTER].strip()
  151. vlan = row[SHEET_VLAN].strip()
  152. except Exception as e:
  153. print(f"WARNING: Failed to process malformed row {i}: {e}")
  154. continue
  155. if name == "" or vlan == "" or dc == "":
  156. print(f"WARNING: Ignoring malformed row {i}")
  157. continue
  158. ova_bool = False
  159. if is_ova.lower() == "true" or is_ova.lower() == "yes":
  160. ova_bool = True
  161. ostype = None
  162. platform = "other"
  163. for ostypes in OSTYPE_LIST:
  164. if re.search(ostypes[0], opsys):
  165. ostype = ostypes[1]
  166. platform = ostypes[2]
  167. break
  168. if not ova_bool and ostype is None:
  169. print(f"WARNING: Did not find OS type for {vm['os']} on row {i}")
  170. continue
  171. vm = {
  172. "name": name.upper(),
  173. "os": opsys,
  174. "ostype": ostype,
  175. "platform": platform,
  176. "mem": mem,
  177. "is_ova": ova_bool,
  178. "cpu": cpu,
  179. "disk": disk,
  180. "vlan": vlan,
  181. "cluster": cluster,
  182. "dc": dc,
  183. }
  184. ip_obj = get_next_ip(enb, NETWORK_MAP[vm["vlan"]]["subnet"])
  185. if not ip_obj:
  186. print(f"WARNING: No free IP addresses for {name} in subnet {NETWORK_MAP[vm['vlan']]}.")
  187. continue
  188. vm["ip"] = ip_obj.address.split("/")[0]
  189. vm_obj = enb.virtualization.virtual_machines.filter(name=name.lower())
  190. if vm_obj and len(vm_obj) > 0:
  191. print(f"WARNING: Duplicate VM name {name} in NetBox for row {i}.")
  192. continue
  193. platform_obj = enb.dcim.platforms.get(name=vm["platform"])
  194. cluster_obj = enb.virtualization.clusters.get(name=vm["cluster"])
  195. vm_obj = enb.virtualization.virtual_machines.create(
  196. name=name.lower(), platform=platform_obj.id, vcpus=vm["cpu"], disk=vm["disk"], memory=vm["mem"], cluster=cluster_obj.id
  197. )
  198. vm["vm_obj"] = vm_obj
  199. vm_intf = enb.virtualization.interfaces.create(virtual_machine=vm_obj.id, name=INTERFACE_MAP[vm["platform"]])
  200. ip_obj.assigned_object_id = vm_intf.id
  201. ip_obj.assigned_object_type = "virtualization.vminterface"
  202. ip_obj.save()
  203. vm_obj.primary_ip4 = ip_obj.id
  204. contacts = []
  205. for owner in owners:
  206. owner = owner.strip()
  207. if owner not in users:
  208. users[owner] = []
  209. users[owner].append(vm)
  210. contacts.append(owner)
  211. vm_obj.custom_fields["Contact"] = ",".join(contacts)
  212. vm_obj.save()
  213. for user, vms in users.items():
  214. m = re.search(r"<?(\S+)@", user)
  215. username = m.group(1)
  216. body = "Please find the CLEU Data Centre Access details below\r\n\r\n"
  217. body += f"Before you can access the Data Centre from remote, AnyConnect to {VPN_SERVER_IP} and login with {CLEUCreds.VPN_USER} / {CLEUCreds.VPN_PASS}\r\n"
  218. body += f"Once connected, your browser should redirect you to the password change tool. If not go to {PW_RESET_URL} and login with {username} and password {CLEUCreds.DEFAULT_USER_PASSWORD}\r\n"
  219. body += "Reset your password. You must use a complex password that contains lower and uppercase letters, numbers, or a special character.\r\n"
  220. body += f"After resetting your password, drop the VPN and reconnect to {VPN_SERVER_IP} with {username} and the new password you just set.\r\n\r\n"
  221. body += "You can use any of the following Windows Jump Hosts to access the data centre using RDP:\r\n\r\n"
  222. for js in JUMP_HOSTS:
  223. body += f"{js}\r\n"
  224. body += "\r\nIf a Jump Host is full, try the next one.\r\n\r\n"
  225. body += f"Your login is {username} (or {username}@{AD_DOMAIN} on Windows). Your password is the same you used for the VPN.\r\n\r\n"
  226. body += "The network details for your VM(s) are:\r\n\r\n"
  227. body += f"DNS1 : {DNS1}\r\n"
  228. body += f"DNS2 : {DNS2}\r\n"
  229. body += f"NTP1 : {NTP1}\r\n"
  230. body += f"NTP2 : {NTP2}\r\n"
  231. body += f"DNS DOMAIN : {DOMAIN}\r\n"
  232. body += f"SMTP : {SMTP_SERVER}\r\n"
  233. body += f"AD DOMAIN : {AD_DOMAIN}\r\n"
  234. body += f"Syslog/NetFlow: {SYSLOG}\r\n\r\n"
  235. body += f"vCenter is {VCENTER}. You MUST use the web client. Your AD credentials above will work there. VMs that don't require an OVA have been pre-created, but require installation and configuration. If you use an OVA, you will need to deploy it yourself.\r\n\r\n"
  236. body += "Your VM details are as follows. DNS records have been pre-created for the VM name (i.e., hostname) below:\r\n\r\n"
  237. created = {}
  238. for vm in vms:
  239. iso_ds = ISO_DS
  240. cluster = DEFAULT_CLUSTER
  241. if vm["dc"] in HX_DCs:
  242. if vm["dc"].endswith("2"):
  243. iso_ds = ISO_DS_HX2
  244. else:
  245. iso_ds = ISO_DS_HX1
  246. cluster = vm["dc"]
  247. if not vm["is_ova"] and vm["vlan"] != "" and vm["name"] not in created:
  248. print(f"===Adding VM for {vm['name']}===")
  249. scsi = "lsilogic"
  250. if re.search(r"^win", vm["ostype"]):
  251. scsi = "lsilogicsas"
  252. os.chdir(ANSIBLE_PATH)
  253. command = [
  254. "ansible-playbook",
  255. "-i",
  256. "inventory/hosts",
  257. "-e",
  258. f"vmware_cluster='{cluster}'",
  259. "-e",
  260. f"vmware_datacenter='{DATACENTER}'",
  261. "-e",
  262. f"guest_id={vm['ostype']}",
  263. "-e",
  264. f"guest_name={vm['name']}",
  265. "-e",
  266. f"guest_size={vm['disk']}",
  267. "-e",
  268. f"guest_mem={vm['mem']}",
  269. "-e",
  270. f"guest_cpu={vm['cpu']}",
  271. "-e",
  272. f"guest_datastore={DC_MAP[vm['dc']]}",
  273. "-e",
  274. f"guest_network='{vm['vlan']}'",
  275. "-e",
  276. f"guest_scsi={scsi}",
  277. "-e",
  278. f"ansible_python_interpreter={sys.executable}",
  279. "add-vm-playbook.yml",
  280. ]
  281. p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
  282. output = ""
  283. for c in iter(lambda: p.stdout.read(1), b""):
  284. output += c.decode("utf-8")
  285. p.wait()
  286. rc = p.returncode
  287. if rc != 0:
  288. print(f"\n\n***ERROR: Failed to add VM {vm['name']}\n{output}!")
  289. vm["vm_obj"].delete()
  290. continue
  291. print("===DONE===")
  292. octets = vm["ip"].split(".")
  293. body += '{} : {} (v6: {}{}) (Network: {}, Subnet: {}, GW: {}, v6 Prefix: {}/64, v6 GW: {}) : Deploy to the {} datastore in the "{}" cluster.\r\n\r\nFor this VM upload ISOs to the {} datastore. There is an "ISOs" folder there already.\r\n\r\n'.format(
  294. vm["name"],
  295. vm["ip"],
  296. NETWORK_MAP[vm["vlan"]]["prefix"],
  297. format(int(octets[3]), "x"),
  298. vm["vlan"],
  299. NETWORK_MAP[vm["vlan"]]["subnet"],
  300. NETWORK_MAP[vm["vlan"]]["gw"],
  301. NETWORK_MAP[vm["vlan"]]["prefix"],
  302. NETWORK_MAP[vm["vlan"]]["gw6"],
  303. DC_MAP[vm["dc"]],
  304. cluster,
  305. iso_ds,
  306. )
  307. created[vm["name"]] = True
  308. body += "Let us know via Webex if you need any other details.\r\n\r\n"
  309. body += "Joe, Anthony, and Jara\r\n\r\n"
  310. subject = f"Cisco Live Europe {CISCOLIVE_YEAR} Data Centre Access Info"
  311. smtp = smtplib.SMTP(SMTP_SERVER)
  312. msg = EmailMessage()
  313. msg.set_content(body)
  314. msg["Subject"] = subject
  315. msg["From"] = FROM
  316. msg["To"] = user
  317. msg["Cc"] = CC + "," + FROM
  318. smtp.send_message(msg)
  319. smtp.quit()
  320. if __name__ == "__main__":
  321. os.environ["NETBOX_ADDRESS"] = C.NETBOX_SERVER
  322. os.environ["NETBOX_API_TOKEN"] = CLEUCreds.NETBOX_API_TOKEN
  323. os.environ["CPNR_USERNAME"] = CLEUCreds.CPNR_USERNAME
  324. os.environ["CPNR_PASSWORD"] = CLEUCreds.CPNR_PASSWORD
  325. os.environ["VMWARE_USER"] = CLEUCreds.VMWARE_USER
  326. os.environ["VMWARE_PASSWORD"] = CLEUCreds.VMWARE_PASSWORD
  327. main()