notify_dc_users.py 16 KB

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