Commit e3376633 authored by Nigel Kukard's avatar Nigel Kukard
Browse files

Initial commit

parents
Pipeline #4504 failed with stage
in 23 seconds
image: idmslinux/rolling
# Stages we need to progress through
stages:
- test
test_job:
stage: test
script:
# Create environment
- pacman -Syu --noconfirm
- pacman -S --noconfirm grep python
- pacman -S --noconfirm python-pytest python-pytest-runner python-pytest-cov python-pylint python-isort mypy pylama python-mccabe
- pacman -S --noconfirm iproute2 bird exabgp
# Run tests
- python setup.py test
# Artifacts
artifacts:
expire_in: 1 day
paths:
- build/
This diff is collapsed.
This diff is collapsed.
# Router X
[router routerX BirdRouterNode]
configfile = /root/routers/rX/config/bird.conf
routes = 172.16.100.0/24 via 172.16.10.10
fefe:1::/64 via fefe::10
[network-interface routerX eth0]
mac = 02:01:00:00:00:01
ips = 192.168.0.1/24
fec0::1/64
switch = switchA
[network-interface routerX eth9]
mac = 02:01:00:00:00:02
ips = 172.16.10.1/24
fefe::1/64
# Router Y
[router routerY BirdRouterNode]
configfile = /root/routers/rY/config/bird.conf
[network-interface routerY eth0]
mac = 02:02:00:00:00:01
ips = 192.168.0.2/24
fec0::2/64
switch = switchA
# Router A
[router routerA BirdRouterNode]
configfile = /root/routers/rA/config/bird.conf
[network-interface routerA eth0]
mac = 02:03:00:00:00:01
ips = 192.168.0.3/24
fec0::3/64
switch = switchA
[network-interface routerA eth1]
mac = 02:03:00:00:00:02
ips = 192.168.1.1/24
fec0:1::1/64
switch = switchB
# Router B
[router routerB BirdRouterNode]
configfile = /root/routers/rB/config/bird.conf
[network-interface routerB eth0]
mac = 02:04:00:00:00:01
ips = 192.168.1.2/24
fec0:1::2/64
switch = switchB
[network-interface routerB eth9]
mac = 02:04:00:00:00:02
ips = 172.16.20.1/24
febe::2/64
#!/usr/bin/python
# Copyright (C) 2019, AllWorldIT.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Namespace Network Simulator."""
import argparse
import configparser
import os
import sys
sys.path.insert(0, '%s/src' % os.path.dirname(os.path.abspath(__file__)))
from nsnetsim.topology import Topology
from nsnetsim.bird_router_node import BirdRouterNode
__version__ = '0.0.1'
def _load_topology(configfile: str) -> Topology:
"""Run the network simulation."""
config = configparser.ConfigParser()
config.read(configfile)
topology = Topology()
for section in config.sections():
# Split the section name up into its components
(node_type, router_name, arg) = section.split()
# Check we at least have node_type and router_name
if not node_type or not router_name:
raise RuntimeError('The configuration file must have at least two components in the section "[NODE_TYPE ROUTER_NAME]"')
# Check if this is a router
if node_type == 'router':
# Check we have a arg
if not arg:
raise RuntimeError('To define a router you need to add a type "[router ROUTER_NAME ROUTER_TYPE]"')
# Check the router type is supported
if arg == 'BirdRouterNode':
router_type = BirdRouterNode
else:
raise RuntimeError('Router type "{arg}" not supported?')
# Grab config file
configfile = None
if 'configfile' in config[section]:
configfile = config[section]['configfile']
# Add the router
router = topology.add_router(router_name, router_class=router_type,
configfile=configfile)
# Grab routes we may have and add them
if 'routes' in config[section]:
routes = config[section]['routes']
for route in routes.splitlines():
router.add_route(route.split())
# Check if this is a network interface
elif node_type == 'network-interface':
# Check we have a arg
if not arg:
raise RuntimeError('To define a router you need to add a type "[network-interface ROUTER_NAME INTERFACE_NAME]"')
# Try get node
router = topology.get_node(router_name)
if not router:
raise RuntimeError(f'Router node "{router_name}" not found')
# Work out MAC address
mac = None
if 'mac' in config[section]:
mac = config[section]['mac']
# Add the interface
interface = router.add_interface(name=arg, mac=mac)
# Check if we should link this interface to a switch
if 'switch' in config[section]:
switch_name = config[section]['switch']
# Grab the switch from the topology
switch = topology.get_node(switch_name)
# If we didn't get one, create it
if not switch:
switch = topology.add_switch(switch_name)
# Add the interface to the switch...
switch.add_interface(interface)
# Add IP's to interface
if 'ips' in config[section]:
interface_ips = config[section]['ips']
for interface_ip in interface_ips.splitlines():
interface.add_ip(interface_ip)
# Check if this is a network interface
else:
raise RuntimeError(f'Unsupported configuration item "{node_type}"')
# Return the topology we just created
return topology
def start(configfile: str):
"""Start the simulation."""
topology = _load_topology(configfile)
topology.build()
def stop(configfile: str):
"""Stop the simulation."""
topology = _load_topology(configfile)
topology.destroy()
def cmdline():
"""Process command line arguments."""
print(f'nsNetSim v{__version__} - Copyright © 2019, AllWorldIT.\n')
# Start argument parser
argparser = argparse.ArgumentParser(add_help=False)
# Create argument group for main options
main_group = argparser.add_argument_group('Main options')
main_group.add_argument('--config', dest='configfile', action='store',
help='Load configuration from this file')
main_group.add_argument('cmd', metavar='CMD', type=str, nargs='?', choices=['start', 'stop', 'exec'], help='Command to run')
main_group.add_argument('cmdargs', metavar='CMDARGS', type=str, nargs='*', help="Arguments for CMD")
# Create argument group for optionals
optional_group = argparser.add_argument_group('Optional arguments')
optional_group.add_argument('-n', '--node', dest="node", help="Node to run command on")
optional_group.add_argument('-h', '--help', action="help", help="Show this help message and exit")
# Parse args
args = argparser.parse_args()
if not args.cmd:
print(f'ERROR: No command given')
exit(1)
if args.cmd in ('start', 'stop'):
if args.cmdargs:
print(f'ERROR: The "{args.cmd}" command does not support arguments')
exit(1)
if not args.configfile:
print(f'ERROR: The "{args.cmd}" command requires a "--config" argument')
exit(1)
if args.cmd == 'start':
start(args.configfile)
elif args.cmd == 'stop':
stop(args.configfile)
if __name__ == '__main__':
cmdline()
[aliases]
test = pytest
[tool:pytest]
addopts = --pylama --cov=src/ tests/ src/ --verbose
[coverage:run]
branch = true
parallel = true
[coverage:report]
show_missing = true
exclude_lines =
pragma: no cover
def __repr__
if self.debug:
raise AssertionError
raise NotImplementedError
if __name__ == ['"]__main__['"]:$
[pylama]
linters = pep257,pycodestyle,pyflakes,pylint,mccabe,mypy,isort
# D202: No blank lines allowed after function docstring
# D203: 1 blank line required before class docstring
# D213: Multi-line docstring summary should start at the second line
# R0201: Method could be a function
# R0903: Too few public methods
ignore = D202,D203,D213,R0201,R0903
[pylama:pycodestyle]
max_line_length = 132
[pylama:pylint]
max_line_length = 132
[mypy]
ignore_missing_imports = true
"""Namespace Network Simulator."""
from setuptools import find_packages, setup
NAME = 'nsnetsim'
VERSION = '0.0.1'
with open("README.md", "r") as fh:
LONG_DESCRIPTION = fh.read()
setup(
name=NAME,
version=VERSION,
author="Nigel Kukard",
author_email="nkukard@lbsd.net",
description="Network namespace network simulator",
long_description=LONG_DESCRIPTION,
long_description_content_type="text/markdown",
url="https://gitlab.devlabs.linuxassist.net/allworldit/python/nsnetsim",
classifiers=[
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"Intended Audience :: System Administrators",
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 3 :: Only",
"Topic :: Software Development :: Libraries :: Python Modules",
],
python_requires='>=3.6',
packages=find_packages('src', exclude=['tests']),
package_dir={'': 'src'},
)
# Copyright (C) 2019, AllWorldIT.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Namespace Network Simulator package."""
# Copyright (C) 2019, AllWorldIT.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""BIRD router support."""
import os
import signal
import subprocess
from typing import Any, Dict, List
from birdclient import BirdClient
from .router_node import RouterNode
__version__ = "0.0.1"
class BirdRouterNode(RouterNode):
"""BirdRouterNode implements a network isolated BIRD router node."""
# Configuration file
_configfile: str
# Control socket
_controlsocket: str
# PID file
_pidfile: str
def _init(self, **kwargs):
"""Initialize the object."""
# Call parent create
super()._init()
# We should be getting a config file
configfile = kwargs.get('configfile', None)
if not configfile:
raise RuntimeError('The "configfile" argument should of been provided')
# Check it exists
if not os.path.exists(configfile):
raise RuntimeError(f'BIRD config file "{configfile}" does not exist')
# Set config file
self._configfile = configfile
# Set control socket
self._controlsocket = f'{self._rundir}/bird-control.socket'
self._pidfile = f'{self._rundir}/bird.pid'
# Test config file
try:
subprocess.check_output(['bird', '-c', self._configfile, '-p'],
stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as exception:
output = exception.output.decode('utf-8').rstrip()
self._log(f'ERROR: Failed to validate BIRD configuration file "{self._configfile}": '
f'{output}')
exit(1)
# We start out with no process
self._bird_process = None
# Send something to birdc
def birdc(self, query: str) -> List[str]:
"""Send a query to birdc."""
birdc = BirdClient(self._controlsocket)
return birdc.query(query)
def birdc_show_status(self) -> Dict[str, str]:
"""Return status."""
birdc = BirdClient(self._controlsocket)
return birdc.show_status()
def birdc_show_protocols(self) -> Dict[str, Any]:
"""Return protocols."""
birdc = BirdClient(self._controlsocket)
return birdc.show_protocols()
def birdc_show_route_table(self, table: str) -> List:
"""Return a routing table."""
birdc = BirdClient(self._controlsocket)
return birdc.show_route_table(table)
def _create(self):
"""Create the router."""
# Call parent create
super()._create()
# Run bird within the network namespace
try:
subprocess.check_output([
'ip', 'netns', 'exec', self.namespace,
'bird', '-c', self._configfile, '-s', self._controlsocket, '-P', self._pidfile,
])
except subprocess.CalledProcessError as exception:
output = exception.output.decode('utf-8').rstrip()
self._log(f'ERROR: Failed to start BIRD with configuration file "{self._configfile}": '
f'{output}')
exit(1)
def _remove(self):
"""Remove the router."""
# Grab PID of the process...
if os.path.exists(self._pidfile):
with open(self._pidfile, 'r') as pidfile_file:
pid = int(pidfile_file.read())
# Terminate process
try:
os.kill(pid, signal.SIGTERM)
except ProcessLookupError:
self._log(f'WARNING: Failed to kill BIRD process {pid}')
# Remove pid file
try:
os.remove(self._pidfile)
except FileNotFoundError:
pass
# Call parent remove
super()._remove()
# Copyright (C) 2019, AllWorldIT.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""ExaBGP router support."""
import getpass
import os
import signal
import subprocess
from typing import List
from .router_node import RouterNode
__version__ = "0.0.1"
class ExaBGPRouterNode(RouterNode):
"""ExaBGPRouterNode implements a network isolated ExaBGP router node."""
# Configuration file
_configfile: str
# Control socket
_namedpipe: str
# PID file
_pidfile: str
def _init(self, **kwargs):
"""Initialize the object."""
# Call parent create
super()._init()
# We should be getting a config file
configfile = kwargs.get('configfile', None)
# Check it exists
if configfile and (not os.path.exists(configfile)):
raise RuntimeError(f'ExaBGP config file "{configfile}" does not exist')
# Set config file
self._configfile = configfile
# Set named pipe
self._namedpipe = f'exabgp-{self._name}'
self._pidfile = f'{self._rundir}/exabgp-{self._name}.pid'
self._fifo_in = f'/run/{self._namedpipe}.in'
self._fifo_out = f'/run/{self._namedpipe}.out'
self._logfile = f'{self._namedpipe}.log'
# We start out with no process
self._exabgp_process = None
def exabgpcli(self, args: List[str]) -> List[str]:
"""Send a query to ExaBGP."""
cmdline = ['exabgpcli']
cmdline.extend(args)
# Now for the actual configuration, which is done using the environment
environment = {}
environment['exabgp.api.pipename'] = self._namedpipe
res = self.run(cmdline, env=environment, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
return res.stdout.decode('utf-8').splitlines()
def _create(self):
"""Create the router."""
# Call parent create
super()._create()
# Work out the arguments we're going to pass
args = ['ip', 'netns', 'exec', self.namespace,
'exabgp']
# If we were given a config file, add it
if self._configfile:
args.append(self._configfile)
# Now for the actual configuration, which is done using the environment
environment = {}
environment['exabgp.api.pipename'] = self._namedpipe
environment['exabgp.daemon.daemonize'] = 'true'
environment['exabgp.daemon.pid'] = self._pidfile
environment['exabgp.daemon.user'] = getpass.getuser()
environment['exabgp.log.all'] = 'true'
environment['exabgp.log.destination'] = self._logfile
try:
subprocess.check_output(['mkfifo', self._fifo_in, self._fifo_out])
except subprocess.CalledProcessError as exception:
output = exception.output.decode('utf-8').rstrip()
self._log(f'ERROR: Failed to create ExaBGP fifo files: '
f'{output}')
# Run ExaBGP within the network namespace
try:
subprocess.check_output(args, env=environment)
except subprocess.CalledProcessError as exception:
output = exception.output.decode('utf-8').rstrip()
self._log(f'ERROR: Failed to start ExaBGP with configuration file "{self._configfile}": '
f'{output}')
def _remove(self):
"""Remove the router."""
# Grab PID of the process...
if os.path.exists(self._pidfile):
with open(self._pidfile, 'r') as pidfile_file:
pid = int(pidfile_file.read())
# Terminate process
try:
os.kill(pid, signal.SIGTERM)
except ProcessLookupError:
self._log(f'WARNING: Failed to kill ExaBGP process {pid}')
# Remove pid file
try:
os.remove(self._pidfile)
except FileNotFoundError:
pass
# Remove fifo's
if os.path.exists(self._fifo_in):
os.remove(self._fifo_in)
if os.path.exists(self._fifo_out):