(define-module (weblate-service) #:use-module (weblate) #:use-module (guix gexp) #:use-module (guix utils) #:use-module (guix build python-build-system) #:use-module (guix packages) #:use-module (gnu services) #:use-module (gnu services web) #:use-module (gnu packages version-control) #:use-module (gnu packages web) #:use-module (gnu packages python) #:use-module (gnu packages ssh) #:use-module (gnu packages databases) #:use-module (gnu services shepherd) #:use-module (guix records) #:use-module (ice-9 match) #:use-module (gnu system shadow) #:export (weblate-configuration weblate-configuration? weblate-service-type)) (define-record-type* weblate-configuration make-weblate-configuration weblate-configuration? (settings-file weblate-configuration-settings-file) (user weblate-configuration-user) (group weblate-configuration-group) (listen weblate-configuration-listen) (root weblate-configuration-root) (uwsgi-listen weblate-configuration-uwsgi-listen)) ;; (define %weblate-accounts ;; (list (user-group (name "weblate") (system? #t)) ;; (user-account ;; (name "weblate") ;; (group "weblate") ;; (system? #t) ;; (comment "Weblate server user") ;; (home-directory "/var/empty") ;; (shell (file-append shadow "/sbin/nologin"))))) (define (weblate-accounts config) (let ((user (weblate-configuration-user config)) (group (weblate-configuration-group config))) (list (user-group (name group) (system? #t)) ;; FIXME: change default group in uwsgi.ini (user-group (name "weblate") (system? #t)) (user-account (name user) (group group) (system? #t) (comment "Weblate service user") (home-directory "/var/empty") ;; (shell (file-append shadow "/sbin/nologin")) )))) (define weblate-service-type (service-type (name 'weblate) (description "Run WEBLATE") (extensions (list (service-extension nginx-service-type weblate-nginx-service) (service-extension account-service-type weblate-accounts) (service-extension activation-service-type weblate-activation) (service-extension shepherd-root-service-type weblate-shepherd-services))))) (define (weblate-shepherd-services config) (list (weblate-initial-database-setup-service config) (uwsgi-weblate-service config) (celery-weblate-service config))) ;; TODO: ;; - Failed to find git-http-backend, the git exporter will not work. ;; - /var/lib/weblate/cache/fonts/ ;; - /var/lib/weblate/home/.gitconfig ;; https://develop.sentry.dev/self-hosted/ (define (weblate-initial-database-setup-service config) (define start-gexp #~(lambda () (let ((pid (primitive-fork)) (postgres (getpwnam "postgres"))) (if (eq? pid 0) (dynamic-wind (const #t) (lambda () (setgid (passwd:gid postgres)) (setuid (passwd:uid postgres)) (primitive-exit (if (and (zero? (system* #$(file-append postgresql "/bin/createuser") "--superuser" "weblate")) (zero? (system* #$(file-append postgresql "/bin/createdb") "-O" "weblate" "weblate"))) 0 1))) (lambda () (primitive-exit 1))) (zero? (cdr (waitpid pid))))) (let ((pid (primitive-fork)) (weblate (getpwnam "weblate"))) (if (eq? pid 0) (dynamic-wind (const #t) (lambda () (setgid (passwd:gid weblate)) (setuid (passwd:uid weblate)) (primitive-exit (begin (setenv "PYTHONPATH" #$(weblate-configuration-root config)) (setenv "DJANGO_SETTINGS_MODULE" "guix.settings") (setenv "PATH" (string-append #$(file-append git) "/bin/")) (if (and (zero? (system* #$(file-append python-weblate "/bin/weblate") "migrate" "--no-input")) (zero? (system* #$(file-append python-weblate "/bin/weblate") "createadmin" "--password" "default-password" ;; graceful manage existing admin ;; "--update" ))) 0 1)))) (lambda () (primitive-exit 1))) (zero? (cdr (waitpid pid))))))) (shepherd-service (requirement '(postgres)) (provision '(weblate-initial-database-setup)) (start start-gexp) (stop #~(const #f)) (respawn? #f) (one-shot? #t) (documentation "Setup Weblate database."))) (define (weblate-activation config) ;; Activation gexp. #~(begin (use-modules (guix build utils)) (let* ((user #$(weblate-configuration-user config)) (group #$(weblate-configuration-group config)) (root #$(weblate-configuration-root config)) (settings-file #$(weblate-configuration-settings-file config))) (when (not (file-exists? root)) (mkdir-p root) (let* ((user (getpwnam user)) (group (getpwnam group))) (for-each (lambda (dir) (mkdir-p (string-append root "/" dir))) '("guix" "ssh" "home" "celery" "backups" "cache" "cache/fonts")) (for-each (lambda (file) ;; nginx needs to serve static files (chmod file #o750) (chown file (passwd:uid user) (passwd:gid group))) (find-files root #:directories? #t)))) ;; Delete stale pid files (let ((pid-dir (string-append root "/celery/pids")) (user (getpwnam user)) (group (getpwnam group))) (when (file-exists? pid-dir) (delete-file-recursively pid-dir)) (mkdir pid-dir) (chmod pid-dir #o750) (chown pid-dir (passwd:uid user) (passwd:gid group))) (let ((pid-dir (string-append root "/home")) (user (getpwnam user)) (group (getpwnam group))) (when (file-exists? pid-dir) (delete-file-recursively pid-dir)) (mkdir pid-dir) (chmod pid-dir #o750) (chown pid-dir (passwd:uid user) (passwd:gid group))) (setenv "DATA_DIR" root) (let ((guix-dir (string-append root "/guix")) (user (getpwnam user)) (group (getpwnam group))) (when (file-exists? guix-dir) (delete-file-recursively guix-dir)) (mkdir guix-dir) (call-with-output-file (string-append root "/guix/__init__.py") (const #t)) (chmod guix-dir #o750) (chmod (string-append root "/guix/__init__.py") #o750) (chown guix-dir (passwd:uid user) (passwd:gid group)) ;; TODO: This is copied from the touch implementation somewhere (copy-file settings-file (string-append root "/guix/settings.py"))) (copy-file #$(file-append python-weblate "/lib/python" (version-major+minor (package-version python)) "/site-packages/weblate/examples/weblate.uwsgi.ini") #$(string-append (weblate-configuration-root config) "/uwsgi.ini")) (substitute* #$(string-append (weblate-configuration-root config) "/uwsgi.ini") ;; TODO: Change this too ;; uid = weblate ;; gid = weblate (("socket\\s+=.*" all) (string-append "socket = " #$(weblate-configuration-uwsgi-listen config) "\n"))) (delete-file (string-append root "/guix/settings.py")) (copy-file #$(weblate-configuration-settings-file config) (string-append root "/guix/settings.py")) (let ((user (getpwnam user)) (group (getpwnam group))) (for-each (lambda (file) (let ((file (string-append #$(weblate-configuration-root config) file))) (chmod file #o750) (chown file (passwd:uid user) (passwd:gid group)))) '("/guix/settings.py" "/guix/__init__.py" "/uwsgi.ini"))) (setenv "PYTHONPATH" #$(weblate-configuration-root config)) (setenv "DJANGO_SETTINGS_MODULE" "guix.settings") (setenv "GI_TYPELIB_PATH" ;; FIXE: use the correct path "/run/current-system/profile/lib/girepository-1.0") (let ((pid (primitive-fork)) (weblate (getpwnam "weblate"))) (if (eq? pid 0) (dynamic-wind (const #t) (lambda () (setgid (passwd:gid weblate)) (setuid (passwd:uid weblate)) (primitive-exit (if (system* #$(file-append python-weblate "/bin/weblate") "collectstatic" "--noinput") 0 1))) (lambda () (primitive-exit 1))) (zero? (cdr (waitpid pid))))) (let ((user (getpwnam user)) (group (getpwnam group))) (for-each (lambda (file) ;; nginx needs to serve static files (chmod file #o750) (chown file (passwd:uid user) (passwd:gid group))) (find-files "static" #:directories? #t)) ;; Be sure that private key permissions are right (let ((private-key (string-append root "/ssh/id_rsa"))) (chmod private-key #o600) (chown private-key (passwd:uid user) (passwd:gid group))))))) (define (weblate-nginx-service config) (let ((listen (weblate-configuration-listen config)) (root (weblate-configuration-root config))) (list (nginx-server-configuration (listen listen) (server-name '("weblate")) (root root) (locations (list (nginx-location-configuration (uri "~ ^/favicon.ico$") (body `(,(string-append "alias " root "/static/favicon.ico;") "expires 30d;"))) (nginx-location-configuration (uri "/static/") (body `(,(string-append "alias " root "/static/;") "expires 30d;"))) (nginx-location-configuration (uri "/media/") (body `(,(string-append "alias " root "/media/;") "expires 30d;"))) (nginx-location-configuration (uri "/") (body `(,#~(string-append "include " '#$nginx "/share/nginx/conf/uwsgi_params;") "# Needed for long running operations in" "# admin interface" "uwsgi_read_timeout 3600;" "# Adjust based to uwsgi configuration:" "# uwsgi_pass unix:///run/uwsgi/app/weblate/socket;" ,(string-append "uwsgi_pass " (weblate-configuration-uwsgi-listen config) ";")))))) (try-files (list "$uri" "$uri/index.html")))))) (define (uwsgi-weblate-service config) (shepherd-service (documentation "Run uwsgi weblate service.") (provision '(weblate-uwsgi)) (requirement '(networking)) (start #~(make-forkexec-constructor '(#$(file-append uwsgi "/bin/uwsgi") #$(string-append (weblate-configuration-root config) "/uwsgi.ini")) #:environment-variables `(,(string-append "PYTHONPATH=" #$(weblate-configuration-root config)) "DJANGO_SETTINGS_MODULE=guix.settings" "GIT_SSL_CAINFO=/etc/ssl/certs/ca-certificates.crt" "LC_ALL=en_US.utf8" "LANG=en_US.utf8" "GI_TYPELIB_PATH=/run/current-system/profile/lib/girepository-1.0") ;; ;; #:environment-variables ;; (list (string-append "PYTHON_PATH=" root)) #:user #$(weblate-configuration-user config))) (stop #~(make-kill-destructor)))) ;; FIXME: this is an hack to get a celery program that knows where to ;; find weblate dependencies (define python-celery-for-weblate (package (inherit python-celery) (propagated-inputs (list (append (package-inputs python-celery) `("python-weblate" ,python-weblate)))))) (define (celery-weblate-service config) (shepherd-service (documentation "Run celery weblate service. Restart with RESTART.") (provision '(weblate-celery)) ;; TODO: abstract the celery call and put it here according to ;; https://docs.weblate.org/en/latest/admin/install.html#background-tasks-using-celery ;; (actions '((shepherd-action ;; (name 'reststart) ;; (documentation "Restart celery runners") ;; (procedure #~(lambda (running . args) ;; (format #t "Hello, friend! arguments: ~s\n" ;; args) ;; #t))))) (requirement '(networking)) ;redis? (auto-start? #t) (start #~(lambda _ ;; celery is forking (let ((pid (primitive-fork)) (weblate (getpwnam "weblate"))) (if (eq? pid 0) (dynamic-wind (const #t) (lambda () (setgid (passwd:gid weblate)) (setuid (passwd:uid weblate)) (setenv "GIT_SSL_CAINFO" "/etc/ssl/certs/ca-certificates.crt") (primitive-exit (if (zero? (system* #$(file-append python-celery-for-weblate "/bin/celery") "multi" "start" ;; Processes to start "celery" "notify" "backup" "memory" "translate" ;; must be under a folder which is writable by weblate! (string-append "--pidfile=" #$(weblate-configuration-root config) "/celery/pids/weblate-%n.pid") (string-append "--logfile=" #$(weblate-configuration-root config) "/celery/weblate-%n%I.log") ;; FIXME: pass as param! (string-append "--loglevel=" "INFO") "-A" "weblate.utils" "--beat:celery" "--queues:celery=celery" "--prefetch-multiplier:celery=4" "--queues:notify=notify" "--prefetch-multiplier:notify=10" "--queues:memory=memory" "--prefetch-multiplier:memory=10" "--queues:translate=translate" "--prefetch-multiplier:translate=4" "--concurrency:backup=1" "--queues:backup=backup" "--prefetch-multiplier:backup=2")) 0 1))) (lambda () (primitive-exit 1))) (zero? (cdr (waitpid pid))))) #:environment-variables ;; FIXME: use python-weblate store path instead of this `(,(string-append "PYTHONPATH=" (string-join (list ;; for settings path #$(weblate-configuration-root config) ;; FIXME: Replace the hack above ;; (python-celery-for-weblate) with the right list ;; of search paths. This include weblate path and ;; all weblate dependencies ) ":")) "GIT_SSL_CAINFO=/etc/ssl/certs/ca-certificates.crt" "DJANGO_SETTINGS_MODULE=guix.settings" ;; Internal Weblate variable to indicate we're running inside Celery "CELERY_WORKER_RUNNING=1") ;; #:environment-variables ;; '( ;; ;; "LC_ALL=en_US.utf8" "LANG=en_US.utf8" ;; "GI_TYPELIB_PATH=/run/current-system/profile/lib/girepository-1.0") ;; ;; #:environment-variables ;; (list (string-append "PYTHON_PATH=" root)) #:user #$(weblate-configuration-user config) #:group #$(weblate-configuration-group config))) (stop #~(make-kill-destructor))))