├── .gitattributes ├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.rst ├── boot2docker ├── .gitattributes ├── Dockerfile ├── boot2docker.iso └── build-iso.sh ├── dev-requirements.txt ├── docker_command.py ├── example1 └── topo.yaml ├── example2 └── topo.yaml ├── screenplay.gif ├── setup.py ├── tasks.py ├── test_yans.py ├── topology.py ├── tox.ini ├── yans-node └── Dockerfile └── yans.py /.gitattributes: -------------------------------------------------------------------------------- 1 | *.iso filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | 37 | # Complexity 38 | output/*.html 39 | output/*/index.html 40 | 41 | # Sphinx 42 | docs/_build 43 | README.html 44 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Config file for automatic testing at travis-ci.org 2 | 3 | language: python 4 | 5 | python: 6 | - "3.3" 7 | - "2.7" 8 | - "pypy" 9 | 10 | # command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors 11 | install: pip install -U . 12 | 13 | # command to run tests, e.g. python setup.py test 14 | script: py.test 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Kenneth Jiang 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst LICENSE 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =============================== 2 | YANS 3 | =============================== 4 | 5 | .. image:: https://badge.fury.io/py/YANS.png 6 | :target: http://badge.fury.io/py/YANS 7 | 8 | .. image:: https://travis-ci.org/kennethjiang/YANS.png?branch=master 9 | :target: https://travis-ci.org/kennethjiang/YANS 10 | 11 | 12 | **Yet Another Network Simulator** 13 | 14 | YANS is a `Docker `_-based network simulator. It is lightening-fast. The screenplay below demonstrates that YANS can launch a simulated network in **under 3 seconds**. 15 | 16 | .. image:: https://github.com/kennethjiang/YANS/raw/master/screenplay.gif 17 | :height: 512 px 18 | :width: 499 px 19 | :scale: 50 % 20 | 21 | 0. Install prerequisites: 22 | ========================== 23 | 24 | Mac OS X 25 | 26 | * `Docker `__ 27 | * `Docker Machine `__ 28 | 29 | Ubuntu 30 | 31 | * `Docker `__ 32 | * ``sudo apt install bridge-utils`` 33 | 34 | 35 | 1. Install YANS 36 | ===================== 37 | 38 | .. code:: bash 39 | 40 | pip install YANS 41 | 42 | 43 | 2. Create a file named ``topo.yaml`` 44 | ======================================= 45 | 46 | .. code:: 47 | 48 | links: 49 | - name: link1 50 | nodes: 51 | - node1 52 | - node2 53 | - name: link2 54 | nodes: 55 | - node1 56 | - name: link3 57 | 58 | 59 | 3. Go! 60 | ============ 61 | 62 | Linux 63 | 64 | sudo yans -t up 65 | 66 | 67 | Mac OS X 68 | 69 | yans -t up 70 | 71 | 72 | Requirements 73 | =============== 74 | 75 | - Python >= 2.6 or >= 3.3 76 | 77 | License 78 | ================ 79 | 80 | MIT licensed. See the bundled `LICENSE `_ file for more details. 81 | -------------------------------------------------------------------------------- /boot2docker/.gitattributes: -------------------------------------------------------------------------------- 1 | *.iso filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /boot2docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM boot2docker/boot2docker 2 | ADD . $ROOTFS/data/ 3 | WORKDIR / 4 | 5 | RUN git clone https://git.kernel.org/pub/scm/linux/kernel/git/shemminger/bridge-utils.git && \ 6 | cd bridge-utils && \ 7 | autoconf && \ 8 | ./configure && \ 9 | make && \ 10 | make DESTDIR=$ROOTFS install && \ 11 | ln -s ../local/sbin/brctl $ROOTFS/usr/sbin/brctl 12 | 13 | RUN curl -fL https://www.kernel.org/pub/linux/utils/util-linux/v2.29/util-linux-2.29.tar.xz | tar -C / -xJ && \ 14 | cd util-linux-2.29 && \ 15 | ./configure && \ 16 | make nsenter && \ 17 | cp nsenter $ROOTFS/usr/local/bin 18 | 19 | RUN /tmp/make_iso.sh 20 | CMD ["cat", "boot2docker.iso"] 21 | 22 | -------------------------------------------------------------------------------- /boot2docker/boot2docker.iso: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:57408efe9b90fd0a7a63ad383cf436f23aeb3ea37f5f9fc1e4131bfb8cad9861 3 | size 40894464 4 | -------------------------------------------------------------------------------- /boot2docker/build-iso.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker build -t my-boot2docker-img . 4 | docker run --rm my-boot2docker-img > boot2docker.iso 5 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | invoke 3 | tox 4 | -------------------------------------------------------------------------------- /docker_command.py: -------------------------------------------------------------------------------- 1 | import docker 2 | 3 | from logging import debug 4 | import sys 5 | import subprocess 6 | import os 7 | 8 | docker_client = None 9 | 10 | def exists(exe): 11 | return any(os.access(os.path.join(path, exe), os.X_OK) for path in os.environ["PATH"].split(os.pathsep)) 12 | 13 | def is_linux(): 14 | return sys.platform == 'linux' or sys.platform == 'linux2' 15 | 16 | def run(cmd, cont=False): 17 | debug('Running command: ' + cmd) 18 | 19 | import shlex 20 | args = shlex.split(cmd) 21 | if cont: 22 | return subprocess.call(args, stdout=open(os.devnull, 'w')) 23 | else: 24 | return subprocess.check_output(args) 25 | 26 | def docker_machine_run(cmd): 27 | if is_linux(): 28 | return run(cmd) 29 | else: 30 | return run('docker-machine ssh YANS-machine ' + cmd) 31 | 32 | def create_links(links): 33 | for lnk in links: 34 | docker_machine_run('sudo brctl addbr ' + lnk.bridge_name) 35 | docker_machine_run('sudo ip link set ' + lnk.bridge_name + ' up') 36 | 37 | def destroy_links(links): 38 | for lnk in links: 39 | docker_machine_run('sudo ip link set ' + lnk.bridge_name + ' down') 40 | docker_machine_run('sudo brctl delbr ' + lnk.bridge_name) 41 | 42 | def create_nodes(nodes): 43 | client().images.pull('kennethjiang/yans-node') 44 | for node in nodes: 45 | client().containers.run('kennethjiang/yans-node', name=node.container_name, command='sleep 3153600000', detach=True, privileged=True) 46 | 47 | def destroy_nodes(nodes): 48 | for node in nodes: 49 | try: 50 | client().containers.get(node.container_name).remove(force=True) 51 | except docker.errors.NotFound: 52 | pass 53 | 54 | def attach_node(node): 55 | set_docker_machine_env() 56 | import shlex 57 | subprocess.call(shlex.split('docker exec -it --privileged ' + node.container_name + ' bash'), stdin=sys.stdin, stdout=sys.stdout) 58 | 59 | def bind_interface(interface): 60 | docker_machine_run('sudo ip link add ' + interface.name + ' type veth peer name ' + interface.peer_name) 61 | docker_machine_run('sudo ip link set ' + interface.peer_name + ' up') 62 | docker_machine_run('sudo brctl addif ' + interface.link.bridge_name + ' ' + interface.peer_name) 63 | container_pid = str(client().api.inspect_container( interface.node.container_name )['State']['Pid']) 64 | docker_machine_run('sudo ip link set netns ' + container_pid + ' dev ' + interface.name) 65 | 66 | def ensure_docker_machine(): 67 | if is_linux(): # docker machine not required on linux 68 | return 69 | 70 | if not exists('docker-machine'): 71 | sys.exit("docker-machine is required to run yans on Mac OS X. Please make sure it is installed and in $PATH") 72 | 73 | if run('docker-machine inspect YANS-machine', cont=True) != 0: # create docker machine needed for YANS if one doesn't exist 74 | print('Creating docker machine that will host all YANS containers') 75 | run('docker-machine create -d virtualbox --virtualbox-boot2docker-url https://github.com/kennethjiang/YANS/raw/master/boot2docker/boot2docker.iso YANS-machine') 76 | 77 | run('docker-machine start YANS-machine', cont=True) # make sure YANS-machine is started 78 | 79 | 80 | def client(): 81 | ensure_docker_client() 82 | return docker_client 83 | 84 | def ensure_docker_client(): 85 | global docker_client 86 | if not docker_client: 87 | set_docker_machine_env() 88 | docker_client = docker.from_env() 89 | 90 | def set_docker_machine_env(): 91 | if not is_linux(): 92 | out = run('docker-machine env YANS-machine') 93 | import re 94 | for (name, value) in re.findall('export ([^=]+)="(.+)"', out): 95 | os.environ[name] = value 96 | -------------------------------------------------------------------------------- /example1/topo.yaml: -------------------------------------------------------------------------------- 1 | links: 2 | - name: link1 3 | nodes: 4 | - node1 5 | - node2 6 | - name: link2 7 | nodes: 8 | - node1 9 | - name: link3 10 | -------------------------------------------------------------------------------- /example2/topo.yaml: -------------------------------------------------------------------------------- 1 | links: 2 | - name: link1 3 | nodes: 4 | - node1 5 | - node2 6 | - node3 7 | - name: link2 8 | nodes: 9 | - node4 10 | - node5 11 | - node6 12 | - node1 13 | - name: link3 14 | -------------------------------------------------------------------------------- /screenplay.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennethjiang/YANS/f4e7f965b97915bb4a61e67817d18b58a6a77e46/screenplay.gif -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import re 3 | import sys 4 | from setuptools import setup 5 | from setuptools.command.test import test as TestCommand 6 | 7 | 8 | REQUIRES = [ 9 | 'docopt', 10 | 'PyYAML', 11 | 'docker', 12 | 'termcolor', 13 | ] 14 | 15 | class PyTest(TestCommand): 16 | def finalize_options(self): 17 | TestCommand.finalize_options(self) 18 | self.test_args = [] 19 | self.test_suite = True 20 | 21 | def run_tests(self): 22 | import pytest 23 | errcode = pytest.main(self.test_args) 24 | sys.exit(errcode) 25 | 26 | 27 | def find_version(fname): 28 | '''Attempts to find the version number in the file names fname. 29 | Raises RuntimeError if not found. 30 | ''' 31 | version = '' 32 | with open(fname, 'r') as fp: 33 | reg = re.compile(r'__version__ = [\'"]([^\'"]*)[\'"]') 34 | for line in fp: 35 | m = reg.match(line) 36 | if m: 37 | version = m.group(1) 38 | break 39 | if not version: 40 | raise RuntimeError('Cannot find version information') 41 | return version 42 | 43 | __version__ = find_version("yans.py") 44 | 45 | 46 | def read(fname): 47 | with open(fname) as fp: 48 | content = fp.read() 49 | return content 50 | 51 | setup( 52 | name='YANS', 53 | version="0.2.3", 54 | description='Yet Another Network Simulator', 55 | long_description=read("README.rst"), 56 | author='Kenneth Jiang', 57 | author_email='kenneth.jiang@gmail.com', 58 | url='https://github.com/kennethjiang/YANS', 59 | install_requires=REQUIRES, 60 | license=read("LICENSE"), 61 | zip_safe=False, 62 | keywords='network simulator', 63 | classifiers=[ 64 | 'Development Status :: 2 - Pre-Alpha', 65 | 'Intended Audience :: Developers', 66 | 'License :: OSI Approved :: MIT License', 67 | 'Natural Language :: English', 68 | "Programming Language :: Python :: 2", 69 | 'Programming Language :: Python :: 2.7', 70 | 'Programming Language :: Python :: 3', 71 | 'Programming Language :: Python :: 3.3', 72 | 'Programming Language :: Python :: Implementation :: CPython', 73 | 'Programming Language :: Python :: Implementation :: PyPy' 74 | ], 75 | py_modules=["yans", "docker_command", "topology"], 76 | entry_points={ 77 | 'console_scripts': [ 78 | "yans = yans:main" 79 | ] 80 | }, 81 | tests_require=['pytest'], 82 | cmdclass={'test': PyTest} 83 | ) 84 | -------------------------------------------------------------------------------- /tasks.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import sys 4 | 5 | from invoke import task, run 6 | 7 | docs_dir = 'docs' 8 | build_dir = os.path.join(docs_dir, '_build') 9 | 10 | @task 11 | def test(): 12 | run('python setup.py test', pty=True) 13 | 14 | @task 15 | def clean(): 16 | run("rm -rf build") 17 | run("rm -rf dist") 18 | run("rm -rf YANS.egg-info") 19 | clean_docs() 20 | print("Cleaned up.") 21 | 22 | @task 23 | def clean_docs(): 24 | run("rm -rf %s" % build_dir) 25 | 26 | @task 27 | def browse_docs(): 28 | run("open %s" % os.path.join(build_dir, 'index.html')) 29 | 30 | @task 31 | def build_docs(clean=False, browse=False): 32 | if clean: 33 | clean_docs() 34 | run("sphinx-build %s %s" % (docs_dir, build_dir), pty=True) 35 | if browse: 36 | browse_docs() 37 | 38 | @task 39 | def readme(browse=False): 40 | run('rst2html.py README.rst > README.html') 41 | 42 | @task 43 | def publish(test=False): 44 | """Publish to the cheeseshop.""" 45 | if test: 46 | run('python setup.py register -r test sdist upload -r test') 47 | else: 48 | run("python setup.py register sdist upload") 49 | -------------------------------------------------------------------------------- /test_yans.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | from subprocess import check_output 4 | 5 | 6 | def test_echo(): 7 | '''An example test.''' 8 | result = run_cmd("echo hello world") 9 | assert result == "hello world\n" 10 | 11 | 12 | def run_cmd(cmd): 13 | '''Run a shell command `cmd` and return its output.''' 14 | return check_output(cmd, shell=True).decode('utf-8') 15 | -------------------------------------------------------------------------------- /topology.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | import uuid 3 | import string 4 | import sys 5 | 6 | class TopologySpecError(Exception): 7 | pass 8 | 9 | class Topology: 10 | 11 | def __init__(self, topo_yaml): 12 | with open(topo_yaml, 'r') as f: 13 | self.spec = yaml.load(f) 14 | 15 | # Figure out how many nodes in this topo 16 | link_spec = self.spec['links'] 17 | node_list = [l.get('nodes', []) for l in link_spec] 18 | flattened_node_list = [item for sublist in node_list for item in sublist] # Python's way to flatten nested lists. dont' ask me why: http://stackoverflow.com/questions/952914/making-a-flat-list-out-of-list-of-lists-in-python 19 | uniq_nodes = set(flattened_node_list) # Python's way of getting unique list 20 | self.nodes = [Node(n) for n in uniq_nodes] 21 | 22 | all_link_names = [l['name'] for l in link_spec] 23 | if len(set(all_link_names)) != len(all_link_names): 24 | raise TopologySpecError('Duplicate link names in ' + topo_yaml) 25 | 26 | self.links = [] 27 | for link_dict in link_spec: 28 | ajacent_nodes = [n for n in self.nodes if n.name in link_dict.get('nodes', [])] 29 | self.links.append(Link(link_dict, ajacent_nodes)) 30 | 31 | def node_by_name(self, name): 32 | matches = [n for n in self.nodes if n.name == name] 33 | return matches[0] if matches else None 34 | 35 | def draw(self): 36 | from termcolor import colored, cprint 37 | print('') 38 | cprint('Link', 'green', end='') 39 | sys.stdout.write(7*' ') 40 | cprint('Network Interface', 'yellow', end='') 41 | sys.stdout.write(5*' ') 42 | cprint('Node', 'red') 43 | print(50*'-' + '\n') 44 | for link in self.links: 45 | cprint(link.name, 'green') 46 | print('|') 47 | for interface in link.interfaces: 48 | print('|') 49 | sys.stdout.write(12*'-' + '<') 50 | cprint(interface.name, 'yellow', end='') 51 | sys.stdout.write('>' + 8*'-') 52 | cprint(interface.node.name, 'red') 53 | print('') 54 | 55 | class Link: 56 | 57 | def __init__(self, data_dict, ajacent_nodes): 58 | self.name = data_dict['name'] 59 | self.bridge_name = 'YANS-' + self.name 60 | self.interfaces = [Interface(self, node) for node in ajacent_nodes] 61 | 62 | 63 | class Interface: 64 | 65 | def __init__(self, link, node): 66 | self.link = link 67 | self.node = node 68 | self.name = 'yans' + random_id() 69 | self.peer_name = self.name + '-p' 70 | self.node.interfaces.append(self) 71 | 72 | 73 | class Node: 74 | 75 | def __init__(self, name): 76 | self.name = name 77 | self.container_name = 'YANS-' + self.name 78 | self.interfaces = [] 79 | 80 | 81 | def random_id(size=6, chars=string.letters + string.digits): 82 | import random 83 | return ''.join(random.choice(chars) for _ in range(size)) 84 | 85 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist =py27,py33 3 | [testenv] 4 | deps=pytest 5 | commands= 6 | py.test 7 | -------------------------------------------------------------------------------- /yans-node/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu 2 | 3 | RUN apt-get update && \ 4 | apt-get install -y vim iproute2 tcpdump radvd iputils-ping kmod net-tools mtr-tiny traceroute netcat dnsutils curl 5 | 6 | CMD ["bash"] 7 | -------------------------------------------------------------------------------- /yans.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | '''yans 4 | Yet Another Network Simulator 5 | 6 | Usage: 7 | yans [-V] [-t --topo=] (up|stop|destroy) 8 | yans [-V] [-t --topo=] console 9 | yans -h | --help 10 | yans --version 11 | 12 | Options: 13 | -h --help Show this screen. 14 | --version Show version. 15 | -t --topo= Network topology YAML [default: ./topo.yaml]. 16 | -V --verbose Verbose mode 17 | ''' 18 | 19 | from __future__ import unicode_literals, print_function 20 | from docopt import docopt 21 | import logging 22 | import sys 23 | 24 | from docker_command import destroy_links, create_nodes, create_links, ensure_docker_machine, destroy_nodes, bind_interface, attach_node 25 | from topology import Topology, TopologySpecError 26 | 27 | __version__ = "0.1.0" 28 | __author__ = "Kenneth Jiang" 29 | __license__ = "MIT" 30 | 31 | def main(): 32 | '''Main entry point for the yans CLI.''' 33 | args = docopt(__doc__, version=__version__) 34 | 35 | ensure_docker_machine() 36 | 37 | if args['--verbose']: 38 | logging.getLogger().setLevel(logging.DEBUG) 39 | 40 | topo_file = args['--topo'] 41 | try: 42 | topo = Topology(topo_file) 43 | except TopologySpecError as err: 44 | sys.exit(err) 45 | 46 | if args['up']: 47 | create_links(topo.links) 48 | create_nodes(topo.nodes) 49 | for link in topo.links: 50 | for interface in link.interfaces: 51 | bind_interface(interface) 52 | topo.draw() 53 | print('To log into each node:') 54 | for node in topo.nodes: 55 | print('`$ yans -t ' + topo_file + ' console ' + node.name + '`') 56 | 57 | if args['destroy']: 58 | destroy_nodes(topo.nodes) 59 | destroy_links(topo.links) 60 | 61 | if args['console']: 62 | node_name = args[''] 63 | node = topo.node_by_name(node_name) 64 | if node: 65 | attach_node(node) 66 | else: 67 | sys.exit('Node named "' + node_name + '" is not found in ' + topo_file) 68 | 69 | 70 | if __name__ == '__main__': 71 | main() 72 | --------------------------------------------------------------------------------