diff --git a/recipe/provision-fedora.php b/recipe/provision-fedora.php new file mode 100644 index 000000000..506c38239 --- /dev/null +++ b/recipe/provision-fedora.php @@ -0,0 +1,216 @@ + $name, 'VERSION_ID' => $version] = parse_ini_string($release); + if ($name !== 'Fedora Linux') { + warning('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!'); + warning('!! !!'); + warning('!! Only Fedora Linux is supported! !!'); + warning('!! !!'); + warning('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!'); + if (!askConfirmation(' Do you want to continue? (Not recommended)', false)) { + throw new \RuntimeException('Provision aborted due to incompatible OS.'); + } + } + // Also only version 43 and newer are supported. + if (version_compare($version, '43', '<')) { + warning("Fedora $version is not supported. Use Fedora 43 or newer."); + if (!askConfirmation(' Do you want to continue? (Not recommended)', false)) { + throw new \RuntimeException('Provision aborted due to incompatible OS.'); + } + } +})->oncePerNode(); + +desc('Collects required params'); +task('provision:configure', function () { + set('remote_user', get('provision_user')); + + $params = [ + 'sudo_password', + 'domain', + 'public_path', + 'db_type', + ]; + $dbparams = [ + 'db_user', + 'db_name', + 'db_password', + ]; + + $showCode = false; + + foreach ($params as $name) { + if (!Context::get()->getConfig()->hasOwn($name)) { + $showCode = true; + } + get($name); + } + + if (get('db_type') !== 'none') { + foreach ($dbparams as $name) { + if (!Context::get()->getConfig()->hasOwn($name)) { + $showCode = true; + } + get($name); + } + } + + if ($showCode) { + $code = "\n\n====== Configuration Start ======"; + $code .= "\nhost('{{alias}}')"; + $codeParams = $params; + if (get('db_type') !== 'none') { + $codeParams = array_merge($codeParams, $dbparams); + } + foreach ($codeParams as $name) { + $code .= "\n ->set('$name', '" . get($name) . "')"; + } + $code .= ";\n"; + $code .= "====== Configuration End ======\n\n"; + writeln($code); + } +}); + + +desc('Adds repositories and update'); +task('provision:update', function () { + set('remote_user', get('provision_user')); + + // Update before installing anything + run('dnf -y makecache'); + + // Pre-requisites + run('dnf install -y curl gnupg2 dnf-plugins-core'); + + // Caddy + run('dnf copr enable -y @caddy/caddy'); + + // Update + run('dnf -y makecache'); +}) + ->oncePerNode() + ->verbose(); + +desc('Upgrades all packages'); +task('provision:upgrade', function () { + set('remote_user', get('provision_user')); + run('dnf upgrade -y', timeout: 900); +}) + ->oncePerNode() + ->verbose(); + +desc('Installs packages'); +task('provision:install', function () { + set('remote_user', get('provision_user')); + $packages = [ + 'acl', + 'caddy', + 'curl', + 'fail2ban', + 'firewalld', + 'gcc', + 'gcc-c++', + 'git', + 'make', + 'memcached', + 'ncdu', + 'nodejs', + 'openssh-server', + 'pcre-devel', + 'pkgconf-pkg-config', + 'policycoreutils-python-utils', + 'python3', + 'sendmail', + 'sqlite', + 'sqlite-devel', + 'unzip', + 'util-linux', + 'valkey', + 'whois', + ]; + run('dnf install -y ' . implode(' ', $packages), timeout: 900); + + run('systemctl enable --now caddy'); + run('systemctl enable --now valkey'); + run('systemctl enable --now memcached'); + run('systemctl enable --now fail2ban'); +}) + ->verbose() + ->oncePerNode(); + +desc('Configures the ssh'); +task('provision:ssh', function () { + set('remote_user', get('provision_user')); + run("sed -i 's/PasswordAuthentication .*/PasswordAuthentication no/' /etc/ssh/sshd_config"); + run('ssh-keygen -A'); + run('systemctl enable sshd'); + run('systemctl restart sshd'); + if (test('[ ! -d /root/.ssh ]')) { + run('mkdir -p /root/.ssh'); + run('touch /root/.ssh/authorized_keys'); + } +})->oncePerNode(); + +desc('Setups a firewall'); +task('provision:firewall', function () { + set('remote_user', get('provision_user')); + run('systemctl enable --now firewalld'); + run('firewall-cmd --permanent --add-service=ssh'); + run('firewall-cmd --permanent --add-service=http'); + run('firewall-cmd --permanent --add-service=https'); + run('firewall-cmd --reload'); +})->oncePerNode(); + +desc('Verifies what provision was successful'); +task('provision:verify', function () { + fetch('{{domain}}', 'get', [], null, $info, true); + if ($info['http_code'] === 404) { + info("provisioned successfully!"); + } +}); diff --git a/recipe/provision-fedora/404.html b/recipe/provision-fedora/404.html new file mode 100644 index 000000000..58665778d --- /dev/null +++ b/recipe/provision-fedora/404.html @@ -0,0 +1,51 @@ + + + + + + 404 Not Found + + + +
+ + + +

