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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601
  1. #!/usr/bin/python3
  2. # -*- coding: utf-8 -*-
  3. #
  4. # Copyright (C) 2018 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 IOError:
  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 IOError 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(project_id, sel_items):
  86. """ get node (with link information) and notes of a project by GNS3 API """
  87. # connect to GNS3 controller
  88. try:
  89. api = gns3api.GNS3Api()
  90. api_version = api.request('GET', '/v2/version')['version']
  91. except (IOError, OSError, gns3api.GNS3BaseException) as err:
  92. die("Can't connect to GNS3 controller:", err)
  93. # get all node and link information
  94. all_nodes = {}
  95. all_links = {}
  96. notes = []
  97. try:
  98. # check project status
  99. project = api.request('GET', ('/v2/projects', project_id))
  100. if project['status'] != 'opened':
  101. die("Project '{}' is {}, please open it.".format(
  102. project['name'], project['status']))
  103. compute_host = {}
  104. for compute in api.request('GET', '/v2/computes'):
  105. compute_host[compute["compute_id"]] = compute["host"]
  106. for node in api.request('GET', ('/v2/projects', project_id, 'nodes')):
  107. console_host = node.get("console_host")
  108. if console_host == "0.0.0.0" or console_host == "::":
  109. node["console_host"] = compute_host[node["compute_id"]]
  110. all_nodes[node["node_id"]] = node
  111. for link in api.request('GET', ('/v2/projects', project_id, 'links')):
  112. all_links[link["link_id"]] = link
  113. if sel_items: # get selected notes
  114. if api_version < '2.1.2':
  115. drawings_method = 'PUT' # GET implemented in v2.1.2
  116. else:
  117. drawings_method = 'GET'
  118. for item in sel_items:
  119. if item.startswith("text_drawings/"):
  120. drawing = api.request(drawings_method, \
  121. ('/v2/projects', project_id, "drawings", item[14:]))
  122. svg = ET.fromstring(drawing["svg"])
  123. if svg[0].tag == 'text':
  124. notes.append(svg[0].text)
  125. else: # nothing selected: get all of them
  126. for drawing in api.request('GET', ('/v2/projects', project_id, "drawings")):
  127. svg = ET.fromstring(drawing["svg"])
  128. if svg[0].tag == 'text':
  129. notes.append(svg[0].text)
  130. except (IOError, OSError, gns3api.GNS3BaseException) as err:
  131. die("Can't get node/link information:", err)
  132. nodes = select_nodes(all_nodes, all_links, sel_items)
  133. return nodes, notes
  134. def select_nodes(all_nodes, all_links, sel_items):
  135. """ select nodes and add link information """
  136. nodes = {}
  137. try:
  138. if sel_items: # get selected nodes
  139. for item in sel_items:
  140. if item.startswith("nodes/"):
  141. item = item[6:]
  142. nodes[all_nodes[item]['name']] = all_nodes[item]
  143. nodes[all_nodes[item]['name']]['_links'] = []
  144. else: # nothing selected: get all nodes
  145. for item in all_nodes:
  146. nodes[all_nodes[item]['name']] = all_nodes[item]
  147. nodes[all_nodes[item]['name']]['_links'] = []
  148. # add link informations to nodes
  149. for link in all_links:
  150. link_nodes = all_links[link]['nodes']
  151. node_0_id = link_nodes[0]['node_id']
  152. node_1_id = link_nodes[1]['node_id']
  153. node_0_name = all_nodes[node_0_id]['name']
  154. node_1_name = all_nodes[node_1_id]['name']
  155. label_0 = link_nodes[0].get("label", {}).get("text")
  156. label_1 = link_nodes[1].get("label", {}).get("text")
  157. if node_0_name in nodes:
  158. nodes[node_0_name]['_links'].append(
  159. {'link_id': link, 'link_type': all_links[link]['link_type'],
  160. 'adapter_number': link_nodes[0].get('adapter_number'),
  161. 'port_number': link_nodes[0].get('port_number'),
  162. 'label': label_0,
  163. 'remote_name': node_1_name, 'remote_label': label_1})
  164. if node_1_name in nodes:
  165. nodes[node_1_name]['_links'].append(
  166. {'link_id': link, 'link_type': all_links[link]['link_type'],
  167. 'adapter_number': link_nodes[1].get('adapter_number'),
  168. 'port_number': link_nodes[1].get('port_number'),
  169. 'label': label_1,
  170. 'remote_name': node_0_name, 'remote_label': label_0})
  171. except KeyError:
  172. die("Project informations are inconsistent")
  173. return nodes
  174. def get_vlan_interfaces(nodes):
  175. """ get vlan interfaces in switch groups """
  176. vlan_interfaces = {}
  177. for name in nodes:
  178. is_switch = False
  179. switch_group = {} # new switch group
  180. switch_list = [name]
  181. while switch_list: # process a group of switches
  182. name = switch_list.pop()
  183. if name not in nodes: # node not selected
  184. continue
  185. if name in vlan_interfaces: # already processed
  186. continue
  187. for link in nodes[name]['_links']:
  188. if not link['label']:
  189. pass
  190. elif re.search(r'\btrunk\b', link['label'], re.IGNORECASE):
  191. is_switch = True
  192. switch_list.append(link['remote_name'])
  193. else:
  194. match = re.search(r'\bvlan *(\d+)\b', link['label'], re.IGNORECASE)
  195. if match: # add vlan / link to vlan_interfaces
  196. is_switch = True
  197. vlan = int(match.group(1))
  198. switch_group.setdefault(vlan, [])
  199. switch_group[vlan].append(
  200. [name, link['label'],
  201. link['remote_name'], link['remote_label'],
  202. link['link_id']])
  203. if is_switch:
  204. vlan_interfaces[name] = switch_group
  205. for vlan in switch_group:
  206. switch_group[vlan].sort(key=lambda k: [k[0].lower(), k[1].lower()])
  207. for name in vlan_interfaces:
  208. nodes[name]['_vlans'] = sorted(vlan_interfaces[name].keys())
  209. return vlan_interfaces
  210. def base_networks(notes):
  211. """ get base IP networks for loopbacks and infrastructure interfaces """
  212. loopback_base = None
  213. infra_base = None
  214. for note in notes:
  215. # loopback address
  216. match = re.search(r'^ *loopback: *(\S+)', note,
  217. flags=re.IGNORECASE|re.MULTILINE)
  218. if match:
  219. if loopback_base is None:
  220. try:
  221. loopback_base = ipaddress.ip_interface(match.group(1))
  222. except ValueError:
  223. die("Invalid loopback address '{}'".format(match.group(1)))
  224. else:
  225. die("Multiple loopback addresses")
  226. # infrastructure address
  227. match = re.search(r'^ *infralink: *(\S+)', note,
  228. flags=re.IGNORECASE|re.MULTILINE)
  229. if match:
  230. if infra_base is None:
  231. try:
  232. infra_base = ipaddress.ip_interface(match.group(1))
  233. except ValueError:
  234. die("Invalid infrastructure link address '{}'".format(match.group(1)))
  235. if infra_base.network.num_addresses < 4:
  236. die("Network '{}' is too small for 2 interface addresses.".format(infra_base.with_prefixlen))
  237. if infra_base.ip == infra_base.network.network_address:
  238. infra_base = next_ip_address(infra_base)
  239. if infra_base.ip + 1 >= infra_base.network.broadcast_address:
  240. die("'{}' or it's next address is the broadcast address.".format(infra_base.with_prefixlen))
  241. else:
  242. die("Multiple infrastructure link addresses")
  243. if loopback_base is None:
  244. die("No loopback address defined")
  245. if infra_base is None:
  246. die("No infrastructure link (InfraLink) address defined")
  247. return loopback_base, infra_base
  248. def cisco_router_config(notes):
  249. """ get additional cisco router config """
  250. router_config = []
  251. for note in notes:
  252. match = re.search(r'\bcisco +router +config *:', note,
  253. flags=re.IGNORECASE)
  254. if match:
  255. conf_pos = match.end()
  256. router_config.extend(note[conf_pos:].strip().splitlines())
  257. return router_config
  258. def sorted_node_names(nodes):
  259. """ return sorted list of node names """
  260. return sorted(nodes, key=lambda k: str(k).lower())
  261. def sorted_links(links):
  262. """ return sorted list of node links """
  263. return sorted(links, key=lambda k: "" if k['label'] is None else \
  264. str(k['label']).lower())
  265. def add_link_ip(nodes, vlan_interfaces, ip_net_base):
  266. """" Add IP addresses to the links between the nodes """
  267. for name in sorted_node_names(nodes):
  268. if '_vlans' in nodes[name]:
  269. continue
  270. for link in sorted_links(nodes[name]['_links']):
  271. if not link['label'] or not link['remote_label']:
  272. continue
  273. if 'IP' in link:
  274. continue
  275. if link['remote_name'] in vlan_interfaces:
  276. match = re.search(r'\bvlan *(\d+)\b', link['remote_label'], re.IGNORECASE)
  277. if match: # link to switch group
  278. vlan = int(match.group(1))
  279. ip_addr = ip_net_base
  280. ip_count = 0
  281. ip_last_link = None
  282. for switch_if in vlan_interfaces[link['remote_name']][vlan]:
  283. rem_name = switch_if[2]
  284. rem_link_id = switch_if[4]
  285. if rem_name not in vlan_interfaces:
  286. if rem_name in nodes:
  287. for rem_link in nodes[rem_name]['_links']:
  288. if rem_link['link_id'] == rem_link_id:
  289. rem_link['IP'] = ip_addr
  290. ip_count += 1
  291. ip_last_link = rem_link
  292. break
  293. ip_addr = next_ip_address(ip_addr)
  294. if ip_count == 1: # ignore networks with 1 IP
  295. del ip_last_link['IP']
  296. elif ip_count >= 2:
  297. ip_net_base = next_ip_network(ip_net_base)
  298. elif link['remote_name'] in nodes:
  299. for rem_link in nodes[link['remote_name']]['_links']:
  300. if rem_link['link_id'] == link['link_id']:
  301. if 'IP' not in rem_link:
  302. link['IP'] = ip_net_base
  303. rem_link['IP'] = next_ip_address(ip_net_base)
  304. ip_net_base = next_ip_network(ip_net_base)
  305. break
  306. return ip_net_base
  307. def add_loopback(nodes, ip_net_base):
  308. """" Add loopback to nodes """
  309. for name in sorted_node_names(nodes):
  310. if '_vlans' in nodes[name]:
  311. continue
  312. nodes[name].setdefault("_loopback", {})
  313. if 0 not in nodes[name]['_loopback']:
  314. nodes[name]['_loopback'][0] = ip_net_base
  315. ip_net_base = next_ip_network(ip_net_base)
  316. return ip_net_base
  317. def str_clean(arg):
  318. """ cleanup string: collapse whitespaces """
  319. return " ".join(str(arg).split())
  320. class CiscoRouter(dict):
  321. """ cisco router """
  322. def create_config(self):
  323. """ create router configuration """
  324. config = []
  325. if self['node_type'] == 'qemu':
  326. config.append("hostname {}".format(self['name']))
  327. for loopback in sorted(self.get('_loopback', [])):
  328. ip, mask = self['_loopback'][loopback].with_netmask.split('/', 1)
  329. config.append("interface lo{}".format(loopback))
  330. config.append(" ip address {} {}".format(ip, mask))
  331. for link in sorted_links(self['_links']):
  332. if not link['label']:
  333. continue
  334. config.append("interface {}".format(link['label']))
  335. config.append(" description {} {}".format(
  336. link['remote_name'], str_clean(link['remote_label'])))
  337. if 'IP' in link:
  338. ip, mask = link['IP'].with_netmask.split('/', 1)
  339. config.append(" ip address {} {}".format(ip, mask))
  340. config.append(" no shutdown")
  341. config += self.get('_router_config', [])
  342. if config:
  343. config = ["configure terminal"] + config + ["end"]
  344. return config
  345. def send_commands(self, commands):
  346. """ send commands to router """
  347. return send_cisco_commands(self['name'], self["console_host"],
  348. self["console"], commands)
  349. class CiscoSwitch(dict):
  350. """ cisco switch """
  351. def create_config(self):
  352. """ create switch configuration """
  353. config = []
  354. vlan_database = []
  355. if self['node_type'] == 'qemu':
  356. config.append("hostname {}".format(self['name']))
  357. if self['node_type'] == 'dynamips':
  358. for vlan in self.get('_vlans', []):
  359. vlan_database.append("vlan {}".format(vlan))
  360. if vlan_database:
  361. vlan_database = ["vlan database"] + vlan_database + ["exit"]
  362. else:
  363. for vlan in self.get('_vlans', []):
  364. config.append("vlan {}".format(vlan))
  365. for link in sorted_links(self['_links']):
  366. if not link['label']:
  367. continue
  368. match = re.match(r'([a-zA-Z]+ ?)?[0-9.:/]*[0-9]', link['label'])
  369. if not match:
  370. continue
  371. ifname = match.group(0)
  372. config.append("interface {}".format(ifname))
  373. config.append(" description {} {}".format(
  374. link['remote_name'], str_clean(link['remote_label'])))
  375. if re.search(r'\btrunk\b', link['label'], re.IGNORECASE):
  376. config.append(" switchport trunk encapsulation dot1q")
  377. config.append(" switchport mode trunk")
  378. else:
  379. match = re.search(r'\bvlan *(\d+)\b', link['label'], re.IGNORECASE)
  380. if match: # access link
  381. vlan = int(match.group(1))
  382. config.append(" switchport access vlan {}".format(vlan))
  383. config.append(" switchport mode access")
  384. if config:
  385. config = ["configure terminal"] + config + ["end"]
  386. config = vlan_database + config
  387. return config
  388. def send_commands(self, commands):
  389. """ send commands to switch """
  390. return send_cisco_commands(self['name'], self["console_host"],
  391. self["console"], commands)
  392. def select_cisco_devices(nodes, notes):
  393. """ select cisco devices, using some heuristics """
  394. devices = {}
  395. router_config = cisco_router_config(notes)
  396. print("Checking for devices, that are non Cisco router/switches...")
  397. for name in sorted_node_names(nodes):
  398. node = nodes[name]
  399. if node["node_type"] == 'dynamips':
  400. properties = node.get("properties", {})
  401. if "NM-16ESW" in (properties.get("slot0"), properties.get("slot1"),
  402. properties.get("slot2"), properties.get("slot3"),
  403. properties.get("slot4"), properties.get("slot5"),
  404. properties.get("slot6")):
  405. devices[name] = CiscoSwitch(node)
  406. else:
  407. devices[name] = CiscoRouter(node)
  408. if router_config:
  409. devices[name]['_router_config'] = router_config
  410. elif node["node_type"] == 'iou':
  411. properties = node.get("properties", {})
  412. image = properties.get("path", "").lower()
  413. image = image.split("/")[-1]
  414. if "l2" in image:
  415. devices[name] = CiscoSwitch(node)
  416. elif "l3" in image:
  417. devices[name] = CiscoRouter(node)
  418. if router_config:
  419. devices[name]['_router_config'] = router_config
  420. else:
  421. print(" {}: unknown IOU device type".format(node["name"]))
  422. elif node["node_type"] == 'qemu':
  423. properties = node.get("properties", {})
  424. image = properties.get("hda_disk_image", "")
  425. image = image.replace("\\", "/").split("/")[-1].lower()
  426. if "ios" in image:
  427. if "l2" in image:
  428. devices[name] = CiscoSwitch(node)
  429. else:
  430. devices[name] = CiscoRouter(node)
  431. if router_config:
  432. devices[name]['_router_config'] = router_config
  433. else:
  434. print(" {}: is not an IOS node".format(node["name"]))
  435. else:
  436. print(" {}: Non Cisco type '{}'".format(node["name"], node["node_type"]))
  437. return devices
  438. def get_project_id(argv):
  439. """ parse command line args and determine the project ID """
  440. if len(argv) <= 1 or argv[1] == '-h' or argv[1] == '-?':
  441. prog_name = os.path.splitext(os.path.basename(argv[0]))[0]
  442. die("Usage: {} <project>".format(prog_name))
  443. project_id = None
  444. sel_items = []
  445. if len(argv) == 2: # started as a script
  446. try: # check, if argument is project UUID
  447. uuid.UUID(argv[1])
  448. project_id = argv[1]
  449. except ValueError: # argument is project name
  450. # connect to GNS3 controller
  451. try:
  452. api = gns3api.GNS3Api()
  453. except (IOError, OSError, gns3api.GNS3BaseException) as err:
  454. die("Can't connect to GNS3 controller:", err)
  455. # search for the project id
  456. project_name = argv[1]
  457. for proj in api.request('GET', '/v2/projects'):
  458. if proj['name'] == project_name:
  459. project_id = proj['project_id']
  460. break
  461. else:
  462. die("Project '{}' not found".format(project_name))
  463. elif len(argv) >= 3: # started as an external tool
  464. project_id = argv[2]
  465. sel_items = argv[3:]
  466. return project_id, sel_items
  467. def main(argv):
  468. """ Main function """
  469. project_id, sel_items = get_project_id(argv)
  470. # get nodes (with link informations) and notes by GNS3 API
  471. nodes, notes = get_project_data(project_id, sel_items)
  472. if not nodes:
  473. die("No nodes selected")
  474. vlan_interfaces = get_vlan_interfaces(nodes)
  475. # get base networks from notes
  476. loopback_base, infra_base = base_networks(notes)
  477. # select the Cisco devices
  478. devices = select_cisco_devices(nodes, notes)
  479. if not devices:
  480. die("No Cisco routers/switches found")
  481. # assign links and IP addresses
  482. add_loopback(devices, loopback_base)
  483. add_link_ip(devices, vlan_interfaces, infra_base)
  484. # configure devices
  485. print("Configuring...")
  486. for name in sorted_node_names(devices):
  487. node = devices[name]
  488. if node["status"] != "started":
  489. sys.stderr.write("{}: Node status is '{}'\n".format(name, node["status"]))
  490. elif node.get("console") is None or \
  491. node.get("console_host") is None or \
  492. node.get("console_type") != "telnet":
  493. sys.stderr.write("{}: Doesn't use telnet console\n".format(name))
  494. else:
  495. config = node.create_config()
  496. if config:
  497. print("{}...".format(name))
  498. node.send_commands(config)
  499. else:
  500. print("{}: Nothing to configure.".format(name))
  501. if __name__ == "__main__":
  502. main(sys.argv)