master-thesis/p4app/controller.py

526 lines
18 KiB
Python
Raw Normal View History

from __future__ import unicode_literals
2019-02-21 22:38:09 +00:00
import nnpy
import struct
from p4utils.utils.topology import Topology
from p4utils.utils.sswitch_API import SimpleSwitchAPI
2019-03-19 22:03:56 +00:00
from scapy.all import *
2019-03-04 17:05:56 +00:00
from scapy.data import ETHER_TYPES
2019-02-21 22:38:09 +00:00
import sys
import re
import logging
import argparse
2019-02-23 14:13:47 +00:00
import subprocess
import ipaddress
2019-02-21 22:38:09 +00:00
logging.basicConfig()
log = logging.getLogger("main")
2019-03-04 16:50:38 +00:00
cpu_fields = {
0: 'UNSET',
2019-03-04 16:50:38 +00:00
1: 'ICMP6_NS',
2: 'ICMP6_GENERAL',
3: 'DEBUG'
}
table_id_fields = {
0: 'UNSET_TABLE',
1: 'TABLE_NAT64',
2019-03-25 12:45:18 +00:00
2: 'TABLE_ICMP6',
3: 'TABLE_V6_NETWORKS',
4: 'TABLE_NAT46',
2019-03-31 13:48:00 +00:00
5: 'TABLE_V4_NETWORKS',
2019-04-02 15:13:05 +00:00
6: 'TABLE_ARP',
7: 'TABLE_ARP_EGRESS'
}
table_proto = {
'ICMP6_ECHO_REQUEST' : 128,
'ICMP6_ECHO_REPLY' : 129,
'ICMP6_NS' : 135,
'ICMP6_NA' : 136,
'ICMP_ECHO_REPLY' : 0,
'ICMP_ECHO_REQUEST' : 8
}
2019-03-31 13:48:00 +00:00
table_arp = {
'ARP_REQUEST': 1,
'ARP_REPLY': 2
}
class CpuHeader(Packet):
name = 'CpuPacket'
fields_desc = [
2019-03-04 17:16:18 +00:00
ShortEnumField('task', 1, cpu_fields ),
2019-03-04 17:05:56 +00:00
ShortField('ingress_port', 0),
2019-03-25 12:46:10 +00:00
XShortEnumField("type", 0x9000, ETHER_TYPES),
ShortEnumField('table_id', 1, table_id_fields )
]
2019-02-21 22:38:09 +00:00
class L2Controller(object):
def __init__(self, sw_name):
2019-02-23 13:36:19 +00:00
# Command line mapping
self.modes = ['base', 'router', "range_router" ]
2019-02-23 17:58:04 +00:00
2019-03-04 16:50:38 +00:00
# Reverse maps the cpu header
self.task = dict(reversed(item) for item in cpu_fields.items())
self.info={}
# https://en.wikipedia.org/wiki/Solicited-node_multicast_address
2019-03-04 14:30:38 +00:00
self.info['ndp_multicast'] = ipaddress.ip_network("ff02::1:ff00:0/104")
2019-03-31 13:56:55 +00:00
self.info['mac_addr'] = "00:00:0a:00:00:42"
self.info['mac_broadcast'] = "ff:ff:ff:ff:ff:ff"
2019-03-19 22:21:40 +00:00
self.info['ipv6_link_local'] = ipaddress.ip_address("fe80::200:aff:fe00:42")
2019-03-04 14:32:27 +00:00
self.info['v6_mask'] = 64
self.info['v6_nat64_mask'] = 96
2019-03-04 14:30:38 +00:00
self.info['v6_base'] = ipaddress.ip_network("2001:db8::/32")
2019-03-25 10:51:36 +00:00
self.info['v6_base_hostnet'] = ipaddress.ip_network("2001:db8::/48")
self.info['v6_gen'] = self.info['v6_base_hostnet'].subnets(new_prefix=self.info['v6_mask'])
2019-03-25 10:51:36 +00:00
# possible new range for NAT64 prefixes
self.info['v6_nat64_base'] = ipaddress.ip_network("2001:db8:1::/48")
2019-03-25 10:51:36 +00:00
# We reserve /64 (easier for reading), but only use /96
self.info['v6_nat64_gen'] = self.info['v6_nat64_base'].subnets(new_prefix=self.info['v6_mask'])
2019-03-04 14:32:27 +00:00
self.info['v4_mask'] = 24
2019-03-04 14:30:38 +00:00
self.info['v4_base'] = ipaddress.ip_network("10.0.0.0/8")
self.info['v4_gen'] = self.info['v4_base'].subnets(new_prefix=self.info['v4_mask'])
self.info['v4_nat64_base'] = ipaddress.ip_network("10.1.0.0/16")
self.info['v4_nat64_map'] = self.info['v4_nat64_base'].subnets(new_prefix=self.info['v4_mask'])
2019-03-04 14:30:38 +00:00
2019-03-04 18:06:09 +00:00
self.info['switch_suffix'] = 0x42
self.info['nat64_prefix'] = ipaddress.ip_network("64:ff9b::/96")
self.v6_routes = {}
self.v6_routes[None] = []
self.v6_routes['base'] = []
2019-03-04 14:30:38 +00:00
2019-03-19 22:01:55 +00:00
self.ports = []
2019-03-04 14:30:38 +00:00
for port in range(1,3):
net = self.info['v6_gen'].next()
self.v6_routes['base'].append({
"net": net,
"port": port}
)
2019-03-19 22:01:55 +00:00
self.ports.append(port)
2019-03-04 14:30:38 +00:00
2019-02-23 17:58:04 +00:00
self.v6_routes['router'] = self.v6_routes['base']
2019-03-14 16:26:40 +00:00
# only 1 route to avoid table duplicate/conflict
self.v6_routes['range_router'] = self.v6_routes['base'][0:1]
2019-02-23 13:36:19 +00:00
self.v4_routes = {}
self.v4_routes[None] = []
self.v4_routes['base'] = []
2019-03-04 14:30:38 +00:00
for port in range(3,5):
net = self.info['v4_gen'].next()
self.v4_routes['base'].append({
"net": net,
"port": port}
)
2019-03-19 22:01:55 +00:00
self.ports.append(port)
2019-02-23 17:58:04 +00:00
self.v4_routes['router'] = self.v4_routes['base']
self.v4_routes['range_router'] = self.v4_routes['base']
2019-02-23 17:58:04 +00:00
self.v6_addresses = {}
self.v6_addresses[None] = []
for mode in self.modes:
self.v6_addresses[mode] = [ net['net'][self.info['switch_suffix']] for net in self.v6_routes[mode] ]
2019-02-23 17:58:04 +00:00
self.v4_addresses = {}
self.v4_addresses[None] = []
for mode in self.modes:
self.v4_addresses[mode] = [ net['net'][self.info['switch_suffix']] for net in self.v4_routes[mode] ]
self.nat64_map = {}
# init default
for mode in self.modes:
self.nat64_map[mode] = []
# specific settings - mapping 256 IPv6 IPs max statically (based on /24)
for mode in ["range_router"]:
for v6_net in self.v6_routes[mode]:
# This is a /64
2019-03-25 11:16:22 +00:00
v6_dst_base = self.info['v6_nat64_gen'].next()
2019-03-25 11:16:22 +00:00
# This is a /96 -> the first /96 inside the /64
v6_dst = v6_dst_base.subnets(new_prefix=self.info['v6_nat64_mask']).next()
for v4_net in self.v4_routes[mode]:
v4_dst = self.info['v4_nat64_map'].next()
self.nat64_map[mode].append({
"v6_src": v6_net['net'],
# "v6_dst": self.info['nat64_prefix'] # static -- not supported ATM
2019-03-25 11:13:10 +00:00
"v6_dst": v6_dst,
"v4_src": v4_net['net'],
"v4_dst": v4_dst
})
2019-02-23 13:36:19 +00:00
self.init_boilerplate(sw_name)
2019-03-19 22:01:55 +00:00
self.init_other_port_multicast_groups()
2019-02-23 13:36:19 +00:00
def gen_ndp_multicast_addr(self, addr):
""" append the 24 bit of the address to the multicast address"""
2019-03-04 14:30:38 +00:00
last_24 = int(addr) & 0xffffff
addr = self.info['ndp_multicast'][last_24]
return addr
2019-03-19 22:01:55 +00:00
def init_other_port_multicast_groups(self):
""" map multicast group x to send to
all ports but x - basically broadcasting without sending back to ourselves
"""
# create multicast nodes
2019-03-19 22:01:55 +00:00
for rid in self.ports:
ports = [ x for x in self.ports if not x == rid ]
n_handle = self.controller.mc_node_create(rid, ports)
log.debug("Creating MC node rid={} ports={} handle={}".format(rid, ports, n_handle))
g_handle = self.controller.mc_mgrp_create(rid)
2019-03-19 22:01:55 +00:00
log.debug("Created MC group mgrp={} handle={} && associating afterwards".format(rid, g_handle))
self.controller.mc_node_associate(g_handle, n_handle)
2019-02-21 22:38:09 +00:00
def init_boilerplate(self, sw_name):
self.topo = Topology(db="topology.db")
self.sw_name = sw_name
self.thrift_port = self.topo.get_thrift_port(sw_name)
self.cpu_port = self.topo.get_cpu_port_index(self.sw_name)
self.controller = SimpleSwitchAPI(self.thrift_port)
self.intf = str(self.topo.get_cpu_port_intf(self.sw_name).replace("eth0", "eth1"))
self.controller.reset_state()
if self.cpu_port:
self.controller.mirroring_add(100, self.cpu_port)
def config(self):
self.fill_tables()
self.config_hosts()
def listen_to_icmp6_multicast(self):
"""Only needed for debugging"""
2019-03-04 14:30:38 +00:00
net = self.info['ndp_multicast']
self.controller.table_add("v6_networks", "controller_debug", [str(net)])
2019-02-23 17:58:04 +00:00
def init_ndp_in_switch(self, addr):
icmp6_addr = self.gen_ndp_multicast_addr(addr)
icmp6_net = "{}/128".format(icmp6_addr)
2019-03-23 14:07:07 +00:00
icmp6_type = 135
mac_addr = self.info['mac_addr']
2019-03-23 14:03:42 +00:00
self.controller.table_add("icmp6",
"icmp6_neighbor_solicitation",
2019-03-23 14:07:07 +00:00
[str(icmp6_net), str(icmp6_type)], [str(addr), str(mac_addr)])
2019-03-23 14:03:42 +00:00
def init_icmp6_echo_in_switch(self, addr):
icmp6_addr = addr
2019-03-23 14:07:07 +00:00
icmp6_type = 128
2019-03-23 14:03:42 +00:00
icmp6_net = "{}/128".format(icmp6_addr)
self.controller.table_add("icmp6",
"icmp6_echo_reply",
[str(icmp6_net), str(icmp6_type)], [])
2019-03-23 14:03:42 +00:00
2019-03-31 13:48:00 +00:00
def ipv4_router(self, net):
return net[self.info['switch_suffix']]
2019-02-21 22:38:09 +00:00
def fill_tables(self):
self.controller.table_clear("v6_networks")
for v6route in self.v6_routes[self.mode]:
2019-03-04 14:30:38 +00:00
self.controller.table_add("v6_networks", "set_egress_port", [str(v6route['net'])], [str(v6route['port'])])
self.controller.table_clear("v4_networks")
2019-03-31 14:04:05 +00:00
self.controller.table_clear("v4_arp")
for v4route in self.v4_routes[self.mode]:
self.controller.table_add("v4_networks", "set_egress_port", [str(v4route['net'])], [str(v4route['port'])])
2019-03-31 13:48:00 +00:00
# ARP support
2019-04-02 15:13:05 +00:00
self.controller.table_add("v4_arp_egress", "set_egress_port", [str(v4route['net'])], [str(v4route['port'])])
2019-03-31 13:48:00 +00:00
router = "{}/32".format(self.ipv4_router(v4route['net']))
self.controller.table_add("v4_arp", "arp_reply",
[str(self.info['mac_broadcast']),
str(table_arp['ARP_REQUEST']),
router],
[str(self.info['mac_addr'])]
2019-03-31 13:51:11 +00:00
)
2019-03-31 13:48:00 +00:00
2019-03-05 15:19:43 +00:00
if self.args.multicast_to_controller:
self.listen_to_icmp6_multicast()
2019-03-23 14:03:42 +00:00
self.controller.table_clear("icmp6")
for v6addr in self.v6_addresses[self.mode]:
self.init_ndp_in_switch(v6addr)
2019-03-23 14:03:42 +00:00
self.init_icmp6_echo_in_switch(v6addr)
self.controller.table_clear("nat64")
self.controller.table_clear("nat46")
for nat64map in self.nat64_map[self.mode]:
self.static_nat64_mapping(**nat64map)
def static_nat64_mapping(self, v6_src, v6_dst, v4_src, v4_dst):
"""
Currently using destination only matching due to non priority
LPM support in P4
This could be solved with ternary matches or smart double table usage
"""
log.info("NAT64 map: ({} -> {} => {}), ({} -> {} -> {} (only /24)))".format(
v6_src, v6_dst, v4_dst,
2019-03-25 11:15:10 +00:00
v4_src, v4_dst, v6_src))
self.controller.table_add("nat64",
"nat64_static",
[str(v6_dst)],
[str(v6_src.network_address),
str(v4_dst.network_address),
str(v6_dst.network_address)]
)
self.controller.table_add("nat46",
"nat46_static",
[str(v4_dst)],
[str(v6_src.network_address),
str(v4_dst.network_address),
str(v6_dst.network_address)]
2019-03-14 16:03:56 +00:00
)
def config_hosts(self):
""" Assumptions:
- all routes are networks (no /128 v6 or /32 v4
- hosts get the first ip address in the network
"""
2019-02-23 14:13:47 +00:00
for v6route in self.v6_routes[self.mode]:
2019-02-23 14:13:47 +00:00
host = "h{}".format(v6route['port'])
dev = "{}-eth0".format(host)
2019-03-04 14:38:16 +00:00
net = v6route['net']
2019-03-04 14:38:42 +00:00
ipaddr = "{}/{}".format(net[1],net.prefixlen)
2019-03-06 13:00:35 +00:00
router = str(net[self.info['switch_suffix']])
2019-03-06 12:48:02 +00:00
self.config_v6_host(host, str(net), str(ipaddr), dev, router)
2019-03-04 14:34:12 +00:00
for v4route in self.v4_routes[self.mode]:
host = "h{}".format(v4route['port'])
dev = "{}-eth0".format(host)
net = v4route['net']
ipaddr = "{}/{}".format(net[1],net.prefixlen)
2019-03-31 13:52:11 +00:00
router = str(self.ipv4_router(net))
self.config_v4_host(host, str(net), str(ipaddr), dev, router)
2019-03-04 14:34:12 +00:00
@staticmethod
2019-03-06 12:48:02 +00:00
def config_v6_host(host, net, ipaddr, dev, router=None):
log.debug("Config v6 host: {} {}->{} on {}".format(host, net, ipaddr, dev))
2019-03-04 14:34:12 +00:00
subprocess.call(["mx", host, "ip", "addr", "flush", "dev", dev])
for v6dev in [ "lo", "default", "all", dev ]:
subprocess.call(["mx", host, "sysctl", "net.ipv6.conf.{}.disable_ipv6=0".format(v6dev)])
# Set down & up to regain link local address
subprocess.call(["mx", host, "ip", "link", "set", dev, "down"])
subprocess.call(["mx", host, "ip", "link", "set", dev, "up"])
subprocess.call(["mx", host, "ip", "addr", "add", ipaddr, "dev", dev])
2019-02-23 14:13:47 +00:00
if router:
subprocess.call(["mx", host, "ip", "route", "add", "default", "via", router])
@staticmethod
def config_v4_host(host, net, ipaddr, dev, router=None):
2019-03-31 08:55:33 +00:00
log.debug("Config v4 host: {} {}->{} on {} via {}".format(host, net, ipaddr, dev, router))
subprocess.call(["mx", host, "ip", "addr", "flush", "dev", dev])
subprocess.call(["mx", host, "ip", "addr", "add", ipaddr, "dev", dev])
2019-03-06 12:48:02 +00:00
if router:
subprocess.call(["mx", host, "ip", "route", "add", "default", "via", router])
2019-02-21 22:38:09 +00:00
def debug_print_pkg(self, pkg, msg="INCOMING"):
2019-02-24 14:58:15 +00:00
log.debug("{}: {}".format(msg, pkg.__repr__()))
2019-02-21 22:38:09 +00:00
def debug_format_pkg(self, pkg):
packet = Ether(str(pkg))
if packet.type == 0x800:
ip = pkg.getlayer(IP)
elif packet.type == 0x86dd:
ip = pkg.getlayer(IPv6)
# tcp = pkg.getlayer(TCP)
# raw = pkg.getlayer(Raw)
# return "{}:{} => {}:{}: flags={} seq={} ack={} raw={}".format(
# ip.src, tcp.sport,
# ip.dst, tcp.dport,
# tcp.flags,
# tcp.seq,
# tcp.ack,
# raw)
2019-03-05 21:04:04 +00:00
def handle_icmp6_echo_request(self, pkg):
"""
Sample from the wire:
DEBUG:main:reassambled=<Ether dst=00:00:0a:00:00:42 src=00:00:0a:00:00:01 type=0x86dd |<IPv6 version=6 tc=0 fl=474570 plen=64 nh=ICMPv6 hlim=64 src=2001:db8::1 dst=2001:db8::42 |<ICMPv6EchoRequest type=Echo Request code=0 cksum=0xb76d id=0x16cd seq=0x1 data='\x1f\xe2~\\\x00\x00\x00\x00\xf8\x82\x00\x00\x00\x00\x00\x00\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'()*+,-./01234567' |>>>
"""
log.info("Replying to ICMP packet")
dst_mac = pkg[Ether].src
src_mac = pkg[Ether].dst
dst_addr = pkg[IPv6].src
src_addr = pkg[IPv6].dst
e = Ether(src=src_mac, dst=dst_mac)
i = IPv6(src=src_addr, dst=dst_addr)
i2 = ICMPv6EchoReply(id=pkg[ICMPv6EchoRequest].id,
seq=pkg[ICMPv6EchoRequest].seq,
data=pkg[ICMPv6EchoRequest].data)
i2.cksum = None
2019-03-05 21:06:17 +00:00
answer = e / i / i2
2019-03-05 21:04:04 +00:00
self.send_pkg(answer)
2019-03-05 15:59:27 +00:00
def handle_icmp6_ns(self, pkg):
""" Solicitated NA"""
# Both ways should work
dst_mac = pkg[Ether].src
2019-03-05 16:01:21 +00:00
dst_mac = pkg[ICMPv6NDOptSrcLLAddr].lladdr
2019-03-05 15:59:27 +00:00
src_mac = self.info['mac_address']
dst_addr = pkg[IPv6].src
src_addr = pkg[ICMPv6ND_NS].tgt
e = Ether(src=src_mac, dst=dst_mac)
i = IPv6(src=src_addr, dst=dst_addr)
# S=1 -> solicitated
2019-03-05 19:12:24 +00:00
i2 = ICMPv6ND_NA(S=1, R=0, tgt=src_addr)
2019-03-05 20:49:50 +00:00
# try5: cksum not chksum !
i2.cksum = None
2019-03-05 19:15:55 +00:00
i3 = ICMPv6NDOptDstLLAddr(lladdr=src_mac)
2019-03-05 15:59:27 +00:00
answer = e / i / i2 / i3
2019-03-05 20:22:55 +00:00
2019-03-05 20:37:38 +00:00
# try 4
2019-03-05 20:49:50 +00:00
# for l in [Ether, IPv6, ICMPv6ND_NA, ICMPv6NDOptDstLLAddr]:
# try:
# del answer[l].chksum
# except AttributeError:
# pass
2019-03-05 20:37:38 +00:00
2019-03-05 20:22:55 +00:00
# Let scapy recalc checksum (try3)
2019-03-05 20:37:38 +00:00
# answer = answer.__class__(str(answer))
2019-03-05 20:22:55 +00:00
2019-03-05 16:08:31 +00:00
self.send_pkg(answer)
2019-03-05 15:59:27 +00:00
2019-03-05 16:08:31 +00:00
def send_pkg(self, pkg):
self.debug_print_pkg(pkg, "OUTGOING")
2019-03-05 16:09:17 +00:00
sendp(pkg, iface=self.intf, verbose=False)
2019-03-05 15:59:27 +00:00
2019-02-21 22:38:09 +00:00
def recv_msg_cpu(self, pkg):
packet = Ether(str(pkg))
2019-03-25 13:12:51 +00:00
cpu_header = ""
ether_orig = ""
orig_packet = ""
2019-03-25 13:28:36 +00:00
if packet.type == 0x4242:
2019-03-04 15:38:06 +00:00
cpu_header = CpuHeader(packet.payload)
# Not necessary anymore - cpu decoding works
log.debug("cpu = {}".format(cpu_header.__repr__()))
2019-03-04 16:50:38 +00:00
2019-03-04 17:55:25 +00:00
ether_orig = Ether(src=packet.src, dst=packet.dst, type=cpu_header.type)
2019-03-25 13:12:51 +00:00
if cpu_header.type == 0x0800:
orig_packet = ether_orig / IP(cpu_header.load)
elif cpu_header.type == 0x86dd:
orig_packet = ether_orig / IPv6(cpu_header.load)
else:
print("Broken pkg: {}".format(pkg.__repr__()))
return
2019-02-21 22:38:09 +00:00
else:
2019-03-25 13:28:36 +00:00
print("Broken / unhandled pkg: {}".format(pkg.__repr__()))
2019-02-21 22:38:09 +00:00
return
2019-03-25 13:12:51 +00:00
# Process parsed
if ICMPv6ND_NS in orig_packet and orig_packet['IPv6'].src == '::':
2019-03-25 13:30:04 +00:00
log.info("Neighbor solicitation for checking her own IP address")
2019-03-25 13:12:51 +00:00
elif ICMPv6MLReport2 in orig_packet and orig_packet['IPv6'].dst == 'ff02::16':
mc_group = orig_packet['ICMPv6MLDMultAddrRec'].dst
2019-03-25 13:30:04 +00:00
log.info("Multicast registration for {} port {} -- should probably handle this".format(mc_group, cpu_header.ingress_port))
2019-03-25 13:12:51 +00:00
elif ICMPv6ND_RS in orig_packet and orig_packet['IPv6'].dst == 'ff02::2':
src = orig_packet['IPv6'].src
2019-03-25 13:30:04 +00:00
log.info("Router solicitation from {} -- should probably handle this?".format(src))
2019-03-25 13:12:51 +00:00
elif cpu_header.task == self.task['ICMP6_NS']:
log.info("Doing neighbor solicitation for the switch in the controller")
self.handle_icmp6_ns(orig_packet)
elif cpu_header.task == self.task['ICMP6_GENERAL']:
if ICMPv6EchoRequest in orig_packet:
self.handle_icmp6_echo_request(orig_packet)
else:
2019-03-25 13:30:04 +00:00
log.info("unhandled reassambled={} from table {}".format(orig_packet.__repr__(), table_id_fields[cpu_header.table_id]))
2019-03-25 13:12:51 +00:00
2019-02-21 22:38:09 +00:00
def run_cpu_port_loop(self):
sniff(iface=self.intf, prn=self.recv_msg_cpu)
def commandline(self):
parser = argparse.ArgumentParser(description='controller++')
parser.add_argument('--mode', help='Select mode / settings to use', choices=self.modes)
2019-02-23 17:25:53 +00:00
parser.add_argument('--debug', help='Enable debug logging', action='store_true')
2019-03-05 16:08:31 +00:00
parser.add_argument('--verbose', help='Enable verbose logging', action='store_true')
2019-03-05 15:19:43 +00:00
parser.add_argument('--multicast-to-controller', help='Send debug multicast to controller', action='store_true')
2019-02-23 17:26:34 +00:00
self.args = parser.parse_args()
2019-02-23 17:27:09 +00:00
self.mode = self.args.mode
2019-03-04 13:57:27 +00:00
self.debug = self.args.debug
2019-02-21 22:38:09 +00:00
if __name__ == "__main__":
import sys
import os
2019-03-26 20:56:32 +00:00
sw_name = "s1"
controller = L2Controller(sw_name)
2019-03-25 13:32:08 +00:00
controller.commandline()
2019-03-26 20:56:32 +00:00
2019-03-25 13:32:08 +00:00
if controller.args.debug:
2019-02-21 22:38:09 +00:00
log.setLevel(logging.DEBUG)
2019-03-25 13:32:08 +00:00
elif controller.args.verbose:
2019-02-21 22:38:09 +00:00
log.setLevel(logging.INFO)
2019-03-25 13:32:08 +00:00
else:
log.setLevel(logging.WARNING)
2019-02-21 22:38:09 +00:00
log.info("Booting...")
log.debug("Debug enabled.")
2019-03-05 16:08:31 +00:00
controller.config()
controller.run_cpu_port_loop()