Automatic IOS Base Configuration for GNS3
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ios_base_config.py 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608
  1. #!/usr/bin/python3
  2. # -*- coding: utf-8 -*-
  3. #
  4. # Copyright (C) 2018-2019 Bernhard Ehlers
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. """
  19. Base interface and loopback configuration for Cisco router
  20. """
  21. import os
  22. import sys
  23. import ipaddress
  24. import re
  25. import telnetlib
  26. import uuid
  27. import xml.etree.ElementTree as ET
  28. import gns3api
  29. def die(*msg_list):
  30. """ abort program with error message """
  31. error_msg = ' '.join(str(x) for x in msg_list)
  32. sys.exit(error_msg.rstrip("\n\r"))
  33. def next_ip_address(ip_intf):
  34. """" next IP address """
  35. return ipaddress.ip_interface(str(ip_intf.ip + 1) +
  36. "/" + str(ip_intf.network.prefixlen))
  37. def next_ip_network(ip_intf):
  38. """" next IP network """
  39. return ipaddress.ip_interface(
  40. str(ip_intf.ip + ip_intf.network.num_addresses) +
  41. "/" + str(ip_intf.network.prefixlen))
  42. def send_cisco_commands(name, host, port, commands, privileged=True):
  43. """ send config to Cisco router/switch """
  44. if not commands:
  45. return True
  46. prompt = b"[>#] ?$"
  47. status = "???"
  48. try:
  49. # open telnet connection
  50. status = "connect"
  51. tn = telnetlib.Telnet(host, port, 10)
  52. # read old junk
  53. while tn.read_until(b"xyzzy", timeout=0.3):
  54. pass
  55. # send a <CR>
  56. status = "first contact"
  57. tn.write(b"\r")
  58. try:
  59. response = tn.expect([prompt], 5)[2]
  60. except OSError:
  61. pass
  62. tn.write(b"\r") # second <return>
  63. response = tn.expect([prompt], 5)[2]
  64. if privileged and response.endswith(b">"):
  65. sys.stderr.write("{}: Router must be in priviledged mode.\n".format(name))
  66. return False
  67. # commands
  68. status = "sending commands"
  69. for line in commands:
  70. while tn.read_until(b"xyzzy", timeout=0.1):
  71. pass
  72. tn.write((line + "\r").encode('utf-8', errors='replace'))
  73. # wait for prompt
  74. tn.expect([prompt])
  75. # close connection
  76. status = "close"
  77. tn.close()
  78. except OSError as err:
  79. sys.stderr.write("{}: I/O error during {} - {}\n".format(name, status, err))
  80. return False
  81. except KeyboardInterrupt:
  82. sys.stderr.write("{}: Aborted\n".format(name))
  83. return False
  84. return True # No error
  85. def get_project_data(api, project_id, sel_items):
  86. """ get node (with link information) and notes of a project by GNS3 API """
  87. # get all node and link information
  88. all_nodes = {}
  89. all_links = {}
  90. notes = []
  91. try:
  92. # check project status
  93. project = api.request('GET', ('/v2/projects', project_id))
  94. if project['status'] != 'opened':
  95. die("Project '{}' is {}, please open it.".format(
  96. project['name'], project['status']))
  97. compute_host = {}
  98. for compute in api.request('GET', '/v2/computes'):
  99. compute_host[compute["compute_id"]] = compute["host"]
  100. for node in api.request('GET', ('/v2/projects', project_id, 'nodes')):
  101. console_host = node.get("console_host")
  102. if console_host in ('0.0.0.0', '::'):
  103. node["console_host"] = compute_host[node["compute_id"]]
  104. all_nodes[node["node_id"]] = node
  105. for link in api.request('GET', ('/v2/projects', project_id, 'links')):
  106. all_links[link["link_id"]] = link
  107. if sel_items: # get selected notes
  108. for item in sel_items:
  109. if item.startswith("text_drawings/"):
  110. drawing = api.request('GET', \
  111. ('/v2/projects', project_id, "drawings", item[14:]))
  112. svg = ET.fromstring(drawing["svg"])
  113. if svg[0].tag == 'text':
  114. notes.append(svg[0].text)
  115. else: # nothing selected: get all of them
  116. for drawing in api.request('GET', ('/v2/projects', project_id, "drawings")):
  117. svg = ET.fromstring(drawing["svg"])
  118. if svg[0].tag == 'text':
  119. notes.append(svg[0].text)
  120. except gns3api.GNS3ApiException as err:
  121. die("Can't get node/link information:", err)
  122. nodes = select_nodes(all_nodes, all_links, sel_items)
  123. return nodes, notes
  124. def select_nodes(all_nodes, all_links, sel_items):
  125. """ select nodes and add link information """
  126. nodes = {}
  127. try:
  128. if sel_items: # get selected nodes
  129. for item in sel_items:
  130. if item.startswith("nodes/"):
  131. item = item[6:]
  132. nodes[all_nodes[item]['name']] = all_nodes[item]
  133. nodes[all_nodes[item]['name']]['_links'] = []
  134. else: # nothing selected: get all nodes
  135. for item in all_nodes:
  136. nodes[all_nodes[item]['name']] = all_nodes[item]
  137. nodes[all_nodes[item]['name']]['_links'] = []
  138. # add link informations to nodes
  139. for link in all_links:
  140. link_nodes = all_links[link]['nodes']
  141. node_0_id = link_nodes[0]['node_id']
  142. node_1_id = link_nodes[1]['node_id']
  143. node_0_name = all_nodes[node_0_id]['name']
  144. node_1_name = all_nodes[node_1_id]['name']
  145. label_0 = link_nodes[0].get("label", {}).get("text")
  146. label_1 = link_nodes[1].get("label", {}).get("text")
  147. if node_0_name in nodes:
  148. nodes[node_0_name]['_links'].append(
  149. {'link_id': link, 'link_type': all_links[link]['link_type'],
  150. 'adapter_number': link_nodes[0].get('adapter_number'),
  151. 'port_number': link_nodes[0].get('port_number'),
  152. 'label': label_0,
  153. 'remote_name': node_1_name, 'remote_label': label_1})
  154. if node_1_name in nodes:
  155. nodes[node_1_name]['_links'].append(
  156. {'link_id': link, 'link_type': all_links[link]['link_type'],
  157. 'adapter_number': link_nodes[1].get('adapter_number'),
  158. 'port_number': link_nodes[1].get('port_number'),
  159. 'label': label_1,
  160. 'remote_name': node_0_name, 'remote_label': label_0})
  161. except KeyError:
  162. die("Project informations are inconsistent")
  163. return nodes
  164. def get_vlan_interfaces(nodes):
  165. """ get vlan interfaces in switch groups """
  166. vlan_interfaces = {}
  167. for name in nodes:
  168. is_switch = False
  169. switch_group = {} # new switch group
  170. switch_list = [name]
  171. while switch_list: # process a group of switches
  172. name = switch_list.pop()
  173. if name not in nodes: # node not selected
  174. continue
  175. if name in vlan_interfaces: # already processed
  176. continue
  177. for link in nodes[name]['_links']:
  178. if not link['label']:
  179. pass
  180. elif re.search(r'\btrunk\b', link['label'], re.IGNORECASE):
  181. is_switch = True
  182. switch_list.append(link['remote_name'])
  183. else:
  184. match = re.search(r'\bvlan *(\d+)\b', link['label'], re.IGNORECASE)
  185. if match: # add vlan / link to vlan_interfaces
  186. is_switch = True
  187. vlan = int(match.group(1))
  188. switch_group.setdefault(vlan, [])
  189. switch_group[vlan].append(
  190. [name, link['label'],
  191. link['remote_name'], link['remote_label'],
  192. link['link_id']])
  193. if is_switch:
  194. vlan_interfaces[name] = switch_group
  195. for vlan in switch_group:
  196. switch_group[vlan].sort(key=lambda k: [k[0].lower(), k[1].lower()])
  197. for name in vlan_interfaces:
  198. nodes[name]['_vlans'] = sorted(vlan_interfaces[name].keys())
  199. return vlan_interfaces
  200. def base_networks(notes):
  201. """ get base IP networks for loopbacks and infrastructure interfaces """
  202. loopback_base = None
  203. infra_base = None
  204. for note in notes:
  205. # loopback address
  206. match = re.search(r'^ *loopback: *(\S+)', note,
  207. flags=re.IGNORECASE|re.MULTILINE)
  208. if match:
  209. if loopback_base is None:
  210. try:
  211. loopback_base = ipaddress.ip_interface(match.group(1))
  212. except ValueError:
  213. die("Invalid loopback address '{}'".format(match.group(1)))
  214. else:
  215. die("Multiple loopback addresses")
  216. # infrastructure address
  217. match = re.search(r'^ *infralink: *(\S+)', note,
  218. flags=re.IGNORECASE|re.MULTILINE)
  219. if match:
  220. if infra_base is None:
  221. try:
  222. infra_base = ipaddress.ip_interface(match.group(1))
  223. except ValueError:
  224. die("Invalid infrastructure link address '{}'".format(match.group(1)))
  225. if infra_base.network.num_addresses < 4:
  226. die("Network '{}' is too small for 2 interface addresses.".format(infra_base.with_prefixlen))
  227. if infra_base.ip == infra_base.network.network_address:
  228. infra_base = next_ip_address(infra_base)
  229. if infra_base.ip + 1 >= infra_base.network.broadcast_address:
  230. die("'{}' or it's next address is the broadcast address.".format(infra_base.with_prefixlen))
  231. else:
  232. die("Multiple infrastructure link addresses")
  233. if loopback_base is None:
  234. die("No loopback address defined")
  235. if infra_base is None:
  236. die("No infrastructure link (InfraLink) address defined")
  237. return loopback_base, infra_base
  238. def cisco_router_config(notes):
  239. """ get additional cisco router config """
  240. router_config = []
  241. for note in notes:
  242. match = re.search(r'\bcisco +router +config *:', note,
  243. flags=re.IGNORECASE)
  244. if match:
  245. conf_pos = match.end()
  246. router_config.extend(note[conf_pos:].strip().splitlines())
  247. return router_config
  248. def sorted_node_names(nodes):
  249. """ return sorted list of node names """
  250. return sorted(nodes, key=lambda k: str(k).lower())
  251. def sorted_links(links):
  252. """ return sorted list of node links """
  253. return sorted(links, key=lambda k: "" if k['label'] is None else \
  254. str(k['label']).lower())
  255. def add_link_ip(nodes, vlan_interfaces, ip_net_base):
  256. """" Add IP addresses to the links between the nodes """
  257. for name in sorted_node_names(nodes):
  258. if '_vlans' in nodes[name]:
  259. continue
  260. for link in sorted_links(nodes[name]['_links']):
  261. if not link['label'] or not link['remote_label']:
  262. continue
  263. if 'IP' in link:
  264. continue
  265. if link['remote_name'] in vlan_interfaces:
  266. match = re.search(r'\bvlan *(\d+)\b', link['remote_label'], re.IGNORECASE)
  267. if match: # link to switch group
  268. vlan = int(match.group(1))
  269. ip_addr = ip_net_base
  270. ip_count = 0
  271. ip_last_link = None
  272. for switch_if in vlan_interfaces[link['remote_name']][vlan]:
  273. rem_name = switch_if[2]
  274. rem_link_id = switch_if[4]
  275. if rem_name not in vlan_interfaces:
  276. if rem_name in nodes:
  277. for rem_link in nodes[rem_name]['_links']:
  278. if rem_link['link_id'] == rem_link_id:
  279. rem_link['IP'] = ip_addr
  280. ip_count += 1
  281. ip_last_link = rem_link
  282. break
  283. ip_addr = next_ip_address(ip_addr)
  284. if ip_count == 1: # ignore networks with 1 IP
  285. del ip_last_link['IP']
  286. elif ip_count >= 2:
  287. ip_net_base = next_ip_network(ip_net_base)
  288. elif link['remote_name'] in nodes:
  289. for rem_link in nodes[link['remote_name']]['_links']:
  290. if rem_link['link_id'] == link['link_id']:
  291. if 'IP' not in rem_link:
  292. link['IP'] = ip_net_base
  293. rem_link['IP'] = next_ip_address(ip_net_base)
  294. ip_net_base = next_ip_network(ip_net_base)
  295. break
  296. return ip_net_base
  297. def add_loopback(nodes, ip_net_base):
  298. """" Add loopback to nodes """
  299. for name in sorted_node_names(nodes):
  300. if '_vlans' in nodes[name]:
  301. continue
  302. nodes[name].setdefault("_loopback", {})
  303. if 0 not in nodes[name]['_loopback']:
  304. nodes[name]['_loopback'][0] = ip_net_base
  305. ip_net_base = next_ip_network(ip_net_base)
  306. return ip_net_base
  307. def str_clean(arg):
  308. """ cleanup string: collapse whitespaces """
  309. return " ".join(str(arg).split())
  310. class CiscoRouter(dict):
  311. """ cisco router """
  312. def create_config(self):
  313. """ create router configuration """
  314. config = []
  315. if self['node_type'] == 'qemu':
  316. config.append("hostname {}".format(self['name']))
  317. for loopback in sorted(self.get('_loopback', [])):
  318. ip, mask = self['_loopback'][loopback].with_netmask.split('/', 1)
  319. config.append("interface lo{}".format(loopback))
  320. config.append(" ip address {} {}".format(ip, mask))
  321. for link in sorted_links(self['_links']):
  322. if not link['label']:
  323. continue
  324. config.append("interface {}".format(link['label']))
  325. config.append(" description {} {}".format(
  326. link['remote_name'], str_clean(link['remote_label'])))
  327. if 'IP' in link:
  328. ip, mask = link['IP'].with_netmask.split('/', 1)
  329. config.append(" ip address {} {}".format(ip, mask))
  330. config.append(" no shutdown")
  331. config += self.get('_router_config', [])
  332. if config:
  333. config = ["configure terminal"] + config + ["end"]
  334. return config
  335. def send_commands(self, commands):
  336. """ send commands to router """
  337. return send_cisco_commands(self['name'], self["console_host"],
  338. self["console"], commands)
  339. class CiscoSwitch(dict):
  340. """ cisco switch """
  341. def create_config(self):
  342. """ create switch configuration """
  343. config = []
  344. vlan_database = []
  345. if self['node_type'] == 'qemu':
  346. config.append("hostname {}".format(self['name']))
  347. if self['node_type'] == 'dynamips':
  348. for vlan in self.get('_vlans', []):
  349. vlan_database.append("vlan {}".format(vlan))
  350. if vlan_database:
  351. vlan_database = ["vlan database"] + vlan_database + ["exit"]
  352. else:
  353. for vlan in self.get('_vlans', []):
  354. config.append("vlan {}".format(vlan))
  355. for link in sorted_links(self['_links']):
  356. if not link['label']:
  357. continue
  358. match = re.match(r'([a-zA-Z]+ ?)?[0-9.:/]*[0-9]', link['label'])
  359. if not match:
  360. continue
  361. ifname = match.group(0)
  362. config.append("interface {}".format(ifname))
  363. config.append(" description {} {}".format(
  364. link['remote_name'], str_clean(link['remote_label'])))
  365. if re.search(r'\btrunk\b', link['label'], re.IGNORECASE):
  366. config.append(" switchport trunk encapsulation dot1q")
  367. config.append(" switchport mode trunk")
  368. else:
  369. match = re.search(r'\bvlan *(\d+)\b', link['label'], re.IGNORECASE)
  370. if match: # access link
  371. vlan = int(match.group(1))
  372. config.append(" switchport access vlan {}".format(vlan))
  373. config.append(" switchport mode access")
  374. if config:
  375. config = ["configure terminal"] + config + ["end"]
  376. config = vlan_database + config
  377. return config
  378. def send_commands(self, commands):
  379. """ send commands to switch """
  380. return send_cisco_commands(self['name'], self["console_host"],
  381. self["console"], commands)
  382. def select_cisco_devices(nodes, notes):
  383. """ select cisco devices, using some heuristics """
  384. devices = {}
  385. router_config = cisco_router_config(notes)
  386. print("Checking for devices, that are non Cisco router/switches...")
  387. for name in sorted_node_names(nodes):
  388. node = nodes[name]
  389. if node["node_type"] == 'dynamips':
  390. properties = node.get("properties", {})
  391. if "NM-16ESW" in (properties.get("slot0"), properties.get("slot1"),
  392. properties.get("slot2"), properties.get("slot3"),
  393. properties.get("slot4"), properties.get("slot5"),
  394. properties.get("slot6")):
  395. devices[name] = CiscoSwitch(node)
  396. else:
  397. devices[name] = CiscoRouter(node)
  398. if router_config:
  399. devices[name]['_router_config'] = router_config
  400. elif node["node_type"] == 'iou':
  401. properties = node.get("properties", {})
  402. image = properties.get("path", "").lower()
  403. image = image.split("/")[-1]
  404. if "l2" in image:
  405. devices[name] = CiscoSwitch(node)
  406. elif "l3" in image:
  407. devices[name] = CiscoRouter(node)
  408. if router_config:
  409. devices[name]['_router_config'] = router_config
  410. else:
  411. print(" {}: unknown IOU device type".format(node["name"]))
  412. elif node["node_type"] == 'qemu':
  413. properties = node.get("properties", {})
  414. image = properties.get("hda_disk_image", "")
  415. image = image.replace("\\", "/").split("/")[-1].lower()
  416. if "ios" in image:
  417. if "l2" in image:
  418. devices[name] = CiscoSwitch(node)
  419. else:
  420. devices[name] = CiscoRouter(node)
  421. if router_config:
  422. devices[name]['_router_config'] = router_config
  423. else:
  424. print(" {}: is not an IOS node".format(node["name"]))
  425. else:
  426. print(" {}: Non Cisco type '{}'".format(node["name"], node["node_type"]))
  427. return devices
  428. def parse_args(argv):
  429. """ parse command line args and determine the project ID """
  430. argc = len(argv)
  431. if argc <= 1 or argv[1] == '-h' or argv[1] == '-?':
  432. prog_name = os.path.splitext(os.path.basename(argv[0]))[0]
  433. die("Usage: {} [<GNS3 profile>] <project>".format(prog_name))
  434. if argc <= 3: # started as a script
  435. if argc == 2: # only project, default profile
  436. profile = None
  437. project_name = argv[1]
  438. else:
  439. profile = argv[1]
  440. project_name = argv[2]
  441. sel_items = []
  442. # connect to GNS3 controller
  443. try:
  444. api = gns3api.GNS3Api(profile=profile)
  445. except gns3api.GNS3ApiException as err:
  446. die("Can't connect to GNS3 controller:", err)
  447. try: # check, if project is UUID
  448. uuid.UUID(project_name)
  449. project_id = project_name
  450. except ValueError: # convert project name to UUID
  451. # search for the project id
  452. for proj in api.request('GET', '/v2/projects'):
  453. if proj['name'] == project_name:
  454. project_id = proj['project_id']
  455. break
  456. else:
  457. die("Project '{}' not found".format(project_name))
  458. else: # started as an external tool
  459. try:
  460. with open(argv[2], "r") as file:
  461. ctl_url, ctl_user, ctl_passwd, *_ = file.read(512).splitlines()
  462. if argv[2].endswith(".tmp"):
  463. os.remove(argv[2])
  464. except (OSError, ValueError) as err:
  465. die("Can't get controller connection params:", err)
  466. project_id = argv[3]
  467. sel_items = argv[4:]
  468. # connect to GNS3 controller
  469. try:
  470. api = gns3api.GNS3Api(ctl_url, ctl_user, ctl_passwd)
  471. except gns3api.GNS3ApiException as err:
  472. die("Can't connect to GNS3 controller:", err)
  473. return api, project_id, sel_items
  474. def main(argv):
  475. """ Main function """
  476. api, project_id, sel_items = parse_args(argv)
  477. # get nodes (with link informations) and notes by GNS3 API
  478. nodes, notes = get_project_data(api, project_id, sel_items)
  479. if not nodes:
  480. die("No nodes selected")
  481. vlan_interfaces = get_vlan_interfaces(nodes)
  482. # get base networks from notes
  483. loopback_base, infra_base = base_networks(notes)
  484. # select the Cisco devices
  485. devices = select_cisco_devices(nodes, notes)
  486. if not devices:
  487. die("No Cisco routers/switches found")
  488. # assign links and IP addresses
  489. add_loopback(devices, loopback_base)
  490. add_link_ip(devices, vlan_interfaces, infra_base)
  491. # configure devices
  492. print("Configuring...")
  493. for name in sorted_node_names(devices):
  494. node = devices[name]
  495. if node["status"] != "started":
  496. sys.stderr.write("{}: Node status is '{}'\n".format(name, node["status"]))
  497. elif node.get("console") is None or \
  498. node.get("console_host") is None or \
  499. node.get("console_type") != "telnet":
  500. sys.stderr.write("{}: Doesn't use telnet console\n".format(name))
  501. else:
  502. config = node.create_config()
  503. if config:
  504. print("{}...".format(name))
  505. node.send_commands(config)
  506. else:
  507. print("{}: Nothing to configure.".format(name))
  508. if __name__ == "__main__":
  509. main(sys.argv)