A library for writing host-specific, single-binary configuration management and deployment tools
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

279 lines
11 KiB

use std::borrow::Cow;
use std::ops::Deref;
use std::path::Path;
use command_runner::{CommandRunner, SetuidCommandRunner};
use storage::{SimpleStorage, Storage};
use symbols::{Action, Symbol, SymbolRunner};
use symbols::acme::{AcmeCert, AcmeCertChain};
use symbols::file::File;
use symbols::git::checkout::GitCheckout;
use symbols::hook::Hook;
use symbols::list::ListAction;
use symbols::mariadb::{DatabaseDump, MariaDBDatabase, MariaDBUser};
use symbols::nginx::server::{NginxServer, server_config, php_server_config_snippet};
use symbols::owner::Owner;
use symbols::stored_directory::{StoredDirectory, StorageDirection};
use symbols::systemd::reload::ReloadService;
use symbols::tls::SelfSignedTlsCert;
pub trait Policy {
fn user_name_for_host(&self, host_name: &'static str) -> String;
fn home_for_user(&self, user_name: &str) -> String {
format!("/home/{}", user_name)
}
}
pub struct DefaultPolicy;
impl Policy for DefaultPolicy {
fn user_name_for_host(&self, host_name: &'static str) -> String {
host_name.split('.').rev().fold(String::new(), |result, part| if result.is_empty() { result } else { result + "_" } + part)
}
}
pub struct SymbolFactory<'a, C: 'a + CommandRunner, R: 'a + SymbolRunner, P: 'a + Policy>{
command_runner: &'a C,
acme_command_runner: SetuidCommandRunner<'a, C>,
symbol_runner: &'a R,
policy: &'a P
}
impl<'b, C: 'b + CommandRunner, R: 'b + SymbolRunner, P: 'b + Policy> SymbolFactory<'b, C, R, P> {
pub fn new(command_runner: &'b C, symbol_runner: &'b R, policy: &'b P) -> Self {
let acme_user = "acme"; // FIXME: CONFIG
let acme_command_runner = SetuidCommandRunner::new(acme_user, command_runner);
SymbolFactory { command_runner, acme_command_runner, symbol_runner, policy }
}
pub fn get_nginx_acme_server<'a, 'c: 'a, S: 'a + Symbol>(&'c self, host: &'static str, nginx_server_symbol: S) -> Box<dyn Action + 'a> {
Box::new(ListAction::new(vec![
Box::new(SelfSignedTlsCert::new(
host.into(),
self.command_runner
)).into_action(self.symbol_runner),
Box::new(Hook::new(
nginx_server_symbol,
ReloadService::new("nginx", self.command_runner)
)).into_action(self.symbol_runner),
Box::new(AcmeCert::new(
host.into(),
&self.acme_command_runner
)).into_action(self.symbol_runner),
Box::new(Hook::new(
AcmeCertChain::new(
host.into(),
&self.acme_command_runner
),
ReloadService::new("nginx", self.command_runner)
)).into_action(self.symbol_runner)
]))
}
pub fn get_nginx_acme_challenge_config<'a>(&'a self) -> Box<dyn Action + 'a> {
Box::new(File::new(
"/etc/nginx/snippets/acme-challenge.conf", "location ^~ /.well-known/acme-challenge/ {
alias /home/acme/challenges/;
try_files $uri =404;
}"
)).into_action(self.symbol_runner)
}
fn get_php_fpm_pool_socket_path<'a>(&'a self, user_name: &str) -> String {
format!("/run/php/{}.sock", user_name)
}
fn get_php_fpm_pool<'a>(&'a self, user_name: &str) -> Box<dyn Action + 'a> {
let socket = self.get_php_fpm_pool_socket_path(user_name);
Box::new(Hook::new(
File::new(
format!("/etc/php/7.0/fpm/pool.d/{}.conf", user_name),
format!(
"[{0}]
user = {0}
group = www-data
listen = {1}
listen.owner = www-data
pm = ondemand
pm.max_children = 10
catch_workers_output = yes
env[PATH] = /usr/local/bin:/usr/bin:/bin
"
, user_name, socket)),
ReloadService::new("php7.0-fpm", self.command_runner)
)).into_action(self.symbol_runner)
}
pub fn serve_php<'a>(&'a self, host_name: &'static str, root_dir: Cow<'a, str>) -> Box<dyn Action + 'a> {
let user_name = self.policy.user_name_for_host(host_name);
let socket = self.get_php_fpm_pool_socket_path(&user_name);
Box::new(ListAction::new(vec![
self.get_php_fpm_pool(&user_name),
self.get_nginx_acme_server(host_name,
NginxServer::new_php(
host_name,
socket.into(),
root_dir,
self.command_runner
)
)
]))
}
pub fn serve_wordpress<'a>(&'a self, host_name: &'static str, root_dir: Cow<'a, str>) -> Box<dyn Action + 'a> {
let user_name = self.policy.user_name_for_host(host_name);
let socket = self.get_php_fpm_pool_socket_path(&user_name);
Box::new(ListAction::new(vec![
self.get_php_fpm_pool(&user_name),
self.get_nginx_acme_server(host_name,
NginxServer::new(
host_name,
server_config(host_name, &format!("{}
location / {{
try_files $uri $uri/ /index.php?$args;
}}
", php_server_config_snippet(socket.into(), root_dir))),
self.command_runner
))
]))
}
pub fn serve_dokuwiki<'a>(&'a self, host_name: &'static str, root_dir: &'static str) -> Box<dyn Action + 'a> {
let user_name = self.policy.user_name_for_host(host_name);
let socket = self.get_php_fpm_pool_socket_path(&user_name);
Box::new(ListAction::new(vec![
self.get_php_fpm_pool(&user_name),
self.get_nginx_acme_server(host_name,
NginxServer::new(
host_name,
server_config(host_name, &format!("
root {};
index doku.php;
location ~ [^/]\\.php(/|$) {{
fastcgi_pass unix:{};
include \"snippets/fastcgi-php.conf\";
}}
location ~ /(data/|conf/|bin/|inc/|install.php) {{ deny all; }}
location / {{ try_files $uri $uri/ @dokuwiki; }}
location @dokuwiki {{
# rewrites \"doku.php/\" out of the URLs if you set the userewrite setting to .htaccess in dokuwiki config page
rewrite ^/_media/(.*) /lib/exe/fetch.php?media=$1 last;
rewrite ^/_detail/(.*) /lib/exe/detail.php?media=$1 last;
rewrite ^/_export/([^/]+)/(.*) /doku.php?do=export_$1&id=$2 last;
rewrite ^/(.*) /doku.php?id=$1&$args last;
}}
",
root_dir,
socket)),
self.command_runner
))
]))
}
pub fn serve_nextcloud<'a>(&'a self, host_name: &'static str, root_dir: Cow<'a, str>) -> Box<dyn Action + 'a> {
let user_name = self.policy.user_name_for_host(host_name);
let socket = self.get_php_fpm_pool_socket_path(&user_name);
Box::new(ListAction::new(vec![
self.get_php_fpm_pool(&user_name),
self.get_nginx_acme_server(host_name,
NginxServer::new(
host_name,
server_config(host_name, &format!("{}
client_max_body_size 500M;
# Disable gzip to avoid the removal of the ETag header
gzip off;
rewrite ^/caldav(.*)$ /remote.php/caldav$1 redirect;
rewrite ^/carddav(.*)$ /remote.php/carddav$1 redirect;
rewrite ^/webdav(.*)$ /remote.php/webdav$1 redirect;
error_page 403 /core/templates/403.php;
error_page 404 /core/templates/404.php;
location = /robots.txt {{
allow all;
log_not_found off;
access_log off;
}}
location ~ ^/(?:\\.htaccess|data|config|db_structure\\.xml|README) {{
deny all;
}}
location / {{
# The following 2 rules are only needed with webfinger
rewrite ^/.well-known/host-meta /public.php?service=host-meta last;
rewrite ^/.well-known/host-meta.json /public.php?service=host-meta-json last;
rewrite ^/.well-known/carddav /remote.php/carddav/ redirect;
rewrite ^/.well-known/caldav /remote.php/caldav/ redirect;
rewrite ^(/core/doc/[^\\/]+/)$ $1/index.html;
try_files $uri $uri/ /index.php;
}}
# Adding the cache control header for js and css files
# Make sure it is BELOW the location ~ \\.php(?:$|/) {{ block
location ~* \\.(?:css|js)$ {{
add_header Cache-Control \"public, max-age=7200\";
# Optional: Don't log access to assets
access_log off;
}}
# Optional: Don't log access to other assets
location ~* \\.(?:jpg|jpeg|gif|bmp|ico|png|swf)$ {{
access_log off;
}}
", php_server_config_snippet(socket.into(), root_dir))),
self.command_runner
))
]))
}
pub fn serve_redir<'a>(&'a self, host_name: &'static str, target: &'static str) -> Box<dyn Action + 'a> {
self.get_nginx_acme_server(host_name, NginxServer::new_redir(host_name, target, self.command_runner))
}
pub fn serve_static<'a>(&'a self, host_name: &'static str, dir: &'a str) -> Box<dyn Action + 'a> {
self.get_nginx_acme_server(host_name, NginxServer::new_static(host_name, dir, self.command_runner))
}
pub fn get_stored_directory<'a, T: Into<String>>(&'a self, storage_name: &'static str, target: T) -> (Box<dyn Action + 'a>, Box<dyn Action + 'a>) {
let data = SimpleStorage::new("/root/data".to_string(), storage_name.to_string());
let string_target = target.into();
(
Box::new(StoredDirectory::new(string_target.clone().into(), data.clone(), StorageDirection::Save, self.command_runner)).into_action(self.symbol_runner),
Box::new(StoredDirectory::new(string_target.into(), data.clone(), StorageDirection::Load, self.command_runner)).into_action(self.symbol_runner)
)
}
pub fn get_mariadb_database<'a>(&'a self, name: &'static str) -> Box<dyn Action + 'a> {
let db_dump = SimpleStorage::new("/root/data".to_string(), format!("{}.sql", name));
Box::new(ListAction::new(vec![
Box::new(MariaDBDatabase::new(name.into(), db_dump.read_filename().unwrap().into(), self.command_runner)).into_action(self.symbol_runner),
Box::new(DatabaseDump::new(name, db_dump, self.command_runner)).into_action(self.symbol_runner)
]))
}
pub fn get_mariadb_user<'a>(&'a self, user_name: &'static str) -> Box<dyn Action + 'a> {
Box::new(MariaDBUser::new(user_name.into(), self.command_runner)).into_action(self.symbol_runner)
}
pub fn get_git_checkout<'a, T: 'a + AsRef<str>>(&'a self, target: T, source: &'a str, branch: &'a str) -> Box<dyn Action + 'a> {
Box::new(GitCheckout::new(target, source, branch, self.command_runner)).into_action(self.symbol_runner)
}
pub fn get_owner<'a, F: 'a + AsRef<str>>(&'a self, file: F, user: &'a str) -> Box<dyn Action + 'a> {
Box::new(Owner::new(file, user.into(), self.command_runner)).into_action(self.symbol_runner)
}
pub fn get_file<'a, F: 'a + Deref<Target=str>, Q: 'a + AsRef<Path>>(&'a self, path: Q, content: F) -> Box<dyn Action + 'a> {
Box::new(File::new(path, content)).into_action(self.symbol_runner)
}
}