hw/virtio/vhost: Add simple tests backed by fakes

This slightly advances the use of fakes to test higher level
application logic. The fakes are rudimentary at this point, but I
wanted to get feedback on the addition of generics in order to
facilitate swaping concrete implementations out with fakes in higher
level code.

BUG=none
TEST=./build_test and
cargo test -p crosvm -p data_model -p syscall_defines -p kernel_loader
-p net_util -p x86_64 -p virtio_sys -p kvm_sys -p vhost -p io_jail -p
net_sys -p sys_util -p kvm

Change-Id: Ib64581014391f49cff30ada10677bbbcd0088f20
Reviewed-on: https://chromium-review.googlesource.com/689740
Commit-Ready: Jason Clinton <jclinton@chromium.org>
Tested-by: Jason Clinton <jclinton@chromium.org>
Reviewed-by: Stephen Barber <smbarber@chromium.org>
This commit is contained in:
Jason D. Clinton 2017-09-27 22:04:03 -06:00 committed by chrome-bot
parent 19e57b9532
commit 865323d0ed
9 changed files with 293 additions and 87 deletions

2
Cargo.lock generated
View file

@ -24,10 +24,12 @@ dependencies = [
"kernel_loader 0.1.0",
"kvm 0.1.0",
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
"net_util 0.1.0",
"plugin_proto 0.5.0",
"qcow 0.1.0",
"qcow_utils 0.1.0",
"sys_util 0.1.0",
"vhost 0.1.0",
"vm_control 0.1.0",
"x86_64 0.1.0",
]

View file

@ -20,6 +20,8 @@ sys_util = { path = "sys_util" }
kernel_loader = { path = "kernel_loader" }
libc = "=0.2.34"
byteorder = "=1.1.0"
net_util = { path = "net_util" }
vhost = { path = "vhost" }
vm_control = { path = "vm_control" }
data_model = { path = "data_model" }
qcow = { path = "qcow" }

View file