Not Found

+

The requested URL was not found on this server.

+
+ + diff --git a/recipe/provision-fedora/Caddyfile b/recipe/provision-fedora/Caddyfile new file mode 100644 index 000000000..727c9a9d4 --- /dev/null +++ b/recipe/provision-fedora/Caddyfile @@ -0,0 +1,25 @@ +{{domain}} { + root * {{deploy_path}}/current/{{public_path}} + encode zstd gzip + file_server + php_fastcgi * unix//run/php-fpm/www.sock { + resolve_root_symlink + } + + log { + output file {{deploy_path}}/log/access.log { + mode 0644 + } + } + + handle_errors { + @404 { + expression {http.error.status_code} == 404 + } + rewrite @404 /404.html + encode zstd gzip + file_server { + root /var/deployer + } + } +} diff --git a/recipe/provision-fedora/databases.php b/recipe/provision-fedora/databases.php new file mode 100644 index 000000000..bbe34ba7f --- /dev/null +++ b/recipe/provision-fedora/databases.php @@ -0,0 +1,72 @@ +limit(1); + +desc('Provision MariaDB'); +task('provision:mariadb', function () { + run('dnf install -y mariadb-server', timeout: 900); + run('systemctl enable --now mariadb'); + run("mysql --user=\"root\" -e \"CREATE USER IF NOT EXISTS '{{db_user}}'@'0.0.0.0' IDENTIFIED BY '%db_password%';\"", secrets: ['db_password' => get('db_password')]); + run("mysql --user=\"root\" -e \"CREATE USER IF NOT EXISTS '{{db_user}}'@'%' IDENTIFIED BY '%db_password%';\"", secrets: ['db_password' => get('db_password')]); + run("mysql --user=\"root\" -e \"GRANT ALL PRIVILEGES ON *.* TO '{{db_user}}'@'0.0.0.0' WITH GRANT OPTION;\""); + run("mysql --user=\"root\" -e \"GRANT ALL PRIVILEGES ON *.* TO '{{db_user}}'@'%' WITH GRANT OPTION;\""); + run("mysql --user=\"root\" -e \"FLUSH PRIVILEGES;\""); + run("mysql --user=\"root\" -e \"CREATE DATABASE IF NOT EXISTS {{db_name}} character set UTF8mb4 collate utf8mb4_bin;\""); +}); + +desc('Provision PostgreSQL'); +task('provision:postgresql', function () { + $version = get('postgresql_version'); + + // Fedora's own repos only ship a single PostgreSQL version. PGDG gives us + // a properly maintained, versioned package instead. + run('dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/F-{{fedora_version}}-x86_64/pgdg-fedora-repo-latest.noarch.rpm'); + run('dnf -y -q module disable postgresql', nothrow: true); + + run("dnf install -y postgresql$version-server postgresql$version-contrib", timeout: 900); + run("/usr/pgsql-$version/bin/postgresql-$version-setup initdb", nothrow: true); + run("systemctl enable --now postgresql-$version"); + + run("sudo -u postgres /usr/pgsql-$version/bin/psql <<< $'CREATE DATABASE {{db_name}};'"); + run("sudo -u postgres /usr/pgsql-$version/bin/psql <<< $'CREATE USER {{db_user}} WITH ENCRYPTED PASSWORD \'%db_password%\';'", secrets: ['db_password' => get('db_password')]); + run("sudo -u postgres /usr/pgsql-$version/bin/psql <<< $'GRANT ALL PRIVILEGES ON DATABASE {{db_name}} TO {{db_user}};'"); +}); diff --git a/recipe/provision-fedora/nodejs.php b/recipe/provision-fedora/nodejs.php new file mode 100644 index 000000000..45eba13e5 --- /dev/null +++ b/recipe/provision-fedora/nodejs.php @@ -0,0 +1,36 @@ +> /etc/profile.d/fnm.sh"); +}) + ->oncePerNode(); diff --git a/recipe/provision-fedora/php.php b/recipe/provision-fedora/php.php new file mode 100644 index 000000000..d5d5a58e1 --- /dev/null +++ b/recipe/provision-fedora/php.php @@ -0,0 +1,81 @@ +verbose() + ->limit(1); + +desc('Shows php-fpm logs'); +task('logs:php-fpm', function () { + run('sudo tail -f /var/log/php-fpm/www-error.log'); +})->verbose(); + +desc('Installs Composer'); +task('provision:composer', function () { + run('curl -sS https://getcomposer.org/installer | php'); + run('mv composer.phar /usr/local/bin/composer'); +})->oncePerNode(); diff --git a/recipe/provision-fedora/user.php b/recipe/provision-fedora/user.php new file mode 100644 index 000000000..019edc93a --- /dev/null +++ b/recipe/provision-fedora/user.php @@ -0,0 +1,85 @@ +/dev/null 2>&1')) { + // TODO: Check what created deployer user configured correctly. + // TODO: Update sudo_password of deployer user. + // TODO: Copy ssh_copy_id to deployer ssh dir. + info('deployer user already exist'); + } else { + run('useradd deployer'); + run('mkdir -p /home/deployer/.ssh'); + run('mkdir -p /home/deployer/.deployer'); + run('usermod -aG wheel deployer'); + + run('chsh -s /bin/bash deployer'); + run('cp /root/.bashrc /home/deployer/.bashrc'); + + // Make color prompt. + run("sed -i 's/#force_color_prompt=yes/force_color_prompt=yes/' /home/deployer/.bashrc"); + + $password = run("openssl passwd -6 '%password%'", secrets: ['password' => get('sudo_password')]); + run("usermod --password '%password%' deployer", secrets: ['password' => $password]); + + // Copy root public key to deployer user so user can login without password. + run('cp /root/.ssh/authorized_keys /home/deployer/.ssh/authorized_keys'); + + // Create ssh key if not already exists. + run('ssh-keygen -f /home/deployer/.ssh/id_ed25519 -t ed25519 -N ""'); + + try { + run('chown -R deployer:deployer /home/deployer'); + run('chmod -R 755 /home/deployer'); + run('chmod 700 /home/deployer/.ssh'); + run('chmod 600 /home/deployer/.ssh/id_ed25519'); + run('chmod 600 /home/deployer/.ssh/authorized_keys'); + } catch (\Throwable $e) { + warning($e->getMessage()); + } + + run('usermod -a -G caddy deployer'); + } +})->oncePerNode(); + + +desc('Copy public key to remote server'); +task('provision:ssh_copy_id', function () { + $defaultKeys = [ + '~/.ssh/id_rsa.pub', + '~/.ssh/id_ed25519.pub', + '~/.ssh/id_ecdsa.pub', + '~/.ssh/id_dsa.pub', + ]; + + $publicKeyContent = false; + foreach ($defaultKeys as $key) { + $file = parse_home_dir($key); + if (file_exists($file)) { + $publicKeyContent = file_get_contents($file); + break; + } + } + + if (!$publicKeyContent) { + $publicKeyContent = ask(' Public key: ', ''); + } + + if (empty($publicKeyContent)) { + info('Skipping public key copy as no public key was found or provided.'); + return; + } + + run('echo "$PUBLIC_KEY" >> /home/deployer/.ssh/authorized_keys', env: ['PUBLIC_KEY' => $publicKeyContent]); +}); diff --git a/recipe/provision-fedora/website.php b/recipe/provision-fedora/website.php new file mode 100644 index 000000000..7844148da --- /dev/null +++ b/recipe/provision-fedora/website.php @@ -0,0 +1,83 @@ + /var/deployer/404.html"); +})->oncePerNode(); + +desc('Provision website'); +task('provision:website', function () { + $restoreBecome = become('deployer'); + + run("[ -d {{deploy_path}} ] || mkdir -p {{deploy_path}}"); + run("chown -R deployer:deployer {{deploy_path}}"); + + set('deploy_path', run("realpath {{deploy_path}}")); + cd('{{deploy_path}}'); + + run("[ -d log ] || mkdir log"); + run("chgrp caddy log"); + run("chmod g+w log"); + + $caddyfile = parse(file_get_contents(__DIR__ . '/Caddyfile')); + + if (test('[ -f Caddyfile ]')) { + run("echo $'$caddyfile' > Caddyfile.new"); + $diff = run('diff -U5 --color=always Caddyfile Caddyfile.new', nothrow: true); + if (empty($diff)) { + run('rm Caddyfile.new'); + } else { + info('Found Caddyfile changes'); + writeln("\n" . $diff); + $answer = askChoice(' Which Caddyfile to save? ', ['old', 'new'], 0); + if ($answer === 'old') { + run('rm Caddyfile.new'); + } else { + run('mv Caddyfile.new Caddyfile'); + } + } + } else { + run("echo $'$caddyfile' > Caddyfile"); + } + + $restoreBecome(); + + // Caddy runs confined under the httpd_t SELinux domain. deploy_path lives + // under the deployer user's home (user_home_t), which httpd_t can't read + // or write, so relabel the whole tree as webserver content. + run("semanage fcontext -a -t httpd_sys_rw_content_t '{{deploy_path}}(/.*)?'", nothrow: true); + run('restorecon -R {{deploy_path}}'); + + if (!test("grep -q 'import {{deploy_path}}/Caddyfile' /etc/caddy/Caddyfile")) { + run("echo 'import {{deploy_path}}/Caddyfile' >> /etc/caddy/Caddyfile"); + } + run('systemctl reload caddy'); + + info("Website {{domain}} configured!"); +})->limit(1); + +desc('Shows access logs'); +task('logs:access', function () { + run('tail -f {{deploy_path}}/log/access.log'); +})->verbose(); + +desc('Shows caddy syslog'); +task('logs:caddy', function () { + run('sudo journalctl -u caddy -f'); +})->verbose();