feat: add radicale

This commit is contained in:
mustard 2025-11-03 12:07:18 +01:00
parent 9c7903ba01
commit 8886e06a3d
8 changed files with 575 additions and 2 deletions

View file

@ -2,5 +2,13 @@
- hosts: jellyfin - hosts: jellyfin
roles: roles:
- acme.sh - acme.sh
- nginx - role: nginx
- jellyfin nginx_dependent_service: jellyfin
- role: jellyfin
- hosts: calendar
roles:
- acme.sh
- radicale
- role: nginx
nginx_dependent_service: radicale

View file

@ -3,3 +3,6 @@ proxmox_vms:
jellyfin: jellyfin:
ansible_host: 10.0.1.8 ansible_host: 10.0.1.8
ansible_user: root ansible_user: root
calendar:
ansible_host: 10.0.1.20
ansible_user: root

View file

@ -51,6 +51,7 @@ resource "proxmox_virtual_environment_file" "cloud_config" {
file_name = "user-data-cloud-config.yaml" file_name = "user-data-cloud-config.yaml"
} }
} }
resource "proxmox_virtual_environment_vm" "jellyfin" { resource "proxmox_virtual_environment_vm" "jellyfin" {
node_name = "homelab-one" node_name = "homelab-one"
name = "jellyfin" name = "jellyfin"
@ -124,4 +125,76 @@ resource "proxmox_virtual_environment_vm" "jellyfin" {
} }
} }
resource "proxmox_virtual_environment_vm" "calendar" {
node_name = "homelab-one"
name = "calendar"
acpi = true
bios = "ovmf"
boot_order = ["scsi0"]
machine = "q35"
stop_on_destroy = true
scsi_hardware = "virtio-scsi-single"
operating_system {
type = "l26"
}
agent {
enabled = true
trim = true
}
efi_disk {
datastore_id = "spinny-zfs"
file_format = "raw"
type = "4m"
}
serial_device {}
vga {
type = "virtio"
}
tpm_state {
datastore_id = "spinny-zfs"
version = "v2.0"
}
cpu {
cores = 4
sockets = 1
type = "host"
}
memory {
dedicated = 1024
}
initialization {
datastore_id = "spinny-zfs"
user_data_file_id = proxmox_virtual_environment_file.cloud_config.id
}
# boot disk
disk {
cache = "none"
datastore_id = "spinny-zfs"
discard = "on"
file_id = "local:iso/Fedora-Cloud-Base-UEFI-UKI-42-1.1.x86_64.img"
interface = "scsi0"
iothread = true
replicate = false
size = 32
}
network_device {
bridge = "vmbr2"
vlan_id = 100
enabled = true
firewall = true
mac_address = "BC:24:11:21:6E:61"
}
}

363
roles/radicale/files/config Normal file
View file

