diff --git a/deploy.yaml b/deploy.yaml index da9b6e9..bc3d789 100644 --- a/deploy.yaml +++ b/deploy.yaml @@ -2,5 +2,13 @@ - hosts: jellyfin roles: - acme.sh - - nginx - - jellyfin + - role: nginx + nginx_dependent_service: jellyfin + - role: jellyfin + +- hosts: calendar + roles: + - acme.sh + - radicale + - role: nginx + nginx_dependent_service: radicale diff --git a/inventory.yaml b/inventory.yaml index f6010d7..3734e1b 100644 --- a/inventory.yaml +++ b/inventory.yaml @@ -3,3 +3,6 @@ proxmox_vms: jellyfin: ansible_host: 10.0.1.8 ansible_user: root + calendar: + ansible_host: 10.0.1.20 + ansible_user: root diff --git a/main.tofu b/main.tofu index 743c681..eae238c 100644 --- a/main.tofu +++ b/main.tofu @@ -51,6 +51,7 @@ resource "proxmox_virtual_environment_file" "cloud_config" { file_name = "user-data-cloud-config.yaml" } } + resource "proxmox_virtual_environment_vm" "jellyfin" { node_name = "homelab-one" 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" + } +} + diff --git a/roles/radicale/files/config b/roles/radicale/files/config new file mode 100644 index 0000000..f7e5cfd --- /dev/null +++ b/roles/radicale/files/config @@ -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 = + +# 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/' +#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 diff --git a/roles/radicale/files/radicale.conf b/roles/radicale/files/radicale.conf new file mode 100644 index 0000000..4f45ac0 --- /dev/null +++ b/roles/radicale/files/radicale.conf @@ -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; + } + + } + diff --git a/roles/radicale/files/radicale.container b/roles/radicale/files/radicale.container new file mode 100644 index 0000000..7cd2b39 --- /dev/null +++ b/roles/radicale/files/radicale.container @@ -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 + diff --git a/roles/radicale/files/users b/roles/radicale/files/users new file mode 100644 index 0000000..3764637 --- /dev/null +++ b/roles/radicale/files/users @@ -0,0 +1 @@ +calendar:$2y$10$geRB3AZiWrODsNiTMXBZA.b//5nwVoIN/tTQ6NB.bYqnz4y97C6pW diff --git a/roles/radicale/tasks/main.yaml b/roles/radicale/tasks/main.yaml new file mode 100644 index 0000000..ad9b72d --- /dev/null +++ b/roles/radicale/tasks/main.yaml @@ -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