importpicklefromnetaddrimportIPAddress,IPNetworkfrombase_objectsimportSwitchfromgeneric_vm_objectsimportGenericRouterfromfirewheel.control.experiment_graphimportrequire_classconfig={"vyos_system_username":"vyos","vyos_system_password":"vyos","interfaces_duplex":"auto","interfaces_smp_affinity":"auto","snmp_community":"public","default_lease_time":"infinite","ssh_port":"22","ssh_version":"2","netflow_version":"5",# 9 or 5"netflow_sampling_rate":"1024",# if needed (due to too much traffic)# then uncomment the sampling-rate block in# vyosconfig.py or ciscoconfig.py"netflow_expiry_interval":"10","netflow_flow_generic":"10","netflow_icmp":"10","netflow_max_active_life":"10","netflow_tcp_fin":"10","netflow_tcp_generic":"10","netflow_tcp_rst":"10","netflow_udp":"10","control_ip_network_base":"172","default_out_firewall_name":"fw_default_out",}
[docs]@require_class(GenericRouter)classVyOSRouter:""" This object provides some generic functionality that is common to all versions of VyOS virtual router operating system If the router isn't already a GenericRouter this will fail on ``__init__`` arguments missing. """def__init__(self,name=None):""" Initialize the VM as a VyOS router. This schedules dropping the VyOS config (as a callback function) and configuring the system. Args: name (str, optional): The name of the VM. Defaults to None. Raises: NameError: If the Vertex does not have a name. """self.name=getattr(self,"name",name)ifnotself.name:raiseNameError("Must specify name for VyOS router!")self.vyos_config_class=getattr(self,"vyos_config_class",None)self._firewall_policies=Noneself.assign_firewall_policies({})configuration_location="/opt/vyatta/etc/config/firewheel-config.sh"# Drop the configuration file on the router.# This is done by supplying a callback to the ScheduleEntry.# This will be called when the schedule is being generated to be uploadedself.drop_content(-100,configuration_location,self._configure_vyos,executable=True)remap_config={"configuration_location":configuration_location}self.add_vm_resource(-97,"interface_remap.py",pickle.dumps(remap_config,protocol=0).decode(),None,)# Run the configuration script to actually configure the routerself.run_executable(-95,f"/bin/vbash {configuration_location}")
[docs]defadd_default_profiles(self):""" Adds default ssh keys, .bashrc, .vimrc, etc. to both the ``root`` and ``vyos`` user. """# rootself.drop_file(-249,"/root/combined_profiles.tgz","combined_profiles.tgz")self.run_executable(-248,"chown","-R root:root /root/combined_profiles.tgz",vm_resource=False)self.run_executable(-247,"tar","--no-same-owner -C /root/ -xf /root/combined_profiles.tgz")self.run_executable(-246,"rm","-f /root/combined_profiles.tgz")# vyosself.drop_file(-249,"/home/vyos/combined_profiles.tgz","combined_profiles.tgz")self.run_executable(-248,"chown","-R vyos:vyos /home/vyos/combined_profiles.tgz",vm_resource=False,)self.run_executable(-247,"su",'vyos -c "tar -C /home/vyos -xf /home/vyos/combined_profiles.tgz"',)self.run_executable(-246,"rm","-f /home/vyos/combined_profiles.tgz")
[docs]defassign_firewall_policies(self,policies):""" Assign firewall policies/rules to the router. Args: policies (dict): A mapping between policy categories and a list containing rule sets and/or groups that apply to the policy category. Keys be a subset of ``{"in", "out", "local"}`` and values must be a list of :py:class:`VyOSConfigItem` objects. """VyOSRouter._validate_firewall_policies(policies)self._firewall_policies=policies# Set default policy values (if not otherwise specified)def_out_fw=VyOSConfigItem("name",config["default_out_firewall_name"])def_out_fw.add_children(VyOSConfigItem("default-action","accept"))self._firewall_policies.setdefault("out",[def_out_fw])
[docs]@staticmethoddef_validate_firewall_policies(policies):permissible_keys=["in","out","local"]ifnotall(_inpermissible_keysfor_inpolicies.keys()):raiseValueError("The only valid policy categories are 'in', 'out', and 'local'.")ifnotall(isinstance(_,list)for_inpolicies.values()):raiseTypeError("The values for each policy must be a `list`.")forvaluesinpolicies.values():ifnotall(isinstance(_,VyOSConfigItem)for_invalues):raiseTypeError("Groups and Rule sets must be provided as instances of ""`VyOSConfigItem` objects.")
[docs]def_configure_vyos(self):""" Configure VyOS by setting interfaces, OSPF, BGP, DHCP, etc. Returns: str: The configuration script as a string. Raises: RuntimeError: If the ``self.vyos_config_class`` is not an instance of :py:class:`vyos.VyOSConfiguration`. """# The specific instance of a :py:class:`vyos.VyOSConfiguration`# to use. Defaults to :py:class:`vyos.VyOSConfiguration`.ifself.vyos_config_classisNone:self.vyos_config_class=VyOSConfiguration()ifnotisinstance(self.vyos_config_class,VyOSConfiguration):raiseRuntimeError(f"The `vyos_config_class` attribute {self.vyos_config_class} of this ""`VyOSRouter` object must be an instance of `VyOSConfiguration`")self.vc=self.vyos_config_classself.vc.set_router_interfaces(self.interfaces.interfaces,self._firewall_policies)try:self.vc.set_router_ospf(self.routing)self.vc.set_router_bgp(self.routing)self.vc.set_router_static(self.routing)exceptAttributeError:self.log.debug("VyOS was not configured for routing")self.vc.set_firewall(rule_sets=self._firewall_policies.values())self.vc.set_service()self.vc.set_system(self.name)try:self._resolve_nat()self.vc.set_nat(self.nat)exceptAttributeError:self.log.debug("VyOS was not configured for NAT")self._configure_dhcp()# If the router is configured for netflow then# enable forwarding to a collectortry:ifself.netflow:self.vc.create_flow_accounting(self.netflow)exceptAttributeError:self.log.debug("VyOS was not configured for netflow.")configuration=self.vc.build_configuration_script()returnconfiguration
[docs]def_resolve_nat(self):""" Resolve a NAT block so all specifications use the detailed syntax and interfaces refer to names, not nat-labels. This is needed for the configuration generation to function properly. """self._resolve_nat_simplified_syntax()self._resolve_nat_interfaces()
[docs]def_resolve_nat_interfaces(self):""" Resolve a NAT block so all interface references use name, not nat-label. Assumes all rules are already use detailed syntax. Raises: ValueError: If there is an invalid NAT rule. """try:ifnotself.nat:returnexceptAttributeError:returnforruleinself.nat:if"interface"inrule:forifaceinself.interfaces.interfaces:if"nat-label"inifaceandiface["nat-label"]==rule["interface"]:rule["interface"]=iface["name"]continueelse:# All rules should be resolved to full syntax by now--that means# even with the simplified syntax they should have a 'type'# field.if"type"notinrule:raiseValueError('NAT rule on %s is missing required "type" value. ''Please specify either "source" or "destination". Rule: %s'%(self.name,rule))ifrule["type"]=="source":forifaceinself.interfaces.interfaces:if("address"inrule["translation"]andrule["translation"]["address"]!="masquerade"):# Setting the NAT access address to the local# interface address doesn't do much good.# We instead check that the address given for NAT is# in the same subnet as an interface's address.iface_subnet=IPNetwork(f"{iface['address']}/{iface['netmask']}")nat_addr=IPAddress(rule["translation"]["address"])ifnat_addriniface_subnet:rule["interface"]=iface["name"]continueelif("subnet"inrule["translation"]andrule["translation"]["subnet"]):nat_subnet=IPNetwork(rule["translation"]["subnet"])iface_addr=IPAddress(iface["address"])ififace_addrinnat_subnet:rule["interface"]=iface["name"]continueelifrule["type"]=="destination":forifaceinself.interfaces:if"address"inrule["access"]andrule["access"]["address"]:iface_subnet=IPNetwork(f"{iface['address']}/{iface['netmask']}")nat_addr=IPAddress(rule["access"]["address"])ifnat_addriniface_subnet:rule["interface"]=iface["name"]continueelif"subnet"inrule["access"]andrule["access"]["subnet"]:nat_subnet=IPNetwork(rule["access"]["subnet"])iface_addr=IPAddress(iface["address"])ififace_addrinnat_subnet:rule["interface"]=iface["name"]continue
[docs]def_resolve_nat_simplified_syntax(self):""" Resolve a NAT block so all rules use the detailed syntax. Raises: ValueError: If there is an invalid NAT rule. """try:ifnotself.nat:returnexceptAttributeError:returnnew_rules=[]forruleinself.nat:# Translate masquerade rules to detailed syntax.if"masquerade"inrule:rule["type"]="source"rule["translation"]={"address":"masquerade"}subnet_list=rule["subnet_list"]rule["access"]={}rule["access"]["subnet"]=f"{subnet_list[0]}"rule["interface"]=rule["masquerade"]iflen(subnet_list)>1:# Build additional rules.forcur_subnetinsubnet_list[1:]:new_rule={"type":"source","translation":{"address":"masquerade"},"access":{"subnet":f"{cur_subnet}"},"interface":rule["masquerade"],}new_rules.append(new_rule)# Translate port forward rules to detailed syntax.if"port forward"inrule:forportinrule["port forward"]:firstarr=port.split("/")orig_port=firstarr[0]try:protocol=firstarr[1]exceptIndexErrorasexp:raiseValueError('Must specify incoming port as "port/protocol". Had "%s".'%port)fromexpvalarr=rule["port forward"][port].split(":")dest_addr=valarr[0]try:dest_port=valarr[1]exceptIndexErrorasexp:raiseValueError('Must specify the NAT destination as "address:port". Had "%s"'%rule["port forward"][port])fromexp# Find the interface that has the dest_addr.# If there is more than 1 other non-control addr,# generate more rules# Each rule: interface is other interface# my_addr is iface addresscontrol_net_names=self._find_control_network()dest_ip=IPAddress(dest_addr)src_ifaces=[]forifaceinself.interfaces:iface_subnet=IPNetwork(f"{iface['address']}/{iface['netmask']}")ifdest_ipiniface_subnet:my_addr=iface["address"]elififace["name"]notincontrol_net_names:src_ifaces.append({"name":iface["name"],"address":iface["address"]})iflen(src_ifaces)==0:# We couldn't determine a source interface for the rule.# This is a fatal error--the rule must specify an interface.# NAT processing could continue, but we have an invalid rule here.raiseValueError("Invalid NAT rule on %s--cannot determine source interface. Rule: %s"%(self.name,rule))iface=src_ifaces[0]["name"]rule["type"]="destination"rule["translation"]={"address":dest_addr,"port":dest_port}rule["access"]={"address":my_addr,"port":orig_port}rule["interface"]=ifacerule["protocol"]=protocoliflen(src_ifaces)>1:# Build additional rules.forcur_ifaceinsrc_ifaces[1:]:new_rule={"type":"destination","translation":{"address":dest_addr,"port":dest_port,},"access":{"address":my_addr,"port":orig_port},"interface":cur_iface["name"],"protocol":protocol,}new_rules.append(new_rule)iflen(new_rules)>0:forruleinnew_rules:self.nat.append(rule)
[docs]def_configure_dhcp(self):""" Determine if and where we need to run a DHCP server and find the necessary info to do so in the graph. Build the configuration entries for this. """dhcp_config={}forifaceinself.interfaces.interfaces:if"dhcp"inifaceandiface["dhcp"]isTrue:net=str(iface["network"])net_name=iface["switch"].namedhcp_config[net_name]={"authoritative":True}dhcp_config[net_name][net]={}switch=iface["switch"]ifswitch.get("dns1"):dhcp_config[net_name][net]["dns1"]=switch["dns1"]ifswitch.get("dns2"):dhcp_config[net_name][net]["dns2"]=switch["dns2"]ifswitch.get("default_gw"):dhcp_config[net_name][net]["gateway"]=switch["default_gw"]# Ignore domain for now# This is in seconds. Cisco will need to convert to daysdhcp_config[net_name][net]["lease"]=262144ipnet=iface["network"]start=str(IPAddress(ipnet.first+1))stop=str(IPAddress(ipnet.last-1))dhcp_config[net_name][net]["range"]=(start,stop)dhcp_config[net_name][net]["static-mapping"]=(self._configure_dhcp_mappings(switch,self.name))self.log.debug("DHCP static-mapping for %s: %s",switch.name,dhcp_config[net_name][net]["static-mapping"],)self.vc.set_dhcp_service(dhcp_config)
[docs]def_configure_dhcp_mappings(self,switch,host_to_ignore):""" Build a list of static mappings for IP addresses based on hosts connected to the network. Args: switch (base_objects.Switch): Switch for the network for which we are building the list. host_to_ignore (str): A name of a host for which we should ignore mapping. Returns: dict: A dictionary in the correct format for the 'static-mapping' field of a DHCP configuration. Raises: RuntimeError: The given switch was not decorated as a :py:class:`base_objects.Switch`. """hosts={}ifnotswitch.is_decorated_by(Switch):raiseRuntimeError("The given switch was not decorated as a `Switch`.")neighbors=switch.get_neighbors()forneighborinneighbors:ifneighbor.name==host_to_ignore:continue# A switch could have non-host neighbors (e.g. SwitchBridge),# depending when exactly we run.try:forifaceinneighbor.interfaces.interfaces:ififace["switch"]==switch:hosts[neighbor.name]={"ip":iface["address"],"netmask":iface["netmask"],"mac":iface["mac"],}exceptAttributeError:self.log.warning('Tried to access interfaces on "%s", but they don\'t exist.',neighbor.name,)returnhosts
[docs]classVyOSConfiguration:""" Create configuration files for VyOS routers. Each OS may need minor differences in the command syntax, so this class and the methods within should be inherited extend functionality as needed. """config=""root=None
[docs]def__init__(self):""" Constructor. Creates a root node that can be stored in the graph """self.root=VyOSConfigItem("root")
[docs]defget_configuration_root(self):""" Returns the root config item so that all the VyOSConfigItems can be stored in the graph. Returns: vyos.VyOSConfigItem: The root configuration item. """returnself.root
[docs]defbuild_configuration_script(self):""" Generate the configuration script. This is called after all router attributes have been set. This traverses the tree to build the config and then returns the resulting configuration script Returns: str: The newly created configuration script. """# Add the required setup at the top of the config scriptconfig=""config+=("#!/bin/vbash\n""\n""su vyos\n""source /opt/vyatta/etc/functions/script-template\n""\n")# The following loop was put in place due to the commit sporadically# failing due to being unable to acquire a write lock causing the# Vyos router to end in an non-configured state breaking experiments.## The key fix needed was checking based on the `vyatta_cli_shell_api`# as other attempts such as looking at the error code from `commit`# were unsuccessful. The idea was sourced from the `vyatta-cfg` source# code at https://github.com/vyos/vyatta-cfg/blob/equuleus/functions/interpreter/vyatta-cfg-run#L106## The re-try is not very clean in the logs as you will see many warnings# that the configuration has already been set from the previous loop# execution, however it does end up working and that's the desired end result.config+=("COMMIT_FAILURE=1\n""\n""until (( ! $COMMIT_FAILURE )); do\n""configure\n""\n")forchildinself.root.children:child_commands=child.generate_commands("",[])forcmdinchild_commands:config+=f"set {cmd}\n"config+="\n"config+=("commit\n""\n""if ! vyatta_cli_shell_api sessionChanged; then\n"'echo "Commit succeeded, continuing"\n'"COMMIT_FAILURE=0\n""else\n"'echo "Commit failed, restarting router to re-try"\n'"sudo /etc/init.d/vyatta-router restart\n"'echo "Sleeping for 120 seconds to allow router to restart"\n'"sleep 120\n"'echo "Done sleeping"\n'"fi\n""\n""done\n""\n""save\n""exit\n""exit\n")config+="sudo chown -R root:vyattacfg /opt/vyatta/config/active\n"config+="sudo chown -R root:vyattacfg /opt/vyatta/etc/quagga/\n"returnconfig
[docs]defset_system(self,hostname):""" Creates (if necessary) and sets the 'system' block in the configuration file. This is the main function for creating the 'system' block. A sample 'system' block looks like: :: system { host-name subnet-0-amsterdam-Rtr-0 login { user vyos { authentication { plaintext-password vyos } level admin } } } Arguments: hostname (str): The name of the router """# Check to see if there is already a system block createdsystem=self.root.find("system")ifnotsystem:# no system block exists, create onesystem=VyOSConfigItem("system")self.root.add_children(system)# hostname can not have dots or underscores, replace them# with hyphenshostname=hostname.replace(".","-").replace("_","-")name=VyOSConfigItem("host-name",hostname)system.add_children(name)# create the 'login' blockself.create_system_login()
[docs]defcreate_flow_accounting(self,router):""" Configures flow accounting for the router Arguments: router(dict): The router to add flow accounting to """system=self.root.find("system")ifnotsystem:# no system block exists, create onesystem=VyOSConfigItem("system")self.root.add_children(system)flow_accounting=VyOSConfigItem("flow-accounting")system.add_children(flow_accounting)interfaces=router["interfaces"]# create a 'flow-accounting' block for each interfaceforifaceininterfaces:# Don't configure netflow on control planeififace["address"].startswith(config["control_ip_network_base"])oriface["address"].startswith("0.0.0.0"# noqa: S104):continue# Specify which interface for flow-accountinginterface=VyOSConfigItem("interface",iface["name"])flow_accounting.add_children(interface)# Create block for netflownetflow=self.create_netflow(router["netflow_collector_ip"],router["netflow_collector_port"],router["netflow_engine_id"],)flow_accounting.add_children(netflow)
[docs]defcreate_netflow(self,collector_ip,collector_port,engine_id):""" Configures netflow for the router Arguments: collector_ip (str): The IP address of the collector collector_port (str): The port the collector runs on engine_id (str): The netflow engine ID Returns: vyos.VyOSConfigItem: The netflow configuration block. """netflow=VyOSConfigItem("netflow")# Add all fields inside the netflow block.# All are configurable in the configversion=VyOSConfigItem("version",config["netflow_version"])netflow.add_children(version)eid=VyOSConfigItem("engine-id",engine_id)netflow.add_children(eid)server=VyOSConfigItem("server",collector_ip)port=VyOSConfigItem("port",collector_port)server.add_children(port)netflow.add_children(server)timeout=VyOSConfigItem("timeout")netflow.add_children(timeout)expiry_interval=VyOSConfigItem("expiry-interval",config["netflow_expiry_interval"])timeout.add_children(expiry_interval)flow_generic=VyOSConfigItem("flow-generic",config["netflow_flow_generic"])timeout.add_children(flow_generic)icmp=VyOSConfigItem("icmp",config["netflow_icmp"])timeout.add_children(icmp)max_active_life=VyOSConfigItem("max-active-life",config["netflow_max_active_life"])timeout.add_children(max_active_life)tcp_fin=VyOSConfigItem("tcp-fin",config["netflow_tcp_fin"])timeout.add_children(tcp_fin)tcp_rst=VyOSConfigItem("tcp-rst",config["netflow_tcp_rst"])timeout.add_children(tcp_rst)tcp_generic=VyOSConfigItem("tcp-generic",config["netflow_tcp_generic"])timeout.add_children(tcp_generic)udp=VyOSConfigItem("udp",config["netflow_udp"])timeout.add_children(udp)returnnetflow
[docs]defcreate_system_login(self):""" Creates the 'login' block which is nested inside the 'system' block Raises: IncorrectDefinitionOrderError: Must set system hostname before setting the system login. """# find the 'system' blocksystem=self.root.find("system")ifnotsystem:# 'system' block has not been created and needs to be before moving# forward.raiseIncorrectDefinitionOrderError("Must set system hostname before setting the system login")# create the 'login' blocklogin=VyOSConfigItem("login")system.add_children(login)# create the 'user' blockuser=VyOSConfigItem("user",config["vyos_system_username"])login.add_children(user)# create the 'authentication' blockauthentication=VyOSConfigItem("authentication")user.add_children(authentication)# set the user's passwordpassword=VyOSConfigItem("plaintext-password",config["vyos_system_password"])authentication.add_children(password)
[docs]defset_service(self):""" Creates (if necessary) the 'service' block of the vyos configuration file. This is the main function for creating this block. A sample 'service' block looks like: :: service { ssh { allow-root port 22 protocol-version v2 } snmp { community public } } """# create the service blockservice=self.root.find("service")ifnotservice:# No service block was found, create oneservice=VyOSConfigItem("service")self.root.add_children(service)# Create 'ssh' blockself.create_ssh_service()# Create 'snmp' blockself.create_snmp_service()
[docs]defcreate_snmp_service(self):""" Creates the 'snmp' block nested in the 'service' block Raises: IncorrectDefinitionOrderError: Must set snmp service through the :py:meth:`vyos.VyOSConfiguration.set_service` method. """service=self.root.find("service")ifnotservice:# 'service' block was not found, force declaration of the 'snmp'# service block through the set_service functionraiseIncorrectDefinitionOrderError("Must set snmp service through the set_service function")snmp=service.find("snmp")ifnotsnmp:snmp=VyOSConfigItem("snmp")service.add_children(snmp)# create 'community' parameter for snmpcommunity=VyOSConfigItem("community",config["snmp_community"])snmp.add_children(community)
[docs]defcreate_ssh_service(self):""" Create 'ssh' block which is nested in the 'service' block Raises: IncorrectDefinitionOrderError: Must set ssh service through the :py:meth:`vyos.VyOSConfiguration.set_service` method. """service=self.root.find("service")ifnotservice:# 'service' block was not found, force declaration of the 'ssh'# service block through the set_service functionraiseIncorrectDefinitionOrderError("Must set ssh service through the set_service function")ssh=service.find("ssh")ifnotssh:ssh=VyOSConfigItem("ssh")service.add_children(ssh)# create port parameterport=VyOSConfigItem("port",config["ssh_port"])ssh.add_children(port)
[docs]defset_dhcp_service(self,network_info):""" Creates the 'dhcp-server' block which is nested in the 'service' block. Arguments: network_info(dict): Dictionary describing DHCP parameters (IP, CIDR as string): :: { <network name>: { 'authoritative': <bool> <cidr>: { 'gateway': <ip>, 'dns1': <ip>, 'dns2': <ip>, 'domain': <string>, 'lease': <int>, 'range': (<ip>, <ip>), 'static-mapping': { <hostname>: { 'ip': <ip>, 'mac': <mac> }, ... } }, ... }, ... } """# If we have nothing to configure, don't do anything.iflen(network_info.keys())==0:returnservice=self.root.find("service")ifnotservice:# No service block was found, create oneservice=VyOSConfigItem("service")self.root.add_children(service)dhcp=service.find("dhcp-server")ifnotdhcp:dhcp=VyOSConfigItem("dhcp-server")service.add_children(dhcp)# Make sure DHCP is enabled.disabled=VyOSConfigItem("disabled","false")dhcp.add_children(disabled)fornet_nameinnetwork_info:network=VyOSConfigItem("shared-network-name",net_name)dhcp.add_children(network)if"authoritative"notinnetwork_info[net_name]:# Assume not authoritative.ifnetwork_info[net_name]["authoritative"]isTrue:auth_str="enable"else:auth_str="disable"authoritative=VyOSConfigItem("authoritative",auth_str)network.add_children(authoritative)forsubnet_cidrinnetwork_info[net_name]:ifsubnet_cidr=="authoritative":continuesubnet=VyOSConfigItem("subnet",subnet_cidr)network.add_children(subnet)if"gateway"innetwork_info[net_name][subnet_cidr]:gateway=VyOSConfigItem("default-router",network_info[net_name][subnet_cidr]["gateway"])subnet.add_children(gateway)if"dns1"innetwork_info[net_name][subnet_cidr]:dns1=VyOSConfigItem("dns-server",network_info[net_name][subnet_cidr]["dns1"])subnet.add_children(dns1)if"dns2"innetwork_info[net_name][subnet_cidr]:dns2=VyOSConfigItem("dns-server",network_info[net_name][subnet_cidr]["dns2"])subnet.add_children(dns2)if"domain"innetwork_info[net_name][subnet_cidr]:domain_name=VyOSConfigItem("domain-name",network_info[net_name][subnet_cidr]["domain"])subnet.add_children(domain_name)if"lease"innetwork_info[net_name][subnet_cidr]:lease=VyOSConfigItem("lease",network_info[net_name][subnet_cidr]["lease"])subnet.add_children(lease)if"range"innetwork_info[net_name][subnet_cidr]:range_start=VyOSConfigItem("start",network_info[net_name][subnet_cidr]["range"][0])range_end=VyOSConfigItem("stop",network_info[net_name][subnet_cidr]["range"][1])range_start.add_children(range_end)subnet.add_children(range_start)if"static-mapping"innetwork_info[net_name][subnet_cidr]:forhostinnetwork_info[net_name][subnet_cidr]["static-mapping"]:static=VyOSConfigItem("static-mapping",host)subnet.add_children(static)ip=VyOSConfigItem("ip-address",network_info[net_name][subnet_cidr]["static-mapping"][host]["ip"],)static.add_children(ip)mac=VyOSConfigItem("mac-address",network_info[net_name][subnet_cidr]["static-mapping"][host]["mac"],)static.add_children(mac)
[docs]defset_router_interfaces(self,ifaces,firewall_policies):""" Creates (if necessary) the router's interfaces. Accomplishes this by creating the 'interfaces' block followed by the 'ethernet' block, which has several block nested in itself. A sample 'interfaces' block looks like:: interfaces { ethernet eth0 { address 172.16.0.2/14 duplex auto smp_affinity auto } ethernet eth1 { address 62.58.99.2/24 duplex auto smp_affinity auto ip { ospf { dead-interval 40 hello-interval 10 retransmit-interval 5 transmit-delay 1 } } } } Arguments: ifaces (dict): Double dictionary containing the interface information for the router. Structure is defined as: :: interface number (int): 'name' (i.e. eth0) 'address' (i.e. 192.168.1.2) 'netmask' (i.e. 255.255.255.0) firewall_policies (dict): A mapping between the firewall policy category and associated rule set (each set is a :py:class:`VyOSConfigItem` object). # noqa: DAR101 firewall_policies # - required because newlines are required by RST but break # :spelling:ignore:`darglint` # (see https://github.com/terrencepreilly/darglint/issues/120) """# Get interfaces block, most likely isn't created yetinterfaces=self.root.find("interfaces")ifnotinterfaces:# does not exist yet, create itinterfaces=VyOSConfigItem("interfaces")self.root.add_children(interfaces)# loop through all interfaces specified in the ifaces structure# and define each interface in the router configurationforifaceinifaces:# Create an ethernet blockethernet=VyOSConfigItem("ethernet",iface["name"])interfaces.add_children(ethernet)# Get the network address in CIDR notationaddress=Noneififace["address"]!="0.0.0.0":# noqa: S104network_address=IPNetwork(f"{iface['address']}/{iface['netmask']}")# create the address parameteraddress=VyOSConfigItem("address",str(network_address))# create the hardware id (MAC address) for this interfacehwid=VyOSConfigItem("hw-id",iface["mac"])# create the firewall name blockfirewall=VyOSConfigItem("firewall")forcategory,rule_setsinfirewall_policies.items():policy_fw=VyOSConfigItem(category)forrule_setinrule_sets:rule_set_name=rule_set.valuepolicy_fw.add_children(VyOSConfigItem("name",rule_set_name))firewall.add_children(policy_fw)# Add all the childrenifaddress:ethernet.add_children(address)ethernet.add_children(hwid,firewall)
[docs]defadd_quality_of_service(self,iface):"""Adds quality of service blocks for all interfaces with QoS. QoS traffic shapers are named according to the router interface. Currently, the bandwidth can be restricted to a maximum value, but the traffic shapers offer more advanced options, such as different types of queue scheduling. Args: iface (dict): The interface to add QoS configs Returns: VyOSConfigItem: The VyOSConfigItem to add to the ``iface`` """ifnot(iface.get("bandwidth")):returnNoneqos_policy=self.root.find("qos-policy")ifnotqos_policy:# does not exist yet, create itqos_policy=VyOSConfigItem("qos-policy")self.root.add_children(qos_policy)bandwidth=iface["bandwidth"]policy_name="qos"+iface["name"].strip()iface_config=VyOSConfigItem("qos-policy","{out "+policy_name+"}")qos_config=VyOSConfigItem("traffic-shaper",policy_name)qos_policy.add_children(qos_config)bandwidth_config=VyOSConfigItem("bandwidth",bandwidth)ceiling_config=VyOSConfigItem("ceiling",bandwidth)default_config=VyOSConfigItem("default","{bandwidth 100%}")qos_config.add_children(bandwidth_config,ceiling_config,default_config)returniface_config
[docs]defset_router_ospf(self,routing):""" Defines OSPF information in the correct blocks in the vyos configuration. This is the main function for specifying all OSPF information for the router. The OSPF information comes in through the OSPF structure. The OSPF structure is defined as: :: interface number (integer): 'name' (i.e. eth0) 'status' (i.e. Enabled) 'area' (i.e. 0) 'hello-interval' (i.e. 10) 'transmit-delay' (i.e. 1) 'retransmit-interval' (i.e. 5) 'dead-interval' (i.e. 40) The redistribute structure specifies which links will be redistributing BGP information over the OSPF link. The redistribute structure is defined as: - status -- Enabled or Disabled, specifies if redistribution is active - metric -- the weight specified for the link - metric-type -- specifies how cost is calculated for the link - route-map -- route-map to be used when advertising the network Arguments: routing(dict): The routing information for this router. """if("ospf"notinroutingornotrouting["ospf"]or"interfaces"notinrouting["ospf"]ornotrouting["ospf"]["interfaces"]):returnospf=routing["ospf"]# add OSPF definitions to the 'interfaces' blockself.create_interfaces_ospf(ospf)# add OSPF definitions to the 'protocols' blockself.create_protocols_ospf(routing)
[docs]defset_router_static(self,routing):""" Defines static routing information Arguments: routing(dict): The routing information for this router """# If no static routing is configured, then just returnif"static"notinroutingornotrouting["static"]:returnprotocols=self.root.find("protocols")ifnotprotocols:protocols=VyOSConfigItem("protocols")self.root.add_children(protocols)static=protocols.find("static")ifnotstatic:static=VyOSConfigItem("static")protocols.add_children(static)forrouteinrouting["static"]:r=VyOSConfigItem("route",route)nh=VyOSConfigItem("next-hop",routing["static"][route])r.add_children(nh)static.add_children(r)
[docs]defset_router_bgp(self,routing):""" Defines the BGP information for the router. This requires the neighbor_info structure which is defined as: interface number (integer): 'address' (i.e. 192.168.1.4) 'as' (peer's AS number, i.e. 1044) The redistribute structure specifies information about which links will be redistributing OSPF information. The structure is defined as: status -- Enabled or Disabled, specifies if redistribution is active metric -- the weight specified for the link route-map -- route-map to be used when advertising the network Arguments: routing (dict): The routing information for this router. Raises: Exception: Must specify an AS when defining a BGP block. """# If BGP is not configured or there are no neighbors for this router# then just returnif("bgp"notinroutingornotrouting["bgp"]or"neighbors"notinrouting["bgp"]ornotrouting["bgp"]["neighbors"]):returnprotocols=self.root.find("protocols")ifnotprotocols:protocols=VyOSConfigItem("protocols")self.root.add_children(protocols)bgp=protocols.find("bgp")ifnotbgp:if("bgp"notinroutingornotrouting["bgp"]or"parameters"notinrouting["bgp"]or"router-as"notinrouting["bgp"]["parameters"]ornotrouting["bgp"]["parameters"]["router-as"]):raiseException("Cannot create BGP block without an AS specified")bgp=VyOSConfigItem("bgp",routing["bgp"]["parameters"]["router-as"])protocols.add_children(bgp)# BGP block exists, so fill it with neighbor informationneighbors=self.create_bgp_neighbors(routing["bgp"]["neighbors"])# Add all the neighbors to the treeforneighborinneighbors:bgp.add_children(neighbor)# Just redistribute connected instead of specific networks""" if bgp_networks: for network in bgp_networks.keys(): network_cidr = str(IPNetwork('%s/%s' % \ (bgp_networks[network]['address'], bgp_networks[network]['netmask'])).cidr) network = VyOSConfigItem('network', network_cidr) bgp.add_children(network) """# Make networks block in graph struct, anything in there should# be explicitly advertisedif"networks"inrouting["bgp"]:fornetinrouting["bgp"]["networks"]:# This should almost always be an IPNetwork Typeifnotisinstance(net,IPNetwork):ifnotisinstance(net,str):net_cidr=str(IPNetwork(f"{net['address']}/{net['netmask']}"))else:net_cidr=netelse:net_cidr=str(net)network=VyOSConfigItem("network",net_cidr)bgp.add_children(network)redistribute=self.create_protocols_bgp_redistribute_ospf(routing["bgp"])self.bgp_redistribute_ospf(redistribute,routing["bgp"])
[docs]defcreate_bgp_neighbors(self,neighbor_info):""" Loop the BGP peer information and create config item objects for each. Arguments: neighbor_info (dict): BGP peer information. Structure defined in comments in set_router_bgp() Returns: list: The list of all neighbors that were created. """neighbors=[]# Loop through all the neighborsforn_infoinneighbor_info:# Create the neighborneighbor=VyOSConfigItem("neighbor",n_info["address"])# Create the remote-as parameterremote_as=VyOSConfigItem("remote-as",n_info["remote-as"])neighbor.add_children(remote_as)neighbors.append(neighbor)# return all the neighbors just createdreturnneighbors
[docs]defcreate_interfaces_ospf(self,ospf):""" Add OSPF information to the 'interfaces' block. This requires the creation of an 'ip' block as well as an 'ospf' block nested inside the 'ip' block. See example in comments in set_router_interfaces() Arguments: ospf(dict): OSPF information. Structure defined in comments of set_router_ospf() Raises: IncorrectDefinitionOrderError: Must set router interfaces before setting its OSPF information. """# vyos configs have as ospf block inside the interfaces blockinterfaces=self.root.find("interfaces")ifnotinterfaces:raiseIncorrectDefinitionOrderError("Must set router interfaces before setting its OSPF information")# Loops through the ospf information adding an 'ip' block and# an 'ospf' block to each 'ethernet' block that defines an# interface that has OSPF enabledforifaceinospf["interfaces"]:# Get the ethernet block that pertains to what we're looking forethernet=interfaces.find("ethernet",iface)ifnotethernet:continue# ospf information is in an ip blockip=ethernet.find("ip")ifnotip:ip=VyOSConfigItem("ip")ethernet.add_children(ip)ospf_block=VyOSConfigItem("ospf")ip.add_children(ospf_block)if"dead-interval"inospf["interfaces"][iface]:dead_interval=VyOSConfigItem("dead-interval",int(float(ospf["interfaces"][iface]["dead-interval"])),)ospf_block.add_children(dead_interval)if"hello-interval"inospf["interfaces"][iface]:hello_interval=VyOSConfigItem("hello-interval",int(float(ospf["interfaces"][iface]["hello-interval"])),)ospf_block.add_children(hello_interval)if"retransmit-interval"inospf["interfaces"][iface]:retransmit_interval=VyOSConfigItem("retransmit-interval",int(float(ospf["interfaces"][iface]["retransmit-interval"])),)ospf_block.add_children(retransmit_interval)if"transmit-delay"inospf["interfaces"][iface]:transmit_delay=VyOSConfigItem("transmit-delay",int(float(ospf["interfaces"][iface]["transmit-delay"])),)ospf_block.add_children(transmit_delay)
[docs]defcreate_protocols_ospf(self,routing):""" Add OSPF information to the 'protocols' block. Arguments: routing(dict): The routing info for this router. """ospf=routing["ospf"]# OSPF is defined in the protocols blockprotocols=self.root.find("protocols")ifnotprotocols:protocols=VyOSConfigItem("protocols")self.root.add_children(protocols)# Create the 'ospf' block, nested in the 'protocols' blockospf_block=VyOSConfigItem("ospf")protocols.add_children(ospf_block)# Create and add areas to the OSPF blockareas=self.create_protocols_ospf_areas(ospf)forareainareas:ospf_block.add_children(area)if"parameters"notinrouting:print(routing)# Create Parameters blockparameters=VyOSConfigItem("parameters")ospf_block.add_children(parameters)rid=VyOSConfigItem("router-id",routing["parameters"]["router-id"])parameters.add_children(rid)# Create redistribute blockself.create_protocols_ospf_redistribute(ospf)
[docs]defcreate_protocols_ospf_redistribute(self,ospf):""" Create the 'redistribution' block that is nested inside of the 'protocols' block. Specifies which OSPF links redistribute. Arguments: ospf (dict): Specifies which OSPF links will be redistributing BGP information. Structure defined in comments of set_router_ospf() Raises: IncorrectDefinitionOrderError: Must specify OSPF information before specifying redistribution of BGP on OSPF links. """if"redistribution"notinospf:# not redistribting anything, nothing to doreturnprotocols=self.root.find("protocols")ifnotprotocols:raiseIncorrectDefinitionOrderError("Must specify OSPF information "+"before specifying redistribution "+"of BGP on OSPF links")ospf_block=protocols.find("ospf")ifnotospf_block:raiseIncorrectDefinitionOrderError("Must specify OSPF information "+"before specifying redistribution "+"of BGP on OSPF links")# create redistribute blockredistribute=ospf_block.find("redistribute")ifnotredistribute:redistribute=VyOSConfigItem("redistribute")ospf_block.add_children(redistribute)# Redistribute connected if necessaryif"connected"inospf["redistribution"]:redistribute_connected=VyOSConfigItem("connected")redistribute.add_children(redistribute_connected)# create 'bgp' block to be nested in 'redistribute' blockif"bgp"inospf["redistribution"]:bgp=VyOSConfigItem("bgp")redistribute.add_children(bgp)if"parameters"notinospf["redistribution"]["bgp"]:returnif"metric"inospf["redistribution"]["bgp"]["parameters"]:ifospf["redistribution"]["bgp"]["parameters"]["metric"]>16:metric_val=16else:metric_val=ospf["redistribution"]["bgp"]["parameters"]["metric"]metric=VyOSConfigItem("metric",metric_val)bgp.add_children(metric)if"metric-type"inospf["redistribution"]["bgp"]["parameters"]:type_val=1metric_type=VyOSConfigItem("metric-type",type_val)bgp.add_children(metric_type)if"route-map"inospf["redistribution"]["bgp"]["parameters"]:route_map=VyOSConfigItem("route-map",ospf["redistribution"]["bgp"]["parameters"]["route-map"],)bgp.add_children(route_map)
[docs]defbgp_redistribute_ospf(self,redistribute,bgp_config=None):""" Create the 'redistribution' block that is nested inside of the 'protocols' block. Specifies which BGP links redistribute OSPF information. Arguments: redistribute (VyOSConfigItem): The redistribute item or None if it does not exist. bgp_config (dict): Specifies which BGP links will be redistributing OSPF information. Structure defined in comments of set_router_ospf() """ifredistributeisNone:# no redistribution happening, nothing to doreturnredistribution=bgp_config["redistribution"]if"ospf"notinredistribution:returnospf=VyOSConfigItem("ospf")redistribute.add_children(ospf)if"parameters"inredistribution["ospf"]:parameters=redistribution["ospf"]["parameters"]ifparameters.get("metric"):ifparameters["metric"]>16:metric_val=16else:metric_val=parameters["metric"]metric=VyOSConfigItem("metric",metric_val)ospf.add_children(metric)ifparameters.get("route-map"):route_map=VyOSConfigItem("route-map",parameters["route-map"])ospf.add_children(route_map)
[docs]defcreate_protocols_bgp_redistribute_ospf(self,bgp_config):""" Create the 'redistribution' block that is nested inside of the 'protocols' block. Specifies which BGP links redistribute OSPF information. Arguments: bgp_config (dict): Specifies which BGP links will be redistributing OSPF information. Structure defined in comments of set_router_ospf() Returns: VyOSConfigItem: The "redistribute" configuration item that may need to be added elsewhere. Raises: IncorrectDefinitionOrderError: Must specify BGP information before specifying redistribution of OSPF on BGP links. """if"redistribution"notinbgp_config:# no redistribution happening, nothing to doreturnprotocols=self.root.find("protocols")ifnotprotocols:raiseIncorrectDefinitionOrderError("Must specify BGP information "+"before specifying redistribution "+"of OSPF on BGP links")bgp=protocols.find("bgp")ifnotbgp:raiseIncorrectDefinitionOrderError("Must specify BGP information "+"before specifying redistribution "+"of OSPF on BGP links")redistribute=bgp.find("redistribute")ifnotredistribute:redistribute=VyOSConfigItem("redistribute")bgp.add_children(redistribute)returnredistribute
[docs]defcreate_protocols_ospf_areas(self,ospf):""" Create the 'area' block that is nested inside the 'ospf' block which is nested inside the 'protocols' block. Specifies which area corresponds to which networks. Arguments: ospf (dict): OSPF information for each interface. Structure defined in comments of set_router_ospf() Returns: list: A list of dictionaries containing a mapping of OSPF areas to networks. """areas=[]# group all networks based on area idarea_networks=self.create_protocols_ospf_area_networks(ospf)# go through each area id, create an 'area' blockforareainarea_networks:# Create the area blockarea_block=VyOSConfigItem("area",area)# create a network block for each network in the areafornetworkinarea_networks[area]:net=VyOSConfigItem("network",network)area_block.add_children(net)areas.append(area_block)returnareas
[docs]defcreate_protocols_ospf_area_networks(self,ospf):""" Groups OSPF information by area id with each network that is specified for that id. Arguments: ospf (dict): OSPF information for each interface. Structure defined in comments of set_router_ospf() Returns: dict: The OSPF area dictionary. Raises: IncorrectDefinitionOrderError: If the router interfaces were not set before adding OSPF information. """area_networks={}# get active interfacesinterfaces=self.root.find("interfaces")ifnotinterfaces:raiseIncorrectDefinitionOrderError("Must set router interfaces "+"before setting its OSPF information")# Loop through ospf enabled interfacesforifaceinospf["interfaces"]:ethernet=interfaces.find("ethernet",iface)# only specify networks for active interfacesifethernet:# the interface has already been set with an address in# <IP Address>/<netmask> format, so don't need to specify# the prefix length to get the cidr addressaddress=IPNetwork(ethernet.find("address").value)ifospf["interfaces"][iface]["area"]notinarea_networks:area_networks[ospf["interfaces"][iface]["area"]]=[]area_networks[ospf["interfaces"][iface]["area"]].append(address.cidr)returnarea_networks
[docs]defset_firewall(self,rule_sets):""" Set the firewall parameters for this router Args: rule_sets (list): A list containing firewall configurations (groups, rule sets, etc.) being applied to the router (each item in a configuration is a :py:class:`VyOSConfigItem` object). """firewall=VyOSConfigItem("firewall")forconfiginrule_sets:forconfig_iteminconfig:firewall.add_children(config_item)# Enable sending redirects (default)firewall.add_children(VyOSConfigItem("send-redirects","disable"))self.root.add_children(firewall)
[docs]defset_nat(self,nat):""" Set up the NAT rules for this router. Arguments: nat (list): A list of NAT rules (in dictionary format). Raises: Exception: If there is an invalid NAT rule. """nat_root=VyOSConfigItem("nat")self.root.add_children(nat_root)source_rule_counter=0destination_rule_counter=0rule_increment=5source=Nonedestination=Noneforruleinnat:ifrule.get("type"):# Find the root node for the rule: source or destination NAT.ifrule["type"]=="source":ifsourceisNone:source=VyOSConfigItem("source")nat_root.add_children(source)source_rule_counter+=rule_incrementcur_rule=VyOSConfigItem("rule",str(source_rule_counter))source.add_children(cur_rule)# Set up the access block.source_filter=VyOSConfigItem("source")cur_rule.add_children(source_filter)if"address"inrule["access"]andrule["access"]["address"]:src_addr=VyOSConfigItem("address",rule["access"]["address"])source_filter.add_children(src_addr)elif"subnet"inrule["access"]andrule["access"]["subnet"]:src_addr=VyOSConfigItem("address",rule["access"]["subnet"])source_filter.add_children(src_addr)if"port"inrule["access"]andrule["access"]["port"]:src_port=VyOSConfigItem("port",rule["access"]["port"])source_filter.add_children(src_port)protocol=VyOSConfigItem("protocol",rule["protocol"])cur_rule.add_children(protocol)# Set up the translation block.translation=VyOSConfigItem("translation")cur_rule.add_children(translation)if("address"inrule["translation"]andrule["translation"]["address"]):trans_addr=VyOSConfigItem("address",rule["translation"]["address"])translation.add_children(trans_addr)elif("subnet"inrule["translation"]andrule["translation"]["subnet"]):trans_addr=VyOSConfigItem("address",rule["translation"]["subnet"])translation.add_children(trans_addr)# Set up the out-bound interface.ifrule.get("interface"):out_iface=VyOSConfigItem("outbound-interface",rule["interface"])cur_rule.add_children(out_iface)elifrule["type"]=="destination":ifdestinationisNone:destination=VyOSConfigItem("destination")nat_root.add_children(destination)destination_rule_counter+=rule_incrementcur_rule=VyOSConfigItem("rule",str(destination_rule_counter))destination.add_children(cur_rule)use_port=False# Set up the access block.dest=VyOSConfigItem("destination")cur_rule.add_children(dest)if"address"inrule["access"]andrule["access"]["address"]:dest_addr=VyOSConfigItem("address",rule["access"]["address"])dest.add_children(dest_addr)elif"subnet"inrule["access"]andrule["access"]["subnet"]:dest_addr=VyOSConfigItem("address",rule["access"]["subnet"])dest.add_children(dest_addr)if"port"inrule["access"]andrule["access"]["port"]:dest_port=VyOSConfigItem("port",rule["access"]["port"])dest.add_children(dest_port)use_port=True# Set up the translation block.translation=VyOSConfigItem("translation")cur_rule.add_children(translation)if("address"inrule["translation"]andrule["translation"]["address"]):trans_addr=VyOSConfigItem("address",rule["translation"]["address"])translation.add_children(trans_addr)elif("subnet"inrule["translation"]andrule["translation"]["subnet"]):trans_addr=VyOSConfigItem("subnet",rule["translation"]["subnet"])translation.add_children(trans_addr)if"port"inrule["translation"]andrule["translation"]["port"]:trans_port=VyOSConfigItem("port",rule["translation"]["port"])translation.add_children(trans_port)use_port=True# Se up the in-bound interface.ifrule.get("interface"):in_iface=VyOSConfigItem("inbound-interface",rule["interface"])cur_rule.add_children(in_iface)ifuse_portisTrue:protocol=VyOSConfigItem("protocol",rule["protocol"])cur_rule.add_children(protocol)else:raiseException("Invalid NAT rule type.")else:raiseException("Need to specify type.")
[docs]classVyOSConfigItem:""" Single configuration item that represents either a block or a parameter in the vyos configuration file. """
[docs]def__init__(self,name,value=None):""" Constructor. Arguments: name (str): The name of the block or parameter value (str, optional): Value for this block or parameter """self.name=name# Value is optional since some blocks only contain a nameself.value=valueifvalueelse""# Initialize variables to reference relatives in the treeself.parent=Noneself.children=[]
[docs]deffind(self,child_name,value=None):""" Search the children of this node to find the specified configuration item. Arguments: child_name (str): The name field for the desired config item value (str, optional): If specified then matches both the name and the item's value. Returns: vyos.VyOSConfigItem: The child being searched for, or None if one is not found. """forcinself.children:ifvalue:ifc.name==child_nameandc.value==value:returncelifc.name==child_name:returncreturnNone
[docs]defrecursive_find(self,child_name,value=None):""" Recursively search the children of this node to find the specified configuration item. Arguments: child_name (str): The name field for the desired config item value (str, optional): If specified then matches both the name and the item's value. Returns: vyos.VyOSConfigItem: The child being searched for, or None if one is not found. """test_list=[]forcinself.children:ifc.name==child_name:ifvalue:ifc.value==value:returncelse:returncelse:test_list.append(c)forcintest_list:res=c.recursive_find(child_name,value)ifresisnotNone:returnresreturnNone
[docs]defget_child_values(self,child_name):""" Search the children and get the values of all children that have the given name. Useful for getting all the interface names that have been declared since they are the values to 'ethernet' blocks. Arguments: child_name (str): The name field for the desired config item Returns: list: A list of values. """values=[]forcinself.children:ifc.name==child_name:values.append(c.value)returnvalues
[docs]defadd_children(self,*args):""" Add a child to this config item Arguments: *args (list): A list of :py:class:`vyos.VyOSConfigItem` objects. """forarginargs:self.children.append(arg)arg.parent=self
[docs]defgenerate_commands(self,base_command,commands):""" Generate the configuration commands for this item and all of its children. Then return commands back up to the parent to eventually be returned to the initial caller Arguments: base_command (str): The base command for a given item. commands (list): The list of commands. Returns: list: The list of commands being generated. """command=f"{self.name}"ifself.value:command+=f" {self.value}"# The new command needs the base (its parents)ifbase_command:command=f"{base_command}{command}"# The new base needs to include this new command moving forwardbase_command=commandiflen(self.children)>0:forchildinself.children:child.generate_commands(base_command,commands)else:commands.append(command)returncommands
[docs]classIncorrectDefinitionOrderError(Exception):""" Exception to specify that a value has been defined out of order. The message will specify what value was needed before the exception was thrown """