@ -0,0 +1,363 @@
# -*- mode: conf -*-
# vim:ft=cfg
# Config file for Radicale - A simple calendar server
#
# Place it into /etc/radicale/config (global)
# or ~/.config/radicale/config (user)
#
# The current values are the default ones
[server]
# CalDAV server hostnames separated by a comma
# IPv4 syntax: address:port
# IPv6 syntax: [address]:port
# Hostname syntax (using "getaddrinfo" to resolve to IPv4/IPv6 adress(es)): hostname:port
# For example: 0.0.0.0:9999, [::]:9999, localhost:9999
#hosts = localhost:5232
hosts = 0.0.0.0:5232
# Max parallel connections
#max_connections = 8
# Max size of request body (bytes)
# In case of using a reverse proxy in front of check also there related option
#max_content_length = 100000000
# Socket timeout (seconds)
#timeout = 30
# SSL flag, enable HTTPS protocol
#ssl = False
# SSL certificate path
#certificate = /etc/ssl/radicale.cert.pem
# SSL private key
#key = /etc/ssl/radicale.key.pem
# CA certificate for validating clients. This can be used to secure
# TCP traffic between Radicale and a reverse proxy
#certificate_authority =
# SSL protocol, secure configuration: ALL -SSLv3 -TLSv1 -TLSv1.1
#protocol = (default)
# SSL ciphersuite, secure configuration: DHE:ECDHE:-NULL:-SHA (see also "man openssl-ciphers")
#ciphersuite = (default)
# script name to strip from URI if called by reverse proxy
#script_name = (default taken from HTTP_X_SCRIPT_NAME or SCRIPT_NAME)
[encoding]
# Encoding for responding requests
#request = utf-8
# Encoding for storing local collections
#stock = utf-8
[auth]
# Authentication method
# Value: none | htpasswd | remote_user | http_x_remote_user | dovecot | ldap | oauth2 | pam | denyall
#type = denyall
type = htpasswd
htpasswd_filename = /config/users
htpasswd_encryption = bcrypt
# Cache logins for until expiration time
#cache_logins = false
# Expiration time for caching successful logins in seconds
#cache_successful_logins_expiry = 15
## Expiration time of caching failed logins in seconds
#cache_failed_logins_expiry = 90
# URI to the LDAP server
#ldap_uri = ldap://localhost
# Base DN of the LDAP server to search for user accounts
#ldap_base = ##BASE_DN##
# Reader DN of the LDAP server; (needs read access to users and - if defined - groups)
#ldap_reader_dn = CN=ldapreader,CN=Users,##BASE_DN##
# Password of the reader DN (better: use 'ldap_secret_file'!)
#ldap_secret = ldapreader-secret
# Path to the file containing the password of the reader DN
#ldap_secret_file = /run/secrets/ldap_password
# Filter to search for the LDAP entry of the user to authenticate. It must contain '{0}' as placeholder for the login name.
#ldap_filter = (&(objectClass=person)(uid={0}))
# Attribute holding the value to be used as username after authentication
#ldap_user_attribute = cn
# Use ssl on the LDAP connection (DEPRECATED - use 'ldap_security'!)
#ldap_use_ssl = False
# Encryption mode to be used. Default: none; one of: none, tls, starttls
#ldap_security = none
# Certificate verification mode for tls & starttls. Default: REQUIRED; one of NONE, OPTIONAL, REQUIRED
#ldap_ssl_verify_mode = REQUIRED
# Path to the CA file in PEM format to certify the server certificate
#ldap_ssl_ca_file =
# Attribute in the user's LDAP entry to read the group memberships from; default: not set
#ldap_groups_attribute = memberOf
# Attribute in the group entries to read the group's members from, e.g. member; default: not set
#ldap_group_members_attribute = member
# Base DN to search for groups; only if it differs from 'ldap_base' and if 'ldap_group_members_attribute' is set
#ldap_group_base = ##GROUP_BASE_DN##
# Search filter to search for groups having the user DN found as member; only if 'ldap_group_members_attribute' is set
#ldap_group_filter = (objectclass=groupOfNames)
# Quirks for Authentik LDAP server: ignore modifyTimestamp and createTimestamp attributes
#ldap_ignore_attribute_create_modify_timestamp = false
# Connection type for dovecot authentication (AF_UNIX|AF_INET|AF_INET6)
# Note: credentials are transmitted in cleartext
#dovecot_connection_type = AF_UNIX
# The path to the Dovecot client authentication socket (eg. /run/dovecot/auth-client on Fedora). Radicale must have read / write access to the socket.
#dovecot_socket = /var/run/dovecot/auth-client
# Host of via network exposed dovecot socket
#dovecot_host = localhost
# Port of via network exposed dovecot socket
#dovecot_port = 12345
# Remote address source for authentication mechanisms (such as dovecot)
# that are passed this information.
#remote_ip_source = REMOTE_ADDR
# IMAP server hostname
# Syntax: address | address:port | [address]:port | imap.server.tld
#imap_host = localhost
# Secure the IMAP connection
# Value: tls | starttls | none
#imap_security = tls
# OAuth2 token endpoint URL
#oauth2_token_endpoint = <URL>
# PAM service
#pam_serivce = radicale
# PAM group user should be member of
#pam_group_membership =
# Htpasswd filename
#htpasswd_filename = /etc/radicale/users
# Htpasswd encryption method
# Value: plain | bcrypt | md5 | sha256 | sha512 | argon2 | autodetect
# bcrypt requires the installation of 'bcrypt' module.
# argon2 requires the installation of 'argon2-cffi' module.
#htpasswd_encryption = autodetect
# Enable caching of htpasswd file based on size and mtime_ns
#htpasswd_cache = False
# Incorrect authentication delay (seconds)
#delay = 1
# Message displayed in the client when a password is needed
#realm = Radicale - Password Required
# Convert username to lowercase, must be true for case-insensitive auth providers
#lc_username = False
# Strip domain name from username
#strip_domain = False
[rights]
# Rights backend
# Value: authenticated | owner_only | owner_write | from_file
#type = owner_only
# File for rights management from_file
#file = /etc/radicale/rights
# Permit delete of a collection (global)
#permit_delete_collection = True
# Permit overwrite of a collection (global)
#permit_overwrite_collection = True
# URL Decode the given username (when URL-encoded by the client - useful for iOS devices when using email address)
# urldecode_username = False
[storage]
# Storage backend
# Value: multifilesystem | multifilesystem_nolock
#type = multifilesystem
# Folder for storing local collections, created if not present
#filesystem_folder = /var/lib/radicale/collections
filesystem_folder = /data/collections
# Folder for storing cache of local collections, created if not present
# Note: only used in case of use_cache_subfolder_* options are active
# Note: can be used on multi-instance setup to cache files on local node (see below)
#filesystem_cache_folder = (filesystem_folder)
# Use subfolder 'collection-cache' for 'item' cache file structure instead of inside collection folder
# Note: can be used on multi-instance setup to cache 'item' on local node
#use_cache_subfolder_for_item = False
# Use subfolder 'collection-cache' for 'history' cache file structure instead of inside collection folder
# Note: use only on single-instance setup, will break consistency with client in multi-instance setup
#use_cache_subfolder_for_history = False
# Use subfolder 'collection-cache' for 'sync-token' cache file structure instead of inside collection folder
# Note: use only on single-instance setup, will break consistency with client in multi-instance setup
#use_cache_subfolder_for_synctoken = False
# Use last modifiction time (nanoseconds) and size (bytes) for 'item' cache instead of SHA256 (improves speed)
# Note: check used filesystem mtime precision before enabling
# Note: conversion is done on access, bulk conversion can be done offline using storage verification option: radicale --verify-storage
#use_mtime_and_size_for_item_cache = False
# Use configured umask for folder creation (not applicable for OS Windows)
# Useful value: 0077 | 0027 | 0007 | 0022
#folder_umask = (system default, usual 0022)
# Delete sync token that are older (seconds)
#max_sync_token_age = 2592000
# Skip broken item instead of triggering an exception
#skip_broken_item = True
# Command that is run after changes to storage, default is emtpy
# Supported placeholders:
# %(user)s: logged-in user
# %(cwd)s : current working directory
# %(path)s: full path of item
# %(to_path)s: full path of destination item (only set on MOVE request)
# %(request)s: request method
# Command will be executed with base directory defined in filesystem_folder
# For "git" check DOCUMENTATION.md for bootstrap instructions
# Example(test): echo \"user=%(user)s path=%(path)s cwd=%(cwd)s\"
# Example(test/json): echo \"hook-json {'user':'%(user)s', 'cwd':'%(cwd)s', 'path':'%(path)s', 'request':'%(request)s', 'to_path':'%(to_path)s'}\"
# Example(git): git add -A && (git diff --cached --quiet || git commit -m "Changes by \"%(user)s\"")
#hook =
# Create predefined user collections
#
# json format:
#
# {
# "def-addressbook": {
# "D:displayname": "Personal Address Book",
# "tag": "VADDRESSBOOK"
# },
# "def-calendar": {
# "C:supported-calendar-component-set": "VEVENT,VJOURNAL,VTODO",
# "D:displayname": "Personal Calendar",
# "tag": "VCALENDAR"
# }
# }
#
#predefined_collections =
[web]
# Web interface backend
# Value: none | internal
#type = internal
[logging]
# Threshold for the logger
# Value: debug | info | warning | error | critical
#level = info
# do not filter debug messages starting with 'TRACE'
#trace_on_debug = False
# filter debug messages starting with 'TRACE/<TOKEN>'
#trace_filter = ""
# Don't include passwords in logs
#mask_passwords = True
# Log bad PUT request content
#bad_put_request_content = False
# Log backtrace on level=debug
#backtrace_on_debug = False
# Log request header on level=debug
#request_header_on_debug = False
# Log request content on level=debug
#request_content_on_debug = False
# Log response content on level=debug
#response_content_on_debug = False
# Log rights rule which doesn't match on level=debug
#rights_rule_doesnt_match_on_debug = False
# Log storage cache actions on level=debug
#storage_cache_actions_on_debug = False
[headers]
# Additional HTTP headers
#Access-Control-Allow-Origin = *
[hook]
# Hook types
# Value: none | rabbitmq | email
#type = none
# dry-run (do not really trigger hook action)
#dryrun = False
# hook: rabbitmq
#rabbitmq_endpoint =
#rabbitmq_topic =
#rabbitmq_queue_type = classic
# hook: email
#smtp_server = localhost
#smtp_port = 25
#smtp_security = starttls
#smtp_ssl_verify_mode = REQUIRED
#smtp_username =
#smtp_password =
#from_email =
#mass_email = False
#new_or_added_to_event_template =
#deleted_or_removed_from_event_template =
#updated_event_template =
[reporting]
# When returning a free-busy report, limit the number of returned
# occurences per event to prevent DoS attacks.
#max_freebusy_occurrence = 10000

