diff --git a/QEMU-Readme.txt b/QEMU-Readme.txt new file mode 100644 index 00000000..ab5a0f39 --- /dev/null +++ b/QEMU-Readme.txt @@ -0,0 +1,92 @@ +# cohydra + +[![master](https://api.travis-ci.com/osmhpi/cohydra.svg?branch=master)](https://travis-ci.com/osmhpi/cohydra) + +## Contributors + + - Malte Andersch + - Arne Boockmeyer + - Felix Gohla + - Martin Michaelis + - Benedikt Schenkel + +## Installation + +### Installation With Docker + +Cohydra can be obtained via docker. +The easiest solution is using the VSCode *Remote - Containers* extension. +After cloning the repository and opening it in the container, your scenarios will by executing them with `python3.7`. + +Otherwise, you can build the [Dockerfile](./Dockerfile) in the project's root directory yourself by running `make`. In the container, cohydra will be added to your +`PYTHONPATH`. But you need to make sure, that you run the container with privileges to access the host network in order to have access to the host's network interfaces. You of course need to modify the volume mount to allow cohydra access to your scenarios. + +```sh +docker run -it --rm --cap-add=ALL -v /var/run/docker.sock:/var/run/docker.sock --net host --pid host --userns host --privileged osmhpi/cohydra:latest +``` + +The main image is based on the images in the [docker](./docker) directory. +The [`cohydra-base`](./docker/cohydra-base/Dockerfile) installs all neccessary dependencies for cohydra, +[`cohydra-dev`](./docker/cohydra-dev/Dockerfile) is for development purposes (docker-cli in the container). + +### Installation Without Docker + +In the case you do not want to use the prebuilt docker, a normal ns-3 installation with *NetAnim* Python bindings will work, too. +The Python libraries / directory provided by ns-3 has to be in your `PYTHONPATH`, though. +Cohydra so far has only been tested with **Debian 10 Buster** and **Ubuntu 18.04 Bionic Beaver**. + +There is no installation via `pip`. + + +## Marvis Docu: + +### Installation ++ Python3.7 + + sudo apt install python3.7-dev + + sudo apt install -y python3-pip + ++ NS-3 + + Download ns-3 python wheel from: https://github.com/osmhpi/python-wheels/releases + + wget https://github.com/osmhpi/python-wheels/releases/download/2020-04-07-15-51-05/ns-3.30-cp37-cp37m-linux_x86_64.whl + ++ Docker + + sudo apt install docker.io + ++ no tty present and no askpass program specified + + /etc/sudoers, where username=your_username + + Add `username ALL=(ALL) NOPASSWD: ALL` + +## QEMU information + +### QEMU installation ++ `sudo apt-get install qemu-system-x86 ` for x86 emulations or ++ `sudo apt-get install qemu-system` for other Systems + +### qcow2 file creation ++ Download a Linux image and create a qcow2 file + + `qemu-img create -f qcow2 xxx.qcow2` ++ Start the empty qcow2 file and install the downloaded OS (qemu-system must fit the selected OS ex. arm, x86, ...) + + `qemu-system-x86_64 -boot d -cdrom image.iso -m 512 -hda xxx.qcow2` + +### Example QEMU starting arguments for testing ++ Raspberry Pi image + + `qemu-system-arm -M versatilepb -kernel pathTo/kernel-qemu-4.14.79-stretch -append "root=/dev/sda2 panic=1 rootfstype=ext4 rw" -hda pathTo/raspbian-stretch-lite.qcow -dtb pathTo/QEMU/versatile-pb.dtb -cpu arm1176 -m 256 -no-reboot -machine versatilepb -net nic -net user,hostfwd=tcp::2222-:22,hostfwd=tcp::22280-:80` ++ Lubuntu image (kvm needs root) + + `sudo qemu-system-x86_64 -hda pathTo/lubuntu.qcow2 -m 512 -enable-kvm -no-reboot -net nic -serial mon:stdio -net user,hostfwd=tcp::2222-:22,hostfwd=tcp::22280-:80` + +### Preparations for usage (setup on QEMU image) ++ SSH + + Possible package: `openssh-client` + + If it won't start on system boot, a crontab can be used + + `sudo crontab -e` + + Add `@reboot service ssh restart` + + Copy ssh key from Host to VM + + ex. `ssh-copy-id -p 2222 -i pathTo/id_rsa lubuntu@127.0.0.1` ++ For the integration in Cohydra + + Add an IP address and IP route (host side is taken care by Cohydra) + + ex. `sudo crontab -e` + + Add `@reboot sudo ip addr add 12.0.1.2 dev eth0`, where the IP and eth0 device may be changed + + Add `@reboot sudo ip link set eth0 up` + + Add `@reboot sudo ip route add 12.0.1.0/24 dev eth0 protocol kernel src 12.0.1.2` + + Try deactivating `GRUB` or set the timeout to a low value to reduce booting time + diff --git a/examples/qemu_example.py b/examples/qemu_example.py new file mode 100644 index 00000000..a4607661 --- /dev/null +++ b/examples/qemu_example.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 + +from cohydra import ArgumentParser, Scenario, Network, DockerNode, QEMUNode, SwitchNode + +def main(): + scenario = Scenario() + + net = Network("10.0.0.0", "255.255.255.0") + + node1 = DockerNode('pong', docker_build_dir='./docker/pong') + node2 = QEMUNode('lubuntu', password='8559', ip='12.0.5.2', image_path='/home/julian/Master/GIT/hector_iot_testing_framework/VMs/lubuntu.qcow2', + username='lubuntu', system='qemu-system-x86_64', guest_interface='ens3', mac_address='52:54:00:12:34:56', qemu_options='-m 512 -enable-kvm') + #node3 = QEMUNode('rasp', password='raspberry', ip='12.0.6.2', image_path='/home/julian/Master/GIT/hector_iot_testing_framework/VMs/raspbian-stretch-lite.qcow', + # username='pi', system='qemu-system-arm', guest_interface='eth0', mac_address='52:54:00:12:34:55', + # qemu_options='-M versatilepb -kernel /home/julian/Master/GIT/hector_iot_testing_framework/QEMU/kernel-qemu-4.14.79-stretch -append \"root=/dev/sda2 panic=1 rootfstype=ext4 rw\" -dtb /home/julian/Master/GIT/hector_iot_testing_framework/QEMU/versatile-pb.dtb -cpu arm1176 -m 256 -machine versatilepb') + switch = SwitchNode('switch-1') + + net.connect(node1, switch, delay='40ms') + net.connect(node2, switch, delay='30ms') + #net.connect(node3, switch, delay='20ms') + + scenario.add_network(net) + + with scenario as sim: + # To simulate forever, just do not specifiy the simulation_time parameter. + sim.simulate() #simulation_time=180 + + +if __name__ == "__main__": + parser = ArgumentParser() + parser.run(main) diff --git a/marvis/__init__.py b/marvis/__init__.py index 123f0d73..8f469dfb 100644 --- a/marvis/__init__.py +++ b/marvis/__init__.py @@ -2,6 +2,6 @@ from .channel import Channel, CSMAChannel, WiFiChannel from .network import Network -from .node import Node, SwitchNode, DockerNode, LXDNode, ExternalNode, SSHNode, InterfaceNode +from .node import Node, SwitchNode, DockerNode, LXDNode, ExternalNode, SSHNode, InterfaceNode, QEMUNode from .scenario import Scenario from .argparse import ArgumentParser diff --git a/marvis/channel/csma.py b/marvis/channel/csma.py index 9d7700e6..4696b308 100644 --- a/marvis/channel/csma.py +++ b/marvis/channel/csma.py @@ -61,7 +61,7 @@ def __init__(self, network, nodes, delay="0ms", speed="100Mbps"): netmask = network.network.prefixlen address = ipaddress.ip_interface(f'{ip_address}/{netmask}') - interface = Interface(node=node, ns3_device=ns3_device, address=address) + interface = Interface(node=node, ns3_device=ns3_device, address=address, mac_address=node.get_custom_mac()) ns3_device.SetAddress(ns_net.Mac48Address(interface.mac_address)) node.add_interface(interface) self.interfaces.append(interface) diff --git a/marvis/channel/wifi.py b/marvis/channel/wifi.py index c36ab4dd..67e435e7 100644 --- a/marvis/channel/wifi.py +++ b/marvis/channel/wifi.py @@ -251,7 +251,7 @@ def __init__(self, network, nodes, frequency=None, channel=1, channel_width=40, netmask = network.network.prefixlen address = ipaddress.ip_interface(f'{ip_address}/{netmask}') - interface = Interface(node=node, ns3_device=ns3_device, address=address) + interface = Interface(node=node, ns3_device=ns3_device, address=address, mac_address=node.get_custom_mac()) ns3_device.GetMac().SetAddress(ns_net.Mac48Address(interface.mac_address)) node.add_interface(interface) self.interfaces.append(interface) diff --git a/marvis/command_executor/local.py b/marvis/command_executor/local.py index 6103d102..c9a1d402 100644 --- a/marvis/command_executor/local.py +++ b/marvis/command_executor/local.py @@ -21,7 +21,7 @@ class LocalCommandExecutor(CommandExecutor): def __init__(self, name=None): super().__init__(name) - def execute(self, command, user=None, shell=None, stdout_logfile=None, stderr_logfile=None): + def execute(self, command, user=None, shell=None, stdout_logfile=None, stderr_logfile=None, universal_newlines=None): if user is not None: raise ValueError('LocalCommandExecutor does not implement user argument') if stdout_logfile is not None or stderr_logfile is not None: @@ -36,11 +36,15 @@ def execute(self, command, user=None, shell=None, stdout_logfile=None, stderr_lo process = subprocess.Popen( # pylint: disable=subprocess-run-check command, shell=False if shell is None else shell, + universal_newlines=False if universal_newlines is None else universal_newlines, encoding='utf8', stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) + result = '' + for line in iter(process.stdout.readline, ''): + result += line.rstrip() out_thread = threading.Thread(target=log_file, args=(logger, logging.INFO, process.stdout, stdout_logfile)) err_thread = threading.Thread(target=log_file, args=(logger, logging.ERROR, process.stderr, stderr_logfile)) @@ -53,3 +57,4 @@ def execute(self, command, user=None, shell=None, stdout_logfile=None, stderr_lo if code != 0: raise ExitCode(code, command) + return result diff --git a/marvis/interface.py b/marvis/interface.py index 91ed0f33..8fb75b05 100644 --- a/marvis/interface.py +++ b/marvis/interface.py @@ -204,3 +204,16 @@ def setup_veth_container_end(self, ifname): index = ipr.link_lookup(ifname=ifname)[0] ipr.addr('add', index=index, address=str(self.address.ip), mask=self.address.network.prefixlen) ipr.link('set', index=index, state='up') + + def setup_qemu_host_address(self, address): + """Setup the management IP address of the host to the QEMU VM. + + Parameters + ---------- + ifname : str + The interface name on the conthost machine. + address : str + The address to communicate with the guest VM. + """ + ipr = IPRoute() + ipr.addr('add', index=ipr.link_lookup(ifname=self.bridge_name)[0], address=address, mask=24) \ No newline at end of file diff --git a/marvis/node/__init__.py b/marvis/node/__init__.py index 3e4f7000..39c4ee3c 100644 --- a/marvis/node/__init__.py +++ b/marvis/node/__init__.py @@ -8,3 +8,4 @@ from .external import ExternalNode from .interface import InterfaceNode from .ssh import SSHNode +from .qemu import QEMUNode diff --git a/marvis/node/base.py b/marvis/node/base.py index abb9cbe0..62d0eaee 100644 --- a/marvis/node/base.py +++ b/marvis/node/base.py @@ -118,6 +118,9 @@ def wants_ip_stack(self): :code:`True` indicates that a ns-3 IP stack shall be installed when preparing this node. """ raise NotImplementedError + + def get_custom_mac(self): + return None def execute_command(self, command, user=None): """Execute a command within the node. diff --git a/marvis/node/qemu.py b/marvis/node/qemu.py new file mode 100644 index 00000000..b7708e15 --- /dev/null +++ b/marvis/node/qemu.py @@ -0,0 +1,206 @@ +"""QEMU VM in the simulation.""" + +import logging +import os +import threading +import time +import paramiko + +from ..context import defer +from ..command_executor import LocalCommandExecutor, SSHCommandExecutor +from .base import Node + +logger = logging.getLogger(__name__) + +class QEMUNode(Node): + """A QEMUNode represents a QEMU VM. + + Parameters + ---------- + name : str + The name of the node (and QEMU instance). + It must consist only of *alphanumeric characters* and :code:`-`, :code:`_` and :code:`.`. + ip : str + The ip address for the connection to the QEMU VM. + Must be unique and not in use already. Cohydra will set it up automatically. + Format is `a.b.c.d`, where `d` not equal 1. + image_path : str + The (absolute or relative) path to the QEMU image file. + username : str + The username for the QEMU vm. + system: str + The system to start QEMU with. **Example:** `'qemu-system-x86_64'` + qemu_options : str + Additional QEMU starting parameters. + mac_address : str + The mac address of the QEMU VMs main network interface (guest_interface) will be set to the entered value. + guest_interface : str + Name of the guests main network interface for communication with the host. + copy_vm : bool + If set to :code:`True` the QEMU image will be copied and this copy will be deleted afterwards. + Currently not implemented! + """ + + def __init__(self, name, ip=None, image_path=None, username=None, password=None, system=None, qemu_options="", mac_address=None, guest_interface=None, copy_vm=True): + super().__init__(name) + + #: The ip of the VM's interface to communicate with the host and allow access via ssh. + self.ip = ip + #: The QEMU image to use. + self.image_path = image_path + #: The username. + self.username = username + #: The password. + self.password = password + #: The interface on the virtual machine for communication with the host. + self.guest_interface = guest_interface + + #: QEMU system to use for this image, ex: qemu-system-x86_64, qemu-system-arm, ... + self.system = system + + #: PID of the QEMU instance. Required to terminate process afterwards. + self.proc_id = None + + #: MAC address must be specified by the user to successfully add the device to Cohydras ns-3 network. + self.mac_address = mac_address + if mac_address is None: + raise Exception('Please specify the mac_address: xx:xx:xx:xx:xx:xx') + + #: Additional QEMU options. + self.qemu_options = f'-hda {self.image_path} {qemu_options} -no-reboot -daemonize -display none -net nic,macaddr={mac_address} -net tap' + + if image_path is None or system is None or guest_interface is None: + raise Exception('Please specify image path, system and guest_interfaces') + + #: For commands executed on the host machine. + self.local_command_executor = LocalCommandExecutor(self.name) + + #: For commands executed on the QEMU VM. + self.command_executor = None + + def wants_ip_stack(self): + return True + + def prepare(self, simulation): + """This runs a setup on network interfaces and starts the QEMU VM.""" + logger.info('Preparing node %s', self.name) + self.setup_host_interfaces() + self.start_qemu() + success = self.wait_for_connection() + if(success): + for interface in self.interfaces.values(): + self.setup_remote_address(interface.address) + + def copy_qemu_image(self): + # TODO + pass + + def setup_host_interfaces(self): + """Setup the interfaces (bridge, tap and IP on device) on the host and guest machine. + Generates management IP from given guest machine IP. + """ + host_ip = self.ip[0:self.ip.rfind('.')] + '.1' + for name, interface in self.interfaces.items(): + interface.setup_bridge() + interface.connect_tap_to_bridge() + for interface in self.interfaces.values(): + interface.setup_qemu_host_address(host_ip) + + def start_qemu(self): + """Start the QEMU VM with the additional parameters set by the user. + Creates an additional tap device for the communication with the host machine. + Enables proxy_arp and sets the local command executor up (commands on host side) + """ + logger.info('Starting QEMU instance: %s', self.name) + tap_name = f'{self.name}-tap' + + self.local_command_executor.execute(f'{self.system} {self.qemu_options},ifname={tap_name}', shell=True) + self.proc_id = self.get_pid('qemu', f'ifname={tap_name}') + for interface in self.interfaces.values(): + self.local_command_executor.execute(f'ip link set {tap_name} master {interface.bridge_name}', shell=True) + #self.local_command_executor.execute(f'echo 1 > /proc/sys/net/ipv4/conf/{self.host_interface}/proxy_arp', shell=True) + self.local_command_executor.execute(f'echo 1 > /proc/sys/net/ipv4/conf/{tap_name}/proxy_arp', shell=True) + defer(f'stop QEMU instance {self.name}', self.stop_qemu_vm) + + self.local_command_executor = LocalCommandExecutor(self.name) + + def stop_qemu_vm(self): + """Stop the QEMU VM.""" + #self.local_command_executor.execute(f'echo 0 > /proc/sys/net/ipv4/conf/{self.host_interface}/proxy_arp', shell=True) + if(self.proc_id != -1): + logger.info('Stopping QEMU vm: %s, with pid: %s', self.name, self.proc_id) + self.local_command_executor.execute(f'kill -9 {self.proc_id}', shell=True) + else: + logger.info('Could not retrieve pid from QEMU VM: %s. To terminate all running QEMU instances do: sudo pkill qemu', self.name) + + def wait_for_connection(self): + """Wait until the VM can be reached via SSH. Outputs logger info on timeout.""" + timeout = 0 + timeout_max = 12 + timeout_sleep = 5 + client = paramiko.SSHClient() + client.load_system_host_keys() + logger.info('Waiting ' + str(timeout_max*timeout_sleep) + ' seconds for QEMU VM %s to complete booting...', self.name) + while True: + try: + client.connect(self.ip, username=self.username, password=self.password) + self.command_executor = SSHCommandExecutor(self.name, client, sudo=True) + logger.info('ssh connection to %s successful.', self.ip) + return True + except Exception as ex: + #logger.info(ex) + if timeout > timeout_max: + logger.info("QEMU VM %s could not be reached.", self.name) + return False + time.sleep(timeout_sleep) + timeout += 1 + + def get_custom_mac(self): + """Returns the custom MAC address.""" + return self.mac_address + + def get_pid(self, grep, args): + """Returns the process id of a given process with the name `grep` and the specific arguments `args` + Required for terminating the process afterwards. + + Parameters + ---------- + grep : str + The name of the process + args : str + Additional grep parameter if more than one process with the given name (grep) exists + """ + qemu_pids = "" + try: + if args != "": + qemu_pids = self.local_command_executor.execute('pgrep ' + grep + ' -a | grep ' + args, shell=True, universal_newlines=True) + else: + qemu_pids = self.local_command_executor.execute('pgrep ' + grep + ' -a', shell=True, universal_newlines=True) + pids = str(qemu_pids[:-2]).split('\n') + for pid in pids: + return int(pid.lstrip().split(' ')[0]) + except Exception as ex: + logger.info('No processes %s running to retrieve pid', args) + #logger.info(ex) + return -1 + + def setup_remote_address(self, address): + """Add the simulation IP address to the remote device. + + Parameters + ---------- + address : str + The address to assign to the external node in simulation. + """ + self.command_executor.execute(f'sudo ip addr add {address} dev {self.guest_interface}', user='root') + defer(f'remove remote ip {address}', self.remove_remote_address, address) + + def remove_remote_address(self, address): + """Remove the simulation IP address from the remote device. + + Parameters + ---------- + address : str + The address to remove from the external node. + """ + self.command_executor.execute(f'sudo ip addr del {address} dev {self.guest_interface}', user='root') \ No newline at end of file