From c3200c91db8388d11963a2ffbfe3580e0a20b73c Mon Sep 17 00:00:00 2001 From: Lukas Bestle Date: Mon, 1 Jun 2020 14:04:35 +0200 Subject: [PATCH] Initial commit --- ansible.cfg | 6 + hosts | 14 + requirements.yml | 5 + .../home/.config/fish/functions/radicale.fish | 3 + roles/dav/files/home/bin/radicale_to_ics | 54 + roles/dav/files/home/etc/services.d/.gitkeep | 0 roles/dav/files/home/radicale/rights | 24 + .../files/home/radicale/storage/.gitignore | 3 + roles/dav/handlers/main.yml | 8 + roles/dav/tasks/main.yml | 63 + roles/dav/templates/config.j2 | 16 + roles/dav/templates/service.ini.j2 | 4 + roles/dav/templates/users.j2 | 3 + .../home/.config/fish/functions/gitea.fish | 3 + roles/git/files/home/etc/services.d/.gitkeep | 0 .../git/files/home/gitea/custom/conf/.gitkeep | 0 .../home/gitea/custom/public/img/favicon.png | Bin 0 -> 6341 bytes .../home/gitea/custom/public/img/gitea-lq.png | Bin 0 -> 26900 bytes .../gitea/custom/public/img/gitea-safari.svg | 1 + .../home/gitea/custom/public/img/gitea-sm.png | Bin 0 -> 2583 bytes .../custom/templates/base/footer_content.tmpl | 24 + .../custom/templates/base/head_navbar.tmpl | 133 ++ roles/git/files/home/gitea/data/.gitkeep | 0 roles/git/files/home/gitea/log/.gitkeep | 0 roles/git/files/home/gitea/repos/.gitkeep | 0 roles/git/handlers/main.yml | 8 + roles/git/tasks/main.yml | 47 + roles/git/templates/app.ini.j2 | 73 ++ roles/git/templates/service.ini.j2 | 5 + roles/homebrew/handlers/main.yml | 3 + roles/homebrew/tasks/main.yml | 13 + roles/host-site/tasks/main.yml | 11 + roles/host-site/templates/index.html.j2 | 127 ++ roles/mail-redirect/tasks/main.yml | 15 + roles/mail/tasks/main.yml | 59 + roles/mail/templates/muttrc.j2 | 32 + roles/mail/templates/qmail-dated.j2 | 3 + roles/terminal/files/authorized_keys | 4 + .../dotfiles/.composer/vendor/bin/.gitkeep | 0 .../dotfiles/.config/fish/conf.d/pkgs.fish | 60 + .../files/dotfiles/.config/fish/config.fish | 46 + .../.config/fish/functions/fish_greeting.fish | 3 + .../.config/fish/functions/nsreverse.fish | 21 + .../.config/fish/functions/nsreverse6.fish | 21 + .../files/dotfiles/.config/fish/pkgs/.gitkeep | 0 .../files/dotfiles/.config/git/.gitkeep | 0 .../dotfiles/.config/welcome/colossal.flf | 1136 +++++++++++++++++ .../.config/welcome/summarizers/01-date.sh | 6 + .../.config/welcome/summarizers/02-uptime.sh | 111 ++ .../.config/welcome/summarizers/03-quota.sh | 12 + .../.config/welcome/summarizers/04-ram.sh | 8 + .../dotfiles/.config/welcome/warners/.gitkeep | 0 .../files/dotfiles/.local/bin/.gitkeep | 0 roles/terminal/handlers/main.yml | 3 + roles/terminal/tasks/main.yml | 58 + roles/terminal/templates/gitconfig.j2 | 17 + .../web/files/home/.config/projectr/.gitkeep | 0 roles/web/files/home/bin/qdated-generate | 5 + roles/web/tasks/main.yml | 102 ++ roles/web/templates/deploy.php.j2 | 172 +++ run.sh | 9 + site.yml | 46 + vars/config.yml | 51 + vars/installs.yml | 20 + vars/vault.yml | 30 + 65 files changed, 2701 insertions(+) create mode 100644 ansible.cfg create mode 100644 hosts create mode 100644 requirements.yml create mode 100644 roles/dav/files/home/.config/fish/functions/radicale.fish create mode 100755 roles/dav/files/home/bin/radicale_to_ics create mode 100644 roles/dav/files/home/etc/services.d/.gitkeep create mode 100644 roles/dav/files/home/radicale/rights create mode 100644 roles/dav/files/home/radicale/storage/.gitignore create mode 100644 roles/dav/handlers/main.yml create mode 100644 roles/dav/tasks/main.yml create mode 100644 roles/dav/templates/config.j2 create mode 100644 roles/dav/templates/service.ini.j2 create mode 100644 roles/dav/templates/users.j2 create mode 100644 roles/git/files/home/.config/fish/functions/gitea.fish create mode 100644 roles/git/files/home/etc/services.d/.gitkeep create mode 100644 roles/git/files/home/gitea/custom/conf/.gitkeep create mode 100644 roles/git/files/home/gitea/custom/public/img/favicon.png create mode 100644 roles/git/files/home/gitea/custom/public/img/gitea-lq.png create mode 100644 roles/git/files/home/gitea/custom/public/img/gitea-safari.svg create mode 100644 roles/git/files/home/gitea/custom/public/img/gitea-sm.png create mode 100644 roles/git/files/home/gitea/custom/templates/base/footer_content.tmpl create mode 100644 roles/git/files/home/gitea/custom/templates/base/head_navbar.tmpl create mode 100644 roles/git/files/home/gitea/data/.gitkeep create mode 100644 roles/git/files/home/gitea/log/.gitkeep create mode 100644 roles/git/files/home/gitea/repos/.gitkeep create mode 100644 roles/git/handlers/main.yml create mode 100644 roles/git/tasks/main.yml create mode 100644 roles/git/templates/app.ini.j2 create mode 100644 roles/git/templates/service.ini.j2 create mode 100644 roles/homebrew/handlers/main.yml create mode 100644 roles/homebrew/tasks/main.yml create mode 100644 roles/host-site/tasks/main.yml create mode 100644 roles/host-site/templates/index.html.j2 create mode 100644 roles/mail-redirect/tasks/main.yml create mode 100644 roles/mail/tasks/main.yml create mode 100644 roles/mail/templates/muttrc.j2 create mode 100644 roles/mail/templates/qmail-dated.j2 create mode 100644 roles/terminal/files/authorized_keys create mode 100644 roles/terminal/files/dotfiles/.composer/vendor/bin/.gitkeep create mode 100644 roles/terminal/files/dotfiles/.config/fish/conf.d/pkgs.fish create mode 100644 roles/terminal/files/dotfiles/.config/fish/config.fish create mode 100644 roles/terminal/files/dotfiles/.config/fish/functions/fish_greeting.fish create mode 100644 roles/terminal/files/dotfiles/.config/fish/functions/nsreverse.fish create mode 100644 roles/terminal/files/dotfiles/.config/fish/functions/nsreverse6.fish create mode 100644 roles/terminal/files/dotfiles/.config/fish/pkgs/.gitkeep create mode 100644 roles/terminal/files/dotfiles/.config/git/.gitkeep create mode 100644 roles/terminal/files/dotfiles/.config/welcome/colossal.flf create mode 100755 roles/terminal/files/dotfiles/.config/welcome/summarizers/01-date.sh create mode 100755 roles/terminal/files/dotfiles/.config/welcome/summarizers/02-uptime.sh create mode 100755 roles/terminal/files/dotfiles/.config/welcome/summarizers/03-quota.sh create mode 100755 roles/terminal/files/dotfiles/.config/welcome/summarizers/04-ram.sh create mode 100644 roles/terminal/files/dotfiles/.config/welcome/warners/.gitkeep create mode 100644 roles/terminal/files/dotfiles/.local/bin/.gitkeep create mode 100644 roles/terminal/handlers/main.yml create mode 100644 roles/terminal/tasks/main.yml create mode 100644 roles/terminal/templates/gitconfig.j2 create mode 100644 roles/web/files/home/.config/projectr/.gitkeep create mode 100755 roles/web/files/home/bin/qdated-generate create mode 100644 roles/web/tasks/main.yml create mode 100644 roles/web/templates/deploy.php.j2 create mode 100755 run.sh create mode 100644 site.yml create mode 100644 vars/config.yml create mode 100644 vars/installs.yml create mode 100755 vars/vault.yml diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..c4ec182 --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,6 @@ +[defaults] +inventory = hosts +transport = ssh + +[ssh_connection] +pipelining = true diff --git a/hosts b/hosts new file mode 100644 index 0000000..1a19aba --- /dev/null +++ b/hosts @@ -0,0 +1,14 @@ +[dav] +kodos-cdddav.codesignd.net + +[git] +kodos-cddgit.codesignd.net + +[mail] +kodos-cddmail[1:2].codesignd.net + +[misc] +kodos-cddmisc1.codesignd.net + +[web] +kodos-cddweb[1:2].codesignd.net diff --git a/requirements.yml b/requirements.yml new file mode 100644 index 0000000..a4e1ab1 --- /dev/null +++ b/requirements.yml @@ -0,0 +1,5 @@ +--- +- src: snapstromegon.uberspace_mail_catchall +- src: snapstromegon.uberspace_mail_user +- src: snapstromegon.uberspace_web_backend +- src: snapstromegon.uberspace_web_domain diff --git a/roles/dav/files/home/.config/fish/functions/radicale.fish b/roles/dav/files/home/.config/fish/functions/radicale.fish new file mode 100644 index 0000000..fcf443c --- /dev/null +++ b/roles/dav/files/home/.config/fish/functions/radicale.fish @@ -0,0 +1,3 @@ +function radicale + ~/.local/bin/radicale --config ~/radicale/config $argv +end diff --git a/roles/dav/files/home/bin/radicale_to_ics b/roles/dav/files/home/bin/radicale_to_ics new file mode 100755 index 0000000..008538e --- /dev/null +++ b/roles/dav/files/home/bin/radicale_to_ics @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +######################################################################## +# 2018-11-25 Lukas Bestle +######################################################################## +# Concats a directory of ICS files to a single ICS file for sharing +# +# Usage: radicale_to_ics [] +# +# Directory with ICS files (e.g. from Radicale) +# Output path for the concatenated ICS file +# Optional name of the ICS file +# (displayed in clients) +######################################################################## + +directory="$1" +output="$2" +calname="$3" + +if [[ -z "$output" ]]; then + # Print help + echo -e "\033[1mUsage:\033[0m \033[34mradicale_to_ics\033[0m []" + exit 1 +fi + +# Place temporary file right next to the output file +# Will be moved when done with processing +tmp="$output.tmp" + +# iCalendar header +echo "BEGIN:VCALENDAR" > "$tmp" +echo "VERSION:2.0" >> "$tmp" +echo "X-WR-TIMEZONE:Europe/Berlin" >> "$tmp" +echo "CALSCALE:GREGORIAN" >> "$tmp" +echo "METHOD:PUBLISH" >> "$tmp" + +# Add calendar name if given +if [[ -n "$calname" ]]; then + echo "X-WR-CALNAME:$calname" >> "$tmp" +fi + +# Concat all event files in the given directory +# unless the directory is empty +if [[ "$(ls "$directory")" ]]; then + for file in "$directory"/*; do + # Extract all VEVENT elements (supports multiple per input file) + sed '/^BEGIN:VEVENT/, /^END:VEVENT/!d' "$file" >> "$tmp" + done +fi + +# Add iCalendar footer +echo "END:VCALENDAR" >> "$tmp" + +# Move to final destination +mv "$tmp" "$output" diff --git a/roles/dav/files/home/etc/services.d/.gitkeep b/roles/dav/files/home/etc/services.d/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/roles/dav/files/home/radicale/rights b/roles/dav/files/home/radicale/rights new file mode 100644 index 0000000..30f3a43 --- /dev/null +++ b/roles/dav/files/home/radicale/rights @@ -0,0 +1,24 @@ +# The user "lukas" can read and write any collection +[admin] +user: lukas +collection: .* +permissions: RrWw + +# Allow reading root collection for authenticated users (required for the web interface) +[root] +user: .+ +collection: +permissions: R + +# Allow reading and writing principal collection (same as user name) +[principal] +user: .+ +collection: {user} +permissions: RW + +# Allow reading and writing calendars and address books that are direct +# children of the principal collection +[calendars] +user: .+ +collection: {user}/[^/]+ +permissions: rw diff --git a/roles/dav/files/home/radicale/storage/.gitignore b/roles/dav/files/home/radicale/storage/.gitignore new file mode 100644 index 0000000..7cced80 --- /dev/null +++ b/roles/dav/files/home/radicale/storage/.gitignore @@ -0,0 +1,3 @@ +.Radicale.cache +.Radicale.lock +.Radicale.tmp-* diff --git a/roles/dav/handlers/main.yml b/roles/dav/handlers/main.yml new file mode 100644 index 0000000..3a4bdc0 --- /dev/null +++ b/roles/dav/handlers/main.yml @@ -0,0 +1,8 @@ +--- +- name: Reread service config + command: supervisorctl reread + +- name: Restart Radicale + supervisorctl: + name: radicale + state: restarted diff --git a/roles/dav/tasks/main.yml b/roles/dav/tasks/main.yml new file mode 100644 index 0000000..fa714cf --- /dev/null +++ b/roles/dav/tasks/main.yml @@ -0,0 +1,63 @@ +--- +- name: Add DAV domain to Uberspace config + import_role: + name: snapstromegon.uberspace_web_domain + vars: + domain: dav.bstl.xyz + +- name: Copy config to home directory + copy: + src: home/ + dest: "{{ ansible_env.HOME }}/" + mode: preserve + tags: radicale-update + notify: Restart Radicale + +- name: Copy service config + template: + src: service.ini.j2 + dest: "{{ ansible_env.HOME }}/etc/services.d/radicale.ini" + notify: Reread service config + +- name: Copy app config + template: + src: config.j2 + dest: "{{ ansible_env.HOME }}/radicale/config" + tags: radicale-update + notify: Restart Radicale + +- name: Set up Radicale users + template: + src: users.j2 + dest: "{{ ansible_facts.env.HOME }}/radicale/users" + tags: radicale-update + notify: Restart Radicale + +- name: Configure web backend + import_role: + name: snapstromegon.uberspace_web_backend + vars: + route: dav.bstl.xyz + http: + port: 8080 + +- name: Install and update Radicale + pip: + name: radicale[bcrypt] + state: latest + extra_args: --user + executable: pip3.8 + tags: radicale-update + notify: Restart Radicale + +- name: Initialize storage repository + command: + chdir: "{{ ansible_env.HOME }}/radicale/storage" + cmd: git init + creates: "{{ ansible_env.HOME }}/radicale/storage/.git" + +- name: Set up garbage collection cronjob + cron: + name: "Garbage-collection" + special_time: weekly + job: git --git-dir {{ ansible_env.HOME }}/radicale/storage/.git gc diff --git a/roles/dav/templates/config.j2 b/roles/dav/templates/config.j2 new file mode 100644 index 0000000..44d4f70 --- /dev/null +++ b/roles/dav/templates/config.j2 @@ -0,0 +1,16 @@ +[server] +hosts = 0.0.0.0:8080 + +[auth] +type = htpasswd +htpasswd_filename = {{ ansible_env.HOME }}/radicale/users +htpasswd_encryption = bcrypt +realm = bstl DAV - Password Required + +[rights] +type = from_file +file = {{ ansible_env.HOME }}/radicale/rights + +[storage] +filesystem_folder = {{ ansible_env.HOME }}/radicale/storage +hook = git add -A && (git diff --cached --quiet || git commit -m "Changes by "%(user)s) diff --git a/roles/dav/templates/service.ini.j2 b/roles/dav/templates/service.ini.j2 new file mode 100644 index 0000000..25d013c --- /dev/null +++ b/roles/dav/templates/service.ini.j2 @@ -0,0 +1,4 @@ +[program:radicale] +command={{ ansible_env.HOME }}/.local/bin/radicale --config {{ ansible_env.HOME }}/radicale/config +autostart=yes +autorestart=yes diff --git a/roles/dav/templates/users.j2 b/roles/dav/templates/users.j2 new file mode 100644 index 0000000..78601eb --- /dev/null +++ b/roles/dav/templates/users.j2 @@ -0,0 +1,3 @@ +{% for username, password in dav_users.items() %} +{{ username }}:{{ password }} +{% endfor %} diff --git a/roles/git/files/home/.config/fish/functions/gitea.fish b/roles/git/files/home/.config/fish/functions/gitea.fish new file mode 100644 index 0000000..8f7d4f8 --- /dev/null +++ b/roles/git/files/home/.config/fish/functions/gitea.fish @@ -0,0 +1,3 @@ +function gitea + env GITEA_WORK_DIR=$HOME/gitea ~/.linuxbrew/bin/gitea $argv +end diff --git a/roles/git/files/home/etc/services.d/.gitkeep b/roles/git/files/home/etc/services.d/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/roles/git/files/home/gitea/custom/conf/.gitkeep b/roles/git/files/home/gitea/custom/conf/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/roles/git/files/home/gitea/custom/public/img/favicon.png b/roles/git/files/home/gitea/custom/public/img/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..51fc51f1163678d9500059f08bf5546396cb5a7c GIT binary patch literal 6341 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 Eg1@#FQ2+n{ literal 0 HcmV?d00001 diff --git a/roles/git/files/home/gitea/custom/public/img/gitea-safari.svg b/roles/git/files/home/gitea/custom/public/img/gitea-safari.svg new file mode 100644 index 0000000..066f529 --- /dev/null +++ b/roles/git/files/home/gitea/custom/public/img/gitea-safari.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/roles/git/files/home/gitea/custom/public/img/gitea-sm.png b/roles/git/files/home/gitea/custom/public/img/gitea-sm.png new file mode 100644 index 0000000000000000000000000000000000000000..d515f85b5af07d8b4e260c26ceb0c93ec8584e7e GIT binary patch literal 2583 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 literal 0 HcmV?d00001 diff --git a/roles/git/files/home/gitea/custom/templates/base/footer_content.tmpl b/roles/git/files/home/gitea/custom/templates/base/footer_content.tmpl new file mode 100644 index 0000000..f69533e --- /dev/null +++ b/roles/git/files/home/gitea/custom/templates/base/footer_content.tmpl @@ -0,0 +1,24 @@ +
+
+
+ © Lukas Bestle +
+ +
+
diff --git a/roles/git/files/home/gitea/custom/templates/base/head_navbar.tmpl b/roles/git/files/home/gitea/custom/templates/base/head_navbar.tmpl new file mode 100644 index 0000000..21e8acb --- /dev/null +++ b/roles/git/files/home/gitea/custom/templates/base/head_navbar.tmpl @@ -0,0 +1,133 @@ + diff --git a/roles/git/files/home/gitea/data/.gitkeep b/roles/git/files/home/gitea/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/roles/git/files/home/gitea/log/.gitkeep b/roles/git/files/home/gitea/log/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/roles/git/files/home/gitea/repos/.gitkeep b/roles/git/files/home/gitea/repos/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/roles/git/handlers/main.yml b/roles/git/handlers/main.yml new file mode 100644 index 0000000..3078312 --- /dev/null +++ b/roles/git/handlers/main.yml @@ -0,0 +1,8 @@ +--- +- name: Reread service config + command: supervisorctl reread + +- name: Restart Gitea + supervisorctl: + name: gitea + state: restarted diff --git a/roles/git/tasks/main.yml b/roles/git/tasks/main.yml new file mode 100644 index 0000000..798f8b3 --- /dev/null +++ b/roles/git/tasks/main.yml @@ -0,0 +1,47 @@ +--- +- name: Add Git domain to Uberspace config + import_role: + name: snapstromegon.uberspace_web_domain + vars: + domain: git.codesignd.com + +- name: Copy config to home directory + copy: + src: home/ + dest: "{{ ansible_env.HOME }}/" + mode: preserve + tags: gitea-update + notify: Restart Gitea + +- name: Copy service config + template: + src: service.ini.j2 + dest: "{{ ansible_env.HOME }}/etc/services.d/gitea.ini" + notify: Reread service config + +- name: Copy app config + template: + src: app.ini.j2 + dest: "{{ ansible_env.HOME }}/gitea/custom/conf/app.ini" + tags: gitea-update + notify: Restart Gitea + +- name: Configure web backend + import_role: + name: snapstromegon.uberspace_web_backend + vars: + route: git.codesignd.com + http: + port: 8080 + +- name: Set up Gitea Homebrew tap + homebrew_tap: + name: gitea/tap + url: https://gitea.com/gitea/homebrew-gitea.git + +- name: Install and update Gitea + homebrew: + name: gitea + state: latest + notify: Restart Gitea + tags: gitea-update diff --git a/roles/git/templates/app.ini.j2 b/roles/git/templates/app.ini.j2 new file mode 100644 index 0000000..86c1d14 --- /dev/null +++ b/roles/git/templates/app.ini.j2 @@ -0,0 +1,73 @@ +; Config last updated based on: +; commit: bcb722daec4bdd45303da97173a2b8a3c4c95dcd +; date: 2020-01-19 + +APP_NAME = codesignd Git +RUN_USER = {{ ansible_user_id }} +RUN_MODE = prod + +; Server settings +; =============== + +[server] +PROTOCOL = http +DOMAIN = git.codesignd.com +HTTP_PORT = 8080 +ROOT_URL = https://%(DOMAIN)s/ +SSH_PORT = 22 +OFFLINE_MODE = true +ENABLE_GZIP = true +LANDING_PAGE = explore + +[security] +INSTALL_LOCK = true +SECRET_KEY = {{ git_secret_key }} +INTERNAL_TOKEN = {{ git_internal_token }} +PASSWORD_COMPLEXITY = off + +[oauth2] +JWT_SECRET = {{ git_jwt_secret }} + +; Paths +; ===== + +[database] +DB_TYPE = sqlite3 +PATH = /home/%(RUN_USER)s/gitea/data/gitea.db + +[repository] +ROOT = /home/%(RUN_USER)s/gitea/repos + +; Functionality settings +; ====================== + +[admin] +DISABLE_REGULAR_ORG_CREATION = true + +[log] +MODE = file +LEVEL = Info + +[mailer] +ENABLED = true +FROM = %(APP_NAME)s +MAILER_TYPE = sendmail + +[service] +DISABLE_REGISTRATION = true +ENABLE_NOTIFY_MAIL = true +NO_REPLY_ADDRESS = noreply.codesignd.com + +[session] +COOKIE_SECURE = true + +; Branding +; ======== + +[ui] +THEME_COLOR_META_TAG = `#eb8e2e` + +[other] +SHOW_FOOTER_BRANDING = false +SHOW_FOOTER_VERSION = false +SHOW_FOOTER_TEMPLATE_LOAD_TIME = false diff --git a/roles/git/templates/service.ini.j2 b/roles/git/templates/service.ini.j2 new file mode 100644 index 0000000..d52bc9c --- /dev/null +++ b/roles/git/templates/service.ini.j2 @@ -0,0 +1,5 @@ +[program:gitea] +command={{ ansible_env.HOME }}/.linuxbrew/bin/gitea web +environment=GITEA_WORK_DIR={{ ansible_env.HOME }}/gitea +autostart=yes +autorestart=yes diff --git a/roles/homebrew/handlers/main.yml b/roles/homebrew/handlers/main.yml new file mode 100644 index 0000000..66e7135 --- /dev/null +++ b/roles/homebrew/handlers/main.yml @@ -0,0 +1,3 @@ +--- +- name: Opt out from Homebrew analytics + command: ~/.linuxbrew/bin/brew analytics off diff --git a/roles/homebrew/tasks/main.yml b/roles/homebrew/tasks/main.yml new file mode 100644 index 0000000..1d17c8a --- /dev/null +++ b/roles/homebrew/tasks/main.yml @@ -0,0 +1,13 @@ +--- +- name: Install Homebrew + git: + repo: "https://github.com/Homebrew/brew.git" + version: master + dest: "{{ ansible_env.HOME }}/.linuxbrew" + notify: Opt out from Homebrew analytics + +- name: Install Homebrew formulae + homebrew: + name: "{{ homebrew_formulae }}" + state: latest + path: "{{ ansible_env.HOME}}/.linuxbrew/bin" diff --git a/roles/host-site/tasks/main.yml b/roles/host-site/tasks/main.yml new file mode 100644 index 0000000..16bec7d --- /dev/null +++ b/roles/host-site/tasks/main.yml @@ -0,0 +1,11 @@ +--- +- name: Add host domain to Uberspace config + import_role: + name: snapstromegon.uberspace_web_domain + vars: + domain: kodos-{{ ansible_user_id }}.codesignd.net + +- name: Set up host site + template: + src: index.html.j2 + dest: /var/www/virtual/{{ ansible_user_id }}/html/index.html diff --git a/roles/host-site/templates/index.html.j2 b/roles/host-site/templates/index.html.j2 new file mode 100644 index 0000000..db899f6 --- /dev/null +++ b/roles/host-site/templates/index.html.j2 @@ -0,0 +1,127 @@ + + + + + + + kodos-{{ ansible_user_id }}.codesignd.net + + + + + +
+ +

