Use libvirt to start cuttlefish.
This change employs libvirt to set up virtual machine monitor for us.
Libvirt is a library for configuring and managing VM instances.
Change-Id: Ib42b0642f07c5309243e75e790d85e539d801fa4
(cherry picked from commit 0413c7e535e78472e187b83917ca4ba747b266fa)
diff --git a/README.md b/README.md
index 10f4cf1..0c2c55a 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,89 @@
-# Host-side binaries for Android Virtual Device.
+# Host-side binaries for Android Virtual Device
+
+## Launcher package
+
+### Requirements
+
+* Cuttlefish requires the following packages to be installed on your system:
+ * binaries
+ * python3
+ * libvirt-bin
+ * libvirt-dev
+ * qemu-2.8 or newer
+ * python packages (to be installed with pip):
+ * argparse
+ * eventfd
+ * glog
+ * linuxfd
+ * libvirt
+ * posix_ipc
+ * pylint (development purposes only)
+
+* Users running cuttlefish must be a member of a relevant group enabling them to
+ use `virsh` tool, eg. `libvirtd`.
+ * Group is created automatically when installing `libvirt-bin` package.
+ * Users may need to log out after their membership has been updated; optionally
+ you can use `newgrp` to switch currently active group to `libvirtd`.
+ * Once configured, users should be able to execute
+
+ ```sh
+ $ virsh -c qemu:///system net-list --all
+ Name State Autostart Persistent
+ ----------------------------------------------------------
+ [...]
+ ```
+
+ * You will need to update your configuration `/etc/libvirt/qemu.conf` to disable
+ dynamic permission management for image files. Uncomment and modify relevant
+ config line:
+
+ ```sh
+ dynamic_ownership = 1
+ user = "libvirt-qemu"
+ group = "kvm"
+ # Apparmor would stop us from creating files in /tmp.
+ # TODO(ender): find out a better way to manage these permissions.
+ security_driver = "none"
+ ```
+
+ and restart `libvirt-bin` service:
+
+ ```sh
+ sudo service libvirt-bin restart
+ ```
+
+* Make sure to start the `abr0` android bridge using
+ `experimental/etc/avd/android-metadata-server.sh` script.
+
+ TODO(ender): remove this and make this script part of init.
+
+### I'm seeing `permission denied` errors
+
+libvirt is not executing virtual machines on behalf of the calling user.
+Instead, it calls its own privileged process to configure VM on user's behalf.
+If you're seeing `permission denied` errors chances are that the QEmu does
+not have access to relevant files _OR folders_.
+
+To work with this problem, it's best to copy (not _link_!) all files QEmu would
+need to a separate folder (placed eg. under `/tmp` or `/run`), and give that
+folder proper permissions.
+
+```sh
+➜ ls -l /run/cf
+total 1569216
+drwxr-x--- 2 libvirt-qemu eng 180 Jun 28 14:27 .
+drwxr-xr-x 45 root root 2080 Jun 28 14:27 ..
+-rwxr-x--- 1 root root 2147483648 Jun 28 14:27 cache.img
+-rwxr-x--- 1 root root 10737418240 Jun 28 14:27 data.img
+-rwxr-x--- 1 root root 825340 Jun 28 14:27 gce_ramdisk.img
+-rwxr-x--- 1 root root 6065728 Jun 28 14:27 kernel
+-rwxr-x--- 1 root root 2083099 Jun 28 14:27 ramdisk.img
+-rwxr-x--- 1 root root 3221225472 Jun 28 14:27 system.img
+```
+
+**Note**: the `/run/cf` folder's owner is `libvirt-qemu:eng`. This allows QEmu
+to access images - and me to poke in the folder.
+
+Now don't worry about the `root` ownership. Libvirt manages permissions dynamically.
+You may want to give yourself write permissions to these files during development,
+though.
diff --git a/launcher/BUILD b/launcher/BUILD
index f5c0fd2..d57c02f 100644
--- a/launcher/BUILD
+++ b/launcher/BUILD
@@ -5,8 +5,9 @@
srcs = [
"channel.py",
"clientconnection.py",
- "errors.py",
+ "guest_definition.py",
"ivserver.py",
+ "libvirt_client.py",
"vmconnection.py",
"vsocsharedmem.py",
],
diff --git a/launcher/channel.py b/launcher/channel.py
index cf91da3..ae624d9 100644
--- a/launcher/channel.py
+++ b/launcher/channel.py
@@ -1,61 +1,70 @@
'''
- Related to the UNIX Domain Socket.
+ Related to the UNIX Domain Socket.
'''
import fcntl
+import os
import socket
+import stat
import struct
+import glog
# From include/uapi/asm-generic/ioctls.h
# #define FIONBIO 0x5421
_FIONBIO = 0x5421
def start_listener(path):
- uds = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
- uds.bind(path)
- uds.listen(5)
- return uds
+ uds = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ uds.bind(path)
+ uds.listen(5)
+ os.chmod(path,
+ stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR |
+ stat.S_IRGRP | stat.S_IWGRP | stat.S_IXGRP |
+ stat.S_IROTH | stat.S_IWOTH | stat.S_IXOTH)
+ glog.info('Socket %s ready.' % path)
+ return uds
def connect_to_channel(path):
- uds = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
- uds.connect(path)
- return uds
+ uds = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ uds.connect(path)
+ return uds
def handle_new_connection(uds, nonblocking=True):
- sock, addr = uds.accept()
+ sock, _ = uds.accept()
- if nonblocking:
- # Set the new socket to non-blocking mode.
- try:
- fcntl.ioctl(sock.fileno(), _FIONBIO, struct.pack('L', 1))
- except OSError as e:
- print('Failed in changing the socket to Non-blocking mode ' + e)
- return sock
+ if nonblocking:
+ # Set the new socket to non-blocking mode.
+ try:
+ fcntl.ioctl(sock.fileno(), _FIONBIO, struct.pack('L', 1))
+ except OSError as exc:
+ glog.exception('Exception caught while trying to make client nonblocking: ', exc)
+ return sock
-#
-# Send 8 bytes.
-#
+
def send_msg_8(sock, data):
- sock.sendmsg([bytes(struct.pack('q', data))])
+ """Send 8 bytes of data over the socket.
+ """
+ sock.sendmsg([bytes(struct.pack('q', data))])
+
def send_ctrl_msg(sock, fd, quad_data):
- sock.sendmsg([bytes(struct.pack('q', quad_data))],
- [(socket.SOL_SOCKET, socket.SCM_RIGHTS,
- bytes(struct.pack('i', fd)))])
+ sock.sendmsg([bytes(struct.pack('q', quad_data))],
+ [(socket.SOL_SOCKET, socket.SCM_RIGHTS,
+ bytes(struct.pack('i', fd)))])
def send_msg_utf8(sock, data):
- sock.sendall(bytes(data, encoding='ascii'))
+ sock.sendall(bytes(data, encoding='ascii'))
def recv_msg(sock, expected_length):
- chunks = []
- received = 0
- while received < expected_length:
- part = sock.recv(expected_length - received)
- received += len(part)
- chunks.append(part.decode(encoding='ascii'))
- data = ''.join(chunks)
- return data
+ chunks = []
+ received = 0
+ while received < expected_length:
+ part = sock.recv(expected_length - received)
+ received += len(part)
+ chunks.append(part.decode(encoding='ascii'))
+ data = ''.join(chunks)
+ return data
diff --git a/launcher/errors.py b/launcher/errors.py
deleted file mode 100644
index 09d0d34..0000000
--- a/launcher/errors.py
+++ /dev/null
@@ -1,7 +0,0 @@
-
-class VersionException(BaseException):
- def __init__(self):
- pass
-
- def __str__(self):
- return "Python 3 needed to run this program"
diff --git a/launcher/guest_definition.py b/launcher/guest_definition.py
new file mode 100644
index 0000000..9784a74
--- /dev/null
+++ b/launcher/guest_definition.py
@@ -0,0 +1,406 @@
+"""Guest definition.
+"""
+# pylint: disable=too-many-instance-attributes,no-self-use
+
+import os
+from xml.etree import ElementTree as ET
+import glog
+
+class GuestDefinition(object):
+ """Guest resource requirements definition.
+
+ Args:
+ lvc LibVirtClient instance.
+ """
+ def __init__(self, lvc):
+ self._cmdline = None
+ self._initrd = None
+ self._instance_name = None
+ self._instance_id = None
+ self._kernel = None
+ self._memory_mb = None
+ self._net_mobile_bridge = None
+ self._iv_vectors = None
+ self._iv_socket_path = None
+ self._iv_socket_path = None
+ self._part_cache = None
+ self._part_data = None
+ self._part_ramdisk = None
+ self._part_system = None
+ self._vcpus = None
+ self._vmm_path = None
+ # Initialize the following fields. These should not be configured by user.
+ self._num_net_interfaces = 0
+ self._num_ttys_interfaces = 0
+ self._num_virtio_channels = 0
+
+ self._lvc = lvc
+ self.set_instance_id(1)
+ self.set_num_vcpus(1)
+ self.set_memory_mb(512)
+ self.set_kernel('vmlinuz')
+ self.set_initrd('initrd.img')
+ self.set_net_mobile_bridge(None)
+
+
+ def set_instance_id(self, inst_id):
+ """Set instance ID of this guest.
+
+ Args:
+ inst_id Numerical instance ID, starting at 1.
+ """
+ if inst_id < 1 or inst_id > 255:
+ glog.error('Ignoring invalid instance id requested: %d.', inst_id)
+ return
+ self._instance_id = inst_id
+ self._instance_name = None
+
+
+ def get_instance_name(self):
+ """Return name of this instance.
+ """
+ if self._instance_name is None:
+ self._instance_name = self._lvc.build_instance_name(self._instance_id)
+ return self._instance_name
+
+
+ def set_num_vcpus(self, cpus):
+ """Set number of virtual CPUs for this guest.
+
+ Number of VCPUs will be checked for sanity and local maximums.
+ """
+ max_cpus = self._lvc.get_max_vcpus()
+ if cpus < 0 or cpus > max_cpus:
+ glog.error('Ignoring invalid number of vcpus requested (%d); ' +
+ 'max is %d.', cpus, max_cpus)
+ return
+ self._vcpus = cpus
+
+
+ def set_memory_mb(self, memory_mb):
+ """Set memory size allocated for the quest in MB.
+
+ Args:
+ memory_mb Total amount of memory allocated for the guest in MB.
+ """
+ if memory_mb < 0:
+ glog.error('Ignoring invalid amount of memory requested (%d).', memory_mb)
+ return
+ self._memory_mb = memory_mb
+
+
+ def set_kernel(self, kernel):
+ """Specify kernel path.
+
+ Args:
+ kernel Path to vmlinuz file.
+ """
+ if not kernel:
+ glog.error('Kernel path must be specified.')
+ return
+ self._kernel = kernel
+
+
+ def set_initrd(self, initrd):
+ """Specify initrd path.
+
+ Args:
+ initrd Path to initrd.img file.
+ """
+ if not initrd:
+ glog.error('Initial ramdisk path must be specified.')
+ return
+ self._initrd = initrd
+
+
+ def set_cmdline(self, cmdline):
+ """Specify kernel command line arguments.
+
+ Args:
+ cmdline Additional kernel command line arguments.
+ """
+ self._cmdline = cmdline
+
+
+ def set_cf_ramdisk_path(self, path):
+ """Specify cuttlefish ramdisk path.
+
+ Args:
+ path Cuttlefish built 'ramdisk.img' path.
+ """
+ if path is not None and not os.path.exists(path):
+ glog.warning("Cuttlefish ramdisk.img not found at %s", path)
+ self._part_ramdisk = path
+
+
+ def set_cf_system_path(self, path):
+ """Specify cuttlefish system path.
+
+ Args:
+ path Cuttlefish built 'system.img' path.
+ """
+ if path is not None and not os.path.exists(path):
+ glog.warning("Cuttlefish system.img not found at %s", path)
+ self._part_system = path
+
+
+ def set_cf_data_path(self, path):
+ """Specify cuttlefish data path.
+
+ Args:
+ path Cuttlefish built 'data.img' path.
+ """
+ if path is not None and not os.path.exists(path):
+ glog.warning("Cuttlefish data.img not found at %s", path)
+ self._part_data = path
+
+
+ def set_cf_cache_path(self, path):
+ """Specify cuttlefish cache path.
+
+ Args:
+ path Cuttlefish built 'cache.img' path.
+ """
+ if path is not None and not os.path.exists(path):
+ glog.warning("Cuttlefish cache.img not found at %s", path)
+ self._part_cache = path
+
+
+ def set_net_mobile_bridge(self, bridge):
+ """Specify mobile network bridge name.
+
+ Args:
+ bridge Name of the mobile network bridge.
+ """
+ if bridge is not None:
+ # TODO(ender): check if bridge exists.
+ pass
+ self._net_mobile_bridge = bridge
+
+
+ def set_vmm_path(self, path):
+ """Specify path to virtual machine monitor that will be running our guest.
+
+ Args:
+ path Path to virtual machine monitor, eg. /usr/bin/qemu.
+ """
+ self._vmm_path = path
+
+
+ def set_ivshmem_vectors(self, num):
+ """Specify number of IV Shared Memory vectors.
+
+ Args:
+ num Number of vectors (non-negative).
+ """
+ if num < 0:
+ glog.error('Invalid number of iv shared memory vectors: %d', num)
+ return
+ self._iv_vectors = num
+
+
+ def set_ivshmem_socket_path(self, path):
+ """Specify path to unix socket managed by IV Shared Memory daemon.
+
+ Args:
+ path Path to unix domain socket.
+ """
+ self._iv_socket_path = path
+
+
+ def _configure_vm(self, tree):
+ """Create basic guest details.
+
+ Args:
+ tree Top level 'domain' element of the XML tree.
+ """
+ ET.SubElement(tree, 'name').text = self.get_instance_name()
+ ET.SubElement(tree, 'on_poweroff').text = 'destroy'
+ ET.SubElement(tree, 'on_reboot').text = 'restart'
+ # TODO(ender): should this be restart?
+ ET.SubElement(tree, 'on_crash').text = 'destroy'
+ ET.SubElement(tree, 'vcpu').text = str(self._vcpus)
+ ET.SubElement(tree, 'memory').text = str(self._memory_mb << 10)
+
+
+ def _configure_kernel(self, tree):
+ """Configure boot parameters for guest.
+
+ Args:
+ tree Top level 'domain' element of the XML tree.
+ """
+ node = ET.SubElement(tree, 'os')
+ desc = ET.SubElement(node, 'type')
+ desc.set('arch', 'x86_64')
+ desc.set('machine', 'pc')
+ desc.text = 'hvm'
+
+ ET.SubElement(node, 'kernel').text = self._kernel
+ ET.SubElement(node, 'initrd').text = self._initrd
+ if self._cmdline is not None:
+ ET.SubElement(node, 'cmdline').text = self._cmdline
+
+
+ def _build_device_serial_port(self):
+ """Configure serial ports for guest.
+
+ More useful information can be found here:
+ https://libvirt.org/formatdomain.html#elementCharSerial
+ """
+ index = self._num_ttys_interfaces
+ self._num_ttys_interfaces += 1
+ path = '/tmp/%s-ttyS%d.log' % (self.get_instance_name(), index)
+ tty = ET.Element('serial')
+ tty.set('type', 'file')
+ src = ET.SubElement(tty, 'source')
+ src.set('path', path)
+ src.set('append', 'no')
+ ET.SubElement(tty, 'target').set('port', str(index))
+ glog.info('Serial port %d will send data to %s', index, path)
+ return tty
+
+
+ def _build_device_virtio_channel(self):
+ """Build fast paravirtualized virtio channel.
+
+ More useful information can be found here:
+ https://libvirt.org/formatdomain.html#elementCharSerial
+ """
+ index = self._num_virtio_channels
+ self._num_virtio_channels += 1
+ path = '/tmp/%s-vport0p%d.log' % (self.get_instance_name(), index)
+ vio = ET.Element('channel')
+ vio.set('type', 'file')
+ src = ET.SubElement(vio, 'source')
+ src.set('path', path)
+ src.set('append', 'no')
+ tgt = ET.SubElement(vio, 'target')
+ tgt.set('type', 'virtio')
+ tgt.set('name', 'vport0p%d' % index)
+ adr = ET.SubElement(vio, 'address')
+ adr.set('type', 'virtio-serial')
+ adr.set('controller', '0')
+ adr.set('bus', '0')
+ adr.set('port', str(index))
+ glog.info('Virtio channel %d will send data to %s', index, path)
+ return vio
+
+
+ def _build_device_disk_node(self, path, name, target_dev):
+ """Create disk node for guest.
+
+ More useful information can be found here:
+ https://libvirt.org/formatdomain.html#elementsDisks
+
+ Args:
+ path Path to file containing partition or disk image.
+ name Purpose of partition or disk image.
+ target_dev Target device.
+ """
+ bus = 'ide'
+ if target_dev.startswith('sd'):
+ bus = 'sata'
+ elif target_dev.startswith('vd'):
+ bus = 'virtio'
+
+ if path is None:
+ glog.fatal('No file specified for %s; (%s) %s is not available.',
+ name, bus, target_dev)
+ return None
+
+ disk = ET.Element('disk')
+ disk.set('type', 'file')
+ # disk.set('snapshot', 'external')
+ drvr = ET.SubElement(disk, 'driver')
+ drvr.set('name', 'qemu')
+ drvr.set('type', 'raw')
+ drvr.set('io', 'threads')
+ trgt = ET.SubElement(disk, 'target')
+ trgt.set('dev', target_dev)
+ trgt.set('bus', bus)
+ srce = ET.SubElement(disk, 'source')
+ srce.set('file', path)
+ return disk
+
+
+ def _build_mac_address(self, index):
+ """Create mac address from local instance number.
+ """
+ return '00:41:56:44:%02X:%02X' % (self._instance_id, index + 1)
+
+
+ def _build_device_net_node(self, local_node, bridge):
+ """Create virtual ethernet for guest.
+
+ More useful information can be found here:
+ https://libvirt.org/formatdomain.html#elementsNICSVirtual
+ https://wiki.libvirt.org/page/Virtio
+
+ Args:
+ local_node Name of the local interface.
+ bridge Name of the corresponding bridge.
+ """
+ index = self._num_net_interfaces
+ self._num_net_interfaces += 1
+ net = ET.Element('interface')
+ net.set('type', 'bridge')
+ ET.SubElement(net, 'source').set('bridge', bridge)
+ ET.SubElement(net, 'mac').set('address', self._build_mac_address(index))
+ ET.SubElement(net, 'model').set('type', 'e1000')
+ ET.SubElement(net, 'target').set('dev', '%s%d' % (local_node, self._instance_id))
+ return net
+
+ def _configure_devices(self, tree):
+ """Configure guest devices.
+
+ Args:
+ tree Top level 'domain' element of the XML tree.
+ """
+ dev = ET.SubElement(tree, 'devices')
+ if self._vmm_path:
+ ET.SubElement(dev, 'emulator').text = self._vmm_path
+ dev.append(self._build_device_serial_port())
+ dev.append(self._build_device_virtio_channel())
+ dev.append(self._build_device_disk_node(self._part_ramdisk, 'ramdisk', 'vda'))
+ dev.append(self._build_device_disk_node(self._part_system, 'system', 'vdb'))
+ dev.append(self._build_device_disk_node(self._part_data, 'data', 'vdc'))
+ dev.append(self._build_device_disk_node(self._part_cache, 'cache', 'vdd'))
+ dev.append(self._build_device_net_node('amobile', self._net_mobile_bridge))
+
+
+ def _configure_ivshmem(self, tree):
+ """Configure InterVM Shared Memory region.
+
+ Args:
+ tree Top level 'domain' element of the XML tree.
+ """
+ if self._iv_vectors:
+ cmd = ET.SubElement(tree, 'qemu:commandline')
+ ET.SubElement(cmd, 'qemu:arg').set('value', '-chardev')
+ ET.SubElement(cmd, 'qemu:arg').set(
+ 'value', 'socket,path=%s,id=ivsocket' % (self._iv_socket_path))
+ ET.SubElement(cmd, 'qemu:arg').set('value', '-device')
+ ET.SubElement(cmd, 'qemu:arg').set(
+ 'value', 'ivshmem-doorbell,chardev=ivsocket,vectors=%d' % (self._iv_vectors)
+ )
+
+
+ def to_xml(self):
+ """Build XML document describing guest properties.
+
+ The created document will be used directly by libvirt to create corresponding
+ virtual machine.
+
+ Returns:
+ string containing an XML document describing a VM.
+ """
+ tree = ET.Element('domain')
+ tree.set('type', 'kvm')
+ tree.set('xmlns:qemu', 'http://libvirt.org/schemas/domain/qemu/1.0')
+
+ self._configure_vm(tree)
+ self._configure_kernel(tree)
+ self._configure_devices(tree)
+ self._configure_ivshmem(tree)
+
+ return ET.tostring(tree).decode('utf-8')
diff --git a/launcher/ivserver.py b/launcher/ivserver.py
index 486d55f..a7a645d 100644
--- a/launcher/ivserver.py
+++ b/launcher/ivserver.py
@@ -1,227 +1,205 @@
'''
- ivshmem server main
+ ivshmem server main
'''
+# pylint: disable=too-many-instance-attributes,relative-beyond-top-level
+
import argparse
import json
-import linuxfd
import os
import select
-import subprocess
+import signal
import sys
+import threading
+import glog
+from .libvirt_client import LibVirtClient
+from .guest_definition import GuestDefinition
from . import clientconnection
from . import channel
-from . import errors
from . import vmconnection
from . import vsocsharedmem
-#
-# eventfd for synchronizing ivshmemserver initialization and QEMU launch.
-# Also used to pass the vector count to QEMU.
-#
-efd = {
- 'efd' : None
-}
+class IVServer(object):
+ def __init__(self, region_name, region_size,
+ vm_socket_path, client_socket_path, layout_json):
+ self.layout_json = layout_json
-class IVServer():
- def __init__(self, args, layout_json):
- self.args = args
- self.layout_json = layout_json
+ # Create the SharedMemory. Linux zeroes out the contents initially.
+ self.shmobject = vsocsharedmem.VSOCSharedMemory(region_size, region_name)
- # Create the SharedMemory. Linux zeroes out the contents initially.
- self.shmobject = vsocsharedmem.VSOCSharedMemory(self.args.size,
- self.args.name)
+ # Populate the shared memory with the data from layout description.
+ self.shmobject.create_layout(self.layout_json)
- # Populate the shared memory with the data from layout description.
- self.shmobject.create_layout(self.layout_json)
+ # get the number of vectors. This will be passed to qemu.
+ self.num_vectors = self.get_vector_count()
- # get the number of vectors. This will be passed to qemu.
- self.num_vectors = self.get_vector_count()
+ # Establish the listener socket for QEMU.
+ self.vm_listener_socket_path = vm_socket_path
+ self.vm_listener_socket = channel.start_listener(vm_socket_path)
- # Establish the listener socket for QEMU.
- self.vm_listener_socket = channel.start_listener(self.args.path)
+ # Establish the listener socket for Clients.
+ self.client_listener_socket = channel.start_listener(client_socket_path)
- # Establish the listener socket for Clients.
- self.client_listener_socket = channel.start_listener(self.args.client)
+ self.vm_connection = None
+ self.client_connection = None
- self.vm_connection = None
- self.client_connection = None
+ # _control_channel and _thread_channel are two ends of the same, control pipe.
+ # _thread_channel will be used by the serving thread until pipe is closed.
+ (self._control_channel, self._thread_channel) = os.pipe()
+ self._thread = threading.Thread(target=self._serve_in_background)
+
+ def get_vector_count(self):
+ # TODO: Parse from json instead of picking it up from shmobject.
+ return self.shmobject.num_vectors
+
+ def get_socket_path(self):
+ return self.vm_listener_socket_path
+
+ def serve(self):
+ """Begin serving IVShMem data to QEmu.
+ """
+ self._thread.start()
+
+ def stop(self):
+ """Stop serving data to QEmu and join the serving thread.
+ """
+ os.close(self._control_channel)
+ self._thread.join()
+
+ def _serve_in_background(self):
+ readfdlist = [
+ self.vm_listener_socket,
+ self.client_listener_socket,
+ self._thread_channel
+ ]
+
+ while True:
+ readable, _, _ = select.select(readfdlist, [], [])
+
+ for client in readable:
+ if client == self.vm_listener_socket:
+ self.handle_new_vm_connection(client)
+ elif client == self.client_listener_socket:
+ self.handle_new_client_connection(client)
+ elif client == self._thread_channel:
+ # For now we do not expect any communication over pipe.
+ # Since this bit of code is going away, we'll just assume
+ # that the parent wants the thread to exit.
+ return
- # TODO: Parse from json instead of picking it up from shmobject.
- def get_vector_count(self):
- return self.shmobject.num_vectors
+ def handle_new_client_connection(self, listenersocket):
+ client_socket = channel.handle_new_connection(listenersocket,
+ nonblocking=False)
+ print(client_socket)
+ self.client_connection = \
+ clientconnection.ClientConnection(client_socket,
+ self.shmobject.posix_shm.fd,
+ self.layout_json,
+ 0)
+ self.client_connection.handshake()
- def serve(self):
- readfdlist = [self.vm_listener_socket, self.client_listener_socket]
- writefdlist = []
- exceptionfdlist = [self.vm_listener_socket, self.client_listener_socket]
- #
- # We are almost ready.
- # There still may be a race between the following select and QEMU
- # execution.
- #
- efd['efd'].write(self.num_vectors)
- while True:
- readable, writeable, exceptions = \
- select.select(readfdlist, writefdlist, exceptionfdlist)
+ def handle_new_vm_connection(self, listenersocket):
+ vm_socket = channel.handle_new_connection(listenersocket)
+ print(vm_socket)
+ self.vm_connection = vmconnection.VMConnection(self.layout_json,
+ self.shmobject.posix_shm,
+ vm_socket,
+ self.num_vectors,
+ hostid=0,
+ vmid=1)
+ self.vm_connection.handshake()
- if exceptions:
- print(exceptions)
- return
-
- for listenersocket in readable:
- if listenersocket == self.vm_listener_socket:
- self.handle_new_vm_connection(listenersocket)
- elif listenersocket == self.client_listener_socket:
- self.handle_new_client_connection(listenersocket)
-
-
- def handle_new_client_connection(self, listenersocket):
- client_socket = channel.handle_new_connection(listenersocket,
- nonblocking=False)
- print(client_socket)
- self.client_connection = \
- clientconnection.ClientConnection(client_socket,
- self.shmobject.posix_shm.fd,
- self.layout_json,
- 0)
- self.client_connection.handshake()
-
-
- def handle_new_vm_connection(self, listenersocket):
- vm_socket = channel.handle_new_connection(listenersocket)
- print(vm_socket)
- self.vm_connection = vmconnection.VMConnection(self.layout_json,
- self.shmobject.posix_shm,
- vm_socket,
- self.num_vectors,
- hostid=0,
- vmid=1)
- self.vm_connection.handshake()
def setup_arg_parser():
- def unsigned_integer(size):
- size = int(size)
- if size < 1:
- raise argparse.ArgumentTypeError('should be >= 1 but we have %r' % size)
- return size
- parser = argparse.ArgumentParser()
- parser.add_argument('-c', '--cpu', type=unsigned_integer, default=2,
- help='Number of cpus to use in the guest')
- parser.add_argument('-C', '--client', type=str,
- default='/tmp/ivshmem_socket_client')
- parser.add_argument('-i', '--image_dir', type=str, required=True,
- help='Path to the directory of image files for the guest')
- parser.add_argument('-s', '--script_dir', type=str, required=True,
- help='Path to a directory of scripts')
- parser.add_argument('-I', '--instance_number', type=unsigned_integer,
- default=1,
- help='Instance number for this device')
- parser.add_argument('-L', '--layoutfile', type=str, required=True)
- parser.add_argument('-M', '--memory', type=unsigned_integer, default=2048,
- help='Size of the non-shared guest RAM in MiB')
- parser.add_argument('-N', '--name', type=str, default='ivshmem',
- help='Name of the POSIX shared memory segment')
- parser.add_argument('-P', '--path', type=str, default='/tmp/ivshmem_socket',
- help='Path to UNIX Domain Socket, default=/tmp/ivshmem_socket')
- parser.add_argument('-S', '--size', type=unsigned_integer, default=4,
- help='Size of shared memory region in MiB, default=4MiB')
- return parser
-
-
-def make_telnet_chardev(args, tcp_base, name):
- return 'socket,nowait,server,host=127.0.0.1,port=%d,ipv4,nodelay,id=%s' % (
- tcp_base + args.instance_number, name)
-
-
-def make_network_netdev(name, args):
- return ','.join((
- 'type=tap',
- 'id=%s' % name,
- 'ifname=android%d' % args.instance_number,
- 'script=%s/android-ifup' % args.script_dir,
- 'downscript=%s/android-ifdown' % args.script_dir))
-
-
-def make_network_device(name, instance_number):
- mac_addr = '00:41:56:44:%02X:%02X' % (
- instance_number / 10, instance_number % 10)
- return 'e1000,netdev=%s,mac=%s' % (name, mac_addr)
+ def unsigned_integer(size):
+ size = int(size)
+ if size < 1:
+ raise argparse.ArgumentTypeError(
+ 'should be >= 1 but we have %r' % size)
+ return size
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-c', '--cpu', type=unsigned_integer, default=2,
+ help='Number of cpus to use in the guest')
+ parser.add_argument('-C', '--client', type=str,
+ default='/tmp/ivshmem_socket_client')
+ parser.add_argument('-i', '--image_dir', type=str, required=True,
+ help='Path to the directory of image files for the guest')
+ parser.add_argument('-s', '--script_dir', type=str, required=True,
+ help='Path to a directory of scripts')
+ parser.add_argument('-I', '--instance_number', type=unsigned_integer,
+ default=1,
+ help='Instance number for this device')
+ parser.add_argument('-L', '--layoutfile', type=str, required=True)
+ parser.add_argument('-M', '--memory', type=unsigned_integer, default=2048,
+ help='Size of the non-shared guest RAM in MiB')
+ parser.add_argument('-N', '--name', type=str, default='ivshmem',
+ help='Name of the POSIX shared memory segment')
+ parser.add_argument('-P', '--path', type=str, default='/tmp/ivshmem_socket',
+ help='Path to UNIX Domain Socket, default=/tmp/ivshmem_socket')
+ parser.add_argument('-S', '--size', type=unsigned_integer, default=4,
+ help='Size of shared memory region in MiB, default=4MiB')
+ return parser
def check_version():
- if sys.version_info.major != 3:
- raise errors.VersionException
+ if sys.version_info.major != 3:
+ glog.fatal('This program requires python3 to work.')
+ sys.exit(1)
def main():
- check_version()
- parser = setup_arg_parser()
- args = parser.parse_args()
- layout_json = json.loads(open(args.layoutfile).read())
- efd['efd'] = linuxfd.eventfd(initval=0,
- semaphore=False,
- nonBlocking=False,
- closeOnExec=True)
- pid = os.fork()
+ glog.setLevel(glog.INFO)
+ try:
+ check_version()
+ parser = setup_arg_parser()
+ args = parser.parse_args()
- if pid:
- ivshmem_server = IVServer(args, layout_json)
- ivshmem_server.serve()
- else:
- #
- # wait for server to complete initialization
- # the initializing process will also write the
- # number of vectors as a part of signaling us.
- #
- num_vectors = efd['efd'].read()
- qemu_args = []
- qemu_args.append(layout_json['guest']['vmm_path'])
- qemu_args += ('-smp', '%d' % args.cpu)
- qemu_args += ('-m', '%d' % args.memory)
- qemu_args.append('-enable-kvm')
- qemu_args.append('-nographic')
- # Serial port setup
- qemu_args += (
- '-chardev', make_telnet_chardev(args, 10000, 'serial_kernel'),
- '-device', 'isa-serial,chardev=serial_kernel')
- qemu_args += (
- '-device', 'virtio-serial',
- '-chardev', make_telnet_chardev(args, 10100, 'serial_logcat'),
- '-device', 'virtserialport,chardev=serial_logcat')
- # network setup
- qemu_args += (
- '-netdev', make_network_netdev('net0', args),
- '-device', make_network_device('net0', args.instance_number)
- )
- # Configure image files
- launchable = True
- # Use %% to protect the path. The index will be set outside the loop,
- # the path will be set inside.
- DRIVE_VALUE = 'file=%%s,index=%d,format=raw,if=virtio,media=disk'
- for flag, template, path in (
- ('-kernel', '%s', 'kernel'),
- ('-initrd', '%s', 'gce_ramdisk.img'),
- ('-drive', DRIVE_VALUE % 0, 'ramdisk.img'),
- ('-drive', DRIVE_VALUE % 1, 'system.img'),
- ('-drive', DRIVE_VALUE % 2, 'data-%d.img' % args.instance_number),
- ('-drive', DRIVE_VALUE % 3, 'cache-%d.img' % args.instance_number)):
- full_path = os.path.join(args.image_dir, path)
- if not os.path.isfile(full_path):
- print('Missing required image file %s' % full_path)
- launchable = False
- qemu_args += (flag, template % full_path)
- # TODO(romitd): Should path and id be configured per-instance?
- qemu_args += ('-chardev', 'socket,path=%s,id=ivsocket' % (args.path))
- qemu_args += (
- '-device', ('ivshmem-doorbell,chardev=ivsocket,vectors=%d' %
- num_vectors))
- qemu_args += ('-append', ' '.join(layout_json['guest']['kernel_command_line']))
- if not launchable:
- print('Refusing to launch due to errors')
- sys.exit(2)
- subprocess.Popen(qemu_args)
+ lvc = LibVirtClient()
+
+ layout_json = json.loads(open(args.layoutfile).read())
+ ivshmem_server = IVServer(args.name, args.size, args.path, args.client, layout_json)
+
+ if 'movbe' not in lvc.get_cpu_features():
+ glog.warning('host CPU may not support movbe instruction')
+
+ guest = GuestDefinition(lvc)
+
+ guest.set_num_vcpus(args.cpu)
+ guest.set_memory_mb(args.memory)
+ guest.set_instance_id(args.instance_number)
+ guest.set_cmdline(' '.join(layout_json['guest']['kernel_command_line']))
+ guest.set_kernel(os.path.join(args.image_dir, 'kernel'))
+ guest.set_initrd(os.path.join(args.image_dir, 'gce_ramdisk.img'))
+
+ guest.set_cf_ramdisk_path(os.path.join(args.image_dir, 'ramdisk.img'))
+ guest.set_cf_system_path(os.path.join(args.image_dir, 'system.img'))
+ guest.set_cf_data_path(os.path.join(args.image_dir, 'data.img'))
+ guest.set_cf_cache_path(os.path.join(args.image_dir, 'cache.img'))
+ guest.set_net_mobile_bridge('abr0')
+
+ guest.set_ivshmem_vectors(ivshmem_server.get_vector_count())
+ guest.set_ivshmem_socket_path(ivshmem_server.get_socket_path())
+
+ guest.set_vmm_path(layout_json['guest']['vmm_path'])
+
+ # Accept and process IVShMem connections from QEmu.
+ ivshmem_server.serve()
+
+ glog.info('Creating virtual instance...')
+ dom = lvc.create_instance(guest.to_xml())
+ glog.info('VM ready.')
+ dom.resume()
+ try:
+ signal.pause()
+ except KeyboardInterrupt:
+ glog.info('Stopping IVShMem server')
+ dom.destroy()
+ ivshmem_server.stop()
+
+
+ except Exception as exception:
+ glog.exception('Could not start VM: %s', exception)
if __name__ == '__main__':
- main()
+ main()
diff --git a/launcher/launcher.sh b/launcher/launcher.sh
index f1bcaf4..4219337 100755
--- a/launcher/launcher.sh
+++ b/launcher/launcher.sh
@@ -1,9 +1,9 @@
#!/bin/bash
-rm -f /tmp/ivshmem_socket /tmp/ivshmem_socket_client
+trap 'rm -f /tmp/ivshmem_socket /tmp/ivshmem_socket_client' INT KILL EXIT
DIR=$(dirname $(realpath $0))
WS=$(bazel info workspace)
BIN=$(bazel info bazel-bin)
DIR=${DIR##${WS}}
bazel build /${DIR}:launcher
-sudo ${BIN}/${DIR}/launcher "$@"
+${BIN}/${DIR}/launcher "$@"
diff --git a/launcher/libvirt_client.py b/launcher/libvirt_client.py
new file mode 100644
index 0000000..864f95d
--- /dev/null
+++ b/launcher/libvirt_client.py
@@ -0,0 +1,114 @@
+"""libvirt interface.
+
+Primary purpose of this class is to aid communication between libvirt and other classes.
+libvirt's preferred method of delivery of larger object is, sadly, xml (rather than objects).
+"""
+
+# pylint: disable=no-self-use
+
+from xml.etree import ElementTree
+import glog
+import libvirt
+
+class LibVirtClient(object):
+ """Client of the libvirt library.
+ """
+ def __init__(self):
+ # Open channel to QEmu instance running locally.
+ self.lvch = libvirt.open('qemu:///system')
+ if self.lvch is None:
+ raise Exception('Could not open libvirt channel. Did you install libvirt package?')
+
+ self.capabilities = None
+
+ # Parse host capabilities. Confirm our CPU is capable of executing movbe instruction
+ # which allows further compatibility with atom cpus.
+ self.host_capabilities = ElementTree.fromstring(self.lvch.getCapabilities())
+ glog.info('Starting cuttlefish on %s', self.get_hostname())
+ glog.info('Max number of virtual CPUs: %d', self.get_max_vcpus())
+ glog.info('Supported virtualization type: %s', self.get_virtualization_type())
+
+
+ def get_hostname(self):
+ """Return name of the host that will run guest images.
+
+ Returns:
+ hostname as string.
+ """
+ return self.lvch.getHostname()
+
+
+ def get_max_vcpus(self):
+ """Query max number of VCPUs that can be used by virtual instance.
+
+ Returns:
+ number of VCPUs that can be used by virtual instance.
+ """
+ return self.lvch.getMaxVcpus(None)
+
+
+ def get_virtualization_type(self):
+ """Query virtualization type supported by host.
+
+ Returns:
+ string describing supported virtualization type.
+ """
+ return self.lvch.getType()
+
+
+ def get_instance(self, name):
+ """Get libvirt instance matching supplied name.
+
+ Args:
+ name Name of the virtual instance.
+ Returns:
+ libvirt instance or None, if no instance by that name was found.
+ """
+ return self.lvch.lookupByName(name)
+
+
+ def create_instance(self, description):
+ """Create new instance based on the XML description.
+
+ Args:
+ description XML string describing instance.
+ Returns:
+ libvirt domain representing started domain. Domain will be automatically
+ destroyed when this handle is orphaned.
+ """
+ return self.lvch.createXML(description,
+ libvirt.VIR_DOMAIN_START_AUTODESTROY)
+
+
+ def get_cpu_features(self):
+ """Get host capabilities from libvirt.
+
+ Returns:
+ set of CPU features reported by the host.
+ """
+ caps = self.capabilities or set()
+ try:
+ if self.capabilities is None:
+ features = self.host_capabilities.findall('./host/cpu/feature')
+ if features is None:
+ glog.warning('no \'host.cpu.feature\' nodes reported by libvirt.')
+ return caps
+
+ for feature in features:
+ caps.add(feature.get('name'))
+ return caps
+
+ finally:
+ # Make sure to update self.capabilities with empty set if anything goes wrong.
+ self.capabilities = caps
+
+
+ def build_instance_name(self, instance_number: int):
+ """Convert instance number to an instance id (or domain).
+
+ Args:
+ instance_number Number of Cuttlefish instance.
+ Returns:
+ string representing instance (domain) name.
+ """
+ return 'android_cuttlefish_{}'.format(instance_number)