diff --git a/.gitignore b/.gitignore index dea088c..f462268 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .DS_Store *.bak +old internal-lab-setup-assets/workshop[0-9]*.yml internal-lab-setup-assets/clab-automation-workshop-* internal-lab-setup-assets/images/xrd diff --git a/1-reading-network-configuration/.gitkeep b/1-reading-network-configuration/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/1-reading-network-configuration/answers/exercise1/1_netmiko_show_interfaces_raw.py b/1-reading-network-configuration/answers/exercise1/1_netmiko_show_interfaces_raw.py deleted file mode 100644 index 45a1cb4..0000000 --- a/1-reading-network-configuration/answers/exercise1/1_netmiko_show_interfaces_raw.py +++ /dev/null @@ -1,26 +0,0 @@ -# pip install --user netmiko -from netmiko import Netmiko - -username = "clab" -password = "clab@123" -device_type = "cisco_xr" -hosts = ["172.16.30.2", "172.16.30.3"] -command_to_run = "show int brief" - -for host in hosts: - # Create a variable that represents an SSH connection to our router. - connection = Netmiko( - username=username, password=password, device_type=device_type, ip=host - ) - - # Send a command to the router, and get back the raw output - raw_output = connection.send_command(command_to_run) - - # The "really raw" output has '\n' characters appear instead of a real carriage return. - # Converting them into carriage returns will make it a little more readable for this demo. - raw_output = raw_output.replace("\\n", "\n") - - print( - f"### This is the raw output from {host}, without any parsing: ###\n", - raw_output + "\n", - ) diff --git a/1-reading-network-configuration/answers/exercise2/1_create_network_structured_data.py b/1-reading-network-configuration/answers/exercise2/1_create_network_structured_data.py deleted file mode 100644 index 8df9d71..0000000 --- a/1-reading-network-configuration/answers/exercise2/1_create_network_structured_data.py +++ /dev/null @@ -1,401 +0,0 @@ -# pip install --user textfsm -# pip install --user netmiko -import json -import yaml -import csv -import os -import netmiko -import ipaddress -from copy import deepcopy -from ciscoconfparse import CiscoConfParse - -# Configure logging so it goes to a .log file next to this script. -import logging - -this_script_dir = os.path.dirname(os.path.realpath(__file__)) -log_file = f"{this_script_dir}/log/exercise2.log" -logging.basicConfig( - filename=log_file, encoding="utf-8", level=logging.DEBUG, filemode="w" -) - -# Configure a global variables to store things like -# our known BGP key. -# (Don't try this at home) -BGP_MD5_KEY = "foobar" -INPUT_FILENAME = "hosts.yaml" -OUTPUT_FILENAME = "devices.json" - - -# The real script -def main(): - with open("hosts.yaml") as f: - # This creates a list of dictionaries from a YAML file. - # - # This will take the YAML file that looks similar to the following: - # - host: "10.0.0.1" - # device_type: "cisco_xr" - # username: "root" - # password: "password" - # - host: "10.0.0.2 - # {...} - - # And create a dictionary, looking like the following: - # [{"host": "10.0.0.1", "device_type": "cisco_xr", "username": "root", "password": "password"}, {...}] - # - # We can access the IP address of the first host like so: - # first_host_ip = mylist[0]["host"] - - # YAML is convenient, and only a single line of code is required. - hosts = yaml.safe_load(f) - - # Now we are in the meat of it. Let's look at each host. - parsed_data = [] - for host in hosts: - # Create the connection with our host. - connection = netmiko.ConnectHandler(**host) - - try: - # We need the name, ip, platform, version, BGP peers, and interfaces of our host. - # The functions that are called to obtain these are different based on if we are - # on a Cisco or Juniper device. We know ahead of time which hosts are going to - # which OS, so we will look and then collect data based on it. - if host["device_type"] == "cisco_xr": - # Call each helper function from below to collect the data. - data = { - "name": get_cisco_hostname(connection), - "ip": host["host"], - "platform": host["device_type"], - "version": get_cisco_version(connection), - "peers": get_cisco_bgp_peers(connection, md5_key=BGP_MD5_KEY), - "interfaces": get_cisco_interfaces(connection), - } - - elif host["device_type"] == "juniper_junos": - # Call each helper function from below to collect the data. - data = { - "name": get_junos_hostname(connection), - "ip": host["host"], - "platform": host["device_type"], - "version": get_junos_version(connection), - "peers": get_junos_bgp_peers(connection, md5_key=BGP_MD5_KEY), - "interfaces": get_junos_interfaces(connection), - } - - # If the host is neither our Cisco and Juniper OSs, then cause an error. - else: - raise Exception(f"Device type {host['device_type']} not recognized.") - - # We are done with this host, let's add it to the data. - parsed_data.append(data) - print(json.dumps(data, indent=4)) - - # If anything wrong happens that causes an Exception, the script will move to this line. - # It will print out the host that we errored on, and then containue raising the exception. - except Exception: - print(f"Errored on host: {host}!") - raise - - # "finally" means that we will run this line, no matter what. - # We want to make sure we close our SSH connections whether things work, or not, - # otherwise, they will become stale and take up TTYs on the routers. - finally: - connection.disconnect() - - # Put our data into a file as a JSON. - with open(OUTPUT_FILENAME, "w") as f: - json.dump(parsed_data, f, indent=4) - - """ Done! """ - - -##### -# Helper functions -##### - - -def get_junos_hostname(connection: netmiko.ConnectHandler): - # Extract the hostname from running "show version | display json" - # Juniper makes this easy since it will already be in JSON format. - - # Run the command, and convert the JSON output into a Python dictionary. - output = json.loads(connection.send_command("show version | display json")) - # Read the dictionary to pull out the hostname value. - hostname = output["software-information"][0]["host-name"][0]["data"] - return hostname - - -def get_junos_version(connection: netmiko.ConnectHandler): - # Extract the version number from "show version | display json" - # Juniper makes this easy since it will already be in JSON format. - - # Run the command, and convert the JSON output into a Python dictionary. - output = json.loads(connection.send_command("show version | display json")) - # Read the dictionary to pull out the version number value. - version = output["software-information"][0]["junos-version"][0]["data"] - return version - - -def get_junos_bgp_peers(connection: netmiko.ConnectHandler, md5_key=""): - # Extract the BGP peers from running "show bgp neighbor | display json" - # Juniper makes this easy since it will already be in JSON format. - - # Create an empty list to store our data in. - result = [] - - # Run the command, and convert the JSON output into a Python dictionary. - bgp_data = json.loads(connection.send_command("show bgp neighbor | display json")) - - # Read the dictionary to pull out the list of BGP peers. - list_of_peers = bgp_data["bgp-information"][0]["bgp-peer"] - - # Iterate over each peer - for peer in list_of_peers: - # Extract the IP and port number for each BGP peer. - # It will look like "10.10.10.1+12345" - peer_ip_and_port = peer["peer-address"][0]["data"] - - # Manipulate the string and get just the IP. - ip = peer_ip_and_port.split("+")[0] - - # Add it to our "result" list that we will return at the end. - result.append({"remote_address": ip, "md5_key": md5_key}) - - return result - - -def get_junos_interfaces(connection: netmiko.ConnectHandler): - # Create a detailed dictionary of all interfaces and their configuration - # using "show configuration interfaces | display json" - # Juniper makes this easy since it will already be in JSON format. - - # Create an empty list to store our data in. - result = [] - - # Run the command, and convert the JSON output into a Python dictionary. - intf_data = json.loads( - connection.send_command("show configuration interfaces | display json") - ) - - # Drill down into the dictionary to where the interfaces really are. - interfaces = intf_data["configuration"]["interfaces"]["interface"] - for intf in interfaces: - # Now we will look at each interface. The data we are looking at right now looks simiar to: - # - # eth1 { - # description foobar; - # unit 0 { - # family inet { - # address 172.17.1.17/31; - # } - # } - # unit 100 { - # description foo; - # vlan-id 100; - # family inet { - # address 198.51.100.2/24; - # } - # } - # unit 200 { - # description foo; - # vlan-id 200; - # family inet { - # address 192.0.2.2/24; - # } - # } - # - # For each interface, add the name and description (if it exists) to our own result. - # Also add an empty list for subinterfaces, which we will populate next. - data = { - "name": intf["name"], - "description": intf["description"] if "description" in intf.keys() else "", - "sub_ints": [], - } - # Drill down more into the interface and look at its subinterfaces. - for sub_int in intf["unit"]: - # Create the "full name" based off the unit number that we see. - # Ex. "100" becomes "eth1.100" - name = f"{intf['name']}.{sub_int['name']}" - - # Add the description to our subinterface data, if it exists. - description = ( - sub_int["description"] if "description" in sub_int.keys() else "" - ) - # Add the vlan id to our subinterface data, if it exists. - vlan_id = sub_int["vlan-id"] if "vlan-id" in sub_int.keys() else "" - - # Now, extract the IP address - # We will assume there is only a single IPv4 address configured. - addr = sub_int["family"]["inet"]["address"][0]["name"] - - # Use Python's ipaddress module to read our string into a sophisticated - # IPv4_Interface object. This lets us do cool things. - addr = ipaddress.ip_interface(addr) - - # The cool thing we do: It automatically converts our /24 to 255.255.255.0. - # (We won't code the conversions ourself, that's what this is for) - ip, mask = addr.with_netmask.split("/") - - # If the unit is 0, add our collected data to the top-level interface (ex. eth1). - # We do this instead of adding it as "eth1.0" to the subinterfaces. - # This keeps our behavior consistent among different vendors. - if str(name) == "0": - data.update({"ip_address": ip, "subnet_mask": mask, "vlan": vlan_id}) - - # If it isn't unit 0, then add a subinterface to our list. - else: - data["sub_ints"].append( - { - "name": name, - "description": description, - "vlan": vlan_id, - "ip_address": ip, - "subnet_mask": mask, - } - ) - # Add all data about this interface into our result to send later. - # Then, move to the next interface. - result.append(data) - - return result - - -def get_cisco_hostname(connection: netmiko.ConnectHandler): - # Run "show run hostname" and collect the output. - output = connection.send_command("show run hostname") - - # Ex. Turn "hostname cisco1" into "cisco1" and return. - return output.split()[-1] - - -def get_cisco_version(connection: netmiko.ConnectHandler): - # Run "show version | i ^ Version" and collect the output. - output = connection.send_command("show version | i ^ Version") - - # Ex. Turn the outputted " Version : 7.9.1" into "7.9.1" and return. - return output.split()[-1] - - -def get_cisco_bgp_peers(connection: netmiko.ConnectHandler, md5_key=""): - # Run "show ip bgp summary" and get the IPs of all peers. - command = "show ip bgp summary" - - # Create an empty list to store our data. - result = [] - - # Send our command and get the output. - # Our output will be pre-formatted because are turning TextFSM on. - # TextFSM understands what the output on th router will look like, since - # we are running a command it supports. - bgp_neighbors = connection.send_command(command, use_textfsm=True) - - for peer in bgp_neighbors: - # Add the IP addresses to our data that we're collecting. - try: - result.append({"remote_address": peer["bgp_neigh"], "md5_key": md5_key}) - - # If BGP is not running, the router will print something like, - # "% BGP instance 'default' not active" - # Netmiko sees "% " and knows an error happened. - # Catch this error so we can choose just to log and ignore this device. - except TypeError: - # This 'replace' turns the carriage returns in the raw output into a single-lined string. - # We don't want that in our logs. - flattened_output = bgp_neighbors.replace("\n", "\\n") - logging.info( - f'Cannot format output for "{command}". BGP may not be running? Raw output:{flattened_output}' - ) - return [] - return result - - -def get_cisco_interfaces(connection: netmiko.ConnectHandler): - # For interface configuration on Cisco devices, we can use the "ciscoconfparse" module, since - # TextFSM doesn't support our command. - # - # We can search and extract blocks of configuration like this, getting only the interfaces - # we care about by using the right CiscoConfParse functions. - # - # interface GigabitEthernet0/0/0/1.100 - # description bar to foo - # ipv4 address 198.51.100.1 255.255.255.0 - # encapsulation dot1q 100 - # ! - # - # After cleaning up the output (like removing extra spaces), we can format like so: - # - # { - # "name": "Gi0/0/0/1", - # "description": "Some customer connects here!", - # "vlan": "100", - # "ip_address": "10.0.0.1", - # "subnet_mask": "255.255.255.0" - # } - - # Create an empty dictionary to store our interfaces as we discover them. - interfaces = {} - - # Create an empty list to store subinterfaces as we discover them, and we'll - # nest them in the appropriate parent interfaces later. - sub_interfaces = [] - - # Get the output for "show run". This will be raw and unformatted. - cisco_config = connection.send_command("show run") - - # Turn this giant singular string of output into a list of lines. - parser = CiscoConfParse(cisco_config.split("\n")) - - # parser.find_objects('^interface .*') will automatically make a list of all - # lines that start with "interface " that we can iterate over. - # It's also nice because it stores that interface's configuration with it. - for intf in parser.find_objects("^interface .*"): - # Get the name by converting "interface GigabitEthernet0/1" to "GigabitEthernet0/1". - intf_name = intf.text.split()[-1] - - # Find the "description" line, and extract. Ex. Turn "description hello!" into "hello!" - intf_description = intf.re_search_children("^ description ") - if intf_description: - tmp = intf_description[0].text.strip() - intf_description = " ".join(tmp.split()[1:]) - else: - # If description doesn't exist, just use an empty string. - intf_description = "" - - # Extract the vlan id. Ex. turn "encapsulation dot1q 100" into "100". - intf_vlan = intf.re_search_children("^ encapsulation dot1q ") - intf_vlan = intf_vlan[0].text.split()[-1] if intf_vlan else "" - - # Extract the IP address and mask. - # Ex. Turn "ipv4 address 10.10.10.1 255.255.255.0" into two separate stringsm - # one in our 'ip' variable and the other in our 'mask' variable. - raw_ipmask = intf.re_search_children("^ ipv4 address ") - ip, mask = raw_ipmask[0].text.split()[-2] if raw_ipmask else "", "" - - # Take all the interface config we collected and put it into a nicely-formatted dictionary. - data = { - "name": intf_name, - "description": intf_description, - "vlan": intf_vlan, - "ip_address": ip, - "subnet_mask": mask, - } - - # If it's a subinterface, put in the 'sub_interfaces' list to store later. - if "." in intf_name: - sub_interfaces.append(data) - # Otherwise, put it in our top-most 'interfaces' dictionary. - else: - data["sub_ints"] = [] - interfaces[intf_name] = data - - # Finally we are done going through our interfaces. - # Lets go back and sort all our subinterfaces into their parents. - for i in sub_interfaces: - parent_intf = i["name"].split(".")[0] - interfaces[parent_intf]["sub_ints"].append(i) - - # Return our interfaces. - return list(interfaces.values()) - - -if __name__ == "__main__": - main() diff --git a/1-reading-network-configuration/answers/exercise2/2_add_customers_to_interfaces.py b/1-reading-network-configuration/answers/exercise2/2_add_customers_to_interfaces.py deleted file mode 100644 index f219199..0000000 --- a/1-reading-network-configuration/answers/exercise2/2_add_customers_to_interfaces.py +++ /dev/null @@ -1,102 +0,0 @@ -import csv -import json - -# Configure a global variables to store certain things. -# (Don't try this at home) -DEVICES_FILENAME = "devices.json" -CUSTOMERS_FILENAME = "customer_interfaces.csv" -OUTPUT_FILENAME = "answer.json" - - -def main(): - with open(DEVICES_FILENAME) as f: - # First, let's read in our previous data from JSON to a Python Dictionary. - devices = json.load(f) - if not devices: - raise ValueError(f"File {DEVICES_FILENAME} is empty!") - - with open(CUSTOMERS_FILENAME) as f: - # Next, we'll read in our customer data. - # - # This creates a list containing dictionaries. - # This will take a line like the following: - # cisco1,Gi0/0/0/1.100,Acme Co. - # - # And add it to the a list, which will look like: - # [{"device": "cisco1", "interface": "Gi0/0/0/1.100", "customer": "Acme Co."}, {...}] - # - # We can access the customer of the first interface like so: - # some_customer = mylist[0]["customer"] - - # Create an empty list where we can store the rows we read. - customer_interfaces = [] - - # The csv.DictReader reader will automatically associate each row - # with the CSV headers, like "customer" or "device". - csv_reader = csv.DictReader(f) - for row in csv_reader: - customer_interfaces.append(row) - - # If we didn't see anything in the customers file, raise an exception. - if not customer_interfaces: - raise ValueError(f"File {CUSTOMERS_FILENAME} is empty!") - - # Now that we have both our device data and customer data, - # let's blend the data together. - for row in customer_interfaces: - # First, find the device. - device_dict = find_device(devices, row["device"]) - - # Then, find the interface within that device. - interface_dict = find_interface(device_dict, row["interface"]) - - # Finally, at the customer into the interface we found. - interface_dict["customer"] = row["customer"] - - # We've now added customer names to each of our interfaces. - # Let's print our new data for good measure, and then store it - # in our new file. - print(json.dumps(devices, indent=4)) - with open(OUTPUT_FILENAME, "w") as f: - json.dump(devices, f, indent=4) - - """ Done! """ - - -##### -# Helper functions -##### - - -def find_device(devices, device_name): - for device in devices: - # If the name matches, the device was found. Return. - if device["name"] == device_name: - return device - - raise ValueError(f"Could not find device {device_name}!") - - -def find_interface(device, interface_name): - is_sub_int = False - if "." in interface_name: - is_sub_int = True - - for intf in device["interfaces"]: - # If we know it is a sub interface, then we know - # we need to go deeper. - if is_sub_int: - for sub_int in intf["sub_ints"]: - if interface_name == sub_int["name"]: - return sub_int - - # If it isn't a sub interface, stay at the top. - else: - if interface_name == intf["name"]: - return intf - - raise ValueError(f"Could not find interface {interface_name} on device {device}!") - - -if __name__ == "__main__": - main() diff --git a/1-reading-network-configuration/answers/exercise2/answer.json b/1-reading-network-configuration/answers/exercise2/answer.json deleted file mode 100644 index 6b77b0b..0000000 --- a/1-reading-network-configuration/answers/exercise2/answer.json +++ /dev/null @@ -1,177 +0,0 @@ -[ - { - "name": "cisco1", - "ip": "172.16.1.2", - "platform": "cisco_xr", - "version": "7.9.1", - "peers": [ - { - "remote_address": "198.51.100.2", - "md5_key": "foobar" - } - ], - "interfaces": [ - { - "name": "Loopback1", - "description": "PEER_A_NETWORK", - "vlan": "", - "ip_address": "10.0.1.1", - "subnet_mask": "", - "sub_ints": [], - "customer": "Acme Corporation" - }, - { - "name": "MgmtEth0/RP0/CPU0/0", - "description": "", - "vlan": "", - "ip_address": "172.16.1.2", - "subnet_mask": "", - "sub_ints": [], - "customer": "Beta Industries" - }, - { - "name": "GigabitEthernet0/0/0/0", - "description": "NOT_IN_USE", - "vlan": "", - "ip_address": "", - "subnet_mask": "", - "sub_ints": [], - "customer": "Gamma Enterprises" - }, - { - "name": "GigabitEthernet0/0/0/1", - "description": "foobar", - "vlan": "", - "ip_address": "172.17.1.16", - "subnet_mask": "", - "sub_ints": [ - { - "name": "GigabitEthernet0/0/0/1.100", - "description": "bar to foo", - "vlan": "100", - "ip_address": "198.51.100.1", - "subnet_mask": "", - "customer": "Epsilon Electronics" - }, - { - "name": "GigabitEthernet0/0/0/1.200", - "description": "foo to biz", - "vlan": "200", - "ip_address": "192.0.2.1", - "subnet_mask": "", - "customer": "Zeta Zoological" - } - ], - "customer": "Delta Dynamics" - }, - { - "name": "GigabitEthernet0/0/0/2", - "description": "NOT_IN_USE", - "vlan": "", - "ip_address": "", - "subnet_mask": "", - "sub_ints": [], - "customer": "Eta Enterprises" - } - ] - }, - { - "name": "cisco2", - "ip": "172.16.1.3", - "platform": "cisco_xr", - "version": "7.9.1", - "peers": [], - "interfaces": [ - { - "name": "MgmtEth0/RP0/CPU0/0", - "description": "", - "vlan": "", - "ip_address": "172.16.1.3", - "subnet_mask": "", - "sub_ints": [] - }, - { - "name": "GigabitEthernet0/0/0/0", - "description": "", - "vlan": "", - "ip_address": "", - "subnet_mask": "", - "sub_ints": [] - }, - { - "name": "GigabitEthernet0/0/0/1", - "description": "", - "vlan": "", - "ip_address": "", - "subnet_mask": "", - "sub_ints": [] - }, - { - "name": "GigabitEthernet0/0/0/2", - "description": "", - "vlan": "", - "ip_address": "", - "subnet_mask": "", - "sub_ints": [] - } - ] - }, - { - "name": "juniper1", - "ip": "172.16.1.4", - "platform": "juniper_junos", - "version": "23.1R1.8", - "peers": [ - { - "remote_address": "198.51.100.1", - "md5_key": "foobar" - } - ], - "interfaces": [ - { - "name": "eth1", - "description": "foobar", - "sub_ints": [ - { - "name": "eth1.0", - "description": "", - "vlan": "", - "ip_address": "172.17.1.17", - "subnet_mask": "255.255.255.254", - "customer": "Theta Technologies" - }, - { - "name": "eth1.100", - "description": "foo", - "vlan": 100, - "ip_address": "198.51.100.2", - "subnet_mask": "255.255.255.0", - "customer": "Iota Innovations" - }, - { - "name": "eth1.200", - "description": "foo", - "vlan": 200, - "ip_address": "192.0.2.2", - "subnet_mask": "255.255.255.0", - "customer": "Kappa Kinetics" - } - ] - }, - { - "name": "eth2", - "description": "", - "sub_ints": [ - { - "name": "eth2.0", - "description": "EXAMPLE_NETWORK", - "vlan": "", - "ip_address": "10.0.2.1", - "subnet_mask": "255.255.255.0", - "customer": "Lambda Labs" - } - ] - } - ] - } -] \ No newline at end of file diff --git a/1-reading-network-configuration/blah.py b/1-reading-network-configuration/blah.py deleted file mode 100644 index 7ae77fb..0000000 --- a/1-reading-network-configuration/blah.py +++ /dev/null @@ -1,153 +0,0 @@ -# pip install --user textfsm -# pip install --user netmiko -import json -import netmiko -import ipaddress - -bgp_md5_key = "foobar" -hosts = [ - { - "host": "172.16.3.2", - "device_type": "cisco_xr", - "username": "clab", - "password": "clab@123", - }, - { - "host": "172.16.3.3", - "device_type": "cisco_xr", - "username": "clab", - "password": "clab@123", - }, - { - "host": "172.16.3.4", - "device_type": "juniper_junos", - "username": "clab", - "password": "clab123", - }, -] - - -def main(): - result = {"hosts": []} - - for host in hosts: - parsed_data = {} - - connection = netmiko.ConnectHandler(**host) - - try: - if host["device_type"] == "cisco_xr": - print(get_cisco_hostname(connection)) - print(get_cisco_version(connection)) - print(get_cisco_bgp_peers(connection, md5_key=bgp_md5_key)) - print(get_cisco_interfaces(connection)) - - elif host["device_type"] == "juniper_junos": - print(get_junos_hostname(connection)) - print(get_junos_version(connection)) - print(get_junos_bgp_peers(connection, md5_key=bgp_md5_key)) - print(get_junos_interfaces(connection)) - - else: - raise Exception(f"Device type {host['device_type']} not recognized.") - - except Exception: - print(f"Errored on host: {host}!") - raise - finally: - connection.disconnect() - - -def get_junos_hostname(connection: netmiko.ConnectHandler): - output = json.loads(connection.send_command("show version | display json")) - hostname = output["software-information"][0]["host-name"][0]["data"] - return hostname - - -def get_junos_version(connection: netmiko.ConnectHandler): - output = json.loads(connection.send_command("show version | display json")) - version = output["software-information"][0]["junos-version"][0]["data"] - return version - - -def get_junos_bgp_peers(connection: netmiko.ConnectHandler, md5_key=""): - result = [] - bgp_data = json.loads(connection.send_command("show bgp neighbor | display json")) - - list_of_peers = bgp_data["bgp-information"][0]["bgp-peer"] - - for peer in list_of_peers: - peer_ip_and_port = peer["peer-address"][0]["data"] - ip = peer_ip_and_port.split("+")[0] - result.append({"remote_address": ip, "md5_key": md5_key}) - - return result - - -def get_junos_interfaces(connection: netmiko.ConnectHandler): - result = [] - intf_data = json.loads( - connection.send_command("show configuration interfaces | display json") - ) - interfaces = intf_data["configuration"]["interfaces"]["interface"] - for intf in interfaces: - data = { - "name": intf["name"], - "description": intf["description"] if "description" in intf.keys() else "", - "sub_ints": [], - } - for sub_int in intf["unit"]: - name = sub_int["name"] - description = ( - sub_int["description"] if "description" in sub_int.keys() else "" - ) - vlan_id = sub_int["vlan-id"] if "vlan-id" in sub_int.keys() else "" - - # We will assume there is only a single IPv4 address configured. - addr = sub_int["family"]["inet"]["address"][0]["name"] - addr = ipaddress.ip_interface(addr) - ip, mask = addr.with_netmask.split("/") - - if str(name) == "0": - data.update({"ip_address": ip, "subnet_mask": mask, "vlan": vlan_id}) - else: - data["sub_ints"].append( - { - "name": name, - "description": description, - "vlan": vlan_id, - "ip_address": ip, - "subnet_mask": mask, - } - ) - result.append(data) - - return result - - -def get_cisco_hostname(connection: netmiko.ConnectHandler): - output = connection.send_command("show run hostname").split()[-1] - return output - - -def get_cisco_version(connection: netmiko.ConnectHandler): - output = connection.send_command("show version | i ^ Version") - return output.split()[-1] - - -def get_cisco_bgp_peers(connection: netmiko.ConnectHandler, md5_key=""): - result = [] - bgp_neighbors = connection.send_command("show ip bgp summary", use_textfsm=True) - for peer in bgp_neighbors: - result.append({"remote_address": peer["bgp_neigh"], "md5_key": md5_key}) - return result - - -def get_cisco_interfaces(connection: netmiko.ConnectHandler): - # TODO - intf_data = connection.send_command("show interfaces", use_textfsm=True) - return intf_data - - -if __name__ == "__main__": - main() diff --git a/1-reading-network-configuration/customer_ports.json b/1-reading-network-configuration/customer_ports.json deleted file mode 100644 index e05aa3c..0000000 --- a/1-reading-network-configuration/customer_ports.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "cisco1": { - "Loopback1": "Acme Co.", - "MgmtEth0/RP0/CPU0/0": "Globex Corp.", - "GigabitEthernet0/0/0/0": "Initech", - "GigabitEthernet0/0/0/1": "Hooli", - "GigabitEthernet0/0/0/1.100": "Wonka Industries", - "GigabitEthernet0/0/0/1.200": "Stark Industries", - "GigabitEthernet0/0/0/2": "Acme Co." - }, - "juniper1": { - "eth1.0": "Monsters, Inc.", - "eth1.100": "Umbrella Corporation", - "eth1.200": "Wayne Enterprises", - "eth2.0": "Dunder Mifflin" - } -} \ No newline at end of file diff --git a/1-reading-network-configuration/exercise2/1_create_network_structured_data.py b/1-reading-network-configuration/exercise2/1_create_network_structured_data.py deleted file mode 100644 index 1db8731..0000000 --- a/1-reading-network-configuration/exercise2/1_create_network_structured_data.py +++ /dev/null @@ -1,410 +0,0 @@ -# pip install --user pyyaml # TODO -# pip install --user textfsm # TODO -# pip install --user netmiko # TODO -# pip install --user ciscoconfparse # TODO -import json -import yaml -import csv -import os -import netmiko -import ipaddress -from copy import deepcopy -from ciscoconfparse import CiscoConfParse - -# Configure logging so it goes to a .log file next to this script. -import logging - -this_script_dir = os.path.dirname(os.path.realpath(__file__)) -log_file = f"{this_script_dir}/log/exercise2.log" -logging.basicConfig( - filename=log_file, encoding="utf-8", level=logging.DEBUG, filemode="w" -) - -# Configure a global variables to store things like -# our known BGP key. -# (Don't try this at home) -BGP_MD5_KEY = "foobar" -INPUT_FILENAME = "hosts.yaml" -OUTPUT_FILENAME = "devices.json" - - -# The real script -def main(): - with open(INPUT_FILENAME) as f: - # This creates a list of dictionaries from a YAML file. - # - # This will take the YAML file that looks similar to the following: - # - host: "10.0.0.1" - # device_type: "cisco_xr" - # username: "root" - # password: "password" - # - host: "10.0.0.2 - # {...} - - # And create a dictionary, looking like the following: - # [{"host": "10.0.0.1", "device_type": "cisco_xr", "username": "root", "password": "password"}, {...}] - # - # We can access the IP address of the first host like so: - # first_host_ip = mylist[0]["host"] - - # YAML is convenient, and only a single line of code is required. - hosts = yaml.safe_load(f) - - # Now we are in the meat of it. Let's look at each host. - parsed_data = [] - for host in hosts: - # Create the Netmiko connection with our host. - # Under the hood, "**host" looks like this: - # { - # host: "172.16.1.2" - # device_type: "cisco_xr" - # username: "clab" - # password: "clab@123" - # } - connection = netmiko.ConnectHandler(**host) - - try: - # We need the name, ip, platform, version, BGP peers, and interfaces of our host. - # The functions that are called to obtain these are different based on if we are - # on a Cisco or Juniper device. We know ahead of time which hosts are going to - # which OS, so we will look and then collect data based on it. - if host["device_type"] == "fill me in!": # TODO - # Call each helper function from below to collect the data. - data = { - "name": get_cisco_hostname(connection), - "ip": host["host"], - "platform": host["device_type"], - "version": get_cisco_version(connection), - "peers": get_cisco_bgp_peers(connection, md5_key=BGP_MD5_KEY), - "interfaces": get_cisco_interfaces(connection), - } - - elif host["device_type"] == "fill me in!": # TODO - # Call each helper function from below to collect the data. - data = { - "name": get_junos_hostname(connection), - "ip": host["host"], - "platform": host["device_type"], - "version": get_junos_version(connection), - "peers": get_junos_bgp_peers(connection, md5_key=BGP_MD5_KEY), - "interfaces": get_junos_interfaces(connection), - } - - # If the host is neither our Cisco nor Juniper OSs, then cause an error. - else: - raise Exception(f"Device type {host['device_type']} not recognized.") - - # We are done with this host, let's add it to the data. - parsed_data.append(data) - print(json.dumps(data, indent=4)) - - # If anything wrong happens that causes an Exception, the script will move to this line. - # It will print out the host that we errored on, and then containue raising the exception. - except Exception: - print(f"Errored on host: {host}!") - raise - - # "finally" means that we will run this line, no matter what. - # We want to make sure we close our SSH connections whether things work, or not, - # otherwise, they will become stale and take up TTYs on the routers. - finally: - connection.disconnect() - - # Put our data into a file as a JSON. - with open(OUTPUT_FILENAME, "w") as f: - json.dump(parsed_data, f, indent=4) - - """ Done! """ - - -##### -# Helper functions -##### - - -def get_junos_hostname(connection: netmiko.ConnectHandler): - # Extract the hostname from running "show version | display json" - # Juniper makes this easy since it will already be in JSON format. - - # Run the command, and convert the JSON output into a Python dictionary. - output = json.loads(connection.send_command("show version | display json")) - # Read the dictionary to pull out the hostname value. - hostname = output["software-information"][0]["host-name"][0]["data"] - return hostname - - -def get_junos_version(connection: netmiko.ConnectHandler): - # Extract the version number from "show version | display json" - # Juniper makes this easy since it will already be in JSON format. - - # Run the command, and convert the JSON output into a Python dictionary. - output = json.loads(connection.send_command("show version | display json")) - # Read the dictionary to pull out the version number value. - version = output["software-information"][0]["junos-version"][0]["data"] - return version - - -def get_junos_bgp_peers(connection: netmiko.ConnectHandler, md5_key=""): - # Extract the BGP peers from running "show bgp neighbor | display json" - # Juniper makes this easy since it will already be in JSON format. - - # Create an empty list to store our data in. - result = [] - - # Run the command, and convert the JSON output into a Python dictionary. - bgp_data = json.loads(connection.send_command("show bgp neighbor | display json")) - - # Read the dictionary to pull out the list of BGP peers. - list_of_peers = bgp_data["bgp-information"][0]["bgp-peer"] - - # Iterate over each peer - for peer in list_of_peers: - # Extract the IP and port number for each BGP peer. - # It will look like "10.10.10.1+12345" - peer_ip_and_port = peer["peer-address"][0]["data"] - - # Manipulate the string and get just the IP. - ip = peer_ip_and_port.split("+")[0] - - # Add it to our "result" list that we will return at the end. - result.append({"remote_address": ip, "md5_key": md5_key}) - - return result - - -def get_junos_interfaces(connection: netmiko.ConnectHandler): - # Create a detailed dictionary of all interfaces and their configuration - # using "show configuration interfaces | display json" - # Juniper makes this easy since it will already be in JSON format. - - # Create an empty list to store our data in. - result = [] - - # Run the command, and convert the JSON output into a Python dictionary. - intf_data = json.loads( - connection.send_command("show configuration interfaces | display json") - ) - - # Drill down into the dictionary to where the interfaces really are. - interfaces = intf_data["configuration"]["interfaces"]["interface"] - for intf in interfaces: - # Now we will look at each interface. The data we are looking at right now looks simiar to: - # - # eth1 { - # description foobar; - # unit 0 { - # family inet { - # address 172.17.1.17/31; - # } - # } - # unit 100 { - # description foo; - # vlan-id 100; - # family inet { - # address 198.51.100.2/24; - # } - # } - # unit 200 { - # description foo; - # vlan-id 200; - # family inet { - # address 192.0.2.2/24; - # } - # } - # - # For each interface, add the name and description (if it exists) to our own result. - # Also add an empty list for subinterfaces, which we will populate next. - data = { - "name": intf["name"], - "description": intf["description"] if "description" in intf.keys() else "", - "sub_ints": [], - } - # Drill down more into the interface and look at its subinterfaces. - for sub_int in intf["unit"]: - # Create the "full name" based off the unit number that we see. - # Ex. "100" becomes "eth1.100" - name = f"{intf['name']}.{sub_int['name']}" - - # Add the description to our subinterface data, if it exists. - description = ( - sub_int["description"] if "description" in sub_int.keys() else "" - ) - # Add the vlan id to our subinterface data, if it exists. - vlan_id = sub_int["vlan-id"] if "vlan-id" in sub_int.keys() else "" - - # Now, extract the IP address - # We will assume there is only a single IPv4 address configured. - addr = sub_int["family"]["inet"]["address"][0]["name"] - - # Use Python's ipaddress module to read our string into a sophisticated - # IPv4_Interface object. This lets us do cool things. - addr = ipaddress.ip_interface(addr) - - # The cool thing we do: It automatically converts our /24 to 255.255.255.0. - # (We won't code the conversions ourself, that's what this is for) - ip, mask = addr.with_netmask.split("/") - - # If the unit is 0, add our collected data to the top-level interface (ex. eth1). - # We do this instead of adding it as "eth1.0" to the subinterfaces. - # This keeps our behavior consistent among different vendors. - if str(name) == "0": - data.update({"ip_address": ip, "subnet_mask": mask, "vlan": vlan_id}) - - # If it isn't unit 0, then add a subinterface to our list. - else: - data["sub_ints"].append( - { - "name": name, - "description": description, - "vlan": vlan_id, - "ip_address": ip, - "subnet_mask": mask, - } - ) - # Add all data about this interface into our result to send later. - # Then, move to the next interface. - result.append(data) - - return result - - -def get_cisco_hostname(connection: netmiko.ConnectHandler): - # Run "show run hostname" and collect the output. - output = connection.send_command("show run hostname") - - # Ex. Turn "hostname cisco1" into "cisco1" and return. - return output.split()[-1] - - -def get_cisco_version(connection: netmiko.ConnectHandler): - # Run "show version | i ^ Version" and collect the output. - output = connection.send_command("show version | i ^ Version") - - # Ex. Turn the outputted " Version : 7.9.1" into "7.9.1" and return. - return output.split()[-1] - - -def get_cisco_bgp_peers(connection: netmiko.ConnectHandler, md5_key=""): - # Run "show ip bgp summary" and get the IPs of all peers. - command = "show ip bgp summary" - - # Create an empty list to store our data. - result = [] - - # Send our command and get the output. - # Our output will be pre-formatted because are turning TextFSM on. - # TextFSM understands what the output on th router will look like, since - # we are running a command it supports. - bgp_neighbors = connection.send_command(command, use_textfsm=True) - - for peer in bgp_neighbors: - # Add the IP addresses to our data that we're collecting. - try: - result.append({"remote_address": peer["bgp_neigh"], "md5_key": md5_key}) - - # If BGP is not running, the router will print something like, - # "% BGP instance 'default' not active" - # Netmiko sees "% " and knows an error happened. - # Catch this error so we can choose just to log and ignore this device. - except TypeError: - # This 'replace' turns the carriage returns in the raw output into a single-lined string. - # We don't want that in our logs. - flattened_output = bgp_neighbors.replace("\n", "\\n") - logging.info( - f'Cannot format output for "{command}". BGP may not be running? Raw output:{flattened_output}' - ) - return [] - return result - - -def get_cisco_interfaces(connection: netmiko.ConnectHandler): - # For interface configuration on Cisco devices, we can use the "ciscoconfparse" module, since - # TextFSM doesn't support our command. - # - # We can search and extract blocks of configuration like this, getting only the interfaces - # we care about by using the right CiscoConfParse functions. - # - # interface GigabitEthernet0/0/0/1.100 - # description bar to foo - # ipv4 address 198.51.100.1 255.255.255.0 - # encapsulation dot1q 100 - # ! - # - # After cleaning up the output (like removing extra spaces), we can format like so: - # - # { - # "name": "Gi0/0/0/1", - # "description": "Some customer connects here!", - # "vlan": "100", - # "ip_address": "10.0.0.1", - # "subnet_mask": "255.255.255.0" - # } - - # Create an empty dictionary to store our interfaces as we discover them. - interfaces = {} - - # Create an empty list to store subinterfaces as we discover them, and we'll - # nest them in the appropriate parent interfaces later. - sub_interfaces = [] - - # Get the output for "show run". This will be raw and unformatted. - cisco_config = connection.send_command("show run") - - # Turn this giant singular string of output into a list of lines. - parser = CiscoConfParse(cisco_config.split("\n")) - - # parser.find_objects('^interface .*') will automatically make a list of all - # lines that start with "interface " that we can iterate over. - # It's also nice because it stores that interface's configuration with it. - for intf in parser.find_objects("^interface .*"): - # Get the name by converting "interface GigabitEthernet0/1" to "GigabitEthernet0/1". - intf_name = intf.text.split()[-1] - - # Find the "description" line, and extract. Ex. Turn "description hello!" into "hello!" - intf_description = intf.re_search_children("^ description ") - if intf_description: - tmp = intf_description[0].text.strip() - intf_description = " ".join(tmp.split()[1:]) - else: - # If description doesn't exist, just use an empty string. - intf_description = "" - - # Extract the vlan id. Ex. turn "encapsulation dot1q 100" into "100". - intf_vlan = intf.re_search_children("^ encapsulation dot1q ") - intf_vlan = intf_vlan[0].text.split()[-1] if intf_vlan else "" - - # Extract the IP address and mask. - # Ex. Turn "ipv4 address 10.10.10.1 255.255.255.0" into two separate stringsm - # one in our 'ip' variable and the other in our 'mask' variable. - raw_ipmask = intf.re_search_children("^ ipv4 address ") - ip, mask = raw_ipmask[0].text.split()[-2] if raw_ipmask else "", "" - - # Take all the interface config we collected and put it into a nicely-formatted dictionary. - data = { - "name": intf_name, - "description": intf_description, - "vlan": intf_vlan, - "ip_address": ip, - "subnet_mask": mask, - } - - # If it's a subinterface, put in the 'sub_interfaces' list to store later. - if "." in intf_name: - sub_interfaces.append(data) - # Otherwise, put it in our top-most 'interfaces' dictionary. - else: - data["sub_ints"] = [] - interfaces[intf_name] = data - - # Finally we are done going through our interfaces. - # Lets go back and sort all our subinterfaces into their parents. - for i in sub_interfaces: - parent_intf = i["name"].split(".")[0] - interfaces[parent_intf]["sub_ints"].append(i) - - # Return our interfaces. - return list(interfaces.values()) - - -if __name__ == "__main__": - main() diff --git a/1-reading-network-configuration/exercise2/2_add_customers_to_interfaces.py b/1-reading-network-configuration/exercise2/2_add_customers_to_interfaces.py deleted file mode 100644 index f219199..0000000 --- a/1-reading-network-configuration/exercise2/2_add_customers_to_interfaces.py +++ /dev/null @@ -1,102 +0,0 @@ -import csv -import json - -# Configure a global variables to store certain things. -# (Don't try this at home) -DEVICES_FILENAME = "devices.json" -CUSTOMERS_FILENAME = "customer_interfaces.csv" -OUTPUT_FILENAME = "answer.json" - - -def main(): - with open(DEVICES_FILENAME) as f: - # First, let's read in our previous data from JSON to a Python Dictionary. - devices = json.load(f) - if not devices: - raise ValueError(f"File {DEVICES_FILENAME} is empty!") - - with open(CUSTOMERS_FILENAME) as f: - # Next, we'll read in our customer data. - # - # This creates a list containing dictionaries. - # This will take a line like the following: - # cisco1,Gi0/0/0/1.100,Acme Co. - # - # And add it to the a list, which will look like: - # [{"device": "cisco1", "interface": "Gi0/0/0/1.100", "customer": "Acme Co."}, {...}] - # - # We can access the customer of the first interface like so: - # some_customer = mylist[0]["customer"] - - # Create an empty list where we can store the rows we read. - customer_interfaces = [] - - # The csv.DictReader reader will automatically associate each row - # with the CSV headers, like "customer" or "device". - csv_reader = csv.DictReader(f) - for row in csv_reader: - customer_interfaces.append(row) - - # If we didn't see anything in the customers file, raise an exception. - if not customer_interfaces: - raise ValueError(f"File {CUSTOMERS_FILENAME} is empty!") - - # Now that we have both our device data and customer data, - # let's blend the data together. - for row in customer_interfaces: - # First, find the device. - device_dict = find_device(devices, row["device"]) - - # Then, find the interface within that device. - interface_dict = find_interface(device_dict, row["interface"]) - - # Finally, at the customer into the interface we found. - interface_dict["customer"] = row["customer"] - - # We've now added customer names to each of our interfaces. - # Let's print our new data for good measure, and then store it - # in our new file. - print(json.dumps(devices, indent=4)) - with open(OUTPUT_FILENAME, "w") as f: - json.dump(devices, f, indent=4) - - """ Done! """ - - -##### -# Helper functions -##### - - -def find_device(devices, device_name): - for device in devices: - # If the name matches, the device was found. Return. - if device["name"] == device_name: - return device - - raise ValueError(f"Could not find device {device_name}!") - - -def find_interface(device, interface_name): - is_sub_int = False - if "." in interface_name: - is_sub_int = True - - for intf in device["interfaces"]: - # If we know it is a sub interface, then we know - # we need to go deeper. - if is_sub_int: - for sub_int in intf["sub_ints"]: - if interface_name == sub_int["name"]: - return sub_int - - # If it isn't a sub interface, stay at the top. - else: - if interface_name == intf["name"]: - return intf - - raise ValueError(f"Could not find interface {interface_name} on device {device}!") - - -if __name__ == "__main__": - main() diff --git a/1-reading-network-configuration/exercise2/README.md b/1-reading-network-configuration/exercise2/README.md deleted file mode 100644 index ea958e6..0000000 --- a/1-reading-network-configuration/exercise2/README.md +++ /dev/null @@ -1,205 +0,0 @@ -# Exercise 2: Structured Data and Scraping the Network - - -### How to Complete - -To complete this exercise, all TODOs must be completed in both scripts and one input YAML: -- 1_create_network_structured_data.py -- 2_add_customers_to_interfaces.py -- hosts.yaml - -If done successfully, both scripts can be run in sequential order. They will output results as they run. - -``` -From the 2023-workshop-automation folder: - -cd 1-reading-network-configuration/exercise2 -python 1_create_network_structured_data.py -python 2_add_customers_to_interfaces.py -``` - - -### Answer - -The newly outputted `answers.json` will match the following, where {{x}} is your lab number: - -``` -[ - { - "name": "cisco1", - "ip": "172.16.{{x}}.2", - "platform": "cisco_xr", - "version": "7.9.1", - "peers": [ - { - "remote_address": "198.51.100.2", - "md5_key": "foobar" - } - ], - "interfaces": [ - { - "name": "Loopback1", - "description": "PEER_A_NETWORK", - "vlan": "", - "ip_address": "10.0.1.1", - "subnet_mask": "", - "sub_ints": [], - "customer": "Acme Corporation" - }, - { - "name": "MgmtEth0/RP0/CPU0/0", - "description": "", - "vlan": "", - "ip_address": "172.16.{{x}}.2", - "subnet_mask": "", - "sub_ints": [], - "customer": "Beta Industries" - }, - { - "name": "GigabitEthernet0/0/0/0", - "description": "NOT_IN_USE", - "vlan": "", - "ip_address": "", - "subnet_mask": "", - "sub_ints": [], - "customer": "Gamma Enterprises" - }, - { - "name": "GigabitEthernet0/0/0/1", - "description": "foobar", - "vlan": "", - "ip_address": "172.17.1.16", - "subnet_mask": "", - "sub_ints": [ - { - "name": "GigabitEthernet0/0/0/1.100", - "description": "bar to foo", - "vlan": "100", - "ip_address": "198.51.100.1", - "subnet_mask": "", - "customer": "Epsilon Electronics" - }, - { - "name": "GigabitEthernet0/0/0/1.200", - "description": "foo to biz", - "vlan": "200", - "ip_address": "192.0.2.1", - "subnet_mask": "", - "customer": "Zeta Zoological" - } - ], - "customer": "Delta Dynamics" - }, - { - "name": "GigabitEthernet0/0/0/2", - "description": "NOT_IN_USE", - "vlan": "", - "ip_address": "", - "subnet_mask": "", - "sub_ints": [], - "customer": "Eta Enterprises" - } - ] - }, - { - "name": "cisco2", - "ip": "172.16.{{x}}.3", - "platform": "cisco_xr", - "version": "7.9.1", - "peers": [], - "interfaces": [ - { - "name": "MgmtEth0/RP0/CPU0/0", - "description": "", - "vlan": "", - "ip_address": "172.16.{{x}}.3", - "subnet_mask": "", - "sub_ints": [] - }, - { - "name": "GigabitEthernet0/0/0/0", - "description": "", - "vlan": "", - "ip_address": "", - "subnet_mask": "", - "sub_ints": [] - }, - { - "name": "GigabitEthernet0/0/0/1", - "description": "", - "vlan": "", - "ip_address": "", - "subnet_mask": "", - "sub_ints": [] - }, - { - "name": "GigabitEthernet0/0/0/2", - "description": "", - "vlan": "", - "ip_address": "", - "subnet_mask": "", - "sub_ints": [] - } - ] - }, - { - "name": "juniper1", - "ip": "172.16.{{x}}.4", - "platform": "juniper_junos", - "version": "23.1R1.8", - "peers": [ - { - "remote_address": "198.51.100.1", - "md5_key": "foobar" - } - ], - "interfaces": [ - { - "name": "eth1", - "description": "foobar", - "sub_ints": [ - { - "name": "eth1.0", - "description": "", - "vlan": "", - "ip_address": "172.17.1.17", - "subnet_mask": "255.255.255.254", - "customer": "Theta Technologies" - }, - { - "name": "eth1.100", - "description": "foo", - "vlan": 100, - "ip_address": "198.51.100.2", - "subnet_mask": "255.255.255.0", - "customer": "Iota Innovations" - }, - { - "name": "eth1.200", - "description": "foo", - "vlan": 200, - "ip_address": "192.0.2.2", - "subnet_mask": "255.255.255.0", - "customer": "Kappa Kinetics" - } - ] - }, - { - "name": "eth2", - "description": "", - "sub_ints": [ - { - "name": "eth2.0", - "description": "EXAMPLE_NETWORK", - "vlan": "", - "ip_address": "10.0.2.1", - "subnet_mask": "255.255.255.0", - "customer": "Lambda Labs" - } - ] - } - ] - } -] -``` - diff --git a/1-reading-network-configuration/exercise2/customer_interfaces.csv b/1-reading-network-configuration/exercise2/customer_interfaces.csv deleted file mode 100644 index 6dfea76..0000000 --- a/1-reading-network-configuration/exercise2/customer_interfaces.csv +++ /dev/null @@ -1,12 +0,0 @@ -device,interface,customer -cisco1,Loopback1,Acme Corporation -cisco1,MgmtEth0/RP0/CPU0/0,Beta Industries -cisco1,GigabitEthernet0/0/0/0,Gamma Enterprises -cisco1,GigabitEthernet0/0/0/1,Delta Dynamics -cisco1,GigabitEthernet0/0/0/1.100,Epsilon Electronics -cisco1,GigabitEthernet0/0/0/1.200,Zeta Zoological -cisco1,GigabitEthernet0/0/0/2,Eta Enterprises -juniper1,eth1.0,Theta Technologies -juniper1,eth1.100,Iota Innovations -juniper1,eth1.200,Kappa Kinetics -juniper1,eth2.0,Lambda Labs diff --git a/1-reading-network-configuration/exercise2/hosts.yaml b/1-reading-network-configuration/exercise2/hosts.yaml deleted file mode 100644 index cf69b15..0000000 --- a/1-reading-network-configuration/exercise2/hosts.yaml +++ /dev/null @@ -1,12 +0,0 @@ -- host: "" - device_type: "cisco_xr" - username: "clab" - password: "clab@123" -- host: "" - device_type: "cisco_xr" - username: "clab" - password: "clab@123" -- host: "" - device_type: "juniper_junos" - username: "clab" - password: "clab123" diff --git a/1-reading-network-configuration/exercise2/log/.gitkeep b/1-reading-network-configuration/exercise2/log/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/1-reading-network-configuration/my_hosts.yaml b/1-reading-network-configuration/my_hosts.yaml deleted file mode 100644 index be43f58..0000000 --- a/1-reading-network-configuration/my_hosts.yaml +++ /dev/null @@ -1,13 +0,0 @@ -hosts: - - host: "172.16.3.2" - device_type: "cisco_xr" - username: "clab" - password: "clab@123" - - host": 172.16.3.3" - device_type: "cisco_xr" - username": "clab" - password: "clab@123" - - host: "172.16.3.4" - device_type": "juniper_junos" - username: "clab" - password: "clab123" diff --git a/1-reading-network-configuration/workshop_exercise/2_netmiko_show_interfaces_textfsm.py b/1-reading-network-configuration/workshop_exercise/2_netmiko_show_interfaces_textfsm.py deleted file mode 100644 index 1d91594..0000000 --- a/1-reading-network-configuration/workshop_exercise/2_netmiko_show_interfaces_textfsm.py +++ /dev/null @@ -1,30 +0,0 @@ -# pip install --user textfsm -# pip install --user netmiko -import json -from netmiko import Netmiko - -username = "fill me in!" -password = "fill me in!" -device_type = "fill me in!" -hosts = ["x.x.x.x", "y.y.y.y"] -command_to_run = "show lldp neighbors" - -for host in hosts: - # Create a variable that represents an SSH connection to our router. - connection = Netmiko( - username=username, - password=password, - device_type=device_type, - ip=host, - ) - - # Send a command to the router, and get back the output "dictionaried" by textfsm. - textfsm_output = connection.send_command(command_to_run, use_textfsm=True) - - print(f"### This is the TextFSM output from {host}: ###") - print(textfsm_output) - print("\n") # Add extra space between our outputs for each host - - print(f"### This is the TextFSM output from {host}, but JSON-formatted: ###") - print(json.dumps(textfsm_output, indent=4)) # indent for readability - print("\n") # Add extra space between our outputs for each host diff --git a/internal-lab-setup-assets/images/internet2_getting_started/Containerfile b/internal-lab-setup-assets/images/internet2_getting_started/Containerfile index cc574e7..cc6d587 100644 --- a/internal-lab-setup-assets/images/internet2_getting_started/Containerfile +++ b/internal-lab-setup-assets/images/internet2_getting_started/Containerfile @@ -10,14 +10,12 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ net-tools \ iputils-ping \ traceroute \ + curl \ git \ pip \ make \ vim nano emacs -# Install Poetry to save our friends some time and confusion -RUN curl -sSL https://install.python-poetry.org | python3 - - # Create user account "lab" for workshop participant RUN adduser --gecos "" clab && usermod -aG sudo clab diff --git a/internal-lab-setup-assets/images/internet2_getting_started/workshop-init.sh b/internal-lab-setup-assets/images/internet2_getting_started/workshop-init.sh index b556293..9c7395a 100755 --- a/internal-lab-setup-assets/images/internet2_getting_started/workshop-init.sh +++ b/internal-lab-setup-assets/images/internet2_getting_started/workshop-init.sh @@ -8,9 +8,6 @@ echo "clab:$LONI_LAB_PASSWORD" | chpasswd # Enable highlighting in vim echo "syntax on" > /home/clab/.vimrc -# Add poetry to PATH so it's a little easier -export PATH="$HOME/.local/bin:$PATH" - # Required in some environments, like ours, to make clab happy /sbin/sysctl -w net.ipv6.conf.all.disable_ipv6=1 2>/dev/null /sbin/sysctl -w net.ipv6.conf.default.disable_ipv6=1 2>/dev/null diff --git a/internal-lab-setup-assets/startup-config/cisco1.conf b/internal-lab-setup-assets/startup-config/cisco1.conf index 5e86b32..92337c3 100644 --- a/internal-lab-setup-assets/startup-config/cisco1.conf +++ b/internal-lab-setup-assets/startup-config/cisco1.conf @@ -53,6 +53,7 @@ interface GigabitEthernet0/0/0/2 interface GigabitEthernet0/0/0/2.100 description Engineering Users encapsulation dot1Q 100 + ipv4 helper-address vrf default 192.168.100.100 ip address 10.1.0.1 255.255.255.0 ! route-policy PERMIT_ALL diff --git a/internal-lab-setup-assets/startup-config/cisco2.conf b/internal-lab-setup-assets/startup-config/cisco2.conf index 9079f8d..271e05c 100644 --- a/internal-lab-setup-assets/startup-config/cisco2.conf +++ b/internal-lab-setup-assets/startup-config/cisco2.conf @@ -53,6 +53,7 @@ interface GigabitEthernet0/0/0/2 interface GigabitEthernet0/0/0/2.100 description Biochemistry Users encapsulation dot1Q 100 + ipv4 helper-address vrf default 10.2.3.4 ip address 10.2.0.1 255.255.255.0 ! router static diff --git a/internal-lab-setup-assets/startup-config/cisco3.conf b/internal-lab-setup-assets/startup-config/cisco3.conf index 4108a0e..fe02992 100644 --- a/internal-lab-setup-assets/startup-config/cisco3.conf +++ b/internal-lab-setup-assets/startup-config/cisco3.conf @@ -43,6 +43,7 @@ interface GigabitEthernet0/0/0/2 interface GigabitEthernet0/0/0/2.100 description Psychology Users encapsulation dot1Q 100 + ipv4 helper-address vrf default 10.2.3.4 ip address 10.3.0.1 255.255.255.0 ! router static diff --git a/1-reading-network-configuration/workshop_exercise/1_netmiko_show_interfaces_raw.py b/lab-1/1_netmiko_lldp_neighbors_raw.py similarity index 78% rename from 1-reading-network-configuration/workshop_exercise/1_netmiko_show_interfaces_raw.py rename to lab-1/1_netmiko_lldp_neighbors_raw.py index 81f64b7..cc82ee9 100644 --- a/1-reading-network-configuration/workshop_exercise/1_netmiko_show_interfaces_raw.py +++ b/lab-1/1_netmiko_lldp_neighbors_raw.py @@ -1,11 +1,11 @@ # pip install --user netmiko from netmiko import Netmiko -username = "fill me in!" -password = "fill me in!" -device_type = "fill me in!" -hosts = ["x.x.x.x", "y.y.y.y", "z.z.z.z"] -command_to_run = "show lldp neighbors" +username = "fill me in!" # TODO +password = "fill me in!" # TODO +device_type = "fill me in!" # TODO +hosts = ["x.x.x.x", "y.y.y.y", "z.z.z.z"] # TODO +command_to_run = "show lldp neighbors detail" # TODO for host in hosts: # Create a variable that represents an SSH connection to our router. diff --git a/1-reading-network-configuration/answers/exercise1/2_netmiko_show_interfaces_textfsm.py b/lab-1/2_netmiko_lldp_neighbors_textfsm.py similarity index 78% rename from 1-reading-network-configuration/answers/exercise1/2_netmiko_show_interfaces_textfsm.py rename to lab-1/2_netmiko_lldp_neighbors_textfsm.py index 4d3c919..73bd751 100644 --- a/1-reading-network-configuration/answers/exercise1/2_netmiko_show_interfaces_textfsm.py +++ b/lab-1/2_netmiko_lldp_neighbors_textfsm.py @@ -3,11 +3,11 @@ import json from netmiko import Netmiko -username = "clab" -password = "clab@123" -device_type = "cisco_xr" -hosts = ["172.16.30.2", "172.16.30.3"] -command_to_run = "show int brief" +username = "fill me in!" # TODO +password = "fill me in!" # TODO +device_type = "fill me in!" # TODO +hosts = ["x.x.x.x", "y.y.y.y", "z.z.z.z"] # TODO +command_to_run = "show lldp neighbors detail" # TODO for host in hosts: # Create a variable that represents an SSH connection to our router. @@ -25,6 +25,6 @@ print(textfsm_output) print("\n") # Add extra space between our outputs for each host - print(f"### This is the TextFSM output from {host}, but JSON-formatted: ###") + print(f"### This is the TextFSM output from {host}, but JSON-formatted to be prettier: ###") print(json.dumps(textfsm_output, indent=4)) # indent for readability print("\n") # Add extra space between our outputs for each host diff --git a/1-reading-network-configuration/README.md b/lab-1/README.md similarity index 100% rename from 1-reading-network-configuration/README.md rename to lab-1/README.md diff --git a/lab-2/3_netmiko_update_description_to_lldp_neighbors.py b/lab-2/3_netmiko_update_description_to_lldp_neighbors.py new file mode 100644 index 0000000..8bb014a --- /dev/null +++ b/lab-2/3_netmiko_update_description_to_lldp_neighbors.py @@ -0,0 +1,25 @@ +# pip install --user textfsm +# pip install --user netmiko +import json +from netmiko import Netmiko + +username = "clab" +password = "clab@123" +device_type = "ios_xr" +hosts = ["172.16.x.2", "172.16.x.3", "172.16.x.4"] # TODO +command_to_run = "show lldp neighbors detail" # TODO + +for host in hosts: + # Create a variable that represents an SSH connection to our router. + connection = Netmiko( + username=username, + password=password, + device_type=device_type, + ip=host, + ) + + # Send a command to the router, and get back the output "dictionaried" by textfsm. + textfsm_output = connection.send_command(command_to_run, use_textfsm=True) + + # We know that the output looks like this: + # OUTPUT HERE diff --git a/lab-2/4_netmiko_seek_helper_addrs.py b/lab-2/4_netmiko_seek_helper_addrs.py new file mode 100644 index 0000000..4f547a4 --- /dev/null +++ b/lab-2/4_netmiko_seek_helper_addrs.py @@ -0,0 +1,48 @@ +# pip install --user netmiko +from netmiko import Netmiko +from ciscoconfparse import CiscoConfParse + +username = "clab" +password = "clab@123" +device_type = "ios_xr" +hosts = ["172.16.x.2", "172.16.x.3", "172.16.x.4"] # TODO +target_ip_helper = "fill me in!" + +for host in hosts: + # Create a variable that represents an SSH connection to our router. + connection = Netmiko( + username=username, password=password, device_type=device_type, ip=host + ) + + # Get the output for "show run". This will be raw and unformatted. + raw_config = connection.send_command("show run") + + # Turn this giant singular string of output into a list of lines. + parser = CiscoConfParse(raw_config.split("\n")) + + for intf in parser.find_objects("^interface .*"): + # Retrieve the helper address, if it exists. + helper_address_line = intf.re_search_children("^ ipv4 helper-address") + if not helper_address_line: + # Nothing to see here! Skip. + # Don't configure a new IP helper on interfaces that don't have one. + continue + + # Get the interface name. + intf_name = intf.text + + # Get the last "word" in the line, which is the helper IP address. + ip = helper_address_line.split()[-1] + + if ip != target_ip_helper: + commands = [ + intf_name, + f"no ipv4 helper-address {ip}", + f"ipv4 helper-address {target_ip_helper}" + ] + # Let's observe: + print(f"Running on {host}: {commands}") + output = connection.send_config_set(commands) + print(output) + +print("Done!")