Exemplo n.º 1
0
def filter_paths(network,
                 paths,
                 min_hops=None,
                 max_hops=None,
                 path_count=None):
    dijkstra = Dijkstra(network)

    if min_hops is None:
        min_hops = 1

    if max_hops is None:
        max_hops = math.inf

    filtered = []
    for path in paths:
        d = dijkstra.find_shortest_distance(path[0], path[1])
        if d >= min_hops and d <= max_hops and d != math.inf:
            filtered.append(path)

    if path_count is not None:
        if len(filtered) < path_count:
            eprint(
                f"Only {len(filtered)} paths left after filtering. Required were at least {path_count}."
            )
            exit(1)

        if len(filtered) > path_count:
            filtered = filtered[:path_count]

    return filtered
Exemplo n.º 2
0
def start_yggdrasil_instances(ids, rmap):
    if ip_protocol == '4':
        eprint('warning: yggdrasil does not support IPv4')

    config = 'AdminListen: none'
    for id in ids:
        remote = rmap[id]

        if verbosity == 'verbose':
            address = remote.get('address', 'local')
            print(f'start yggdrasil in {address}/ns-{id}')

        # Create a configuration file
        exec(remote, f'printf "{config}" > /tmp/yggdrasil-{id}.conf')

        # yggdrasil uses a tun0 interface, uplink only needs an fe80:* address
        interface_down(remote, id, 'uplink')
        interface_up(remote, id, 'uplink')

        if ip_protocol == '4':
            # yggdrasil does not support IPv4!
            pass

        exec(
            remote,
            f'ip netns exec "ns-{id}" nohup yggdrasil -useconffile /tmp/yggdrasil-{id}.conf > /dev/null 2> /dev/null < /dev/null &'
        )
Exemplo n.º 3
0
def wait(beg_ms, until_sec):
    now_ms = millis()

    # wait until time is over
    if (now_ms - beg_ms) < (until_sec * 1000):
        time.sleep(((until_sec * 1000) - (now_ms - beg_ms)) / 1000.0)
    else:
        eprint('Wait timeout already passed by {:.2f}sec'.format(
            ((now_ms - beg_ms) - (until_sec * 1000)) / 1000))
        exit(1)
Exemplo n.º 4
0
def main():
    parser = argparse.ArgumentParser(description='Measure mean traffic.')
    parser.add_argument(
        '--remotes',
        help='Distribute nodes and links on remotes described in the JSON file.'
    )
    parser.add_argument('--interface', help='Interface to measure traffic on.')
    parser.add_argument('--duration',
                        type=int,
                        help='Measurement duration in seconds.')

    args = parser.parse_args()

    if args.remotes:
        if not os.path.isfile(args.remotes):
            eprint(f'File not found: {args.remotes}')
            stop_all_terminals()
            exit(1)

        with open(args.remotes) as file:
            args.remotes = [Remote.from_json(obj) for obj in json.load(file)]
    else:
        args.remotes = default_remotes

    # need root for local setup
    for remote in args.remotes:
        if remote.address is None:
            if os.geteuid() != 0:
                eprint('Need to run as root.')
                exit(1)

    rmap = get_remote_mapping(args.remotes)
    if args.duration:
        ds = args.duration
        ts_beg = traffic(args.remotes, interface=args.interface, rmap=rmap)
        time.sleep(ds)
        ts_end = traffic(args.remotes, interface=args.interface, rmap=rmap)
        ts = ts_end - ts_beg
        n = ds * len(rmap)
        print(
            f'rx: {format_size(ts.rx_bytes / n)}/s, {ts.rx_packets / n:.2f} packets/s, {ts.rx_dropped / n:.2f} dropped/s (avg. per node)'
        )
        print(
            f'tx: {format_size(ts.tx_bytes / n)}/s, {ts.tx_packets / n:.2f} packets/s, {ts.tx_dropped / n:.2f} dropped/s (avg. per node)'
        )
    else:
        ts = traffic(args.remotes, interface=args.interface, rmap=rmap)
        print(
            f'rx: {format_size(ts.rx_bytes)} / {ts.rx_packets} packets / {ts.rx_dropped} dropped'
        )
        print(
            f'tx: {format_size(ts.tx_bytes)} / {ts.tx_packets} packets / {ts.tx_dropped} dropped'
        )

    stop_all_terminals()
Exemplo n.º 5
0
def start_routing_protocol(protocol, rmap, ids, ignore_error=False):
    beg_count = count_instances(protocol, rmap)
    beg_ms = millis()

    for id in ids:
        remote = rmap[id]
        interface_up(remote, id, 'uplink', ignore_error)

    if protocol == 'babel':
        start_babel_instances(ids, rmap)
    elif protocol == 'batman-adv':
        start_batmanadv_instances(ids, rmap)
    elif protocol == 'bmx6':
        start_bmx6_instances(ids, rmap)
    elif protocol == 'cjdns':
        start_cjdns_instances(ids, rmap)
    elif protocol == 'olsr1':
        start_olsr1_instances(ids, rmap)
    elif protocol == 'olsr2':
        start_olsr2_instances(ids, rmap)
    elif protocol == 'ospf':
        start_ospf_instances(ids, rmap)
    elif protocol == 'yggdrasil':
        start_yggdrasil_instances(ids, rmap)
    elif protocol == 'none':
        return
    else:
        eprint(f'Error: unknown routing protocol: {protocol}')
        exit(1)

    wait_for_completion()

    # wait for last started process to fork
    # otherwise we might have one extra counted instance
    time.sleep(0.5)

    end_ms = millis()
    end_count = count_instances(protocol, rmap)

    count = end_count - beg_count
    if count != len(ids):
        eprint(
            f'Error: Failed to start {protocol} instances: {count}/{len(ids)} started'
        )
        stop_all_terminals()
        exit(1)

    if verbosity != 'quiet':
        print('Started {} {} instances in {}'.format(
            count, protocol, format_duration(end_ms - beg_ms)))
Exemplo n.º 6
0
def stop_routing_protocol(protocol, rmap, ids, ignore_error=False):
    beg_count = count_instances(protocol, rmap)
    beg_ms = millis()

    if protocol == 'babel':
        stop_babel_instances(ids, rmap)
    elif protocol == 'batman-adv':
        stop_batmanadv_instances(ids, rmap)
    elif protocol == 'bmx6':
        stop_bmx6_instances(ids, rmap)
    elif protocol == 'cjdns':
        stop_cjdns_instances(ids, rmap)
    elif protocol == 'olsr1':
        stop_olsr1_instances(ids, rmap)
    elif protocol == 'olsr2':
        stop_olsr2_instances(ids, rmap)
    elif protocol == 'ospf':
        stop_ospf_instances(ids, rmap)
    elif protocol == 'yggdrasil':
        stop_yggdrasil_instances(ids, rmap)
    elif protocol == 'none':
        pass
    else:
        eprint('Error: unknown routing protocol: {}'.format(protocol))
        exit(1)

    for id in ids:
        remote = rmap[id]
        interface_down(remote, id, 'uplink', ignore_error=ignore_error)

    wait_for_completion()

    # wait for last stopped process to disappear
    # otherwise we might have one extra counted instance
    time.sleep(0.5)

    end_ms = millis()
    end_count = count_instances(protocol, rmap)

    count = beg_count - end_count
    if count != len(ids):
        eprint(
            f'Error: Failed to stop {protocol} instances: {count}/{len(ids)} left'
        )
        exit(1)

    if not ignore_error and verbosity != 'quiet':
        print('Stopped {} {} instances in {}'.format(
            len(ids), protocol, format_duration(end_ms - beg_ms)))
