diff options
| author | Kumar <kumar@kumardamani.xyz> | 2021-11-23 14:59:08 +0000 |
|---|---|---|
| committer | Kumar Damani <me@kumardamani.net> | 2022-03-25 19:45:25 +0000 |
| commit | a857eb82ec9c4a79ad5e41462e2ab82c2660982e (patch) | |
| tree | 9ca51ddfb551322bd4d2fa3f949fa8898032632d | |
initial commit
67 files changed, 1614 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f428a1f --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +roles/*.* +collections/ +hosts +vault +vaultid +migration/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/.gitmodules diff --git a/README.md b/README.md new file mode 100644 index 0000000..10a7261 --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +# My VPS Setup + +**This is still very much un-stable, and very much a work-in-progress.** + +So I decided to migrate *most* of my remote VPS to a self-hosted private server. There are 3 reasons for this: +1. Cost savings. VPS SSD storage can get quite [expensive](https://racknerd.com/kvm-vps) as your storage demands increase. Meanwhile I have a few SSDs lying my house around not being utilized. +2. Learning experience. There are many parts of the self-hosting stack that I'm not familiar with, and I am hoping to get more familiar with them as part of this exercise. +3. I want to be able to be to reproduce all of the tooling I use, as quickly as possible in a "doomsday" scenario, so I need it automated. + +My requirements are as follows: + +| Status | Feature | Choice | +| ------------------ | ------------------------------------------- | --------------------------------------- | +| | Email | n/a | +| :heavy_check_mark: | Hosting/Sharing files, calender, tasks etc. | [Nextcloud](https://nextcloud.com/) | +| :heavy_check_mark: | Hosting misc. static websites | [Nginx](https://www.nginx.com/) | +| :heavy_check_mark: | Media (pictures, tv, movies) Server | [Jellyfin](https://jellyfin.org/) | +| :heavy_check_mark: | Music Server | [Navidrome](https://www.navidrome.org/) | +| :heavy_check_mark: | Network accessible storage for media | [Samba](https://www.samba.org/) | +| :heavy_check_mark: | Password Hosting/Share | [Bitwarden](https://bitwarden.com/) | +| :heavy_check_mark: | VPN | [Wireguard](https://www.wireguard.com/) | + +> Consider items marked as :heavy_check_mark: above to be implemented in this repo. +> You may notice more [roles](./roles) than listed here. This is due to me trying other options for that feature, eg. "seafile" for file hosting. Feel free to use that instead. + +## Architecture + + +Notice that this setup requires a very small VPS since we are just running a wireguard client on it. All storage, and compute is being done by the Home Server. This means that my cost went from ~ $13/mo to ~ $1/mo for the VPS service, *while* increasing my SSD storage capacity from 50GB to 2TB (or whatever you have lying around)! And this does not incude the cost savings from self-hosting many of these services in the first place. + +### Tradeoffs +* I haven't factored in the cost of the hardware I'm putting to use or the cost of future replacement hardware, or the utility (power) cost of running these locally. +* VPS service providers have certain uptime guarantees that factors into their end-user pricing, especially with extra storage. No such guarantees exist with this setup, but things can be done to get most of the way there (which has its own time/effort, and equipment costs). +* Maintenance of these self-hosted services. But the way I see it, having it automated allows me to 1. minimize the time/effort spent on maintenance, and 2. to potentially turn this into a profitable venture for a growing privacy-minded audience - which at ~$13/mo would be a lot less profitable. + +There are many cheap VPS providers out there. I use Racknerd now. I have used Vultr previously. Both are good in-terms of technical support. + +## Monitoring + +You can get this dashboard [here](https://grafana.com/grafana/dashboards/15980). + +## Install +1. Fork this repo. +2. Clone you fork with `git clone` +3. Change directory to this repo `cd vps`. +4. Setup repo by running all of the following: +``` +python3 -m venv ~/venv/vps +source $HOME/venv/vps/bin/activate +pip install --upgrade pip +pip install 'ansible<2.10' 'jmespath' +ansible-galaxy install -r requirements.yml +ansible-galaxy collection install -r requirements.yml +cp -a vaultid.example vaultid +cp -a hosts.example hosts +cp -a group_vars/all/vault.example group_vars/all/vault +``` + +## Develop +1. Modify the [playbooks](./playbooks/) per your needs. +2. Modify the vault per your needs: `ansible-vault edit group_vars/all/vault --vault-id vaultid`. +3. Modify the [hosts](./hosts/) file per your needs. +4. Modify the [vars](./group_vars/all/vars.yaml/) file per your needs. + +## Execute +``` +ansible-playbook -i hosts playbooks/deploy.yaml --vault-id vaultid +ansible-playbook -i hosts playbooks/vps.yaml --vault-id vaultid +``` + +## Future Plans +1. Simplify install. +2. Documentation for each role. +3. Backups. +4. Redundancy. diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..006991b --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,13 @@ +[defaults] +roles_path = ./roles +playbook_dir = ./playbooks +collections_paths = ./collections +callback_whitelist = profile_tasks +host_key_checking = False +interpreter_python = /usr/bin/python3 +#strategy_plugins = ~/venvs/ansible/lib/python3.9/site-packages/ansible_mitogen/plugins/strategy +#strategy = mitogen_linear + + +[privilege_escalation] +become_method=sudo diff --git a/architecture.png b/architecture.png Binary files differnew file mode 100644 index 0000000..e7a092d --- /dev/null +++ b/architecture.png diff --git a/architecture.xml b/architecture.xml new file mode 100644 index 0000000..6a254c4 --- /dev/null +++ b/architecture.xml @@ -0,0 +1 @@ +<mxfile host="Electron" modified="2022-03-25T17:32:58.796Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/16.1.2 Chrome/96.0.4664.55 Electron/16.0.5 Safari/537.36" etag="ZWYLZcLEWb1IzxvfEjyp" version="16.1.2" type="device"><diagram id="7WZ78P3QGKG2k_Q4Ax3o" name="Page-1">7Vxbb+I6EP41kc55aJX75bEtvZxqz6otUs+efVkZYpJsQ8wmpkB//drgQMiYkm6BGLXSaptMHCf5vpnxeMZGsy6G0+scjeJ/SYhTzdTDqWZ1NNM0bNPU+D89nC0kpmN6C0mUJ6FotRJ0kxcshLqQjpMQF2sNKSEpTUbrwj7JMtynazKU52Sy3mxA0vWnjlCEgaDbRymU/peENBZSww1WF25wEsXi0X75fUNUNhZfUsQoJJOKyLrUrIucELo4Gk4vcMrRK3FZ3He14eryxXKc0SY3JMl9ej35ldwk9rfH6Yz27O/2iejlGaVj8cGPd13xvnRWgpCTcRZi3o+hWeeTOKG4O0J9fnXCeGeymA5TcfkZ5zRhAJ6lSZQxGSW8AXzb8tGsOZ5WROLtrzEZYprPWBNx1XcEkkKXTgzbdhaSyYoaXzSKK6R4ohkSyhAt+17hxQ4EZHL4XibRw4N3/fUkMrzH2+e7h+e75MQGUOGQqY84JTmNSUQylF6upOcrMHV2tmrzhXCg5hD+xJTOhC2gMSXrADME89k3fv+pU57+L7qbn3Sma2czcVZQlNMzbhNMkJEMl7KrhH+1uCOstWCSyvWNNBZknPfxK1AJR8CeF2G6XSM5jq8qRY5TRJPndTPdOcEmsI9/MorzjH0CYD5NmVOagxqjERf2UzIOtxvMLkzDqpuGU9pBxTQMU2IbZbvdQ+e2YRwDklFx0Whf6S2o9HKsvF1rvbj1jiTsnZd64pW3LPVEt2v8L95V3FdTgeWLvEMr/E+taKwVwWG0wtANoBbuYdXCAn72hvXJJF2cMzfIDv56uEv+VjQycY26+7UM6H4dS+J+jWBf/tcDkE6SHEdjlIea6abs+ec9Bqwb8aNCwLxxTNsCLSpGi+h7kEw5HcDidoGyW0fZlAxyjgRkZ18Ylywf1ptt815GJQZJk+zpXc7KCBp6q50PYe9ixn+L9vfTBGevRXQKaL/vOjXtt004+zmo9i916AOP5eXIudU8DFMp+ygTM5/cNeHOUos765O75tzZanHXSsroWLlzlOKufO9KUJFFSTZVPHCwbOXCZphWQ6MRE0gCCpWg9ECabTl9aw1KOHNeQJmpDaWvqwelDaCc4F7BoFJfM31PPTidV+BUXDsDWz04XQBnGGaF2jgaOtDL1gefsghSAbJAwx5SG8kgUA9ImGH8QvqI33fT6QA435Sq3cfg7Qen9RTKMs1dwdAzIIbW3jAMNmLY7aqHoe9vLBRUEZRo4d4QtKA5n+MByTEAj30jrSdSc/KEL0hKcm05yxmwKU5NhETBoI958VdSSRgmYZhu8hTrs7cqK3Z5Ll5SsqbjD1xujSHXhgy5h2SofH57U1679Smv1XTKa6mVJrTglFfJBRCmVXftrg7V/rALIGzvU+9Ldd6q97arlt7DDIW6C+NAVsJ2JKHhYavPduu1IQWUv+lCD0utHLVtfnLXnDu1ctR267WhY+JOrUHHbr02dEzcqbXexIJ5+COoDZlOvTZkmy1nlSyYhT+K2pABdidYVttQwgz8UdSGlltmFIISZt+PpjZkgtRx+3BKFqceS23IAstQ24cTrnY8G1DJet4PkgI1QHG5/SQodCDzhIKSaX4TrrSWrGffV5rfGAY/7JdvnXvvxXu86d+lUx9LNipCLwGC0gpK6xq4EaOt0WRDE3/njgzHr8dlbhmplZ0sImmwIaNBV7pe62p3ezuiEy//PinQz87k9jtyH7uDX7eSvR2AuqpDKrfJcp5CVMRL0pq7oDL/PJxGfFvy6WIjsLn4y7udb4k8dQN+zHHiRJxnhPZj8aycUMYfybbpyxuGDbCkQGZT+/JJUmJgpK1d6VpwpQW+5l82GDyaM5LjInlBvXlXHNARV7b59zjnmtPhfbGJaSHc2BvGpYr3s3ZUS1jVNCtVzmZL7eub6XZGFYzkS6oCzYfx50ehygbVTtNomSk4zn9Ebxf40IQO5+3Mi9unH8+T4mU07lyTbNrTu/eSCOJsftNo3EuTPjvofOXxGN8zn+CCXxADKSXsP1n1R0kr28CUhM/NqzzAhNGWrDUydQl75r7i53IbGqAvpnTEeuR/OGk0R4PBnM6IzFmkMQv9onj+7ssNYDodZxlOTxtQuo57iAdoPN89dsA50o5s0oG0SpzlsiR4EKM0t3vLQ2+o7KeoKJgGbd1TCecbmmwO8Wd5cDGj375FIpBz/s4JiGS3oV7TgaYTENevdwWWXOxuAiJVMjgB+fT8m1YSgN8RkSyj2ZXnZ6ernz9akL36FSnr8jc=</diagram></mxfile>
\ No newline at end of file diff --git a/group_vars/all/vars.yaml b/group_vars/all/vars.yaml new file mode 100644 index 0000000..f5c2645 --- /dev/null +++ b/group_vars/all/vars.yaml @@ -0,0 +1,27 @@ +--- + +# the wildcard domain under which all apps will be hosted. +apps_domain: 'kumardamani.net' + +# defines the confs for each app we are going to install. +apps_conf_map: + nextcloud: + port: 31900 + access_domain: 'files.{{ apps_domain }}' + prometheus: + port: 9090 + access_domain: 'metrics.{{ apps_domain }}' + bitwarden: + port: 31901 + access_domain: 'vault.{{ apps_domain }}' + +# list all static websites you plan to host here. +nginx_websites: + - name: 'kumardamani.net' + git_url: 'https://{{ (git_user|default(None)) | urlencode() }}:{{ (git_pass|default(None)) | urlencode() }}@gitlab.com/kdam0/kumardamani.net.git' # noqa yaml + git_version: 'main' + - name: 'amayastuff.com' + git_url: 'https://{{ (git_user|default(None)) | urlencode() }}:{{ (git_pass|default(None)) | urlencode() }}@gitlab.com/kdam0/amayastuff.com.git' # noqa yaml + git_version: 'main' + +vpn_domain: 'kdvpn.crabdance.com' diff --git a/group_vars/all/vault.example b/group_vars/all/vault.example new file mode 100644 index 0000000..8dc5545 --- /dev/null +++ b/group_vars/all/vault.example @@ -0,0 +1,16 @@ +$ANSIBLE_VAULT;1.1;AES256 +33646338366162383266356237656436666231633439663936303263333261386436653331656330 +3666623261626263366232646631633763303363356239640a656165643764393864353137323037 +37343237306361633834366236353539376162373262373161656236616533323633376230666239 +6337306438373536650a373330386330346665643936393839626561383933363031626564343066 +64363335613139323735653230633262396533623065356439356533663631613033356635366163 +65613632623933313734643765373966633231633838306339323461653533386134653239623966 +37393133613836653232613130366535616435333130333332313832363636623066373635626561 +39313238353264336435313735646363326330613538613664363436323362363730663433363833 +61323535306665316664643231306665663665626331356330623265353062353262356661653137 +66643565653934626161326261373934323162386562303265393834393238373566313566383164 +35343438353363326439613462343237303735346134613061323531663534346531356465323964 +61353630373336333131353135623165633365323135383932326562653135633566346165623137 +33306266326238303130393332306463363866656239666130313665333466653631626564346534 +33393434366135633039363935383333363461333835333933346536396364636138356266623965 +373233303165616238303463666635396164 diff --git a/hosts.example b/hosts.example new file mode 100644 index 0000000..03562a3 --- /dev/null +++ b/hosts.example @@ -0,0 +1,5 @@ +[vps] +some_host ansible_host=some_ip ansible_user=some_root_user + +[compute] +some_local_host ansible_host= ansible_user= data_vol_ssd="/ssd" data_vol_hdd="/hdd" ansible_ssh_pass= diff --git a/playbooks/deploy.yaml b/playbooks/deploy.yaml new file mode 100644 index 0000000..9ba8c3b --- /dev/null +++ b/playbooks/deploy.yaml @@ -0,0 +1,226 @@ +# Usage: +# ansible-playbook -i hosts playbooks/deploy.yaml --vault-id vaultid --tags base +# +# Tags: +# base, ddclient, wireguard, nginx, website, samba +# nextcloud, monitoring (these all depend on the nginx tag) +# +# Install: +# git submodule update --remote --merge +# python3 -m venv ~/venv/ansible +# source $HOME/venv/ansible/bin/activate +# pip install --upgrade pip +# pip install 'ansible<2.10' 'jmespath' +# ansible-galaxy install -r requirements.yml +# ansible-galaxy collection install -r requirements.yml +# +# Assumptions: +# Run apt update, upgrade +# Run rpi-update +# HD, SSD formatted appropriately. +--- + + +- hosts: compute + gather_facts: false + tags: 'ddclient' + become: true + roles: + - role: ddclient + vars: + ddclient_proto: 'freedns' + ddclient_user: '{{ vault_ddclient_user }}' + ddclient_password: '{{ vault_ddclient_password }}' + ddclient_domain: '{{ vpn_domain }}' + +- hosts: compute + gather_facts: false + tags: 'wireguard' + become: true + roles: + - role: wireguard + vars: + wireguard_server_priv_key: '{{ vault_wireguard_server_priv_key }}' + wireguard_server_pub_key: 'iwsriL3AUn4dgKfsSNgJtj/G808k4jXvSC+mM70YnAw=' + wireguard_server_listen_port: 51820 + wireguard_server_ip: '192.168.10.1/24' + wireguard_peers: + - peer_ip: '192.168.10.2/32' # my phone + peer_key: '/mFv6y9QA8dhX/A9fFn+mzg/SZq4BZPeNofm1w754y8=' + - peer_ip: '192.168.10.3/32' # personal laptop + peer_key: '6+t6FbEHSAOuzBtQwb0bDqfFa5Kvfkb2QIUGpRKA5Eg=' + - peer_ip: '192.168.10.4/32' # vps + peer_key: 'oz2jxeSUWD4r5g3y7XuSItqSiqOOJz8vulYYVXAsKS8=' + +- hosts: compute + gather_facts: false + tags: 'nginx' + become: true + roles: + - role: nginx + vars: + nginx_website_domains: '{{ ["default"] + (nginx_websites | map(attribute="name") | list) }}' + nginx_global_apps_domain: '{{ apps_domain }}' + nginx_apps_confs: '{{ apps_conf_map | dict2items(key_name="app", value_name="conf") }}' + +- hosts: compute + gather_facts: false + tags: 'website' + become: true + roles: + - role: website + vars: + websites: '{{ nginx_websites }}' + git_user: '{{ vault_git_user }}' + git_pass: '{{ vault_git_password }}' + +- hosts: compute + gather_facts: false + tags: 'nextcloud' + become: true + roles: + - role: nextcloud + vars: + nextcloud_domain: '{{ apps_conf_map["nextcloud"]["access_domain"] }}' + nextcloud_host_port: '{{ apps_conf_map["nextcloud"]["port"] }}' + nextcloud_data_root: '{{ data_vol_hdd }}/nextcloud' + nextcloud_postgres_password: '{{ vault_nextcloud_postgres_password }}' + +- hosts: compute + gather_facts: false + tags: 'bitwarden' + become: true + roles: + - role: bitwarden + vars: + bitwarden_host_port: '{{ apps_conf_map["bitwarden"]["port"] }}' + bitwarden_data_dir: '{{ data_vol_hdd }}/bitwarden' + +# Access: http://art-jr/data +- hosts: compute + gather_facts: false + tags: 'samba' + become: true + roles: + - role: samba + vars: + samba_user: '{{ vault_samba_user }}' + samba_user_pass: '{{ vault_samba_password }}' + +- hosts: compute + become: true + tags: 'monitoring' + roles: + - role: 'cloudalchemy.node_exporter' + vars: + node_exporter_version: '1.2.2' + node_exporter_web_listen_address: '0.0.0.0:9100' + node_exporter_web_telemetry_path: '/metrics' + + # monitor cpu temp. stats for the rpi. + - role: 'rpi_exporter' + + - role: 'cloudalchemy.prometheus' + vars: + prometheus_version: 'latest' + prometheus_skip_install: false + prometheus_db_dir: '{{ data_vol_hdd }}/prometheus' + prometheus_web_listen_address: '0.0.0.0:{{ apps_conf_map["prometheus"]["port"] }}' + prometheus_web_external_url: 'https://{{ apps_conf_map["prometheus"]["access_domain"] }}' + prometheus_storage_retention: '15d' + prometheus_global: + scrape_interval: '30s' + evaluation_interval: '30s' + prometheus_targets: + node: + - targets: + - localhost:9100 + - localhost:9243 + labels: + env: prod + prometheus_scrape_configs: + - job_name: "prometheus" + metrics_path: "{{ prometheus_metrics_path }}" + static_configs: + - targets: + - "{{ ansible_fqdn | default(ansible_host) | default('localhost') }}:9090" + - job_name: "node" + file_sd_configs: + - files: + - "{{ prometheus_config_dir }}/file_sd/node.yml" + - job_name: 'blackbox' + metrics_path: /probe + params: + module: [http_2xx] + static_configs: + # all apps, nginx websites, prefixed with "https://" + - targets: '{{ ["https://"] | product((apps_conf_map | dict2items(key_name="app", value_name="conf") | map(attribute="conf") | map(attribute="access_domain") | list)+(nginx_websites | map(attribute="name") | list)) | map("join") | list }}' # noqa yaml + relabel_configs: + - source_labels: [__address__] + target_label: __param_target + - source_labels: [__param_target] + target_label: instance + - target_label: __address__ + replacement: 127.0.0.1:9115 # Blackbox exporter. + + - role: 'cloudalchemy.blackbox-exporter' + vars: + blackbox_exporter_web_listen_address: '0.0.0.0:9115' + +- hosts: compute + become: true + tags: 'base' + roles: + - role: geerlingguy.pip + vars: + pip_package: python3-pip + pip_install_packages: + - name: docker + - name: pexpect + + - role: geerlingguy.docker_arm + vars: + docker_install_recommends: true + docker_install_compose: true + docker_users: + - pi + docker_pip_executable: pip3 + docker_version_armv7: 5:20.10.10~3-0~raspbian-bullseye + + - role: geerlingguy.nfs + vars: + nfs_exports: + - '{{ data_vol_ssd }} *(rw,sync,no_root_squash)' + - '{{ data_vol_hdd }} *(rw,sync,no_root_squash)' + pre_tasks: + - name: 'Install quality of life packages are present' + apt: + name: + - 'git' + - 'net-tools' + - 'neofetch' + - 'neovim' + - 'nnn' + - 'nmap' + update_cache: true + + - name: 'Ensure that mount for data vol exists' + mount: + state: 'mounted' + path: '{{ item.vol_path }}' + src: 'UUID="{{ item.vol_uuid }}"' + boot: true + fstype: '{{ item.vol_fstype }}' + opts: 'rw,user,exec' + loop: + - vol_path: '{{ data_vol_ssd }}' + vol_uuid: '734d1190-b222-4979-91c8-0582e030fd1a' + vol_fstype: 'ext4' + - vol_path: '{{ data_vol_hdd }}' + vol_uuid: 'aa392d86-bc93-4ad4-9e9e-c1274384bbfb' + vol_fstype: 'ext4' + + - name: 'Set python3 as default python' + alternatives: + name: 'python' + path: '/bin/python3' diff --git a/playbooks/vps.yaml b/playbooks/vps.yaml new file mode 100644 index 0000000..67780d2 --- /dev/null +++ b/playbooks/vps.yaml @@ -0,0 +1,26 @@ +# Usage: +# ansible-playbook -i hosts playbooks/vps.yaml --vault-id vaultid +# +# Install: +# ansible-galaxy install -r requirements.yml +# ansible-galaxy role install -r requirements.yml +# +--- + +- hosts: vps + gather_facts: false + become: true + pre_tasks: + - name: 'Set python3 as default python' # noqa no-changed-when + command: 'update-alternatives --install /bin/python python /bin/python3 3' + roles: + - role: wireguard_client + vars: + wireguard_client_server_pub_key: 'iwsriL3AUn4dgKfsSNgJtj/G808k4jXvSC+mM70YnAw=' + wireguard_client_server_endpoint: 'kdvpn.crabdance.com' + wireguard_client_server_allowed_ips: '192.168.10.0/24' + wireguard_client_peer_ip: '192.168.10.4/24' + wireguard_client_server_listen_port: 51820 + wireguard_client_port_forwards: + - 80 # webserver + - 443 # webserver diff --git a/requirements.yml b/requirements.yml new file mode 100644 index 0000000..8aca51c --- /dev/null +++ b/requirements.yml @@ -0,0 +1,12 @@ +--- + +collections: + - name: community.docker +roles: + - name: cloudalchemy.blackbox-exporter + - name: cloudalchemy.node_exporter + - name: cloudalchemy.prometheus + - name: geerlingguy.certbot + - name: geerlingguy.docker_arm + - name: geerlingguy.pip + - name: geerlingguy.nfs diff --git a/roles/bitwarden/defaults/main.yaml b/roles/bitwarden/defaults/main.yaml new file mode 100644 index 0000000..cc46411 --- /dev/null +++ b/roles/bitwarden/defaults/main.yaml @@ -0,0 +1,6 @@ +# Default Values +--- + +bitwarden_data_dir: '' +bitwarden_host_port: '' +bitwarden_container_name: 'bitwarden' diff --git a/roles/bitwarden/handlers/main.yaml b/roles/bitwarden/handlers/main.yaml new file mode 100644 index 0000000..cd21505 --- /dev/null +++ b/roles/bitwarden/handlers/main.yaml @@ -0,0 +1,2 @@ +--- + diff --git a/roles/bitwarden/tasks/main.yaml b/roles/bitwarden/tasks/main.yaml new file mode 100644 index 0000000..5fd0ed4 --- /dev/null +++ b/roles/bitwarden/tasks/main.yaml @@ -0,0 +1,17 @@ +--- + +- name: 'Make sure the Vaultwarden container is created and running' + docker_container: + name: '{{ bitwarden_container_name }}' + image: 'vaultwarden/server' + pull: true + state: 'started' + env: + WEBSOCKET_ENABLED: 'true' + INVITATIONS_ALLOWED: 'true' + SIGNUPS_ALLOWED: 'false' + volumes: + - "{{ bitwarden_data_dir }}:/data" + ports: + - '{{ bitwarden_host_port }}:80' + restart_policy: unless-stopped diff --git a/roles/ddclient/defaults/main.yaml b/roles/ddclient/defaults/main.yaml new file mode 100644 index 0000000..9a163d4 --- /dev/null +++ b/roles/ddclient/defaults/main.yaml @@ -0,0 +1,2 @@ +# Default Values +--- diff --git a/roles/ddclient/handlers/main.yaml b/roles/ddclient/handlers/main.yaml new file mode 100644 index 0000000..be19365 --- /dev/null +++ b/roles/ddclient/handlers/main.yaml @@ -0,0 +1,7 @@ +--- + +- name: 'Restart ddclient' + systemd: + name: 'ddclient' + state: 'restarted' + listen: 'restart ddclient' diff --git a/roles/ddclient/tasks/main.yaml b/roles/ddclient/tasks/main.yaml new file mode 100644 index 0000000..8deffc2 --- /dev/null +++ b/roles/ddclient/tasks/main.yaml @@ -0,0 +1,22 @@ +# https://notthebe.ee/raspi.html +--- + +- name: 'Install required apt packages' + apt: + name: + - 'ddclient' + state: 'present' + update_cache: true + +- name: 'Update ddclient conf' + template: + dest: '/etc/ddclient.conf' + src: 'ddclient.conf.j2' + mode: '644' + notify: 'restart ddclient' + +- name: 'Start ddclient' + systemd: + name: 'ddclient' + state: 'started' + enabled: true diff --git a/roles/ddclient/templates/ddclient.conf.j2 b/roles/ddclient/templates/ddclient.conf.j2 new file mode 100644 index 0000000..99dab47 --- /dev/null +++ b/roles/ddclient/templates/ddclient.conf.j2 @@ -0,0 +1,9 @@ +# {{ ansible_managed }} +# +# /etc/ddclient.conf + +protocol={{ ddclient_proto }} \ +use=web, web=https://freedns.afraid.org/dynamic/check.php \ +login={{ ddclient_user }} \ +password='{{ ddclient_password }}' \ +{{ ddclient_domain }} diff --git a/roles/jellyfin/README.md b/roles/jellyfin/README.md new file mode 100644 index 0000000..79d061e --- /dev/null +++ b/roles/jellyfin/README.md @@ -0,0 +1,34 @@ +# Ansible - Jellyfin + +## Example Playbook +``` +# Direct Access: http://art-jr:8096/ +# Domain Access: http://kumardamani.xyz/media +# Create login from GUI. +# While doing initial setup set the base url to '/media' in Networking settings. +- hosts: compute + gather_facts: false + tags: 'jellyfin' + become: true + roles: + - role: jellyfin + vars: + jellyfin_domain: 'kumardamani.xyz' +``` + +## Nginx Conf +``` + location /media/ { + proxy_pass http://127.0.0.1:8096/media/; + proxy_pass_request_headers on; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + # Disable buffering when the nginx proxy gets very resource heavy upon streaming + proxy_buffering off; + } +``` diff --git a/roles/jellyfin/defaults/main.yaml b/roles/jellyfin/defaults/main.yaml new file mode 100644 index 0000000..9a163d4 --- /dev/null +++ b/roles/jellyfin/defaults/main.yaml @@ -0,0 +1,2 @@ +# Default Values +--- diff --git a/roles/jellyfin/handlers/main.yaml b/roles/jellyfin/handlers/main.yaml new file mode 100644 index 0000000..7613b54 --- /dev/null +++ b/roles/jellyfin/handlers/main.yaml @@ -0,0 +1,7 @@ +--- + +- name: 'Restart nginx' + systemd: + name: 'nginx' + state: 'restarted' + listen: 'nginx_restart' diff --git a/roles/jellyfin/tasks/main.yaml b/roles/jellyfin/tasks/main.yaml new file mode 100644 index 0000000..647c1f1 --- /dev/null +++ b/roles/jellyfin/tasks/main.yaml @@ -0,0 +1,45 @@ +# https://jellyfin.org/docs/general/administration/installing.html +# https://jellyfin.org/docs/general/networking/nginx.html +--- + +- name: 'Install required apt packages' + apt: + name: + - 'apt-transport-https' + - 'gnupg' + - 'lsb-release' + state: 'present' + update_cache: true + +- name: Add an Apt signing key, uses whichever key is at the URL + apt_key: + url: https://repo.jellyfin.org/debian/jellyfin_team.gpg.key + state: present + +- name: Add specified repository into sources list + apt_repository: + repo: deb [arch=armhf] https://repo.jellyfin.org/debian bullseye main + state: present + filename: jellyfin + +- name: 'Install Jellyfin' + apt: + name: + - 'jellyfin' + state: 'present' + update_cache: true + +- name: 'Add jellyfin user to video group' + user: + name: 'jellyfin' + groups: 'video' + append: true + state: 'present' + +- name: 'Start Jellyfin' + systemd: + name: '{{ item }}' + state: 'started' + enabled: true + loop: + - 'jellyfin' diff --git a/roles/navidrome/README.md b/roles/navidrome/README.md new file mode 100644 index 0000000..755d428 --- /dev/null +++ b/roles/navidrome/README.md @@ -0,0 +1,31 @@ +# Ansible - Navidrome + +## Example Playbook +``` +# Access: http://art-jr:4533/music/ +# Domain Access: http://kumardamani.xyz/music +# Create login from GUI +- hosts: compute + gather_facts: false + tags: 'navidrome' + become: true + roles: + - role: navidrome + vars: + navidrome_domain: 'kumardamani.xyz' + navidrome_data_root: '{{ data_vol_ssd }}/music' +``` + +## Nginx Conf +``` + location /music/ { + proxy_pass http://127.0.0.1:4533/music/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_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/navidrome/defaults/main.yaml b/roles/navidrome/defaults/main.yaml new file mode 100644 index 0000000..a7c9858 --- /dev/null +++ b/roles/navidrome/defaults/main.yaml @@ -0,0 +1,6 @@ +# Default Values +--- + +navidrome_home_root: '/opt/navidrome' +navidrome_home_db: '/opt/navidrome/db' +navidrome_data_root: '' diff --git a/roles/navidrome/handlers/main.yaml b/roles/navidrome/handlers/main.yaml new file mode 100644 index 0000000..7613b54 --- /dev/null +++ b/roles/navidrome/handlers/main.yaml @@ -0,0 +1,7 @@ +--- + +- name: 'Restart nginx' + systemd: + name: 'nginx' + state: 'restarted' + listen: 'nginx_restart' diff --git a/roles/navidrome/tasks/main.yaml b/roles/navidrome/tasks/main.yaml new file mode 100644 index 0000000..72982c2 --- /dev/null +++ b/roles/navidrome/tasks/main.yaml @@ -0,0 +1,34 @@ +# https://www.navidrome.org/docs/installation/docker/ +--- + +- name: 'Ensure required vars exist' + assert: + that: item | mandatory + loop: + - '{{ navidrome_data_root }}' + no_log: true + +- name: 'Ensure Navidrome dirs exists' + file: + path: '{{ item }}' + state: 'directory' + owner: '{{ ansible_user }}' + group: '{{ ansible_user }}' + mode: '755' + loop: + - '{{ navidrome_home_root }}' + - '{{ navidrome_home_db }}' + - '{{ navidrome_data_root }}' + +- name: 'Copy Navidrome docker compose config to home dir' + become_user: '{{ ansible_user }}' + template: + dest: '{{ navidrome_home_root }}/docker-compose.yaml' + src: 'docker-compose.yaml.j2' + mode: '644' + +- name: 'Create and start services' + become_user: '{{ ansible_user }}' + community.docker.docker_compose: + project_src: '{{ navidrome_home_root }}' + state: 'present' diff --git a/roles/navidrome/templates/docker-compose.yaml.j2 b/roles/navidrome/templates/docker-compose.yaml.j2 new file mode 100644 index 0000000..cc9cf59 --- /dev/null +++ b/roles/navidrome/templates/docker-compose.yaml.j2 @@ -0,0 +1,18 @@ +version: "3" +services: + navidrome: + image: deluan/navidrome:latest + user: 1000:1000 + ports: + - "4533:4533" + restart: unless-stopped + environment: + # Optional: put your config options customization here. Examples: + ND_SCANSCHEDULE: 1h + ND_LOGLEVEL: info + ND_SESSIONTIMEOUT: 24h + ND_BASEURL: "/music" + volumes: + - "{{ navidrome_home_db }}:/data" + - "{{ navidrome_data_root }}:/music:ro" + diff --git a/roles/nextcloud/defaults/main.yaml b/roles/nextcloud/defaults/main.yaml new file mode 100644 index 0000000..cd11c66 --- /dev/null +++ b/roles/nextcloud/defaults/main.yaml @@ -0,0 +1,17 @@ +# Default Values +--- + +nextcloud_uid: "1000" +nextcloud_gid: "1000" +nextcloud_tz: 'America/Toronto' +nextcloud_data_root: '' +nextcloud_host_port: '' + +nextcloud_docker_network_name: 'nextcloud_network' + +nextcloud_postgres_db: 'nextcloud_db' +nextcloud_postgres_user: 'nextcloud' +nextcloud_postgres_password: '' + +nextcloud_container_name: 'nextcloud' +nextcloud_dashboard_url: 'https://{{ nextcloud_domain }}' diff --git a/roles/nextcloud/handlers/main.yaml b/roles/nextcloud/handlers/main.yaml new file mode 100644 index 0000000..7613b54 --- /dev/null +++ b/roles/nextcloud/handlers/main.yaml @@ -0,0 +1,7 @@ +--- + +- name: 'Restart nginx' + systemd: + name: 'nginx' + state: 'restarted' + listen: 'nginx_restart' diff --git a/roles/nextcloud/tasks/main.yaml b/roles/nextcloud/tasks/main.yaml new file mode 100644 index 0000000..593bfa3 --- /dev/null +++ b/roles/nextcloud/tasks/main.yaml @@ -0,0 +1,63 @@ +# https://docs.nextcloud.com/server/19/admin_manual/configuration_server/config_sample_php_parameters.html#default-parameters +# https://docs.nextcloud.com/server/19/admin_manual/configuration_server/caching_configuration.html#id2 +--- + +- name: 'Create the nextcloud network' + community.docker.docker_network: + name: '{{ nextcloud_docker_network_name }}' + +- name: Make sure the Postgres container is created and running + community.docker.docker_container: + name: 'nextcloud-postgres' + image: 'postgres:14.1-alpine' + pull: true + state: 'started' + labels: + "flame.type": "application" + "flame.name": "{{ nextcloud_container_name | title }}" + "flame.url": "{{ nextcloud_dashboard_url }}" + "flame.icon": "custom" + env: + "PUID": '{{ nextcloud_uid }}' + "PGID": '{{ nextcloud_gid }}' + "TZ": '{{ nextcloud_tz }}' + "POSTGRES_DB": "{{ nextcloud_postgres_db }}" + "POSTGRES_USER": "{{ nextcloud_postgres_user }}" + "POSTGRES_PASSWORD": "{{ nextcloud_postgres_password }}" + volumes: + - '{{ nextcloud_data_root }}/postgres:/var/lib/postgresql/data' + restart_policy: 'unless-stopped' + +- name: 'Make sure the Redis container is created and running' + community.docker.docker_container: + name: 'nextcloud-redis' + image: 'redis:alpine' + pull: true + state: 'started' + restart_policy: 'unless-stopped' + +- name: 'Make sure the Nextcloud container is created and running' + community.docker.docker_container: + name: '{{ nextcloud_container_name }}' + image: 'ghcr.io/linuxserver/nextcloud:php8' + pull: true + state: 'started' + env: + "PUID": '{{ nextcloud_uid }}' + "PGID": '{{ nextcloud_gid }}' + "TZ": '{{ nextcloud_tz }}' + volumes: + - '{{ nextcloud_data_root }}/config:/config' + - '{{ nextcloud_data_root }}/data:/data' + ports: + - '{{ nextcloud_host_port }}:443' + restart_policy: unless-stopped + +- name: 'Add {{ nextcloud_container_name }} to the docker network' + community.docker.docker_network: + name: '{{ nextcloud_docker_network_name }}' + connected: + - '{{ nextcloud_container_name }}' + - '{{ nextcloud_container_name }}-postgres' + - '{{ nextcloud_container_name }}-redis' + appends: true diff --git a/roles/nginx/defaults/main.yaml b/roles/nginx/defaults/main.yaml new file mode 100644 index 0000000..af90628 --- /dev/null +++ b/roles/nginx/defaults/main.yaml @@ -0,0 +1,7 @@ +# Default Values +--- + +nginx_port: '80' +nginx_website_domains: [] +nginx_global_apps_domain: '' +nginx_apps_confs: {} diff --git a/roles/nginx/handlers/main.yaml b/roles/nginx/handlers/main.yaml new file mode 100644 index 0000000..7613b54 --- /dev/null +++ b/roles/nginx/handlers/main.yaml @@ -0,0 +1,7 @@ +--- + +- name: 'Restart nginx' + systemd: + name: 'nginx' + state: 'restarted' + listen: 'nginx_restart' diff --git a/roles/nginx/tasks/main.yaml b/roles/nginx/tasks/main.yaml new file mode 100644 index 0000000..91fd8a5 --- /dev/null +++ b/roles/nginx/tasks/main.yaml @@ -0,0 +1,67 @@ +# References +# https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-debian-10 + +--- + +- name: 'Install nginx' + apt: + name: + - 'nginx' + state: 'present' + update_cache: true + notify: + - 'nginx_restart' + +- name: 'Copy the http nginx.conf files' + template: + src: 'http.nginx.conf.j2' + dest: '/etc/nginx/sites-available/{{ item }}' + mode: '644' + loop: + - 'default' + - 'kumardamani.xyz' + - 'amayastuff.com' + register: '_config_copy' + notify: + - 'nginx_restart' + +- name: 'Remove the old symbolic link' + file: + path: '/etc/nginx/sites-enabled/{{ item }}' + state: 'absent' + loop: '{{ nginx_website_domains }}' + +- name: 'Create a http symbolic link' + file: + src: '/etc/nginx/sites-available/{{ item }}' + dest: '/etc/nginx/sites-enabled/{{ item }}' + state: 'link' + loop: '{{ nginx_website_domains }}' + +- name: 'Check nginx config' # noqa no-changed-when + command: 'nginx -t' + +- name: 'Copy the http nginx.conf for apps' + template: + src: 'app.nginx.conf.j2' + dest: '/etc/nginx/sites-available/{{ item.app }}' + mode: '644' + loop: '{{ nginx_apps_confs }}' + notify: + - 'nginx_restart' + +- name: 'Remove the old symbolic link for apps' + file: + path: '/etc/nginx/sites-enabled/{{ item.app }}' + state: 'absent' + loop: '{{ nginx_apps_confs }}' + +- name: 'Create a http symbolic link for apps' + file: + src: '/etc/nginx/sites-available/{{ item.app }}' + dest: '/etc/nginx/sites-enabled/{{ item.app }}' + state: 'link' + loop: '{{ nginx_apps_confs }}' + +- name: 'Check nginx config' # noqa no-changed-when + command: 'nginx -t' diff --git a/roles/nginx/templates/app.nginx.conf.j2 b/roles/nginx/templates/app.nginx.conf.j2 new file mode 100644 index 0000000..c87fe29 --- /dev/null +++ b/roles/nginx/templates/app.nginx.conf.j2 @@ -0,0 +1,21 @@ +server { + listen 443 ssl; + root _; + server_name {{ item.conf.access_domain }}; + ssl_certificate /etc/letsencrypt/live/{{ nginx_global_apps_domain }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ nginx_global_apps_domain }}/privkey.pem; + ssl_protocols TLSv1.1 TLSv1.2; + ssl_ciphers HIGH:!aNULL:!MD5; + + location / { + proxy_pass http://127.0.0.1:{{ item.conf.port }}; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $server_name; + proxy_set_header X-Forwarded-Proto https; + proxy_read_timeout 1200s; + client_max_body_size 0; + error_log /var/log/nginx/{{ item.app }}.error.log; + } +} diff --git a/roles/nginx/templates/http.nginx.conf.j2 b/roles/nginx/templates/http.nginx.conf.j2 new file mode 100644 index 0000000..2d4c61b --- /dev/null +++ b/roles/nginx/templates/http.nginx.conf.j2 @@ -0,0 +1,24 @@ +server { + {% if item == "default" %} + listen 80 default_server; + server_name _; + return 301 https://$host$request_uri; + {% else %} + listen 443 ssl; + root /var/www/{{ item }}/html; + server_name {{ item }} www.{{ item }}; + ssl_certificate /etc/letsencrypt/live/{{ item }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ item }}/privkey.pem; + ssl_protocols TLSv1.1 TLSv1.2; + ssl_ciphers HIGH:!aNULL:!MD5; + index index.html index.htm index.nginx-debian.html; + + location ~ /.well-known { + allow all; + } + + location / { + try_files $uri $uri/ =404; + } + {% endif %} +} diff --git a/roles/rpi_exporter/defaults/main.yaml b/roles/rpi_exporter/defaults/main.yaml new file mode 100644 index 0000000..8a446ec --- /dev/null +++ b/roles/rpi_exporter/defaults/main.yaml @@ -0,0 +1,3 @@ +# Default Values +--- + diff --git a/roles/rpi_exporter/tasks/main.yaml b/roles/rpi_exporter/tasks/main.yaml new file mode 100644 index 0000000..9cf3052 --- /dev/null +++ b/roles/rpi_exporter/tasks/main.yaml @@ -0,0 +1,45 @@ +# https://github.com/lukasmalkmus/rpi_exporter +--- + +- name: 'download rpi_exporter binary package to local folder' + become: false + get_url: + url: "https://github.com/lukasmalkmus/rpi_exporter/releases/download/v0.8.0/rpi_exporter-0.8.0.linux-armv7.tar.gz" + dest: "/tmp/rpi_exporter.tar.gz" + register: _download_archive + until: _download_archive is succeeded + retries: 5 + delay: 2 + delegate_to: localhost + check_mode: false + +- name: 'unpack prometheus binaries' + become: false + unarchive: + src: "/tmp/rpi_exporter.tar.gz" + dest: "/tmp" + creates: "/tmp/rpi_exporter/rpi_exporter" + delegate_to: localhost + check_mode: false + +- name: 'propagate rpi_exporter bin' + copy: + src: "/tmp/rpi_exporter/rpi_exporter" + dest: "/usr/local/bin/rpi_exporter" + mode: 0755 + owner: root + group: root + +- name: 'propagate rpi_exporter service' + copy: + src: "/tmp/rpi_exporter/rpi_exporter.service" + dest: "/etc/systemd/system/rpi_exporter.service" + mode: 0755 + owner: root + group: root + +- name: 'Start rpi_exporter' + systemd: + name: 'rpi_exporter' + state: 'started' + enabled: true diff --git a/roles/samba/defaults/main.yaml b/roles/samba/defaults/main.yaml new file mode 100644 index 0000000..971a9c5 --- /dev/null +++ b/roles/samba/defaults/main.yaml @@ -0,0 +1,4 @@ +# Default Values +--- + +samba_user_pass: '' diff --git a/roles/samba/tasks/main.yaml b/roles/samba/tasks/main.yaml new file mode 100644 index 0000000..5207ac5 --- /dev/null +++ b/roles/samba/tasks/main.yaml @@ -0,0 +1,29 @@ +# https://pimylifeup.com/raspberry-pi-samba/ +--- + +- name: 'Install required apt packages' + apt: + name: + - 'samba' + - 'samba-common-bin' + state: 'present' + update_cache: true + +- name: 'Copy conf files' + template: + src: 'smb.conf' + dest: '/etc/samba/smb.conf' + mode: '644' + +- name: 'Add Samba user' + expect: + command: 'sudo smbpasswd -a {{ samba_user }}' + responses: + (?i)password: "{{ samba_user_pass }}" + +- name: 'Restart Samba' + systemd: + name: '{{ item }}' + state: 'restarted' + loop: + - 'smbd' diff --git a/roles/samba/templates/smb.conf b/roles/samba/templates/smb.conf new file mode 100644 index 0000000..eb5839a --- /dev/null +++ b/roles/samba/templates/smb.conf @@ -0,0 +1,6 @@ +[data] +path = {{ data_vol_ssd }} +writeable=Yes +create mask=0777 +directory mask=0777 +public=no diff --git a/roles/seafile_docker/defaults/main.yaml b/roles/seafile_docker/defaults/main.yaml new file mode 100644 index 0000000..ed7cad3 --- /dev/null +++ b/roles/seafile_docker/defaults/main.yaml @@ -0,0 +1,14 @@ +# Default Values +--- + +seafile_image_repo: 'docker.seadrive.org' +seafile_image_name: '{{ seafile_image_repo }}/seafileltd/seafile-pro-mc' +seafile_image_tag: 'latest' +seafile_image_user: 'seafile' +seafile_image_password: '' +seafile_data_root: '{{ data_vol_path }}/seafile' +seafile_mysql_root_password: '' +seafile_timezone: 'America/Toronto' +seafile_admin_email: '' +seafile_admin_password: '' +seafile_listen_port: '' diff --git a/roles/seafile_docker/files/ccnet.conf b/roles/seafile_docker/files/ccnet.conf new file mode 100644 index 0000000..b1b35fd --- /dev/null +++ b/roles/seafile_docker/files/ccnet.conf @@ -0,0 +1,2 @@ +[General] +SERVICE_URL = http://{{ inventory_hostname }}/seafile diff --git a/roles/seafile_docker/files/seafdav.conf b/roles/seafile_docker/files/seafdav.conf new file mode 100644 index 0000000..8ce50cf --- /dev/null +++ b/roles/seafile_docker/files/seafdav.conf @@ -0,0 +1,6 @@ +[WEBDAV] +enabled = true +port = 8080 +fastcgi = false +host = 0.0.0.0 +share_name = /seafdav diff --git a/roles/seafile_docker/files/seafile.service b/roles/seafile_docker/files/seafile.service new file mode 100644 index 0000000..593ae64 --- /dev/null +++ b/roles/seafile_docker/files/seafile.service @@ -0,0 +1,14 @@ +[Unit] +Description=Seafile +After=network.target + +[Service] +Type=forking +ExecStart=/opt/seafile/seafile-server-latest/seafile.sh start +ExecStop=/opt/seafile/seafile-server-latest/seafile.sh stop +LimitNOFILE=infinity +User=seafserver +Group=seafserver + +[Install] +WantedBy=multi-user.target diff --git a/roles/seafile_docker/files/seahub.service b/roles/seafile_docker/files/seahub.service new file mode 100644 index 0000000..83afd48 --- /dev/null +++ b/roles/seafile_docker/files/seahub.service @@ -0,0 +1,13 @@ +[Unit] +Description=Seahub +After=network.target seafile.service + +[Service] +Type=forking +ExecStart=/opt/seafile/seafile-server-latest/seahub.sh start +ExecStop=/opt/seafile/seafile-server-latest/seahub.sh stop +User=seafserver +Group=seafserver + +[Install] +WantedBy=multi-user.target diff --git a/roles/seafile_docker/tasks/main.yaml b/roles/seafile_docker/tasks/main.yaml new file mode 100644 index 0000000..b40e2fc --- /dev/null +++ b/roles/seafile_docker/tasks/main.yaml @@ -0,0 +1,49 @@ +# https://manual.seafile.com/docker/pro-edition/deploy_seafile_pro_with_docker/ +--- + +- name: 'Ensure required vars exist' + assert: + that: item | mandatory + loop: + - '{{ data_vol_path }}' + - '{{ seafile_image_repo }}' + - '{{ seafile_image_name }}' + - '{{ seafile_image_tag }}' + - '{{ seafile_image_user }}' + - '{{ seafile_image_password }}' + - '{{ seafile_data_root }}' + - '{{ seafile_mysql_root_password }}' + - '{{ seafile_timezone }}' + - '{{ seafile_admin_email }}' + - '{{ seafile_admin_password }}' + - '{{ seafile_listen_port }}' + no_log: true + +- name: 'Log into DockerHub' + community.docker.docker_login: + registry_url: '{{ seafile_image_repo }}' + username: '{{ seafile_image_user }}' + password: '{{ seafile_image_password }}' + +- name: 'Pull Seafile Pro image' + community.docker.docker_image: + name: '{{ seafile_image_name }}' + tag: '{{ seafile_image_tag }}' + state: 'present' + source: 'pull' + +- name: 'Ensure Seafile data dir exists' + file: + path: '{{ seafile_data_root }}' + state: 'directory' + mode: '755' + +- name: 'Copy Seafile docker compose config to seafile dir' + template: + dest: '{{ seafile_data_root }}/docker-compose.yaml' + src: 'docker-compose.yaml.j2' + mode: '644' + +- name: 'Create and start services' + community.docker.docker_compose: + project_src: '{{ seafile_data_root }}' diff --git a/roles/seafile_docker/templates/docker-compose.yaml.j2 b/roles/seafile_docker/templates/docker-compose.yaml.j2 new file mode 100644 index 0000000..3dd34b3 --- /dev/null +++ b/roles/seafile_docker/templates/docker-compose.yaml.j2 @@ -0,0 +1,61 @@ +version: '2.0' +services: + db: + image: mariadb:10.5 + container_name: seafile-mysql + environment: + - MYSQL_ROOT_PASSWORD={{ seafile_mysql_root_password }} + - MYSQL_LOG_CONSOLE=true + volumes: + - {{ seafile_data_root }}/seafile-mysql/db:/var/lib/mysql + networks: + - seafile-net + + memcached: + image: memcached:1.5.6 + container_name: seafile-memcached + entrypoint: memcached -m 256 + networks: + - seafile-net + + elasticsearch: + image: seafileltd/elasticsearch-with-ik:5.6.16 + container_name: seafile-elasticsearch + environment: + - discovery.type=single-node + - bootstrap.memory_lock=true + - "ES_JAVA_OPTS=-Xms1g -Xmx1g" + ulimits: + memlock: + soft: -1 + hard: -1 + mem_limit: 2g + volumes: + - {{ seafile_data_root }}/seafile-elasticsearch/data:/usr/share/elasticsearch/data + networks: + - seafile-net + + seafile: + image: {{ seafile_image_name }}:{{ seafile_image_tag }} + container_name: seafile + ports: + - "{{ seafile_listen_port }}:80" + volumes: + - {{ seafile_data_root }}/seafile-data:/shared + environment: + - DB_HOST=db + - DB_ROOT_PASSWD={{ seafile_mysql_root_password }} + - TIME_ZONE={{ seafile_timezone }} + - SEAFILE_ADMIN_EMAIL={{ seafile_admin_email }} + - SEAFILE_ADMIN_PASSWORD={{ seafile_admin_password }} + - SEAFILE_SERVER_LETSENCRYPT=false + #- SEAFILE_SERVER_HOSTNAME=example.seafile.com # Specifies your host name if https is enabled + depends_on: + - db + - memcached + - elasticsearch + networks: + - seafile-net + +networks: + seafile-net: diff --git a/roles/seafile_non_docker/README.md b/roles/seafile_non_docker/README.md new file mode 100644 index 0000000..e806c13 --- /dev/null +++ b/roles/seafile_non_docker/README.md @@ -0,0 +1,60 @@ +# Ansible - Seafile + +## Example Playbook +``` +# Direct Access: http://art-jr:8000/ +# Domain Access: http://kumardamani.xyz/files +- hosts: compute + gather_facts: false + tags: 'seafile' + become: true + roles: + - role: seafile_non_docker + vars: + seafile_user_email: 'kumar@kumardamani.xyz' + seafile_user_password: '{{ vault_seafile_user_password }}' + seafile_domain: 'kumardamani.xyz' + seafile_data_dir: '{{ data_vol_hdd }}/seafile' +``` + +## Nginx Conf +``` + location /files/ { + proxy_pass http://127.0.0.1:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $server_name; + proxy_set_header X-Forwarded-Proto https; + proxy_read_timeout 1200s; + # used for view/edit office file via Office Online Server + client_max_body_size 0; + error_log /var/log/nginx/seahub.error.log; + } + location /seafhttp/ { + rewrite ^/seafhttp(.*)$ $1 break; + proxy_pass http://127.0.0.1:8082; + client_max_body_size 0; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_connect_timeout 36000s; + proxy_read_timeout 36000s; + proxy_send_timeout 36000s; + send_timeout 36000s; + error_log /var/log/nginx/seafhttp.error.log; + } + location /seafmedia/ { + rewrite ^/seafmedia(.*)$ /media$1 break; + root /opt/seafile/seafile-server-latest/seahub; + } + location /seafdav/ { + proxy_pass http://127.0.0.1:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $server_name; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 1200s; + client_max_body_size 0; + error_log /var/log/nginx/seafdav.error.log; + } +``` diff --git a/roles/seafile_non_docker/defaults/main.yaml b/roles/seafile_non_docker/defaults/main.yaml new file mode 100644 index 0000000..9df3048 --- /dev/null +++ b/roles/seafile_non_docker/defaults/main.yaml @@ -0,0 +1,10 @@ +# Default Values +--- + +# https://github.com/haiwen/seafile-rpi/releases +seafile_version: '8.0.7' +seafile_download_url: 'https://github.com/haiwen/seafile-rpi/releases/download/v{{ seafile_version }}/seafile-server-{{ seafile_version }}-buster-armv7l.tar.gz' + +seafile_install_dir: '/opt/seafile' +seafile_data_dir: '' +seafile_user_grp: '{{ ansible_user }}' diff --git a/roles/seafile_non_docker/handlers/main.yaml b/roles/seafile_non_docker/handlers/main.yaml new file mode 100644 index 0000000..73f755f --- /dev/null +++ b/roles/seafile_non_docker/handlers/main.yaml @@ -0,0 +1,19 @@ +--- + +- name: 'Restart nginx' + systemd: + name: 'nginx' + state: 'restarted' + listen: 'nginx_restart' + +- name: 'Restart Seafile' + systemd: + name: 'seafile' + state: 'restarted' + listen: 'seafile restart' + +- name: 'Restart Seahub' + systemd: + name: 'seahub' + state: 'restarted' + listen: 'seahub restart' diff --git a/roles/seafile_non_docker/tasks/main.yaml b/roles/seafile_non_docker/tasks/main.yaml new file mode 100644 index 0000000..597c389 --- /dev/null +++ b/roles/seafile_non_docker/tasks/main.yaml @@ -0,0 +1,220 @@ +# https://seafile.readthedocs.io/en/latest/config/seafile/seadav/#seafdav-configuration +# https://manual.seafile.com/deploy/using_sqlite/ +# https://manual.seafile.com/deploy/using_sqlite/#setup-in-non-interactive-way +# https://manual.seafile.com/deploy/deploy_with_nginx +# https://manual.seafile.com/deploy/deploy_seahub_at_non-root_domain/ +# https://seafile.readthedocs.io/en/latest/installation/seafile/#enable-seafile-server-autostart-systemd +--- + +- name: 'install required apt packages' + apt: + name: + - 'libmemcached-dev' + - 'memcached' + - 'pwgen' + - 'python3' + - 'python3-setuptools' + - 'python3-pip' + - 'sqlite3' + - 'libopenjp2-7' + - 'libtiff5' + state: 'present' + update_cache: true + +- name: 'install required pip packages' + become_user: '{{ seafile_user_grp }}' + pip: + name: + - 'captcha' + - 'django==2.2.*' + - 'django-pylibmc' + - 'django-simple-captcha' + - 'future' + - 'jinja2' + - 'Pillow' + - 'psd-tools' + - 'pylibmc' + - 'pexpect' + state: 'present' + +- name: 'create seafile installation and data directories' + file: + path: '{{ item.path }}' + state: 'directory' + owner: '{{ item.owner }}' + group: '{{ item.group }}' + mode: '{{ item.mode }}' + loop: + - {path: '{{ seafile_install_dir }}/installed', owner: '{{ seafile_user_grp }}', group: '{{ seafile_user_grp }}', mode: '775'} + - {path: '{{ seafile_data_dir }}', owner: '{{ ansible_user }}', group: '{{ ansible_user }}', mode: '755'} + +- name: 'download seafile package to local folder' + become_user: '{{ seafile_user_grp }}' + get_url: + url: '{{ seafile_download_url }}' + dest: '{{ seafile_install_dir }}/installed/seafile-{{ seafile_version }}.tar.gz' + mode: '644' + register: '_download_archive' + until: '_download_archive is succeeded' + retries: 5 + delay: 2 + check_mode: false + +- name: 'unpack seafile binaries' + become_user: '{{ seafile_user_grp }}' + unarchive: + src: '{{ seafile_install_dir }}/installed/seafile-{{ seafile_version }}.tar.gz' + dest: '{{ seafile_install_dir }}' + remote_src: true + creates: '{{ seafile_install_dir }}/seafile-server-{{ seafile_version }}' + check_mode: false + +# this hack is needed for this version only +- name: 'Remove old PIL dir' + become_user: '{{ seafile_user_grp }}' + file: + path: '{{ seafile_install_dir }}/seafile-server-{{ seafile_version }}/seahub/thirdpart/PIL' + state: 'absent' + when: '"buster" in seafile_download_url' + +# RUN NON-INTERACTIVELY +- name: 'Run setup script' # noqa no-changed-when command-instead-of-shell + become_user: '{{ seafile_user_grp }}' + shell: './setup-seafile.sh auto -n "{{ inventory_hostname }}" -i "0.0.0.0"' + args: + executable: '/bin/bash' + chdir: '{{ seafile_install_dir }}/seafile-server-{{ seafile_version }}' + creates: '{{ seafile_install_dir }}/conf/ccnet.conf' + +- name: 'Get stats of the data object' + stat: + path: '{{ seafile_install_dir }}/seafile-data' + register: sym + +- block: + - name: 'Ensure Seafile is not running yet' # noqa command-instead-of-shell + become_user: '{{ seafile_user_grp }}' + shell: './seafile.sh stop' + args: + executable: '/bin/bash' + chdir: '{{ seafile_install_dir }}/seafile-server-{{ seafile_version }}' + + - name: 'Move old data to new data' # noqa risky-file-permissions + become_user: '{{ seafile_user_grp }}' + copy: + src: '{{ seafile_install_dir }}/seafile-data/' + dest: '{{ seafile_data_dir }}' + remote_src: true + + - name: 'Delete old data dir' + become_user: '{{ seafile_user_grp }}' + file: + path: '{{ seafile_install_dir }}/seafile-data' + state: 'absent' + + - name: 'Create symlink to new data dir' + become_user: '{{ seafile_user_grp }}' + file: + src: '{{ seafile_data_dir }}' + dest: '{{ seafile_install_dir }}/seafile-data' + state: 'link' + when: sym.stat.islnk is defined and (not sym.stat.islnk) # => first time setup + +- name: 'Copy conf files' + become_user: '{{ seafile_user_grp }}' + template: + src: '{{ item }}.j2' + dest: '{{ seafile_install_dir }}/conf/{{ item }}' + mode: '644' + loop: + - 'ccnet.conf' + - 'seafdav.conf' + notify: + - 'seafile restart' + - 'seahub restart' + +- name: 'Update gunicorn conf' + lineinfile: + path: '{{ seafile_install_dir }}/conf/gunicorn.conf.py' + regexp: '^bind' + line: 'bind = "127.0.0.1:8000"' + notify: + - 'seahub restart' + +# some stuff needed for nginx based install +- name: 'Update seahub_settings.py' + become_user: '{{ seafile_user_grp }}' + blockinfile: + path: '{{ seafile_install_dir }}/conf/seahub_settings.py' + insertafter: '^SECRET_KEY' + block: | + DEBUG = False + FILE_SERVER_ROOT = 'https://{{ seafile_domain }}/seafhttp' + SERVE_STATIC = False + MEDIA_URL = '/seafmedia/' + COMPRESS_URL = MEDIA_URL + STATIC_URL = MEDIA_URL + 'assets/' + SITE_ROOT = '/files/' + LOGIN_URL = '/files/accounts/login/' + notify: + - 'seahub restart' + +- block: + - name: 'Run Seafile start script' # noqa command-instead-of-shell + become_user: '{{ seafile_user_grp }}' + shell: './seafile.sh start' + args: + executable: '/bin/bash' + chdir: '{{ seafile_install_dir }}/seafile-server-{{ seafile_version }}' + + - name: 'Init Seahub with a user' + become_user: '{{ seafile_user_grp }}' + expect: + command: './seahub.sh start' + chdir: '{{ seafile_install_dir }}/seafile-server-{{ seafile_version }}' + responses: + (?i)email: "{{ seafile_user_email }}" + (?i)password: "{{ seafile_user_password }}" + + when: sym.stat.islnk is defined and (not sym.stat.islnk) # => first time setup + +- name: 'Create systemd services' + template: + src: '{{ item }}.j2' + dest: '/etc/systemd/system/{{ item }}' + mode: '644' + loop: + - 'seafile.service' + - 'seahub.service' + notify: + - 'seafile restart' + - 'seahub restart' + +- name: 'Reload systemd units' + systemd: + daemon_reload: true + +- block: + - name: 'Run Seahub start script' # noqa command-instead-of-shell + become_user: '{{ seafile_user_grp }}' + shell: './seahub.sh stop' + args: + executable: '/bin/bash' + chdir: '{{ seafile_install_dir }}/seafile-server-{{ seafile_version }}' + + - name: 'Run Seafile stop script' # noqa command-instead-of-shell + become_user: '{{ seafile_user_grp }}' + shell: './seafile.sh stop' + args: + executable: '/bin/bash' + chdir: '{{ seafile_install_dir }}/seafile-server-{{ seafile_version }}' + when: sym.stat.islnk is defined and (not sym.stat.islnk) # => first time setup + +- name: 'Start Seafile and Seahub' + systemd: + name: '{{ item }}' + state: 'started' + enabled: true + loop: + - 'seafile' + - 'seahub' diff --git a/roles/seafile_non_docker/templates/ccnet.conf.j2 b/roles/seafile_non_docker/templates/ccnet.conf.j2 new file mode 100644 index 0000000..d6d69cb --- /dev/null +++ b/roles/seafile_non_docker/templates/ccnet.conf.j2 @@ -0,0 +1,2 @@ +[General] +SERVICE_URL = https://{{ seafile_domain }}/files diff --git a/roles/seafile_non_docker/templates/seafdav.conf.j2 b/roles/seafile_non_docker/templates/seafdav.conf.j2 new file mode 100644 index 0000000..54712a1 --- /dev/null +++ b/roles/seafile_non_docker/templates/seafdav.conf.j2 @@ -0,0 +1,6 @@ +[WEBDAV] +enabled = true +port = 8080 +fastcgi = false +host = 127.0.0.1 +share_name = /seafdav diff --git a/roles/seafile_non_docker/templates/seafile.service.j2 b/roles/seafile_non_docker/templates/seafile.service.j2 new file mode 100644 index 0000000..8e4c94f --- /dev/null +++ b/roles/seafile_non_docker/templates/seafile.service.j2 @@ -0,0 +1,14 @@ +[Unit] +Description=Seafile +After=network.target + +[Service] +Type=forking +ExecStart={{ seafile_install_dir }}/seafile-server-latest/seafile.sh start +ExecStop={{ seafile_install_dir }}/seafile-server-latest/seafile.sh stop +LimitNOFILE=infinity +User={{ seafile_user_grp }} +Group={{ seafile_user_grp }} + +[Install] +WantedBy=multi-user.target diff --git a/roles/seafile_non_docker/templates/seahub.service.j2 b/roles/seafile_non_docker/templates/seahub.service.j2 new file mode 100644 index 0000000..aa61a83 --- /dev/null +++ b/roles/seafile_non_docker/templates/seahub.service.j2 @@ -0,0 +1,14 @@ +[Unit] +Description=Seahub +After=network.target seafile.service + +[Service] +Type=forking +ExecStart={{ seafile_install_dir }}/seafile-server-latest/seahub.sh start +ExecStop={{ seafile_install_dir }}/seafile-server-latest/seahub.sh stop +LimitNOFILE=infinity +User={{ seafile_user_grp }} +Group={{ seafile_user_grp }} + +[Install] +WantedBy=multi-user.target diff --git a/roles/website/defaults/main.yaml b/roles/website/defaults/main.yaml new file mode 100644 index 0000000..47cf58c --- /dev/null +++ b/roles/website/defaults/main.yaml @@ -0,0 +1,4 @@ +# Default Values +--- + +websites: [] diff --git a/roles/website/tasks/main.yaml b/roles/website/tasks/main.yaml new file mode 100644 index 0000000..6fdf7fa --- /dev/null +++ b/roles/website/tasks/main.yaml @@ -0,0 +1,24 @@ +--- + +- name: 'Install git' + package: + name: 'git' + state: 'present' + +- name: 'Create domain level dir' + file: + path: '/var/www/{{ item.name }}/html' + state: 'directory' + mode: '0755' + loop: '{{ websites }}' + +- name: 'Clone all the website project repos' + git: + repo: '{{ item.git_url }}' + dest: '/var/www/{{ item.name }}/html' + depth: 1 + update: true + force: true + version: '{{ item.git_version }}' + umask: '002' + loop: '{{ websites }}' diff --git a/roles/wireguard/defaults/main.yaml b/roles/wireguard/defaults/main.yaml new file mode 100644 index 0000000..9a163d4 --- /dev/null +++ b/roles/wireguard/defaults/main.yaml @@ -0,0 +1,2 @@ +# Default Values +--- diff --git a/roles/wireguard/handlers/main.yaml b/roles/wireguard/handlers/main.yaml new file mode 100644 index 0000000..bd5fc0d --- /dev/null +++ b/roles/wireguard/handlers/main.yaml @@ -0,0 +1,7 @@ +--- + +- name: 'Restart wireguard' + systemd: + name: 'wg-quick@wg0' + state: 'restarted' + listen: 'restart wireguard' diff --git a/roles/wireguard/tasks/main.yaml b/roles/wireguard/tasks/main.yaml new file mode 100644 index 0000000..d7e1e51 --- /dev/null +++ b/roles/wireguard/tasks/main.yaml @@ -0,0 +1,38 @@ +# https://dev.to/tangramvision/exploring-ansible-via-setting-up-a-wireguard-vpn-3389 +--- + +- name: 'install wireguard package' + apt: + name: 'wireguard' + state: 'present' + update_cache: true + +- name: 'create server wireguard config' + template: + dest: '/etc/wireguard/wg0.conf' + src: 'wg0.conf.j2' + owner: 'root' + group: 'root' + mode: '600' + notify: 'restart wireguard' + +- name: 'Enable IP Forwarding' + sysctl: + name: '{{ item.sysctl_key }}' + reload: true + state: 'present' + sysctl_set: true + value: '{{ item.sysctl_val }}' + loop: + - sysctl_key: 'net.ipv4.ip_forward' + sysctl_val: "1" + - sysctl_key: 'net.ipv6.conf.all.disable_ipv6' + sysctl_val: "0" + - sysctl_key: 'net.ipv6.conf.all.forwarding' + sysctl_val: "1" + +- name: 'start wireguard and enable on boot' + systemd: + name: 'wg-quick@wg0' + state: 'started' + enabled: true diff --git a/roles/wireguard/templates/wg0.conf.j2 b/roles/wireguard/templates/wg0.conf.j2 new file mode 100644 index 0000000..6e7bcc1 --- /dev/null +++ b/roles/wireguard/templates/wg0.conf.j2 @@ -0,0 +1,17 @@ +# {{ ansible_managed }} + +[Interface] +Address = {{ wireguard_server_ip }} +ListenPort = {{ wireguard_server_listen_port }} +PrivateKey = {{ wireguard_server_priv_key }} +PostUp = iptables -A FORWARD -i wg0 -o wg0 -j ACCEPT +PostUp = iptables -w -t nat -A POSTROUTING -o eth0 -j MASQUERADE; ip6tables -w -t nat -A POSTROUTING -o eth0 -j MASQUERADE +PostDown = iptables -w -t nat -D POSTROUTING -o eth0 -j MASQUERADE; ip6tables -w -t nat -D POSTROUTING -o eth0 -j MASQUERADE + + +{% for peer in wireguard_peers %} +[Peer] +PublicKey = {{ peer.peer_key }} +AllowedIPs = {{ peer.peer_ip}} + +{% endfor %} diff --git a/roles/wireguard_client/defaults/main.yaml b/roles/wireguard_client/defaults/main.yaml new file mode 100644 index 0000000..9a163d4 --- /dev/null +++ b/roles/wireguard_client/defaults/main.yaml @@ -0,0 +1,2 @@ +# Default Values +--- diff --git a/roles/wireguard_client/handlers/main.yaml b/roles/wireguard_client/handlers/main.yaml new file mode 100644 index 0000000..bd5fc0d --- /dev/null +++ b/roles/wireguard_client/handlers/main.yaml @@ -0,0 +1,7 @@ +--- + +- name: 'Restart wireguard' + systemd: + name: 'wg-quick@wg0' + state: 'restarted' + listen: 'restart wireguard' diff --git a/roles/wireguard_client/tasks/main.yaml b/roles/wireguard_client/tasks/main.yaml new file mode 100644 index 0000000..04814ba --- /dev/null +++ b/roles/wireguard_client/tasks/main.yaml @@ -0,0 +1,36 @@ +# https://dev.to/tangramvision/exploring-ansible-via-setting-up-a-wireguard-vpn-3389 +--- + +- name: 'Enable Debian 10 buster backports repo' + shell: + cmd: 'echo "deb http://deb.debian.org/debian buster-backports main contrib non-free" > /etc/apt/sources.list.d/buster-backports.list' + creates: '/etc/apt/sources.list.d/buster-backports.list' + +- name: 'Install Wireguard' + apt: + name: 'wireguard' + state: 'present' + update_cache: true + +- name: 'enable and persist ip forwarding' + sysctl: + name: 'net.ipv4.ip_forward' + value: "1" + state: 'present' + sysctl_set: true + reload: true + +- name: 'generate private key' + shell: + cmd: 'umask 077 && wg genkey | tee privatekey | wg pubkey > publickey' + chdir: '/etc/wireguard' + creates: '/etc/wireguard/publickey' + +- name: 'create client wireguard config' + template: + dest: '/etc/wireguard/wg0.conf' + src: 'wg0.conf.j2' + owner: 'root' + group: 'root' + mode: '600' + notify: 'restart wireguard' diff --git a/roles/wireguard_client/templates/wg0.conf.j2 b/roles/wireguard_client/templates/wg0.conf.j2 new file mode 100644 index 0000000..d667d8a --- /dev/null +++ b/roles/wireguard_client/templates/wg0.conf.j2 @@ -0,0 +1,16 @@ +# {{ ansible_managed }} + +[Interface] +Address = {{ wireguard_client_peer_ip }} +PostUp = wg set %i private-key /etc/wireguard/privatekey +PostUp = ping -c1 192.168.10.1 +{% for port in wireguard_client_port_forwards %} +PostUp = iptables -t nat -A PREROUTING -p tcp --dport {{ port }} -j DNAT --to-destination 192.168.10.1:{{ port }} +{% endfor %} +PostUp = iptables -t nat -A POSTROUTING -j MASQUERADE + +[Peer] +PublicKey = {{ wireguard_client_server_pub_key }} +AllowedIPs = {{ wireguard_client_server_allowed_ips }} +Endpoint = {{ wireguard_client_server_endpoint }}:{{ wireguard_client_server_listen_port }} +PersistentKeepalive = 20 diff --git a/vaultid.example b/vaultid.example new file mode 100644 index 0000000..9daeafb --- /dev/null +++ b/vaultid.example @@ -0,0 +1 @@ +test |