View file

@ -0,0 +1,38 @@
server
{
listen 8080;
listen [::]:8080;
server_name calendar.homelab0ne.xyz;
return 301 https://$host$request_uri;
}
server
{
# listen 8443 ssl proxy_protocol;
listen 8443 ssl;
# deny all;
# listen [::]:8443 ssl;
# listen [::]:8444 ssl proxy_protocol;
http2 on;
server_name calendar.homelab0ne.xyz;
client_max_body_size 20M;
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
ssl_protocols TLSv1.3 TLSv1.2;
location /
{
proxy_pass http://radicale:5232;
proxy_set_header Host $host;
# proxy_set_header X-Real-IP $proxy_protocol_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Protocol $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_buffering off;
}
}

View file

@ -0,0 +1,24 @@
[Unit]
Description=radicale.container
[Container]
ContainerName=radicale
RunInit=true
DropCapability=ALL
AddCapability=SETUID SETGID CHOWN KILL
Image=docker.io/tomsquest/docker-radicale
Network=frontend.network
Volume=/srv/radicale/config:/config:Z,ro
Volume=/srv/radicale/data:/data:Z
#PodmanArgs=--runtime runsc --security-opt label:disable
#Label=disable
AutoUpdate=registry
[Install]
WantedBy=multi-user.target default.target
[Service]
TasksMax=50
MemoryHigh=256M
Restart=always

