weblate-guix/weblate-service.scm

415 lines
14 KiB
Scheme

(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>
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))))