add_vlan.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. #!/usr/bin/env python3
  2. import argparse
  3. import sys
  4. import re
  5. import subprocess
  6. import os
  7. import tempfile
  8. from yaml import load, dump
  9. try:
  10. from yaml import CLoader as Loader, CDumper as Dumper
  11. except ImportError:
  12. from yaml import Loader, Dumper
  13. IPV4SEG = r"(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])"
  14. IPV4ADDR = r"(?:(?:" + IPV4SEG + r"\.){3,3}" + IPV4SEG + r")"
  15. IPV6SEG = r"(?:(?:[0-9a-fA-F]){1,4})"
  16. IPV6GROUPS = (
  17. r"(?:" + IPV6SEG + r":){7,7}" + IPV6SEG, # 1:2:3:4:5:6:7:8
  18. # 1:: 1:2:3:4:5:6:7::
  19. r"(?:" + IPV6SEG + r":){1,7}:",
  20. # 1::8 1:2:3:4:5:6::8 1:2:3:4:5:6::8
  21. r"(?:" + IPV6SEG + r":){1,6}:" + IPV6SEG,
  22. # 1::7:8 1:2:3:4:5::7:8 1:2:3:4:5::8
  23. r"(?:" + IPV6SEG + r":){1,5}(?::" + IPV6SEG + r"){1,2}",
  24. # 1::6:7:8 1:2:3:4::6:7:8 1:2:3:4::8
  25. r"(?:" + IPV6SEG + r":){1,4}(?::" + IPV6SEG + r"){1,3}",
  26. # 1::5:6:7:8 1:2:3::5:6:7:8 1:2:3::8
  27. r"(?:" + IPV6SEG + r":){1,3}(?::" + IPV6SEG + r"){1,4}",
  28. # 1::4:5:6:7:8 1:2::4:5:6:7:8 1:2::8
  29. r"(?:" + IPV6SEG + r":){1,2}(?::" + IPV6SEG + r"){1,5}",
  30. # 1::3:4:5:6:7:8 1::3:4:5:6:7:8 1::8
  31. IPV6SEG + r":(?:(?::" + IPV6SEG + r"){1,6})",
  32. # ::2:3:4:5:6:7:8 ::2:3:4:5:6:7:8 ::8 ::
  33. r":(?:(?::" + IPV6SEG + r"){1,7}|:)",
  34. # fe80::7:8%eth0 fe80::7:8%1 (link-local IPv6 addresses with zone index)
  35. r"fe80:(?::" + IPV6SEG + r"){0,4}%[0-9a-zA-Z]{1,}",
  36. # ::255.255.255.255 ::ffff:255.255.255.255 ::ffff:0:255.255.255.255 (IPv4-mapped IPv6 addresses and IPv4-translated addresses)
  37. r"::(?:ffff(?::0{1,4}){0,1}:){0,1}[^\s:]" + IPV4ADDR,
  38. # 2001:db8:3:4::192.0.2.33 64:ff9b::192.0.2.33 (IPv4-Embedded IPv6 Address)
  39. r"(?:" + IPV6SEG + r":){1,4}:[^\s:]" + IPV4ADDR,
  40. )
  41. # Reverse rows for greedy match
  42. IPV6ADDR = "|".join(["(?:{})".format(g) for g in IPV6GROUPS[::-1]])
  43. def main():
  44. parser = argparse.ArgumentParser(prog=sys.argv[0], description="Add a VLAN to the network")
  45. parser.add_argument(
  46. "--vlan-name", "-n", metavar="<VLAN_NAME>", help="Name of the VLAN to add", required=True,
  47. )
  48. parser.add_argument(
  49. "--vlan-id", "-i", metavar="<VLAN_ID>", help="ID of the VLAN to add", type=int, required=True,
  50. )
  51. parser.add_argument(
  52. "--vm-vlan-name", metavar="<VM_VLAN_NAME>", help="Name of the VLAN port group in VMware (required when adding to vCenter)",
  53. )
  54. parser.add_argument(
  55. "--svi-v4-network", metavar="<SVI_NETWORK>", help="IPv4 network address of the SVI",
  56. )
  57. parser.add_argument(
  58. "--svi-subnet-len", metavar="<SVI_PREFIX_LEN>", help="Subnet length of the SVI v4 IP (e.g., 24 for a /24)", type=int,
  59. )
  60. parser.add_argument(
  61. "--svi-standard-v4", help="Follow the standard rules to add a MAJOR.VLAN.IDF.0/24 SVI address", action="store_true",
  62. )
  63. parser.add_argument(
  64. "--svi-v6-network",
  65. metavar="<SVI_NETWORK>",
  66. help='IPv6 network address of the SVI (should end with "::"; prefix len is assumed to be /64)',
  67. )
  68. parser.add_argument(
  69. "--svi-standard-v6", help="Follow the standard rules to add a PREFIX:[VLAN][IDF]::/64 SVI address", action="store_true",
  70. )
  71. parser.add_argument("--svi-descr", metavar="<SVI_DESCRIPTION>", help="Description of the SVI")
  72. parser.add_argument(
  73. "--no-add-acl", help="Do not add the standard ACL restrictions to the SVI (default: add the ACLs)", action="store_true",
  74. )
  75. parser.add_argument("--mtu", "-m", metavar="<MTU>", help="MTU of SVI (default: 9216)", type=int)
  76. parser.add_argument(
  77. "--is-stretched", help="VLAN is stretched between both data centres (default: False)", action="store_true",
  78. )
  79. parser.add_argument(
  80. "--no-hsrp", help="Use HSRP or not (default: HSRP will be configured)", action="store_true",
  81. )
  82. parser.add_argument(
  83. "--no-passive-interface",
  84. help="Whether or not to have OSPF use passive interface (default: SVI will be a passive interface)",
  85. action="store_true",
  86. )
  87. parser.add_argument(
  88. "--v6-link-local", help="Only use v6 link-local addresses (default: global IPv6 is expected)", action="store_true",
  89. )
  90. parser.add_argument(
  91. "--ospf-broadcast", help="OSPF network is broadcast instead of P2P (default: P2P)", action="store_true",
  92. )
  93. parser.add_argument(
  94. "--interface", action="append", metavar="<INTF>", help="Interface to enable for VLAN (can be specified more than once)",
  95. )
  96. parser.add_argument(
  97. "--generate-iflist", help="Automatically generate a list of allowed interfaces for VLAN (default: False)", action="store_true",
  98. )
  99. parser.add_argument(
  100. "--vmware-cluster",
  101. action="append",
  102. metavar="<CLUSTER>",
  103. help="VMware cluster to configure for VLAN (can be specified more than once) (default: all clusters are configured)",
  104. )
  105. parser.add_argument(
  106. "--username", "-u", metavar="<USERNAME>", help="Username to use to connect to the N9Ks", required=True,
  107. )
  108. parser.add_argument(
  109. "--limit",
  110. "-L",
  111. metavar="<HOSTS_OR_GROUP_NAMES>",
  112. help="Comma-separated list of hosts or host group names (from inventory/hosts) on which to restrict operations",
  113. )
  114. parser.add_argument(
  115. "--tags", metavar="<TAG_LIST>", help="Comma-separated list of task tags to execute",
  116. )
  117. parser.add_argument("--list-tags", help="List available task tags", action="store_true")
  118. parser.add_argument(
  119. "--test-only", help="Only check syntax and attempt to predict changes (NO CHANGES WILL BE MADE)", action="store_true",
  120. )
  121. args = parser.parse_args()
  122. if args.vlan_id < 1 or args.vlan_id > 3967:
  123. print("ERROR: VLAN ID must be between 1 and 3967")
  124. sys.exit(1)
  125. svi_prefix = None
  126. build_v4 = False
  127. use_hsrp = True
  128. passive_interface = True
  129. svi_v6_link_local = False
  130. build_v6 = True
  131. ospf_type = "point-to-point"
  132. is_stretched = False
  133. generate_iflist = False
  134. add_acl = True
  135. if args.svi_v4_network and args.svi_standard_v4:
  136. print("ERROR: Cannot specify both --svi-v4-network and --svi-standard-v4.")
  137. sys.exit(1)
  138. if args.svi_standard_v4:
  139. build_v4 = True
  140. if args.is_stretched:
  141. is_stretched = True
  142. if args.generate_iflist and args.interface and len(args.interface) > 0:
  143. print("ERROR: Cannot specify both an interface list and --generate-iflist.")
  144. sys.exit(1)
  145. if args.generate_iflist:
  146. generate_iflist = True
  147. if args.svi_v4_network:
  148. m = re.match(r"(\d+)\.(\d+)\.(\d+).(\d+)", args.svi_v4_network)
  149. if not m:
  150. print("ERROR: SVI Network must be an IPv4 network address.")
  151. sys.exit(1)
  152. for i in range(1, 5):
  153. if int(m.group(i)) > 255:
  154. print("ERROR: Invalid SVI IPv4 address, {}".format(args.svi_v4_network))
  155. sys.exit(1)
  156. if not args.svi_subnet_len:
  157. print("ERROR: SVI Prefix Length is required when an SVI Network is specified.")
  158. sys.exit(1)
  159. if int(args.svi_subnet_len) < 8 or int(args.svi_subnet_len) > 30:
  160. print("ERROR: SVI Prefix Length must be between 8 and 30.")
  161. sys.exit(1)
  162. if args.svi_subnet_len >= 24:
  163. svi_prefix = "{}.{}.{}".format(m.group(1), m.group(2), m.group(3))
  164. elif args.svi_prefix_len < 24 and args.svi_subnet_len >= 16:
  165. svi_prefix = "{}.{}".format(m.group(1), m.group(2))
  166. else:
  167. svi_prefix = m.group(1)
  168. if args.svi_v4_network or args.svi_v6_network or args.svi_standard_v4 or args.svi_standard_v6:
  169. if args.mtu and (args.mtu < 1500 or args.mtu > 9216):
  170. print("ERROR: MTU must be between 1500 and 9216.")
  171. sys.exit(1)
  172. elif not args.mtu:
  173. args.mtu = 9216
  174. if args.no_passive_interface:
  175. passive_interface = False
  176. if args.no_hsrp:
  177. use_hsrp = False
  178. if args.ospf_broadcast:
  179. ospf_type = "broadcast"
  180. if args.svi_standard_v6 and args.svi_v6_network:
  181. print("ERROR: Cannot specify both --svi-v6-network and --svi-standard-v6.")
  182. sys.exit(1)
  183. if args.svi_standard_v6:
  184. build_v6 = True
  185. if args.svi_v6_network:
  186. m = re.match(IPV6ADDR, args.svi_v6_network)
  187. if not m:
  188. print("ERROR: SVI Network must be an IPv6 network address.")
  189. sys.exit(1)
  190. if args.v6_link_local:
  191. print("ERROR: Cannot specify both svi-v6-network and v6-link-local.")
  192. sys.exit(1)
  193. elif args.v6_link_local:
  194. svi_v6_link_local = True
  195. if args.no_add_acl:
  196. add_acl = False
  197. os.environ["ANSIBLE_FORCE_COLOR"] = "True"
  198. os.environ["ANSIBLE_HOST_KEY_CHECKING"] = "False"
  199. os.environ["ANSIBLE_PERSISTENT_COMMAND_TIMEOUT"] = "300"
  200. os.environ["ANSIBLE_DEPRECATION_WARNINGS"] = "False"
  201. if "AD_PASSWORD" not in os.environ:
  202. print("ERROR: AD_PASSWORD must be set in the environment first (used for vCenter and UCS).")
  203. sys.exit(1)
  204. os.environ["VMWARE_USER"] = args.username
  205. os.environ["VMWARE_PASSWORD"] = os.environ["AD_PASSWORD"]
  206. cred_file = tempfile.NamedTemporaryFile(mode="w", delete=False)
  207. vars = {
  208. "ucs_mgr_username": args.username,
  209. "ucs_mgr_password": os.environ["AD_PASSWORD"],
  210. }
  211. dump(vars, cred_file, Dumper=Dumper)
  212. cred_file.close()
  213. command = [
  214. "ansible-playbook",
  215. "-i",
  216. "inventory/hosts",
  217. "-u",
  218. args.username,
  219. "-k",
  220. "-e",
  221. "vlan_name={}".format(args.vlan_name),
  222. "-e",
  223. "vlan_id={}".format(args.vlan_id),
  224. "-e",
  225. "ansible_python_interpreter={}".format(sys.executable),
  226. "-e",
  227. "@{}".format(cred_file.name),
  228. "-e",
  229. "build_v4={}".format(build_v4),
  230. "-e",
  231. "build_v6={}".format(build_v6),
  232. "-e",
  233. "is_stretched={}".format(is_stretched),
  234. "-e",
  235. "generate_iflist={}".format(generate_iflist),
  236. "-e",
  237. "ospf_type={}".format(ospf_type),
  238. "-e",
  239. "add_acl={}".format(add_acl),
  240. "add-vlan-playbook.yml",
  241. ]
  242. if args.vm_vlan_name:
  243. command += ["-e", "vm_vlan_name='{}'".format(args.vm_vlan_name)]
  244. if args.svi_v4_network:
  245. command += [
  246. "-e",
  247. "svi_v4_prefix={}".format(svi_prefix),
  248. "-e",
  249. "svi_subnet_len={}".format(args.svi_subnet_len),
  250. "-e",
  251. "svi_v4_network={}".format(args.svi_v4_network),
  252. ]
  253. if args.svi_v6_network:
  254. command += ["-e", "svi_v6_network={}".format(args.svi_v6_network)]
  255. if args.mtu:
  256. command += ["-e", "svi_mtu={}".format(args.mtu)]
  257. if args.svi_descr:
  258. command += ["-e", "svi_descr='{}'".format(args.svi_descr)]
  259. if use_hsrp:
  260. command += ["-e", "use_hsrp={}".format(use_hsrp)]
  261. if passive_interface:
  262. command += ["-e", "passive_interface={}".format(passive_interface)]
  263. if svi_v6_link_local:
  264. command += ["-e", "svi_v6_link_local={}".format(svi_v6_link_local)]
  265. if args.interface and len(args.interface) > 0:
  266. command += ["-e", '{{"iflist": [{}]}}'.format(",".join(args.interface))]
  267. if args.vmware_cluster and len(args.vmware_cluster) > 0:
  268. command += [
  269. "-e",
  270. '{{"vm_clusters": [{}]}}'.format(",".join(args.vmware_cluster)),
  271. ]
  272. if args.limit:
  273. command += ["--limit", args.limit]
  274. if args.tags:
  275. command += ["--tags", args.tags]
  276. if args.list_tags:
  277. command += ["--list-tags"]
  278. if args.test_only:
  279. command += ["-C"]
  280. p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
  281. for c in iter(lambda: p.stdout.read(1), b""):
  282. sys.stdout.write(c.decode("utf-8"))
  283. sys.stdout.flush()
  284. p.poll()
  285. if os.path.isfile(cred_file.name):
  286. os.remove(cred_file.name)
  287. if __name__ == "__main__":
  288. main()