ENHANCEMENT: Using MOAR symfony
This commit is contained in:
parent
92e74f0797
commit
0fef3a275e
|
@ -33,13 +33,15 @@ class DaemonCommand extends Command
|
|||
$this
|
||||
->setDescription('The deamon slayer of webcron')
|
||||
->setHelp('This command is the daemon process of webcron, enabling webcron to actually run jobs on time')
|
||||
->addOption('time-limit', 't', InputOption::VALUE_REQUIRED, 'Time limit in seconds before stopping the daemon.');
|
||||
->addOption('time-limit', 't', InputOption::VALUE_REQUIRED, 'Time limit in seconds before stopping the daemon.')
|
||||
->addOption('async', 'a', InputOption::VALUE_NEGATABLE, 'Time limit in seconds before stopping the daemon.');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$jobRepo = $this->doctrine->getRepository(Job::class);
|
||||
$timelimit = $input->getOption('time-limit') ?? false;
|
||||
$async = $input->getOption('async') ?? function_exists('pcntl_fork');
|
||||
if ($timelimit === false) {
|
||||
$endofscript = false;
|
||||
} elseif(is_numeric($timelimit)) {
|
||||
|
@ -55,27 +57,29 @@ class DaemonCommand extends Command
|
|||
$jobsToRun = $jobRepo->getJobsDue();
|
||||
if(!empty($jobsToRun)) {
|
||||
foreach($jobsToRun as $job) {
|
||||
$jobObj = $jobRepo->getJob($job['id']);
|
||||
if($jobObj['data']['crontype'] == 'reboot') {
|
||||
if($job->getData('crontype') == 'reboot') {
|
||||
$str = @file_get_contents('/proc/uptime');
|
||||
$num = floatval($str);
|
||||
$rebootedself = ($num < $jobObj['data']['reboot-duration'] * 60);
|
||||
$consolerun = $jobRepo->getTempVar($job['id'], 'consolerun', false);
|
||||
$rebootedself = ($num < $job->getData('reboot-duration') * 60);
|
||||
$consolerun = $jobRepo->getTempVar($job->getId(), 'consolerun', false);
|
||||
if($consolerun && !$rebootedself) continue;
|
||||
}
|
||||
$jobRepo->setJobRunning($job['id'], true);
|
||||
$output->writeln('Running Job ' . $job['id']);
|
||||
declare(ticks = 1);
|
||||
pcntl_signal(SIGCHLD, SIG_IGN);
|
||||
$pid = pcntl_fork();
|
||||
$this->doctrine->getConnection()->close();
|
||||
$jobRepo = $this->doctrine->getRepository(Job::class);
|
||||
if($pid == -1) {
|
||||
$jobRepo->RunJob($job['id'], $job['running'] == 2);
|
||||
$jobRepo->setJobRunning($job['id'], false);
|
||||
$jobRepo->setJobRunning($job->getId(), true);
|
||||
$output->writeln('Running Job ' . $job->getId());
|
||||
if($async) {
|
||||
declare(ticks = 1);
|
||||
pcntl_signal(SIGCHLD, SIG_IGN);
|
||||
$pid = pcntl_fork();
|
||||
$this->doctrine->getConnection()->close();
|
||||
$jobRepo = $this->doctrine->getRepository(Job::class);
|
||||
}
|
||||
|
||||
if(!$async || $pid == -1) {
|
||||
$jobRepo->RunJob($job->getId(), $job->getRunning() == 2);
|
||||
$jobRepo->setJobRunning($job->getId(), false);
|
||||
} elseif ($pid == 0) {
|
||||
$jobRepo->RunJob($job['id'], $job['running'] == 2);
|
||||
$jobRepo->setJobRunning($job['id'], false);
|
||||
$jobRepo->RunJob($job->getId(), $job->getRunning() == 2);
|
||||
$jobRepo->setJobRunning($job->getId(), false);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ class JobController extends AbstractController
|
|||
if($request->getMethod() == 'GET') {
|
||||
$jobRepo = $doctrine->getRepository(Job::class);
|
||||
$job = $jobRepo->getJob($id, true);
|
||||
return $this->render('job/edit.html.twig', $job);
|
||||
return $this->render('job/edit.html.twig', ['job' => $job]);
|
||||
} elseif($request->getMethod() == 'POST') {
|
||||
$allValues = $request->request->all();
|
||||
$jobRepo = $doctrine->getRepository(Job::class);
|
||||
|
|
|
@ -5,6 +5,7 @@ namespace App\Entity;
|
|||
use App\Repository\JobRepository;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use InvalidArgumentException;
|
||||
|
||||
#[ORM\Entity(repositoryClass: JobRepository::class)]
|
||||
class Job
|
||||
|
@ -100,33 +101,78 @@ class Job
|
|||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getData(): array
|
||||
public function getData(?string $name = ''): mixed
|
||||
{
|
||||
return json_decode($this->data, true);
|
||||
$data = json_decode($this->data, true);
|
||||
if(!empty($name)) {
|
||||
$names = explode('.', $name);
|
||||
foreach($names as $item) {
|
||||
if(!isset($data[$item])) {
|
||||
return NULL;
|
||||
}
|
||||
$data = $data[$item];
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @return Job
|
||||
*/
|
||||
public function setData(array $data): Job
|
||||
{
|
||||
$this->data = json_encode($data);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addData(string $name, mixed $value): Job
|
||||
public function addData(string $name, mixed $value): mixed
|
||||
{
|
||||
$data = json_decode($this->data, true);
|
||||
$data[$name] = $value;
|
||||
if (!empty($name)) {
|
||||
$this->addDataItem($data, $name, $value);
|
||||
}
|
||||
$this->data = json_encode($data);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function addDataItem(array &$data, array|string $name, mixed $value): bool
|
||||
{
|
||||
$names = is_array($name) ? $name : explode('.', $name);
|
||||
$current = $names[0];
|
||||
if(isset($data[$current]) && is_array($data[$current])) {
|
||||
unset($names[0]);
|
||||
$this->addDataItem($data[$current], array_values($names), $value);
|
||||
} else {
|
||||
$data[$names[0]] = $value;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function removeData(?string $name = ''): mixed
|
||||
{
|
||||
$data = json_decode($this->data, true);
|
||||
if (!empty($name)) {
|
||||
$this->removeDataItem($data, $name);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function removeDataItem(array &$data, array|string $name): bool
|
||||
{
|
||||
$names = is_array($name) ? $name : explode('.', $name);
|
||||
$current = $names[0];
|
||||
if(is_array($data[$current])) {
|
||||
unset($names[0]);
|
||||
$this->removeDataItem($data[$current], array_values($names));
|
||||
} elseif(!isset($data[$current])) {
|
||||
return false;
|
||||
} else {
|
||||
unset($names[0]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function hasData($name): bool
|
||||
{
|
||||
return !empty($this->getData($name));
|
||||
}
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
|
|
|
@ -25,7 +25,7 @@ class JobRepository extends EntityRepository
|
|||
|
||||
$return = [];
|
||||
foreach($jobs as $job) {
|
||||
if($job->getData()['needschecking']) {
|
||||
if($job->getData('needschecking')) {
|
||||
$return[] = $job;
|
||||
}
|
||||
}
|
||||
|
@ -35,13 +35,14 @@ class JobRepository extends EntityRepository
|
|||
public function getRunningJobs(bool $idiskey = false): array
|
||||
{
|
||||
$qb = $this->createQueryBuilder('job');
|
||||
return $qb->where('job.running != 0')->getQuery()->getResult();
|
||||
return $qb
|
||||
->where('job.running != 0')
|
||||
->getQuery()->getResult();
|
||||
}
|
||||
|
||||
public function getAllJobs(bool $idiskey = false)
|
||||
{
|
||||
$qb = $this->createQueryBuilder('job');
|
||||
|
||||
/** @var Job[] $jobs */
|
||||
$jobs = $qb
|
||||
->orderBy('job.name')
|
||||
|
@ -79,19 +80,29 @@ class JobRepository extends EntityRepository
|
|||
|
||||
public function getJobsDue()
|
||||
{
|
||||
$jobsSql = "SELECT id, running
|
||||
FROM job
|
||||
WHERE (
|
||||
nextrun <= :timestamp
|
||||
AND (lastrun IS NULL OR lastrun > :timestamplastrun)
|
||||
AND running IN (0,2)
|
||||
)
|
||||
OR (running NOT IN (0,1,2) AND running < :timestamprun)
|
||||
OR (running = 2)";
|
||||
$jobsStmt = $this->getEntityManager()->getConnection()->prepare($jobsSql);
|
||||
$jobsRslt = $jobsStmt->executeQuery([':timestamp' => time(), ':timestamplastrun' => time(), ':timestamprun' => time()]);
|
||||
$jobs = $jobsRslt->fetchAllAssociative();
|
||||
return $jobs;
|
||||
$qb = $this->createQueryBuilder('job');
|
||||
return $qb
|
||||
->where(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->lte('job.nextrun', ':timestamp'),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->isNull('job.lastrun'),
|
||||
$qb->expr()->gt('job.lastrun', ':timestamp')
|
||||
),
|
||||
$qb->expr()->in('job.running', [0,2])
|
||||
)
|
||||
)
|
||||
->orWhere(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->notIn('job.running', [0,1,2]),
|
||||
$qb->expr()->lt('job.running', ':timestamp')
|
||||
)
|
||||
)
|
||||
->orWhere('job.running = 2')
|
||||
->orderBy('job.running', 'DESC')
|
||||
->addOrderBy('job.nextrun', 'ASC')
|
||||
->setParameter(':timestamp', time())
|
||||
->getQuery()->getResult();
|
||||
}
|
||||
|
||||
public function getTimeOfNextRun()
|
||||
|
@ -186,25 +197,25 @@ class JobRepository extends EntityRepository
|
|||
return $result['temp_vars'][$name] ?? $default;
|
||||
}
|
||||
|
||||
private function runHttpJob(array $job): array
|
||||
private function runHttpJob(Job $job): array
|
||||
{
|
||||
$client = new Client();
|
||||
|
||||
if(!empty($job['data']['vars'])) {
|
||||
foreach($job['data']['vars'] as $key => $var) {
|
||||
if (!empty($job['data']['basicauth-username'])) $job['data']['basicauth-username'] = str_replace('{' . $key . '}', $var['value'], $job['data']['basicauth-username']);
|
||||
$job['data']['url'] = str_replace('{' . $key . '}', $var['value'], $job['data']['url']);
|
||||
if(!empty($job->getData('vars'))) {
|
||||
foreach($job->getData('vars') as $key => $var) {
|
||||
if (!empty($job->getData('basicauth-username'))) $job->addData('basicauth-username', str_replace('{' . $key . '}', $var['value'], $job->getData('basicauth-username')));
|
||||
$job->addData('url', str_replace('{' . $key . '}', $var['value'], $job->getData('url')));
|
||||
}
|
||||
}
|
||||
|
||||
$url = $job['data']['url'];
|
||||
$url = $job->getData('url');
|
||||
$options['http_errors'] = false;
|
||||
$options['auth'] = !empty($job['data']['basicauth-username']) ? [$job['data']['basicauth-username'], $job['data']['basicauth-password']] : NULL;
|
||||
$options['auth'] = !empty($job->getData('basicauth-username')) ? [$job->getData('basicauth-username'), $job->getData('basicauth-password')] : NULL;
|
||||
try {
|
||||
$res = $client->request('GET', $url, $options);
|
||||
$return['exitcode'] = $res->getStatusCode();
|
||||
$return['output'] = $res->getBody();
|
||||
$return['failed'] = !in_array($return['exitcode'], $job['data']['http-status']);
|
||||
$return['failed'] = !in_array($return['exitcode'], $job->getData('http-status'));
|
||||
} catch(GuzzleException $exception) {
|
||||
$return['exitcode'] = $exception->getCode();
|
||||
$return['output'] = $exception->getMessage();
|
||||
|
@ -214,25 +225,25 @@ class JobRepository extends EntityRepository
|
|||
return $return;
|
||||
}
|
||||
|
||||
private function runCommandJob(array $job): array
|
||||
private function runCommandJob(Job $job): array
|
||||
{
|
||||
if(!empty($job['data']['vars'])) {
|
||||
foreach ($job['data']['vars'] as $key => $var) {
|
||||
$job['data']['command'] = str_replace('{' . $key . '}', $var['value'], $job['data']['command']);
|
||||
if(!empty($job->getData('vars'))) {
|
||||
foreach ($job->getData('vars') as $key => $var) {
|
||||
$job->addData('command', str_replace('{' . $key . '}', $var['value'], $job->getData('command')));
|
||||
}
|
||||
}
|
||||
|
||||
$command = $job['data']['command'];
|
||||
if ($job['data']['containertype'] == 'docker') {
|
||||
$command = $this->prepareDockerCommand($command, $job['data']['service'], $job['data']['container-user']);
|
||||
$command = $job->getData('command');
|
||||
if ($job->getData('containertype') == 'docker') {
|
||||
$command = $this->prepareDockerCommand($command, $job->getData('service'), $job->getData('container-user'));
|
||||
}
|
||||
try {
|
||||
if($job['data']['hosttype'] == 'local') {
|
||||
if($job->getData('hosttype') == 'local') {
|
||||
$return = $this->runLocalCommand($command);
|
||||
} elseif($job['data']['hosttype'] == 'ssh') {
|
||||
$return = $this->runSshCommand($command, $job['data']['host'], $job['data']['user'], $job['data']['ssh-privkey'], $job['data']['privkey-password']);
|
||||
} elseif($job->getData('hosttype') == 'ssh') {
|
||||
$return = $this->runSshCommand($command, $job->getData('host'), $job->getData('user'), $job->getData('ssh-privkey'), $job->getData('privkey-password'));
|
||||
}
|
||||
$return['failed'] = !in_array($return['exitcode'], $job['data']['response']);
|
||||
$return['failed'] = !in_array($return['exitcode'], $job->getData('response'));
|
||||
} catch (\RuntimeException $exception) {
|
||||
$return['exitcode'] = $exception->getCode();
|
||||
$return['output'] = $exception->getMessage();
|
||||
|
@ -253,7 +264,7 @@ class JobRepository extends EntityRepository
|
|||
return $return;
|
||||
}
|
||||
|
||||
private function runSshCommand(string $command, string $host, string $user, string $privkey, string $password): array
|
||||
private function runSshCommand(string $command, string $host, string $user, ?string $privkey, ?string $password): array
|
||||
{
|
||||
$ssh = new SSH2($host);
|
||||
$key = null;
|
||||
|
@ -378,14 +389,13 @@ class JobRepository extends EntityRepository
|
|||
|
||||
public function runJob(int $job, bool $manual): array
|
||||
{
|
||||
global $kernel;
|
||||
$starttime = microtime(true);
|
||||
$job = $this->getJob($job, true);
|
||||
if ($job['data']['crontype'] == 'http') {
|
||||
if ($job->getData('crontype') == 'http') {
|
||||
$result = $this->runHttpJob($job);
|
||||
} elseif ($job['data']['crontype'] == 'command') {
|
||||
} elseif ($job->getData('crontype') == 'command') {
|
||||
$result = $this->runCommandJob($job);
|
||||
} elseif ($job['data']['crontype'] == 'reboot') {
|
||||
} elseif ($job->getData('crontype') == 'reboot') {
|
||||
$result = $this->runRebootJob($job, $starttime, $manual);
|
||||
if(isset($result['status']) && $result['status'] == 'deferred') return $result;
|
||||
}
|
||||
|
@ -405,8 +415,8 @@ class JobRepository extends EntityRepository
|
|||
}
|
||||
|
||||
// Remove secrets from output
|
||||
if(!empty($job['data']['vars'])) {
|
||||
foreach($job['data']['vars'] as $key => $var) {
|
||||
if(!empty($job->getData('vars'))) {
|
||||
foreach($job->getData('vars') as $key => $var) {
|
||||
if ($var['issecret']) {
|
||||
$result['output'] = str_replace($var['value'], '{'.$key.'}', $result['output']);
|
||||
}
|
||||
|
@ -415,20 +425,20 @@ class JobRepository extends EntityRepository
|
|||
// saving to database
|
||||
$this->getEntityManager()->getConnection()->close();
|
||||
$runRepo = $this->getEntityManager()->getRepository(Run::class);
|
||||
$runRepo->addRun($job['id'], $result['exitcode'], floor($starttime), $runtime, $result['output'], $flags);
|
||||
$runRepo->addRun($job->getId(), $result['exitcode'], floor($starttime), $runtime, $result['output'], $flags);
|
||||
if (!$manual){
|
||||
// setting nextrun to next run
|
||||
$nextrun = $job['nextrun'];
|
||||
$nextrun = $job->getNextrun();
|
||||
do {
|
||||
$nextrun = $nextrun + $job['interval'];
|
||||
$nextrun = $nextrun + $job->getInterval();
|
||||
} while ($nextrun < time());
|
||||
|
||||
|
||||
$addRunSql = 'UPDATE job SET nextrun = :nextrun WHERE id = :id';
|
||||
$addRunStmt = $this->getEntityManager()->getConnection()->prepare($addRunSql);
|
||||
$addRunStmt->executeQuery([':id' => $job['id'], ':nextrun' => $nextrun]);
|
||||
$addRunStmt->executeQuery([':id' => $job->getId(), ':nextrun' => $nextrun]);
|
||||
}
|
||||
return ['job_id' => $job['id'], 'exitcode' => $result['exitcode'], 'timestamp' =>floor($starttime), 'runtime' => $runtime, 'output' => (string)$result['output'], 'flags' => implode("", $flags)];
|
||||
return ['job_id' => $job->getId(), 'exitcode' => $result['exitcode'], 'timestamp' =>floor($starttime), 'runtime' => $runtime, 'output' => (string)$result['output'], 'flags' => implode("", $flags)];
|
||||
}
|
||||
|
||||
public function unlockJob(int $id = 0): void
|
||||
|
@ -618,50 +628,47 @@ class JobRepository extends EntityRepository
|
|||
}
|
||||
|
||||
public function getJob(int $id, bool $withSecrets = false) {
|
||||
$jobSql = "SELECT * FROM job WHERE id = :id";
|
||||
$jobStmt = $this->getEntityManager()->getConnection()->prepare($jobSql);
|
||||
$jobRslt = $jobStmt->executeQuery([':id' => $id])->fetchAssociative();
|
||||
$job = $this->find($id);
|
||||
|
||||
$jobRslt['data'] = json_decode($jobRslt['data'], true);
|
||||
|
||||
if(!empty($jobRslt['data']['vars'])) {
|
||||
foreach ($jobRslt['data']['vars'] as $key => &$value) {
|
||||
if(!empty($job->getData('vars'))) {
|
||||
foreach ($job->getData('vars') as $key => &$value) {
|
||||
if ($value['issecret']) {
|
||||
$value['value'] = ($withSecrets) ? Secret::decrypt(base64_decode($value['value'])) : '';
|
||||
$job->addData('vars.' . $key . '.value', ($withSecrets) ? Secret::decrypt(base64_decode($value['value'])) : '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch($jobRslt['data']['crontype']) {
|
||||
switch($job->getData('crontype')) {
|
||||
case 'http':
|
||||
if(isset($jobRslt['data']['vars']['basicauth-password']['value'])) {
|
||||
$jobRslt['data']['basicauth-password'] = $jobRslt['data']['vars']['basicauth-password']['value'];
|
||||
unset($jobRslt['data']['vars']['basicauth-password']);
|
||||
if($job->hasData('vars.basicauth-password.value')) {
|
||||
$job->addData('basicauth-password', $job->getData('vars.basicauth-password.value'));
|
||||
$job->removeData('vars.basicauth-password');
|
||||
}
|
||||
break;
|
||||
case 'reboot':
|
||||
$jobRslt['data']['reboot-delay'] = $jobRslt['data']['vars']['reboot-delay']['value'];
|
||||
$jobRslt['data']['reboot-delay-secs'] = $jobRslt['data']['vars']['reboot-delay-secs']['value'];
|
||||
unset($jobRslt['data']['vars']['reboot-delay']);
|
||||
unset($jobRslt['data']['vars']['reboot-delay-secs']);
|
||||
$job->addData('reboot-delay', $job->getData('vars.reboot-delay.value'));
|
||||
$job->addData('reboot-delay-secs', $job->getData('vars.reboot-delay-secs.value'));
|
||||
|
||||
$job->removeData('vars.reboot-delay');
|
||||
$job->removeData('vars.reboot-delay-secs');
|
||||
break;
|
||||
}
|
||||
|
||||
switch($jobRslt['data']['hosttype']) {
|
||||
switch($job->getData('hosttype')) {
|
||||
case 'ssh':
|
||||
if(isset($jobRslt['data']['vars']['ssh-privkey']['value'])) {
|
||||
$jobRslt['data']['ssh-privkey'] = $jobRslt['data']['vars']['ssh-privkey']['value'];
|
||||
unset($jobRslt['data']['vars']['ssh-privkey']);
|
||||
if($job->hasData('vars.ssh-privkey.value')) {
|
||||
$job->addData('ssh-privkey', $job->getData('vars.ssh-privkey.value'));
|
||||
$job->removeData('vars.ssh-privkey');
|
||||
}
|
||||
if(isset($jobRslt['data']['vars']['privkey-password']['value'])) {
|
||||
$jobRslt['data']['privkey-password'] = $jobRslt['data']['vars']['privkey-password']['value'];
|
||||
unset($jobRslt['data']['vars']['privkey-password']);
|
||||
if($job->hasData('vars.privkey-password.value')) {
|
||||
$job->addData('privkey-password', $job->getData('vars.privkey-password.value'));
|
||||
$job->removeData('vars.privkey-password');
|
||||
}
|
||||
break;
|
||||
}
|
||||
if($jobRslt['data']['crontype'] == 'http') {
|
||||
if($job->getData('crontype') == 'http') {
|
||||
}
|
||||
return $jobRslt;
|
||||
return $job;
|
||||
}
|
||||
|
||||
public function deleteJob(int $id)
|
||||
|
|
|
@ -16,20 +16,20 @@ class RunRepository extends EntityRepository
|
|||
|
||||
public function getRunsForJob(int $id, bool $onlyfailed = false, int $maxage = NULL, bool $ordered = true): array
|
||||
{
|
||||
$runsSql = "SELECT * FROM run WHERE job_id = :job";
|
||||
$params = [':job' => $id];
|
||||
$qb = $this->createQueryBuilder('run');
|
||||
$job = $this->getEntityManager()->getRepository(Job::class)->find($id);
|
||||
$runs = $qb
|
||||
->where('run.job = :job')
|
||||
->setParameter(':job', $job);
|
||||
|
||||
if ($onlyfailed) {
|
||||
$runsSql .= ' AND flags LIKE "%' . RunRepository::FAILED . '%"';
|
||||
$runs = $runs->andWhere('run.flags LIKE :flags')->setParameter(':flags', '%' . RunRepository::FAILED . '%');
|
||||
}
|
||||
if($maxage !== NULL) {
|
||||
$runsSql .= ' AND timestamp > :timestamp';
|
||||
$params[':timestamp'] = time() - ($maxage * 24 * 60 * 60);
|
||||
$runs = $runs->andWhere('run.timestamp > :timestamp')->setParameter(':timestamp', time() - ($maxage * 24 * 60 * 60));
|
||||
}
|
||||
if ($ordered) $runsSql .= ' ORDER by timestamp DESC';
|
||||
$runsStmt = $this->getEntityManager()->getConnection()->prepare($runsSql);
|
||||
$runsRslt = $runsStmt->executeQuery($params);
|
||||
$runs = $runsRslt->fetchAllAssociative();
|
||||
return $runs;
|
||||
if ($ordered) $runs->orderBy('run.timestamp', 'DESC');
|
||||
return $runs->getQuery()->getResult();
|
||||
}
|
||||
|
||||
public function addRun(int $jobid, string $exitcode, int $starttime, float $runtime, string $output, array $flags): void
|
||||
|
|
|
@ -2,12 +2,12 @@
|
|||
{% block title %}Add job{% endblock %}
|
||||
{% block content %}
|
||||
<h2>Add a cronjob</h2>
|
||||
<form method="post" class="form-horizontal" enctype="multipart/form-data" action="{{ path('job_edit', { id : id }) }}">
|
||||
<form method="post" class="form-horizontal" enctype="multipart/form-data" action="{{ path('job_edit', { id : job.id }) }}">
|
||||
|
||||
<h3>General info</h3>
|
||||
<div class="mb-3">
|
||||
<label for="name">Name</label>
|
||||
<input type="text" name="name" class="form-control" id="name" placeholder="System update" value="{{ name }}">
|
||||
<input type="text" name="name" class="form-control" id="name" placeholder="System update" value="{{ job.name }}">
|
||||
<small id="name-help" class="form-text text-muted">You can create colored tags by using [tag]</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
|
@ -23,54 +23,54 @@
|
|||
<li><a class="dropdown-item intervalpattern-item" href="#" data-time="604800">Every week</a></li>
|
||||
<li><a class="dropdown-item intervalpattern-item" href="#" data-time="2419200">Every 4 weeks</a></li>
|
||||
</ul>
|
||||
<input type="number" class="form-control" id="interval" name="interval" value="{{ interval }}">
|
||||
<input type="number" class="form-control" id="interval" name="interval" value="{{ job.interval }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="nextrun">Next run</label>
|
||||
<input type="text" autocomplete="off" pattern="[0-9]{2}\/[0-9]{2}\/[0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2}" placeholder="{{ date() | date("d/m/Y H:i:s")}}" id="nextrunselector" class="form-control datetimepicker-input" data-target="#nextrunselector" data-bs-toggle="datetimepicker" name="nextrun" value="{{ nextrun | date("d/m/Y H:i:s")}}">
|
||||
<input type="text" autocomplete="off" pattern="[0-9]{2}\/[0-9]{2}\/[0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2}" placeholder="{{ date() | date("d/m/Y H:i:s")}}" id="nextrunselector" class="form-control datetimepicker-input" data-target="#nextrunselector" data-bs-toggle="datetimepicker" name="nextrun" value="{{ job.nextrun | date("d/m/Y H:i:s")}}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="lastrun">Last run</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-text border-end-0">
|
||||
<input type="checkbox" name="lastrun-eternal" class="lastrun-eternal" placeholder="value" value="true"{% if lastrun is not defined or lastrun is empty %} checked{% endif %}>
|
||||
<input type="checkbox" name="lastrun-eternal" class="lastrun-eternal" placeholder="value" value="true"{% if job.lastrun is not defined or job.lastrun is empty %} checked{% endif %}>
|
||||
</div>
|
||||
<span class="input-group-text border-start-0">Eternal</span>
|
||||
<input type="text" autocomplete="off" pattern="[0-9]{2}\/[0-9]{2}\/[0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2}" data-placeholder="{{ date() | date("d/m/Y H:i:s")}}" id="lastrunselector" class="form-control datetimepicker-input" data-target="#lastrunselector" data-bs-toggle="datetimepicker" name="lastrun"{% if lastrun is not defined or lastrun is empty %} disabled{% else %} value="{{ lastrun | date("d/m/Y H:i:s")}}"{% endif %}>
|
||||
<input type="text" autocomplete="off" pattern="[0-9]{2}\/[0-9]{2}\/[0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2}" data-placeholder="{{ date() | date("d/m/Y H:i:s")}}" id="lastrunselector" class="form-control datetimepicker-input" data-target="#lastrunselector" data-bs-toggle="datetimepicker" name="lastrun"{% if job.lastrun is not defined or job.lastrun is empty %} disabled{% else %} value="{{ job.lastrun | date("d/m/Y H:i:s")}}"{% endif %}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="retention">Retention (in days)</label>
|
||||
<input type="number" name="retention" class="form-control" id="retention" placeholder="7" value="{% if attribute(data, 'retention') is defined %}{{ attribute(data, 'retention') }}{% endif %}">
|
||||
<input type="number" name="retention" class="form-control" id="retention" placeholder="7" value="{% if attribute(job.data, 'retention') is defined %}{{ attribute(job.data, 'retention') }}{% endif %}">
|
||||
<small id="retention-help" class="form-text text-muted">How many days (at least) to keep runs of this job in the database</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="fail-pct">Max fail percentage</label>
|
||||
<div class="input-group d-flex">
|
||||
<div class="range-value range-value-fail-pct pe-1">{% if attribute(data, 'fail-pct') is defined %}{{ attribute(data, 'fail-pct') }}{% else %}50{% endif %}%</div>
|
||||
<div class="range-value range-value-fail-pct pe-1">{% if attribute(job.data, 'fail-pct') is defined %}{{ attribute(job.data, 'fail-pct') }}{% else %}50{% endif %}%</div>
|
||||
<div class="range-input ps-1 flex-grow-1">
|
||||
<input type="range" name="fail-pct" class="form-range range-input-fail-pct" id="fail-pct" max="100" step="5" value="{% if attribute(data, 'fail-pct') is defined %}{{ attribute(data, 'fail-pct') }}{% else %}50{% endif %}">
|
||||
<input type="range" name="fail-pct" class="form-range range-input-fail-pct" id="fail-pct" max="100" step="5" value="{% if attribute(job.data, 'fail-pct') is defined %}{{ attribute(job.data, 'fail-pct') }}{% else %}50{% endif %}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="fail-days">Number of days calculated for fail percentage</label>
|
||||
<input type="number" name="fail-days" class="form-control" id="fail-days" placeholder="7" value="{% if attribute(data, 'fail-days') is defined %}{{ attribute(data, 'fail-days') }}{% endif %}">
|
||||
<input type="number" name="fail-days" class="form-control" id="fail-days" placeholder="7" value="{% if attribute(job.data, 'fail-days') is defined %}{{ attribute(job.data, 'fail-days') }}{% endif %}">
|
||||
</div>
|
||||
|
||||
<h3>Job details</h3>
|
||||
<div class="mb-3 btn-group croncategory-selector">
|
||||
<div class="dropdown croncategory-group crontype-group{% if data.crontype != 'http' %} btn-group{% endif %}">
|
||||
<div class="dropdown croncategory-group crontype-group{% if job.data.crontype != 'http' %} btn-group{% endif %}">
|
||||
<button class="btn btn-outline-primary dropdown-toggle" type="button" id="crontypeButton" data-default-text="Job type" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
{% if data.crontype == 'command' %}
|
||||
{% if job.data.crontype == 'command' %}
|
||||
Command
|
||||
{% elseif data.crontype == 'reboot' %}
|
||||
{% elseif job.data.crontype == 'reboot' %}
|
||||
Reboot
|
||||
{% elseif data.crontype == 'http' %}
|
||||
{% elseif job.data.crontype == 'http' %}
|
||||
Http request
|
||||
{% else %}
|
||||
Job type
|
||||
|
@ -83,11 +83,11 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dropdown croncategory-group hosttype-group{% if data.crontype != 'http' %} btn-group{% else %} d-none{% endif %}">
|
||||
<div class="dropdown croncategory-group hosttype-group{% if job.data.crontype != 'http' %} btn-group{% else %} d-none{% endif %}">
|
||||
<button class="btn btn-outline-primary dropdown-toggle" type="button" id="hosttypeButton" data-default-text="Host type" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
{% if data.hosttype == 'local' %}
|
||||
{% if job.data.hosttype == 'local' %}
|
||||
Local
|
||||
{% elseif data.hosttype == 'ssh' %}
|
||||
{% elseif job.data.hosttype == 'ssh' %}
|
||||
SSH
|
||||
{% else %}
|
||||
Host type
|
||||
|
@ -99,15 +99,15 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{% if data.crontype == 'reboot' %}
|
||||
{% if job.data.crontype == 'reboot' %}
|
||||
</div>
|
||||
<div id="btn-group-discriminator" class="d-none">
|
||||
{% endif %}
|
||||
<div class="dropdown croncategory-group containertype-group{% if data.crontype != 'http' %} btn-group{% else %} d-none{% endif %}">
|
||||
<div class="dropdown croncategory-group containertype-group{% if job.data.crontype != 'http' %} btn-group{% else %} d-none{% endif %}">
|
||||
<button class="btn btn-outline-primary dropdown-toggle" type="button" id="containertypeButton" data-default-text="Container" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
{% if data.containertype == 'none' or data.containertype == '' %}
|
||||
{% if job.data.containertype == 'none' or job.data.containertype == '' %}
|
||||
None
|
||||
{% elseif data.containertype == 'docker' %}
|
||||
{% elseif job.data.containertype == 'docker' %}
|
||||
Docker
|
||||
{% else %}
|
||||
Container
|
||||
|
@ -120,99 +120,99 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="crontype-command crontype-inputs croncategory-inputs{% if data.crontype != 'command' %} d-none{% endif %}">
|
||||
<div class="crontype-command crontype-inputs croncategory-inputs{% if job.data.crontype != 'command' %} d-none{% endif %}">
|
||||
<h4>Command details</h4>
|
||||
<div class="mb-3">
|
||||
<label for="command">Command</label>
|
||||
<input type="text" name="command" class="form-control" id="command" placeholder="sudo apt update" value="{% if data.command is defined %}{{ data.command }}{% endif %}">
|
||||
<input type="text" name="command" class="form-control" id="command" placeholder="sudo apt update" value="{% if job.data.command is defined %}{{ job.data.command }}{% endif %}">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="response">Expected exit code</label>
|
||||
<input type="text" name="response" class="form-control" id="response" placeholder="0" value="{% if data.response is defined %}{{ data.response | join(',') }}{% endif %}">
|
||||
<input type="text" name="response" class="form-control" id="response" placeholder="0" value="{% if job.data.response is defined %}{{ job.data.response | join(',') }}{% endif %}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="crontype-reboot crontype-inputs croncategory-inputs{% if data.crontype != 'reboot' %} d-none{% endif %}">
|
||||
<div class="crontype-reboot crontype-inputs croncategory-inputs{% if job.data.crontype != 'reboot' %} d-none{% endif %}">
|
||||
<h4>Reboot job details</h4>
|
||||
<div class="mb-3">
|
||||
<label for="reboot-command">Reboot command</label>
|
||||
<input type="text" name="reboot-command" class="form-control" id="reboot-command" placeholder="systemctl reboot" value="
|
||||
{% if attribute(data, 'reboot-command') is defined %}
|
||||
{{ attribute(data, 'reboot-command') }}
|
||||
{% if attribute(job.data, 'reboot-command') is defined %}
|
||||
{{ attribute(job.data, 'reboot-command') }}
|
||||
{% endif %}">
|
||||
<small id="reboot-command-help" class="form-text text-muted">Use {reboot-delay} or {reboot-delay-secs} to add the delay in your command</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="getservices-command">Get services command</label>
|
||||
<input type="text" name="getservices-command" class="form-control" id="getservices)command" placeholder="systemctl list-units" value="{% if attribute(data, 'getservices-command') is defined %}{{ attribute(data, 'getservices-command') }}{% endif %}">
|
||||
<input type="text" name="getservices-command" class="form-control" id="getservices)command" placeholder="systemctl list-units" value="{% if attribute(job.data, 'getservices-command') is defined %}{{ attribute(job.data, 'getservices-command') }}{% endif %}">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="getservices-response">Get services command exit code</label>
|
||||
<input type="text" name="getservices-response" class="form-control" id="getservices-response" placeholder="0" value="{% if attribute(data, 'getservices-response') is defined %}{{ attribute(data, 'getservices-response') | join(',') }}{% endif %}">
|
||||
<input type="text" name="getservices-response" class="form-control" id="getservices-response" placeholder="0" value="{% if attribute(job.data, 'getservices-response') is defined %}{{ attribute(job.data, 'getservices-response') | join(',') }}{% endif %}">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="reboot-delay">Reboot delay (in minutes)</label>
|
||||
<input type="number" name="reboot-delay" class="form-control" placeholder="5" value="{% if attribute(data, 'reboot-delay') is defined %}{{ attribute(data, 'reboot-delay') }}{% endif %}">
|
||||
<input type="number" name="reboot-delay" class="form-control" placeholder="5" value="{% if attribute(job.data, 'reboot-delay') is defined %}{{ attribute(job.data, 'reboot-delay') }}{% endif %}">
|
||||
<small id="reboot-delay-help" class="form-text text-muted">Delay between triggering reboot and actual reboot</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="reboot-duration">Reboot duration (in minutes)</label>
|
||||
<input type="number" name="reboot-duration" class="form-control" placeholder="10" value="{% if attribute(data, 'reboot-duration') is defined %}{{ attribute(data, 'reboot-duration') }}{% endif %}">
|
||||
<input type="number" name="reboot-duration" class="form-control" placeholder="10" value="{% if attribute(job.data, 'reboot-duration') is defined %}{{ attribute(job.data, 'reboot-duration') }}{% endif %}">
|
||||
<small id="reboot-duration-help" class="form-text text-muted">The amount of time the system takes to actually reboot</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="crontype-http crontype-inputs croncategory-inputs{% if data.crontype != 'http' %} d-none{% endif %}">
|
||||
<div class="crontype-http crontype-inputs croncategory-inputs{% if job.data.crontype != 'http' %} d-none{% endif %}">
|
||||
<h4>HTTP request details</h4>
|
||||
<div class="mb-3">
|
||||
<label for="url">Url</label>
|
||||
<input type="text" name="url" class="form-control" id="url" placeholder="https://scripts.example.com/" value="{% if data.url is defined %}{{ data.url }}{% endif %}">
|
||||
<input type="text" name="url" class="form-control" id="url" placeholder="https://scripts.example.com/" value="{% if job.data.url is defined %}{{ job.data.url }}{% endif %}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="basicauth-username">Username for Basic-Auth</label>
|
||||
<input type="text" name="basicauth-username" class="form-control" id="basicauth-username" placeholder="www-data" value="{% if attribute(data, 'basicauth-username') is defined %}{{ attribute(data, 'basicauth-username') }}{% endif %}">
|
||||
<input type="text" name="basicauth-username" class="form-control" id="basicauth-username" placeholder="www-data" value="{% if attribute(job.data, 'basicauth-username') is defined %}{{ attribute(job.data, 'basicauth-username') }}{% endif %}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="basicauth-password">Password for Basic-Auth</label>
|
||||
<input type="password" name="basicauth-password" class="form-control" placeholder="correct horse battery staple" value="{% if attribute(data, 'basicauth-password') is defined %}{{ attribute(data, 'basicauth-password') }}{% endif %}">
|
||||
<input type="password" name="basicauth-password" class="form-control" placeholder="correct horse battery staple" value="{% if attribute(job.data, 'basicauth-password') is defined %}{{ attribute(job.data, 'basicauth-password') }}{% endif %}">
|
||||
<small id="basicauth-password-help" class="form-text text-muted">This field is being saved as a secret</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="http-status">Expected http status code</label>
|
||||
<input type="text" name="http-status" class="form-control" id="http-status" placeholder="200" value="{% if attribute(data, 'http-status') is defined %}{{ attribute(data, 'http-status') | join(',')}}{% endif %}">
|
||||
<input type="text" name="http-status" class="form-control" id="http-status" placeholder="200" value="{% if attribute(job.data, 'http-status') is defined %}{{ attribute(job.data, 'http-status') | join(',')}}{% endif %}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hosttype-local hosttype-inputs croncategory-inputs{% if data.hosttype != 'local' %} d-none{% endif %}">
|
||||
<div class="hosttype-local hosttype-inputs croncategory-inputs{% if job.data.hosttype != 'local' %} d-none{% endif %}">
|
||||
<h4>Localhost details</h4>
|
||||
<h5>No options</h5>
|
||||
</div>
|
||||
|
||||
<div class="hosttype-ssh hosttype-inputs croncategory-inputs{% if data.hosttype != 'ssh' %} d-none{% endif %}">
|
||||
<div class="hosttype-ssh hosttype-inputs croncategory-inputs{% if job.data.hosttype != 'ssh' %} d-none{% endif %}">
|
||||
<h4>SSH host details</h4>
|
||||
<div class="mb-3">
|
||||
<label for="host">Hostname</label>
|
||||
<input type="text" name="host" class="form-control" id="host" placeholder="ssh.abc.xyz" value="{% if data.host is defined %}{{ data.host }}{% endif %}">
|
||||
<input type="text" name="host" class="form-control" id="host" placeholder="ssh.abc.xyz" value="{% if job.data.host is defined %}{{ job.data.host }}{% endif %}">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="user">Username</label>
|
||||
<input type="text" name="user" class="form-control" id="user" placeholder="larry" value="{% if data.user is defined %}{{ data.user }}{% endif %}">
|
||||
<input type="text" name="user" class="form-control" id="user" placeholder="larry" value="{% if job.data.user is defined %}{{ job.data.user }}{% endif %}">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="privkey">Private key</label>
|
||||
<div class="input-group">
|
||||
<span class=" input-group-text border-end-0">
|
||||
<input type="checkbox" name="privkey-keep" class="privkey-keep" value="true" data-privkey="{% if attribute(data, 'ssh-privkey') is defined %}{{ attribute(data, 'ssh-privkey') }}{% endif %}" checked>
|
||||
<input type="checkbox" name="privkey-keep" class="privkey-keep" value="true" data-privkey="{% if attribute(job.data, 'ssh-privkey') is defined %}{{ attribute(job.data, 'ssh-privkey') }}{% endif %}" checked>
|
||||
</span>
|
||||
<input type="hidden" name="privkey-orig" class="privkey-orig" value="{% if attribute(data, 'ssh-privkey') is defined %}{{ attribute(data, 'ssh-privkey') }}{% endif %}">
|
||||
<input type="hidden" name="privkey-orig" class="privkey-orig" value="{% if attribute(job.data, 'ssh-privkey') is defined %}{{ attribute(job.data, 'ssh-privkey') }}{% endif %}">
|
||||
<span class="input-group-text border-start-0">Keep</span>
|
||||
<input type="file" id="privkey" name="privkey" class="form-control " disabled>
|
||||
</div>
|
||||
|
@ -221,25 +221,25 @@
|
|||
|
||||
<div class="mb-3">
|
||||
<label for="privkey-password">Password for private key</label>
|
||||
<input type="password" name="privkey-password" class="form-control" placeholder="correct horse battery staple" value="{% if attribute(data, 'privkey-password') is defined %}{{ attribute(data, 'privkey-password') }}{% endif %}">
|
||||
<input type="password" name="privkey-password" class="form-control" placeholder="correct horse battery staple" value="{% if attribute(job.data, 'privkey-password') is defined %}{{ attribute(job.data, 'privkey-password') }}{% endif %}">
|
||||
<small id="privkey-password-help" class="form-text text-muted">If private key is empty this field is being used as ssh-password</small>
|
||||
<small id="privkey-password-help-2" class="form-text text-muted">This field is being saved as a secret</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="containertype-none containertype-inputs croncategory-inputs{% if data.containertype != 'none' %} d-none{% endif %}">
|
||||
<div class="containertype-none containertype-inputs croncategory-inputs{% if job.data.containertype != 'none' %} d-none{% endif %}">
|
||||
</div>
|
||||
|
||||
<div class="containertype-docker containertype-inputs croncategory-inputs{% if data.containertype != 'docker' %} d-none{% endif %}">
|
||||
<div class="containertype-docker containertype-inputs croncategory-inputs{% if job.data.containertype != 'docker' %} d-none{% endif %}">
|
||||
<h4>Docker container details</h4>
|
||||
<div class="mb-3">
|
||||
<label for="service">Service</label>
|
||||
<input type="text" name="service" class="form-control" id="service" placeholder="mysql" value="{% if attribute(data, 'service') is defined %}{{ attribute(data, 'service') }}{% endif %}">
|
||||
<input type="text" name="service" class="form-control" id="service" placeholder="mysql" value="{% if attribute(job.data, 'service') is defined %}{{ attribute(job.data, 'service') }}{% endif %}">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="container-user">Username</label>
|
||||
<input type="text" name="container-user" class="form-control" id="container-user" placeholder="larry" value="{% if attribute(data, 'container-user') is defined %}{{ attribute(data, 'container-user') }}{% endif %}">
|
||||
<input type="text" name="container-user" class="form-control" id="container-user" placeholder="larry" value="{% if attribute(job.data, 'container-user') is defined %}{{ attribute(job.data, 'container-user') }}{% endif %}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -255,8 +255,9 @@
|
|||
<input type="text" name="var-value[0]" class="form-control var-value" placeholder="value">
|
||||
</div>
|
||||
{% set key = 1 %}
|
||||
{% if data.vars is defined %}
|
||||
{% for id,var in data.vars %}
|
||||
{% if job.data.vars is defined %}
|
||||
Im defined!
|
||||
{% for id,var in job.data.vars %}
|
||||
<div class="input-group var-group">
|
||||
<div class="input-group-text border-end-0">
|
||||
<input type="checkbox" name="var-issecret[{{ key }}]" class="var-issecret" placeholder="value" value="true"{% if var.issecret %} checked{% endif %}>
|
||||
|
@ -278,9 +279,9 @@
|
|||
<a href="#" class="btn btn-outline-primary addvar-btn">Add variable</a>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="crontype" class="crontype" value="{{ data.crontype }}">
|
||||
<input type="hidden" name="hosttype" class="hosttype" value="{{ data.hosttype }}">
|
||||
<input type="hidden" name="containertype" class="containertype" value="{{ data.containertype }}">
|
||||
<input type="hidden" name="crontype" class="crontype" value="{{ job.data.crontype }}">
|
||||
<input type="hidden" name="hosttype" class="hosttype" value="{{ job.data.hosttype }}">
|
||||
<input type="hidden" name="containertype" class="containertype" value="{{ job.data.containertype }}">
|
||||
<button type="submit" class="btn btn-outline-primary">Submit</button>
|
||||
</form>
|
||||
|
||||
|
|
Loading…
Reference in New Issue