/scripts/worker.php (7b83c10ddca7281e0ce3c6d75e739c6eb3713e39) (30891 bytes) (mode 100644) (type blob)

<?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();


Mode Type Size Ref File
100644 blob 9 f3c7a7c5da68804a1bdf391127ba34aed33c3cca .exclude
100644 blob 102 eaeb7d777062c60a55cdd4b5734902cdf6e1790c .gitignore
100644 blob 375 1f425bcd2049c526744d449511094fc045ceac74 AUTHORS
100644 blob 1132 dd65951315f3de6d52d52a82fca59889d1d95187 Certs.txt
100644 blob 1274 09165f115226fccce99add24cdf77becdd1ec1f6 History.txt
100644 blob 34520 dba13ed2ddf783ee8118c6a581dbf75305f816a3 LICENSE
100644 blob 3464 ec8d83caa215ab1aca88b7ac273557262009c82a Makefile.in
100644 blob 5867 0c5899445818b82269b17da3fff4c39a89f760bb README
100644 blob 146199 66abf9b425c5b7e86a25d198793750877cafff56 TODO
100644 blob 1294 f22911eb777f0695fcf81ad686eac133eb11fcc4 TODO-plans
100644 blob 203 a2863c67c3da44126b61a15a6f09738c25e0fbe0 TODO.perf
100644 blob 967 56bbaa7c937381fb10a2907b6bbe056ef8cc824a TODO.vm
040000 tree - 21928e906ad2907a55c2e81c2a8b0502b586b8a0 artwork
100644 blob 5550 bde1bc0fad6f0ebb858d324a2d58510bb339a6de compare.csv
100755 blob 30 92c4bc48245c00408cd7e1fd89bc1a03058f4ce4 configure
040000 tree - 69114e8648f8e0e7173c76e30ca6bbfcece7df31 debian
040000 tree - a9d8117dcc14048c006970b5debd2f51cf52fdfd docker
040000 tree - f67d3605efbd6422a8acdd953578991139266391 docs
100755 blob 16720 52405deef0d3708e7553022e1e9db73faa28d05c duilder
100644 blob 536 7e834f8f0a52ada786dd978522cd0f310e2438f6 duilder.conf
040000 tree - 77290d80669b96d488189caad411699db4b0a6a0 hooks
040000 tree - 8d09657bf79729a4df1d2856b7afadcbd13b3de3 inc
040000 tree - e255ce234c3993998edc12bc7e93fff555376eda misc
100644 blob 4616 df249b84bedcc590a2a9b73cdc17ad1a8e008ca6 rocketgit.spec.in
040000 tree - ecdfe0855baba8218eeabb995e579bc9446abee2 root
040000 tree - 2ac5565bc70ea971f2ea36d94953c7e00e3f4b8e samples
040000 tree - c292fa2029303fcfeb6b3c328e8a776cf16e5475 scripts
040000 tree - ecb1da91f5ae28f3f33eca9e5d076c3f9be92f49 selinux
100755 blob 256 462ccd108c431f54e380cdac2329129875a318b5 spell_check.sh
040000 tree - 3aee54193d1f2fb794cb1133433e4645b864f5a0 techdocs
040000 tree - 56db4df97c8496c2a7f99c56d64db58657f0040c tests
040000 tree - 3a262971aa172ade74d5a4930e04393918ca1911 tools
Hints:
Before first commit, do not forget to setup your git environment:
git config --global user.name "your_name_here"
git config --global user.email "your@email_here"

Clone this repository using HTTP(S):
git clone https://rocketgit.com/user/catalinux/rocketgit

Clone this repository using ssh (do not forget to upload a key first):
git clone ssh://rocketgit@ssh.rocketgit.com/user/catalinux/rocketgit

Clone this repository using git:
git clone git://git.rocketgit.com/user/catalinux/rocketgit

You are allowed to anonymously push to this repository.
This means that your pushed commits will automatically be transformed into a merge request:
... clone the repository ...
... make some changes and some commits ...
git push origin main