;;; genenetwork-machines --- Guix configuration for genenetwork machines ;;; Copyright © 2025 Munyoki Kilyungi ;;; ;;; 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 ;;; . ;;; This is the production genenetwork container currently deployed on ;;; tux04. (use-modules (gnu) (genenetwork services genenetwork) (gnu build linux-container) ((gn packages genenetwork) #:select (genenetwork2 genenetwork3 gn-auth gn-libs)) ((gnu packages admin) #:select (shepherd shadow)) ((gnu packages certs) #:select (nss-certs)) ((gnu packages bash) #:select (bash)) ((gnu packages check) #:select (python-pylint python-hypothesis)) ((gnu packages python-check) #:select (python-mypy python-mypy-extensions)) ((gnu packages python-web) #:select (gunicorn python-flask)) ((gnu packages version-control) #:select (git-minimal)) ((gn packages guile) #:select (gn-guile)) (gn services databases) (gnu services databases) (guix modules) (gnu services shepherd) (guix search-paths) (guix least-authority) (guix packages) (guix profiles) (guix records) (forge socket) (forge utils) (srfi srfi-1) (ice-9 match)) (define-record-type* genenetwork-configuration make-genenetwork-configuration genenetwork-configuration? (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/conf/gn2/secrets.py")) (gn3-secrets genenetwork-configuration-gn3-secrets (default "/etc/genenetwork/conf/gn3/secrets")) (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:7082/sparql")) (data-directory genenetwork-data-directory (default "/var/genenetwork")) (xapian-db-path genenetwork-xapian-db-path (default "/var/lib/xapian")) (auth-db-path genenetwork-auth-db-path (default "/var/genenetwork/auth.db")) (llm-db-path genenetwork-llm-db-path (default "/var/lib/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 "/var/lib/gn-docs"))) (define (genenetwork2-gexp config) "Return a G-expression that runs the latest genenetwork2 development server described by CONFIG, a object." (match-record config (gn2-port gn3-port 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)) ;; Override the genenetwork3 used by genenetwork2. (setenv "GN3_PYTHONPATH" "/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=\"/etc/genenetwork/conf/gn2/secrets.py\"\n" "AI_SEARCH_ENABLED=True\n" "GN3_LOCAL_URL=\"" (string-append "http://localhost:" (number->string gn3-port)) "\"\n" "GN_SERVER_URL=\"http://localhost:8083/api/\"\n" "AUTH_SERVER_URL=\"http://localhost:8084/\"\n" "SQL_URI=\"mysql://webqtlout:webqtlout@localhost/db_webqtl?unix_socket=/run/mysqld/mysqld.sock\"\n" "SSL_PRIVATE_KEY=\"/etc/genenetwork/conf/gn2/private.pem\"\n" "AUTH_SERVER_SSL_PUBLIC_KEY=\"/etc/genenetwork/conf/gn-auth/clients-public-keys/gn-auth.pem\"\n")) ;; Start genenetwork2. (with-directory-excursion "/genenetwork2" (invoke #$(file-append bash "/bin/sh") "bin/genenetwork2" "gn2/default_settings.py" "-gunicorn-dev"))))))) (define (genenetwork3-gexp config) "Return a G-expression that runs the latest genenetwork3 development server described by CONFIG, a object." (match-record config (gn3-port gn3-secrets sparql-endpoint data-directory xapian-db-path auth-db-path llm-db-path) (with-manifest (package->development-manifest genenetwork3) (with-imported-modules '((guix build utils)) #~(begin (use-modules (guix build utils) (ice-9 match)) ;; Configure genenetwork3. (setenv "GN3_CONF" #$(mixed-text-file "gn3.conf" "SPARQL_ENDPOINT=\"" sparql-endpoint "\"\n" "DATA_DIR=\"" data-directory "\"\n" "AUTH_SERVER_URL=\"http://localhost:8084/\"\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")) (setenv "FLASK_ENV" "development") (setenv "FLASK_DEBUG" "1") ;; Run genenetwork3. (with-directory-excursion "/genenetwork3" (invoke #$(file-append python-flask "/bin/flask") "run" #$(string-append "--port=" (number->string gn3-port))))))))) (define (gn-auth-gexp config) "Return a G-expression that runs the latest gn-auth development server described by CONFIG, a object." (match-record config (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)) ;; Configure gn-auth. (setenv "GN_AUTH_CONF" #$(mixed-text-file "gn-auth.conf" "LOGLEVEL=\"DEBUG\"\n" "SQL_URI=\"mysql://webqtlout:webqtlout@localhost/db_webqtl?unix_socket=/run/mysqld/mysqld.sock\"\n" "AUTH_DB=\"" auth-db-path "\"\n" "GN_AUTH_SECRETS=\"/etc/genenetwork/conf/gn-auth/secrets.py\"\n" "CLIENTS_SSL_PUBLIC_KEYS_DIR=\"/etc/genenetwork/conf/gn-auth/clients-public-keys/\"\n" "SSL_PRIVATE_KEY=\"/etc/genenetwork/conf/gn-auth/private.pem\"\n")) (setenv "HOME" "/tmp") (setenv "AUTHLIB_INSECURE_TRANSPORT" "true") ;; Run gn-auth. (with-directory-excursion "/gn-auth" (invoke #$(file-append gunicorn "/bin/gunicorn") "-b" #$(string-append "localhost:" (number->string gn-auth-port)) "--workers" "8" "gn_auth.wsgi:app")))))))) (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 "/var/lib/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 (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/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-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 "/genenetwork2") (target source) (writable? #t)) (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/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-gexp config)) #:name "genenetwork3-pola-wrapper" #:mappings (list (file-system-mapping (source "/genenetwork3") (target source) (writable? #t)) (file-system-mapping (source "/run/mysqld") (target source) (writable? #t)) (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/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-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 "/gn-auth") (target source) (writable? #t)) (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/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-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))) (default-value (genenetwork-configuration)))) (operating-system (host-name "genenetwork-work-container") (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) (sudoers-file (mixed-text-file "sudoers" "@include " %sudoers-specification "\nacme ALL = NOPASSWD: " (file-append shepherd "/bin/herd") " restart nginx\n")) (packages (cons* genenetwork2 python-hypothesis gn-auth python-mypy python-mypy-extensions python-pylint %base-packages)) (services (cons* (service virtuoso-service-type (virtuoso-configuration (server-port 7081) (http-server-port 7082) (dirs-allowed "/var/lib/virtuoso/data") (database-file "/var/lib/virtuoso/genenetwork-virtuoso.db") (transaction-file "/var/lib/virtuoso/genenetwork-virtuoso.trx"))) (service redis-service-type (redis-configuration (bind "127.0.0.1") (port 6379) (working-directory "/var/lib/redis"))) (service genenetwork-service-type) %base-services)))