Exemplo n.º 7
0
def _stop_protocol(protocol, rmap, ids):
    base = os.path.dirname(os.path.realpath(__file__))
    path = f'{base}/protocols/{protocol}_stop.sh'

    if not os.path.isfile(path):
        eprint(f'File does not exist: {path}')
        stop_all_terminals()
        exit(1)

    for id in ids:
        remote = rmap[id]

        label = remote.address or 'local'
        cmd = f'ip netns exec ns-{id} sh -s {label} {id} < {path}'

        _exec_verbose(remote, cmd)

    wait_for_completion()
Exemplo n.º 8
0
def remove_link(link, rmap={}):
    source = str(link['source'])
    target = str(link['target'])
    remote1 = rmap.get(source)
    remote2 = rmap.get(target)

    ifname1 = f've-{source}-{target}'
    ifname2 = f've-{target}-{source}'

    if source == target:
        eprint(
            f'Warning: Cannot remove link with identical source ({source}) and target ({target}) => ignore'
        )
        return

    if remote1 == remote2:
        exec(
            remote1,
            f'ip netns exec "switch" ip link del "{ifname1}" type veth peer name "{ifname2}"'
        )
    else:
        # multiple remotes always have address set
        addr1 = remote1.address
        addr2 = remote2.address

        # ids do not have to be the same on both remotes - but it is simpler that way
        tunnel_id = link_num(addr1, addr2, min=1, max=2**32)
        session_id = link_num(source, target, min=1, max=2**32)

        exec(
            remote1,
            f'ip l2tp del session tunnel_id {tunnel_id} session_id {session_id}'
        )
        if l2tp_session_count(remote1, tunnel_id) == 0:
            exec(remote1, f'ip l2tp del tunnel tunnel_id {tunnel_id}')

        exec(
            remote2,
            f'ip l2tp del session tunnel_id {tunnel_id} session_id {session_id}'
        )
        if l2tp_session_count(remote2, tunnel_id) == 0:
            exec(remote2, f'ip l2tp del tunnel tunnel_id {tunnel_id}')
Exemplo n.º 9
0
def run(command, rmap, quiet=False):
    for (id, remote) in rmap.items():
        cmd = command.replace('{name}', id[3:])

        if quiet:
            exec(remote,
                 f'ip netns exec "ns-{id}" {cmd}',
                 get_output=False,
                 ignore_error=True)
        else:
            stdout, stderr, rcode = exec(remote,
                                         f'ip netns exec "ns-{id}" {cmd}',
                                         get_output=True,
                                         ignore_error=True)

            if stdout or stderr:
                print(f'{id}')
                if stdout:
                    print(stdout)
                if stderr:
                    eprint(stderr)
Exemplo n.º 10
0
    def partition_into_subgraph_nodes(neighbor_map, nodes, rmap, remotes):
        random.shuffle(nodes)
        # remote_id => [<node_ids>]
        partitions = {}

        # keep running nodes on the same partition
        for node_id, remote in rmap.items():
            remote_id = remotes.index(remote)
            partitions.setdefault(remote_id, []).append(node_id)
            if node_id not in nodes:
                eprint(f'Node {node_id} not in previous state!')
                stop_all_terminals()
                exit(1)
            nodes.remove(node_id)

        for remote_id in range(len(remotes)):
            if len(nodes) == 0:
                break
            if remote_id not in partitions:
                partitions[remote_id] = [nodes.pop()]

        # find neighbor node of cluster
        def grow_cluster(cluster, nodes):
            for node in nodes:
                for cluster_node in cluster:
                    if node in neighbor_map[cluster_node]:
                        cluster.append(node)
                        nodes.remove(node)
                        return
            # cannot extend cluster via neighbor => use left over node
            cluster.append(nodes.pop())

        while len(nodes) > 0:
            # get smallest cluster (remote) key
            key = min(partitions.keys(), key=lambda k: len(partitions[k]))
            grow_cluster(partitions[key], nodes)

        return partitions
Exemplo n.º 11
0
def _get_random_paths(nodes,
                      count=10,
                      seed=None,
                      sample_without_replacement=False):
    if sample_without_replacement:
        if count > (len(nodes) / 2):
            eprint(
                f"Not enough nodes ({len(nodes)}) to generate {count} unique paths."
            )
            stop_all_terminals()
            exit(1)
    else:
        if len(nodes) < 2:
            eprint(
                f"Not enough nodes ({len(nodes)}) to generate {count} paths.")
            stop_all_terminals()
            exit(1)

    if seed is not None:
        random.seed(seed)

    paths = []
    s = list(range(0, len(nodes)))
    for i in range(count):
        a = random.choice(s[:-1])
        a_index = s.index(a)
        b = random.choice(s[(a_index + 1):])
        b_index = s.index(b)

        if sample_without_replacement:
            s = s[:a_index] + s[(a_index + 1):b_index] + s[(b_index + 1):]

        if random.uniform(0, 1) > 0.5:
            paths.append((nodes[a], nodes[b]))
        else:
            paths.append((nodes[b], nodes[a]))

    return paths
Exemplo n.º 12
0
def start_olsr1_instances(ids, rmap):
    if ip_protocol == '6':
        eprint('warning: IPv6 support for olsr1 is broken/buggy')

    for id in ids:
        remote = rmap[id]

        if verbosity == 'verbose':
            address = remote.get('address', 'local')
            print(f'start olsr1 in {address}/ns-{id}')

        interface_down(remote, id, 'uplink')
        interface_up(remote, id, 'uplink')
        interface_flush(remote, id, 'uplink')

        if ip_protocol == '6':
            # IPv6 support seems to be broken/buggy!
            set_addr6(remote, id, 'uplink', 128)
        else:
            set_addr4(remote, id, 'uplink', 32)

        exec(remote,
             f'ip netns exec "ns-{id}" olsrd -d 0 -i "uplink" -f /dev/null')