@ -4,7 +4,6 @@
use std::cmp;
use std::mem;
use std::io::{Read, Write};
use std::net::Ipv4Addr;
use std::os::unix::io::{AsRawFd, RawFd};
use std::sync::Arc;
@ -13,8 +12,8 @@ use std::thread;
use libc::EAGAIN;
use net_sys;
use net_util::{Tap, Error as TapError};
use sys_util::{Error as SysError};
use net_util::{Error as TapError, TapT};
use sys_util::Error as SysError;
use sys_util::{EventFd, GuestMemory, Pollable, Poller};
use virtio_sys::{vhost, virtio_net};
use virtio_sys::virtio_net::virtio_net_hdr_v1;
@ -50,11 +49,11 @@ pub enum NetError {
PollError(SysError),
}
struct Worker {
struct Worker<T: TapT> {
mem: GuestMemory,
rx_queue: Queue,
tx_queue: Queue,
tap: Tap,
tap: T,
interrupt_status: Arc<AtomicUsize>,
interrupt_evt: EventFd,
rx_buf: [u8; MAX_BUFFER_SIZE],
@ -66,7 +65,10 @@ struct Worker {
acked_features: u64,
}
impl Worker {
impl<T> Worker<T>
where
T: TapT,
{
fn signal_used_queue(&self) {
self.interrupt_status
.fetch_or(INTERRUPT_STATUS_USED_RING as usize, Ordering::SeqCst);
@ -226,7 +228,7 @@ impl Worker {
const KILL: u32 = 4;
'poll: loop {
let tokens = match poller.poll(&[(RX_TAP, &self.tap as &Pollable),
let tokens = match poller.poll(&[(RX_TAP, &self.tap),
(RX_QUEUE, &rx_queue_evt as &Pollable),
(TX_QUEUE, &tx_queue_evt as &Pollable),
(KILL, &kill_evt as &Pollable)]) {
@ -274,21 +276,24 @@ impl Worker {
}
}
pub struct Net {
pub struct Net<T: TapT> {
workers_kill_evt: Option<EventFd>,
kill_evt: EventFd,
tap: Option<Tap>,
tap: Option<T>,
avail_features: u64,
acked_features: u64,
}
impl Net {
impl<T> Net<T>
where
T: TapT,
{
/// Create a new virtio network device with the given IP address and
/// netmask.
pub fn new(ip_addr: Ipv4Addr, netmask: Ipv4Addr) -> Result<Net, NetError> {
pub fn new(ip_addr: Ipv4Addr, netmask: Ipv4Addr) -> Result<Net<T>, NetError> {
let kill_evt = EventFd::new().map_err(NetError::CreateKillEventFd)?;
let tap = Tap::new().map_err(NetError::TapOpen)?;
let tap: T = T::new().map_err(NetError::TapOpen)?;
tap.set_ip_addr(ip_addr).map_err(NetError::TapSetIp)?;
tap.set_netmask(netmask)
.map_err(NetError::TapSetNetmask)?;
@ -306,24 +311,25 @@ impl Net {
let avail_features =
1 << virtio_net::VIRTIO_NET_F_GUEST_CSUM | 1 << virtio_net::VIRTIO_NET_F_CSUM |
1 << virtio_net::VIRTIO_NET_F_GUEST_TSO4 |
1 << virtio_net::VIRTIO_NET_F_GUEST_UFO |
1 << virtio_net::VIRTIO_NET_F_HOST_TSO4 |
1 << virtio_net::VIRTIO_NET_F_HOST_UFO | 1 << vhost::VIRTIO_F_VERSION_1;
1 << virtio_net::VIRTIO_NET_F_GUEST_TSO4 |
1 << virtio_net::VIRTIO_NET_F_GUEST_UFO |
1 << virtio_net::VIRTIO_NET_F_HOST_TSO4 |
1 << virtio_net::VIRTIO_NET_F_HOST_UFO | 1 << vhost::VIRTIO_F_VERSION_1;
Ok(Net {
workers_kill_evt: Some(kill_evt
.try_clone()
.map_err(NetError::CloneKillEventFd)?),
kill_evt: kill_evt,
tap: Some(tap),
avail_features: avail_features,
acked_features: 0u64,
})
workers_kill_evt: Some(kill_evt.try_clone().map_err(NetError::CloneKillEventFd)?),
kill_evt: kill_evt,
tap: Some(tap),
avail_features: avail_features,
acked_features: 0u64,
})
}
}
impl Drop for Net {
impl<T> Drop for Net<T>
where
T: TapT,
{
fn drop(&mut self) {
// Only kill the child if it claimed its eventfd.
if self.workers_kill_evt.is_none() {
@ -333,7 +339,10 @@ impl Drop for Net {
}
}
impl VirtioDevice for Net {
impl<T> VirtioDevice for Net<T>
where
T: 'static + TapT,
{
fn keep_fds(&self) -> Vec<RawFd> {
let mut keep_fds = Vec::new();

View file

@ -10,10 +10,10 @@ use std::sync::atomic::AtomicUsize;
use std::thread;
use net_sys;
use net_util::Tap;
use net_util::TapT;
use sys_util::{EventFd, GuestMemory};
use vhost::Net as VhostNetHandle;
use vhost::net::NetT;
use vhost::NetT as VhostNetT;
use virtio_sys::{vhost, virtio_net};
use super::{Error, Result};
@ -24,23 +24,27 @@ const QUEUE_SIZE: u16 = 256;
const NUM_QUEUES: usize = 2;
const QUEUE_SIZES: &'static [u16] = &[QUEUE_SIZE; NUM_QUEUES];
pub struct Net {
pub struct Net<T: TapT, U: VhostNetT<T>> {
workers_kill_evt: Option<EventFd>,
kill_evt: EventFd,
tap: Option<Tap>,
vhost_net_handle: Option<VhostNetHandle>,
tap: Option<T>,
vhost_net_handle: Option<U>,
vhost_interrupt: Option<EventFd>,
avail_features: u64,
acked_features: u64,
}
impl Net {
impl<T, U> Net<T, U>
where
T: TapT,
U: VhostNetT<T>,
{
/// Create a new virtio network device with the given IP address and
/// netmask.
pub fn new(ip_addr: Ipv4Addr, netmask: Ipv4Addr, mem: &GuestMemory) -> Result<Net> {
pub fn new(ip_addr: Ipv4Addr, netmask: Ipv4Addr, mem: &GuestMemory) -> Result<Net<T, U>> {
let kill_evt = EventFd::new().map_err(Error::CreateKillEventFd)?;
let tap = Tap::new().map_err(Error::TapOpen)?;
let tap: T = T::new().map_err(Error::TapOpen)?;
tap.set_ip_addr(ip_addr).map_err(Error::TapSetIp)?;
tap.set_netmask(netmask).map_err(Error::TapSetNetmask)?;
@ -54,7 +58,7 @@ impl Net {
tap.set_vnet_hdr_size(vnet_hdr_size).map_err(Error::TapSetVnetHdrSize)?;
tap.enable().map_err(Error::TapEnable)?;
let vhost_net_handle = VhostNetHandle::new(mem).map_err(Error::VhostOpen)?;
let vhost_net_handle = U::new(mem).map_err(Error::VhostOpen)?;
let avail_features =
1 << virtio_net::VIRTIO_NET_F_GUEST_CSUM | 1 << virtio_net::VIRTIO_NET_F_CSUM |
@ -79,7 +83,11 @@ impl Net {
}
}
impl Drop for Net {
impl<T, U> Drop for Net<T, U>
where
T: TapT,
U: VhostNetT<T>,
{
fn drop(&mut self) {
// Only kill the child if it claimed its eventfd.
if self.workers_kill_evt.is_none() {
@ -89,7 +97,11 @@ impl Drop for Net {
}
}
impl VirtioDevice for Net {
impl<T, U> VirtioDevice for Net<T, U>
where
T: TapT + 'static,
U: VhostNetT<T> + 'static,
{
fn keep_fds(&self) -> Vec<RawFd> {
let mut keep_fds = Vec::new();
@ -182,7 +194,7 @@ impl VirtioDevice for Net {
status,
interrupt_evt,
acked_features);
let activate_vqs = |handle: &VhostNetHandle| -> Result<()> {
let activate_vqs = |handle: &U| -> Result<()> {
for idx in 0..NUM_QUEUES {
handle
.set_backend(idx, &tap)
@ -207,3 +219,69 @@ impl VirtioDevice for Net {
}
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use std::result;
use net_util::fakes::FakeTap;
use sys_util::{GuestAddress, GuestMemory, GuestMemoryError};
use vhost::net::fakes::FakeNet;
fn create_guest_memory() -> result::Result<GuestMemory, GuestMemoryError> {
let start_addr1 = GuestAddress(0x0);
let start_addr2 = GuestAddress(0x100);
GuestMemory::new(&vec![(start_addr1, 0x100), (start_addr2, 0x400)])
}
fn create_net_common() -> Net<FakeTap, FakeNet<FakeTap>> {
let guest_memory = create_guest_memory().unwrap();
Net::<FakeTap, FakeNet<FakeTap>>::new(
Ipv4Addr::new(127, 0, 0, 1),
Ipv4Addr::new(255, 255, 255, 0),
&guest_memory,
).unwrap()
}
#[test]
fn create_net() {
create_net_common();
}
#[test]
fn keep_fds() {
let net = create_net_common();
let fds = net.keep_fds();
assert!(fds.len() >= 1, "We should have gotten at least one fd");
}
#[test]
fn features() {
let net = create_net_common();
assert_eq!(net.features(0), 822135939);
assert_eq!(net.features(1), 1);
assert_eq!(net.features(2), 0);
}
#[test]
fn ack_features() {
let mut net = create_net_common();
// Just testing that we don't panic, for now
net.ack_features(0, 1);
net.ack_features(1, 1);
}
#[test]
fn activate() {
let mut net = create_net_common();
let guest_memory = create_guest_memory().unwrap();
// Just testing that we don't panic, for now
net.activate(
guest_memory,
EventFd::new().unwrap(),
Arc::new(AtomicUsize::new(0)),
vec![Queue::new(1)],
vec![EventFd::new().unwrap()],
);
}
}

View file

@ -67,9 +67,30 @@ pub struct Tap {
if_name: [u8; 16usize],
}
impl Tap {
pub trait TapT: Read + Write + AsRawFd + Pollable + Send + Sized {
/// Create a new tap interface.
pub fn new() -> Result<Tap> {
fn new() -> Result<Self>;
/// Set the host-side IP address for the tap interface.
fn set_ip_addr(&self, ip_addr: net::Ipv4Addr) -> Result<()>;
/// Set the netmask for the subnet that the tap interface will exist on.
fn set_netmask(&self, netmask: net::Ipv4Addr) -> Result<()>;
/// Set the offload flags for the tap interface.
fn set_offload(&self, flags: c_uint) -> Result<()>;
/// Enable the tap interface.
fn enable(&self) -> Result<()>;
/// Set the size of the vnet hdr.
fn set_vnet_hdr_size(&self, size: c_int) -> Result<()>;
fn get_ifreq(&self) -> net_sys::ifreq;
}
impl TapT for Tap {
fn new() -> Result<Tap> {
// Open calls are safe because we give a constant nul-terminated
// string and verify the result.
let fd = unsafe {
@ -118,8 +139,7 @@ impl Tap {
})
}
/// Set the host-side IP address for the tap interface.
pub fn set_ip_addr(&self, ip_addr: net::Ipv4Addr) -> Result<()> {
fn set_ip_addr(&self, ip_addr: net::Ipv4Addr) -> Result<()> {
let sock = create_socket()?;
let addr = create_sockaddr(ip_addr);
@ -141,8 +161,7 @@ impl Tap {
Ok(())
}
/// Set the netmask for the subnet that the tap interface will exist on.
pub fn set_netmask(&self, netmask: net::Ipv4Addr) -> Result<()> {
fn set_netmask(&self, netmask: net::Ipv4Addr) -> Result<()> {
let sock = create_socket()?;
let addr = create_sockaddr(netmask);
@ -164,8 +183,7 @@ impl Tap {
Ok(())
}
/// Set the offload flags for the tap interface.
pub fn set_offload(&self, flags: c_uint) -> Result<()> {
fn set_offload(&self, flags: c_uint) -> Result<()> {
// ioctl is safe. Called with a valid tap fd, and we check the return.
let ret =
unsafe { ioctl_with_val(&self.tap_file, net_sys::TUNSETOFFLOAD(), flags as c_ulong) };
@ -176,8 +194,7 @@ impl Tap {
Ok(())
}
/// Enable the tap interface.
pub fn enable(&self) -> Result<()> {
fn enable(&self) -> Result<()> {
let sock = create_socket()?;
let mut ifreq = self.get_ifreq();
@ -199,8 +216,7 @@ impl Tap {
Ok(())
}
/// Set the size of the vnet hdr.
pub fn set_vnet_hdr_size(&self, size: c_int) -> Result<()> {
fn set_vnet_hdr_size(&self, size: c_int) -> Result<()> {
// ioctl is safe. Called with a valid tap fd, and we check the return.
let ret = unsafe { ioctl_with_ref(&self.tap_file, net_sys::TUNSETVNETHDRSZ(), &size) };
if ret < 0 {
@ -254,6 +270,90 @@ unsafe impl Pollable for Tap {
}
}
pub mod fakes {
use super::*;
use std::fs::OpenOptions;
use std::fs::remove_file;
const TMP_FILE: &str = "/tmp/crosvm_tap_test_file";
pub struct FakeTap {
tap_file: File,
}
impl TapT for FakeTap {
fn new() -> Result<FakeTap> {
Ok(FakeTap {
tap_file: OpenOptions::new()
.read(true)
.append(true)
.create(true)
.open(TMP_FILE)
.unwrap()
})
}
fn set_ip_addr(&self, _: net::Ipv4Addr) -> Result<()> {
Ok(())
}
fn set_netmask(&self, _: net::Ipv4Addr) -> Result<()> {
Ok(())
}
fn set_offload(&self, _: c_uint) -> Result<()> {
Ok(())
}
fn enable(&self) -> Result<()> {
Ok(())
}
fn set_vnet_hdr_size(&self, _: c_int) -> Result<()> {
Ok(())
}
fn get_ifreq(&self) -> net_sys::ifreq {
let ifreq: net_sys::ifreq = Default::default();
ifreq
}
}
impl Drop for FakeTap {
fn drop(&mut self) {
let _ = remove_file(TMP_FILE);
}
}
impl Read for FakeTap {
fn read(&mut self, _: &mut [u8]) -> IoResult<usize> {
Ok(0)
}
}
impl Write for FakeTap {
fn write(&mut self, _: &[u8]) -> IoResult<usize> {
Ok(0)
}
fn flush(&mut self) -> IoResult<()> {
Ok(())
}
}
impl AsRawFd for FakeTap {
fn as_raw_fd(&self) -> RawFd {
self.tap_file.as_raw_fd()
}
}
unsafe impl Pollable for FakeTap {
fn pollable_fd(&self) -> RawFd {
self.tap_file.as_raw_fd()
}
}
}
#[cfg(test)]
mod tests {
use super::*;

View file

@ -22,9 +22,11 @@ use io_jail::{self, Minijail};
use kernel_cmdline;
use kernel_loader;
use kvm::*;
use net_util::Tap;
use qcow::{self, QcowFile};
use sys_util::*;
use sys_util;
use vhost;
use vm_control::VmRequest;
use Config;
@ -352,10 +354,10 @@ fn setup_mmio_bus(cfg: &Config,
if let Some(host_ip) = cfg.host_ip {
if let Some(netmask) = cfg.netmask {
let net_box: Box<devices::virtio::VirtioDevice> = if cfg.vhost_net {
Box::new(devices::virtio::vhost::Net::new(host_ip, netmask, &mem)
.map_err(Error::VhostNetDeviceNew)?)
Box::new(devices::virtio::vhost::Net::<Tap, vhost::Net<Tap>>::new(host_ip, netmask, &mem)
.map_err(|e| Error::VhostNetDeviceNew(e))?)
} else {
Box::new(devices::virtio::Net::new(host_ip, netmask)
Box::new(devices::virtio::Net::<Tap>::new(host_ip, netmask)
.map_err(|e| Error::NetDeviceNew(e))?)
};

View file

@ -12,9 +12,11 @@ extern crate kvm;
extern crate x86_64;
extern crate kernel_loader;
extern crate byteorder;
extern crate net_util;
extern crate qcow;
#[macro_use]
extern crate sys_util;
extern crate vhost;
extern crate vm_control;
extern crate data_model;

View file

@ -11,6 +11,7 @@ pub mod net;
mod vsock;
pub use net::Net;
pub use net::NetT;
pub use vsock::Vsock;
use std::io::Error as IoError;
@ -333,7 +334,8 @@ pub trait Vhost: AsRawFd + std::marker::Sized {
mod tests {
use super::*;
use net::tests::FakeNet;
use net::fakes::FakeNet;
use net_util::fakes::FakeTap;
use std::result;
use sys_util::{GuestAddress, GuestMemory, GuestMemoryError};
@ -352,9 +354,9 @@ mod tests {
}
}
fn create_fake_vhost_net () -> FakeNet {
fn create_fake_vhost_net() -> FakeNet<FakeTap> {
let gm = create_guest_memory().unwrap();
FakeNet::new(&gm).unwrap()
FakeNet::<FakeTap>::new(&gm).unwrap()
}
#[test]

View file

@ -3,8 +3,9 @@
// found in the LICENSE file.
use libc;
use net_util;
use net_util::TapT;
use std::fs::{File, OpenOptions};
use std::marker::PhantomData;
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::io::{AsRawFd, RawFd};
use virtio_sys;
@ -19,30 +20,37 @@ static DEVICE: &'static str = "/dev/vhost-net";
///
/// This provides a simple wrapper around a VHOST_NET file descriptor and
/// methods that safely run ioctls on that file descriptor.
pub struct Net {
pub struct Net<T> {
// fd must be dropped first, which will stop and tear down the
// vhost-net worker before GuestMemory can potentially be unmapped.
fd: File,
mem: GuestMemory,
phantom: PhantomData<T>,
}
pub trait NetT {
pub trait NetT<T: TapT>: Vhost + AsRawFd + Send + Sized {
/// Create a new NetT instance
fn new(mem: &GuestMemory) -> Result<Self>;
/// Set the tap file descriptor that will serve as the VHOST_NET backend.
/// This will start the vhost worker for the given queue.
///
/// # Arguments
/// * `queue_index` - Index of the queue to modify.
/// * `fd` - Tap interface that will be used as the backend.
fn set_backend(&self, queue_index: usize, fd: &net_util::Tap) -> Result<()>;
fn set_backend(&self, queue_index: usize, fd: &T) -> Result<()>;
}
impl Net {
impl<T> NetT<T> for Net<T>
where
T: TapT,
{
/// Opens /dev/vhost-net and holds a file descriptor open for it.
///
/// # Arguments
/// * `mem` - Guest memory mapping.
pub fn new(mem: &GuestMemory) -> Result<Net> {
Ok(Net {
fn new(mem: &GuestMemory) -> Result<Net<T>> {
Ok(Net::<T> {
fd: OpenOptions::new()
.read(true)
.write(true)
@ -50,12 +58,11 @@ impl Net {
.open(DEVICE)
.map_err(Error::VhostOpen)?,
mem: mem.clone(),
phantom: PhantomData,
})
}
}
impl NetT for Net {
fn set_backend(&self, queue_index: usize, fd: &net_util::Tap) -> Result<()> {
fn set_backend(&self, queue_index: usize, fd: &T) -> Result<()> {
let vring_file = virtio_sys::vhost_vring_file {
index: queue_index as u32,
fd: fd.as_raw_fd(),
@ -72,64 +79,66 @@ impl NetT for Net {
}
}
impl Vhost for Net {
impl<T> Vhost for Net<T> {
fn mem(&self) -> &GuestMemory {
&self.mem
}
}
impl AsRawFd for Net {
impl<T> AsRawFd for Net<T> {
fn as_raw_fd(&self) -> RawFd {
self.fd.as_raw_fd()
}
}
#[cfg(test)]
pub mod tests {
pub mod fakes {
use super::*;
use std::fs::OpenOptions;
use std::fs::remove_file;
const TMP_FILE: &str = "/tmp/crosvm_vhost_test_file";
pub struct FakeNet {
pub struct FakeNet<T> {
fd: File,
mem: GuestMemory,
phantom: PhantomData<T>,
}
impl FakeNet {
pub fn new(mem: &GuestMemory) -> Result<FakeNet> {
Ok(FakeNet {
impl<T> Drop for FakeNet<T> {
fn drop(&mut self) {
let _ = remove_file(TMP_FILE);
}
}
impl<T> NetT<T> for FakeNet<T>
where
T: TapT,
{
fn new(mem: &GuestMemory) -> Result<FakeNet<T>> {
Ok(FakeNet::<T> {
fd: OpenOptions::new()
.read(true)
.append(true)
.create(true)
.open(TMP_FILE)
.unwrap(),
mem: mem.clone()
mem: mem.clone(),
phantom: PhantomData,
})
}
}
impl Drop for FakeNet {
fn drop(&mut self) {
let _ = remove_file(TMP_FILE);
}
}
impl NetT for FakeNet {
fn set_backend(&self, _queue_index: usize, _fd: &net_util::Tap) -> Result<()> {
fn set_backend(&self, _queue_index: usize, _fd: &T) -> Result<()> {
Ok(())
}
}
impl Vhost for FakeNet {
impl<T> Vhost for FakeNet<T> {
fn mem(&self) -> &GuestMemory {
&self.mem
}
}
impl AsRawFd for FakeNet {
impl<T> AsRawFd for FakeNet<T> {
fn as_raw_fd(&self) -> RawFd {
self.fd.as_raw_fd()
}