View file

@ -0,0 +1 @@
calendar:$2y$10$geRB3AZiWrODsNiTMXBZA.b//5nwVoIN/tTQ6NB.bYqnz4y97C6pW

View file

@ -0,0 +1,63 @@
- name: Create radicale dir
ansible.builtin.file:
path: /srv/radicale
state: directory
mode: '0755'
- name: Create config dir if it doesn't exist
ansible.builtin.file:
path: /srv/radicale/config
state: directory
mode: '0755'
- name: Create data dir if it doesn't exist
ansible.builtin.file:
path: /srv/radicale/data
state: directory
mode: '0755'
- name: Copy over radicale.container file
ansible.builtin.copy:
src: ./files/radicale.container
dest: /etc/containers/systemd/radicale.container
owner: root
group: root
mode: '0644'
- name: Copy over radicale config
ansible.builtin.copy:
src: ./files/config
dest: /srv/radicale/config/config
owner: root
group: root
mode: '0644'
- name: Copy over radicale user config
ansible.builtin.copy:
src: ./files/users
dest: /srv/radicale/config/users
owner: root
group: root
mode: '0644'
- name: Copy over radicale nginx config
ansible.builtin.copy:
src: ./files/radicale.conf
dest: /srv/nginx/conf.d/radicale.conf
owner: root
group: root
mode: '0644'
- name: Run systemctl daemon-reload
ansible.builtin.systemd_service:
daemon_reload: true
- name: Start radicale container
ansible.builtin.systemd_service:
name: radicale.service
state: restarted
- name: Restart nginx
ansible.builtin.systemd_service:
name: nginx.service
state: restarted