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.

380 lines
12 KiB

6 years ago
7 years ago
6 years ago
7 years ago
5 years ago
7 years ago
6 years ago
7 years ago
6 years ago
5 years ago
6 years ago
5 years ago
7 years ago
5 years ago
7 years ago
6 years ago
5 years ago
6 years ago
5 years ago
6 years ago
5 years ago
7 years ago
6 years ago
5 years ago
7 years ago
6 years ago
7 years ago
5 years ago
7 years ago
5 years ago
7 years ago
5 years ago
7 years ago
5 years ago
7 years ago
5 years ago
7 years ago
5 years ago
7 years ago
5 years ago
7 years ago
5 years ago
7 years ago
6 years ago
5 years ago
5 years ago
5 years ago
5 years ago
6 years ago
5 years ago
6 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
6 years ago
5 years ago
6 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
6 years ago
5 years ago
5 years ago
6 years ago
5 years ago
6 years ago
5 years ago
6 years ago
5 years ago
6 years ago
5 years ago
5 years ago
5 years ago
6 years ago
5 years ago
5 years ago
6 years ago
5 years ago
6 years ago
5 years ago
6 years ago
5 years ago
6 years ago
5 years ago
7 years ago
  1. use std::borrow::Cow;
  2. use std::ops::Deref;
  3. use std::path::Path;
  4. use command_runner::{CommandRunner, SetuidCommandRunner};
  5. use storage::{SimpleStorage, Storage};
  6. use symbols::acme::{AcmeCert, AcmeCertChain};
  7. use symbols::cron::Cron;
  8. use symbols::file::File;
  9. use symbols::git::checkout::GitCheckout;
  10. use symbols::hook::Hook;
  11. use symbols::list::ListAction;
  12. use symbols::mariadb::{DatabaseDump, MariaDBDatabase, MariaDBUser};
  13. use symbols::nginx::server::{php_server_config_snippet, server_config, NginxServer};
  14. use symbols::owner::Owner;
  15. use symbols::stored_directory::{StorageDirection, StoredDirectory};
  16. use symbols::systemd::reload::ReloadService;
  17. use symbols::tls::SelfSignedTlsCert;
  18. use symbols::{Action, Symbol, SymbolRunner};
  19. pub trait Policy {
  20. fn user_name_for_host(&self, host_name: &'static str) -> String;
  21. fn home_for_user(&self, user_name: &str) -> String {
  22. format!("/home/{}", user_name)
  23. }
  24. }
  25. pub struct DefaultPolicy;
  26. impl Policy for DefaultPolicy {
  27. fn user_name_for_host(&self, host_name: &'static str) -> String {
  28. host_name.split('.').rev().fold(String::new(), |result, part| if result.is_empty() { result } else { result + "_" } + part)
  29. }
  30. }
  31. pub struct SymbolFactory<'a, C: 'a + CommandRunner, R: 'a + SymbolRunner, P: 'a + Policy> {
  32. command_runner: &'a C,
  33. acme_command_runner: SetuidCommandRunner<'a, C>,
  34. symbol_runner: &'a R,
  35. policy: &'a P,
  36. }
  37. impl<'b, C: 'b + CommandRunner, R: 'b + SymbolRunner, P: 'b + Policy> SymbolFactory<'b, C, R, P> {
  38. pub fn new(command_runner: &'b C, symbol_runner: &'b R, policy: &'b P) -> Self {
  39. let acme_user = "acme"; // FIXME: CONFIG
  40. let acme_command_runner = SetuidCommandRunner::new(acme_user, command_runner);
  41. SymbolFactory {
  42. command_runner,
  43. acme_command_runner,
  44. symbol_runner,
  45. policy,
  46. }
  47. }
  48. pub fn get_nginx_acme_server<'a, 'c: 'a, S: 'a + Symbol>(
  49. &'c self,
  50. host: &'static str,
  51. nginx_server_symbol: S,
  52. ) -> Box<dyn Action + 'a> {
  53. Box::new(ListAction::new(vec![
  54. Box::new(SelfSignedTlsCert::new(host.into(), self.command_runner))
  55. .into_action(self.symbol_runner),
  56. Box::new(Hook::new(
  57. nginx_server_symbol,
  58. ReloadService::new("nginx", self.command_runner),
  59. ))
  60. .into_action(self.symbol_runner),
  61. Box::new(AcmeCert::new(host.into(), &self.acme_command_runner))
  62. .into_action(self.symbol_runner),
  63. Box::new(Hook::new(
  64. AcmeCertChain::new(host.into(), &self.acme_command_runner),
  65. ReloadService::new("nginx", self.command_runner),
  66. ))
  67. .into_action(self.symbol_runner),
  68. ]))
  69. }
  70. pub fn get_nginx_acme_challenge_config<'a>(&'a self) -> Box<dyn Action + 'a> {
  71. Box::new(File::new(
  72. "/etc/nginx/snippets/acme-challenge.conf",
  73. "location ^~ /.well-known/acme-challenge/ {
  74. alias /home/acme/challenges/;
  75. try_files $uri =404;
  76. }",
  77. ))
  78. .into_action(self.symbol_runner)
  79. }
  80. fn get_php_fpm_pool_socket_path<'a>(&'a self, user_name: &str) -> String {
  81. format!("/run/php/{}.sock", user_name)
  82. }
  83. fn get_php_fpm_pool<'a>(&'a self, user_name: &str) -> Box<dyn Action + 'a> {
  84. let socket = self.get_php_fpm_pool_socket_path(user_name);
  85. Box::new(Hook::new(
  86. File::new(
  87. format!("/etc/php/7.0/fpm/pool.d/{}.conf", user_name),
  88. format!(
  89. "[{0}]
  90. user = {0}
  91. group = www-data
  92. listen = {1}
  93. listen.owner = www-data
  94. pm = ondemand
  95. pm.max_children = 10
  96. catch_workers_output = yes
  97. env[PATH] = /usr/local/bin:/usr/bin:/bin
  98. ",
  99. user_name, socket
  100. ),
  101. ),
  102. ReloadService::new("php7.0-fpm", self.command_runner),
  103. ))
  104. .into_action(self.symbol_runner)
  105. }
  106. pub fn serve_php<'a>(
  107. &'a self,
  108. host_name: &'static str,
  109. root_dir: Cow<'a, str>,
  110. ) -> Box<dyn Action + 'a> {
  111. let user_name = self.policy.user_name_for_host(host_name);
  112. let socket = self.get_php_fpm_pool_socket_path(&user_name);
  113. Box::new(ListAction::new(vec![
  114. self.get_php_fpm_pool(&user_name),
  115. self.get_nginx_acme_server(
  116. host_name,
  117. NginxServer::new_php(host_name, socket.into(), root_dir, self.command_runner),
  118. ),
  119. ]))
  120. }
  121. pub fn serve_wordpress<'a>(
  122. &'a self,
  123. host_name: &'static str,
  124. root_dir: Cow<'a, str>,
  125. ) -> Box<dyn Action + 'a> {
  126. let user_name = self.policy.user_name_for_host(host_name);
  127. let socket = self.get_php_fpm_pool_socket_path(&user_name);
  128. Box::new(ListAction::new(vec![
  129. self.get_php_fpm_pool(&user_name),
  130. self.get_nginx_acme_server(
  131. host_name,
  132. NginxServer::new(
  133. host_name,
  134. server_config(
  135. host_name,
  136. &format!(
  137. "{}
  138. location / {{
  139. try_files $uri $uri/ /index.php?$args;
  140. }}
  141. ",
  142. php_server_config_snippet(socket.into(), root_dir)
  143. ),
  144. ),
  145. self.command_runner,
  146. ),
  147. ),
  148. ]))
  149. }
  150. pub fn serve_dokuwiki<'a>(
  151. &'a self,
  152. host_name: &'static str,
  153. root_dir: &'static str,
  154. ) -> Box<dyn Action + 'a> {
  155. let user_name = self.policy.user_name_for_host(host_name);
  156. let socket = self.get_php_fpm_pool_socket_path(&user_name);
  157. Box::new(ListAction::new(vec![
  158. self.get_php_fpm_pool(&user_name),
  159. self.get_nginx_acme_server(host_name,
  160. NginxServer::new(
  161. host_name,
  162. server_config(host_name, &format!("
  163. root {};
  164. index doku.php;
  165. location ~ [^/]\\.php(/|$) {{
  166. fastcgi_pass unix:{};
  167. include \"snippets/fastcgi-php.conf\";
  168. }}
  169. location ~ /(data/|conf/|bin/|inc/|install.php) {{ deny all; }}
  170. location / {{ try_files $uri $uri/ @dokuwiki; }}
  171. location @dokuwiki {{
  172. # rewrites \"doku.php/\" out of the URLs if you set the userewrite setting to .htaccess in dokuwiki config page
  173. rewrite ^/_media/(.*) /lib/exe/fetch.php?media=$1 last;
  174. rewrite ^/_detail/(.*) /lib/exe/detail.php?media=$1 last;
  175. rewrite ^/_export/([^/]+)/(.*) /doku.php?do=export_$1&id=$2 last;
  176. rewrite ^/(.*) /doku.php?id=$1&$args last;
  177. }}
  178. ",
  179. root_dir,
  180. socket)),
  181. self.command_runner
  182. ))
  183. ]))
  184. }
  185. pub fn serve_nextcloud<'a>(
  186. &'a self,
  187. host_name: &'static str,
  188. root_dir: Cow<'a, str>,
  189. ) -> Box<dyn Action + 'a> {
  190. let user_name = self.policy.user_name_for_host(host_name);
  191. let socket = self.get_php_fpm_pool_socket_path(&user_name);
  192. Box::new(ListAction::new(vec![
  193. self.get_php_fpm_pool(&user_name),
  194. self.get_nginx_acme_server(
  195. host_name,
  196. NginxServer::new(
  197. host_name,
  198. server_config(
  199. host_name,
  200. &format!(
  201. "{}
  202. client_max_body_size 500M;
  203. # Disable gzip to avoid the removal of the ETag header
  204. gzip off;
  205. rewrite ^/caldav(.*)$ /remote.php/caldav$1 redirect;
  206. rewrite ^/carddav(.*)$ /remote.php/carddav$1 redirect;
  207. rewrite ^/webdav(.*)$ /remote.php/webdav$1 redirect;
  208. error_page 403 /core/templates/403.php;
  209. error_page 404 /core/templates/404.php;
  210. location = /robots.txt {{
  211. allow all;
  212. log_not_found off;
  213. access_log off;
  214. }}
  215. location ~ ^/(?:\\.htaccess|data|config|db_structure\\.xml|README) {{
  216. deny all;
  217. }}
  218. location / {{
  219. # The following 2 rules are only needed with webfinger
  220. rewrite ^/.well-known/host-meta /public.php?service=host-meta last;
  221. rewrite ^/.well-known/host-meta.json /public.php?service=host-meta-json last;
  222. rewrite ^/.well-known/carddav /remote.php/carddav/ redirect;
  223. rewrite ^/.well-known/caldav /remote.php/caldav/ redirect;
  224. rewrite ^(/core/doc/[^\\/]+/)$ $1/index.html;
  225. try_files $uri $uri/ /index.php;
  226. }}
  227. # Adding the cache control header for js and css files
  228. # Make sure it is BELOW the location ~ \\.php(?:$|/) {{ block
  229. location ~* \\.(?:css|js)$ {{
  230. add_header Cache-Control \"public, max-age=7200\";
  231. # Optional: Don't log access to assets
  232. access_log off;
  233. }}
  234. # Optional: Don't log access to other assets
  235. location ~* \\.(?:jpg|jpeg|gif|bmp|ico|png|swf)$ {{
  236. access_log off;
  237. }}
  238. ",
  239. php_server_config_snippet(socket.into(), root_dir)
  240. ),
  241. ),
  242. self.command_runner,
  243. ),
  244. ),
  245. ]))
  246. }
  247. pub fn serve_redir<'a>(
  248. &'a self,
  249. host_name: &'static str,
  250. target: &'static str,
  251. ) -> Box<dyn Action + 'a> {
  252. self.get_nginx_acme_server(
  253. host_name,
  254. NginxServer::new_redir(host_name, target, self.command_runner),
  255. )
  256. }
  257. pub fn serve_static<'a>(&'a self, host_name: &'static str, dir: &'a str) -> Box<dyn Action + 'a> {
  258. self.get_nginx_acme_server(
  259. host_name,
  260. NginxServer::new_static(host_name, dir, self.command_runner),
  261. )
  262. }
  263. pub fn get_stored_directory<'a, T: Into<String>>(
  264. &'a self,
  265. storage_name: &'static str,
  266. target: T,
  267. ) -> (Box<dyn Action + 'a>, Box<dyn Action + 'a>) {
  268. let data = SimpleStorage::new("/root/data".to_string(), storage_name.to_string());
  269. let string_target = target.into();
  270. (
  271. Box::new(StoredDirectory::new(
  272. string_target.clone().into(),
  273. data.clone(),
  274. StorageDirection::Save,
  275. self.command_runner,
  276. ))
  277. .into_action(self.symbol_runner),
  278. Box::new(StoredDirectory::new(
  279. string_target.into(),
  280. data.clone(),
  281. StorageDirection::Load,
  282. self.command_runner,
  283. ))
  284. .into_action(self.symbol_runner),
  285. )
  286. }
  287. pub fn get_mariadb_database<'a>(&'a self, name: &'static str) -> Box<dyn Action + 'a> {
  288. let db_dump = SimpleStorage::new("/root/data".to_string(), format!("{}.sql", name));
  289. Box::new(ListAction::new(vec![
  290. Box::new(MariaDBDatabase::new(
  291. name.into(),
  292. db_dump
  293. .read_filename()
  294. .expect("Initial db dump missing")
  295. .into(),
  296. self.command_runner,
  297. ))
  298. .into_action(self.symbol_runner),
  299. Box::new(DatabaseDump::new(name, db_dump, self.command_runner))
  300. .into_action(self.symbol_runner),
  301. ]))
  302. }
  303. pub fn get_mariadb_user<'a>(&'a self, user_name: &'static str) -> Box<dyn Action + 'a> {
  304. Box::new(MariaDBUser::new(user_name.into(), self.command_runner))
  305. .into_action(self.symbol_runner)
  306. }
  307. pub fn get_git_checkout<'a, T: 'a + AsRef<str>>(
  308. &'a self,
  309. target: T,
  310. source: &'a str,
  311. branch: &'a str,
  312. ) -> Box<dyn Action + 'a> {
  313. Box::new(GitCheckout::new(
  314. target,
  315. source,
  316. branch,
  317. self.command_runner,
  318. ))
  319. .into_action(self.symbol_runner)
  320. }
  321. pub fn get_owner<'a, F: 'a + AsRef<str>>(
  322. &'a self,
  323. file: F,
  324. user: &'a str,
  325. ) -> Box<dyn Action + 'a> {
  326. Box::new(Owner::new(file, user.into(), self.command_runner)).into_action(self.symbol_runner)
  327. }
  328. pub fn get_file<'a, F: 'a + Deref<Target = str>, Q: 'a + AsRef<Path>>(
  329. &'a self,
  330. path: Q,
  331. content: F,
  332. ) -> Box<dyn Action + 'a> {
  333. Box::new(File::new(path, content)).into_action(self.symbol_runner)
  334. }
  335. pub fn get_cron<'a, T: 'a + Deref<Target = str>, U: 'a + Deref<Target = str>>(
  336. &'a self,
  337. user: U,
  338. content: T,
  339. ) -> Box<dyn Action + 'a> {
  340. Box::new(Cron::new(user, content, self.command_runner)).into_action(self.symbol_runner)
  341. }
  342. }