;;; genenetwork-machines --- Guix configuration for genenetwork machines ;;; Copyright © 2022–2024 Arun Isaac ;;; Copyright © 2022–2024 Frederick Muriuki Muriithi ;;; Copyright © 2024 Munyoki Kilyungi ;;; Copyright © 2024 John Nduli ;;; ;;; This file is part of genenetwork-machines. ;;; ;;; genenetwork-machines 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. ;;; ;;; genenetwork-machines 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 genenetwork-machines. If not, see ;;; . (use-modules (gnu) ((gn packages genenetwork) #:select (genenetwork2 genenetwork3 gn-auth gn-libs)) (gn services databases) ((gn packages guile) #:select (gn-guile)) (gnu build linux-container) ((gnu packages admin) #:select (shepherd shadow)) ((gnu packages base) #:select (gnu-make tar)) ((gnu packages bash) #:select (bash)) ((gnu packages bioinformatics) #:select (ccwl) #:prefix guix:) ((gnu packages certs) #:select (nss-certs)) ((gnu packages check) #:select (python-pylint)) ((gnu packages ci) #:select (laminar)) ((gnu packages compression) #:select (gzip)) ((gnu packages databases) #:select (mariadb virtuoso-ose)) ((gnu packages gnupg) #:select (guile-gcrypt)) ((gnu packages graphviz) #:select (graphviz)) ((gnu packages guile) #:select (guile-3.0 guile-git guile-zlib)) ((gnu packages guile-xyz) #:select (guile-dbd-mysql guile-dbi guile-dsv guile-hashing guile-ini guile-lib guile-libyaml guile-smc guile-xapian)) ((gnu packages guile-xyz) #:select (guile-sparql) #:prefix guix:) ((gnu packages haskell-apps) #:select (shellcheck)) ((gnu packages python-check) #:select (python-mypy)) ((gnu packages python-web) #:select (gunicorn)) ((gnu packages rdf) #:select (raptor2)) ((gnu packages tls) #:select (openssl)) ((gnu packages version-control) #:select (git-minimal)) ((gnu packages version-control) #:select (libgit2-1.3) #:prefix guix:) ((gnu packages web) #:select (tissue) #:prefix guix:) (gnu services ci) (gnu services databases) (gnu services mcron) (gnu services shepherd) ((gnu services web) #:select (nginx-server-configuration nginx-location-configuration)) (gnu system file-systems) (guix build-system gnu) (guix channels) (guix ci) (guix git-download) (guix least-authority) ((guix licenses) #:prefix license:) (guix modules) (guix packages) (guix profiles) (guix records) (guix search-paths) (guix store) (guix utils) (forge acme) (forge cgit) (forge forge) (forge laminar) (forge nginx) (forge socket) (forge tissue) (forge utils) (forge webhook) (srfi srfi-1) (ice-9 match)) ;; guix-daemon socket of the host shared inside the container (define %guix-daemon-uri "/var/host-guix/daemon-socket/socket") (define %default-guix-channel-with-substitutes (channel-with-substitutes-available %default-guix-channel "https://ci.guix.gnu.org")) ;; We cannot refer to sudo in the store since that sudo does not have ;; the setuid bit set. See "(guix) Setuid Programs". (define sudo "/run/setuid-programs/sudo") (define (manifest-cons package onto-manifest) "Return a manifest with PACKAGE and all packages in ONTO-MANIFEST." (manifest (cons (package->manifest-entry package) (manifest-entries onto-manifest)))) (define (manifest-cons* . args) "ARGS is of the form (PACKAGES ... ONTO-MANIFEST). Return a manifest with PACKAGES and all packages in ONTO-MANIFEST." (let ((packages (drop-right args 1)) (onto-manifest (last args))) (manifest (append (map package->manifest-entry packages) (manifest-entries onto-manifest))))) (define (import-module? name) "Return #t if NAME, a list of symbols, denotes a module that should be imported into G-expressions." ;; Allow all guix-forge, genenetwork or guix modules. (match name (((or 'forge 'genenetwork) _ ...) #t) (name (guix-module-name? name)))) (define-record-type* genenetwork-configuration make-genenetwork-configuration genenetwork-configuration? (gn2-repository genenetwork-configuration-gn2-repository (default "https://github.com/genenetwork/genenetwork2")) (gn3-repository genenetwork-configuration-gn3-repository (default "https://github.com/genenetwork/genenetwork3")) (gn-auth-repository genenetwork-configuration-gn-auth-repository (default "https://git.genenetwork.org/gn-auth")) (gn-libs-repository genenetwork-configuration-gn-libs-repository (default "https://git.genenetwork.org/gn-libs")) (gn2-port genenetwork-configuration-gn2-port (default 8082)) (gn3-port genenetwork-configuration-gn3-port (default 8083)) (gn-auth-port genenetwork-configuration-gn-auth-port (default 8084)) (gn2-secrets genenetwork-configuration-gn2-secrets (default "/etc/genenetwork")) (gn3-secrets genenetwork-configuration-gn3-secrets (default "/etc/genenetwork/gn3-secrets.py")) (gn-auth-secrets genenetwork-configuration-gn-auth-secrets (default "/etc/genenetwork")) (genotype-files genenetwork-configuration-genotype-files (default "/var/genenetwork/genotype-files")) (sparql-endpoint genenetwork-configuration-sparql-endpoint (default "http://localhost:8081/sparql")) (data-directory genenetwork-data-directory (default "/var/genenetwork")) (xapian-db-path genenetwork-xapian-db-path (default "/var/genenetwork/xapian")) (auth-db-path genenetwork-auth-db-path (default "/export/data/genenetwork-sqlite/auth.db")) (llm-db-path genenetwork-llm-db-path (default "/export/data/genenetwork-sqlite/llm.db")) (gn-guile-port genenetwork-configuration-gn-guile-port (default 8091)) (gn-doc-git-checkout genenetwork-configuration-gn-doc-git-checkout (default "/export/data/gn-docs"))) ;;; ;;; guix-bioinformatics ;;; (define guix-bioinformatics-project (forge-project (name "guix-bioinformatics") (repository "/home/git/public/guix-bioinformatics") (description "Bioinformatics packages for GNU Guix") (ci-jobs (let ((channels (list (channel (name 'gn-bioinformatics) (url "https://git.genenetwork.org/guix-bioinformatics") (branch "master"))))) (list (forge-laminar-job (name "guix-bioinformatics") (run (guix-channel-job-gexp channels #:variables '() #:guix-daemon-uri %guix-daemon-uri))) (forge-laminar-job (name "guix-bioinformatics-all-packages") (run (guix-channel-job-gexp channels #:verbose? #false #:guix-daemon-uri %guix-daemon-uri)))))))) ;;; ;;; genenetwork ;;; (define (genenetwork2-tests config test-command) "Return a G-expression that runs TEST-COMMAND for genenetwork2 described by CONFIG, a object. TEST-COMMAND is a list of strings specifying the command to be executed." (match-record config (gn2-repository gn3-repository gn-libs-repository gn3-port genotype-files) (with-imported-modules '((guix build utils)) (with-packages (list bash coreutils git-minimal nss-certs) #~(begin (use-modules (guix build utils) (srfi srfi-26)) (define (hline) "Print a horizontal line 50 '=' characters long." (display (make-string 50 #\=)) (newline) (force-output)) (define (show-head-commit) (hline) (invoke "git" "log" "--max-count" "1") (hline)) (define (call-with-temporary-directory proc) (let ((tmp-dir (mkdtemp "/tmp/gn.XXXXXX"))) (dynamic-wind (const #t) (cut proc tmp-dir) (cut delete-file-recursively tmp-dir)))) (invoke "git" "clone" "--depth" "1" #$gn3-repository) (with-directory-excursion "genenetwork3" (show-head-commit)) (invoke "git" "clone" "--depth" "1" #$gn-libs-repository) (with-directory-excursion "gn-libs" (show-head-commit)) (invoke "git" "clone" "--depth" "1" #$gn2-repository) (with-directory-excursion "genenetwork2" (show-head-commit)) ;; This is a dummy SERVER_PORT to placate ;; bin/genenetwork2. TODO: Fix bin/genenetwork2 so that ;; this is not needed. (setenv "SERVER_PORT" "8080") ;; Use a profile with all dependencies except ;; genenetwork3. (setenv "GN2_PROFILE" #$(profile (content (package->development-manifest genenetwork2)) (allow-collisions? #t))) ;; Set GN3_PYTHONPATH to the latest genenetwork3. (setenv "GN3_PYTHONPATH" (string-append (getcwd) "/genenetwork3")) (setenv "GN_PROXY_URL" "http://genenetwork.org/gn3-proxy/") (setenv "GN3_LOCAL_URL" (string-append "http://localhost:" (number->string #$gn3-port))) (setenv "GENENETWORK_FILES" #$genotype-files) (setenv "HOME" "/tmp") (setenv "SQL_URI" "mysql://webqtlout:webqtlout@localhost/db_webqtl?unix_socket=/run/mysqld/mysqld.sock") (chdir "genenetwork2") ;; XXXX: FIXME: R/Qtl tests fail because files are generated in ;; the "/tmp" directory. Currently, "/tmp" is mapped by gn2/gn3 ;; so tests will fail because of permission issues. (call-with-temporary-directory (lambda (tmp-dir) (setenv "TMPDIR" tmp-dir) (apply invoke '#$test-command)))))))) (define %xapian-directory "/export/data/genenetwork-xapian") (define (build-xapian-index-gexp project) "Return a G-expression that builds and installs a Xapian index using genenetwork3 source from the latest commit of @var{project}." (with-imported-modules '((guix build utils)) (with-manifest (manifest-cons* git-minimal nss-certs (package->development-manifest genenetwork3)) #~(begin (use-modules (guix build utils) (srfi srfi-26)) (invoke "git" "clone" "--depth" "1" #$(forge-project-repository project) ".") (let ((xapian-directory #$%xapian-directory) (xapian-build-directory (string-append #$%xapian-directory "/build"))) (dynamic-wind (const #t) (lambda () ;; Build xapian index. (setenv "PYTHONPATH" (getcwd)) (invoke "./scripts/index-genenetwork" "create-xapian-index" xapian-build-directory "mysql://webqtlout:webqtlout@localhost/db_webqtl?unix_socket=/run/mysqld/mysqld.sock" "http://localhost:9082/sparql") ;; Stop genenetwork3, replace old xapian index and ;; start genenetwork3. (dynamic-wind (cut invoke #$sudo #$(file-append shepherd "/bin/herd") "stop" "genenetwork3") (lambda () (for-each (lambda (file) (rename-file file (string-append xapian-directory "/" (basename file)))) (find-files xapian-build-directory))) (cut invoke #$sudo #$(file-append shepherd "/bin/herd") "start" "genenetwork3"))) (cut delete-file-recursively xapian-build-directory))))))) ;; G-expression that is configured in cron to trigger a rebuild of the ;; xapian index (define build-xapian-index-cron-gexp (with-imported-modules '((guix build utils)) (with-manifest (manifest-cons* git-minimal nss-certs (package->development-manifest genenetwork3)) #~(begin (use-modules (guix build utils) (srfi srfi-26)) (define (call-with-temporary-directory proc) (let ((tmp-dir (mkdtemp "/tmp/xapian-index-cron.XXXXXX"))) (dynamic-wind (const #t) (cut proc tmp-dir) (cut delete-file-recursively tmp-dir)))) (call-with-temporary-directory (lambda (gn3-dir) (invoke "git" "clone" "--depth" "1" "https://github.com/genenetwork/genenetwork3.git" gn3-dir) (setenv "PYTHONPATH" gn3-dir) ;; Only trigger a laminar build when the data is modified. (when (and (not (file-exists? (string-append #$%xapian-directory "/build"))) (zero? (status:exit-val (system* (string-append gn3-dir "/scripts/index-genenetwork") "is-data-modified" #$%xapian-directory "mysql://webqtlout:webqtlout@localhost/db_webqtl?unix_socket=/run/mysqld/mysqld.sock" "http://localhost:9082/sparql")))) (setenv "LAMINAR_REASON" "Nightly xapian index rebuild") (invoke #$(file-append laminar "/bin/laminarc") "queue" "genenetwork3-build-xapian-index")))))))) (define (genenetwork-projects config) "Return forge projects for genenetwork described by CONFIG, a object." (match-record config (gn2-repository gn3-repository gn-auth-repository gn-libs-repository gn2-port) (list (forge-project (name "genenetwork2") (repository gn2-repository) (ci-jobs (list (forge-laminar-job (name "genenetwork2") (run (genenetwork2-tests config (list "sh" "bin/genenetwork2" "./gn2/default_settings.py" "-c" "-m" "pytest"))) ;; If unit tests pass, redeploy genenetwork2 and ;; trigger Mechanical Rob. (after (with-imported-modules '((guix build utils)) #~(begin (use-modules (guix build utils)) (when (string=? (getenv "RESULT") "success") (invoke #$sudo #$(file-append shepherd "/bin/herd") "restart" "genenetwork2") (invoke #$(file-append laminar "/bin/laminarc") "queue" "genenetwork2-mechanical-rob")))))) (forge-laminar-job (name "genenetwork2-mechanical-rob") (run (genenetwork2-tests config (list "sh" "bin/genenetwork2" "./gn2/default_settings.py" "-c" "test/requests/test-website.py" "--all" (string-append "http://localhost:" (number->string gn2-port))))) (trigger? #f)))) (ci-jobs-trigger 'webhook)) (forge-project (name "genenetwork3") (repository gn3-repository) (ci-jobs (list (forge-laminar-job (name "genenetwork3") (run (guix-channel-job-gexp (list (channel (name 'genenetwork3) (url (forge-project-repository this-forge-project)) (branch "main"))) #:variables (list (variable-specification (module '(genenetwork3-package)) (name 'genenetwork3))) #:guix-daemon-uri %guix-daemon-uri)) ;; If tests run successfully, redeploy ;; genenetwork3 and trigger genenetwork2 tests. (after (with-imported-modules '((guix build utils)) #~(begin (use-modules (guix build utils)) (when (string=? (getenv "RESULT") "success") (invoke #$sudo #$(file-append shepherd "/bin/herd") "restart" "genenetwork3") (invoke #$(file-append laminar "/bin/laminarc") "queue" "genenetwork2")))))) (forge-laminar-job (name "genenetwork3-all-tests") (run (guix-channel-job-gexp (list (channel (name 'genenetwork3) (url (forge-project-repository this-forge-project)) (branch "main"))) #:variables (list (variable-specification (module '(genenetwork3-package)) (name 'genenetwork3-all-tests))) #:guix-daemon-uri %guix-daemon-uri))) (forge-laminar-job (name "genenetwork3-build-xapian-index") (run (build-xapian-index-gexp this-forge-project)) (trigger? #f)))) (ci-jobs-trigger 'webhook)) (forge-project (name "gn-libs") (repository gn-libs-repository) (ci-jobs (list (forge-laminar-job (name "gn-libs") (run (guix-channel-job-gexp (list (channel (name 'gn-libs) (url (forge-project-repository this-forge-project)) (branch "main"))) #:variables (list (variable-specification (module '(gn-libs)) (name 'gn-libs))) #:guix-daemon-uri %guix-daemon-uri))))) (ci-jobs-trigger 'webhook)) (forge-project (name "gn-auth") (repository gn-auth-repository) (ci-jobs (list (forge-laminar-job (name "gn-auth") (run (guix-channel-job-gexp (list (channel (name 'gn-auth) (url (forge-project-repository this-forge-project)) (branch "main"))) #:variables (list (variable-specification (module '(gn-auth)) (name 'gn-auth))) #:guix-daemon-uri %guix-daemon-uri)) ;; If unit tests pass, restart the auth server. (after (with-imported-modules '((guix build utils)) #~(begin (use-modules (guix build utils)) (when (string=? (getenv "RESULT") "success") (invoke #$sudo #$(file-append shepherd "/bin/herd") "restart" "gn-auth") (invoke #$(file-append laminar "/bin/laminarc") "queue" "genenetwork2")))))) (forge-laminar-job (name "gn-auth-all-tests") (run (guix-channel-job-gexp (list (channel (name 'gn-auth) (url (forge-project-repository this-forge-project)) (branch "main"))) #:variables (list (variable-specification (module '(gn-auth)) (name 'gn-auth-all-tests))) #:guix-daemon-uri %guix-daemon-uri))))) (ci-jobs-trigger 'webhook))))) (define (genenetwork2-cd-gexp config) "Return a G-expression that runs the latest genenetwork2 development server described by CONFIG, a object." (match-record config (gn2-repository gn3-repository gn2-port gn3-port gn2-secrets genotype-files) (with-packages (list coreutils git-minimal gunicorn nss-certs) (with-imported-modules '((guix build utils)) #~(begin (use-modules (guix build utils) (ice-9 match)) (define (hline) "Print a horizontal line 50 '=' characters long." (display (make-string 50 #\=)) (newline) (force-output)) (define (show-head-commit) (hline) (invoke "git" "log" "--max-count" "1") (hline)) ;; Clone the latest genenetwork2 and genenetwork3 ;; repositories. (invoke "git" "clone" "--depth" "1" #$gn2-repository) (with-directory-excursion "genenetwork2" (show-head-commit)) (invoke "git" "clone" "--depth" "1" #$gn3-repository) (with-directory-excursion "genenetwork3" (show-head-commit)) ;; Override the genenetwork3 used by genenetwork2. (setenv "GN3_PYTHONPATH" (string-append (getcwd) "/genenetwork3")) ;; Set other environment variables required by ;; genenetwork2. (setenv "GN2_PROFILE" #$(profile (content (package->development-manifest genenetwork2)) (allow-collisions? #t))) (setenv "GN2_SETTINGS" #$(mixed-text-file "gn2.conf" "GN2_SECRETS=\"" gn2-secrets "/gn2-secrets.py\"\n" "AI_SEARCH_ENABLED=True\n" "GN3_LOCAL_URL=\"" (string-append "http://localhost:" (number->string gn3-port)) "\"\n" "GN_SERVER_URL=\"https://cd.genenetwork.org/api3/\"\n" "AUTH_SERVER_URL=\"https://auth-cd.genenetwork.org/\"\n" "SQL_URI=\"mysql://webqtlout:webqtlout@localhost/db_webqtl?unix_socket=/run/mysqld/mysqld.sock\"\n" "SSL_PRIVATE_KEY=\"" gn2-secrets "/gn2-ssl-private-key.pem\"\n" "AUTH_SERVER_SSL_PUBLIC_KEY=\"" gn2-secrets "/gn-auth-ssl-public-key.pem\"\n")) ;; Start genenetwork2. (with-directory-excursion "genenetwork2" (invoke #$(file-append bash "/bin/sh") "bin/genenetwork2" "gn2/default_settings.py" "-gunicorn-prod"))))))) (define (genenetwork3-cd-gexp config) "Return a G-expression that runs the latest genenetwork3 development server described by CONFIG, a object." (match-record config (gn3-repository gn3-port gn3-secrets sparql-endpoint data-directory xapian-db-path auth-db-path llm-db-path) (with-manifest (package->development-manifest genenetwork3) (with-packages (list git-minimal nss-certs) (with-imported-modules '((guix build utils)) #~(begin (use-modules (guix build utils) (ice-9 match)) (define (hline) "Print a horizontal line 50 '=' characters long." (display (make-string 50 #\=)) (newline) (force-output)) (define (show-head-commit) (hline) (invoke "git" "log" "--max-count" "1") (hline)) ;; Clone the latest genenetwork3 repository. (invoke "git" "clone" "--depth" "1" #$gn3-repository) ;; Configure genenetwork3. (setenv "GN3_CONF" #$(mixed-text-file "gn3.conf" "SPARQL_ENDPOINT=\"" sparql-endpoint "\"\n" "DATA_DIR=\"" data-directory "\"\n" "AUTH_SERVER_URL=\"https://auth-cd.genenetwork.org/\"\n" "XAPIAN_DB_PATH=\"" xapian-db-path "\"\n" "AUTH_DB=\"" auth-db-path "\"\n" "LLM_DB_PATH=\"" llm-db-path "\"\n")) (setenv "HOME" "/tmp") (setenv "GN3_SECRETS" #$gn3-secrets) (setenv "RSCRIPT" #$(file-append (profile (content (package->development-manifest genenetwork3)) (allow-collisions? #t)) "/bin/Rscript")) ;; Run genenetwork3. (with-directory-excursion "genenetwork3" (show-head-commit) (invoke #$(file-append gunicorn "/bin/gunicorn") "-b" #$(string-append "localhost:" (number->string gn3-port)) "--workers" "8" "gn3.app:create_app()" ;; gunicorn's default 30 second timeout is ;; insufficient for Fahamu AI endpoints and ;; results in worker timeout errors. "--timeout" "1200")))))))) (define (gn-auth-cd-gexp config) "Return a G-expression that runs the latest gn-auth development server described by CONFIG, a object." (match-record config (gn-auth-repository gn-auth-port auth-db-path gn-auth-secrets) (with-manifest (package->development-manifest gn-auth) (with-packages (list git-minimal nss-certs) (with-imported-modules '((guix build utils)) #~(begin (use-modules (guix build utils) (ice-9 match)) (define (hline) "Print a horizontal line 50 '=' characters long." (display (make-string 50 #\=)) (newline) (force-output)) (define (show-head-commit) (hline) (invoke "git" "log" "--max-count" "1") (hline)) ;; Clone the latest gn-auth repository. (invoke "git" "clone" "--depth" "1" #$gn-auth-repository) ;; Configure gn-auth. (setenv "GN_AUTH_CONF" #$(mixed-text-file "gn-auth.conf" "AUTH_DB=\"" auth-db-path "\"\n" "GN_AUTH_SECRETS=\"" gn-auth-secrets "/gn-auth-secrets.py\"\n" "CLIENTS_SSL_PUBLIC_KEYS_DIR=\"" gn-auth-secrets "/clients-public-keys\"\n" "SSL_PRIVATE_KEY=\"" gn-auth-secrets "/gn-auth-ssl-private-key.pem\"\n")) (setenv "HOME" "/tmp") (setenv "AUTHLIB_INSECURE_TRANSPORT" "true") ;; Run gn-auth. (with-directory-excursion "gn-auth" (show-head-commit) (invoke #$(file-append gunicorn "/bin/gunicorn") "-b" #$(string-append "localhost:" (number->string gn-auth-port)) "--workers" "8" "gn_auth.wsgi:app")))))))) (define (gn-guile-gexp gn-guile-port) (with-imported-modules '((guix build utils)) #~(begin (use-modules (guix build utils)) (let ((current-repo-path (string-append (getcwd) "/gn-docs"))) (when (file-exists? current-repo-path) (delete-file-recursively current-repo-path)) (setenv "CURRENT_REPO_PATH" current-repo-path) (invoke #$(file-append git-minimal "/bin/git") "clone" "--depth" "1" (getenv "CGIT_REPO_PATH"))) (invoke #$(file-append gn-guile "/bin/gn-guile") (number->string #$gn-guile-port))))) (define (genenetwork-shepherd-services config) "Return shepherd services to run the genenetwork development server described by CONFIG, a object." (match-record config (gn2-port gn3-port gn-auth-port genotype-files data-directory xapian-db-path gn2-secrets auth-db-path gn-auth-secrets llm-db-path gn-doc-git-checkout gn-guile-port) (list (shepherd-service (documentation "Run gn-guile server.") (provision '(gn-guile)) (requirement '(networking)) (modules '((ice-9 match) (srfi srfi-1))) (start (let* ((gn-guile-settings `(("CGIT_REPO_PATH" ,gn-doc-git-checkout) ("LC_ALL" "en_US.UTF-8") ("GIT_COMMITTER_NAME" "genenetwork") ("GIT_COMMITTER_EMAIL" "no-reply@git.genenetwork.org")))) #~(make-forkexec-constructor (list #$(least-authority-wrapper (program-file "gn-guile" (gn-guile-gexp gn-guile-port)) #:name "gn-guile-pola-wrapper" #:preserved-environment-variables (map first gn-guile-settings) #:mappings (list (file-system-mapping (source gn-doc-git-checkout) (target source) (writable? #t))) #:namespaces (delq 'net %namespaces)) "127.0.0.1" #$(number->string gn-guile-port)) #:user "genenetwork" #:group "genenetwork" #:environment-variables (map (match-lambda ((spec value) (string-append spec "=" value))) '#$gn-guile-settings) #:log-file "/var/log/cd/gn-guile.log"))) (stop #~(make-kill-destructor))) (shepherd-service (documentation "Run GeneNetwork 2 development server.") (provision '(genenetwork2)) ;; FIXME: The genenetwork2 service should depend on redis. (requirement '(networking genenetwork3)) (modules '((guix search-paths) (ice-9 match) (srfi srfi-1))) (start (let* ((gn2-manifest (packages->manifest (list genenetwork2))) (gn2-profile (profile (content gn2-manifest) (allow-collisions? #t))) (gn2-settings `(("SERVER_PORT" ,(number->string gn2-port)) ("GENENETWORK_FILES" ,genotype-files) ("HOME" "/tmp") ("LC_ALL" "en_US.UTF-8") ("NO_REDIS" "no-redis") ("RUST_BACKTRACE" "1")))) (with-imported-modules (source-module-closure '((guix search-paths))) #~(make-forkexec-constructor (list #$(least-authority-wrapper (program-file "genenetwork2" (genenetwork2-cd-gexp config)) #:name "genenetwork2-pola-wrapper" #:preserved-environment-variables (append '("REQUESTS_CA_BUNDLE") (map first gn2-settings) (map search-path-specification-variable (manifest-search-paths gn2-manifest))) ;; If we mapped only the mysqld.sock ;; socket file, it would break when the ;; external mysqld server is restarted. #:mappings (list (file-system-mapping (source genotype-files) (target source)) (file-system-mapping (source "/run/mysqld") (target source) (writable? #t)) ;; XXXX: FIXME: R/Qtl generates ;; files in "/tmp" and ;; "/tmp/gn2". These files are ;; accessed by gn3 for R/Qtl ;; mapping (file-system-mapping (source "/tmp") (target source) (writable? #t)) (file-system-mapping (source gn2-secrets) (target source) (writable? #t))) #:namespaces (delq 'net %namespaces)) "127.0.0.1" #$(number->string gn2-port)) #:user "genenetwork" #:group "genenetwork" #:environment-variables (append '("REQUESTS_CA_BUNDLE=" #$(file-append gn2-profile "/etc/ssl/certs/ca-certificates.crt")) (map (match-lambda ((spec . value) (string-append (search-path-specification-variable spec) "=" value))) (evaluate-search-paths (map sexp->search-path-specification '#$(map search-path-specification->sexp (manifest-search-paths gn2-manifest))) (list #$gn2-profile))) (map (match-lambda ((spec value) (string-append spec "=" value))) '#$gn2-settings)) #:log-file "/var/log/cd/genenetwork2.log")))) (stop #~(make-kill-destructor))) (shepherd-service (documentation "Run GeneNetwork 3 development server.") (provision '(genenetwork3)) (requirement '(networking)) (start #~(make-forkexec-constructor (list #$(least-authority-wrapper (program-file "genenetwork3" (genenetwork3-cd-gexp config)) #:name "genenetwork3-pola-wrapper" ;; If we mapped only the mysqld.sock ;; socket file, it would break when the ;; external mysqld server is restarted. #:mappings (list (file-system-mapping (source "/run/mysqld") (target source) (writable? #t)) ;; XXXX: FIXME: R/Qtl generates ;; files in "/tmp" and ;; "/tmp/gn2". These files are ;; accessed by gn3 for R/Qtl ;; mapping (file-system-mapping (source "/tmp") (target source) (writable? #t)) (file-system-mapping (source data-directory) (target source)) (file-system-mapping (source xapian-db-path) (target source)) (file-system-mapping (source "/etc/genenetwork/conf/gn3") (target source) (writable? #t)) (file-system-mapping (source auth-db-path) (target source) (writable? #t)) (file-system-mapping (source llm-db-path) (target source) (writable? #t))) #:namespaces (delq 'net %namespaces)) "127.0.0.1" #$(number->string gn3-port)) #:user "genenetwork" #:group "genenetwork" #:log-file "/var/log/cd/genenetwork3.log")) (stop #~(make-kill-destructor))) (shepherd-service (documentation "Run gn-auth development server.") (provision '(gn-auth)) (requirement '(networking)) (start #~(make-forkexec-constructor (list #$(least-authority-wrapper (program-file "gn-auth" (gn-auth-cd-gexp config)) #:name "gn-auth-pola-wrapper" ;; If we mapped only the mysqld.sock ;; socket file, it would break when the ;; external mysqld server is restarted. #:mappings (list (file-system-mapping (source "/run/mysqld") (target source) (writable? #t)) (file-system-mapping (source data-directory) (target source)) (file-system-mapping (source auth-db-path) (target source) (writable? #t)) (file-system-mapping (source gn-auth-secrets) (target source) (writable? #t))) #:namespaces (delq 'net %namespaces)) "127.0.0.1" #$(number->string gn-auth-port)) #:user "genenetwork" #:group "genenetwork" #:log-file "/var/log/cd/gn-auth.log")) (stop #~(make-kill-destructor)))))) (define %genenetwork-accounts (list (user-group (name "genenetwork") (system? #t)) (user-account (name "genenetwork") (group "genenetwork") (system? #t) (comment "GeneNetwork user") (home-directory "/var/empty") (shell (file-append shadow "/sbin/nologin"))))) (define (genenetwork-activation config) (match-record config (gn2-secrets gn3-secrets auth-db-path gn-auth-secrets) (with-imported-modules '((guix build utils)) #~(begin (use-modules (guix build utils)) ;; Set ownership of files. (for-each (lambda (file) (chown file (passwd:uid (getpw "genenetwork")) (passwd:gid (getpw "genenetwork")))) (cons* #$gn3-secrets (append (find-files #$gn2-secrets #:directories? #t) (find-files "/export/data/gn-docs" #:directories? #t) (find-files #$(dirname auth-db-path) #:directories? #t) (find-files #$gn-auth-secrets #:directories? #t)))) ;; Prevent other users from reading secret files. (for-each (lambda (file) (chmod file #o600)) (append (list #$gn3-secrets) (find-files #$gn2-secrets #:directories? #f) (find-files #$gn-auth-secrets #:directories? #f))))))) (define genenetwork-service-type (service-type (name 'genenetwork) (description "Run GeneNetwork development servers and CI.") (extensions (list (service-extension account-service-type (const %genenetwork-accounts)) (service-extension activation-service-type genenetwork-activation) (service-extension shepherd-root-service-type genenetwork-shepherd-services) (service-extension forge-service-type genenetwork-projects))) (default-value (genenetwork-configuration)))) ;;; ;;; transform-genenetwork-database ;;; ;; Unreleased version of ccwl that is required by ;; transform-genenetwork-database for its graphql library. (define ccwl (let ((commit "02677a508b407779f5991a230341e016deb7f69b") (revision "0")) (package (inherit guix:ccwl) (name "ccwl") (version (git-version (package-version guix:ccwl) revision commit)) (source (origin (method git-fetch) (uri (git-reference (url "https://github.com/arunisaac/ccwl") (commit commit))) (file-name (git-file-name name version)) (sha256 (base32 "1kxry8y0pibl0x789jrzqkkh2s59ajyinfvrgvd00gkbqldih82r"))))))) ;; guile-sparql tests are broken. Disable them temporarily. The issue ;; has been reported upstream at ;; https://github.com/roelj/guile-sparql/issues/6 (define guile-sparql (package (inherit guix:guile-sparql) (arguments `(#:tests? #f)))) ;; Temporarily package run64 here until it can be contributed to ;; upstream Guix. (define run64 (package (name "run64") (version "0.1.0") (source (origin (method git-fetch) (uri (git-reference (url "https://git.systemreboot.net/run64") (commit "a271723e3938b158aa6748e8befceb114b84c6df"))) (file-name (git-file-name name version)) (sha256 (base32 "1kdx8h2x5a4cp8azzy1v2xgyb2153sakb1wbi2rj5ygkpmasygbm")))) (build-system gnu-build-system) (arguments `(#:make-flags (list (string-append "prefix=" %output)) #:phases (modify-phases %standard-phases (delete 'configure)))) (home-page "https://run64.systemreboot.net") (synopsis "SRFI-64 test runner for Scheme") (description "run64 is a SRFI-64 test runner for Scheme.") (license license:gpl3+))) ;; Connection settings for Virtuoso and MySQL used to load data into Virtuoso (define %connection-settings "/etc/genenetwork/conf/gn-transform-database/conn.scm") ;; Path to where the data directory from which virtuoso loads all the files (define %virtuoso-data-dir "/var/lib/data") (define (transform-genenetwork-database-gexp connection-settings virtuoso-data-dir repository) (with-imported-modules '((guix build utils)) (with-packages (list ccwl git-minimal gnu-make guile-3.0 guile-dbd-mysql guile-dbi guile-hashing guile-libyaml guile-sparql guile-zlib nss-certs virtuoso-ose raptor2) #~(begin (use-modules (guix build utils) (srfi srfi-26) (ice-9 threads)) (setenv "LC_ALL" "en_US.UTF-8") (let ((build-directory (string-append #$virtuoso-data-dir "/build"))) ;; Only run this job if the build directory does not ;; exists. This ensures that no other process is ;; running this. (unless (file-exists? build-directory) (invoke "git" "clone" "--depth" "1" #$repository ".") (invoke "make" "-j" (number->string (current-processor-count))) (invoke "./generate-ttl-files.scm" "--settings" #$connection-settings "--output" build-directory) ;; First clear all the files in our virtuoso directory (for-each (lambda (file) (unless (string-suffix? "build" (dirname file)) (delete-file file))) (find-files #$virtuoso-data-dir ".ttl")) ;; Move data into the container's virtuoso data directory (copy-recursively build-directory #$virtuoso-data-dir) ;; Load RDF into virtuoso. (invoke "./pre-inst-env" "./load-rdf.scm" #$connection-settings) ;; Visualize schema and archive results. (invoke "./pre-inst-env" "./visualize-schema.scm" #$connection-settings) (invoke #$(file-append graphviz "/bin/dot") "-Tsvg" "sql.dot" (string-append "-o" (getenv "ARCHIVE") "/sql.svg")) (invoke #$(file-append graphviz "/bin/dot") "-Tsvg" "rdf.dot" (string-append "-o" (getenv "ARCHIVE") "/rdf.svg")) (delete-file-recursively build-directory))))))) (define transform-genenetwork-database-project (forge-project (name "transform-genenetwork-database") (repository "https://git.genenetwork.org/gn-transform-databases") (ci-jobs (list (forge-laminar-job (name "transform-genenetwork-database-tests") (run (guix-channel-job-gexp (list (channel (name 'transform-genenetwork-database) (url (forge-project-repository this-forge-project)) (branch "master")) %default-guix-channel-with-substitutes) #:guix-daemon-uri %guix-daemon-uri))) (forge-laminar-job (name "transform-genenetwork-database") (run (transform-genenetwork-database-gexp %connection-settings %virtuoso-data-dir "https://git.genenetwork.org/gn-transform-databases"))))) (ci-jobs-trigger 'webhook))) ;;; ;;; gn-gemtext-threads ;;; (define gn-gemtext-threads-project (forge-project (name "gn-gemtext-threads") (repository "https://github.com/genenetwork/gn-gemtext-threads/") (ci-jobs (list (forge-laminar-job (name "gn-gemtext-threads") (run (with-packages (list nss-certs openssl) (with-imported-modules '((guix build utils)) #~(begin (use-modules (guix build utils)) (setenv "LC_ALL" "en_US.UTF-8") (invoke #$(file-append tissue "/bin/tissue") "pull" "issues.genenetwork.org")))))))) (ci-jobs-trigger 'webhook))) ;;; ;;; guile-lmdb ;;; (define guile-lmdb-project (forge-project (name "guile-lmdb") (repository "https://github.com/aartaka/guile-lmdb") (ci-jobs (list (forge-laminar-job (name "guile-lmdb") (run (guix-channel-job-gexp (list (channel (name 'guile-lmdb) (url (forge-project-repository this-forge-project)) (branch "master")) %default-guix-channel-with-substitutes) #:guix-daemon-uri %guix-daemon-uri))))) (ci-jobs-trigger 'webhook))) ;;; ;;; guile-gsl ;;; (define guile-gsl-project (forge-project (name "guile-gsl") (repository "https://github.com/aartaka/guile-gsl") (ci-jobs (list (forge-laminar-job (name "guile-gsl") (run (guix-channel-job-gexp (list (channel (name 'guile-gsl) (url (forge-project-repository this-forge-project)) (branch "master")) %default-guix-channel-with-substitutes) #:guix-daemon-uri %guix-daemon-uri))))) (ci-jobs-trigger 'webhook))) ;;; ;;; guile-lapack ;;; (define guile-lapack-project (forge-project (name "guile-lapack") (repository "https://github.com/aartaka/guile-lapack") (ci-jobs (list (forge-laminar-job (name "guile-lapack") (run (guix-channel-job-gexp (list (channel (name 'guile-lapack) (url (forge-project-repository this-forge-project)) (branch "master")) %default-guix-channel-with-substitutes) #:guix-daemon-uri %guix-daemon-uri))))) (ci-jobs-trigger 'webhook))) ;;; ;;; operating-system definition ;;; (define (laminar-template-gexp issue-tracker-uri) "Return a G-expression that creates a custom Laminar template with a menu link to channels.scm and the issue tracker at ISSUE-TRACKER-URI." (with-imported-modules '((guix build utils)) #~(begin (use-modules (guix build utils)) (copy-file (string-append #$(package-source laminar) "/src/resources/index.html") #$output) (substitute* #$output (("Jobs" jobs-link) (string-append "CD" jobs-link "Issues" "channels.scm")))))) (define (install-laminar-template-gexp template) "Return a G-expression that installs custom laminar TEMPLATE." (with-imported-modules '((guix build utils)) #~(begin (use-modules (guix build utils)) (mkdir-p "/var/lib/laminar/custom") (switch-symlinks "/var/lib/laminar/custom/index.html" #$template)))) (define %ci-domain "ci.genenetwork.org") (define (cd-error-pages-gexp) "Return a G-expression that builds a directory with error pages for the GeneNetwork continuous deployment." (with-imported-modules '((guix build utils)) (with-extensions (list guile-lib) #~(begin (use-modules (guix build utils) (htmlprag)) (define (ci-badge job) `(div (a (@ (href ,(string-append "https://" #$%ci-domain "/jobs/" job))) (img (@ (src ,(string-append "https://" #$%ci-domain "/badge/" job ".svg"))))))) (define (page-sxml jobs) `(html (head (title "GeneNetwork CD down!")) (body (h1 "GeneNetwork CD is down!") (p "Is the CI red?") ,@(map ci-badge jobs)))) (mkdir-p (string-append #$output "/error")) (call-with-output-file (string-append #$output "/error/502.html") (lambda (port) (display "" port) (newline port) (display (sxml->html ;; Construct a 502 page pulling out CI job names using a ;; dummy default genenetwork configuration. (page-sxml '#$(append-map (lambda (project) (map forge-laminar-job-name (forge-project-ci-jobs project))) (genenetwork-projects (genenetwork-configuration))))) port) (newline port))))))) (define (channels-scm-gexp published-channel-names) "Return a G-expression that builds a directory with a channels.scm file to be served by the laminar reverse proxy. PUBLISHED-CHANNEL-NAMES is a list of names of channels which should be included in the channels.scm file." (with-imported-modules '((guix build utils)) #~(begin (use-modules (ice-9 pretty-print) (guix build utils)) (mkdir-p #$output) (call-with-output-file (string-append #$output "/channels.scm") (lambda (port) (pretty-print '#$`(list ,@(filter-map (lambda (channel) (and (memq (channel-name channel) published-channel-names) (channel->code channel))) (profile-channels ;; Infer profile from guix ;; executable used. (match (program-arguments) ((program . _) (dirname (dirname program))))))) port)))))) (define (development-server-reverse-proxy-server-block gn2-port gn3-port) "Return an object to reverse proxy the GeneNetwork development server. GN2-PORT and GN3-PORT are the ports GeneNetwork2 and GeneNetwork3 are listening on." (nginx-server-configuration (server-name '("cd.genenetwork.org")) (locations (list (nginx-location-configuration ;; Reverse proxy genenetwork2. (uri "/") (body (list (string-append "proxy_pass http://localhost:" (number->string gn2-port) ";") "proxy_set_header Host $host;" "proxy_set_header X-Forwarded-Proto $scheme;"))) (nginx-location-configuration ;; Reverse proxy genenetwork3. (uri "/api3") (body (list "rewrite /api3/(.*) /api/$1 break;" (string-append "proxy_pass http://localhost:" (number->string gn3-port) ";") "proxy_set_header Host $host;"))) (nginx-location-configuration (uri " /error/") (body (list #~(string-append "root " #$(computed-file "genenetwork-cd-error-pages" (cd-error-pages-gexp)) ";")))))) (raw-content (list "error_page 502 /error/502.html;")))) (define (laminar-reverse-proxy-server-block laminar-bind-http webhook-port published-channel-names) "Return an object to reverse proxy laminar. The nginx server will reverse proxy to laminar listening on LAMINAR-BIND-HTTP. WEBHOOK-PORT is the port the webhook server is listening on. PUBLISHED-CHANNEL-NAMES is a list of channel names for which a channels.scm should be published." (nginx-server-configuration (server-name (list %ci-domain)) (locations (list (nginx-location-configuration (uri "/") (body (list (string-append "proxy_pass http://" laminar-bind-http ";") ;; Disable proxy buffering in host's nginx. We ;; need this to allow Laminar's Server-Sent ;; Events to pass through. "proxy_pass_header X-Accel-Buffering;"))) ;; Reverse proxy webhook server. (nginx-location-configuration (uri "/hooks/") (body (list (string-append "proxy_pass http://localhost:" (number->string webhook-port) ";") "proxy_set_header Host $host;"))) ;; Publish the channels.scm used to build this container. (nginx-location-configuration (uri "= /channels.scm") (body (list #~(string-append "root " #$(computed-file "channels.scm" (channels-scm-gexp published-channel-names)) ";")))))))) ;; Port on which tissue is listening (define %tissue-port 9083) (define (tissue-reverse-proxy-server-block) "Return an object to reverse proxy tissue." (nginx-server-configuration (server-name '("issues.genenetwork.org")) (root "/var/lib/tissue/issues.genenetwork.org/website") (try-files (list "$uri" "$uri.html" "@tissue-search")) (locations (list (nginx-location-configuration (uri "@tissue-search") (body (list (string-append "proxy_pass http://localhost:" (number->string %tissue-port) ";") "proxy_set_header Host $host;"))))))) (define (gn-auth-reverse-proxy-server-block) "Return an object to reverse proxy gn-auth." (nginx-server-configuration (server-name '("auth-cd.genenetwork.org")) (locations (list (nginx-location-configuration (uri "/") (body (list (string-append "proxy_pass http://localhost:" (number->string %gn-auth-port) ";") "proxy_set_header Host $host;"))))))) (define set-build-directory-permissions-gexp (with-imported-modules '((guix build utils)) #~(begin (use-modules (guix build utils)) (for-each (lambda (file) (chown file (passwd:uid (getpw "laminar")) (passwd:gid (getpw "laminar")))) (append (find-files #$%xapian-directory #:directories? #t) (find-files #$%virtuoso-data-dir #:directories? #t)))))) ;; Port on which webhook is listening (define %webhook-port 9091) ;; Port on which genenetwork2 is listening (define %genenetwork2-port 9092) ;; Port on which genenetwork3 is listening (define %genenetwork3-port 9093) ;; Port on which gn-auth is listening (define %gn-auth-port 9094) ;; Port on which virtuoso's SPARQL endpoint is listening (define %virtuoso-sparql-port 9082) (operating-system (host-name "genenetwork-development") (timezone "UTC") (locale "en_US.utf8") (bootloader (bootloader-configuration (bootloader grub-bootloader) (targets (list "/dev/sdX")))) (file-systems %base-file-systems) (users %base-user-accounts) (packages %base-packages) (sudoers-file (mixed-text-file "sudoers" "@include " %sudoers-specification ;; Permit the laminar user to restart genenetwork2 ;; and genenetwork3. "\nlaminar ALL = NOPASSWD: " (file-append shepherd "/bin/herd") " restart genenetwork2, " (file-append shepherd "/bin/herd") " start genenetwork3, " (file-append shepherd "/bin/herd") " stop genenetwork3, " (file-append shepherd "/bin/herd") " restart genenetwork3," (file-append shepherd "/bin/herd") " start gn-auth, " (file-append shepherd "/bin/herd") " stop gn-auth, " (file-append shepherd "/bin/herd") " restart gn-auth\n" ;; Permit the acme user to restart nginx. "\nacme ALL = NOPASSWD: " (file-append shepherd "/bin/herd") " restart nginx\n")) (services (cons* (service forge-service-type (forge-configuration (projects (list transform-genenetwork-database-project gn-gemtext-threads-project guile-gsl-project guile-lapack-project guile-lmdb-project guix-bioinformatics-project)))) (service cgit-service-type (cgit-configuration (server-name "git.genenetwork.org") (repository-directory "/home/git/public"))) (service laminar-service-type (laminar-configuration (title "GeneNetwork CI") (bind-http "localhost:9089"))) (service mcron-service-type (mcron-configuration (jobs (list #~(job '(next-hour) #$(program-file "build-xapian-index-cron" build-xapian-index-cron-gexp) #:user "laminar") ;; Run cron once a week at midnight on Sunday morning ;; Verify using: https://crontab.guru/#0_0_*_*_0 #~(job "0 0 * * 0" #$(program-file "update-virtuoso" (transform-genenetwork-database-gexp %connection-settings %virtuoso-data-dir "https://git.genenetwork.org/gn-transform-databases")) #:user "laminar"))))) (simple-service 'install-laminar-template activation-service-type (install-laminar-template-gexp (computed-file "laminar-template.html" (laminar-template-gexp "https://issues.genenetwork.org")))) (service webhook-service-type (webhook-configuration (socket (forge-ip-socket (ip "127.0.0.1") (port %webhook-port))))) (service redis-service-type) (service virtuoso-service-type (virtuoso-configuration (number-of-buffers 4000000) (maximum-dirty-buffers 3000000) (server-port 9081) (dirs-allowed "/var/lib/data") (http-server-port %virtuoso-sparql-port))) (service genenetwork-service-type (genenetwork-configuration (gn2-port %genenetwork2-port) (gn3-port %genenetwork3-port) (gn-auth-port %gn-auth-port) (gn2-secrets "/etc/genenetwork/conf/gn2") (gn3-secrets "/etc/genenetwork/conf/gn3/secrets.py") (gn-auth-secrets "/etc/genenetwork/conf/gn-auth") (genotype-files "/export/data/genenetwork/genotype_files") (sparql-endpoint (string-append "http://localhost:" (number->string %virtuoso-sparql-port) "/sparql")) (data-directory "/export/data/genenetwork") (xapian-db-path %xapian-directory))) (simple-service 'set-build-directory-permissions activation-service-type set-build-directory-permissions-gexp) (service tissue-service-type (tissue-configuration (socket (forge-ip-socket (port %tissue-port))) (hosts (list (tissue-host (name "issues.genenetwork.org") (user "laminar") (upstream-repository "https://github.com/genenetwork/gn-gemtext-threads")))))) (service forge-nginx-service-type (forge-nginx-configuration (http-listen (forge-ip-socket (ip "0.0.0.0") (port 9080))) (https-listen (forge-ip-socket (ip "0.0.0.0") (port 9090))) (server-blocks (list (development-server-reverse-proxy-server-block %genenetwork2-port %genenetwork3-port) (laminar-reverse-proxy-server-block "localhost:9089" %webhook-port (list 'gn-bioinformatics)) (tissue-reverse-proxy-server-block) (gn-auth-reverse-proxy-server-block))))) (service acme-service-type (acme-configuration (email "arunisaac@systemreboot.net"))) %base-services)))