Exemplo n.º 13
0
def update_link(link, link_command=None, rmap={}):
    source = str(link['source'])
    target = str(link['target'])
    remote1 = rmap.get(source)
    remote2 = rmap.get(target)

    ifname1 = f've-{source}-{target}'
    ifname2 = f've-{target}-{source}'

    if source == target:
        eprint(
            f'Warning: Cannot update link with identical source ({source}) and target ({target}) => ignore'
        )
        return

    if link_command is not None:
        # source -> target
        exec(
            remote1, 'ip netns exec "switch" ' +
            format_link_command(link_command, link, 'source', ifname1))
        # target -> source
        exec(
            remote2, 'ip netns exec "switch" ' +
            format_link_command(link_command, link, 'target', ifname2))
Exemplo n.º 14
0
def _get_random_paths(nodes, count=10, seed=None):
    if count > (len(nodes) * (len(nodes) - 1) // 2):
        eprint(f'Path count ({count}) too big to generate unique paths.')
        stop_all_terminals()
        exit(1)

    if seed is not None:
        random.seed(seed)

    def decode(items, i):
        k = math.floor((1 + math.sqrt(1 + 8 * i)) / 2)
        return (items[k], items[i - k * (k - 1) // 2])

    def rand_pair(n):
        return decode(random.randrange(n * (n - 1) // 2))

    def rand_pairs(items, npairs):
        n = len(items)
        return [
            decode(items, i)
            for i in random.sample(range(n * (n - 1) // 2), npairs)
        ]

    return rand_pairs(nodes, count)
Exemplo n.º 15
0
def _process_json(json_data):
    # in reality, only '@', ':', '/' and whitespace should cause problems
    name_re = re.compile(r'^[\w-]{1,6}$')
    links = {}
    nodes = {}

    for node in json_data.get('nodes', []):
        name = str(node['id'])
        if not name_re.match(name):
            eprint(f'Invalid node name: {name}')
            stop_all_terminals()
            exit(1)

        nodes[name] = node

    for link in json_data.get('links', []):
        source = str(link['source'])
        target = str(link['target'])

        if len(source) > 6:
            eprint(f'Node name too long: {source}')
            stop_all_terminals()
            exit(1)

        if len(target) > 6:
            eprint(f'Node name too long: {target}')
            stop_all_terminals()
            exit(1)

        if source not in nodes:
            nodes[source] = {'id': source}

        if target not in nodes:
            nodes[target] = {'id': target}

        if source > target:
            links[f'{source}-{target}'] = link
        else:
            links[f'{target}-{source}'] = link

    return (links, nodes)
Exemplo n.º 16
0
def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--verbosity',
                        choices=['verbose', 'normal', 'quiet'],
                        default='normal',
                        help='Set verbosity.')
    parser.add_argument(
        '--remotes',
        help='Distribute nodes and links on remotes described in the JSON file.'
    )
    parser.add_argument('--ip-protocol',
                        choices=['4', '6'],
                        help='Use IPv4/IPv6 only.')
    parser.set_defaults(to_state=None)

    subparsers = parser.add_subparsers(dest='action',
                                       required=True,
                                       help='Action')

    parser_start = subparsers.add_parser(
        'start', help='Start protocol daemons in every namespace.')
    parser_start.add_argument('protocol',
                              choices=protocol_choices,
                              help='Routing protocol to start.')
    parser_start.add_argument('to_state',
                              nargs='?',
                              default=None,
                              help='To state')

    parser_stop = subparsers.add_parser(
        'stop', help='Stop protocol daemons in every namespace.')
    parser_stop.add_argument('protocol',
                             choices=protocol_choices,
                             help='Routing protocol to stop.')
    parser_stop.add_argument('to_state',
                             nargs='?',
                             default=None,
                             help='To state')

    parser_change = subparsers.add_parser(
        'apply', help='Stop/Start protocol daemons in every namespace.')
    parser_change.add_argument('protocol',
                               choices=protocol_choices,
                               help='Routing protocol to change.')
    parser_change.add_argument('to_state',
                               nargs='?',
                               default=None,
                               help='To state')

    parser_run = subparsers.add_parser(
        'run', help='Execute any command in every namespace.')
    parser_run.add_argument(
        'command',
        nargs=argparse.REMAINDER,
        help='Shell command that is run. {name} is replaced by the nodes name.'
    )
    parser_run.add_argument('--quiet',
                            action='store_true',
                            help='Do not output stdout and stderr.')
    parser_run.add_argument('to_state',
                            nargs='?',
                            default=None,
                            help='To state')

    parser_clear = subparsers.add_parser('clear',
                                         help='Stop all routing protocols.')

    args = parser.parse_args()

    global ip_protocol
    ip_protocol = args.ip_protocol

    if args.remotes:
        with open(args.remotes) as file:
            args.remotes = json.load(file)
    else:
        args.remotes = default_remotes

    check_access(args.remotes)

    global verbosity
    verbosity = args.verbosity

    # get nodes that have been added or will be removed
    (old_ids, new_ids, rmap) = _get_update(args.to_state, args.remotes)

    if args.action == 'start':
        if args.to_state:
            start_routing_protocol(args.protocol, rmap, new_ids)
        else:
            all = list(rmap.keys())
            start_routing_protocol(args.protocol, rmap, all)
    elif args.action == 'stop':
        if args.to_state:
            stop_routing_protocol(args.protocol, rmap, old_ids)
        else:
            all = list(rmap.keys())
            stop_routing_protocol(args.protocol, rmap, all)
    elif args.action == 'apply':
        stop_routing_protocol(args.protocol, rmap, old_ids)
        start_routing_protocol(args.protocol, rmap, new_ids)
    elif args.action == 'clear':
        clear(args.remotes)
    elif args.action == 'run':
        run(' '.join(args.command), rmap, args.quiet)
    else:
        eprint('Unknown action: {}'.format(args.action))
        exit(1)

    stop_all_terminals()
Exemplo n.º 17
0
def _get_remote_mapping(cur_state, new_state, remotes, cur_state_rmap):
    def partition_into_subgraph_nodes(neighbor_map, nodes, rmap, remotes):
        random.shuffle(nodes)
        # remote_id => [<node_ids>]
        partitions = {}

        # keep running nodes on the same partition
        for node_id, remote in rmap.items():
            remote_id = remotes.index(remote)
            partitions.setdefault(remote_id, []).append(node_id)
            if node_id not in nodes:
                eprint(f'Node {node_id} not in previous state!')
                stop_all_terminals()
                exit(1)
            nodes.remove(node_id)

        for remote_id in range(len(remotes)):
            if len(nodes) == 0:
                break
            if remote_id not in partitions:
                partitions[remote_id] = [nodes.pop()]

        # find neighbor node of cluster
        def grow_cluster(cluster, nodes):
            for node in nodes:
                for cluster_node in cluster:
                    if node in neighbor_map[cluster_node]:
                        cluster.append(node)
                        nodes.remove(node)
                        return
            # cannot extend cluster via neighbor => use left over node
            cluster.append(nodes.pop())

        while len(nodes) > 0:
            # get smallest cluster (remote) key
            key = min(partitions.keys(), key=lambda k: len(partitions[k]))
            grow_cluster(partitions[key], nodes)

        return partitions

    # get node distribution balance
    def get_variance(partition):
        median = 0
        for remote_id, cluster in partition.items():
            median += len(cluster)
        median /= len(partition)

        q = 0
        for remote_id, cluster in partition.items():
            q = (len(cluster) - median)**2

        return math.sqrt(q / len(partition))

    def partition_to_map(partition, remotes):
        node_to_remote_map = {}
        for remote_id, node_ids in partition.items():
            for node_id in node_ids:
                node_to_remote_map[node_id] = remotes[remote_id]
        return node_to_remote_map

    '''
    # debug output
    def debug_partition(partition, remotes):
        print('partitioning:')

        for remote_id, cluster in partition.items():
            print('  {}: {} nodes'.format(remotes[remote_id].get('address', 'local'), len(cluster)))

        interconnects = 0
        node_to_remote_map = partition_to_map(partition, remotes)
        for link in new_state.get('links', []):
            if node_to_remote_map[str(link['source'])] is not node_to_remote_map[str(link['target'])]:
                interconnects += 1
        print(f'  l2tp links: {interconnects}')
    '''

    # try multiple random (connected) partitions and select the best
    neighbor_map = convert_to_neighbors(cur_state, new_state)
    tries = 20
    lowest_variance = math.inf
    best_partition = []

    # shortcut: if no mapping on multiple remotes is needed
    if len(remotes) == 1 and len(cur_state_rmap) == 0:
        return partition_to_map({0: neighbor_map.keys()}, remotes)

    for _ in range(tries):
        partition = partition_into_subgraph_nodes(neighbor_map,
                                                  list(neighbor_map.keys()),
                                                  cur_state_rmap, remotes)
        if partition:
            variance = get_variance(partition)
            if variance < lowest_variance:
                lowest_variance = variance
                best_partition = partition

    if len(best_partition) == 0:
        eprint('No network partition found.')
        stop_all_terminals()
        exit(1)

    #if verbosity in ['verbose', 'normal']:
    #    debug_partition(best_partition, remotes)

    # node_id => remote
    return partition_to_map(best_partition, remotes)
Exemplo n.º 18
0
def create_link(link, link_command=None, rmap={}):
    source = str(link['source'])
    target = str(link['target'])
    remote1 = rmap.get(source)
    remote2 = rmap.get(target)

    ifname1 = f've-{source}-{target}'
    ifname2 = f've-{target}-{source}'
    brname1 = f'br-{source}'
    brname2 = f'br-{target}'

    if source == target:
        eprint(
            f'Warning: Cannot create link with identical source ({source}) and target ({target}) => ignore'
        )
        return

    if remote1 == remote2:
        # create veth interface pair
        exec(
            remote1,
            f'ip netns exec "switch" ip link add "{ifname1}" type veth peer name "{ifname2}"'
        )
    else:
        # create l2tp connection
        addr1 = remote1.address
        addr2 = remote2.address

        # ids and port do not have to be the same on both remotes - but it is simpler that way
        tunnel_id = link_num(addr1, addr2, min=1, max=2**32)
        session_id = link_num(source, target, min=1, max=2**32)
        port = link_num(addr1, addr2, min=1024, max=2**16)

        if not l2tp_tunnel_exists(remote1, tunnel_id):
            exec(
                remote1,
                f'ip l2tp add tunnel tunnel_id {tunnel_id} peer_tunnel_id {tunnel_id} encap udp local {addr1} remote {addr2} udp_sport {port} udp_dport {port}'
            )
        exec(
            remote1,
            f'ip l2tp add session name {ifname1} tunnel_id {tunnel_id} session_id {session_id} peer_session_id {session_id}'
        )
        exec(remote1, f'ip link set "{ifname1}" netns "switch"')

        if not l2tp_tunnel_exists(remote2, tunnel_id):
            exec(
                remote2,
                f'ip l2tp add tunnel tunnel_id {tunnel_id} peer_tunnel_id {tunnel_id} encap udp local {addr2} remote {addr1} udp_sport {port} udp_dport {port}'
            )
        exec(
            remote2,
            f'ip l2tp add session name {ifname2} tunnel_id {tunnel_id} session_id {session_id} peer_session_id {session_id}'
        )
        exec(remote2, f'ip link set "{ifname2}" netns "switch"')

    configure_interface(remote1, 'switch', ifname1)
    configure_interface(remote2, 'switch', ifname2)

    # put into bridge
    exec(remote1,
         f'ip netns exec "switch" ip link set "{ifname1}" master "{brname1}"')
    exec(remote2,
         f'ip netns exec "switch" ip link set "{ifname2}" master "{brname2}"')

    # isolate interfaces (they can only speak to the downlink interface in the bridge they are)
    exec(
        remote1,
        f'ip netns exec "switch" bridge link set dev "{ifname1}" isolated on')
    exec(
        remote2,
        f'ip netns exec "switch" bridge link set dev "{ifname2}" isolated on')

    # e.g. execute tc command on link
    if link_command is not None:
        # source -> target
        exec(
            remote1, 'ip netns exec "switch" ' +
            format_link_command(link_command, link, 'source', ifname1))
        # target -> source
        exec(
            remote2, 'ip netns exec "switch" ' +
            format_link_command(link_command, link, 'target', ifname2))
Exemplo n.º 19
0
def main():
    parser = argparse.ArgumentParser(description='Ping various nodes.')
    parser.add_argument(
        '--remotes',
        help='Distribute nodes and links on remotes described in the JSON file.'
    )
    parser.add_argument('--input', help='JSON state of the network.')
    parser.add_argument('--interface',
                        help='Interface to send data over (autodetected).')
    parser.add_argument('--min-hops',
                        type=int,
                        help='Minimum hops to ping. Needs --input.')
    parser.add_argument('--max-hops',
                        type=int,
                        help='Maximum hops to ping. Needs --input.')
    parser.add_argument(
        '--pings',
        type=int,
        default=10,
        help='Number of pings (unique, no self, no reverse paths).')
    parser.add_argument('--duration',
                        type=int,
                        default=1000,
                        help='Spread pings over duration in ms.')
    parser.add_argument('-4',
                        action='store_true',
                        help='Force use of IPv4 addresses.')
    parser.add_argument('-6',
                        action='store_true',
                        help='Force use of IPv6 addresses.')

    args = parser.parse_args()

    if args.remotes:
        if not os.path.isfile(args.remotes):
            eprint(f'File not found: {args.remotes}')
            stop_all_terminals()
            exit(1)

        with open(args.remotes) as file:
            args.remotes = [Remote.from_json(obj) for obj in json.load(file)]
    else:
        args.remotes = default_remotes

    # need root for local setup
    for remote in args.remotes:
        if remote.address is None:
            if os.geteuid() != 0:
                eprint('Need to run as root.')
                exit(1)

    paths = None

    if args.input:
        state = json.load(args.input)
        paths = get_random_paths(network=state, count=args.pings)
        paths = filter_paths(state,
                             paths,
                             min_hops=args.min_hops,
                             max_hops=args.max_hops)
    else:
        if args.min_hops is not None or args.max_hops is not None:
            eprint(
                'No min/max hops available without topology information (--input)'
            )
            stop_all_terminals()
            exit(1)

        rmap = get_remote_mapping(args.remotes)
        all = list(rmap.keys())
        paths = _get_random_paths(nodes=all, count=args.pings)

    address_type = None
    if getattr(args, '4'):
        address_type = '4'
    if getattr(args, '6'):
        address_type = '6'

    ping(paths=paths,
         remotes=args.remotes,
         duration_ms=args.duration,
         interface=args.interface,
         verbosity='verbose',
         address_type=address_type)

    stop_all_terminals()
Exemplo n.º 20
0
def main():
    parser = argparse.ArgumentParser(
        formatter_class=argparse.RawDescriptionHelpFormatter,
        description=
        'Create a virtual network based on linux network names and virtual network interfaces:\n ./network.py change none test.json'
    )

    parser.add_argument('--verbosity',
                        choices=['verbose', 'normal', 'quiet'],
                        default='normal',
                        help='Set verbosity.')
    parser.add_argument(
        '--link-command',
        help=
        'Execute a command to change link properties. JSON elements are accessible. E.g. "myscript.sh {ifname} {tq}"'
    )
    parser.add_argument(
        '--node-command',
        help=
        'Execute a command to change link properties. JSON elements are accessible. E.g. "myscript.sh {ifname} {id}"'
    )
    parser.add_argument('--block-arp',
                        action='store_true',
                        help='Block ARP packets.')
    parser.add_argument('--block-multicast',
                        action='store_true',
                        help='Block multicast packets.')
    parser.add_argument(
        '--remotes',
        help='Distribute nodes and links on remotes described in the JSON file.'
    )

    subparsers = parser.add_subparsers(dest='action', required=True)

    parser_change = subparsers.add_parser(
        'apply', help='Create or change a virtual network.')
    parser_change.add_argument(
        'new_state',
        help=
        'JSON file that describes the target topology. Use "none" to remove all network namespaces.'
    )
    subparsers.add_parser(
        'show',
        help=
        'List all Linux network namespaces. Namespace "switch" is the special cable cabinet namespace.'
    )
    subparsers.add_parser(
        'clear',
        help=
        'Remove all Linux network namespaces. Processes still might need to be killed.'
    )

    args = parser.parse_args()

    block_arp = args.block_arp
    block_multicast = args.block_multicast
    verbosity = args.verbosity

    if args.remotes:
        with open(args.remotes) as file:
            args.remotes = json.load(file)
    else:
        args.remotes = default_remotes

    if args.action == 'clear':
        clear(args.remotes)
    elif args.action == 'show':
        show(args.remotes)
    elif args.action == 'apply':
        apply(args.new_state, args.node_command, args.link_command,
              args.remotes)
    else:
        eprint(f'Invalid command: {args.action}')
        exit(1)
    stop_all_terminals()
Exemplo n.º 21
0
def main():
    parser = argparse.ArgumentParser(description="Ping various nodes.")
    parser.add_argument(
        "--remotes",
        help=
        "Distribute nodes and links on remotes described in the JSON file.",
    )
    parser.add_argument("--input", help="JSON state of the network.")
    parser.add_argument("--interface",
                        help="Interface to send data over (autodetected).")
    parser.add_argument("--min-hops",
                        type=int,
                        help="Minimum hops to ping. Needs --input.")
    parser.add_argument("--max-hops",
                        type=int,
                        help="Maximum hops to ping. Needs --input.")
    parser.add_argument(
        "--pings",
        type=int,
        default=10,
        help="Number of pings (unique, no self, no reverse paths).",
    )
    parser.add_argument("--duration",
                        type=int,
                        default=1000,
                        help="Spread pings over duration in ms.")
    parser.add_argument(
        "--deadline",
        type=int,
        default=1,
        help=
        "Specify a timeout, in seconds, before ping exits regardless of how many packets have been sent or received. In this case ping does not stop after count packet are sent, it waits either for deadline expire or until count probes are answered or for some error notification from network.",
    )
    parser.add_argument(
        "--timeout",
        type=int,
        default=None,
        help=
        "Time to wait for a response, in seconds. The option affects only timeout in absence of any responses, otherwise ping waits for two RTTs.",
    )
    parser.add_argument("--path",
                        nargs=2,
                        help="Send pings from a node to another.")
    parser.add_argument("-4",
                        action="store_true",
                        help="Force use of IPv4 addresses.")
    parser.add_argument("-6",
                        action="store_true",
                        help="Force use of IPv6 addresses.")

    args = parser.parse_args()

    if args.remotes:
        if not os.path.isfile(args.remotes):
            eprint(f"File not found: {args.remotes}")
            stop_all_terminals()
            exit(1)

        with open(args.remotes) as file:
            args.remotes = [Remote.from_json(obj) for obj in json.load(file)]
    else:
        args.remotes = default_remotes

    # need root for local setup
    for remote in args.remotes:
        if remote.address is None:
            if os.geteuid() != 0:
                eprint("Need to run as root.")
                exit(1)

    paths = None

    if args.path:
        for ns in args.path:
            if not namespace_exists(args.remotes, ns):
                eprint(f"Namespace ns-{ns} does not exist")
                stop_all_terminals()
                exit(1)
        paths = [args.path]
    elif args.input:
        state = json.load(args.input)
        paths = get_random_paths(network=state, count=args.pings)
        paths = filter_paths(state,
                             paths,
                             min_hops=args.min_hops,
                             max_hops=args.max_hops)
    else:
        if args.min_hops is not None or args.max_hops is not None:
            eprint(
                "No min/max hops available without topology information (--input)"
            )
            stop_all_terminals()
            exit(1)

        rmap = get_remote_mapping(args.remotes)
        all = list(rmap.keys())
        paths = _get_random_paths(nodes=all, count=args.pings)

    address_type = None
    if getattr(args, "4"):
        address_type = "4"
    if getattr(args, "6"):
        address_type = "6"

    ping(
        paths=paths,
        remotes=args.remotes,
        duration_ms=args.duration,
        interface=args.interface,
        verbosity="verbose",
        address_type=address_type,
        ping_deadline=args.deadline,
        ping_timeout=args.timeout,
    )

    stop_all_terminals()
Exemplo n.º 22
0
def ping(
    paths,
    duration_ms=1000,
    remotes=default_remotes,
    interface=None,
    verbosity="normal",
    address_type=None,
    ping_deadline=1,
    ping_timeout=None,
):
    ping_count = 1
    rmap = get_remote_mapping(remotes)
    path_count = len(paths)

    # prepare ping tasks
    tasks = []
    for (source, target) in paths:
        source_remote = rmap[source]
        target_remote = rmap[target]

        if interface is None:
            interface = _get_interface(source_remote, source)

        target_addr = _get_ip_address(target_remote, target, interface,
                                      address_type)

        if target_addr is None:
            eprint(f"Cannot get address of {interface} in ns-{target}")
        else:
            debug = f"ping {source:>4} => {target:>4} ({target_addr:<18} / {interface})"
            command = (
                f"ip netns exec ns-{source} ping -c {ping_count} " +
                (f"-w {ping_deadline} " if ping_deadline is not None else "") +
                (f"-W {ping_timeout} " if ping_timeout is not None else "") +
                f"-D -I {interface} {target_addr}")
            tasks.append((source_remote, command, debug))

    processes = []
    started = 0

    def process_results():
        for (process, started_ms, debug, result) in processes:
            if not result.processed and process.poll() is not None:
                process.wait()
                (output, err) = process.communicate()
                _parse_ping(result, output.decode())
                result.processed = True

    # keep track of status ouput lines to delete them for updates
    lines_printed = 0

    def print_processes():
        nonlocal lines_printed

        # delete previous printed lines
        for _ in range(lines_printed):
            sys.stdout.write("\x1b[1A\x1b[2K")

        lines_printed = 0
        process_counter = 0
        for (process, started_ms, debug, result) in processes:
            process_counter += 1
            status = "???"
            if result.processed:
                if result.packet_loss == 0.0:
                    status = "success"
                elif result.packet_loss == 100.0:
                    status = "failed"
                else:
                    status = f"mixed ({result.packet_loss:0.2f}% loss)"
            else:
                status = "running"

            print(
                f"[{process_counter:03}:{started_ms:06}] {debug} => {status}")
            lines_printed += 1

    # start tasks in the given time frame
    start_ms = millis()
    last_processed = millis()
    tasks_count = len(tasks)
    while started < tasks_count:
        started_expected = math.ceil(tasks_count *
                                     ((millis() - start_ms) / duration_ms))
        if started_expected > started:
            for _ in range(0, started_expected - started):
                if len(tasks) == 0:
                    break

                (remote, command, debug) = tasks.pop()
                process = create_process(remote, command)
                started_ms = millis() - start_ms
                processes.append(
                    (process, started_ms, debug, _PingResult(ping_count)))

                # process results and print updates once per second
                if (last_processed + 1000) < millis():
                    last_processed = millis()
                    process_results()
                    if verbosity != "quiet":
                        print_processes()

                started += 1
        else:
            # sleep a small amount
            time.sleep(duration_ms / tasks_count / 1000.0 / 10.0)

    stop1_ms = millis()

    # wait until rest fraction of duration_ms is over
    if (stop1_ms - start_ms) < duration_ms:
        time.sleep((duration_ms - (stop1_ms - start_ms)) / 1000.0)

    stop2_ms = millis()

    process_results()
    if verbosity != "quiet":
        print_processes()

    # collect results
    rtt_avg_ms_count = 0
    ret = _PingStats()
    for (process, started_ms, debug, result) in processes:
        ret.send += result.send
        if result.processed:
            ret.received += int(result.send * (1.0 -
                                               (result.packet_loss / 100.0)))
            # failing ping outputs do not have rtt values
            if not math.isnan(result.rtt_avg):
                ret.rtt_avg_ms += result.rtt_avg
                rtt_avg_ms_count += 1

    if rtt_avg_ms_count > 0:
        ret.rtt_avg_ms /= float(rtt_avg_ms_count)

    result_duration_ms = stop1_ms - start_ms
    result_filler_ms = stop2_ms - stop1_ms

    if verbosity != "quiet":
        print(
            "pings send: {}, received: {} ({}), measurement span: {}ms".format(
                ret.send,
                ret.received,
                "-" if (ret.send == 0) else
                f"{100.0 * (ret.received / ret.send):0.2f}%",
                result_duration_ms + result_filler_ms,
            ))

    return ret
Exemplo n.º 23
0
def root():
    if os.geteuid() != 0:
        eprint('Need to run as root.')
        exit(1)
Exemplo n.º 24
0
def apply(state={},
          node_command=None,
          link_command=None,
          remotes=default_remotes):
    check_access(remotes)

    new_state = state
    (cur_state, cur_state_rmap) = get_current_state(remotes)

    # handle different new_state types
    if isinstance(new_state, str):
        if new_state == 'none':
            new_state = {}
        else:
            if not os.path.isfile(new_state):
                eprint(f'File not found: {new_state}')
                stop_all_terminals()
                exit(1)

            with open(new_state) as file:
                new_state = json.load(file)

    # map each node to a remote or local computer
    # distribute evenly with minimized interconnects
    rmap = _get_remote_mapping(cur_state, new_state, remotes, cur_state_rmap)
    data = _get_task(cur_state, new_state)

    beg_ms = millis()

    # add "switch" namespace
    if state_empty(cur_state):
        for remote in remotes:
            # add switch if it does not exist yet
            exec(remote, 'ip netns add "switch" || true')
            # disable IPv6 in switch namespace (no need, less overhead)
            exec(
                remote,
                'ip netns exec "switch" sysctl -q -w net.ipv6.conf.all.disable_ipv6=1'
            )

    for node in data.nodes_update:
        update_node(node, node_command, rmap)

    for link in data.links_update:
        update_link(link, link_command, rmap)

    for node in data.nodes_create:
        create_node(node, node_command, rmap)

    for link in data.links_create:
        create_link(link, link_command, rmap)

    for link in data.links_remove:
        remove_link(link, rmap)

    for node in data.nodes_remove:
        remove_node(node, rmap)

    # remove "switch" namespace
    if state_empty(new_state):
        for remote in remotes:
            exec(remote, 'ip netns del "switch" || true')

    # wait for tasks to complete
    wait_for_completion()
    end_ms = millis()

    if verbosity != 'quiet':
        print('Network setup in {}:'.format(format_duration(end_ms - beg_ms)))
        print(
            f'  nodes: {len(data.nodes_create)} created, {len(data.nodes_remove)} removed, {len(data.nodes_update)} updated'
        )
        print(
            f'  links: {len(data.links_create)} created, {len(data.links_remove)} removed, {len(data.links_update)} updated'
        )

    return new_state
Exemplo n.º 25
0
def ping_paths(paths,
               duration_ms=1000,
               remotes=default_remotes,
               interface=None,
               verbosity='normal'):
    ping_deadline = 1
    ping_count = 1
    processes = []
    start_ms = millis()
    started = 0
    rmap = get_remote_mapping(remotes)
    path_count = len(paths)
    while started < path_count:
        # number of expected tests to have been run
        started_expected = math.ceil(path_count *
                                     ((millis() - start_ms) / duration_ms))
        if started_expected > started:
            for _ in range(0, started_expected - started):
                if len(paths) == 0:
                    break
                (source, target) = paths.pop()

                source_remote = rmap[source]
                target_remote = rmap[target]

                if interface is None:
                    interface = _get_interface(source_remote, source)

                target_addr = _get_ip_address(target_remote, target, interface)

                if target_addr is None:
                    eprint(f'Cannot get address of {interface} in ns-{target}')
                    # count as started
                    started += 1
                else:
                    debug = '[{:06}] Ping {} => {} ({} / {})'.format(
                        millis() - start_ms, source, target, target_addr,
                        interface)
                    process = create_process(
                        source_remote,
                        f'ip netns exec ns-{source} ping -c {ping_count} -w {ping_deadline} -D {target_addr}'
                    )
                    processes.append((process, debug))
                    started += 1
        else:
            # sleep a small amount
            time.sleep(duration_ms / path_count / 1000.0 / 10.0)

    stop1_ms = millis()

    # wait until duration_ms is over
    if (stop1_ms - start_ms) < duration_ms:
        time.sleep((duration_ms - (stop1_ms - start_ms)) / 1000.0)

    stop2_ms = millis()

    ret = _PingResult()

    # wait/collect for results from pings (prolongs testing up to 1 second!)
    for (process, debug) in processes:
        process.wait()
        (output, err) = process.communicate()
        result = _parse_ping(output.decode())
        result.send = ping_count  # TODO: nicer

        ret.send += result.send
        ret.transmitted += result.transmitted
        ret.received += result.received
        ret.rtt_avg += result.rtt_avg

        if verbosity != 'quiet':
            if result.send != result.received:
                print(f'{debug} => failed')
            else:
                # success
                print(f'{debug}')

    ret.rtt_avg = 0 if ret.received == 0 else int(ret.rtt_avg / ret.received)
    result_duration_ms = stop1_ms - start_ms
    result_filler_ms = stop2_ms - stop1_ms

    if verbosity != 'quiet':
        print('send: {}, received: {}, arrived: {}%, measurement span: {}ms'.
              format(
                  ret.send, ret.received, '-' if (ret.send == 0) else
                  '{:0.2f}'.format(100.0 * (ret.received / ret.send)),
                  result_duration_ms + result_filler_ms))

    return ret
Exemplo n.º 26
0
def main():
    parser = argparse.ArgumentParser()
    parser.add_argument(
        '--remotes',
        help='Distribute nodes and links on remotes described in the JSON file.'
    )
    subparsers = parser.add_subparsers(dest='action', required=True)

    parser_traffic = subparsers.add_parser('traffic',
                                           help='Measure mean traffic.')
    parser_traffic.add_argument('--interface',
                                help='Interface to measure traffic on.')
    parser_traffic.add_argument('--duration',
                                type=int,
                                help='Measurement duration in seconds.')

    parser_ping = subparsers.add_parser('ping', help='Ping various nodes.')
    parser_ping.add_argument('--input', help='JSON state of the network.')
    parser_ping.add_argument(
        '--interface', help='Interface to send data over (autodetected).')
    parser_ping.add_argument('--min-hops',
                             type=int,
                             help='Minimum hops to ping. Needs --input.')
    parser_ping.add_argument('--max-hops',
                             type=int,
                             help='Maximum hops to ping. Needs --input.')
    parser_ping.add_argument(
        '--pings',
        type=int,
        default=10,
        help='Number of pings (unique, no self, no reverse paths).')
    parser_ping.add_argument('--duration',
                             type=int,
                             default=1000,
                             help='Spread pings over duration in ms.')

    args = parser.parse_args()

    if args.remotes:
        with open(args.remotes) as file:
            args.remotes = json.load(file)
    else:
        args.remotes = default_remotes

    # need root for local setup
    for remote in args.remotes:
        if remote.get('address') is None:
            if os.geteuid() != 0:
                eprint('Need to run as root.')
                exit(1)

    if args.action == 'traffic':
        rmap = get_remote_mapping(args.remotes)
        if args.duration:
            ds = args.duration
            ts_beg = traffic(args.remotes, interface=args.interface, rmap=rmap)
            time.sleep(ds)
            ts_end = traffic(args.remotes, interface=args.interface, rmap=rmap)
            ts = ts_end - ts_beg
            n = ds * len(rmap)
            print(
                f'rx: {format_size(ts.rx_bytes / n)}/s, {ts.rx_packets / n:.2f} packets/s, {ts.rx_dropped / n:.2f} dropped/s (avg. per node)'
            )
            print(
                f'tx: {format_size(ts.tx_bytes / n)}/s, {ts.tx_packets / n:.2f} packets/s, {ts.tx_dropped / n:.2f} dropped/s (avg. per node)'
            )
        else:
            ts = traffic(args.remotes, interface=args.interface, rmap=rmap)
            print(
                f'rx: {format_size(ts.rx_bytes)} / {ts.rx_packets} packets / {ts.rx_dropped} dropped'
            )
            print(
                f'tx: {format_size(ts.tx_bytes)} / {ts.tx_packets} packets / {ts.tx_dropped} dropped'
            )
    elif args.action == 'ping':
        paths = None

        if args.input:
            state = json.load(args.input)
            paths = get_random_paths(network=state, count=args.pings)
            paths = filter_paths(state,
                                 paths,
                                 min_hops=args.min_hops,
                                 max_hops=args.max_hops)
        else:
            if args.min_hops is not None or args.max_hops is not None:
                eprint(
                    'No min/max hops available without topology information (--input)'
                )
                stop_all_terminals()
                exit(1)

            rmap = get_remote_mapping(args.remotes)
            all = list(rmap.keys())
            paths = _get_random_paths(nodes=all, count=args.pings)

        ping_paths(paths=paths,
                   remotes=args.remotes,
                   duration_ms=args.duration,
                   interface=args.interface,
                   verbosity='verbose')
    else:
        eprint(f'Unknown action: {args.action}')
        exit(1)

    stop_all_terminals()
Exemplo n.º 27
0
def main():
    parser = argparse.ArgumentParser(
        formatter_class=argparse.RawDescriptionHelpFormatter,
        description=
        'Create a virtual network based on linux network names and virtual network interfaces:\n ./network.py change none test.json'
    )

    parser.add_argument('--verbosity',
                        choices=['verbose', 'normal', 'quiet'],
                        default='normal',
                        help='Set verbosity.')
    parser.add_argument(
        '--link-command',
        help=
        'Execute a command to change link properties. JSON elements are accessible. E.g. "myscript.sh {ifname} {tq}"'
    )
    parser.add_argument(
        '--node-command',
        help=
        'Execute a command to change link properties. JSON elements are accessible. E.g. "myscript.sh {ifname} {id}"'
    )
    parser.add_argument('--disable-arp',
                        action='store_true',
                        help='Disable ARP support on each interface.')
    parser.add_argument('--disable-multicast',
                        action='store_true',
                        help='Disable Multicast support each interface.')
    parser.add_argument(
        '--remotes',
        help='Distribute nodes and links on remotes described in the JSON file.'
    )
    parser.add_argument(
        '--mtu',
        type=int,
        default=1500,
        help='Set Maximum Transfer Unit (MTU) on each interface.')

    subparsers = parser.add_subparsers(dest='action', required=True)

    parser_change = subparsers.add_parser(
        'apply', help='Create or change a virtual network.')
    parser_change.add_argument(
        'new_state',
        help=
        'JSON file that describes the target topology. Use "none" to remove all network namespaces.'
    )
    subparsers.add_parser(
        'show',
        help=
        'List all Linux network namespaces. Namespace "switch" is the special cable cabinet namespace.'
    )
    subparsers.add_parser(
        'clear',
        help=
        'Remove all Linux network namespaces. Processes still might need to be killed.'
    )

    args = parser.parse_args()

    global disable_arp
    global disable_multicast
    global verbosity
    global mtu

    disable_arp = args.disable_arp
    disable_multicast = args.disable_multicast
    verbosity = args.verbosity
    mtu = args.mtu

    if args.remotes:
        if not os.path.isfile(args.remotes):
            eprint(f'File not found: {args.remotes}')
            stop_all_terminals()
            exit(1)

        with open(args.remotes) as file:
            args.remotes = [Remote.from_json(obj) for obj in json.load(file)]
    else:
        args.remotes = default_remotes

    if args.action == 'clear':
        clear(args.remotes)
    elif args.action == 'show':
        show(args.remotes)
    elif args.action == 'apply':
        apply(args.new_state, args.node_command, args.link_command,
              args.remotes)
    else:
        eprint(f'Invalid command: {args.action}')
        exit(1)
    stop_all_terminals()
Exemplo n.º 28
0
def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--verbosity',
                        choices=['verbose', 'normal', 'quiet'],
                        default='normal',
                        help='Set verbosity.')
    parser.add_argument(
        '--remotes',
        help='Distribute nodes and links on remotes described in the JSON file.'
    )
    parser.set_defaults(to_state=None)

    subparsers = parser.add_subparsers(dest='action',
                                       required=True,
                                       help='Action')

    parser_start = subparsers.add_parser(
        'start', help='Run start script in every namespace.')
    parser_start.add_argument('protocol',
                              help='Routing protocol script prefix.')
    parser_start.add_argument('to_state',
                              nargs='?',
                              default=None,
                              help='To state')

    parser_stop = subparsers.add_parser(
        'stop', help='Run stop script in every namespace.')
    parser_stop.add_argument('protocol',
                             help='Routing protocol script prefix.')
    parser_stop.add_argument('to_state',
                             nargs='?',
                             default=None,
                             help='To state')

    parser_change = subparsers.add_parser(
        'apply', help='Run stop/start scripts in every namespace.')
    parser_change.add_argument('protocol',
                               help='Routing protocol script prefix.')
    parser_change.add_argument('to_state',
                               nargs='?',
                               default=None,
                               help='To state')

    parser_run = subparsers.add_parser(
        'run', help='Execute any command in every namespace.')
    parser_run.add_argument(
        'command',
        nargs=argparse.REMAINDER,
        help=
        'Shell command that is run. Remote address and namespace id is added to call arguments.'
    )
    parser_run.add_argument('to_state',
                            nargs='?',
                            default=None,
                            help='To state')

    parser_copy = subparsers.add_parser('copy', help='Copy to all remotes.')
    parser_copy.add_argument('source', nargs='+')
    parser_copy.add_argument('destination')

    parser_clear = subparsers.add_parser(
        'clear', help='Run all stop scripts in every namespaces.')

    args = parser.parse_args()

    if args.remotes:
        if not os.path.isfile(args.remotes):
            eprint(f'File not found: {args.remotes}')
            stop_all_terminals()
            exit(1)

        with open(args.remotes) as file:
            args.remotes = [Remote.from_json(obj) for obj in json.load(file)]
    else:
        args.remotes = default_remotes

    check_access(args.remotes)

    global verbosity
    verbosity = args.verbosity

    # get nodes that have been added or will be removed
    (old_ids, new_ids, rmap) = _get_update(args.to_state, args.remotes)

    if args.action == 'start':
        ids = new_ids if args.to_state else list(rmap.keys())

        beg_ms = millis()
        _start_protocol(args.protocol, rmap, ids)
        end_ms = millis()

        if verbosity != 'quiet':
            print('started {} in {} namespaces in {}'.format(
                args.protocol, len(ids), format_duration(end_ms - beg_ms)))
    elif args.action == 'stop':
        ids = old_ids if args.to_state else list(rmap.keys())

        beg_ms = millis()
        _stop_protocol(args.protocol, rmap, ids)
        end_ms = millis()

        if verbosity != 'quiet':
            print('stopped {} in {} namespaces in {}'.format(
                args.protocol, len(ids), format_duration(end_ms - beg_ms)))
    elif args.action == 'apply':
        beg_ms = millis()
        _stop_protocol(args.protocol, rmap, old_ids)
        _start_protocol(args.protocol, rmap, new_ids)
        end_ms = millis()

        if verbosity != 'quiet':
            print('applied {} in {} namespaces in {}'.format(
                args.protocol, len(rmap.keys()),
                format_duration(end_ms - beg_ms)))
    elif args.action == 'clear':
        beg_ms = millis()
        clear(args.remotes)
        end_ms = millis()

        if verbosity != 'quiet':
            print('cleared on {} remotes in {}'.format(
                len(args.remotes), format_duration(end_ms - beg_ms)))
    elif args.action == 'copy':
        beg_ms = millis()
        copy(args.remotes, args.source, args.destination)
        end_ms = millis()

        if verbosity != 'quiet':
            print('copied on {} remotes in {}'.format(
                len(args.remotes), format_duration(end_ms - beg_ms)))
    elif args.action == 'run':
        ids = new_ids if args.to_state else list(rmap.keys())

        for id in ids:
            remote = rmap[id]
            label = remote.address or 'local'
            cmd = f'ip netns exec ns-{id} {" ".join(args.command)} {label} {id}'
            _exec_verbose(remote, cmd)
    else:
        eprint('Unknown action: {}'.format(args.action))
        stop_all_terminals()
        exit(1)

    stop_all_terminals()