From 8e985a84c02ffe189bf8718c1c16cd083b5a57ed Mon Sep 17 00:00:00 2001 From: vincent Date: Tue, 8 Oct 2019 20:18:14 +0200 Subject: [PATCH] init radicale role --- README.md | 33 ++++++++ defaults/main.yaml | 65 ++++++++++++++ files/rights.conf | 29 +++++++ handlers/main.yaml | 9 ++ meta/main.yaml | 22 +++++ tasks/main.yaml | 114 +++++++++++++++++++++++++ templates/etc/radicale/config.j2 | 141 +++++++++++++++++++++++++++++++ tests/test.yaml | 6 ++ 8 files changed, 419 insertions(+) create mode 100644 README.md create mode 100644 defaults/main.yaml create mode 100644 files/rights.conf create mode 100644 handlers/main.yaml create mode 100644 meta/main.yaml create mode 100644 tasks/main.yaml create mode 100644 templates/etc/radicale/config.j2 create mode 100644 tests/test.yaml diff --git a/README.md b/README.md new file mode 100644 index 0000000..498d981 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# Anarcho-Tech NYC: Radicale [![Build Status](https://travis-ci.org/AnarchoTechNYC/ansible-role-radicale.svg?branch=master)](https://travis-ci.org/AnarchoTechNYC/ansible-role-radicale) + +An [Ansible role](https://docs.ansible.com/ansible/latest/user_guide/playbooks_reuse_roles.html) for installing a [Radicale](http://radicale.org/) server. Notably, this role has been tested with [Raspbian](https://www.raspbian.org/) on [Raspberry Pi](https://www.raspberrypi.org/) hardware. This role's purpose is to make it simple to install a CalDAV and CardDAV server. + +# Configuring Radicale + +To configure your Radicale server instance, use the `radicale_config` dictionary. The keys in this dictionary map nearly one-to-one to the configuration directives described in [Radicale's Configuration documentation page](https://radicale.org/configuration/). Configuration directive groups are their own dictionaries, and directives that can accept more than one value are specified as a list. + +Some examples may prove helpful: + +1. Simple Radicale server with default for all values: + ```yaml + radicale_config: + ``` +1. Simple Radicale server bound to the local host only and listening on the alternative HTTP port: + ```yaml + radicale_config: + server: + hosts: + - addr: 127.0.0.1 + port: 8080 + ``` + +See the comments in the [`defaults/main.yaml`](defaults/main.yaml) file for additional details. + +# Adding or removing Radicale user accounts + +The `radicale_users` variable is a list containing dictionaries for each user account. Each user account dictionary in the list can have the following keys: + +* `name`: The name of the user account. This key is required. +* `password`: The password for this user account. It is recommended to encrypt this value with Ansible Vault. If this is omitted, the `bcrypt_hash` key is required. +* `bcrypt_hash`: Instead of supplying a password, you can supply a bcrypt hash of the password in `passlib` format. If this is omitted, the `password` key is required. +* `state`: Whether the user should exist (`present`) or not (`absent`). This key is optional. diff --git a/defaults/main.yaml b/defaults/main.yaml new file mode 100644 index 0000000..0088483 --- /dev/null +++ b/defaults/main.yaml @@ -0,0 +1,65 @@ +--- +radicale_server_username: radicale +radicale_server_home_dir: "/var/lib/{{ radicale_server_username }}" +radicale_service_state: started + +# See https://radicale.org/configuration/ +radicale_config: + server: + hosts: + - addr: 0.0.0.0 + port: 5232 + #daemon: true + #pid: /var/run/radicale/radicale.pid + #max_connections: 20 + #max_connections: 100000000 + #timeout: 30 + dns_lookup: false + #realm: Radicale Realm + # Consider TLS directives carefully before activating them. + #ssl: true + #certificate: "/etc/ssl/radicale.cert.pem" + #key: "/etc/ssl/radicale.key.pem" + #certificate_authority: + #protocol: PROTOCOL_TLSv1_2 + #ciphers: + #encoding: + #request: utf-8 + #stock: utf-8 + auth: + type: htpasswd + htpasswd_filename: "{{ radicale_server_home_dir }}/users.htpasswd" + htpasswd_encryption: bcrypt + delay: 1 + rights: + type: from_file + file: "{{ radicale_server_home_dir }}/rights.conf" + storage: + type: multifilesystem + filesystem_folder: "{{ radicale_server_home_dir }}/collections" + filesystem_locking: true + filesystem_fsync: true + # For an example of the `hook` directive in use, see + # http://radicale.org/versioning/ + #hook: + #web: + #type: internal + #headers: + #X-Extra-HTTP-Header: foo + #X-Another-Header: bar + #logging: + #debug: false + #mask_passwords: true + #full_environment: false + #config: "/etc/radicale/log.conf" + +# List of Radicale user information as a dictionary. +radicale_users: + - name: admin # The username. + password: admin # Their password. This should probably be vault-encrypted. + # As an alternative to a password, you can specify a bcrypt hash. + # Create this hash using the standard `htpasswd` utility, then + # paste it here. This method allows a user to generate a password + # for their account themselves, and then send you the hash rather + # than the plaintext. + #bcrypt_hash: "$2y$05$t31SnKFWj9UcMr5Y96cl3uBFkdhelqkZn77TnquIeVb9sriEByUPK" diff --git a/files/rights.conf b/files/rights.conf new file mode 100644 index 0000000..9d09141 --- /dev/null +++ b/files/rights.conf @@ -0,0 +1,29 @@ +################################################ +# Radicale user rights configuration file. # +# # +# See http://radicale.org/rights/ for details. # +################################################ + +## The user "admin" can read and write any collection. +#[admin] +#user = admin +#collection = .* +#permission = rw + +# Authenticated users can list (discover) their own collections. +[owner-discover] +user = .+ +collection = ^%(login)s$ +permission = rw + +# Authenticated users can read and write their own collections. +[owner-write] +user = .+ +collection = ^%(login)s/.* +permission = rw + +# Everyone can read the root collection +[read] +user = .* +collection = +permission = r diff --git a/handlers/main.yaml b/handlers/main.yaml new file mode 100644 index 0000000..337a005 --- /dev/null +++ b/handlers/main.yaml @@ -0,0 +1,9 @@ +--- +- name: Reload systemd. + systemd: + daemon_reload: true + +- name: Restart Radicale. + service: + name: radicale + state: restarted diff --git a/meta/main.yaml b/meta/main.yaml new file mode 100644 index 0000000..a4c7236 --- /dev/null +++ b/meta/main.yaml @@ -0,0 +1,22 @@ +--- +dependencies: [] +galaxy_info: + role_name: radicale + author: AnarchoTechNYC + description: Provision a Radicale CalDAV/CardDAV server in a number of small- to medium-sized deployments. + company: Eat The Rich, Inc. + license: AGPL-3.0-or-later # SPDX tag from https://spdx.org/licenses/ + min_ansible_version: 2.7 + platforms: + - name: Raspbian + versions: + - all + - name: Debian + versions: + - 9 + galaxy_tags: + - CalDAV + - CardDAV + - calendar + - contacts + - addressbook diff --git a/tasks/main.yaml b/tasks/main.yaml new file mode 100644 index 0000000..1613ec6 --- /dev/null +++ b/tasks/main.yaml @@ -0,0 +1,114 @@ +--- +- name: Install Radicale package dependencies. + apt: + name: "{{ packages }}" + vars: + packages: + - python3 + - python3-pip + - python3-setuptools + - apache2-utils + # These three are for Ansible itself to run on the managed host. + - python-setuptools + - python-passlib + - python-bcrypt + +- name: Install Radicale Python dependencies. + pip: + executable: pip3 # Radicale requires Python 3.3 or greater. + name: "{{ item }}" + state: present + loop: + - passlib + - bcrypt + +- name: Create Radicale system user. + user: + name: "{{ radicale_server_username }}" + system: true + home: "{{ radicale_server_home_dir }}" + shell: "/bin/false" + state: present + +- name: Install Radicale. + pip: + executable: pip3 # Radicale requires Python 3.3 or greater. + name: radicale + state: present + +- name: Create Radicale configuration directory. + file: + path: /etc/radicale + state: directory + +- name: Write Radicale configuration file. + template: + src: etc/radicale/config.j2 + dest: /etc/radicale/config + notify: + - Restart Radicale. + +- name: Write Radicale user rights configuration. + copy: + src: rights.conf + dest: "{{ radicale_server_home_dir }}/rights.conf" + owner: "{{ radicale_server_username }}" + group: "{{ radicale_server_username }}" + mode: "400" + notify: + - Restart Radicale. + +- name: Ensure Radicale user accounts are defined. + when: + - radicale_config.auth is defined + - radicale_config.auth.type is defined + - radicale_config.auth.type == "htpasswd" + block: + - name: Ensure Radicale htpasswd file exists. + file: + path: "{{ radicale_config.auth.htpasswd_filename | default('/var/lib/radicale/users.htpasswd') }}" + state: touch + access_time: preserve + modification_time: preserve + + - name: Set Radicale user with password. + when: item.password is defined + no_log: true + htpasswd: + path: "{{ radicale_config.auth.htpasswd_filename | default('/var/lib/radicale/users.htpasswd') }}" + name: "{{ item.name }}" + password: "{{ item.password }}" + state: "{{ item.state | default('present') }}" + crypt_scheme: "bcrypt" + mode: "600" + owner: "{{ radicale_server_username }}" + group: "{{ radicale_server_username }}" + loop: "{{ radicale_users }}" + + - name: Set Radicale user with password hash. + when: item.bcrypt_hash is defined + no_log: true + lineinfile: + path: "{{ radicale_config.auth.htpasswd_filename | default('/var/lib/radicale/users.htpasswd') }}" + line: "{{ item.name }}:{{ item.bcrypt_hash }}" + state: "{{ item.state | default('present') }}" + mode: "600" + owner: "{{ radicale_server_username }}" + group: "{{ radicale_server_username }}" + loop: "{{ radicale_users }}" + +- name: Create systemd service unit. + template: + src: radicale.service.j2 + dest: /etc/systemd/system/radicale.service + # TODO: + #validate: "systemd-analyze verify %s" + notify: + - Reload systemd. + - Restart Radicale. + +- name: Start and enable Radicale service. + service: + name: radicale + state: "{{ radicale_service_state }}" + enabled: true diff --git a/templates/etc/radicale/config.j2 b/templates/etc/radicale/config.j2 new file mode 100644 index 0000000..c441067 --- /dev/null +++ b/templates/etc/radicale/config.j2 @@ -0,0 +1,141 @@ +# Radicale configuration file. +# +# See http://radicale.org/configuration/ for more details and +# descriptions of additional available configuration directives. + +{% if radicale_config.server is defined %} +[server] +{% if radicale_config.server.hosts is defined %} +hosts = {% for host in radicale_config.server.hosts %} +{{ host.addr | default('0.0.0.0') }}:{{ host.port | default('5232') }}{% if not loop.last %},{% endif %} +{% endfor %} + +{% endif %} +{% if radicale_config.server.daemon is defined %} +daemon = {{ radicale_config.server.daemon }} +{% endif %} +{% if radicale_config.server.pid is defined %} +pid = {{ radicale_config.server.pid }} +{% endif %} +{% if radicale_config.server.max_connections is defined %} +max_connections = {{ radicale_config.server.max_connections | default(20) | int }} +{% endif %} +{% if radicale_config.server.max_content_length is defined %} +max_content_length = {{ radicale_config.server.max_content_length | default(100000000) | int }} +{% endif %} +{% if radicale_config.server.timeout is defined %} +timeout = {{ radicale_config.server.timeout | default(30) | int }} +{% endif %} +{% if radicale_config.server.dns_lookup is defined %} +dns_lookup = {{ radicale_config.server.dns_lookup | default(true) }} +{% endif %} +{% if radicale_config.server.realm is defined %} +realm = {{ radicale_config.server.realm | default('Radicale - Password Required') }} +{% endif %} +{% if radicale_config.server.ssl is defined %} +ssl = {{ radicale_config.server.ssl | default('false') }} +{% endif %} +{% if radicale_config.server.certificate is defined %} +certificate = {{ radicale_config.server.certificate | default('/etc/ssl/radicale.cert.pem') }} +{% endif %} +{% if radicale_config.server.key is defined %} +key = {{ radicale_config.server.key | default('/etc/ssl/radicale.key.pem') }} +{% endif %} +{% if radicale_config.server.certificate_authority is defined %} +certificate_authority = {{ radicale_config.server.certificate_authority }} +{% endif %} +{% if radicale_config.server.protocol is defined %} +protocol = {{ radicale_config.server.protocol | default('PROTOCOL_TLSv1_2') }} +{% endif %} +{% if radicale_config.server.ciphers is defined %} +ciphers = {{ radicale_config.server.ciphers }} +{% endif %} +{% endif %}{# END if radicale_config.server is defined #} +{% if radicale_config.encoding is defined %} + +[encoding] +{% if radicale_config.encoding.request is defined %} +request = {{ radicale_config.encoding.request | default('utf-8') }} +{% endif %} +{% if radicale_config.encoding.stock is defined %} +stock = {{ radicale_config.encoding.stock | default('utf-8') }} +{% endif %} +{% endif %}{# END if radicale_config.encoding is defined #} +{% if radicale_config.auth is defined %} + +[auth] +{% if radicale_config.auth.type is defined %} +type = {{ radicale_config.auth.type }} +{% endif %} +{% if radicale_config.auth.htpasswd_filename is defined %} +htpasswd_filename = {{ radicale_config.auth.htpasswd_filename }} +{% endif %} +{% if radicale_config.auth.htpasswd_encryption is defined %} +htpasswd_encryption = {{ radicale_config.auth.htpasswd_encryption }} +{% endif %} +{% if radicale_config.auth.delay is defined %} +delay = {{ radicale_config.auth.delay | default('1') }} +{% endif %} +{% endif %}{# END if radicale_config.auth is defined #} +{% if radicale_config.rights is defined %} + +[rights] +{% if radicale_config.rights.type is defined %} +type = {{ radicale_config.rights.type | default('owner_only') }} +{% endif %} +{% if radicale_config.rights is defined and radicale_config.rights.type == "from_file" %} +file = {{ radicale_config.rights.file }} +{% endif %} +{% endif %}{# END if radicale_config.rights is defined #} +{% if radicale_config.storage is defined %} + +[storage] +{% if radicale_config.storage.type is defined %} +type = {{ radicale_config.storage.type | default('multifilesystem') }} +{% endif %} +{% if radicale_config.storage.filesystem_folder is defined %} +filesystem_folder = {{ radicale_config.storage.filesystem_folder | default('/var/lib/radicale/collections') }} +{% endif %} +{% if radicale_config.storage.filesystem_locking is defined %} +filesystem_locking = {{ radicale_config.storage.filesystem_locking | default(true) }} +{% endif %} +{% if radicale_config.storage.max_sync_token_age is defined %} +max_sync_token_age = {{ radicale_config.storage.max_sync_token_age | default(2592000) | int }} +{% endif %} +{% if radicale_config.storage.filesystem_fsync is defined %} +filesystem_fsync = {{ radicale_config.storage.filesystem_fsync | default(true) }} +{% endif %} +{% if radicale_config.storage.hook is defined %} +hook = {{ radicale_config.storage.hook }} +{% endif %} +{% endif %}{# END if radicale_config.storage is defined #} +{% if radicale_config.web is defined %} + +[web] +{% if radicale_config.web.type is defined %} +type = {{ radicale_config.web.type | default('internal') }} +{% endif %} +{% endif %}{# END if radicale_config.web is defined #} +{% if radicale_config.headers is defined %} + +[headers] +{% for k in radicale_config.headers %} +{{ k }} = {{ radicale_config.headers[k] }} +{% endfor %} +{% endif %}{# END if radicale_config.headers is defined #} +{% if radicale_config.logging is defined %} + +[logging] +{% if radicale_config.logging.debug is defined %} +debug = {{ radicale_config.logging.debug | default(false) }} +{% endif %} +{% if radicale_config.logging.mask_passwords is defined %} +mask_passwords = {{ radicale_config.logging.mask_passwords | default(true) }} +{% endif %} +{% if radicale_config.logging.full_environment %} +full_environment = {{ radicale_config.logging.full_environment | default(false) }} +{% endif %} +{% if radicale_config.logging.config %} +config = {{ radicale_config.logging.config }} +{% endif %} +{% endif %}{# END if radicale_config.logging is defined #} diff --git a/tests/test.yaml b/tests/test.yaml new file mode 100644 index 0000000..6ef1ddf --- /dev/null +++ b/tests/test.yaml @@ -0,0 +1,6 @@ +# This Ansible playbook runs test plays to ensure the role works. +--- +- name: Test role. + hosts: localhost + roles: + - ansible-role-radicale