Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Tools] Ubuntu production environment "bootstrap" script #3981

Closed
wants to merge 10 commits into from
103 changes: 103 additions & 0 deletions tools/PHP_CLI_Helper.class.inc
@@ -0,0 +1,103 @@
<?php

/**
* Use bash to determine if certain unix tools are installed. Which is used for
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't using bash, it's using the shell

* generic system tools (e.g. wget), and dpkg if which fails.
*
* @param string $tool Name of tool
*
* @return bool representing if tool is installed
*/
function installed($tool) : bool
{
// `which` returns empty if a tool is not installed.
// shell_exec captures this output.
if (shell_exec("which $tool")) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would be more reliable if it used exec and checked the shell return code, rather than trying to parse stdout.

return true;
}
// check installed pacakages with dpkg. Returns 0 if installed.
johnsaigle marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure about that? dpkg(1) says:

EXIT STATUS         top

       0      The requested action was successfully performed.  Or a check
              or assertion command returned true.

       1      A check or assertion command returned false.

       2      Fatal or unrecoverable error due to invalid command-line
              usage, or interactions with the system, such as accesses to
              the database, memory allocations, etc.

(but doesn't really clarify what "a check or assertion command" is)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm well I mean the -s flag gives the status of a package and returns 1 when not found so I think it works in this case.

exec("dpkg -s $tool", $output, $status);
if ($status === 0) {
return true;
}
return false;
}

/**
* Prompts a user with a question and acceptable responses.
*
* @param string $question Prompt to display to user
* @param array $answers List of acceptable answers to prompt
*
* @return void
*/
function writeQuestion($question, $answers) : void
{
echo $question . ' (' . implode('/', $answers) . '): ' . PHP_EOL;
}

/**
* Gets user input from STDIN and checks if it matches an option in
* $possibleAnswers. If not, the default answer is used. Intended to follow
* function writeQuestion
*
* @param array $possibleAnswers Possible answers to a prompt
* @param array $defaultAnswer Response if user entered invalid response
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In most cases if a user responds with an invalid response, shouldn't the scripts be re-prompting for a valid answer?

*
* @return string The user input representing their answer or the default answer
*/
function readAnswer($possibleAnswers, $defaultAnswer) : string
{
$in = fopen('php://stdin', 'rw+');
johnsaigle marked this conversation as resolved.
Show resolved Hide resolved
$answer = trim(fgets($in));

if (!in_array($answer, $possibleAnswers, true)) {
return $defaultAnswer;
}

return $answer;
}

/**
* Prints and executes a bash command using exec. Prints an error message on
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still not necessarily bash.

* failure (a 0 exit code in bash is a success. Anything else is considered an
* error here.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* error here.
* error here.)

*
* @param string $cmd An executable shell command
*
* @return bool True if command exits normally. False otherwise.
*/
function doExec($cmd) : bool
{
echo "[+] Executing command `$cmd`... " . PHP_EOL;
exec($cmd, $output, $status);
if ($status !== 0) {
echo bashErrorToString($cmd, $output, $status);
return false;
}
echo '[+] OK.' . PHP_EOL;
return true;
}

/**
* A to-string method for exec. Captures bash exit code and error message for
* debugging purposes. Also prints the command that was run. Modelled on PHP
* `exec` function.
*
* @param string $cmd A bash command that has been executed
* @param string $output Output of above command.
* @param string $status Exit status of above command
*
* @return string The error message describing what happened in bash
*/
function bashErrorToString($cmd, $output, $status) : string
{
echo PHP_EOL;
$error = "[-] ERROR: Command `$cmd` failed (error code $status)" . PHP_EOL;
if (is_iterable($output)) {
foreach ($output as $item) {
$error .= $item . PHP_EOL;
}
}
return $error;
}
214 changes: 214 additions & 0 deletions tools/bootstrap.php
@@ -0,0 +1,214 @@
#!/usr/bin/env php
# This script verifies a development installation of LORIS by ensuring that
# the system has all of the required dependencies such as the correct PHP and
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if PHP isn't installed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add a check for it

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean the script is written in PHP, so how can it check that?

# Apache versions as well as other miscellaneous extensions and system tools.
#
# Production environments should not run this tool as their dependency
# management should be performed by a proper package such as a .deb file.
#
# Currently only Ubuntu environments are supported by this script.
<?php
error_reporting(E_ALL);

// Go to LORIS root.
chdir(dirname(__FILE__) . '/..');
require('tools/PHP_CLI_Helper.class.inc');

//TODO: Update these values as time passes
$required_major_php = 7;
$required_minor_php = 2;
$required_major_apache = 2;
$required_minor_apache = 4;
// PHP version required for LORIS.
$required_php = "$required_major_php.$required_minor_php";

