notify_dc_users.py 14 KB

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