<?php
// Client for continuous integration and deployment
// It can run on the same machine as the web server.
error_reporting(E_ALL);
ini_set('track_errors', 'On');
set_time_limit(0);
define('RG_JOB_INIT', 1);
define('RG_JOB_HELPER_STARTED', 2);
define('RG_JOB_STARTED', 3);
define('RG_JOB_ERROR', 4);
define('RG_JOB_DONE', 10);
$_s = microtime(TRUE);
$INC = dirname(__FILE__) . "/../inc";
require_once($INC . "/init.inc.php");
require_once($INC . "/log.inc.php");
require_once($INC . "/prof.inc.php");
require_once($INC . "/builder.inc.php");
require_once($INC . "/conn.inc.php");
rg_prof_start('MAIN');
if (!isset($_SERVER['argv'][1])) {
$id = 'main';
} else {
$id = $_SERVER['argv'][1];
}
if (!isset($_SERVER['argv'][2])) {
$conf_file = '/etc/rocketgit/worker.conf';
} else {
$conf_file = $_SERVER['argv'][2];
}
rg_log_set_file($rg_log_dir . '/worker-' . $id . '.log');
rg_log_set_sid("000000"); // to spread the logs
/*
* Load configuration file
*/
function load_config_file($file)
{
global $conf;
$s = @stat($file);
if ($s === FALSE) {
rg_log('Cannot stat conf file ' . $file . ': ' . rg_php_err());
exit(1);
}
if (($s['mode'] & 07) != 0) {
rg_log('Error! Others can access the conf file ['
. $file . '] and read the key'
. ' (mode=' . base_convert($s['mode'], 10, 8) . ')!');
exit(1);
}
rg_log('Loading configuration from ' . $file);
$_conf = @file($file);
if ($_conf === FALSE) {
rg_log('Error! Cannot open the conf file '
. $file . ': ' . rg_php_err());
exit(1);
}
$last_key = FALSE;
foreach ($_conf as $line) {
$tline = trim($line);
if (empty($tline))
continue;
if (strncmp($tline, '#', 1) == 0)
continue;
$t = explode('=', $line, 2);
if (count($t) != 2) {
rg_log('Invalid line [' . $line . ']!');
continue;
}
$var = trim($t[0]);
$value = trim($t[1]);
if (strcmp($var, 'env') == 0) {
$conf['env'][$value] = array('paras' => '');
$last_parent = &$conf['env'][$value];
} else if ((strncmp($line, " ", 1) == 0)
|| (strncmp($line, "\t", 1) == 0)) {
if ($last_parent === FALSE) {
rg_log('Invalid line [' . $line . ']!');
continue;
}
$t = explode('=', $line, 2);
if (count($t) != 2) {
rg_log('Invalid line [' . $line . ']!');
continue;
}
$var = trim($t[0]);
$value = trim($t[1]);
$last_parent[$var] = $value;
} else if (strcmp($var, 'include') == 0) {
if (strncmp($value, '/', 1) != 0)
$value = dirname($file) . '/' . $value;
load_config_file($value);
} else {
$conf[$var] = $value;
$last_parent = FALSE;
}
}
}
/*
* Load configuration file
*/
function load_config($file)
{
global $conf;
$conf = array('env' => array());
load_config_file($file);
if (!isset($conf['master'])) {
rg_log('master line not present in the conf file!');
sleep(60);
exit(1);
}
if (!strstr($conf['master'], '://')) {
$conf['master_proto'] = 'tcp';
$conf['master_host'] = $conf['master'];
$conf['master_port'] = isset($conf['port']) ? $conf['port'] : 65000;
$conf['master_url'] = '';
} else {
$_t = explode('://', $conf['master']);
$conf['master_proto'] = trim($_t[0]);
$_t = explode('/', $_t[1]); // _t[1]: host[:port][/url]
$_x = explode(':', $_t[0]); // _t[0]: host[:port]
$conf['master_host'] = $_x[0];
$conf['master_port'] = isset($_x[1]) ? $_x[1] : 443;
$conf['master_url'] = isset($_t[1]) ? $_t[1] : '';
}
unset($conf['master']);
unset($conf['port']);
// Create state dir
@mkdir($conf['state'], 0771);
// allow libvirt user to access some stuff
$r = @chmod($conf['state'], 0771);
if ($r !== TRUE) {
rg_log('Cannot chmod state dir [' . $conf['state']
. ']: ' . rg_php_err());
sleep(60);
exit(1);
}
if (!file_exists($conf['state'] . '/key.pub')) {
rg_log('Creating SSH key...');
$cmd = 'ssh-keygen -t rsa -b 4096 -N \'\''
. ' -C \'Key to connect to builder\''
. ' -f ' . escapeshellarg($conf['state'] . '/key');
$r = rg_exec($cmd, '', FALSE, FALSE, FALSE);
if ($r['ok'] != 1) {
rg_log('Cannot create key: ' . $r['errmsg'] . '!');
sleep(60);
exit(1);
}
}
$conf['ssh_key'] = @file_get_contents($conf['state'] . '/key.pub');
if ($conf['ssh_key'] === FALSE) {
rg_log('Cannot load key!');
sleep(1);
exit(0);
}
$conf['ssh_key'] = trim($conf['ssh_key']);
if (!isset($conf['templates'])) {
$conf['templates'] = '/var/lib/libvirt/images/rgw/templates';
rg_log('Warn: \'templates\' configuration line is missing'
. '; I will assume to be ' . $conf['templates']);
}
if (!isset($conf['images'])) {
$conf['images'] = '/var/lib/libvirt/images/rgw/images';
rg_log('Warn: \'images\' configuration line is missing'
. '; I will assume the individual section specify full path');
}
// Validate the path to the templates and images
foreach ($conf['env'] as $name => &$i) {
if (!isset($i['image'])) {
// TODO: send this error to rocketgit to be sent by e-mail?
rg_log('Warning! Environment ' . $name . ' is missing \'image\' declaration'
. '; I will disable it.');
unset($conf['env'][$name]);
continue;
}
if (strncmp($i['image'], '/', 1) != 0)
$i['image'] = $conf['images'] . '/' . $i['image'];
if (!file_exists($i['image'])) {
rg_log('Warning! Environment ' . $name . ' is missing ' . $i['image']
. ' file; I will disable it.');
unset($conf['env'][$name]);
continue;
}
if (!isset($i['arch'])) {
rg_log('Warning! Environment ' . $name . ' is missing \'arch\' declaration'
. '; I will assume to be x86_64.');
$i['arch'] = 'x86_64';
}
$f = $conf['templates'] . '/' . $i['arch'] . '.xml';
if (!file_exists($f)) {
rg_log('Warning! Environment ' . $name . ' is missing ' . $i['arch']
. ' file (' . $f . '); I will disable it.');
unset($conf['env'][$name]);
continue;
}
}
if (empty($conf['env'])) {
rg_log('Fatal! No environments found!');
sleep(60);
exit(1);
}
if (!isset($conf['net'])) {
rg_log('\'net\' was not specified, so I will disable network'
. ' access for the build user');
$conf['net'] = 0;
}
if (!isset($conf['libvirtd_user']))
$conf['libvirtd_user'] = 'qemu';
if (!isset($conf['libvirtd_group']))
$conf['libvirtd_group'] = 'qemu';
rg_log_ml('conf: ' . print_r($conf, TRUE));
}
/*
* Save a job
*/
function save_job($job)
{
global $conf;
$ret = array('ok' => 0);
while (1) {
$j_job = @json_encode($job);
if ($j_job === FALSE) {
$ret['errstr'] = 'cannot encode json: ' . json_last_error_msg();
break;
}
$f = $conf['state'] . '/job-' . $job['id'] . '.ser';
$r = @file_put_contents($f . '.tmp', $j_job);
if ($r === FALSE) {
$ret['errstr'] = 'cannot store job: ' . rg_php_err();
break;
}
$r = @rename($f . '.tmp', $f);
if ($r !== TRUE) {
$ret['errstr'] = 'cannot rename job file ['
. $f . ']: ' . rg_php_err();
break;
}
$ret['ok'] = 1;
break;
}
return $ret;
}
/*
* Set correct libvirt rights on a file
*/
function rg_worker_libvirt_rights($f, $mode, $user, $group, &$reason, &$reason2)
{
$ret = FALSE;
while (1) {
// We need to allow libvirt access to the image
$r = @chown($f, $user);
if ($r !== TRUE) {
$reason = 'cannot chown image to qemu user';
$reason2 = rg_php_err();
break;
}
$r = @chgrp($f, $group);
if ($r !== TRUE) {
$reason = 'cannot chgrp image to qemu user';
$reason2 = rg_php_err();
break;
}
$r = @chmod($f, $mode);
if ($r !== TRUE) {
$reason = 'cannot chmod image';
$reason2 = rg_php_err();
break;
}
$r = rg_exec('/usr/sbin/restorecon '
. escapeshellarg($f), '', FALSE, FALSE, FALSE);
if ($r['ok'] !== 1) {
rg_log('restorecon failed, trying to continue ('
. $r['errmsg'] . ': ' . $r['stderr'] . ')');
}
$ret = TRUE;
break;
}
return $ret;
}
/*
* Starts an worker
*/
function start_worker($job)
{
global $conf;
rg_log_ml('DEBUG: start_worker: job: ' . print_r($job, TRUE));
$env = $conf['env'][$job['env']];
rg_log_ml('DEBUG: start_worker: env: ' . print_r($env, TRUE));
$jid = $job['id'];
$emain = escapeshellarg($job['main']);
$name = 'rg-worker-' . $job['id'];
$ename = escapeshellarg($name);
$master = escapeshellarg($env['image']);
$img = $job['main'] . '/image.qcow2'; // TODO: let admin control the image type
$eimg = escapeshellarg($img);
$img2 = $job['main'] . '/image2.raw';
$eimg2 = escapeshellarg($img2);
// TODO: add bellow configuration to the web form
if (!isset($job['disk_size_gib']))
$job['disk_size_gib'] = '10';
if (!isset($job['mem_mib']))
$job['mem_mib'] = '500';
if (!isset($job['cpus']))
$job['cpus'] = '1';
$do_umount = FALSE;
$err = TRUE;
$reason = ''; $reason2 = '';
while (1) {
$r = rg_del_tree($job['main']);
if ($r === FALSE) {
$reason = 'cannot delete main dir';
$reason2 = 'cannot delete main dir (' . $job['main'] . ')';
break;
}
$r = @mkdir($job['main'], 0770);
if ($r === FALSE) {
$reason = 'cannot create main dir';
$reason2 = 'cannot create main dir (' . $job['main'] . '):'
. ' (' . rg_php_err() . ')';
break;
}
// We need to allow libvirt access to the image stored inside
$r = rg_worker_libvirt_rights($job['main'], 0770,
$conf['libvirtd_user'], $conf['libvirtd_group'], $reason, $reason2);
if ($r !== TRUE)
break;
$r = save_job($job);
if ($r['ok'] !== 1) {
$reason = 'cannot save job';
$reason2 = 'cannot save job: ' . $r['errstr'];
break;
}
rg_exec('virsh destroy ' . $ename, '', FALSE, FALSE, FALSE);
rg_exec('virsh undefine --nvram ' . $ename, '', FALSE, FALSE, FALSE);
$r = rg_exec('qemu-img create -o lazy_refcounts=on,cluster_size=256K'
. ' -b ' . $master
. ' -f qcow2 ' . $eimg, '', FALSE, FALSE, FALSE);
if ($r['ok'] !== 1) {
$reason = 'cannot create VM image';
$reason2 = $r['errmsg'] . ': ' . $r['stderr'];
break;
}
$r = rg_worker_libvirt_rights($img, 0770,
$conf['libvirtd_user'], $conf['libvirtd_group'], $reason, $reason2);
if ($r !== TRUE)
break;
$r = rg_exec('qemu-img create -f raw ' . $eimg2
. ' ' . escapeshellarg($job['disk_size_gib'] . 'G'),
'', FALSE, FALSE, FALSE);
if ($r['ok'] !== 1) {
$reason = 'cannot create VM image2';
$reason2 = $r['errmsg'] . ': ' . $r['stderr'];
break;
}
$r = rg_worker_libvirt_rights($img2, 0770,
$conf['libvirtd_user'], $conf['libvirtd_group'], $reason, $reason2);
if ($r !== TRUE)
break;
// Seems that mkfs is not in PATH when we are running from cron.
$path = getenv('PATH');
putenv('PATH=' . $path . ':/usr/sbin');
// TODO: let user choose fs type?
$r = rg_exec('mkfs.ext4 -L RG ' . $eimg2, '', FALSE, FALSE, FALSE);
if ($r['ok'] !== 1) {
$reason = 'cannot create fs';
$reason2 = $r['errmsg'] . ': ' . $r['stderr'];
break;
}
$r = @mkdir($job['main'] . '/root', 0700);
if ($r === FALSE) {
$reason = 'cannot create root dir';
$reason2 = 'cannot create root dir (' . rg_php_err() . ')';
break;
}
$r = rg_exec('mount ' . $eimg2 . ' ' . $emain . '/root',
'', FALSE, FALSE, FALSE);
if ($r['ok'] !== 1) {
$reason = 'cannot mount fs';
$reason2 = $r['errmsg'] . ': ' . $r['stderr'];
break;
}
$do_umount = TRUE;
// Clone repo
$_env = 'GIT_SSH_COMMAND=ssh'
. ' -o PasswordAuthentication=no'
. ' -o ControlMaster=no'
. ' -o IdentitiesOnly=yes'
. ' -o IdentityFile=' . escapeshellarg($conf['state'] . '/key');
putenv($_env);
$_s = time();
$cmd = 'git clone'
. ' --recurse-submodules'
//TODO . ' --shallow-submodules'
. ' --no-checkout'
. ' ' . escapeshellarg($job['url'])
. ' ' . $emain . '/root/git';
$r = rg_exec($cmd, '', FALSE, FALSE, FALSE);
@file_put_contents($job['main'] . '/root/T_clone', time() - $_s);
if ($r['ok'] !== 1) {
$reason = 'git clone error; contact admin';
$reason2 = $r['errmsg'] . ': ' . $r['stderr'];
break;
}
// Build command list
// TODO: document how a user can add labels in configure or make
$s = '#!/bin/bash' . "\n";
$s .= 'date +%s > /mnt/status/build.sh.start' . "\n\n";
$s .= 'export RG_LABELS=/mnt/status/RG_LABELS' . "\n\n";
$s .= 'cd /mnt/git' . "\n\n";
$s .= 'git branch -f rgw ' . escapeshellarg($job['head']) . " &>/mnt/status/git.branch.log\n\n";
$s .= 'git checkout rgw' . " &>/mnt/status/git.checkout.log\n\n";
foreach ($job['cmds'] as $_name => $i) {
if (empty($i['cmd']))
continue;
$prefix = '/mnt/status/'
. escapeshellarg($_name);
if (empty($i['label_ok']))
$lok = 'echo -n';
else
$lok = ' echo '
. escapeshellarg($i['label_ok'])
. ' >>/mnt/status/RG_LABELS' . "\n";
if (empty($i['label_nok']))
$lnok = 'echo -n';
else
$lnok = ' echo '
. escapeshellarg($i['label_nok'])
. ' >>/mnt/status/RG_LABELS' . "\n";
$s .= 'date +%s > ' . $prefix . '.start' . "\n"
. 'echo "Executing [' . $i['cmd'] . ']..."' . "\n"
. '(' . $i['cmd'] . ') &>' . $prefix . '.log' . "\n"
. 'E=${?}' . "\n"
. 'echo ${E} > ' . $prefix . ".status\n"
. 'date +%s > ' . $prefix . '.done' . "\n"
. 'if [ "${E}" != "0" ]; then' . "\n"
. $lnok
. ($i['abort'] ? ' exit 0' . "\n" : '')
. 'else' . "\n"
. $lok
. 'fi' . "\n\n";
}
//rg_log_ml('DEBUG: build.sh: ' . $s);
$r = @file_put_contents($job['main'] . '/root/build.sh', $s);
if ($r === FALSE) {
$reason = 'cannot store build commands';
$reason2 = 'cannot store build commands (' . rg_php_err() . ')';
break;
}
$r = @chmod($job['main'] . '/root/build.sh', 0755);
if ($r === FALSE) {
$reason = 'cannot chmod build.sh';
$reason2 = 'cannot chmod build.sh (' . rg_php_err() . ')';
break;
}
// Prepare packages - for now, we must list every package
// on a single line to avoid error if one is not available
if (!empty($job['packages'])) {
rg_log('DEBUG: packages: ' . $job['packages'] . '.');
$pkgs = explode(' ', $job['packages']);
$p_i_cmd = '';
$p_i_cmd .= '> /mnt/packages.log' . "\n";
foreach ($pkgs as $p) {
$p_i_cmd .= $env['pkg_cmd']
. ' ' . escapeshellarg($p)
. ' >> /mnt/packages.log 2>&1' . "\n";
}
}
// Store commands
$r = @file_put_contents($job['main'] . '/root/rg.sh',
'#!/bin/bash' . "\n"
. 'mkdir /mnt/tmp && chmod 1777 /mnt/tmp' . "\n"
. 'mount --bind /mnt/tmp /tmp' . "\n"
. 'mkdir -p /mnt/var/tmp && chmod 1777 /mnt/var/tmp' . "\n"
. 'mount --bind /mnt/var/tmp /var/tmp' . "\n"
. "\n"
. 'mkdir /mnt/status' . "\n"
. 'chown -R build:build /mnt/git /mnt/status' . "\n"
. 'echo "PATH=${PATH}"' . "\n"
. 'ERR=""' . "\n"
. 'id' . "\n"
. 'date +%s > /mnt/T_START' . "\n"
. '# Waiting for net...' . "\n"
. 'while [ "`ip ro li | grep ^default`" = "" ]; do' . "\n"
. ' (date; ip ro li) &>>/mnt/status/wait_net.log' . "\n"
. ' sleep 1' . "\n"
. 'done' . "\n"
. 'date +%s > /mnt/T_NET_OK' . "\n\n"
. "\n"
. $p_i_cmd
. 'date +%s > /mnt/T_PKGS_OK' . "\n\n"
. "\n"
. '# Disabling further module loading.' . "\n"
. 'echo 1 > /proc/sys/kernel/modules_disabled' . "\n"
. "\n"
. '# Restricting dmesg' . "\n"
. 'sysctl kernel.dmesg_restrict=1' . "\n"
. "\n"
. '# Disabling root login' . "\n"
. 'chage -E 0 root' . "\n"
. 'if [ "${?}" != "0" ]; then' . "\n"
. ' ERR="cannot disable root account"' . "\n"
. 'fi' . "\n"
. "\n"
. '# Disable network access if needed' . "\n"
. 'if [ "' . $conf['net'] . '" = "1" ]; then' . "\n"
. ' echo -n # we allow network access' . "\n"
. 'else' . "\n"
. ' iptables -I OUTPUT -m owner --uid-owner build -j REJECT' . "\n"
. ' if [ "${?}" != "0" ]; then' . "\n"
. ' ERR="Cannot disable network access"' . "\n"
. ' fi' . "\n"
. 'fi' . "\n"
. "\n"
. 'if [ "${ERR}" = "" ]; then' . "\n"
. ' su - build -c "bash /mnt/build.sh" &>/mnt/status/build.log' . "\n"
. ' sync' . "\n"
. 'else' . "\n"
. ' echo "${ERR}" > /mnt/status/err' . "\n"
. 'fi' . "\n"
. 'date +%s > /mnt/T_DONE' . "\n\n"
. 'shutdown -h now'
);
if ($r === FALSE) {
$reason = 'cannot store commands';
$reason2 = 'cannot store commands (' . rg_php_err() . ')';
break;
}
$r = @chmod($job['main'] . '/root/rg.sh', 0700);
if ($r === FALSE) {
$reason = 'cannot to chmod on rg.sh';
$reason2 = 'cannot to chmod on rg.sh (' . rg_php_err() . ')';
break;
}
$r = rg_exec('umount ' . $emain . '/root', '', FALSE, FALSE, FALSE);
if ($r['ok'] !== 1) {
$reason = 'cannot umount fs';
$reason2 = $r['errmsg'] . ': ' . $r['stderr'];
break;
}
$do_umount = FALSE;
$_f = $conf['templates'] . '/' . $env['arch'] . '.xml';
$template = @file_get_contents($_f);
if ($template === FALSE) {
$reason = 'cannot load template';
$reason2 = 'cannot load template from ' . $_f . ': ' . rg_php_err();
break;
}
$template = str_replace('@@name@@', $name, $template);
$template = str_replace('@@mem@@', $job['mem_mib'], $template);
$template = str_replace('@@cpus@@', $job['cpus'], $template);
$template = str_replace('@@disk0@@', $img, $template);
$template = str_replace('@@disk0_type@@', 'qcow2', $template);
$template = str_replace('@@disk1@@', $img2, $template);
$template = str_replace('@@disk1_type@@', 'raw', $template);
// TODO: allow firewall specification
$template = str_replace('@@net0@@', '<interface type=\'network\'><source network=\'default\'/><model type=\'virtio\'/></interface>', $template);
// TODO: take care of XML injection?
$template = str_replace('@@chan1_path@@', $job['main'] . '/x.chan', $template);
$_xml = $job['main'] . '/machine.xml';
$r = @file_put_contents($_xml, $template);
if ($r === FALSE) {
$reason = 'cannot store template file';
$reason2 = 'cannot store template in ' . $_xml . ': ' . rg_php_err();
break;
}
$r = rg_exec('virsh create ' . escapeshellarg($_xml),
'', FALSE, FALSE, FALSE);
if ($r['ok'] !== 1) {
$reason = 'cannot define VM';
$reason2 = $r['errmsg'] . ': ' . $r['stderr'];
break;
}
$r = @file_put_contents($job['main'] . '/vm.start', time());
$err = FALSE;
break;
}
if ($do_umount)
rg_exec('umount ' . $emain . '/root', '', FALSE, FALSE, FALSE);
// Any error above must retrigger the build on other worker
if ($err) {
rg_log('error: ' . $reason);
rg_log('error2: ' . $reason2);
@file_put_contents($job['main'] . '/error.log', $reason);
@file_put_contents($job['main'] . '/error2.log', $reason2);
}
rg_log('Done');
}
/*
* Handle received commands (one JSON)
*/
function xhandle_one($key, $data)
{
global $rg_log_dir;
global $jobs;
global $conf;
global $pid_to_jid;
global $features;
$u = @json_decode($data, TRUE);
if ($u === NULL) {
rg_log('JSON: ' . $data);
rg_log_ml('Cannot decode JSON: ' . json_last_error_msg());
$err = array('errstr' => 'cannot decode json');
rg_conn_enq('master', json_encode($err) . "\n");
rg_conn_destroy($key);
return;
}
if (isset($u['id']))
$jid = $u['id'];
if (strcmp($u['op'], 'BLD') == 0) {
// TODO: should we confirm quickly if the job is accepted,
// even if we could not fork?
if (isset($jobs[$jid])) {
// TODO: this should not happen, right?
rg_log('Job ' . $jid . ' already in queue!');
return;
}
$jobs[$jid] = $u;
$jobs[$jid]['main'] = $conf['state'] . '/rocketgit-j-' . $jid;
$jobs[$jid]['state'] = RG_JOB_INIT;
rg_log_ml('build job: ' . print_r($u, TRUE));
$err = TRUE;
while (1) {
// Fork
$pid = pcntl_fork();
if ($pid == -1) {
$reason = 'Cannot fork!';
break;
}
if ($pid == 0) { // child
rg_log_set_file($rg_log_dir . '/worker-' . $conf['id']
. '-' . $jid . '.log');
start_worker($jobs[$jid]);
exit(0);
}
rg_log('Started worker with pid ' . $pid);
$jobs[$jid]['state'] = RG_JOB_HELPER_STARTED;
$pid_to_jid[$pid] = $jid;
$err = FALSE;
break;
}
$a = array('id' => $jid);
if ($err) {
$a['op'] = 'ABR';
$a['reason'] = $reason;
rg_conn_enq('master', json_encode($a) . "\n");
unset($pid_to_jid[$pid]);
unset($jobs[$jid]);
return;
}
$a['op'] = 'STA';
rg_conn_enq('master', json_encode($a) . "\n");
} else if (strcmp($u['op'], 'DRE') == 0) { // DRE = done received
// So, we can clean up everything related to this job
// TODO: do we clear the state file?
rg_log('DRE command');
$_job = &$jobs[$jid];
unset($pid_to_jid[$_job['pid']]);
unset($jobs[$jid]);
@unlink($conf['state'] . '/job-' . $jid . '.ser');
} else if (strcmp($u['op'], 'FEATURES') == 0) { // what master suports
rg_log_ml('FEATURES command: ' . print_r($u, TRUE));
$features = $u['features'];
} else {
rg_log($key . ': cannot handle op: ' . $u['op']);
}
}
/*
* Handle received commands
*/
function xhandle($key, $data)
{
$ret = 0;
while (1) {
$pos = strpos($data, "\n");
if ($pos === FALSE)
return $ret;
$one = substr($data, 0, $pos);
xhandle_one($key, $one);
$data = substr($data, $pos + 1);
$ret += $pos + 1;
}
return $ret;
}
/*
* Extracts info from the virtual disk
* TODO: if something fails, we may keep the file mounted!
*/
function rg_job_extract_info(&$job)
{
global $conf;
$jid = $job['id'];
rg_log('DEBUG: extract_info: root=' . $job['main']);
$emain = escapeshellarg($job['main']);
while (1) {
if (!is_dir($job['main'])) {
$job['error'] = 'Main dir [' . $job['main']
. '] not presend;'
. ' probably disk space problems';
break;
}
$r = @file_get_contents($job['main'] . '/error.log');
if ($r !== FALSE) {
$job['error'] = $r;
break;
}
$r = @file_get_contents($job['main'] . '/error2.log');
if ($r !== FALSE) {
$job['error2'] = $r;
break;
}
// Extract how much disk space was used
// TODO: Warn the user when the disk space is close to the limit?
// TODO: or alloc a lot of space by default?
$r = @stat($job['main'] . '/image2.raw');
if ($r === FALSE) {
$job['error'] = 'Missing image2 file';
break;
}
$job['status']['disk_used_mib'] = intval($r['blocks'] / 2 / 1024);
// TODO - remove this
$cmd = 'ln -f ' . $emain . '/image2.raw ' . $emain . '/..';
$r = rg_exec($cmd, '', FALSE, FALSE, FALSE);
// TODO - remove this
$cmd = 'ln -f ' . $emain . '/machine.xml ' . $emain . '/..';
$r = rg_exec($cmd, '', FALSE, FALSE, FALSE);
$cmd = 'mount ' . $emain . '/image2.raw ' . $emain . '/root';
$r = rg_exec($cmd, '', FALSE, FALSE, FALSE);
if ($r['ok'] != 1) {
$job['error'] = 'Could not mount image: ' . $r['errmsg'];
break;
}
$r = @file_get_contents($job['main'] . '/root/status/err');
if ($r !== FALSE)
$job['error'] = $r;
$labels = @file($job['main'] . '/root/status/RG_LABELS');
if ($labels === FALSE)
$labels = array();
foreach ($labels as $index => $l)
$labels[$index] = trim($l);
// Add worker name as label
$labels[] = 'worker/' . $conf['name'] . '/color=fff';
$clone_elap = @file_get_contents($job['main'] . '/root/T_clone');
if ($clone_elap === FALSE)
$clone_elap = 'n/a';
$build_sh_start = @file_get_contents($job['main'] . '/root/status/build.sh.start');
if ($build_sh_start === FALSE)
$build_sh_start = 'n/a';
else
$build_sh_start = intval($build_sh_start);
$vm_start = @file_get_contents($job['main'] . '/vm.start');
if ($vm_start === FALSE)
$vm_start = 0;
else
$vm_start = intval($vm_start);
$job['status'] = array(
'vm_start' => $vm_start,
'build_sh_start' => $build_sh_start,
'packages' => @trim(file_get_contents($job['main'] . '/root/packages.log')),
'clone_elap' => $clone_elap,
'start' => @trim(file_get_contents($job['main'] . '/root/T_START')),
'net_ok' => @trim(file_get_contents($job['main'] . '/root/T_NET_OK')),
'pkgs_ok' => @trim(file_get_contents($job['main'] . '/root/T_PKGS_OK')),
'done' => @trim(file_get_contents($job['main'] . '/root/T_DONE')),
'labels' => $labels
);
$job['status']['cmds'] = array();
foreach ($job['cmds'] as $cmd => $i) {
if (empty($i['cmd']))
continue;
$sd = $job['main'] . '/root/status/' . $cmd;
$job['status']['cmds'][$cmd] = array(
'cmd' => $i['cmd'],
'start' => trim(@file_get_contents($sd . '.start')),
'done' => trim(@file_get_contents($sd . '.done')),
'status' => trim(@file_get_contents($sd . '.status')),
'log' => trim(rg_file_get_tail($sd . '.log', 4 * 4096))
);
}
unset($job['cmds']);
unset($job['url']);
unset($job['head']);
unset($job['env']);
break;
}
$cmd = 'umount ' . $emain . '/root';
$r = rg_exec($cmd, '', FALSE, FALSE, FALSE);
if ($r['ok'] != 1)
rg_log('Cannot unmount [' . $job['main'] . '/root]: ' . $r['errmsg'] . '!');
rg_del_tree($job['main']);
rg_log_ml('DEBUG: job: ' . print_r($job, TRUE));
return TRUE;
}
/*
* Extract blk/net/cpu/mem info from a VM
*/
function vm_extract_info($name)
{
$ret = array();
while (1) {
$cmd = 'virsh domstats --raw ' . escapeshellarg($name);
$r = rg_exec($cmd, '', FALSE, FALSE, FALSE);
if ($r['ok'] != 1) {
rg_log('Could not get dom stats: ' . $r['errmsg']);
break;
}
//rg_log_ml('DEBUG: domstats: ' . print_r($r['data'], TRUE));
$data = array();
$t = explode("\n", $r['data']);
foreach ($t as $line) {
$line = trim($line);
$x = explode('=', $line, 2);
if (!isset($x[1]))
continue;
$data[$x[0]] = $x[1];
}
$ret['rx_bytes'] = $data['net.0.rx.bytes'];
$ret['rx_pkts'] = $data['net.0.rx.pkts'];
$ret['tx_bytes'] = $data['net.0.tx.bytes'];
$ret['tx_pkts'] = $data['net.0.tx.pkts'];
$ret['block_read_ops'] = $data['block.1.rd.reqs'];
$ret['block_read_bytes'] = $data['block.1.rd.bytes'];
$ret['block_write_ops'] = $data['block.1.wr.reqs'];
$ret['block_write_bytes'] = $data['block.1.wr.bytes'];
$ret['block_physical_bytes'] = $data['block.1.physical'];
$ret['block_allocation_bytes'] = $data['block.1.allocation'];
$ret['cpu_time_ns'] = $data['cpu.time'];
$ret['ballon_current_mib'] = intval($data['balloon.current'] / 1024);
$ret['ballon_rss_mib'] = intval($data['balloon.rss'] / 1024);
break;
}
return $ret;
}
/*
* Send stats about the worker
*/
function send_worker_stats()
{
static $last_time = 0;
global $features;
global $stats;
if (!isset($features['allow_stats']))
return;
$load = rg_load();
$now = time();
if ($last_time > $now - 30)
return;
$a = array(
'op' => 'WORKER_STATS',
'ts' => $now,
'load' => $load,
'jobs' => $stats['jobs']
);
rg_conn_enq('master', json_encode($a) . "\n");
$last_time = $now;
}
umask(0007);
load_config($conf_file);
rg_log('id is [' . $id . ']');
$conf['id'] = $id;
// What master supports
$features = array();
// stats
$stats = array('jobs' => 0);
rg_log('Connecting to ' . $conf['master_host'] . '/' . $conf['master_port']
. ' with proto ' . $conf['master_proto']
. ' with url [' . $conf['master_url'] . ']' . '...');
if (strcmp($conf['master_proto'], 'tcp') == 0) {
$socket = @socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if ($socket === FALSE) {
rg_log('Cannot create socket: ' . rg_php_err());
exit(1);
}
$r = @socket_connect($socket, $conf['master_host'], $conf['master_port']);
if ($r === FALSE) {
rg_log('Cannot connect: ' . rg_php_err());
exit(1);
}
} else if (strcmp($conf['master_proto'], 'proxy_tls') == 0) {
$context = stream_context_create();
// TODO: make timeout configurable
$socket = @stream_socket_client('tls://' . $conf['master_host']
. ':' . $conf['master_port'], $errno, $errstr, 30,
STREAM_CLIENT_CONNECT, $context);
if ($socket === FALSE) {
rg_log('Cannot connect: ' . $errstr);
exit(1);
}
$s = "GET " . $conf['maser_url'] . " HTTP/1.1\r\n"
. "Host: " . $conf['master_host'] . ':' . $conf['master_port'] . "\r\n"
. "Connection: keep-alive, Upgrade\r\n"
. "Pragma: no-cache\r\n"
. "Cache-Control: no-cache\r\n"
. "Upgrade: websocket\r\n"
. "\r\n";
$r = @fwrite($socket, $s);
if ($r === FALSE) {
rg_log('Cannot write HTTP request: ' . rg_php_err());
exit(1);
}
$buf = '';
while (1) {
$r = @fread($socket, 4096);
if ($r === FALSE) {
rg_log('Cannot read HTTP answer: ' . rg_php_err());
exit(1);
}
$buf .= $r;
rg_log_ml('Answer: ' . $buf);
if (strstr($buf, "\r\n\r\n") || strstr($buf, "\n\n"))
break;
}
} else {
rg_log('Invalid master protocol: ' . $conf['master_proto'] . '!');
sleep(60);
exit(1);
}
rg_log('Connected.');
rg_conn_new('master', $socket);
$rg_conns['master']['exit_on_close'] = 1;
$rg_conns['master']['func_data'] = 'xhandle';
// announce ourselves
$key = $conf['key']; unset($conf['key']);
$ann = $conf;
$ann['op'] = 'ANN';
$ann['uname'] = php_uname('a');
$ann['host'] = php_uname('n');
$ann['arch'] = php_uname('m');
$ann['boot_time'] = time();
$ann['sign'] = hash_hmac('sha512', $ann['boot_time'], $key);
$j_ann = @json_encode($ann);
if ($j_ann === FALSE) {
rg_log('Cannot encode json: ' . json_last_error_msg());
exit(1);
}
rg_conn_enq('master', $j_ann . "\n");
$jobs = array();
$pid_to_jid = array();
while(1) {
rg_conn_wait(3);
send_worker_stats();
// Verify if the jobs are really started
while (1) {
$pid = pcntl_waitpid(-1, $status, WNOHANG);
if ($pid === 0)
break;
if ($pid == -1)
break;
$jid = $pid_to_jid[$pid];
rg_log('Pid ' . $pid . ' exited (job ' . $jid . ')'
. ' with status ' . $status . '!');
unset($pid_to_jid[$pid]);
$jobs[$jid]['state'] = RG_JOB_STARTED;
$stats['jobs']++;
}
// Verify if VMs finished
$vms_loaded = FALSE;
foreach ($jobs as $jid => &$job) {
if ($job['state'] != RG_JOB_STARTED)
continue;
if ($vms_loaded === FALSE) {
$vms = rg_builder_vm_list();
if ($vms === FALSE)
break;
$vms_loaded = TRUE;
//rg_log_ml('vms: ' . print_r($vms, TRUE));
}
$name = 'rg-worker-' . $jid;
$k = array_search($name, $vms);
if ($k !== FALSE) {
//rg_log('VM in progress');
// TODO: if too much time, abort (kill
// worker and destroy virtual machine)
//TODO: $job['error'] = 'too much time';
// TODO: Signal from inside VM that we finished and extracts stats at that time
$job['stats'] = vm_extract_info($name);
continue;
}
rg_log('VM ' . $jid . ' finished');
rg_job_extract_info($job);
if (isset($job['error']))
$job['state'] = RG_JOB_ERROR;
else
$job['state'] = RG_JOB_DONE;
// TODO: store in fs to be able to still inform the
// master if we are crashing.
save_job($job);
$xjob = $job;
$xjob['op'] = 'DON';
unset($xjob['debug']);
unset($xjob['packages']);
unset($xjob['main']);
$j_xjob = @json_encode($xjob);
if ($j_xjob === FALSE) {
rg_log('Cannot encode json: ' . json_last_error_msg());
} else {
rg_conn_enq('master', $j_xjob . "\n");
}
$cmd = 'virsh destroy rocketgit-j-' . $jid;
$r = rg_exec($cmd, '', FALSE, FALSE, FALSE);
if ($r['ok'] != 1) {
// If error, probably the machine was not running, so, this is just a warning
// TODO: But we set an error!
$job['error'] = 'Could not destroy: ' . $r['errmsg'];
rg_log('Error: ' . $job['error']);
}
$stats['jobs']--;
}
}
rg_prof_end('MAIN');
rg_prof_log();