From mboxrd@z Thu Jan 1 00:00:00 1970 From: Hartmut Goebel Subject: [WIP 1/2] services: Add php-fpm service. Date: Thu, 8 Dec 2016 18:52:09 +0100 Message-ID: <1481219530-786-2-git-send-email-h.goebel@crazy-compilers.com> References: <1481219132-27353-1-git-send-email-h.goebel@crazy-compilers.com> <1481219530-786-1-git-send-email-h.goebel@crazy-compilers.com> Mime-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Return-path: Received: from eggs.gnu.org ([2001:4830:134:3::10]:60315) by lists.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1cF2rl-0006zq-4g for guix-devel@gnu.org; Thu, 08 Dec 2016 12:52:23 -0500 Received: from Debian-exim by eggs.gnu.org with spam-scanned (Exim 4.71) (envelope-from ) id 1cF2rh-0004b6-HY for guix-devel@gnu.org; Thu, 08 Dec 2016 12:52:21 -0500 Received: from mail-out.m-online.net ([212.18.0.9]:59650) by eggs.gnu.org with esmtps (TLS1.0:DHE_RSA_AES_256_CBC_SHA1:32) (Exim 4.71) (envelope-from ) id 1cF2rh-0004ay-2R for guix-devel@gnu.org; Thu, 08 Dec 2016 12:52:17 -0500 Received: from frontend01.mail.m-online.net (unknown [192.168.8.182]) by mail-out.m-online.net (Postfix) with ESMTP id 3tZNH36XVTz3hryM for ; Thu, 8 Dec 2016 18:52:15 +0100 (CET) Received: from localhost (dynscan1.mnet-online.de [192.168.6.68]) by mail.m-online.net (Postfix) with ESMTP id 3tZNH36PDqzvnMr for ; Thu, 8 Dec 2016 18:52:15 +0100 (CET) Received: from mail.mnet-online.de ([192.168.8.182]) by localhost (dynscan1.mail.m-online.net [192.168.6.68]) (amavisd-new, port 10024) with ESMTP id ZrqH0mHud6Bv for ; Thu, 8 Dec 2016 18:52:13 +0100 (CET) Received: from hermia.goebel-consult.de (ppp-188-174-144-224.dynamic.mnet-online.de [188.174.144.224]) (using TLSv1 with cipher DHE-RSA-CAMELLIA256-SHA (256/256 bits)) (No client certificate requested) by mail.mnet-online.de (Postfix) with ESMTPS for ; Thu, 8 Dec 2016 18:52:13 +0100 (CET) Received: from lenashee.goebel-consult.de (lenashee.goebel-consult.de [192.168.110.2]) by hermia.goebel-consult.de (Postfix) with ESMTP id 375AA60770 for ; Thu, 8 Dec 2016 18:52:10 +0100 (CET) In-Reply-To: <1481219530-786-1-git-send-email-h.goebel@crazy-compilers.com> List-Id: "Development of GNU Guix and the GNU System distribution." List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: guix-devel-bounces+gcggd-guix-devel=m.gmane.org@gnu.org Sender: "Guix-devel" To: guix-devel@gnu.org * gnu/services/php.scm: New file. * gnu/local.mk (GNU_SYSTEM_MODULES): Add it. --- gnu/local.mk | 1 + gnu/services/php.scm | 568 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 569 insertions(+) create mode 100644 gnu/services/php.scm diff --git a/gnu/local.mk b/gnu/local.mk index eec0e01..a42f6cd 100644 --- a/gnu/local.mk +++ b/gnu/local.mk @@ -413,6 +413,7 @@ GNU_SYSTEM_MODULES = \ %D%/services/mcron.scm \ %D%/services/networking.scm \ %D%/services/nfs.scm \ + %D%/services/php.scm \ %D%/services/shepherd.scm \ %D%/services/herd.scm \ %D%/services/sddm.scm \ diff --git a/gnu/services/php.scm b/gnu/services/php.scm new file mode 100644 index 0000000..e45b54f --- /dev/null +++ b/gnu/services/php.scm @@ -0,0 +1,568 @@ +;;; GNU Guix --- Functional package management for GNU +;;; Copyright © 2016 Hartmut Goebel +;;; +;;; This file is part of GNU Guix. +;;; +;;; GNU Guix is free software; you can redistribute it and/or modify it +;;; under the terms of the GNU General Public License as published by +;;; the Free Software Foundation; either version 3 of the License, or (at +;;; your option) any later version. +;;; +;;; GNU Guix is distributed in the hope that it will be useful, but +;;; WITHOUT ANY WARRANTY; without even the implied warranty of +;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;;; GNU General Public License for more details. +;;; +;;; You should have received a copy of the GNU General Public License +;;; along with GNU Guix. If not, see . +;;; +;;; Some of the help text was taken from the default php-fpm.conf files. + +(define-module (gnu services php) + #:use-module (gnu services) + #:use-module (gnu services base) + #:use-module (gnu services configuration) + #:use-module (gnu services shepherd) + #:use-module (gnu system shadow) + #:use-module (gnu packages php) + #:use-module (guix records) + #:use-module (guix packages) + #:use-module (guix gexp) + #:use-module (ice-9 match) + #:use-module (ice-9 regex) ;; for uglify-field-name + #:export (php-fpm-service + php-ini + php-fpm-configuration + php-fpm-pool)) + +(define (uglify-field-name field-name) + (let ((str (symbol->string field-name))) + (if (string-suffix? "?" str) + (substring str 0 (1- (string-length str))) + (regexp-substitute/global #f "-" str 'pre "_" 'post)))) +(define (serialize-field field-name val) + (format #t "~a = ~a\n" (uglify-field-name field-name) val)) + +(define (serialize-string field-name val) + (serialize-field field-name val)) + +(define (serialize-space-separated-string-list field-name val) + (serialize-field field-name (string-join val " "))) + +(define (serialize-boolean field-name val) + (serialize-string field-name (if val "on" "off"))) + +(define (non-negative-integer? val) + (and (exact-integer? val) (not (negative? val)))) +(define (serialize-non-negative-integer field-name val) + (serialize-field field-name val)) + +(define (optional-string? val) + (or (and (boolean? val) (not val)) (string? val))) +(define (serialize-optional-string field-name val) + (when (not (boolean? val)) + (serialize-field field-name val))) + +(define (section? val) #t) +(define (serialize-section field-name val) + (format #t "\n[~a]\n" val)) + +(define (production? val) + (boolean? val)) +(define (serialize-production field-name val) + (serialize-boolean 'display-errors (not val)) + (serialize-boolean 'display-startup-errors (not val)) + (serialize-boolean 'track-errors (not val)) + (serialize-string 'error-reporting + (if val "E_ALL & ~E_DEPRECATED & ~E_STRICT" + "E_ALL"))) + +(define (pools-list? val) + (and (list? val) (and-map php-fpm-pool? val))) +(define (serialize-pools-list field-name val) + (for-each (lambda (val) (serialize-php-fpm-pool field-name val)) val)) + +(define (serialize-php-fpm-pool field-name val) + (format #t "\n\n; Start a new pool.") + (serialize-configuration val php-fpm-pool-fields)) + +;;; ---- php.ini --- + +(define-configuration php-ini + (php + (package php) + "The php package to use.") + + (PHP (section "PHP") "The global options section") + + (production + (production #t) + "This variable determines if the @file{php.ini} configuration +will be configured towards 'production' systems (don't display +errors), or 'development' systems (display all errors).") + + (max-execution-time + (non-negative-integer 30) + "Specify default maximum execution time, in seconds.") + + (max-input-time + (non-negative-integer 60) + "Specify default maximum input time, in seconds.") + + (memory-limit + (string "128M") + "Specify maximum memory limit for PHP processes, in megabytes.") + + (post-max-size + (string "8M") + "Specify maximum size of the POST data, in megabytes.") + + (file-uploads + (boolean #t) + "Enable or disable file uploading in PHP applications.") + + (upload-max-filesize + (string "8M") ;; TODO default to what has been passed to post-max-size + "Specify maximum size of uploaded files, in megabytes.") + + (max-file-uploads + (non-negative-integer 20) + "Specify maximum number of files uploaded at once.") + + (default-charset + (string "UTF-8") + "Specify default charset used in PHP environment.") + + (allow-url-fopen + (boolean #f) + "Enable or disable access to remote URLs in PHP applications.") + + ;; CGI + (CGI (section "CGI") "The CGI section") + + (cgi.fix-pathinfo + (boolean #f) + "Enable or disable 'cgi.fix_pathinfo' option in PHP. This is highly +dependent on the used webserver (nginx should have the option disabled, +apache2 needs it to be enabled).") + + ;; Date + (Date (section "Date") "The Date section") + + (date.timezone + (string "Etc/UTC") ;; TODO: Take from system timezone + "Configure the PHP timezone.")) + +;;; ---- php-fpm.conf ---- + +(define-configuration php-fpm-configuration + ;; All relative paths in this configuration file are relative to PHP's + ;; install prefix. + (global (section "global") "The global options section") + + ;;(pid "/var/run/php/php-fpm.pid") configured on command line + + (error-log + (string "/var/log/php-fpm.log") + "Path to the 'error.log' file which is used by PHP-FPM to log +error messages. If it's set to 'syslog', error logs are sent to the +local log daemon.") + + (syslog.ident + (string "php-fpm") + "When 'syslog' logging is enabled, specify the program identification +string used by PHP-FPM. This should be one word string, without spaces.") + + (syslog.facility + (string "daemon") + "When 'syslog' logging is enabled, specify the 'syslog' facility to use.") + + (log-level + (string "notice") + "When 'syslog' logging is enabled, specify the log level used by PHP-FPM.") + + (emergency-restart-threshold + (non-negative-integer 0) + "Specify number of PHP-FPM child processes that exit with errors +during a given interval (see below) that will trigger an automatic +restart of the master PHP-FPM process.") + + (emergency-restart-interval + (non-negative-integer 0) + "Specify the interval which is used to determine +emergency-restart-threshold.") + + (process-control-timeout + (non-negative-integer 0) + "Specify maximum wait time the master PHP-FPM process waits for a reaction +on the signals sent to the child processes.") + + (process.max + (non-negative-integer 128) + "Maximum number of PHP-FPM child processes.") + + ;; not yet implemented + ;; ("process.priority" "-19") + ;; ("daemonize "yes" + ;; ("rlimit_files" "1024") + ;; (rlimit_core 0") + ;; ("events.mechanism" "epoll") + ;; ("systemd_interval" "10") + + (pools + (pools-list (error "At least one pool is required.")) + "Pools defined by this service.")) + +;;; ---- pool-configuration ---- + +(define-configuration php-fpm-pool + (name + (section (error "pool name is required")) + "Name for this pool.") + + ;; TODO: prefix (think about whether we want it or not) + + (user + (string "$pool") + ;; TODO: Rethink: The username of the user that owns the site files. + "The user to run the processes.") + (group + (string "$pool") ;; TODO: check what happens if empty + "Unix group of processes. If the group is not set, the default user's group +will be used.") + + (listen + (string "/var/run/php-fpm-$pool.sock") + "The address on which to accept FastCGI requests. +Valid syntaxes are: + 'ip.add.re.ss:port' - to listen on a TCP socket to a specific IPv4 + address on a specific port; + '[ip:6:addr:ess]:port' - to listen on a TCP socket to a specific IPv6 + address on a specific port; + 'port' - to listen on a TCP socket to all addresses + (IPv6 and IPv4-mapped) on a specific port; + '/path/to/unix/socket' - to listen on a unix socket.") + + (listen.owner + (string "nginx") + "The system user that will be the owner of the PHP-FPM socket. This should +be the username of the webserver account, so that it can use the socket to +communicate with the PHP-FPM process. This account needs to exist before the +PHP-FPM process is started (the 'nginx' account is created by default on +GuixSD systems when using the nginx-service).") + + (listen.group + (string "nginx") + "The system group that will be the primary group of the PHP-FPM socket. +This should be the group that the webserver belongs to, so that it can use the +socket to communicate with the PHP-FPM process. This group needs to exist +before the PHP-FPM process is started (the 'nginx' group is created by default +on GuixSD systems when using the nginx-service).") + + (listen.mode + (string "0660") + "The default permissions applied to the PHP-FPM pool sockets.") + + ;; TODO: + ;; When POSIX Access Control Lists are supported you can set them using + ;; these options, value is a comma separated list of user/group names. + ;; When set, listen.owner and listen.group are ignored + ;(listen.acl-users '()) + ;(listen.acl-groups '()) + + ;; TODO: listen.allowed-clients = 127.0.0.1 + ;; TODO: listen.backlog = -1 + ;; TODO: process.priority = -19 + + (pm ;; TODO: nicer name: process-manager + (string "ondemand") + "Select the default way the PHP-FPM master process will manage pool child +processes. Possible values: 'static', 'dynamic' or 'ondemand'.") + + (pm.max-children ;; TODO: nicer name + (non-negative-integer 75) ;; TODO: processor-vcpus + "Maximum number of child processes to be created when pm is set to +'static' and the maximum number of child processes when pm is set to +'dynamic' or 'ondemand'.") + + (pm.start-servers ;; TODO: nicer name + (non-negative-integer 10) ;; TODO: processor-vcpus + "The number of pool child processes created at startup, used by the +'dynamic' management mode.") + + (pm.min-spare-servers ;; TODO: nicer name + (non-negative-integer 5) + "Number of minimum idle spare servers that should be kept around, +used by the 'dynamic' management mode.") + + (pm.max-spare-servers ;; TODO: nicer name + (non-negative-integer 20) ;; TODO: default to pm.max-children + "Number of maximum idle spare servers that should be kept around, +used by the 'dynamic' management mode.") + + (pm.process-idle-timeout ;; TODO: nicer name + (string "10s") + "Timeout in seconds for the PHP-FPM pool child processes, used by +the 'ondemand' management mode.") + + (pm.max-requests ;; TODO: nicer name + (non-negative-integer 500) + "Maximum number of requests after which PHP-FPM pool child +processes will be respawned.") + + (pm.status-path ;; TODO: nicer name + ;; FIXME: should be unset by default for security reasons + (optional-string "/php_status") + "Enable or disable pool status page in all PHP-FPM pools. If this value is +not set (#f), no URI will be recognized as a status page. The value must +start with a leading slash (/). You might need to configure the webserver to +allow access to this page as well. The value can be anything, but it may not +be a good idea to use the .php extension or it may conflict with a real PHP +file.") + + (ping.path ;; TODO: nicer name + ;; FIXME: should be unset by default for security reasons + (optional-string "/php_ping") + "The ping URI to call the monitoring page of FPM. If this value is not set, +no URI will be recognized as a ping page. This could be used to test +from outside that FPM is alive and responding, or to - create a graph +of FPM availability (rrd or such); - remove a server from a group if +it is not responding (load balancing); - trigger alerts for the +operating team (24/7). + +The value must start with a leading slash (/). The value can be +anything, but it may not be a good idea to use the .php extension or +it may conflict with a real PHP file.") + + (ping.response ;; TODO: nicer name + (string "pong") + "A string that defines the expected response of the 'ping' request.") + + (access.log + (optional-string "/var/log/php-fpm/$pool_access.log") + "The access log file for this PHP-FPM pool . Set to #f to disable +logging.") + + (access.format ;; TODO: should be privacy enhanced + (string "%R - %u %t \"%m %r%Q%q\" %s %f %{mili}d %{kilo}M %C%%") + "The access log format.") + + (slowlog + (string "/var/log/php-fpm/$pool_slow.log") + "The log file for slow requests. Note: slowlog is mandatory if +request_slowlog_timeout is set.") + + (request-slowlog-timeout + (string "0") + "The timeout for serving a single request after which a PHP backtrace will +be dumped to the 'slowlog' file. A value of '0s' means 'off'. Available +units: s(econds)(default), m(inutes), h(ours), or d(ays)") + + (request-terminate-timeout + (string "0") + "The timeout for serving a single request after which the worker +process will be killed. This option should be used when the +'max-execution-time' ini option does not stop script execution for +some reason. A value of '0' means 'off'. Available units: +s(econds)(default), m(inutes), h(ours), or d(ays)") + + (rlimit-files + (non-negative-integer 1024) ; TODO: Check; what if the default is set to 0? + "Maximum number of opened file descriptors.") + + (rlimit-core + (non-negative-integer 0) ; TODO: Check; what if the default is set to 0? + ;; TODO: Possible Values: 'unlimited' or an integer greater or equal to 0 + "Specify maximum size of the 'core' files.") + + (chroot + (optional-string #f) + "Chroot to this directory at the start. This value must be defined as an +absolute path. When this value is not set, chroot is not used. +Note: you can prefix with '$prefix' to chroot to the pool prefix or one +of its subdirectories. If the pool prefix is not set, the global prefix +will be used instead. +Note: chrooting is a great security feature and should be used whenever + possible. However, all PHP paths will be relative to the chroot + (error_log, sessions.save_path, ...).") + + (chdir + (string "/") + "Chdir to this directory at the start.") + + (catch-workers-output + (boolean #f) + "Redirect worker stdout and stderr into main error log. If not set, stdout +and stderr will be redirected to /dev/null according to FastCGI specs. Note: +on highloaded environement, this can cause some delay in the page process +time (several ms).") + + (clear-env + (boolean #t) + "Clear environment in FPM workers. Prevents arbitrary environment +variables from reaching FPM worker processes by clearing the environment in +workers before env vars specified in this pool configuration are added. +Setting to #f will make all environment variables available to PHP code +via getenv(), $_ENV and $_SERVER.") + + (security.limit-extensions + (space-separated-string-list '(".php")) + "Limits the extensions of the main script FPM will allow to parse. +This can prevent configuration mistakes on the web server side. You +should only limit FPM to .php extensions to prevent malicious users to +use other extensions to exectute php code. Note: set an empty list to +allow all extensions.") + + ;; TODO: environment: + ;; These values are taken from the nginx example configuration: + ;; ; Pass environment variables + ;; env[HOSTNAME] = $HOSTNAME + ;; env[PATH] = /usr/local/bin:/usr/bin:/bin + ;; env[TMP] = /tmp + ;; env[TMPDIR] = /tmp + ;; env[TEMP] = /tmp + + ;; host-specific php ini settings here (from the nginx example configuration) + ;; php_admin_value[open_basedir] = /var/www/DOMAINNAME/htdocs:/tmp + + ;; TODO: php_flag[ key ] = value + ;; TODO: php_value[ key ] = value + ;; TODO: php_admin_flag[ key ] = value + ;; TODO: php_admin_value[ key ] = value +) + +;;; --- obaque configs --- + +(define-configuration opaque-php-ini + (php + (package php) + "The php package to use.") + + (string + (string (configuration-missing-field 'opaque-php-ini 'string)) + "The contents of the @code{php.ini} to use.")) + +(define-configuration opaque-php-fpm-configuration + (string + (string (configuration-missing-field 'opaque-fpm 'string)) + "The contents of the @code{php-fpm.conf} to use.")) + +(define-configuration opaque-php-fpm-pool + (name + (section (configuration-missing-field 'opaque-php-fpm-pool 'name)) + "The FPM pool name.") + (string + (string (configuration-missing-field 'opaque-php-fpm-pool 'string)) + "The contents of the @code{pool} to use.")) + +;;; --- service definition --- + +(define %php-fpm-activation + ;; Activation gexp. + #~(begin + (use-modules (guix build utils)) + (define (mkdir-p/perms directory owner perms) + (mkdir-p directory) + (chown directory (passwd:uid owner) (passwd:gid owner)) + (chmod directory perms)) + (let ((root (getpwnam "root"))) + (mkdir-p/perms "/etc/php/fpm" root #o755) + (mkdir-p/perms "/var/log/php-fpm" root #o700) + ) + ;; TODO: Check configuration file syntax. + ;; How can both the php-ini-file and the fpm-config-file be passed here? + ;; (system* (string-append #$php-pkg "/sbin/php-fpm") + ;; "--test" + ;; "--php-ini" #$php-ini-file + ;; "--fpm-config" #$fpm-config-file) + )) + +(define (php-fpm-shepherd-service configs) + "Return a list of for PHP-INI and FPM-CONFIG." + (let* ((php-ini (car configs)) + (fpm-config (car (cdr configs))) + (fpm-config-str + (cond + ((opaque-php-fpm-configuration? fpm-config) + (opaque-php-fpm-configuration-string fpm-config)) + (else + (with-output-to-string + (lambda () + (serialize-configuration fpm-config + php-fpm-configuration-fields)))))) + (php-ini-str + (cond + ((opaque-php-ini? php-ini) + (opaque-php-ini-string php-ini)) + (else + (with-output-to-string + (lambda () + (serialize-configuration php-ini + php-ini-fields)))))) + (fpm-config-file (plain-file "php-fpm.conf" fpm-config-str)) + (php-ini-file (plain-file "php.ini" php-ini-str)) + (php-pkg (if (opaque-php-ini? php-ini) + (opaque-php-ini-php php-ini) + (php-ini-php php-ini)))) + (list (shepherd-service + (documentation "Run the PHP-FPM server.") + (provision '(php-fpm)) + (requirement '(networking)) + (start #~(make-forkexec-constructor + (list (string-append #$php-pkg "/sbin/php-fpm") + "--php-ini" #$php-ini-file + "--pid" "/var/run/php-fpm.pid" + "--fpm-config" #$fpm-config-file) + ;; Wait for the PID file. + #:pid-file "/var/run/php-fpm.pid")) + (stop #~(make-kill-destructor)))))) + + +(define php-fpm-service-type + (service-type (name 'php-fpm) + (extensions + (list (service-extension shepherd-root-service-type + php-fpm-shepherd-service) + ;; TODO: Collect the user ids from the pools + ;; How can this be done? + ;;(service-extension account-service-type + ;; (const %php-accounts)) + (service-extension activation-service-type + (const %php-fpm-activation)))))) + +(define* (php-fpm-service #:key (config (php-fpm-configuration)) + (php-ini (php-ini))) + (validate-configuration php-ini + (if (opaque-php-ini? php-ini) + opaque-php-ini-fields + php-ini-fields)) + (validate-configuration config + (if (opaque-php-fpm-configuration? config) + opaque-php-fpm-configuration-fields + php-fpm-configuration-fields)) + (service php-fpm-service-type (list php-ini config))) + + +;; --testt --fpm-config /tmp/fpm.conf + +;; /tmp/fpm.conf +;; ;;[global] +;; pid = /run/php-fpm.pid +;; error_log = /tmp/php-fpm.log +;; +;; [www] +;; include = /tmp/pool-defaults.conf +;; ;pm.status_path = False +;; user = hartmut +;; pm.max_children = 7 +;; catch_workers_output = On +;; +;; ---- +;; /tmp/pool-defaults.conf +;; +;; pm = dynamic +;; pm.max_children = 10 +;; pm.min_spare_servers = 1 +;; pm.max_spare_servers = 5 +;; +;; listen = /tmp/fpm/$pool.sock -- 2.7.4