├── .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 |
--------------------------------------------------------------------------------