683 lines
13 KiB
C
683 lines
13 KiB
C
/*
|
|
* Copyright (C) 2008-2009 Tobias Brunner
|
|
* Copyright (C) 2007 Martin Willi
|
|
* HSR Hochschule fuer Technik Rapperswil
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License as published by the
|
|
* Free Software Foundation; either version 2 of the License, or (at your
|
|
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
* for more details.
|
|
*/
|
|
|
|
#define _GNU_SOURCE
|
|
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/wait.h>
|
|
#include <sys/uio.h>
|
|
#include <unistd.h>
|
|
#include <stdio.h>
|
|
#include <fcntl.h>
|
|
#include <signal.h>
|
|
#include <dirent.h>
|
|
#include <termios.h>
|
|
#include <stdarg.h>
|
|
|
|
#include <utils/debug.h>
|
|
#include <collections/linked_list.h>
|
|
|
|
#include "dumm.h"
|
|
#include "guest.h"
|
|
#include "mconsole.h"
|
|
#include "cowfs.h"
|
|
|
|
#define PERME (S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH)
|
|
#define PERM (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH)
|
|
|
|
#define MASTER_DIR "master"
|
|
#define DIFF_DIR "diff"
|
|
#define UNION_DIR "union"
|
|
#define ARGS_FILE "args"
|
|
#define PID_FILE "pid"
|
|
#define KERNEL_FILE "linux"
|
|
#define LOG_FILE "boot.log"
|
|
#define NOTIFY_FILE "notify"
|
|
#define PTYS 0
|
|
|
|
typedef struct private_guest_t private_guest_t;
|
|
|
|
struct private_guest_t {
|
|
/** implemented public interface */
|
|
guest_t public;
|
|
/** name of the guest */
|
|
char *name;
|
|
/** directory of guest */
|
|
int dir;
|
|
/** directory name of guest */
|
|
char *dirname;
|
|
/** additional args to pass to guest */
|
|
char *args;
|
|
/** pid of guest child process */
|
|
int pid;
|
|
/** state of guest */
|
|
guest_state_t state;
|
|
/** FUSE cowfs instance */
|
|
cowfs_t *cowfs;
|
|
/** mconsole to control running UML */
|
|
mconsole_t *mconsole;
|
|
/** list of interfaces attached to the guest */
|
|
linked_list_t *ifaces;
|
|
};
|
|
|
|
ENUM(guest_state_names, GUEST_STOPPED, GUEST_STOPPING,
|
|
"STOPPED",
|
|
"STARTING",
|
|
"RUNNING",
|
|
"PAUSED",
|
|
"STOPPING",
|
|
);
|
|
|
|
METHOD(guest_t, get_name, char*,
|
|
private_guest_t *this)
|
|
{
|
|
return this->name;
|
|
}
|
|
|
|
METHOD(guest_t, create_iface, iface_t*,
|
|
private_guest_t *this, char *name)
|
|
{
|
|
enumerator_t *enumerator;
|
|
iface_t *iface;
|
|
|
|
if (this->state != GUEST_RUNNING)
|
|
{
|
|
DBG1(DBG_LIB, "guest '%s' not running, unable to add interface",
|
|
this->name);
|
|
return NULL;
|
|
}
|
|
|
|
enumerator = this->ifaces->create_enumerator(this->ifaces);
|
|
while (enumerator->enumerate(enumerator, (void**)&iface))
|
|
{
|
|
if (streq(name, iface->get_guestif(iface)))
|
|
{
|
|
DBG1(DBG_LIB, "guest '%s' already has an interface '%s'",
|
|
this->name, name);
|
|
enumerator->destroy(enumerator);
|
|
return NULL;
|
|
}
|
|
}
|
|
enumerator->destroy(enumerator);
|
|
|
|
iface = iface_create(name, &this->public, this->mconsole);
|
|
if (iface)
|
|
{
|
|
this->ifaces->insert_last(this->ifaces, iface);
|
|
}
|
|
return iface;
|
|
}
|
|
|
|
METHOD(guest_t, destroy_iface, void,
|
|
private_guest_t *this, iface_t *iface)
|
|
{
|
|
enumerator_t *enumerator;
|
|
iface_t *current;
|
|
|
|
enumerator = this->ifaces->create_enumerator(this->ifaces);
|
|
while (enumerator->enumerate(enumerator, (void**)¤t))
|
|
{
|
|
if (current == iface)
|
|
{
|
|
this->ifaces->remove_at(this->ifaces, enumerator);
|
|
current->destroy(current);
|
|
break;
|
|
}
|
|
}
|
|
enumerator->destroy(enumerator);
|
|
}
|
|
|
|
METHOD(guest_t, create_iface_enumerator, enumerator_t*,
|
|
private_guest_t *this)
|
|
{
|
|
return this->ifaces->create_enumerator(this->ifaces);
|
|
}
|
|
|
|
METHOD(guest_t, get_state, guest_state_t,
|
|
private_guest_t *this)
|
|
{
|
|
return this->state;
|
|
}
|
|
|
|
METHOD(guest_t, get_pid, pid_t,
|
|
private_guest_t *this)
|
|
{
|
|
return this->pid;
|
|
}
|
|
|
|
/**
|
|
* write format string to a buffer, and advance buffer position
|
|
*/
|
|
static char* write_arg(char **pos, size_t *left, char *format, ...)
|
|
{
|
|
size_t len;
|
|
char *res = NULL;
|
|
va_list args;
|
|
|
|
va_start(args, format);
|
|
len = vsnprintf(*pos, *left, format, args);
|
|
va_end(args);
|
|
if (len < *left)
|
|
{
|
|
res = *pos;
|
|
len++;
|
|
*pos += len + 1;
|
|
*left -= len + 1;
|
|
}
|
|
return res;
|
|
}
|
|
|
|
METHOD(guest_t, stop, void,
|
|
private_guest_t *this, idle_function_t idle)
|
|
{
|
|
if (this->state != GUEST_STOPPED)
|
|
{
|
|
this->state = GUEST_STOPPING;
|
|
this->ifaces->destroy_offset(this->ifaces, offsetof(iface_t, destroy));
|
|
this->ifaces = linked_list_create();
|
|
kill(this->pid, SIGINT);
|
|
while (this->state != GUEST_STOPPED)
|
|
{
|
|
if (idle)
|
|
{
|
|
idle();
|
|
}
|
|
else
|
|
{
|
|
usleep(50000);
|
|
}
|
|
}
|
|
unlinkat(this->dir, PID_FILE, 0);
|
|
this->pid = 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* save pid in file
|
|
*/
|
|
void savepid(private_guest_t *this)
|
|
{
|
|
FILE *file;
|
|
|
|
file = fdopen(openat(this->dir, PID_FILE, O_RDWR | O_CREAT | O_TRUNC,
|
|
PERM), "w");
|
|
if (file)
|
|
{
|
|
fprintf(file, "%d", this->pid);
|
|
fclose(file);
|
|
}
|
|
}
|
|
|
|
METHOD(guest_t, start, bool,
|
|
private_guest_t *this, invoke_function_t invoke, void* data,
|
|
idle_function_t idle)
|
|
{
|
|
char buf[2048];
|
|
char *notify;
|
|
char *pos = buf;
|
|
char *args[32];
|
|
int i = 0;
|
|
size_t left = sizeof(buf);
|
|
|
|
memset(args, 0, sizeof(args));
|
|
|
|
if (this->state != GUEST_STOPPED)
|
|
{
|
|
DBG1(DBG_LIB, "unable to start guest in state %N", guest_state_names,
|
|
this->state);
|
|
return FALSE;
|
|
}
|
|
this->state = GUEST_STARTING;
|
|
|
|
notify = write_arg(&pos, &left, "%s/%s", this->dirname, NOTIFY_FILE);
|
|
|
|
args[i++] = write_arg(&pos, &left, "nice");
|
|
args[i++] = write_arg(&pos, &left, "%s/%s", this->dirname, KERNEL_FILE);
|
|
args[i++] = write_arg(&pos, &left, "root=/dev/root");
|
|
args[i++] = write_arg(&pos, &left, "rootfstype=hostfs");
|
|
args[i++] = write_arg(&pos, &left, "rootflags=%s/%s", this->dirname, UNION_DIR);
|
|
args[i++] = write_arg(&pos, &left, "uml_dir=%s", this->dirname);
|
|
args[i++] = write_arg(&pos, &left, "umid=%s", this->name);
|
|
args[i++] = write_arg(&pos, &left, "mconsole=notify:%s", notify);
|
|
args[i++] = write_arg(&pos, &left, "con=null");
|
|
if (this->args)
|
|
{
|
|
args[i++] = this->args;
|
|
}
|
|
|
|
this->pid = invoke(data, &this->public, args, i);
|
|
if (!this->pid)
|
|
{
|
|
this->state = GUEST_STOPPED;
|
|
return FALSE;
|
|
}
|
|
savepid(this);
|
|
|
|
/* open mconsole */
|
|
this->mconsole = mconsole_create(notify, idle);
|
|
if (this->mconsole == NULL)
|
|
{
|
|
DBG1(DBG_LIB, "opening mconsole at '%s' failed, stopping guest", buf);
|
|
stop(this, NULL);
|
|
return FALSE;
|
|
}
|
|
|
|
this->state = GUEST_RUNNING;
|
|
return TRUE;
|
|
}
|
|
|
|
METHOD(guest_t, add_overlay, bool,
|
|
private_guest_t *this, char *path)
|
|
{
|
|
if (path == NULL)
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
if (access(path, F_OK) != 0)
|
|
{
|
|
if (!mkdir_p(path, PERME))
|
|
{
|
|
DBG1(DBG_LIB, "creating overlay for guest '%s' failed: %m",
|
|
this->name);
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
return this->cowfs->add_overlay(this->cowfs, path);
|
|
}
|
|
|
|
METHOD(guest_t, del_overlay, bool,
|
|
private_guest_t *this, char *path)
|
|
{
|
|
return this->cowfs->del_overlay(this->cowfs, path);
|
|
}
|
|
|
|
METHOD(guest_t, pop_overlay, bool,
|
|
private_guest_t *this)
|
|
{
|
|
return this->cowfs->pop_overlay(this->cowfs);
|
|
}
|
|
|
|
/**
|
|
* Variadic version of the exec function
|
|
*/
|
|
static int vexec(private_guest_t *this, void(*cb)(void*,char*,size_t), void *data,
|
|
char *cmd, va_list args)
|
|
{
|
|
char buf[1024];
|
|
size_t len;
|
|
|
|
if (this->mconsole)
|
|
{
|
|
len = vsnprintf(buf, sizeof(buf), cmd, args);
|
|
|
|
if (len > 0 && len < sizeof(buf))
|
|
{
|
|
return this->mconsole->exec(this->mconsole, cb, data, buf);
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
METHOD(guest_t, exec, int,
|
|
private_guest_t *this, void(*cb)(void*,char*,size_t), void *data,
|
|
char *cmd, ...)
|
|
{
|
|
int res;
|
|
va_list args;
|
|
va_start(args, cmd);
|
|
res = vexec(this, cb, data, cmd, args);
|
|
va_end(args);
|
|
return res;
|
|
}
|
|
|
|
typedef struct {
|
|
chunk_t buf;
|
|
void (*cb)(void*,char*);
|
|
void *data;
|
|
} exec_str_t;
|
|
|
|
/**
|
|
* callback that combines chunks to a string. if a callback is given, the string
|
|
* is split at newlines and the callback is called for each line.
|
|
*/
|
|
static void exec_str_cb(exec_str_t *data, char *buf, size_t len)
|
|
{
|
|
if (!data->buf.ptr)
|
|
{
|
|
data->buf = chunk_alloc(len + 1);
|
|
memcpy(data->buf.ptr, buf, len);
|
|
data->buf.ptr[len] = '\0';
|
|
}
|
|
else
|
|
{
|
|
size_t newlen = strlen(data->buf.ptr) + len + 1;
|
|
if (newlen > data->buf.len)
|
|
{
|
|
data->buf.ptr = realloc(data->buf.ptr, newlen);
|
|
data->buf.len = newlen;
|
|
}
|
|
strncat(data->buf.ptr, buf, len);
|
|
}
|
|
|
|
if (data->cb)
|
|
{
|
|
char *nl;
|
|
while ((nl = strchr(data->buf.ptr, '\n')) != NULL)
|
|
{
|
|
*nl++ = '\0';
|
|
data->cb(data->data, data->buf.ptr);
|
|
memmove(data->buf.ptr, nl, strlen(nl) + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
METHOD(guest_t, exec_str, int,
|
|
private_guest_t *this, void(*cb)(void*,char*), bool lines, void *data,
|
|
char *cmd, ...)
|
|
{
|
|
int res;
|
|
va_list args;
|
|
va_start(args, cmd);
|
|
if (cb)
|
|
{
|
|
exec_str_t exec = { chunk_empty, NULL, NULL };
|
|
if (lines)
|
|
{
|
|
exec.cb = cb;
|
|
exec.data = data;
|
|
}
|
|
res = vexec(this, (void(*)(void*,char*,size_t))exec_str_cb, &exec, cmd, args);
|
|
if (exec.buf.ptr)
|
|
{
|
|
if (!lines || strlen(exec.buf.ptr) > 0)
|
|
{
|
|
/* return the complete string or the remaining stuff in the
|
|
* buffer (i.e. when there was no newline at the end) */
|
|
cb(data, exec.buf.ptr);
|
|
}
|
|
chunk_free(&exec.buf);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
res = vexec(this, NULL, NULL, cmd, args);
|
|
}
|
|
va_end(args);
|
|
return res;
|
|
}
|
|
|
|
METHOD(guest_t, sigchild, void,
|
|
private_guest_t *this)
|
|
{
|
|
DESTROY_IF(this->mconsole);
|
|
this->mconsole = NULL;
|
|
this->state = GUEST_STOPPED;
|
|
}
|
|
|
|
/**
|
|
* umount the union filesystem
|
|
*/
|
|
static bool umount_unionfs(private_guest_t *this)
|
|
{
|
|
if (this->cowfs)
|
|
{
|
|
this->cowfs->destroy(this->cowfs);
|
|
this->cowfs = NULL;
|
|
return TRUE;
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
/**
|
|
* mount the union filesystem
|
|
*/
|
|
static bool mount_unionfs(private_guest_t *this)
|
|
{
|
|
char master[PATH_MAX];
|
|
char diff[PATH_MAX];
|
|
char mount[PATH_MAX];
|
|
|
|
if (this->cowfs == NULL)
|
|
{
|
|
snprintf(master, sizeof(master), "%s/%s", this->dirname, MASTER_DIR);
|
|
snprintf(diff, sizeof(diff), "%s/%s", this->dirname, DIFF_DIR);
|
|
snprintf(mount, sizeof(mount), "%s/%s", this->dirname, UNION_DIR);
|
|
|
|
this->cowfs = cowfs_create(master, diff, mount);
|
|
if (this->cowfs)
|
|
{
|
|
return TRUE;
|
|
}
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
/**
|
|
* load args configuration from file
|
|
*/
|
|
char *loadargs(private_guest_t *this)
|
|
{
|
|
FILE *file;
|
|
char buf[512], *args = NULL;
|
|
|
|
file = fdopen(openat(this->dir, ARGS_FILE, O_RDONLY, PERM), "r");
|
|
if (file)
|
|
{
|
|
if (fgets(buf, sizeof(buf), file))
|
|
{
|
|
args = strdup(buf);
|
|
}
|
|
fclose(file);
|
|
}
|
|
return args;
|
|
}
|
|
|
|
/**
|
|
* save args configuration to file
|
|
*/
|
|
bool saveargs(private_guest_t *this, char *args)
|
|
{
|
|
FILE *file;
|
|
bool retval = FALSE;
|
|
|
|
file = fdopen(openat(this->dir, ARGS_FILE, O_RDWR | O_CREAT | O_TRUNC,
|
|
PERM), "w");
|
|
if (file)
|
|
{
|
|
if (fprintf(file, "%s", args) > 0)
|
|
{
|
|
retval = TRUE;
|
|
}
|
|
fclose(file);
|
|
}
|
|
return retval;
|
|
}
|
|
|
|
METHOD(guest_t, destroy, void,
|
|
private_guest_t *this)
|
|
{
|
|
stop(this, NULL);
|
|
umount_unionfs(this);
|
|
if (this->dir > 0)
|
|
{
|
|
close(this->dir);
|
|
}
|
|
this->ifaces->destroy(this->ifaces);
|
|
free(this->dirname);
|
|
free(this->args);
|
|
free(this->name);
|
|
free(this);
|
|
}
|
|
|
|
/**
|
|
* generic guest constructor
|
|
*/
|
|
static private_guest_t *guest_create_generic(char *parent, char *name,
|
|
bool create)
|
|
{
|
|
char cwd[PATH_MAX];
|
|
private_guest_t *this;
|
|
|
|
INIT(this,
|
|
.public = {
|
|
.get_name = _get_name,
|
|
.get_pid = _get_pid,
|
|
.get_state = _get_state,
|
|
.create_iface = _create_iface,
|
|
.destroy_iface = _destroy_iface,
|
|
.create_iface_enumerator = _create_iface_enumerator,
|
|
.start = _start,
|
|
.stop = _stop,
|
|
.add_overlay = _add_overlay,
|
|
.del_overlay = _del_overlay,
|
|
.pop_overlay = _pop_overlay,
|
|
.exec = _exec,
|
|
.exec_str = _exec_str,
|
|
.sigchild = _sigchild,
|
|
.destroy = _destroy,
|
|
}
|
|
);
|
|
|
|
if (*parent == '/' || getcwd(cwd, sizeof(cwd)) == NULL)
|
|
{
|
|
if (asprintf(&this->dirname, "%s/%s", parent, name) < 0)
|
|
{
|
|
this->dirname = NULL;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (asprintf(&this->dirname, "%s/%s/%s", cwd, parent, name) < 0)
|
|
{
|
|
this->dirname = NULL;
|
|
}
|
|
}
|
|
if (this->dirname == NULL)
|
|
{
|
|
free(this);
|
|
return NULL;
|
|
}
|
|
if (create)
|
|
{
|
|
mkdir(this->dirname, PERME);
|
|
}
|
|
this->dir = open(this->dirname, O_DIRECTORY, PERME);
|
|
if (this->dir < 0)
|
|
{
|
|
DBG1(DBG_LIB, "opening guest directory '%s' failed: %m", this->dirname);
|
|
free(this->dirname);
|
|
free(this);
|
|
return NULL;
|
|
}
|
|
this->state = GUEST_STOPPED;
|
|
this->ifaces = linked_list_create();
|
|
this->name = strdup(name);
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* create a symlink to old called new in our working dir
|
|
*/
|
|
static bool make_symlink(private_guest_t *this, char *old, char *new)
|
|
{
|
|
char cwd[PATH_MAX];
|
|
char buf[PATH_MAX];
|
|
|
|
if (*old == '/' || getcwd(cwd, sizeof(cwd)) == NULL)
|
|
{
|
|
snprintf(buf, sizeof(buf), "%s", old);
|
|
}
|
|
else
|
|
{
|
|
snprintf(buf, sizeof(buf), "%s/%s", cwd, old);
|
|
}
|
|
return symlinkat(buf, this->dir, new) == 0;
|
|
}
|
|
|
|
|
|
/**
|
|
* create the guest instance, including required dirs and mounts
|
|
*/
|
|
guest_t *guest_create(char *parent, char *name, char *kernel,
|
|
char *master, char *args)
|
|
{
|
|
private_guest_t *this = guest_create_generic(parent, name, TRUE);
|
|
|
|
if (this == NULL)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
if (!make_symlink(this, master, MASTER_DIR) ||
|
|
!make_symlink(this, kernel, KERNEL_FILE))
|
|
{
|
|
DBG1(DBG_LIB, "creating master/kernel symlink failed: %m");
|
|
destroy(this);
|
|
return NULL;
|
|
}
|
|
|
|
if (mkdirat(this->dir, UNION_DIR, PERME) != 0 ||
|
|
mkdirat(this->dir, DIFF_DIR, PERME) != 0)
|
|
{
|
|
DBG1(DBG_LIB, "unable to create directories for '%s': %m", name);
|
|
destroy(this);
|
|
return NULL;
|
|
}
|
|
|
|
this->args = args;
|
|
if (args && !saveargs(this, args))
|
|
{
|
|
destroy(this);
|
|
return NULL;
|
|
}
|
|
|
|
if (!mount_unionfs(this))
|
|
{
|
|
destroy(this);
|
|
return NULL;
|
|
}
|
|
|
|
return &this->public;
|
|
}
|
|
|
|
/**
|
|
* load an already created guest
|
|
*/
|
|
guest_t *guest_load(char *parent, char *name)
|
|
{
|
|
private_guest_t *this = guest_create_generic(parent, name, FALSE);
|
|
|
|
if (this == NULL)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
this->args = loadargs(this);
|
|
|
|
if (!mount_unionfs(this))
|
|
{
|
|
destroy(this);
|
|
return NULL;
|
|
}
|
|
|
|
return &this->public;
|
|
}
|
|
|