/* Validate apache */
// Get string representation of apache version number
$apache_parts = explode(
'/',
// this command yields e.g. Apache/2.4.34
shell_exec(
"apache2 -v | " .
"head -n 1 | " .
"cut -d ' ' -f 3"
)
);
$apache_version = end($apache_parts);
/* Look for the string "$major.$minor" in info string. Also match on versions
* higher than minor version because we want AT LEAST that version.
*/
$pattern = "/$required_major_apache\.[$required_minor_apache-9].[0-9]/";
// When preg_match returns 0 it means no match was found.
if (preg_match($pattern, $apache_version) === 0) {
$required_apache = "$required_major_apache.$required_minor_apache";
die(
"ERROR: LORIS requires Apache v$required_apache or higher."
. PHP_EOL
. "This must be done manually as it has possible security ramifications."
. PHP_EOL
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second sentence is unnecessary.

);
}

// NOTE Update as time passes.
// Dependencies last updated for version: 20.0.1
// This list should consist only of packages that can be installed via apt on
// Ubuntu environments and must not include libraries that should be installed
// via tools such as npm and composer.
$apt_dependencies = array(
"wget",
"zip",
"unzip",
"php-json",
"npm",
"software-properties-common",
"php-ast",
"php$required_php",
"php$required_php-mysql",
"php$required_php-xml",
"php$required_php-json",
"php$required_php-mbstring",
"php$required_php-gd",
"libapache2-mod-php$required_php",
);

if (
!(installMissingRequirements($apt_dependencies))
&& (installAptPackages($apt_dependencies))
) {
die(
'Could not upgrade all required packages. Exiting.'
. PHP_EOL
);
}


// Run package managers and add dev flag if supplied to the script
if (runPackageManagers(isset($argv[1]) && $argv[1] === 'dev')) {
echo '[**] Dependencies up-to-date.' . PHP_EOL;
}

/**
* Prints a list of missing apt packages and prompts user to install them.
*
* @param array $requirements Required packages determined missing earlier.
*
* @return bool True if all packages install properly. False otherwise.
*/
function installMissingRequirements(array $requirements): bool
{
$to_install = getMissingRequirements($requirements);
if (empty($to_install)) {
return true;
}
echo '[-] Required package(s) not installed:' . PHP_EOL;
foreach ($to_install as $tool) {
echo "\t* {$tool}" . PHP_EOL;
}
$answers = [
'Y',
'n',
];
$defaultAnswer = 'Y';
writeQuestion('Install now?', $answers);
$answer = readAnswer($answers, $defaultAnswer);
if ($answer != 'Y') {
echo '[-] Not installing requirements...' . PHP_EOL;
return false;
}
echo '[*] Installing requirements...' . PHP_EOL;
return installAptPackages($to_install);
}

/**
* Takes an array of packages to install using apt-get. Uses exec to install
* or upgrade apt packages based on $upgrade_mode.
*
* @param array $packages List of apt packages to install.
* @param bool $upgrade_mode If true, only upgrades packages. False to install.
*
* @return bool true if all packages installed properly. False otherwise.
*/
function installAptPackages(array $packages, bool $upgrade_mode = false): bool
{
foreach ($packages as $package) {
if (installAptPackage($package, $upgrade_mode) !== true) {
return false;
}
}
return true;
}

/**
* Installs or upgrades an individual apt package.
*
* @param string $name Name of package to install
* @param bool $only_upgrade Whether to upgrade or install a package
*
* @return bool True if package installed/upgraded successfull. Otherwise false
*/
function installAptPackage(string $name, bool $only_upgrade = false): bool
{
if ($only_upgrade) {
$cmd = "sudo apt-get install --only-upgrade ";
} else {
$cmd = "sudo apt-get install ";
}
$cmd .= escapeshellarg($name);
return doExec($cmd);
}

/**
* Runs 3rd-party package managers using exec
*
* @param bool $dev Whether the script should be run in Dev mode.
*
* @return bool True if all commands execute correctly. False otherwise
*/
function runPackageManagers(bool $dev = false): bool
{
if (posix_geteuid() === 0) {
echo "[-] ERROR: Refusing to run package managers as root. Please "
. "try again with a lower-privileged user or without sudo."
. PHP_EOL;
return false;
}

// Run dependency/package managers
$cmd = 'composer install';
if ($dev) {
$cmd .= ' --no-dev';
}
if (doExec($cmd) === false) {
return false;
}

$cmd = 'npm install';
if (doExec($cmd) === false) {
return false;
}

$cmd = 'git describe --tags --always > VERSION';
if (doExec($cmd) === false) {
return false;
}
return true;
}

/**
* Check if all tools in $required are installed
*
* @param array $required Packages required by LORIS
*
* @return array of names of missing requirements
*/
function getMissingRequirements(array $required): array
{
$missing = [];
foreach ($required as $tool) {
if (!installed($tool)) {
$missing[] = $tool;
}
}

return $missing;
}