kodos-{{ ansible_user_id }}.codesignd.net

+
+ + + + diff --git a/roles/mail-redirect/tasks/main.yml b/roles/mail-redirect/tasks/main.yml new file mode 100644 index 0000000..d94f8df --- /dev/null +++ b/roles/mail-redirect/tasks/main.yml @@ -0,0 +1,15 @@ +--- +- name: Set up email redirect + copy: + content: "{{ mail_redirect_to }}" + dest: "{{ ansible_env.HOME }}/.qmail" + +- name: Delete catch-all address and local Maildirs + file: + path: "{{ item }}" + state: absent + loop: + - "{{ ansible_env.HOME }}/.spamfolder" + - "{{ ansible_env.HOME }}/.qmail-default" + - "{{ ansible_env.HOME }}/Maildir" + - "{{ ansible_env.HOME }}/users" diff --git a/roles/mail/tasks/main.yml b/roles/mail/tasks/main.yml new file mode 100644 index 0000000..e66bda3 --- /dev/null +++ b/roles/mail/tasks/main.yml @@ -0,0 +1,59 @@ +--- +- name: Read existing Uberspace domains + command: uberspace mail domain list + register: uberspace_mail_domain_result + changed_when: false + +- name: Add configured domains to Uberspace config + shell: "uberspace mail domain add $(idn {{ item }})" + when: item not in uberspace_mail_domain_result.stdout_lines + loop: "{{ mail_domains.get(ansible_user_id, []) }}" + +- name: Delete system Maildir + file: + path: "{{ ansible_env.HOME }}/Maildir" + state: absent + +- name: Set up local mail user + import_role: + name: snapstromegon.uberspace_mail_user + vars: + user: lukas + password: "{{ mail_password }}" + +- name: Enable catchall mode + import_role: + name: snapstromegon.uberspace_mail_catchall + vars: + user: lukas + +- name: Create qdated key + command: + cmd: qdated-makekey + creates: "{{ ansible_env.HOME }}/.qdated-key" + +- name: Read qdated key + slurp: + src: "{{ ansible_env.HOME }}/.qdated-key" + register: qdated_key + no_log: yes + +- name: Register qdated key as fact + set_fact: + qdated_key: "{{ qdated_key.content | b64decode }}" + no_log: yes + +- name: Set up .qmail-dated-default file + template: + src: qmail-dated.j2 + dest: "{{ ansible_env.HOME }}/.qmail-dated-default" + +- name: Create mutt config directory + file: + path: "{{ ansible_env.HOME }}/.mutt" + state: directory + +- name: Set up mutt config + template: + src: muttrc.j2 + dest: "{{ ansible_env.HOME }}/.mutt/muttrc" diff --git a/roles/mail/templates/muttrc.j2 b/roles/mail/templates/muttrc.j2 new file mode 100644 index 0000000..d26a978 --- /dev/null +++ b/roles/mail/templates/muttrc.j2 @@ -0,0 +1,32 @@ +# We use Maildirs, not Mailboxes +set mbox_type=Maildir + +# Cache headers +set header_cache=~/.cache/mutt_header_cache + +# Directory setup +set folder="~/users/lukas" +set spoolfile="+/" +set record="+.Sent" +set postponed="+.Drafts" + +# Load IMAP folders as mailboxes +mailboxes `echo -n "+ "; find ${maildir:-~/users/lukas} -maxdepth 1 -type d -name ".*" -printf "+'%f' "` +macro index c "?" "open a different folder" +macro pager c "?" "open a different folder" + +# Sender +set from="Lukas Bestle <{{ mail_default_address[ansible_user_id] }}>" + +# Sorting +set sort=threads +set sort_aux=date-sent + +# Better Umlaut support +unset allow_8bit + +# Display formats +set mask="!^\\.[^.]" +set date_format="%a, %d. %b %H:%M" +set index_format="%3C %Z %D %-25.25F (%?l?%4l&%4c?) %s" +set folder_format="%2C %t %d %t %f" diff --git a/roles/mail/templates/qmail-dated.j2 b/roles/mail/templates/qmail-dated.j2 new file mode 100644 index 0000000..fd9bb92 --- /dev/null +++ b/roles/mail/templates/qmail-dated.j2 @@ -0,0 +1,3 @@ +# Email address is valid for 7 days +|~/.linuxbrew/bin/qdated-check 604800 +{{ ansible_user_id }}-lukas diff --git a/roles/terminal/files/authorized_keys b/roles/terminal/files/authorized_keys new file mode 100644 index 0000000..be391d1 --- /dev/null +++ b/roles/terminal/files/authorized_keys @@ -0,0 +1,4 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFi5drlM5Uli7GbTZFiKPKnG8RmOyS2eu3c6XkwwJiSk luX@smithers.codesignd.net +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOBEksH8LMO5jf1QsMH3yRdxSmY2XPk8CH9HNsWfsXVt luX@bart.codesignd.net +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETsPbHqAta4E6j9pm/9E4lLurlPoh+p5zOWiZ9GS5ml luX@maggie.codesignd.net +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG9J5FMq9ttUcnuqWhYsiKVuols5uD2Ddv7L3Z93nD2W luX@homer.codesignd.net diff --git a/roles/terminal/files/dotfiles/.composer/vendor/bin/.gitkeep b/roles/terminal/files/dotfiles/.composer/vendor/bin/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/roles/terminal/files/dotfiles/.config/fish/conf.d/pkgs.fish b/roles/terminal/files/dotfiles/.config/fish/conf.d/pkgs.fish new file mode 100644 index 0000000..440a335 --- /dev/null +++ b/roles/terminal/files/dotfiles/.config/fish/conf.d/pkgs.fish @@ -0,0 +1,60 @@ +### +# Loader for Oh My Fish! packages +# Loads all packages stored in ~/.config/fish/pkgs/ +# +# Inspired by https://github.com/jorgebucaran/fisher +### + +set -q pkgs_path; or set -g pkgs_path ~/.config/fish/pkgs + +# also search for functions/completions/binaries/manpages within packages +set fish_function_path $fish_function_path[1] $pkgs_path/.functions $pkgs_path/*/functions $fish_function_path[2..-1] +set fish_complete_path $fish_complete_path[1] $pkgs_path/*/completions $fish_complete_path[2..-1] +set -x PATH $pkgs_path/*/bin $PATH +set -x MANPATH $pkgs_path/*/man $MANPATH + +# load package key bindings when needed +if functions -q fish_user_key_bindings + functions -c fish_user_key_bindings fish_user_key_bindings_copy +end +function fish_user_key_bindings + for file in $pkgs_path/*/key_bindings.fish + builtin source $file 2>&1 > /dev/null + end + if functions -q fish_user_key_bindings_copy + fish_user_key_bindings_copy + end +end + +# load custom config from packages +for file in $pkgs_path/*/conf.d/*.fish $pkgs_path/*/init.fish + builtin source $file 2> /dev/null +end + +# ensure that the prompt is loaded (doesn't autoload for some reason) +if test -f $pkgs_path/.functions/fish_prompt.fish + builtin source $pkgs_path/.functions/fish_prompt.fish 2> /dev/null +end + +# collect all functions on the top-level of packages into $pkgs_path/.functions +# (run by Ansible after packages have been updated) +function _pkgs_collect_functions + # clear the functions directory as we rebuild it from scratch + if test -d $pkgs_path/.functions + command rm -rf $pkgs_path/.functions + end + command mkdir -p $pkgs_path/.functions + + # consider all fish files in the top-level of each package + for src in $pkgs_path/*/*.fish + set -l package (command basename (command dirname $src)) + set -l filename (command basename $src) + + # we don't care about uninstall.fish; + # init.fish and key_bindings.fish are dynamically handled on runtime above + if not contains $filename {init,key_bindings,uninstall}.fish + # create a relative symlink + command ln -sf ../$package/$filename $pkgs_path/.functions/$filename + end + end +end diff --git a/roles/terminal/files/dotfiles/.config/fish/config.fish b/roles/terminal/files/dotfiles/.config/fish/config.fish new file mode 100644 index 0000000..2299655 --- /dev/null +++ b/roles/terminal/files/dotfiles/.config/fish/config.fish @@ -0,0 +1,46 @@ +# Environment variables +# --------------------- + +set -x PATH $HOME/bin \ + $HOME/.composer/vendor/bin \ + $HOME/.local/bin \ + $HOME/.linuxbrew/bin \ + $PATH + +set -x MANPATH $HOME/.linuxbrew/share/man \ + $MANPATH + +# language +set -x LANG de_DE.UTF-8 + +# editor +set -x VISUAL "$HOME/.config/fish/pkgs/rmate/rmate -p 64364 -w" # Visual Studio Code (wait mode) +set -x EDITOR 'nano -w' # nano as fallback editor + +set -x XDG_CONFIG_HOME $HOME/.config + +# Aliases +# ------- + +# Edit file remotely with VS Code +alias code="$HOME/.config/fish/pkgs/rmate/rmate -p 64364" + +# Short ls command +alias l='ls -la' + +# General Settings +# ---------------- + +# enforce 24-bit color mode +# (the requried env var is not transmitted via SSH) +set -x COLORTERM truecolor +set -g fish_term24bit 1 + +# settings for the bobthefish theme +set -g fish_prompt_pwd_dir_length 0 +set -g theme_color_scheme solarized-dark +set -g theme_date_format '+%a %d.%m.%y %H:%M:%S' +set -g theme_display_git_master_branch yes +set -g theme_show_exit_status yes +set -g theme_title_use_abbreviated_path no +set -x VIRTUAL_ENV_DISABLE_PROMPT 1 diff --git a/roles/terminal/files/dotfiles/.config/fish/functions/fish_greeting.fish b/roles/terminal/files/dotfiles/.config/fish/functions/fish_greeting.fish new file mode 100644 index 0000000..a4f295a --- /dev/null +++ b/roles/terminal/files/dotfiles/.config/fish/functions/fish_greeting.fish @@ -0,0 +1,3 @@ +function fish_greeting + welcome +end diff --git a/roles/terminal/files/dotfiles/.config/fish/functions/nsreverse.fish b/roles/terminal/files/dotfiles/.config/fish/functions/nsreverse.fish new file mode 100644 index 0000000..e4dda59 --- /dev/null +++ b/roles/terminal/files/dotfiles/.config/fish/functions/nsreverse.fish @@ -0,0 +1,21 @@ +### +# Looks up the IPv4 address of the given hostname and tries to find the reverse record of that IP +# +# Globals: +# None +# Arguments: +# $domain string Hostname to look up +# Returns: +# None +### +function nsreverse + # lookup IP + set ip (dig "$argv[1]" A +short); or return 1 + + # lookup reverse record + set reverse (dig -x "$ip" +short); or return 1 + + # output + echo -e "\033[1mIPv4: \033[0;34m $ip\033[0m" + echo -e "\033[1mReverse lookup:\033[0;34m $reverse\033[0m" +end diff --git a/roles/terminal/files/dotfiles/.config/fish/functions/nsreverse6.fish b/roles/terminal/files/dotfiles/.config/fish/functions/nsreverse6.fish new file mode 100644 index 0000000..d84c084 --- /dev/null +++ b/roles/terminal/files/dotfiles/.config/fish/functions/nsreverse6.fish @@ -0,0 +1,21 @@ +### +# Looks up the IPv6 address of the given hostname and tries to find the reverse record of that IP +# +# Globals: +# None +# Arguments: +# $domain string Hostname to look up +# Returns: +# None +### +function nsreverse6 + # lookup IP + set ip (dig "$argv[1]" AAAA +short); or return 1 + + # lookup reverse record + set reverse (dig -x "$ip" +short); or return 1 + + # output + echo -e "\033[1mIPv6: \033[0;34m $ip\033[0m" + echo -e "\033[1mReverse lookup:\033[0;34m $reverse\033[0m" +end diff --git a/roles/terminal/files/dotfiles/.config/fish/pkgs/.gitkeep b/roles/terminal/files/dotfiles/.config/fish/pkgs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/roles/terminal/files/dotfiles/.config/git/.gitkeep b/roles/terminal/files/dotfiles/.config/git/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/roles/terminal/files/dotfiles/.config/welcome/colossal.flf b/roles/terminal/files/dotfiles/.config/welcome/colossal.flf new file mode 100644 index 0000000..fa13ba6 --- /dev/null +++ b/roles/terminal/files/dotfiles/.config/welcome/colossal.flf @@ -0,0 +1,1136 @@ +flf2a$ 11 8 20 32 13 +Colossal.flf (Jonathon - jon@mq.edu.au) +8 June 1994 + +Explanation of first line: +flf2 - "magic number" for file identification +a - should always be `a', for now +$ - the "hardblank" -- prints as a blank, but can't be smushed +11 - height of a character +8 - height of a character, not including descenders +20 - max line length (excluding comment lines) + a fudge factor +32 - default smushmode for this font +13 - number of comment lines + +$ $@ +$ $@ +$ $@ +$ $@ +$ $@ +$ $@ +$ $@ +$ $@ +$ $@ +$ $@ +$ $@@ +888$@ +888$@ +888$@ +888$@ +888$@ +Y8P$@ + " $@ +888$@ + @ + @ + @@ +88 88$@ +8P 8P$@ +" " $@ + $ $ @ + $ $ @ + $ $ @ + $ $ @ + $ $ @ + $ $ @ + $ $ @ + $ $ @@ + 888 888 $@ + 888 888 $@ +888888888888$@ + 888 888 $@ + 888 888 $@ +888888888888$@ + 888 888 $@ + 888 888 $@ + $@ + $@ + $@@ + 88 $@ + .d88888b. $@ +d88P 88"88b$@ +Y88b.88 $ @ + "Y88888b.$ @ + 88"88b$@ +Y88b 88.88P$@ + "Y88888P"$ @ + 88 @ + @ + @@ +d88b d88P$@ +Y88P d88P $@ + d88P $ @ + d88P $ @ + $ d88P $ @ +$ d88P $ @ +$d88P d88b$@ +d88P Y88P$@ + @ + @ + @@ + .d8888b. $ @ +d88P "88b $ @ +Y88b. d88P $ @ + "Y8888P" $ @ +.d88P88K.d88P$@ +888" Y888P" $@ +Y88b .d8888b $@ + "Y8888P" Y88b@ + @ + @ + @@ +d8b$@ +88P$@ +8P $@ +" $ @ + $ @ + $ @ + $ @ + $ @ + $ @ + $ @ + $ @@ + $.d88$@ +$d88P"$@ +d88P $ @ +888 $ @ +888 $ @ +Y88b $ @ +$Y88b.$@ + $"Y88$@ + @ + @ + @@ +88b.$ @ +"Y88b$ @ + Y88b$@ + 888$@ + 888$@ + d88P$@ +.d88P$ @ +88P"$ @ + @ + @ + @@ + @ + o $ @ + d8b $ @ + d888b $ @ +"Y888888888P"@ + "Y88888P"$ @ + d88P"Y88b $@ + dP" "Yb$@ + @ + @ + @@ + $ @ + $ @ + $ @ + 888 $@ +8888888$@ + 888 $@ + $ @ + $ @ + $ @ + $ @ + $ @@ + $ @ + $ @ + $ @ + $ @ + $ @ + $ @ +d8b$@ +88P$@ +8P @ +" @ + @@ + $ $ @ + $ $ @ + $ $ @ + $ $ @ + $ $ @ +888888$@ + $ $ @ + $ $ @ + $ $ @ + $ $ @ + $ $ @@ + $ @ + $ @ + $ @ + $ @ + $ @ + $ @ +d8b$@ +Y8P$@ + @ + @ + @@ + $ d88P$@ + $ d88P $@ + $ d88P $ @ + $ d88P $ @ + $ d88P $ @ +$ d88P $ @ +$d88P $ @ +d88P $ @ + @ + @ + @@ +$.d8888b.$ @ +d88P Y88b$@ +888 888$@ +888 888$@ +888 888$@ +888 888$@ +Y88b d88P$@ +$"Y8888P"$ @ + @ + @ + @@ + d888 $ @ +d8888 $ @ + 888 $ @ + 888 $ @ + 888 $ @ + 888 $ @ + 888 $ @ +8888888$@ + @ + @ + @@ + .d8888b.$ @ +d88P Y88b$@ + $ 888$@ + $ .d88P$@ + .od888P" $@ +d88P" $@ +888" $@ +888888888 $@ + @ + @ + @@ + .d8888b.$ @ +d88P Y88b$@ + $ .d88P$@ + $ 8888" $@ + $ "Y8b.$@ +888 888$@ +Y88b d88P$@ + "Y8888P" $@ + @ + @ + @@ + d8888 $@ + d8P888 $@ + d8P 888 $@ + d8P 888 $@ +d88 888 $@ +8888888888$@ + 888 $@ + 888 $@ + @ + @ + @@ +888888888$ @ +888 $ @ +888 $ @ +8888888b.$ @ +$ "Y88b$@ +$ 888$@ +Y88b d88P$@ + "Y8888P"$ @ + @ + @ + @@ +$.d8888b.$ @ +d88P Y88b$@ +888 $ @ +888d888b.$ @ +888P "Y88b$@ +888 888$@ +Y88b d88P$@ +$"Y8888P"$ @ + @ + @ + @@ +8888888888$@ + $ d88P$@ + $ d88P $@ + $ d88P $ @ +$88888888$ @ +$ d88P $ @ +$d88P $ @ +d88P $ @ + @ + @ + @@ + .d8888b.$ @ +d88P Y88b$@ +Y88b. d88P$@ + "Y88888" $@ +.d8P""Y8b.$@ +888 888$@ +Y88b d88P$@ + "Y8888P" $@ + @ + @ + @@ +$.d8888b.$ @ +d88P Y88b$@ +888 888$@ +Y88b. d888$@ +$"Y888P888$@ +$ 888$@ +Y88b d88P$@ + "Y8888P"$ @ + @ + @ + @@ + $ @ + $ @ + $ @ +d8b$@ +Y8P$@ + $ @ +d8b$@ +Y8P$@ + @ + @ + @@ + $ @ + $ @ + $ @ +d8b @ +Y8P @ + $ @ +d8b$@ +88P$@ +8P @ +" @ + @@ + $ d88P$@ +$ d88P $@ + d88P $ @ +d88P $ @ +Y88b $ @ + Y88b $ @ +$ Y88b $@ + $ Y88b$@ + @ + @ + @@ + $ $ @ + $ $ @ + $ $ @ +888888$@ + $ $ @ +888888$@ + $ $ @ + $ $ @ + $ $ @ + $ $ @ + $ $ @@ +Y88b $ @ + Y88b $ @ + Y88b $@ + Y88b$@ + d88P$@ + d88P $@ + d88P $ @ +d88P $ @ + @ + @ + @@ +$.d8888b.$ @ +d88P Y88b$@ + $ .d88P$@ + $ .d88P"$ @ + $ 888" $ @ + $ 888 $ @ + $ $ @ + $ 888 $ @ + @ + @ + @@ +$.d8888888b.$ @ +d88P" "Y88b$@ +888 d8b 888$@ +888 888 888$@ +888 888bd88P$@ +888 Y8888P" $@ +Y88b. .d8$@ +$"Y88888888P"$@ + @ + @ + @@ + $d8888$@ + $d88888$@ + $d88P888$@ + $d88P 888$@ + $d88P 888$@ + $d88P 888$@ +$d8888888888$@ +d88P 888$@ + @ + @ + @@ +888888b.$ @ +888 "88b$ @ +888 .88P$ @ +8888888K.$ @ +888 "Y88b$@ +888 888$@ +888 d88P$@ +8888888P"$ @ + @ + @ + @@ +$.d8888b.$ @ +d88P Y88b$@ +888 888$@ +888 $ @ +888 $ @ +888 888$@ +Y88b d88P$@ +$"Y8888P"$ @ + @ + @ + @@ +8888888b.$ @ +888 "Y88b$@ +888 888$@ +888 888$@ +888 888$@ +888 888$@ +888 .d88P$@ +8888888P"$ @ + @ + @ + @@ +8888888888$@ +888 $ @ +888 $ @ +8888888$ @ +888 $ @ +888 $ @ +888 $ @ +8888888888$@ + @ + @ + @@ +8888888888$@ +888 $ @ +888 $ @ +8888888$ @ +888 $ @ +888 $ @ +888 $ @ +888 $ @ + @ + @ + @@ +$.d8888b.$ @ +d88P Y88b$@ +888 888$@ +888 $@ +888 88888$@ +888 888$@ +Y88b d88P$@ +$"Y8888P88$@ + @ + @ + @@ +888 888$@ +888 888$@ +888 888$@ +8888888888$@ +888 888$@ +888 888$@ +888 888$@ +888 888$@ + @ + @ + @@ +8888888$@ + 888 $ @ + 888 $ @ + 888 $ @ + 888 $ @ + 888 $ @ + 888 $ @ +8888888$@ + @ + @ + @@ + 888888$@ + "88b$@ + 888$@ + 888$@ + 888$@ + 888$@ + 88P$@ + 888$@ + .d88P$@ + .d88P"$ @ +888P" $ @@ +888 d8P$ @ +888 d8P $ @ +888 d8P $ @ +888d88K $ @ +8888888b $ @ +888 Y88b $ @ +888 Y88b $@ +888 Y88b$@ + @ + @ + @@ +888 $ @ +888 $ @ +888 $ @ +888 $ @ +888 $ @ +888 $ @ +888 $ @ +88888888$@ + @ + @ + @@ +888b d888$@ +8888b d8888$@ +88888b.d88888$@ +888Y88888P888$@ +888 Y888P 888$@ +888 Y8P 888$@ +888 " 888$@ +888 888$@ + @ + @ + @@ +888b 888$@ +8888b 888$@ +88888b 888$@ +888Y88b 888$@ +888 Y88b888$@ +888 Y88888$@ +888 Y8888$@ +888 Y888$@ + @ + @ + @@ +$.d88888b.$ @ +d88P" "Y88b$@ +888 888$@ +888 888$@ +888 888$@ +888 888$@ +Y88b. .d88P$@ +$"Y88888P"$ @ + @ + @ + @@ +8888888b.$ @ +888 Y88b$@ +888 888$@ +888 d88P$@ +8888888P"$ @ +888 $ @ +888 $ @ +888 $ @ + @ + @ + @@ +$.d88888b.$ @ +d88P" "Y88b$@ +888 888$@ +888 888$@ +888 888$@ +888 Y8b 888$@ +Y88b.Y8b88P$@ +$"Y888888" $@ + Y8b $@ + @ + @@ +8888888b.$ @ +888 Y88b$@ +888 888$@ +888 d88P$@ +8888888P"$ @ +888 T88b $ @ +888 T88b$ @ +888 T88b$@ + @ + @ + @@ +$.d8888b.$ @ +d88P Y88b$@ +Y88b. $ @ +$"Y888b. $ @ +$ "Y88b.$@ +$ "888$@ +Y88b d88P$@ + "Y8888P"$ @ + @ + @ + @@ +88888888888$@ + 888 $ @ + 888 $ @ + 888 $ @ + 888 $ @ + 888 $ @ + 888 $ @ + 888 $ @ + @ + @ + @@ +888 888$@ +888 888$@ +888 888$@ +888 888$@ +888 888$@ +888 888$@ +Y88b. .d88P$@ +$"Y88888P"$ @ + @ + @ + @@ +888 888$@ +888 888$@ +888 888$@ +Y88b d88P$@ + Y88b d88P $@ + Y88o88P $ @ + Y888P $ @ + Y8P $ @ + @ + @ + @@ +888 888$@ +888 o 888$@ +888 d8b 888$@ +888 d888b 888$@ +888d88888b888$@ +88888P Y88888$@ +8888P Y8888$@ +888P Y888$@ + @ + @ + @@ +Y88b d88P$@ + Y88b d88P $@ + Y88o88P $ @ + Y888P $ @ + d888b $ @ + d88888b $ @ + d88P Y88b $@ +d88P Y88b$@ + @ + @ + @@ +Y88b d88P$@ + Y88b d88P $@ + Y88o88P $ @ + Y888P $ @ + 888 $ @ + 888 $ @ + 888 $ @ + 888 $ @ + @ + @ + @@ +8888888888P$@ + $ d88P $@ + $ d88P $ @ + $ d88P $ @ + $ d88P $ @ +$ d88P $ @ +$d88P $ @ +d8888888888$@ + @ + @ + @@ +8888888$@ +888 $ @ +888 $ @ +888 $ @ +888 $ @ +888 $ @ +888 $ @ +8888888$@ + @ + @ + @@ +Y88b $ @ +$Y88b $ @ +$ Y88b $ @ + $ Y88b $ @ + $ Y88b $ @ + $ Y88b $ @ + $ Y88b $@ + $ Y88b$@ + @ + @ + @@ +8888888$@ + $ 888$@ + $ 888$@ + $ 888$@ + $ 888$@ + $ 888$@ + $ 888$@ +8888888$@ + @ + @ + @@ + o$ @ + d8b$ @ + d888b$ @ + d8P"Y8b$@ + $ $ @ + $ $ @ + $ $ @ + $ $ @ + $ $ @ + $ $ @ + $ $ @@ + $ $ @ + $ $ @ + $ $ @ + $ $ @ + $ $ @ + $ $ @ + $ $ @ +88888888$@ + @ + @ + @@ + d8b$@ + Y88$@ + Y8$@ + Y$@ + $ @ + $ @ + $ @ + $ @ + $ @ + $ @ + $ @@ + @ + @ + @ +$8888b. $@ +$ "88b$@ +.d888888$@ +888 888$@ +"Y888888$@ + @ + @ + @@ +888 $ @ +888 $ @ +888 $ @ +88888b.$ @ +888 "88b$@ +888 888$@ +888 d88P$@ +88888P"$ @ + @ + @ + @@ + @ + @ + @ +$.d8888b$@ +d88P" $ @ +888 $ @ +Y88b. $ @ +$"Y8888P$@ + @ + @ + @@ + 888$@ + 888$@ + 888$@ +$.d88888$@ +d88" 888$@ +888 888$@ +Y88b 888$@ +$"Y88888$@ + @ + @ + @@ + @ + @ + @ +$.d88b.$ @ +d8P Y8b$@ +88888888$@ +Y8b.$ @ +$"Y8888$ @ + @ + @ + @@ +$.d888$@ +d88P"$ @ +888 $ @ +888888$@ +888 $ @ +888 $ @ +888 $ @ +888 $ @ + @ + @ + @@ + @ + @ + @ +$.d88b.$ @ +d88P"88b$@ +888 888$@ +Y88b 888$@ +$"Y88888$@ +$ 888$@ +Y8b d88P$@ + "Y88P"$ @@ +888 $ @ +888 $ @ +888 $ @ +88888b.$ @ +888 "88b$@ +888 888$@ +888 888$@ +888 888$@ + @ + @ + @@ +d8b$@ +Y8P$@ +$ $@ +888$@ +888$@ +888$@ +888$@ +888$@ + @ + @ + @@ + $d8b$@ + $Y8P$@ + $ $@ + $8888$@ + $"888$@ + $ 888$@ + $ 888$@ + $ 888$@ + $ 888$@ +$ d88P$@ +888P"$ @@ +888 $ @ +888 $ @ +888 $ @ +888 888$@ +888 .88P$@ +888888K$ @ +888 "88b$@ +888 888$@ + @ + @ + @@ +888$@ +888$@ +888$@ +888$@ +888$@ +888$@ +888$@ +888$@ + @ + @ + @@ + @ + @ + @ +88888b.d88b.$ @ +888 "888 "88b$@ +888 888 888$@ +888 888 888$@ +888 888 888$@ + @ + @ + @@ + @ + @ + @ +88888b.$ @ +888 "88b$@ +888 888$@ +888 888$@ +888 888$@ + @ + @ + @@ + @ + @ + @ +$.d88b.$ @ +d88""88b$@ +888 888$@ +Y88..88P$@ +$"Y88P"$ @ + @ + @ + @@ + @ + @ + @ +88888b.$ @ +888 "88b$@ +888 888$@ +888 d88P$@ +88888P"$ @ +888 $ @ +888 $ @ +888 $ @@ + @ + @ + @ +$.d88888$@ +d88" 888$@ +888 888$@ +Y88b 888$@ +$"Y88888$@ + $ 888$@ + $ 888$@ + $ 888$@@ + @ + @ + @ +888d888$@ +888P"$ @ +888 $ @ +888 $ @ +888 $ @ + @ + @ + @@ + @ + @ + @ +.d8888b$ @ +88K $ @ +"Y8888b.$@ +$ X88$@ +$88888P'$@ + @ + @ + @@ +888 $ @ +888 $ @ +888 $ @ +888888$@ +888 $ @ +888 $ @ +Y88b.$ @ + "Y888$@ + @ + @ + @@ + @ + @ + @ +888 888$@ +888 888$@ +888 888$@ +Y88b 888$@ +$"Y88888$@ + @ + @ + @@ + @ + @ + @ +888 888$@ +888 888$@ +Y88 88P$@ +$Y8bd8P$ @ +$ Y88P $ @ + @ + @ + @@ + @ + @ + @ +888 888 888$@ +888 888 888$@ +888 888 888$@ +Y88b 888 d88P$@ +$"Y8888888P"$ @ + @ + @ + @@ + @ + @ + @ +888 888$@ +`Y8bd8P'$@ +$ X88K $ @ +.d8""8b.$@ +888 888$@ + @ + @ + @@ + @ + @ + @ +888 888$@ +888 888$@ +888 888$@ +Y88b 888$@ +$"Y88888$@ +$ 888$@ +Y8b d88P$@ +$"Y88P"$ @@ + @ + @ + @ +88888888$@ + $ d88P $@ +$ d88P $ @ +$d88P $ @ +88888888$@ + @ + @ + @@ + $.d888$@ +$d88P"$ @ +$888 $ @ +.888 $ @ +888( $ @ +"888 $ @ +$888 $ @ +$Y88b.$ @ + $"Y888$@ + @ + @@ +$ 888 $@ +$ 888 $@ +$ 888 $@ +$ 888 $@ +$ $@ +$ 888 $@ +$ 888 $@ +$ 888 $@ +$ 888 $@ + @ + @@ +888b. $ @ +$"Y88b $@ + $ 888 $@ + $ 888.$@ + $ )888$@ + $ 888"$@ + $ 888 $@ +$.d88P $@ +888P" $ @ + @ + @@ + @ + @ +$d888b d88$@ +d888888888P$@ +88P Y888P$ @ + @ + @ + @ + @ + @ + @@ + d8b d8b@ + Y8P Y8P@ + $d88888$@ + $d88P888$@ + $d88P 888$@ + $d88P 888$@ + $d888888888$@ +$d88P 888$@ + @ + @ + @@ + d8b d8b @ + Y8P Y8P @ +$.d88888b.$ @ +d88P" "Y88b$@ +888 888$@ +888 888$@ +Y88b. .d88P$@ +$"Y88888P"$ @ + @ + @ + @@ + d8b d8b @ + Y8P Y8P @ +888 888$@ +888 888$@ +888 888$@ +888 888$@ +Y88b. .d88P$@ +$"Y88888P"$ @ + @ + @ + @@ +d8b d8b @ +Y8P Y8P @ + @ +$8888b. $@ +$ "88b$@ +.d888888$@ +888 888$@ +"Y888888$@ + @ + @ + @@ +d8b d8b @ +Y8P Y8P @ + @ +$.d88b.$ @ +d88""88b$@ +888 888$@ +Y88..88P$@ +$"Y88P"$ @ + @ + @ + @@ +d8b d8b @ +Y8P Y8P @ + @ +888 888$@ +888 888$@ +888 888$@ +Y88b 888$@ +$"Y88888$@ + @ + @ + @@ + .d888b.$ @ +d88" "88b$ @ +888 .88P$ @ +888 888K.$ @ +888 "Y88b$@ +888 888$@ +888 d88P$@ +888 888P"$ @ +888 @ +888 @ + @@ diff --git a/roles/terminal/files/dotfiles/.config/welcome/summarizers/01-date.sh b/roles/terminal/files/dotfiles/.config/welcome/summarizers/01-date.sh new file mode 100755 index 0000000..2cb1608 --- /dev/null +++ b/roles/terminal/files/dotfiles/.config/welcome/summarizers/01-date.sh @@ -0,0 +1,6 @@ +function dateSummarizer() { + echo -n "\033[34m" + date +} + +registerSummarizer "Date" dateSummarizer diff --git a/roles/terminal/files/dotfiles/.config/welcome/summarizers/02-uptime.sh b/roles/terminal/files/dotfiles/.config/welcome/summarizers/02-uptime.sh new file mode 100755 index 0000000..7b7afb9 --- /dev/null +++ b/roles/terminal/files/dotfiles/.config/welcome/summarizers/02-uptime.sh @@ -0,0 +1,111 @@ +function uptimeSummarizer() { + # Get the uptime in seconds depending on the kernel + if [[ "$(uname -s)" == 'Darwin' ]]; then + # OS X + # kern.boottime is an absolute timestamp + bootTime="$(sysctl -n kern.boottime | awk '{print $4}' | sed 's/,//g')" + currentTime="$(date +%s)" + uptime="$(($currentTime - $bootTime))" + elif [[ -f /proc/uptime ]]; then + # Linux + uptime="$(cat /proc/uptime)" + uptime="${uptime%%.*}" + else + # We don't have a handler for that + # If you need one, please create an issue at + # https://github.com/lukasbestle/my-welcome/issues + echo "\033[31mUnsupported environment, can't get uptime. :(\033[0m" + exit 1 + fi + + # Parse into units + seconds="$((uptime % 60))" + minutes="$((uptime / 60 % 60))" + hours="$(( uptime / 60 / 60 % 24))" + days="$(( uptime / 60 / 60 / 24 % 30))" + months="$(( uptime / 60 / 60 / 24 / 30 % 12))" + years="$(( uptime / 60 / 60 / 24 / 30 / 12))" + + # Output depending on the scale of the uptime + if [[ years -ge 1 ]]; then + outputYears=true + outputMonths=true + outputDays=true + elif [[ months -ge 1 ]]; then + outputMonths=true + outputDays=true + elif [[ days -ge 1 ]]; then + outputDays=true + outputHours=true + outputMinutes=true + elif [[ hours -ge 1 ]]; then + outputHours=true + outputMinutes=true + elif [[ minutes -ge 1 ]]; then + outputMinutes=true + outputSeconds=true + else + outputSeconds=true + fi + + # Output each element + output='' + if [[ "$outputYears" == true ]]; then + output+="\033[34m$years" + if [[ "$years" == 1 ]]; then + output+=' year\033[0m\n' + else + output+=' years\033[0m\n' + fi + fi + + if [[ "$outputMonths" == true ]]; then + output+="\033[34m$months" + if [[ "$months" == 1 ]]; then + output+=' month\033[0m\n' + else + output+=' months\033[0m\n' + fi + fi + + if [[ "$outputDays" == true ]]; then + output+="\033[34m$days" + if [[ "$days" == 1 ]]; then + output+=' day\033[0m\n' + else + output+=' days\033[0m\n' + fi + fi + + if [[ "$outputHours" == true ]]; then + output+="\033[34m$hours" + if [[ "$hours" == 1 ]]; then + output+=' hour\033[0m\n' + else + output+=' hours\033[0m\n' + fi + fi + + if [[ "$outputMinutes" == true ]]; then + output+="\033[34m$minutes" + if [[ "$minutes" == 1 ]]; then + output+=' minute\033[0m\n' + else + output+=' minutes\033[0m\n' + fi + fi + + if [[ "$outputSeconds" == true ]]; then + output+="\033[34m$seconds" + if [[ "$seconds" == 1 ]]; then + output+=' second\033[0m\n' + else + output+=' seconds\033[0m\n' + fi + fi + + # Replace newlines with a comma + echo "Host up for $(echo -ne "$output" | paste -s -d ';' - | sed "s/\;/\, /g")" +} + +registerSummarizer "Uptime" uptimeSummarizer diff --git a/roles/terminal/files/dotfiles/.config/welcome/summarizers/03-quota.sh b/roles/terminal/files/dotfiles/.config/welcome/summarizers/03-quota.sh new file mode 100755 index 0000000..69c79bf --- /dev/null +++ b/roles/terminal/files/dotfiles/.config/welcome/summarizers/03-quota.sh @@ -0,0 +1,12 @@ +function quotaSummarizer() { + # Get current quota information in MB (integers) and % of total + quota_used=$(quota -lgs | grep '/dev/' | sed 's/ \/dev\/[a-z]\+[0-9] \+\([0-9]\+\?\)M.*/\1/') + quota_total=$(quota -lgs | grep '/dev/' | sed 's/ \/dev\/[a-z]\+[0-9] \+[0-9]\+\?M \+\([0-9]\+\?\)M.*/\1/') + quota_free=$(( $quota_total - $quota_used )) + quota_percentage=$(( $quota_used * 100 / $quota_total )) + + echo "Used \033[34m$quota_used MB\033[0m of \033[34m$quota_total MB\033[0m (\033[34m$quota_percentage %\033[0m)" + echo -n "\033[34m$quota_free MB\033[0m available" +} + +registerSummarizer "Quota" quotaSummarizer diff --git a/roles/terminal/files/dotfiles/.config/welcome/summarizers/04-ram.sh b/roles/terminal/files/dotfiles/.config/welcome/summarizers/04-ram.sh new file mode 100755 index 0000000..ede3bd7 --- /dev/null +++ b/roles/terminal/files/dotfiles/.config/welcome/summarizers/04-ram.sh @@ -0,0 +1,8 @@ +function ramSummarizer() { + # Get current RAM usage in MB (integer) + ram_used=$(ps -u $USER -o rss= | awk '{rss += $1} END {printf("%0.f", rss / 1000)}') + + echo -n "Used \033[34m$ram_used MB\033[0m" +} + +registerSummarizer "RAM" ramSummarizer diff --git a/roles/terminal/files/dotfiles/.config/welcome/warners/.gitkeep b/roles/terminal/files/dotfiles/.config/welcome/warners/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/roles/terminal/files/dotfiles/.local/bin/.gitkeep b/roles/terminal/files/dotfiles/.local/bin/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/roles/terminal/handlers/main.yml b/roles/terminal/handlers/main.yml new file mode 100644 index 0000000..d931365 --- /dev/null +++ b/roles/terminal/handlers/main.yml @@ -0,0 +1,3 @@ +--- +- name: Collect fish functions from packages + command: fish -c _pkgs_collect_functions diff --git a/roles/terminal/tasks/main.yml b/roles/terminal/tasks/main.yml new file mode 100644 index 0000000..ac65675 --- /dev/null +++ b/roles/terminal/tasks/main.yml @@ -0,0 +1,58 @@ +--- +- name: Set authorized SSH keys + authorized_key: + user: "{{ ansible_user_id }}" + state: present + key: "{{ item }}" + with_file: authorized_keys + +- name: Get current shell + shell: "getent passwd {{ ansible_user_id }} | cut -d: -f7" + register: current_shell + changed_when: no + +- name: Change shell to fish + command: chsh --shell /usr/bin/fish + when: "current_shell.stdout != '/usr/bin/fish'" + +- name: Copy dotfiles to home directory + copy: + src: dotfiles/ + dest: "{{ ansible_env.HOME }}/" + mode: preserve + +- name: Copy Git config to home directory + template: + src: gitconfig.j2 + dest: "{{ ansible_env.HOME }}/.config/git/config" + +- name: Remove unneeded files + file: + path: "{{ item }}" + state: absent + loop: + - "{{ ansible_env.HOME }}/.bash_history" + - "{{ ansible_env.HOME }}/.bash_logout" + - "{{ ansible_env.HOME }}/.bash_profile" + - "{{ ansible_env.HOME }}/.bashrc" + - "{{ ansible_env.HOME }}/.emacs" + - "{{ ansible_env.HOME }}/.zcompdump" + - "{{ ansible_env.HOME }}/.zshrc" + +- name: Download and update iTerm2 shell integration + get_url: + url: https://iterm2.com/shell_integration/fish + dest: "{{ ansible_env.HOME }}/.config/fish/conf.d/iterm2_integration.fish" + force: yes + +- name: Download and update Oh My Fish! packages + git: + repo: "{{ item.value }}" + dest: "{{ ansible_env.HOME }}/.config/fish/pkgs/{{ item.key }}" + loop: "{{ omf_pkgs | dict2items }}" + notify: Collect fish functions from packages + +- name: Disable Cron MAILTO + cronvar: + name: MAILTO + value: "" diff --git a/roles/terminal/templates/gitconfig.j2 b/roles/terminal/templates/gitconfig.j2 new file mode 100644 index 0000000..9c98764 --- /dev/null +++ b/roles/terminal/templates/gitconfig.j2 @@ -0,0 +1,17 @@ +; user settings +[user] + name = kodos-{{ ansible_user_id }}.codesignd.net + email = hostmaster@codesignd.net + +; core settings +[advice] + detachedHead = false +[branch] + autoSetupRebase = always +[core] + editor = code -w + quotepath = false +[help] + autocorrect = 10 +[push] + default = upstream diff --git a/roles/web/files/home/.config/projectr/.gitkeep b/roles/web/files/home/.config/projectr/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/roles/web/files/home/bin/qdated-generate b/roles/web/files/home/bin/qdated-generate new file mode 100755 index 0000000..323334c --- /dev/null +++ b/roles/web/files/home/bin/qdated-generate @@ -0,0 +1,5 @@ +#!/bin/bash + +for host in ~/.config/qdated/*; do + HOME="$host" qdated-now > "$host/current" +done diff --git a/roles/web/tasks/main.yml b/roles/web/tasks/main.yml new file mode 100644 index 0000000..85f0b97 --- /dev/null +++ b/roles/web/tasks/main.yml @@ -0,0 +1,102 @@ +--- +- name: Copy config to home directory + copy: + src: home/ + dest: "{{ ansible_env.HOME }}/" + mode: preserve + +- name: Create link at ~/web + file: + src: /var/www/virtual/{{ ansible_user_id }} + dest: "{{ ansible_env.HOME }}/web" + state: link + +- name: Initialize ~/web/sites + file: + path: /var/www/virtual/{{ ansible_user_id }}/sites + state: directory + +- name: Download and update projectr + git: + repo: https://github.com/lukasbestle/projectr.git + dest: "{{ ansible_env.HOME }}/.config/fish/pkgs/projectr" + +- name: Set up deploy script + template: + src: deploy.php.j2 + dest: /var/www/virtual/{{ ansible_user_id }}/html/deploy.php + +- name: Read existing Uberspace domains + command: uberspace web domain list + register: uberspace_web_domain_result + changed_when: no + +- name: Add configured domains to Uberspace config + shell: "uberspace web domain add $(idn {{ item.key }})" + when: item.key not in uberspace_web_domain_result.stdout_lines + loop: "{{ web_links.get(ansible_user_id, {}) | dict2items }}" + +- name: Set up sites using projectr + command: + argv: + - site_add + - "{{ item.key }}" + creates: /var/www/virtual/{{ ansible_user_id }}/sites/{{ item.key }} + loop: "{{ web_sites.get(ansible_user_id, {}) | dict2items }}" + +- name: Set up site origins using projectr (sites with origin) + command: + argv: + - site_origin + - "{{ item.key }}" + - "{{ item.value }}" + creates: /var/www/virtual/{{ ansible_user_id }}/sites/{{ item.key }}/.origin + loop: "{{ web_sites.get(ansible_user_id, {}) | dict2items }}" + when: item.value != None + +- name: Set up site origins using projectr (sites without origin) + command: + argv: + - site_origin + - "{{ item.key }}" + removes: /var/www/virtual/{{ ansible_user_id }}/sites/{{ item.key }}/.origin + loop: "{{ web_sites.get(ansible_user_id, {}) | dict2items }}" + when: item.value == None + +- name: Deploy sites using projectr (sites with origin) + command: + argv: + - site_deploy + - "{{ item.key }}" + creates: /var/www/virtual/{{ ansible_user_id }}/sites/{{ item.key }}/current + loop: "{{ web_sites.get(ansible_user_id, {}) | dict2items }}" + when: item.value != None + +- name: Link sites to domains + command: + argv: + - site_link + - "{{ item.value.site }}" + - "{{ item.key }}" + - "{{ item.value.get('path', '') }}" + creates: /var/www/virtual/{{ ansible_user_id }}/{{ item.key }} + loop: "{{ web_links.get(ansible_user_id, {}) | dict2items }}" + +- name: Ensure that qdated directories for all Mail Uberspaces exist + file: + path: "{{ ansible_env.HOME }}/.config/qdated/{{ hostvars[item].ansible_user_id }}" + state: directory + loop: "{{ groups.mail }}" + +- name: Copy qdated keys from all Mail Uberspaces + copy: + content: "{{ hostvars[item].qdated_key }}" + dest: "{{ ansible_env.HOME }}/.config/qdated/{{ hostvars[item].ansible_user_id }}/.qdated-key" + loop: "{{ groups.mail }}" + +- name: Set up qdated address generation cronjob + cron: + name: "qdated generation" + hour: "2" + minute: "3" + job: "{{ ansible_env.HOME }}/bin/qdated-generate" diff --git a/roles/web/templates/deploy.php.j2 b/roles/web/templates/deploy.php.j2 new file mode 100644 index 0000000..21c602b --- /dev/null +++ b/roles/web/templates/deploy.php.j2 @@ -0,0 +1,172 @@ + + * @copyright Copyright 2014 Lukas Bestle + * @license MIT + * @file webhook.gitea.php + */ + +// 1. Configuration +// ==================== + +// Secret used as authentication (you have to set this in the Gitea project settings as well) +// ATTENTION: Make sure this is long and secure, as anyone could checkout any commit of your projects otherwise +define('SECRET', '{{ projectr_deploy_secret }}'); + +// Path where you installed the scripts from the "bin" directory of this repository +// This is required, as PHP doesn't automatically use the environment and therefore your $PATH from your shell +define('TOOLKIT_PATH', '/home/{{ ansible_user_id }}/.config/fish/pkgs/projectr/bin'); + +// 2. Setup +// ==================== + +/** + * This is an example webhook for Gitea projects. + * It's fairly simple to set this script up on your server/Uberspace, here's how: + * + * 1. Setup the toolkit, so you can access the tools from your SSH shell (more about that in README.md) + * 2. Take a look at the settings above. + * 2.1. Seriously, have you set a secret? Yes? Alright. + * 3. Place this script in a directory in ~/web/ (something like "~/web/hooks.example.com") + * 4. You should now be able to access https://hooks.example.com/webhook.gitea.php + * This should output "Web hook event (X-Gitea-Event header) is missing from request.". + * + * Now you can create projects and let this script deploy them for you: + * + * 5. If your project is a private repository: + * 5.1. Generate an SSH key by using `ssh-keygen`. The default values are alright for this purpose. + * 5.2. Copy the contents of `~/.ssh/id_rsa.pub` to your clipboard and add it as "Deploy Key" in your project's Gitea settings interface. + * 6. Create the project on the server by running either `project_add #` or + * `site_add #`. + * It's important to use the exact HTTPS url (not the SSH one) so this script can find the project! + * 7. Add the URL (the one you tested in 4.) of the script to your project's web hooks (choose the type "Gitea"). + * Make sure to add the secret configured above and select the "POST" method, the "application/json" type and the "Push Events" trigger. + * 8. To test if it everything works, push some code to your Gitea repository. + * + * Troubleshooting: + * - Use `tail -f /logs/*.log`. This should tell you what went wrong. + * - Check if the TOOLKIT_PATH you set above is correct. + * - Check if you set the origin of your project to the correct URL. + * - Open the web hook URL you entered in Gitea in your browser and check if the output is "Web hook event (X-Gitea-Event header) is missing from request.". + * - Click on the pen icon next to your web hook in Gitea's settings interface. You should see a list of "recent deliveries". + * You can find the exact error message from this script in the "Response" tab of the delivery. + */ + +// 3. Done +// The code starts here +// ==================== + +// We are always returning plain text +header('Content-Type: text/plain'); + +// Check if a secret has been set +if (SECRET === '{{ '{{' }} projectr_deploy_secret }}') { + http_response_code(500); + die('No secret has been set in ' . basename(__FILE__) . ". This script won't work without one."); +} + +// Check which event this is +if (isset($_SERVER['HTTP_X_GITEA_EVENT']) !== true) { + http_response_code(400); + die('Web hook event (X-Gitea-Event header) is missing from request.'); +} +$event = $_SERVER['HTTP_X_GITEA_EVENT']; +switch ($event) { + case 'push': + echo "Received push event.\n"; + break; + default: + http_response_code(400); + die("Received $event event, ignoring."); +} + +// Get the request body +$input = false; +switch ($_SERVER['CONTENT_TYPE']) { + case 'application/json': + echo "Received JSON data in body.\n"; + $input = file_get_contents('php://input'); + break; + case 'application/x-www-form-urlencoded': + echo "Received URL-encoded form data in body.\n"; + $input = (isset($_POST['payload']) === true)? $_POST['payload'] : ''; + break; + default: + http_response_code(400); + die("Don't know what to do with {$_SERVER['CONTENT_TYPE']} content type."); +} +if (!$input) { + http_response_code(400); + die('No POST body sent.'); +} + +// Check if the authentication is valid +if (isset($_SERVER['HTTP_X_GITEA_SIGNATURE']) !== true) { + http_response_code(401); + die("Secret (X-Gitea-Signature header) is missing from request. Have you set a secret in Gitea's project settings?"); +} +if (hash_equals(hash_hmac('sha256', $input, SECRET, false), $_SERVER['HTTP_X_GITEA_SIGNATURE']) !== true) { + http_response_code(403); + die('Secret (X-Gitea-Signature header) is wrong or does not match request body.'); +} + +// Parse payload +$payload = json_decode($input, true); +if (is_array($payload) !== true) { + http_response_code(400); + die('Invalid payload (no JSON?).'); +} + +// Get some interesting information from the payload +$url = $payload['repository']['clone_url']; +$commit = $payload['after']; +$ref = $payload['ref']; +if (preg_match('{(?:.*/){2}(.*)}', $ref, $matches) !== 1) { + http_response_code(400); + die('Invalid ref field (does not match regular expression "(?:.*/){2}(.*)").'); +} +$branch = $matches[1]; + +// Debug +echo "Received commit hash \"$commit\" for repository URL \"$url\" (branch \"$branch\").\n"; + +// Determine the path to the projects file +$xdgProjectsFile = ($_ENV['XDG_CONFIG_HOME'] ?? ($_SERVER['HOME'] . '/.config')) . '/projectr/projects'; +$projectsFile = is_file($xdgProjectsFile) === true ? $xdgProjectsFile : $_SERVER['HOME'] . '/.projects'; + +// Open the projects file and iterate through every project +$listPointer = fopen($projectsFile, 'r'); +$exitCode = 0; +while (($project = fgets($listPointer)) !== false) { + // Trim whitespace + $project = trim($project); + + // Only deployable projects are interesting for us + if (is_file($project . '/.origin') !== true || is_file($project . '/.branch') !== true) { + continue; + } + + // If there is a .origin and .branch file, check if they match + if (trim(file_get_contents($project . '/.origin')) === $url && trim(file_get_contents($project . '/.branch')) === $branch) { + // Found the right project + echo "Found project at $project, running deploy script.\n"; + + // Run deploy script (in the background, because Gitea doesn't like responses > 5sec by default) + passthru('export PATH=' . escapeshellarg(TOOLKIT_PATH) . ':$PATH; project_deploy ' . escapeshellarg($project) . ' ' . escapeshellarg($commit) . ' &> /dev/null &', $exitCode); + + // If it didn't work, add debug statement + if ($exitCode !== 0) { + echo "Something didn't work.\n"; + } + } +} + +// Iterated through every project, exit +http_response_code(($exitCode === 0)? 200 : 500); +die('All done.'); diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..59815ef --- /dev/null +++ b/run.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +cd "$(dirname "$0")" + +if [[ -n "$1" ]]; then + ansible-playbook -v --ask-vault-pass site.yml --tags "$1" +else + ansible-playbook -v --ask-vault-pass site.yml +fi diff --git a/site.yml b/site.yml new file mode 100644 index 0000000..75bf9b5 --- /dev/null +++ b/site.yml @@ -0,0 +1,46 @@ +--- +- name: General tasks for all Uberspaces + hosts: all + vars_files: + - vars/installs.yml + roles: + - homebrew + - host-site + - terminal + +- name: Redirect mail from non-mail Uberspaces + hosts: "!mail" + vars_files: + - vars/config.yml + roles: + - mail-redirect + +- name: DAV Uberspaces + hosts: dav + vars_files: + - vars/vault.yml + roles: + - dav + +- name: Git Uberspaces + hosts: git + vars_files: + - vars/vault.yml + roles: + - git + +- name: Mail Uberspaces + hosts: mail + vars_files: + - vars/config.yml + - vars/vault.yml + roles: + - mail + +- name: Web Uberspaces + hosts: web + vars_files: + - vars/config.yml + - vars/vault.yml + roles: + - web diff --git a/vars/config.yml b/vars/config.yml new file mode 100644 index 0000000..d31335d --- /dev/null +++ b/vars/config.yml @@ -0,0 +1,51 @@ +--- +# Email domains to set up per host +mail_domains: + cddmail1: + - lukasbestle.com + - lukasbestle.de + - bstl.xyz + cddmail2: + - codesignd.com + - codesignd.de + - codesignd.net + - codesigned.de + +# Default sender address per host +mail_default_address: + cddmail1: mail@lukasbestle.com + cddmail2: lukas@codesignd.de + +# Destination for email redirects from non-mail Uberspaces +mail_redirect_to: hostmaster@codesignd.net + +# Web sites to set up per host (name: origin) +web_sites: + cddweb1: + bachelor: null + lukasbestle: https://git.codesignd.com/sites/lukasbestle.com.git + codesignd: null + cddweb2: + shareable: null + cdn: null + +# Domain links to set up per host +web_links: + cddweb1: + bachelor.lukasbestle.com: + site: bachelor + lukasbestle.com: + site: lukasbestle + path: current/public + codesignd.com: + site: codesignd + codesignd.de: + site: codesignd + cddweb2: + bstl.xyz: + site: shareable + f.bstl.xyz: + site: shareable + path: data/files + cdn.codesignd.net: + site: cdn diff --git a/vars/installs.yml b/vars/installs.yml new file mode 100644 index 0000000..a6a2a06 --- /dev/null +++ b/vars/installs.yml @@ -0,0 +1,20 @@ +--- +# Homebrew formulae to install on all Uberspaces +homebrew_formulae: + # Helpers + - lukasbestle/tap/qdated # Timestamped email addresses for qmail + + # Shell tools + - fasd # Directory jump tool + - figlet # ASCII fonts + + # Package managers + - composer + +# Oh My Fish! packages to install and keep updated +# Key: Short package name; Value: URL to a Git repo +omf_pkgs: + theme-bobthefish: https://github.com/oh-my-fish/theme-bobthefish.git + rmate: https://github.com/aurora/rmate.git + welcome: https://github.com/lukasbestle/welcome.git + z: https://github.com/jethrokuan/z.git diff --git a/vars/vault.yml b/vars/vault.yml new file mode 100755 index 0000000..9d18d48 --- /dev/null +++ b/vars/vault.yml @@ -0,0 +1,30 @@ +$ANSIBLE_VAULT;1.1;AES256 +66326137613864613264363534623131343633613662326230346136623738633030336166343231 +6363393961646635336163386530393630333337383432330a316634623932626163313866393861 +33366438346438366639623666643661373365666430393862333839393133363063653637343863 +6464663264393931360a363433333139376264613333333130343136366437643162666132373233 +63383136303230643432633165383939653836313338333766626263643232626539383965363163 +65356336656264333037626366663864663137336661346230306661633430616166383063356539 +35396634326331343935366565386436343866643635356337366631313038306462306137623939 +64306266646638313638313766323336353261366166366433336165383230303866633137653264 +32346131343062396533343238343366643963323131356130643737626464323863323437393262 +36346533623137353234393738643266386165666662346636666436336432656439336430653734 +38326162666630633464376464323130633832626665386464306231653161326135323764366435 +63613637376331346265653738373539316531316539646538356463383832323034333963666261 +37373934333139393662353835316438323839316232666263383661313232613839656236623338 +39333261663936626564383665623933353862323932353734393962646636616235626637373062 +38656136323739353935613634653331663137613761373266633332383534333039646162623437 +36333839373066333263643234633462323635643738643731333139633263643335396636396239 +30343964633964353264373437353463613766343033386362663130633030303362646139393563 +36363635343861636564646266626631313262623664363063393231343838306538393263326633 +66343831346438616665666165353661663131373935353438653061373463623266313832656132 +65653561383732653638373638363036626166633838313736313133656438353334396364333964 +62346265636266663239316432313463346536633762343135366233396331313662653263643036 +30353761656662633862346464353631323465613037623332326664343533656439623236343535 +62323831323139653039353237653832646336363832383139393137386531336230383837343038 +38643761643833643231643731323239643734653532626366313033633563383966376537313735 +62653231343830386430653461366437646261623331313734623036353839306539633364613264 +62636637363363393166313035333835353962303263386633316231353362343665303566326332 +62386339646532343766373537623435653462323566386262393832616563333638353933376364 +31653034373964366134356536393935613931326663323335336437386132613733643964643434 +62643265663632663261393836336561623561333333303161646466346332313239