added input-file option for automatic cli-installation

Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
This commit is contained in:
Michael Kaufmann
2022-07-08 16:52:22 +02:00
parent 7c812df4e0
commit 430aefe0f7
5 changed files with 165 additions and 40 deletions

View File

@@ -27,10 +27,12 @@ namespace Froxlor\Cli;
use Exception;
use Froxlor\Froxlor;
use Froxlor\Config\ConfigParser;
use Froxlor\Install\Install;
use Froxlor\Install\Install\Core;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
@@ -46,27 +48,18 @@ final class InstallCommand extends Command
{
$this->setName('froxlor:install');
$this->setDescription('Installation process to use instead of web-ui');
$this->addArgument('input-file', InputArgument::OPTIONAL, 'Optional JSON array file to use for unattended installations');
$this->addOption('print-example-file', 'p', InputOption::VALUE_NONE, 'Outputs an example JSON content to be used with the input file parameter');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$result = self::SUCCESS;
if (file_exists(Froxlor::getInstallDir() . '/lib/userdata.inc.php')) {
$output->writeln("<error>froxlor seems to be installed already.</>");
return self::INVALID;
}
session_start();
require __DIR__ . '/install.functions.php';
$this->io = new SymfonyStyle($input, $output);
$this->io->title('Froxlor installation');
$extended = $this->io->confirm('Use advanced installation mode?', false);
// set a few defaults CLI cannot know
$_SERVER['SERVER_SOFTWARE'] = 'apache';
$host = [];
@@ -82,12 +75,59 @@ final class InstallCommand extends Command
$_SERVER['SERVER_ADDR'] = filter_var($ips[0] ?? "", FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ? ($ips[0] ?? '') : '';
}
$result = $this->showStep(0, $extended);
if ($input->getOption('print-example-file') !== false) {
$this->printExampleFile($output);
return self::SUCCESS;
}
if (file_exists(Froxlor::getInstallDir() . '/lib/userdata.inc.php')) {
$output->writeln("<error>froxlor seems to be installed already.</>");
return self::INVALID;
}
$this->io = new SymfonyStyle($input, $output);
$this->io->title('Froxlor installation');
if ($input->getArgument('input-file')) {
$inputFile = $input->getArgument('input-file');
if (strtoupper(substr($inputFile, 0, 4)) == 'HTTP') {
$output->writeln("Input file seems to be an URL, trying to download");
$target = "/tmp/froxlor-install-" . time() . ".json";
if (@file_exists($target)) {
@unlink($target);
}
$this->downloadFile($inputFile, $target);
$inputFile = $target;
}
if (!is_file($inputFile)) {
$output->writeln('<error>Given input file is not a file</>');
return self::INVALID;
} elseif (!file_exists($inputFile)) {
$output->writeln('<error>Given input file cannot be found (' . $inputFile . ')</>');
return self::INVALID;
} elseif (!is_readable($inputFile)) {
$output->writeln('<error>Given input file cannot be read (' . $inputFile . ')</>');
return self::INVALID;
}
$inputcontent = file_get_contents($inputFile);
$decoded_input = json_decode($inputcontent, true) ?? [];
$extended = true;
if (empty($decoded_input)) {
$output->writeln('<error>Given input file seems to be invalid JSON</>');
return self::INVALID;
}
$this->io->info('Running unattended installation');
} else {
$extended = $this->io->confirm('Use advanced installation mode?', false);
}
$result = $this->showStep(0, $extended, $decoded_input);
return $result;
}
private function showStep(int $step = 0, bool $extended = false): int
private function showStep(int $step = 0, bool $extended = false, array $decoded_input = []): int
{
$result = self::SUCCESS;
$inst = new Install(['step' => $step, 'extended' => $extended]);
@@ -123,8 +163,8 @@ final class InstallCommand extends Command
}
}
}
if ($result == self::SUCCESS && $this->io->confirm('Continue?', true)) {
return $this->showStep(++$step, $extended);
if ($result == self::SUCCESS) {
return $this->showStep(++$step, $extended, $decoded_input);
}
break;
case 1:
@@ -132,7 +172,9 @@ final class InstallCommand extends Command
case 3:
$section = $inst->formfield['install']['sections']['step' . $step] ?? [];
$this->io->section($section['title']);
$this->io->note($section['description']);
if (empty($decoded_input)) {
$this->io->note($section['description']);
}
foreach ($section['fields'] as $fieldname => $fielddata) {
if ($extended == false && isset($fielddata['advanced']) && $fielddata['advanced'] == true) {
if ($fieldname == 'httpuser' || $fieldname == 'httpgroup') {
@@ -143,26 +185,40 @@ final class InstallCommand extends Command
}
continue;
}
$ask_field = true;
// preset from input-file
if (!empty($decoded_input) && isset($decoded_input[$fieldname])) {
$this->formfielddata[$fieldname] = $decoded_input[$fieldname];
$ask_field = false;
}
$fielddata['value'] = $this->formfielddata[$fieldname] ?? ($fielddata['value'] ?? null);
$fielddata['label'] = strip_tags(str_replace("<br>", " ", $fielddata['label']));
if ($fielddata['type'] == 'password') {
$this->formfielddata[$fieldname] = $this->io->askHidden($fielddata['label'], function ($value) use ($fielddata) {
if (isset($fielddata['mandatory']) && $fielddata['mandatory'] && empty($value)) {
throw new \RuntimeException('You must enter a value.');
}
return $value;
});
} elseif ($fielddata['type'] == 'checkbox') {
$this->formfielddata[$fieldname] = $this->io->confirm($fielddata['label'], $fielddata['value'] ?? false);
} elseif ($fielddata['type'] == 'select') {
$this->formfielddata[$fieldname] = $this->io->choice($fielddata['label'], $fielddata['select_var'], $fielddata['selected'] ?? '');
if ($ask_field) {
if ($fielddata['type'] == 'password') {
$this->formfielddata[$fieldname] = $this->io->askHidden($fielddata['label'], function ($value) use ($fielddata) {
if (isset($fielddata['mandatory']) && $fielddata['mandatory'] && empty($value)) {
throw new \RuntimeException('You must enter a value.');
}
return $value;
});
} elseif ($fielddata['type'] == 'checkbox') {
$this->formfielddata[$fieldname] = $this->io->confirm($fielddata['label'], $fielddata['value'] ?? false);
} elseif ($fielddata['type'] == 'select') {
$this->formfielddata[$fieldname] = $this->io->choice($fielddata['label'], $fielddata['select_var'], $fielddata['selected'] ?? '');
} else {
$this->formfielddata[$fieldname] = $this->io->ask($fielddata['label'], $fielddata['value'] ?? '', function ($value) use ($fielddata) {
if (isset($fielddata['mandatory']) && $fielddata['mandatory'] && empty($value)) {
throw new \RuntimeException('You must enter a value.');
}
return $value;
});
}
} else {
$this->formfielddata[$fieldname] = $this->io->ask($fielddata['label'], $fielddata['value'] ?? '', function ($value) use ($fielddata) {
if (isset($fielddata['mandatory']) && $fielddata['mandatory'] && empty($value)) {
throw new \RuntimeException('You must enter a value.');
}
return $value;
});
$this->io->text("Setting field '" . $fieldname . "' to value '" . ($fielddata['type'] == 'password' ? '*hidden*' : $fielddata['value']) . "'");
if (isset($fielddata['mandatory']) && $fielddata['mandatory'] && empty($fielddata['value'])) {
$this->io->error("Mandatory field '" . $fieldname . "' not specified/empty value in input file");
return self::FAILURE;
}
}
}
@@ -176,7 +232,7 @@ final class InstallCommand extends Command
}
} catch (Exception $e) {
$this->io->error($e->getMessage());
return $this->showStep($step, $extended);
return $this->showStep($step, $extended, $decoded_input);
}
if ($step == 3) {
// do actual install with data from $this->formfielddata
@@ -184,7 +240,7 @@ final class InstallCommand extends Command
$core->doInstall();
$core->createUserdataConf();
}
return $this->showStep(++$step, $extended);
return $this->showStep(++$step, $extended, $decoded_input);
break;
case 4:
$section = $inst->formfield['install']['sections']['step' . $step] ?? [];
@@ -195,11 +251,77 @@ final class InstallCommand extends Command
$cmdfield['label'],
$cmdfield['value']
]);
if ($this->io->confirm('Execute command now?', false)) {
if (!empty($decoded_input) || $this->io->confirm('Execute command now?', false)) {
passthru($cmdfield['value']);
}
break;
}
return $result;
}
private function printExampleFile(OutputInterface $output)
{
// show list of available distro's
$distros = glob(dirname(__DIR__, 3) . '/lib/configfiles/*.xml');
// read in all the distros
foreach ($distros as $distribution) {
// get configparser object
$dist = new ConfigParser($distribution);
// store in tmp array
$supportedOS[str_replace(".xml", "", strtolower(basename($distribution)))] = $dist->getCompleteDistroName();
}
// sort by distribution name
asort($supportedOS);
$webserverBackend = [
'php-fpm' => 'PHP-FPM',
'fcgid' => 'FCGID',
'mod_php' => 'mod_php (not recommended)',
];
$guessedDistribution = "";
$guessedWebserver = "";
$fields = include dirname(dirname(__DIR__)) . '/formfields/install/formfield.install.php';
$json_output = [];
foreach ($fields['install']['sections'] as $section => $section_fields) {
foreach ($section_fields['fields'] as $name => $field) {
if ($name == 'system' || $name == 'manual_config') {
continue;
}
if ($field['type'] == 'text' || $field['type'] == 'email') {
if ($name == 'httpuser' || $name == 'httpgroup') {
$fieldval = 'www-data';
} else {
$fieldval = $field['value'] ?? "";
}
} elseif ($field['type'] == 'password') {
$fieldval = '******';
} elseif ($field['type'] == 'select') {
$fieldval = implode("|", array_keys($field['select_var']));
} else if ($field['type'] == 'checkbox') {
$fieldval = "1|0";
} else {
$fieldval = "?";
}
$json_output[$name] = $fieldval;
}
}
$output->writeln(json_encode($json_output, JSON_PRETTY_PRINT));
}
private function downloadFile($src, $dest)
{
set_time_limit(0);
// This is the file where we save the information
$fp = fopen($dest, 'w+');
// Here is the file we are downloading, replace spaces with %20
$ch = curl_init(str_replace(" ", "%20", $src));
curl_setopt($ch, CURLOPT_TIMEOUT, 50);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
// write curl response to file
curl_setopt($ch, CURLOPT_FILE, $fp);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
// get curl response
curl_exec($ch);
curl_close($ch);
fclose($fp);
}
}

