diff --git a/generate.py b/generate.py index 6bc3936..d26558f 100755 --- a/generate.py +++ b/generate.py @@ -6,7 +6,6 @@ import logging import os import platform -import re import sqlite3 import sys import yaml @@ -17,7 +16,6 @@ from lib import networks from lib import packages from lib import processor -from lib import statistics from lib import tables def generate(database, manifest_file, seatmap_file, @@ -27,7 +25,7 @@ def generate(database, manifest_file, seatmap_file, # Create fresh database file logging.debug('Checking if database file %s exists', database) has_previous_db = False - previous_statistics = None + before = {} if os.path.isfile(database): logging.debug( 'Found existing database file %s, gathering stats before deleting', @@ -87,6 +85,7 @@ def generate(database, manifest_file, seatmap_file, # Parse ipplan logging.debug('Parsing lines in %s', ipplan) + lines = [] try: with open(ipplan, 'r') as f: lines = f.readlines() @@ -110,7 +109,7 @@ def generate(database, manifest_file, seatmap_file, logging.debug('Parsing manifest file as JSON') try: with open(manifest_file, 'r') as f: - manifest = yaml.safe_load(f.read()) + manifest = json.load(f) except Exception as e: logging.error( 'Could not parse manifest file %s as JSON: %s', @@ -149,6 +148,7 @@ def generate(database, manifest_file, seatmap_file, sys.exit(9) logging.debug('Found seatmap file \'%s\'', seatmap_file) logging.debug('Parsing seatmap file as JSON') + seatmap = {} try: with open(seatmap_file, 'r') as f: seatmap = json.loads(f.read()) diff --git a/lib/diff.py b/lib/diff.py index 8709d56..8b79025 100644 --- a/lib/diff.py +++ b/lib/diff.py @@ -1,4 +1,3 @@ -import re import sys @@ -61,10 +60,6 @@ def _print(color, msg, output): def compare_states(before, after, logging, output=sys.stdout, limit=10000): - # Did we detect _any_ changes? - changed = False - - # Any new tables? tables_before = set(before['tables']) tables_after = set(after['tables']) dropped_tables = tables_before - tables_after @@ -73,12 +68,10 @@ def compare_states(before, after, logging, output=sys.stdout, limit=10000): output.write('Dropped %d table(s):' % (len(dropped_tables))) for dropped_table in dropped_tables: _print(bcolors.FAIL, dropped_table, output) - changed = True if len(new_tables) > 0: output.write('Added %d table(s):' % (len(new_tables))) for added_table in new_tables: _print(bcolors.OKGREEN, added_table, output) - changed = True # We can only do diff magic on tables that were in both databases tables = tables_after.intersection(tables_before) @@ -93,7 +86,6 @@ def compare_states(before, after, logging, output=sys.stdout, limit=10000): delta_count = count_after - count_before if delta_count != 0: logging.info('%s %d' % (table, delta_count)) - changed = True removed_objects = objects_before - objects_after added_objects = objects_after - objects_before if len(removed_objects) > 0: diff --git a/lib/firewall.py b/lib/firewall.py index d84acde..67e066c 100644 --- a/lib/firewall.py +++ b/lib/firewall.py @@ -77,7 +77,7 @@ def prefetch_node_and_services(self): self.node_services = dict() self.service_nodes = dict() - for access, access_key in access_to_sql_map.iteritems(): + for access, access_key in access_to_sql_map.items(): # TODO(bluecmd): These are deprecated in favor of packages # We should emit warnings in the presubmit hook to make sure # people are not using these @@ -90,14 +90,14 @@ def prefetch_node_and_services(self): for node, service in explicit: self.register_service(access, node, service) - for node, packset in self.nodes.iteritems(): + for node, packset in self.nodes.items(): for package_name in packset: package = self.packages[package_name] or {} for service in set(package.get(access, [])): self.register_service(access, node, service) # Prune redundant flows (hosts that share the network flows) - for node, services in self.node_services[access].iteritems(): + for node in self.node_services[access].iteritems(): if node not in self.netmap: continue parent = self.node_services[access].get(self.netmap[node]) diff --git a/lib/ipcalc.py b/lib/ipcalc.py index f54f83a..d04bccd 100755 --- a/lib/ipcalc.py +++ b/lib/ipcalc.py @@ -46,7 +46,7 @@ except NameError: def bin(x): ''' - Stringifies an int or long in base 2. + Stringifies an int or int in base 2. ''' if x < 0: return '-' + bin(-x) @@ -70,7 +70,7 @@ class IP(object): Represents a single IP address. :param ip: the ip address - :type ip: :class:`IP` or str or long or int + :type ip: :class:`IP` or str or int or int >>> localhost = IP("127.0.0.1") >>> print localhost @@ -158,8 +158,8 @@ def __init__(self, ip, mask=None, version=0): self.dq = ip.dq self.v = ip.v self.mask = ip.mask - elif isinstance(ip, (int, long)): - self.ip = long(ip) + elif isinstance(ip, int): + self.ip = int(ip) if self.ip <= 0xffffffff: self.v = version or 4 self.dq = self._itodq(ip) @@ -179,10 +179,12 @@ def __init__(self, ip, mask=None, version=0): if self.mask is None: self.mask = self.v == 4 and 32 or 128 # Netmask is numeric CIDR subnet - elif isinstance(self.mask, (int, long)) or self.mask.isdigit(): + elif isinstance(self.mask, int) or (isinstance(self.mask, str) and self.mask.isdigit()): + self.mask = int(self.mask) + self.mask = int(self.mask) # Netmask is in subnet notation - elif isinstance(self.mask, basestring): + elif isinstance(self.mask, str): limit = [32, 128][':' in self.mask] inverted = ~self._dqtoi(self.mask) count = 0 @@ -203,12 +205,17 @@ def __init__(self, ip, mask=None, version=0): def bin(self): ''' Full-length binary representation of the IP address. - >>> ip = IP("127.0.0.1") - >>> print ip.bin() + >>> print(ip.bin()) 01111111000000000000000000000001 ''' - return bin(self.ip).split('b')[1].rjust(self.mask, '0') + if self.mask is None: + raise ValueError("Mask must be set to compute binary representation") + + mask_int = int(self.mask) + return bin(self.ip)[2:].rjust(mask_int, '0') + + def hex(self): ''' @@ -245,7 +252,6 @@ def info(self): CLASS A ''' b = self.bin() - self.v == 4 and 32 or 128 for i in range(len(b), 0, -1): if b[:i] in self._range[self.v]: return self._range[self.v][b[:i]] @@ -253,11 +259,11 @@ def info(self): def _dqtoi(self, dq): ''' - Convert dotquad or hextet to long. + Convert dotquad or hextet to int. ''' # hex notation if dq.startswith('0x'): - ip = long(dq[2:], 16) + ip = int(dq[2:], 16) if ip > 0xffffffffffffffffffffffffffffffff: raise ValueError('%s: IP address is bigger than 2^128' % dq) if ip <= 0xffffffff: @@ -289,7 +295,7 @@ def _dqtoi(self, dq): 'compressed format malformed' % dq) ix = hx.index('') px = len(hx[ix + 1:]) - for x in xrange(ix + px + 1, 8): + for x in range(ix + px + 1, 8): hx.insert(ix, '0') elif dq.endswith('::'): pass @@ -306,11 +312,11 @@ def _dqtoi(self, dq): 'hexlets should be between 0x0000 and 0xffff' % dq) ip += h self.v = 6 - return long(ip, 16) + return int(ip, 16) elif len(dq) == 32: # Assume full heximal notation self.v = 6 - return long(h, 16) + return int(dq, 16) # IPv4 if '.' in dq: @@ -326,13 +332,13 @@ def _dqtoi(self, dq): while len(q) < 4: q.insert(1, '0') self.v = 4 - return sum(long(byte) << 8 * index for index, byte in enumerate(q)) + return sum(int(byte) << 8 * index for index, byte in enumerate(q)) raise ValueError('Invalid address input') def _itodq(self, n): ''' - Convert long to dotquad or hextet. + Convert int to dotquad or hextet. ''' if self.v == 4: return '.'.join(map(str, [ @@ -343,7 +349,7 @@ def _itodq(self, n): ])) else: n = '%032x' % n - return ':'.join(n[4 * x:4 * x + 4] for x in xrange(0, 8)) + return ':'.join(n[4 * x:4 * x + 4] for x in range(0, 8)) def __str__(self): ''' @@ -362,19 +368,19 @@ def __long__(self): return self.ip def __lt__(self, other): - return long(self) < long(IP(other)) + return int(self) < int(IP(other)) def __le__(self, other): - return long(self) <= long(IP(other)) + return int(self) <= int(IP(other)) def __ge__(self, other): - return long(self) >= long(IP(other)) + return int(self) >= int(IP(other)) def __gt__(self, other): - return long(self) > long(IP(other)) + return int(self) > int(IP(other)) def __eq__(self, other): - return long(self) == long(IP(other)) + return int(self) == int(IP(other)) def size(self): return 1 @@ -402,9 +408,9 @@ def to_ipv4(self): return self else: if self.bin().startswith('0' * 96): - return IP(long(self), version=4) - elif long(self) & 0x20020000000000000000000000000000: - return IP((long(self) - 0x20020000000000000000000000000000) >> 80, version=4) + return IP(int(self), version=4) + elif int(self) & 0x20020000000000000000000000000000: + return IP((int(self) - 0x20020000000000000000000000000000) >> 80, version=4) else: return ValueError('%s: IPv6 address is not IPv4 compatible, ' + 'nor an 6-to-4 IP' % self.dq) @@ -415,7 +421,7 @@ def from_bin(cls, value): if len(value) == 32: return cls(int(value, 2)) elif len(value) == 128: - return cls(long(value, 2)) + return cls(int(value, 2)) else: return ValueError('%r: invalid binary notation' % (value,)) @@ -424,7 +430,7 @@ def from_hex(cls, value): if len(value) == 8: return cls(int(value, 16)) elif len(value) == 32: - return cls(long(value, 16)) + return cls(int(value, 16)) else: raise ValueError('%r: invalid hexadecimal notation' % (value,)) @@ -439,9 +445,9 @@ def to_ipv6(self, type='6-to-4'): assert type in ['6-to-4', 'compat'], 'Conversion type not supported' if self.v == 4: if type == '6-to-4': - return IP(0x20020000000000000000000000000000 | long(self) << 80, version=6) + return IP(0x20020000000000000000000000000000 | int(self) << 80, version=6) elif type == 'compat': - return IP(long(self), version=6) + return IP(int(self), version=6) else: return self @@ -474,7 +480,7 @@ class Network(IP): Network slice calculations. :param ip: network address - :type ip: :class:`IP` or str or long or int + :type ip: :class:`IP` or str or int or int :param mask: netmask :type mask: int or str @@ -495,17 +501,23 @@ def netmask(self): return IP(self.netmask_long(), version=self.version()) def netmask_long(self): - ''' - Network netmask derived from subnet size, as long. + """ + Network netmask derived from subnet size, as int. >>> localnet = Network('127.0.0.1/8') - >>> print localnet.netmask_long() + >>> print(localnet.netmask_long()) 4278190080 - ''' + """ + if self.mask is None: + raise ValueError("Mask must be set to compute netmask") + + mask_int = int(self.mask) # ensure integer + if self.version() == 4: - return (0xffffffff >> (32 - self.mask)) << (32 - self.mask) + return (0xffffffff >> (32 - mask_int)) << (32 - mask_int) else: - return (0xffffffffffffffffffffffffffffffff >> (128 - self.mask)) << (128 - self.mask) + return (0xffffffffffffffffffffffffffffffff >> (128 - mask_int)) << (128 - mask_int) + def network(self): ''' @@ -554,15 +566,21 @@ def broadcast_long(self): | (0xffffffffffffffffffffffffffffffff - self.netmask_long()) def host_first(self): - ''' + """ First available host in this subnet. - ''' - if (self.version() == 4 and self.mask > 30) or \ - (self.version() == 6 and self.mask > 126): + """ + if self.mask is None: + raise ValueError("Mask must be set to compute first host") + + mask_int = int(self.mask) # ensure it's an integer + + if (self.version() == 4 and mask_int > 30) or \ + (self.version() == 6 and mask_int > 126): return self else: return IP(self.network_long() + 1, version=self.version()) + def host_last(self): ''' Last available host in this subnet. @@ -572,7 +590,7 @@ def host_last(self): return self elif (self.version() == 4 and self.mask == 31) or \ (self.version() == 6 and self.mask == 127): - return IP(long(self) + 1, version=self.version()) + return IP(int(self) + 1, version=self.version()) else: return IP(self.broadcast_long() - 1, version=self.version()) @@ -581,7 +599,7 @@ def in_network(self, other): Check if the given IP address is within this network. ''' other = Network(other) - return long(other) >= long(self) and long(other) < long(self) + self.size() - other.size() + 1 + return int(other) >= int(self) and int(other) < int(self) + self.size() - other.size() + 1 def __contains__(self, ip): ''' @@ -618,11 +636,11 @@ def __getitem__(self, key): slice_step = key.step or 1 arr = list() while x < slice_stop: - arr.append(IP(long(self) + x)) + arr.append(IP(int(self) + x)) x += slice_step return tuple(arr) else: - return IP(long(self) + key) + return IP(int(self) + key) def __iter__(self): ''' @@ -638,8 +656,8 @@ def __iter__(self): 192.168.114.3 ''' - curr = long(self.host_first()) - stop = long(self.host_last()) + curr = int(self.host_first()) + stop = int(self.host_last()) while curr <= stop: yield IP(curr) curr += 1 @@ -667,7 +685,12 @@ def size(self): >>> print net.size() 256 ''' - return 2 ** ((self.version() == 4 and 32 or 128) - self.mask) + if self.mask is None: + raise ValueError("Mask must be set to compute network size") + + bits = 32 if self.version() == 4 else 128 + return 2 ** (bits - int(self.mask)) + if __name__ == '__main__': @@ -684,23 +707,24 @@ def size(self): for ip, mask, test_ip in tests: net = Network(ip, mask) - print '===========' - print 'ip address:', net - print 'to ipv6...:', net.to_ipv6() - print 'ip version:', net.version() - print 'ip info...:', net.info() - print 'subnet....:', net.subnet() - print 'num ip\'s..:', net.size() - print 'integer...:', long(net) - print 'hex.......:', net.hex() - print 'netmask...:', net.netmask() + print ('===========') + print ('ip address:', net) + print ('to ipv6...:', net.to_ipv6()) + print('ip version:', net.version()) + print('ip info...:', net.info()) + print('subnet....:', net.subnet()) + print('num ip\'s..:', net.size()) + print('integer...:', int(net)) + print('hex.......:', net.hex()) + print('netmask...:', net.netmask()) # Not implemented in IPv6 if net.version() == 4: - print 'network...:', net.network() - print 'broadcast.:', net.broadcast() - print 'first host:', net.host_first() - print 'reverse...:', net.host_first().to_reverse() - print 'last host.:', net.host_last() - print 'reverse...:', net.host_last().to_reverse() - for ip in test_ip: - print '%s in network: ' % ip, ip in net + print('network...:', net.network()) + print('broadcast.:', net.broadcast()) + print('first host:', net.host_first()) + print('reverse...:', net.host_first().to_reverse()) + print('last host.:', net.host_last()) + print('reverse...:', net.host_last().to_reverse()) + for test_ip_addr in test_ip: + in_network = test_ip_addr in net + print(f'{test_ip_addr} in network: {in_network}') diff --git a/lib/layout.py b/lib/layout.py deleted file mode 100644 index edff9e6..0000000 --- a/lib/layout.py +++ /dev/null @@ -1,7 +0,0 @@ -from collections import namedtuple - -Rectangle = namedtuple( - 'Rectangle', ['x1', 'x2', 'y1', 'y2', 'x_start', 'y_start', - 'width', 'height', 'horizontal']) - -Dot = namedtuple('Dot', ['x', 'y']) diff --git a/lib/location.py b/lib/location.py index b3723c7..a75a1c5 100644 --- a/lib/location.py +++ b/lib/location.py @@ -2,7 +2,11 @@ import re from collections import namedtuple -from layout import Rectangle +Rectangle = namedtuple( + 'Rectangle', ['x1', 'x2', 'y1', 'y2', 'x_start', 'y_start', + 'width', 'height', 'horizontal']) + +Dot = namedtuple('Dot', ['x', 'y']) def even(x): @@ -14,14 +18,19 @@ def is_valid_seat(seat): def get_hall_from_table_name(table): - return re.search('([A-Za-z]+)[0-9]+', table).group(1) - + match = re.search(r'([A-Za-z]+)[0-9]+', table) + if match: + return match.group(1) + raise ValueError(f"Invalid table name: {table}") def normalize_table_name(table): table = table.strip() - hall, row = re.search('([A-Za-z]+)([0-9]+)', table).group(1,2) - return "{}{:02}".format(hall.upper(),int(row)) + match = re.search(r'([A-Za-z]+)([0-9]+)', table) + if not match: + raise ValueError(f"Invalid table name: {table}") + hall, row = match.group(1, 2) + return f"{hall.upper()}{int(row):02}" def add_coordinates(seatmap, cursor): halls = {} @@ -45,7 +54,6 @@ def add_coordinates(seatmap, cursor): for hall in halls: table_coordinates[hall] = [] for table in sorted(tables[hall].keys(), key=lambda x: (len(x), x)): - # Ignore tables without switches if not switches.get(table, []): logging.debug("Table %s has no switches, ignoring", table) continue @@ -53,8 +61,7 @@ def add_coordinates(seatmap, cursor): scales.append(scale) table_coordinates[hall].append((table, c)) - # Select a scale (median) - scale = sorted(scales)[len(scales)/2] if scales else 1.0 + scale = sorted(scales)[len(scales) // 2] if scales else 1.0 logging.debug("Selected median scale %f", scale) for hall in halls: @@ -62,7 +69,6 @@ def add_coordinates(seatmap, cursor): y_max = 0 y_min = float("inf") - # Calculate common offsets scaled_table_coordinates = [] for table, c in table_coordinates[hall]: s = Rectangle( diff --git a/lib/networks.py b/lib/networks.py index eb5f288..e65e3a1 100644 --- a/lib/networks.py +++ b/lib/networks.py @@ -1,9 +1,5 @@ -import re import ipcalc -import socket -import struct -from binascii import hexlify -from processor import ip2long, node +from .processor import ip2long, node def add_all(c): diff --git a/lib/packages.py b/lib/packages.py index 566db2a..d9f8d9a 100644 --- a/lib/packages.py +++ b/lib/packages.py @@ -19,7 +19,7 @@ def default_packages(packages, os): def split_package_spec(package_spec): """Given a string in the format 'test(a,b)' return ('test', 'a,b').""" - match = re.match('^(.*?)\((.*)\)$', package_spec) + match = re.match(r'^(.*?)\((.*)\)$', package_spec) if match: package_name = match.group(1) package_options = match.group(2).split(',') @@ -56,7 +56,7 @@ def build(packages, c): logging.debug('debug: %d has %s', node_id, package_name) nodes[node_id][package_name].extend(package_options) - for node_id, packmap in nodes.iteritems(): + for node_id, packmap in nodes.items(): packmap = nodes[node_id] # For hosts include network packages if node_id not in networks and netmap[node_id] in nodes: @@ -75,7 +75,7 @@ def build(packages, c): if package in packmap: del packmap[package] - for node_id, packmap in nodes.iteritems(): + for node_id, packmap in nodes.items(): for package, options in sorted(packmap.iteritems()): for option in options or [None]: row = [node_id, package, option] diff --git a/lib/processor.py b/lib/processor.py index bcc7bbf..38a4f1c 100644 --- a/lib/processor.py +++ b/lib/processor.py @@ -1,4 +1,5 @@ import ipcalc +import json import logging import re import socket @@ -9,9 +10,9 @@ MODULE = sys.modules[__name__] SYNTAX = { - "^#@": "master_network", - "^#\$": "host", - "^[A-Z]": "network" + r"^#@": "master_network", + r"^#\$": "host", + r"^[A-Z]": "network" } _current_domain = None @@ -31,7 +32,10 @@ def master_network(l, c, r): terminator = '' # Set current domain - domain = re.match(r'IPV4-([A-Z0-9]+)-NET', r[1]).group(1).upper() + m = re.match(r'IPV4-([A-Z0-9]+)-NET', r[1]) + if not m: + raise ValueError(f"Invalid network string: {r[1]}") + domain = m.group(1).upper() _current_domain = domain _domains.add(domain) @@ -46,7 +50,6 @@ def master_network(l, c, r): ipv6 = l[2] _current_v6_base = ipv6.split('::', 1)[0] - last_digits = int(str(ipv4_gateway).split('.')[-1]) ipv6_netmask = int(ipv6.split('/', 1)[1]) ipv6_gateway = "%s::1" % (_current_v6_base, ) @@ -68,9 +71,12 @@ def master_network(l, c, r): def host(l, c, network_id): node_id = node(c) - c.execute('SELECT vlan FROM network WHERE node_id = ?', (network_id, )) - vlan = c.fetchone()[0] - vlan = int(vlan) if not vlan is None else None + c.execute('''SELECT vlan FROM network WHERE node_id = ?''', (node_id,)) + row = c.fetchone() + if row != None: + vlan = int(row[0]) + else: + vlan = None name = l[1] ip = l[2] @@ -89,7 +95,7 @@ def host(l, c, network_id): # If it starts with ::, use VLAN if available if vlan and ip.startswith('::'): ipv6_addr = "%s:%d%s" % (_current_v6_base, vlan, ip) - print ipv6_addr + print(ipv6_addr) row = [ node_id, @@ -100,6 +106,7 @@ def host(l, c, network_id): ipv4_addr, ipv6_addr, network_id] + row[5] = json.dumps(row[5]) c.execute('INSERT INTO host VALUES (?,?,?,?,?,?)', row) options(c, node_id, l[3]) @@ -107,7 +114,7 @@ def host(l, c, network_id): return -def network(l, c, r): +def network(l, c, network_id=None): node_id = node(c) short_name = l[0] vlan = int(l[3]) if l[3] != '-' else None @@ -116,13 +123,15 @@ def network(l, c, r): # IPv4 ipv4 = l[1] net_ipv4 = ipcalc.Network(ipv4) - ipv4_gateway = net_ipv4[1] + if len(net_ipv4) <= 2: + ipv4_gateway = net_ipv4[0] + else: + ipv4_gateway = net_ipv4[1] ipv4_netmask = str(net_ipv4.netmask()) ipv4_netmask_dec = int(str(ipv4).split("/")[1]) # IPv6 if vlan: - last_digits = int(str(ipv4_gateway).split('.')[-1]) ipv6 = "%s:%d::/64" % (_current_v6_base, vlan) ipv6_netmask = 64 ipv6_gateway = "%s:%d::1" % (_current_v6_base, vlan) @@ -143,7 +152,7 @@ def network(l, c, r): options(c, node_id, l[4]) - return node_id + return node_id, network_id def split_value(string): @@ -197,9 +206,9 @@ def ip2long(ip, version): return int(hexlify(socket.inet_pton(socket.AF_INET6, ip)), 16) -def parser_func(l): +def parser_func(line): for exp in SYNTAX: - if re.match(exp, l[0]): + if re.match(exp, line[0]): return SYNTAX[exp] return None @@ -213,6 +222,7 @@ def parse(lines, c): continue func = parser_func(line) if func: - parse_using = getattr(MODULE, func, network_id) - result = parse_using(line, c, network_id) - network_id = result if result is not None else network_id + parse_using = getattr(MODULE, func, None) + if callable(parse_using): + result = parse_using(line, c, network_id) + network_id = result if result is not None else network_id diff --git a/lib/statistics.py b/lib/statistics.py index f85885f..71f3003 100644 --- a/lib/statistics.py +++ b/lib/statistics.py @@ -1,21 +1,16 @@ def gather_all(c): - # Number of nodes? c.execute('SELECT COUNT(*) FROM node') nbr_of_nodes = c.fetchone()[0] - # Number of hosts? c.execute('SELECT COUNT(*) FROM host') nbr_of_hosts = c.fetchone()[0] - # Number of networks? c.execute('SELECT COUNT(*) FROM network') nbr_of_networks = c.fetchone()[0] - # Number of firewall rules? c.execute('SELECT COUNT(*) FROM firewall_rule') nbr_of_firewall_rules = c.fetchone()[0] - # Number of switches? c.execute( """SELECT COUNT(*) @@ -26,7 +21,6 @@ def gather_all(c): AND o.name = 'tblswmgmt'""") nbr_of_switches = c.fetchone()[0] - # Number of active? c.execute('''SELECT COUNT(*) FROM active_switch;''') nbr_of_active_switches = c.fetchone()[0] @@ -40,7 +34,3 @@ def gather_all(c): } return results - - -def print_all(current, previous): - pass diff --git a/requirements.txt b/requirements.txt index 9bd63a9..89c1daa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,6 @@ -PyYAML==3.11 \ No newline at end of file +PyYAML==3.11 +requests +layout +ipcalc +six +processor