View File

@@ -70,7 +70,8 @@ class AutoUpdate
}
$latestversion = HttpClient::urlGet(self::UPDATE_URI . Froxlor::VERSION . $channel, true, 3);
} catch (Exception $e) {
throw new Exception("Version-check currently unavailable, please try again later", 504);
self::$lasterror = "Version-check currently unavailable, please try again later";
return -1;
}
self::$latestversion = json_decode($latestversion, true);

View File

@@ -71,6 +71,8 @@ class Install
asort($this->supportedOS);
// guess distribution and webserver to preselect in formfield
$webserverBackend = $this->webserverBackend;
$supportedOS = $this->supportedOS;
$guessedDistribution = $this->guessDistribution();
$guessedWebserver = $this->guessWebserver();

View File

@@ -258,6 +258,6 @@ class Crypt
$x509 = openssl_csr_sign($csr, null, $privkey, 365, array('digest_alg' => 'sha384'));
// export to files
openssl_x509_export_to_file($x509, Settings::Get('system.ssl_cert_file'));
openssl_pkey_export_to_file($private_key, Settings::Get('system.ssl_key_file'));
openssl_pkey_export_to_file($privkey, Settings::Get('system.ssl_key_file'));
}
}

View File

@@ -146,7 +146,7 @@ return [
'label' => lng('admin.configfiles.distribution'),
'type' => 'select',
'mandatory' => true,
'select_var' => $this->supportedOS,
'select_var' => $supportedOS,
'selected' => $guessedDistribution
],
'serveripv4' => [
@@ -186,7 +186,7 @@ return [
'label' => lng('install.system.phpbackend'),
'type' => 'select',
'mandatory' => true,
'select_var' => $this->webserverBackend,
'select_var' => $webserverBackend,
'selected' => old('webserver_backend', 'php-fpm', 'installation'),
],
'httpuser' => [