From 8314dfaa1fd9a8d4ded7733c1c054f4e3a9f6fe0 Mon Sep 17 00:00:00 2001 From: "Antonio J. Delgado" Date: Sat, 19 Nov 2022 11:10:57 +0200 Subject: [PATCH] adapt code from mastodon-ansible --- defaults/main.yml | 6 + files/mastodon/env.production.j2 | 19 ++ files/nginx/letsencrypt.conf.j2 | 8 + files/nginx/mastodon.conf.j2 | 196 ++++++++++++++++++++ files/systemd/mastodon-sidekiq.service.j2 | 53 ++++++ files/systemd/mastodon-streaming.service.j2 | 51 +++++ files/systemd/mastodon-web.service.j2 | 53 ++++++ handlers/main.yml | 3 + meta/.galaxy_install_info | 2 + meta/main.yml | 22 +++ tasks/bare.yml | 54 ++++++ tasks/bare/firewall-cmd.yml | 29 +++ tasks/bare/init.yml | 34 ++++ tasks/bare/letsencrypt.yml | 31 ++++ tasks/bare/mastodon-postflight.yml | 145 +++++++++++++++ tasks/bare/mastodon-preflight.yml | 45 +++++ tasks/bare/nginx.yml | 188 +++++++++++++++++++ tasks/bare/nodejs.yml | 48 +++++ tasks/bare/packages.yml | 32 ++++ tasks/bare/postgresql_database.yml | 74 ++++++++ tasks/bare/postgresql_packages.yml | 19 ++ tasks/bare/preflight-checks.yml | 183 ++++++++++++++++++ tasks/bare/redis.yml | 45 +++++ tasks/bare/repositories.yml | 71 +++++++ tasks/bare/ruby.yml | 81 ++++++++ tasks/bare/selfsigned-ssl.yml | 20 ++ tasks/bare/ufw.yml | 22 +++ tasks/bare/user.yml | 7 + tasks/docker/core.yml | 48 +++++ tasks/docker/docker.yml | 5 + tasks/docker/init.yml | 26 +++ tasks/docker/ubuntu.yml | 26 +++ tasks/main.yml | 8 + templates/env.j2 | 190 +++++++++++++++++++ 34 files changed, 1844 insertions(+) create mode 100644 defaults/main.yml create mode 100644 files/mastodon/env.production.j2 create mode 100644 files/nginx/letsencrypt.conf.j2 create mode 100644 files/nginx/mastodon.conf.j2 create mode 100644 files/systemd/mastodon-sidekiq.service.j2 create mode 100644 files/systemd/mastodon-streaming.service.j2 create mode 100644 files/systemd/mastodon-web.service.j2 create mode 100644 handlers/main.yml create mode 100644 meta/.galaxy_install_info create mode 100644 meta/main.yml create mode 100644 tasks/bare.yml create mode 100644 tasks/bare/firewall-cmd.yml create mode 100644 tasks/bare/init.yml create mode 100644 tasks/bare/letsencrypt.yml create mode 100644 tasks/bare/mastodon-postflight.yml create mode 100644 tasks/bare/mastodon-preflight.yml create mode 100644 tasks/bare/nginx.yml create mode 100644 tasks/bare/nodejs.yml create mode 100644 tasks/bare/packages.yml create mode 100644 tasks/bare/postgresql_database.yml create mode 100644 tasks/bare/postgresql_packages.yml create mode 100644 tasks/bare/preflight-checks.yml create mode 100644 tasks/bare/redis.yml create mode 100644 tasks/bare/repositories.yml create mode 100644 tasks/bare/ruby.yml create mode 100644 tasks/bare/selfsigned-ssl.yml create mode 100644 tasks/bare/ufw.yml create mode 100644 tasks/bare/user.yml create mode 100644 tasks/docker/core.yml create mode 100644 tasks/docker/docker.yml create mode 100644 tasks/docker/init.yml create mode 100644 tasks/docker/ubuntu.yml create mode 100644 tasks/main.yml create mode 100644 templates/env.j2 diff --git a/defaults/main.yml b/defaults/main.yml new file mode 100644 index 0000000..6cbc181 --- /dev/null +++ b/defaults/main.yml @@ -0,0 +1,6 @@ +--- +mastodon_db_password: "{{ vault_mastodon_db_password }}" +redis_pass: "{{ vault_mastodon_redis_password }}" +mastodon_host: mastodon.example.com +use_http: true +mastodon_db_login_unix_socket: /var/run/postgresql diff --git a/files/mastodon/env.production.j2 b/files/mastodon/env.production.j2 new file mode 100644 index 0000000..ac2e278 --- /dev/null +++ b/files/mastodon/env.production.j2 @@ -0,0 +1,19 @@ +LOCAL_DOMAIN={{ local_domain }} +SINGLE_USER_MODE=false +SECRET_KEY_BASE={{ secret_key_base.stdout }} +OTP_SECRET={{ otp_secret.stdout }} +VAPID_PRIVATE_KEY={{ vapid_private_key.stdout }} +VAPID_PUBLIC_KEY={{ vapid_public_key.stdout }} +DB_HOST={{ db_host }} +DB_PORT={{ mastodon_db_port }} +DB_NAME={{ mastodon_db }} +DB_USER={{ mastodon_db_user }} +DB_PASS={{ mastodon_db_password }} +REDIS_HOST={{ redis_host }} +REDIS_PORT={{ redis_port }} +REDIS_PASSWORD={{ redis_pass }} +SMTP_SERVER=localhost +SMTP_PORT=25 +SMTP_AUTH_METHOD=none +SMTP_OPENSSL_VERIFY_MODE=none +SMTP_FROM_ADDRESS=mastodon@{{ local_domain }} \ No newline at end of file diff --git a/files/nginx/letsencrypt.conf.j2 b/files/nginx/letsencrypt.conf.j2 new file mode 100644 index 0000000..d496a89 --- /dev/null +++ b/files/nginx/letsencrypt.conf.j2 @@ -0,0 +1,8 @@ +# This starts a simple nginx for the letsencrypt acme challenge +server { + listen 80; + listen [::]:80; + server_name {{ mastodon_host }}; + root {{ mastodon_home }}/{{ mastodon_path }}/public; + location /.well-known/acme-challenge/ { allow all; } +} \ No newline at end of file diff --git a/files/nginx/mastodon.conf.j2 b/files/nginx/mastodon.conf.j2 new file mode 100644 index 0000000..3ca7c86 --- /dev/null +++ b/files/nginx/mastodon.conf.j2 @@ -0,0 +1,196 @@ +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +server { + {% if nginx_catch_all != "true" %} + listen 80; + listen [::]:80; + server_name {{ mastodon_host }}; + {% endif %} + + {% if nginx_catch_all == "true" %} + listen 80 default_server; + server_name _; + {% endif %} + + # Useful for Let's Encrypt + location /.well-known/acme-challenge/ { + alias {{ mastodon_home }}/{{ mastodon_path }}/public/.well-known/acme-challenge/; + } + + {% if use_http != "true" %} + location / { return 301 https://$host$request_uri; } + {% endif %} + + {% if use_http == "true" %} + keepalive_timeout 70; + sendfile on; + client_max_body_size 8m; + + root {{ mastodon_nginx_symlink }}; + + gzip on; + gzip_disable "msie6"; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_buffers 16 8k; + gzip_http_version 1.1; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + + location / { + try_files $uri @proxy; + } + + location ~ ^/(emoji|packs|system/accounts/avatars|system/media_attachments/files) { + add_header Cache-Control "public, max-age=31536000, immutable"; + try_files $uri @proxy; + } + + location /sw.js { + add_header Cache-Control "public, max-age=0"; + try_files $uri @proxy; + } + + location @proxy { + 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 https; + proxy_set_header Proxy ""; + proxy_pass_header Server; + + proxy_pass http://127.0.0.1:3000; + proxy_buffering off; + proxy_redirect off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + + tcp_nodelay on; + } + + location /api/v1/streaming { + 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 https; + proxy_set_header Proxy ""; + + proxy_pass http://127.0.0.1:4000; + proxy_buffering off; + proxy_redirect off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + + tcp_nodelay on; + } + + error_page 500 501 502 503 504 /500.html; + + {% endif %} +} + +{% if use_http != "true" %} +server { + + {% if nginx_catch_all == "true" %} + listen 443 ssl http2 default_server; + server_name _; + {% endif %} + + {% if nginx_catch_all != "true" %} + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name {{ mastodon_host }}; + {% endif %} + + ssl_protocols TLSv1.2; + ssl_ciphers HIGH:!MEDIUM:!LOW:!aNULL:!NULL:!SHA; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + +{% if disable_letsencrypt != "true" %} + ssl_certificate /etc/letsencrypt/live/{{ mastodon_host }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ mastodon_host }}/privkey.pem; +{% endif %} + +{% if disable_letsencrypt == "true" %} + ssl_certificate {{ self_signed_cert_location }}/server.crt; + ssl_certificate_key {{ self_signed_key_location }}/server.key; +{% endif %} + + keepalive_timeout 70; + sendfile on; + client_max_body_size 8m; + + root {{ mastodon_nginx_symlink }}; + + gzip on; + gzip_disable "msie6"; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_buffers 16 8k; + gzip_http_version 1.1; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + +{% if disable_hsts == "true" %} + add_header Strict-Transport-Security "max-age=31536000"; +{% endif %} + + location / { + try_files $uri @proxy; + } + + location ~ ^/(emoji|packs|system/accounts/avatars|system/media_attachments/files) { + add_header Cache-Control "public, max-age=31536000, immutable"; + try_files $uri @proxy; + } + + location /sw.js { + add_header Cache-Control "public, max-age=0"; + try_files $uri @proxy; + } + + location @proxy { + 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 https; + proxy_set_header Proxy ""; + proxy_pass_header Server; + + proxy_pass http://127.0.0.1:3000; + proxy_buffering off; + proxy_redirect off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + + tcp_nodelay on; + } + + location /api/v1/streaming { + 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 https; + proxy_set_header Proxy ""; + + proxy_pass http://127.0.0.1:4000; + proxy_buffering off; + proxy_redirect off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + + tcp_nodelay on; + } + + error_page 500 501 502 503 504 /500.html; +} +{% endif %} \ No newline at end of file diff --git a/files/systemd/mastodon-sidekiq.service.j2 b/files/systemd/mastodon-sidekiq.service.j2 new file mode 100644 index 0000000..eb97c6d --- /dev/null +++ b/files/systemd/mastodon-sidekiq.service.j2 @@ -0,0 +1,53 @@ +[Unit] +Description=mastodon-sidekiq +After=network.target + +[Service] +Type=simple +User={{ mastodon_user }} +WorkingDirectory={{ mastodon_home }}/{{ mastodon_path }} +Environment="RAILS_ENV=production" +Environment="DB_POOL=25" +Environment="MALLOC_ARENA_MAX=2" +Environment="LD_PRELOAD=libjemalloc.so" +ExecStart={{ mastodon_home }}/.rbenv/shims/bundle exec sidekiq -c 25 +TimeoutSec=15 +Restart=always +# Proc filesystem +ProcSubset=pid +ProtectProc=invisible +# Capabilities +CapabilityBoundingSet= +# Security +NoNewPrivileges=true +# Sandboxing +ProtectSystem=strict +PrivateTmp=true +PrivateDevices=true +PrivateUsers=true +ProtectHostname=true +ProtectKernelLogs=true +ProtectKernelModules=true +ProtectKernelTunables=true +ProtectControlGroups=true +RestrictAddressFamilies=AF_INET +RestrictAddressFamilies=AF_INET6 +RestrictAddressFamilies=AF_NETLINK +RestrictAddressFamilies=AF_UNIX +RestrictNamespaces=true +LockPersonality=true +RestrictRealtime=true +RestrictSUIDSGID=true +RemoveIPC=true +PrivateMounts=true +ProtectClock=true +# System Call Filtering +SystemCallArchitectures=native +SystemCallFilter=~@cpu-emulation @debug @keyring @ipc @mount @obsolete @privileged @setuid +SystemCallFilter=@chown +SystemCallFilter=pipe +SystemCallFilter=pipe2 +ReadWritePaths={{ mastodon_home }}/{{ mastodon_path }} + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/files/systemd/mastodon-streaming.service.j2 b/files/systemd/mastodon-streaming.service.j2 new file mode 100644 index 0000000..8fe7dd1 --- /dev/null +++ b/files/systemd/mastodon-streaming.service.j2 @@ -0,0 +1,51 @@ +[Unit] +Description=mastodon-streaming +After=network.target + +[Service] +Type=simple +User={{ mastodon_user }} +WorkingDirectory={{ mastodon_home }}/{{ mastodon_path }} +Environment="NODE_ENV=production" +Environment="PORT=4000" +Environment="STREAMING_CLUSTER_NUM=1" +ExecStart=/usr/bin/node ./streaming +TimeoutSec=15 +Restart=always +# Proc filesystem +ProcSubset=pid +ProtectProc=invisible +# Capabilities +CapabilityBoundingSet= +# Security +NoNewPrivileges=true +# Sandboxing +ProtectSystem=strict +PrivateTmp=true +PrivateDevices=true +PrivateUsers=true +ProtectHostname=true +ProtectKernelLogs=true +ProtectKernelModules=true +ProtectKernelTunables=true +ProtectControlGroups=true +RestrictAddressFamilies=AF_INET +RestrictAddressFamilies=AF_INET6 +RestrictAddressFamilies=AF_NETLINK +RestrictAddressFamilies=AF_UNIX +RestrictNamespaces=true +LockPersonality=true +RestrictRealtime=true +RestrictSUIDSGID=true +RemoveIPC=true +PrivateMounts=true +ProtectClock=true +# System Call Filtering +SystemCallArchitectures=native +SystemCallFilter=~@cpu-emulation @debug @keyring @ipc @memlock @mount @obsolete @privileged @resources @setuid +SystemCallFilter=pipe +SystemCallFilter=pipe2 +ReadWritePaths={{ mastodon_home }}/{{ mastodon_path }} + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/files/systemd/mastodon-web.service.j2 b/files/systemd/mastodon-web.service.j2 new file mode 100644 index 0000000..3933064 --- /dev/null +++ b/files/systemd/mastodon-web.service.j2 @@ -0,0 +1,53 @@ +[Unit] +Description=mastodon-web +After=network.target + +[Service] +Type=simple +User={{ mastodon_user }} +WorkingDirectory={{ mastodon_home }}/{{ mastodon_path }} +Environment="RAILS_ENV=production" +Environment="PORT=3000" +Environment="LD_PRELOAD=libjemalloc.so" +ExecStart={{ mastodon_home }}/.rbenv/shims/bundle exec puma -C config/puma.rb +ExecReload=/bin/kill -SIGUSR1 $MAINPID +TimeoutSec=15 +Restart=always +# Proc filesystem +ProcSubset=pid +ProtectProc=invisible +# Capabilities +CapabilityBoundingSet= +# Security +NoNewPrivileges=true +# Sandboxing +ProtectSystem=strict +PrivateTmp=true +PrivateDevices=true +PrivateUsers=true +ProtectHostname=true +ProtectKernelLogs=true +ProtectKernelModules=true +ProtectKernelTunables=true +ProtectControlGroups=true +RestrictAddressFamilies=AF_INET +RestrictAddressFamilies=AF_INET6 +RestrictAddressFamilies=AF_NETLINK +RestrictAddressFamilies=AF_UNIX +RestrictNamespaces=true +LockPersonality=true +RestrictRealtime=true +RestrictSUIDSGID=true +RemoveIPC=true +PrivateMounts=true +ProtectClock=true +# System Call Filtering +SystemCallArchitectures=native +SystemCallFilter=~@cpu-emulation @debug @keyring @ipc @mount @obsolete @privileged @setuid +SystemCallFilter=@chown +SystemCallFilter=pipe +SystemCallFilter=pipe2 +ReadWritePaths={{ mastodon_home }}/{{ mastodon_path }} + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/handlers/main.yml b/handlers/main.yml new file mode 100644 index 0000000..ebd48d8 --- /dev/null +++ b/handlers/main.yml @@ -0,0 +1,3 @@ +--- +# - name: Refresh aliases +# shell: newaliases diff --git a/meta/.galaxy_install_info b/meta/.galaxy_install_info new file mode 100644 index 0000000..5692ba9 --- /dev/null +++ b/meta/.galaxy_install_info @@ -0,0 +1,2 @@ +install_date: Sun Nov 6 12:31:50 2022 +version: '' diff --git a/meta/main.yml b/meta/main.yml new file mode 100644 index 0000000..8d728a2 --- /dev/null +++ b/meta/main.yml @@ -0,0 +1,22 @@ +--- +galaxy_info: + author: Antonio J. Delgado (ajdelgado) + description: TODO - Description + +# issue_tracker_url: https://github.com/uoi-io/ansible-galera/issues + + license: GPLv3 + + min_ansible_version: "2.0" + + github_branch: master + + platforms: + - name: Ubuntu + versions: + - focal + - jammy + + galaxy_tags: []# TODO + +dependencies: [] \ No newline at end of file diff --git a/tasks/bare.yml b/tasks/bare.yml new file mode 100644 index 0000000..75d93ba --- /dev/null +++ b/tasks/bare.yml @@ -0,0 +1,54 @@ +--- + +- include_tasks: bare/init.yml + +- include_tasks: bare/preflight-checks.yml + when: run_preflight_checks | bool + +- include_tasks: bare/postgresql_packages.yml +- include_tasks: bare/postgresql_databases.yml +- include_tasks: bare/redis.yml +- include_tasks: bare/web_user.yml + +- include_tasks: bare/web_repositories.yml + +- include_tasks: bare/web_nodejs.yml + +- include_tasks: bare/web_packages.yml + +#RHEL uses firewall-cmd +- include_tasks: bare/web_ufw.yml + when: + - ansible_os_family == "Debian" + +- include_tasks: bare/web_firewall-cmd.yml + when: + - ansible_os_family == "RedHat" + +- include_tasks: bare/web_mastodon-preflight.yml + args: + apply: + become: true + become_user: mastodon + +- include_tasks: bare/web_ruby.yml + args: + apply: + become: true + become_user: mastodon + +- include_tasks: bare/web_redis.yml + +- include_tasks: bare/web_selfsigned-ssl.yml + when: disable_letsencrypt | bool + +- include_tasks: bare/web_mastodon-postflight.yml + args: + apply: + become: true + become_user: mastodon + +- include_tasks: bare/web_letsencrypt.yml + when: disable_letsencrypt | bool == false + +- include_tasks: bare/web_nginx.yml diff --git a/tasks/bare/firewall-cmd.yml b/tasks/bare/firewall-cmd.yml new file mode 100644 index 0000000..94e3a5b --- /dev/null +++ b/tasks/bare/firewall-cmd.yml @@ -0,0 +1,29 @@ +--- +- name: "Start and enable FirewallD service" + become: true + #Workaround for "Interactive authentication required" issue + become_user: root + service: "name={{ item }} state=started enabled=yes" + with_items: + - firewalld + +- name: Allow SSH, HTTP and HTTPS through the firewall + firewalld: + permanent: true + immediate: true + service: "{{ item }}" + state: enabled + with_items: + - http + - https + - ssh + +- name: Add localhost lo interface to the trusted zone + firewalld: + permanent: true + immediate: true + interface: "{{ item }}" + zone: trusted + state: enabled + with_items: + - lo diff --git a/tasks/bare/init.yml b/tasks/bare/init.yml new file mode 100644 index 0000000..b468808 --- /dev/null +++ b/tasks/bare/init.yml @@ -0,0 +1,34 @@ +--- +- name: Check if Python is installed + raw: command -v python3 + register: python_installed + ignore_errors: True + +- name: Check if yum is installed + raw: command -v yum + register: yum_installed + when: python_installed is failed + ignore_errors: True + +#This checks if running higher Redhat version than 7 +- name: Check if dnf is installed + raw: command -v dnf + register: dnf_installed + when: python_installed is failed + ignore_errors: True + +- name: Check if apt is installed + raw: command -v apt + register: apt_installed + when: python_installed is failed + ignore_errors: True + +- name: Bootstrap Python on RHEL + raw: yum install -y python3 + when: python_installed is failed and yum_installed is succeeded + become: true + +- name: Bootstrap Python on Ubuntu Linux + raw: apt update && apt install -y python3 + when: python_installed is failed and apt_installed is succeeded + become: true diff --git a/tasks/bare/letsencrypt.yml b/tasks/bare/letsencrypt.yml new file mode 100644 index 0000000..f2a7785 --- /dev/null +++ b/tasks/bare/letsencrypt.yml @@ -0,0 +1,31 @@ +--- +- stat: path=/etc/letsencrypt/live/{{ mastodon_host }}/fullchain.pem + register: letsencrypt_cert + +- name: Copy letsencrypt nginx config + template: + src: ../files/nginx/letsencrypt.conf.j2 + dest: /etc/nginx/sites-available/mastodon.conf + when: not letsencrypt_cert.stat.exists + +- name: Symlink enabled site + file: + src: "/etc/nginx/sites-available/mastodon.conf" + dest: "/etc/nginx/sites-enabled/mastodon.conf" + state: link + when: not letsencrypt_cert.stat.exists + +- name: Reload nginx + command: "systemctl reload nginx" + +- name: Install letsencrypt cert + command: letsencrypt certonly -n --webroot -d {{ mastodon_host }} -w {{ mastodon_home }}/{{ mastodon_path }}/public/ --email "webmaster@{{ mastodon_host }}" --agree-tos + when: not letsencrypt_cert.stat.exists + +- name: Letsencrypt Job + cron: + name: "letsencrypt renew" + minute: "15" + hour: "0" + job: "letsencrypt renew && service nginx reload" + diff --git a/tasks/bare/mastodon-postflight.yml b/tasks/bare/mastodon-postflight.yml new file mode 100644 index 0000000..2797207 --- /dev/null +++ b/tasks/bare/mastodon-postflight.yml @@ -0,0 +1,145 @@ +- name: Bundle install + shell: "~/.rbenv/shims/bundle config set --local deployment 'true' && ~/.rbenv/shims/bundle config set --local without 'test' && ~/.rbenv/shims/bundle config set --local with 'development' && ~/.rbenv/shims/bundle install -j$(getconf _NPROCESSORS_ONLN)" + args: + chdir: "{{ mastodon_home }}/{{ mastodon_path }}" + +- name: Yarn install + command: yarn install --pure-lockfile + args: + chdir: "{{ mastodon_home }}/{{ mastodon_path }}" + +- name: Install systemd sidekiq Service Files + template: + src: ../files/systemd/mastodon-sidekiq.service.j2 + dest: /etc/systemd/system/mastodon-sidekiq.service + become: true + become_user: root + +- name: Install systemd web Service Files + template: + src: ../files/systemd/mastodon-web.service.j2 + dest: /etc/systemd/system/mastodon-web.service + become: true + become_user: root + +- name: Install systemd streaming Service Files + template: + src: ../files/systemd/mastodon-streaming.service.j2 + dest: /etc/systemd/system/mastodon-streaming.service + become: true + become_user: root + +- name: Media cleanup cronjob + cron: + name: "media cleanup" + minute: "15" + hour: "1" + job: '/bin/bash -c ''export PATH="$HOME/.rbenv/bin:$PATH"; eval "$(rbenv init -)"; cd {{ mastodon_home }}/{{ mastodon_path }} && RAILS_ENV=production ./bin/tootctl media remove''' + +- stat: path={{ mastodon_home }}/{{ mastodon_path }}/.env.production + register: production_config + +- name: Generate SECRET_KEY_BASE secret + shell: "RAILS_ENV=production ~/.rbenv/shims/bundle exec rake secret" + args: + chdir: "{{ mastodon_home }}/{{ mastodon_path }}" + register: secret_key_base + when: not production_config.stat.exists + +- name: Generate OTP_SECRET secret + shell: "RAILS_ENV=production ~/.rbenv/shims/bundle exec rake secret" + args: + chdir: "{{ mastodon_home }}/{{ mastodon_path }}" + register: otp_secret + when: not production_config.stat.exists + +- name: "Generate VAPID key pair into {{ mastodon_home }}/{{ mastodon_path }}/vapid.tmp" + shell: "RAILS_ENV=production ~/.rbenv/shims/bundle exec rake mastodon:webpush:generate_vapid_key > {{ mastodon_home }}/{{ mastodon_path }}/vapid.tmp | head -1 | cut -c 19-" + args: + chdir: "{{ mastodon_home }}/{{ mastodon_path }}" + when: not production_config.stat.exists + +- name: Get VAPID_PRIVATE_KEY secret + shell: "cat {{ mastodon_home }}/{{ mastodon_path }}/vapid.tmp | head -1 | cut -c 19-" + register: vapid_private_key + when: not production_config.stat.exists + +- name: Get VAPID_PUBLIC_KEY secret + shell: "cat {{ mastodon_home }}/{{ mastodon_path }}/vapid.tmp | tail -1 | cut -c 18-" + register: vapid_public_key + when: not production_config.stat.exists + +- name: Ensure that the file used for vapid keypair generation is removed. + ansible.builtin.file: + path: "{{ mastodon_home }}/{{ mastodon_path }}/vapid.tmp" + state: absent + +- name: Install Production env file + template: + src: files/mastodon/env.production.j2 + dest: "{{ mastodon_home }}/{{ mastodon_path }}/.env.production" + when: not production_config.stat.exists + +- name: Create database + shell: "RAILS_ENV=production ~/.rbenv/shims/bundle exec rails db:setup" + args: + chdir: "{{ mastodon_home }}/{{ mastodon_path }}" + environment: + SAFETY_ASSURED: 1 + when: not production_config.stat.exists + +- name: Migrate database + shell: "RAILS_ENV=production ~/.rbenv/shims/bundle exec rails db:migrate" + args: + chdir: "{{ mastodon_home }}/{{ mastodon_path }}" + when: production_config.stat.exists + +- name: Ensure that we have correct file permissions with owner being the user and NGINX being the group + become: true + become_user: root + file: + path: "{{ mastodon_home }}/{{ mastodon_path }}" + owner: "{{ mastodon_user }}" + group: "nginx" + recurse: true + when: + - ansible_os_family == "RedHat" + +- name: Ensure that we have correct file permissions with owner being the user and www-data being the group + become: true + become_user: root + file: + path: "{{ mastodon_home }}/{{ mastodon_path }}" + owner: "{{ mastodon_user }}" + group: "www-data" + recurse: true + when: + - ansible_os_family == "Debian" + +#https://github.com/nodejs/node/issues/40455 +#It's possible that this is a bug with ruby 3.0.3 and gets fixed with Mastodon 4.0.0 +- name: Precompile assets with Legacy OpenSSL provider for RHEL9 + shell: "NODE_OPTIONS=--openssl-legacy-provider RAILS_ENV=production ~/.rbenv/shims/bundle exec rails assets:precompile" + args: + chdir: "{{ mastodon_home }}/{{ mastodon_path }}" + when: + - ansible_os_family == "RedHat" + - ansible_facts['distribution_major_version'] == "9" + +- name: Precompile assets + shell: "RAILS_ENV=production ~/.rbenv/shims/bundle exec rails assets:precompile" + args: + chdir: "{{ mastodon_home }}/{{ mastodon_path }}" + when: not (ansible_os_family == "RedHat" and ansible_facts['distribution_major_version'] == "9") +#We are installing new .env file, checking if .env file exists no longer required +# when: production_config.stat.exists + +- name: "Start and enable Mastodon services" + become: true + #Workaround for "Interactive authentication required" issue + become_user: root + service: "name={{ item }} state=started enabled=yes" + with_items: + - mastodon-web.service + - mastodon-streaming.service + - mastodon-sidekiq.service diff --git a/tasks/bare/mastodon-preflight.yml b/tasks/bare/mastodon-preflight.yml new file mode 100644 index 0000000..a4087cd --- /dev/null +++ b/tasks/bare/mastodon-preflight.yml @@ -0,0 +1,45 @@ +#We need different vars as register always fires off even when skipped +#This creates a mess with duplicate tasks that do different things, but its required + +- name: Fetch latest stable Mastodon version number + shell: "git -c 'versionsort.suffix=-' ls-remote --tags --sort='v:refname' https://github.com/mastodon/mastodon.git | grep -v 'rc' | tail --lines=1 | cut -d '/' -f 3" + when: + - mastodon_version == "latest" + - mastodon_allow_prerelease | bool == false + register: latest_mastodon_tag + +- name: Fetch latest stable Mastodon version number allowing release candidates + shell: "git -c 'versionsort.suffix=-' ls-remote --tags --sort='v:refname' https://github.com/mastodon/mastodon.git | tail --lines=1 | cut -d '/' -f 3" + when: + - mastodon_version == "latest" + - mastodon_allow_prerelease | bool + register: latest_mastodon_tag_rc + +- name: Clone Specific version of Mastodon + git: + repo: "https://github.com/mastodon/mastodon.git" + dest: "{{ mastodon_home }}/{{mastodon_path}}" + clone: true + version: "{{ mastodon_version }}" + when: mastodon_version != "latest" + +- name: Clone Latest Mastodon with allowed prerelease versions + git: + repo: "https://github.com/mastodon/mastodon.git" + dest: "{{ mastodon_home }}/{{mastodon_path}}" + clone: true + version: "{{ latest_mastodon_tag_rc.stdout }}" + when: + - mastodon_version == "latest" + - mastodon_allow_prerelease | bool + +- name: Clone Latest Mastodon + git: + repo: "https://github.com/mastodon/mastodon.git" + dest: "{{ mastodon_home }}/{{mastodon_path}}" + clone: true + version: "{{ latest_mastodon_tag.stdout }}" + when: + - mastodon_version == "latest" + - mastodon_allow_prerelease | bool == false + diff --git a/tasks/bare/nginx.yml b/tasks/bare/nginx.yml new file mode 100644 index 0000000..89683ae --- /dev/null +++ b/tasks/bare/nginx.yml @@ -0,0 +1,188 @@ +--- +#We need to enable the service first or we will not have the appropriate folders generated. +- name: "Start and enable NGINX service" + become: true + #Workaround for "Interactive authentication required" issue + become_user: root + service: "name={{ item }} state=started enabled=yes" + with_items: + - nginx + +- name: "Set NGINX to run under {{ mastodon_user }} to avoid permission issues" + become: true + lineinfile: + dest: "/etc/nginx/nginx.conf" + regexp: "ˆuser" + line: "user mastodon;" + state: present + +- name: "Ensure that NGINX doesn't run under the user nginx" + become: true + lineinfile: + dest: "/etc/nginx/nginx.conf" + regexp: "user.nginx;" + line: "user nginx;" + state: absent + when: + - ansible_os_family == "RedHat" + +- name: "Ensure that NGINX doesn't run under the user www-data" + become: true + lineinfile: + dest: "/etc/nginx/nginx.conf" + regexp: "user.www-data;" + line: "user www-data;" + state: absent + when: + - ansible_os_family == "Debian" + +- name: Copy nginx config + template: + src: ../files/nginx/mastodon.conf.j2 + dest: /etc/nginx/sites-available/mastodon.conf + when: + - ansible_os_family == "Debian" + - mastodon_host is defined + +- name: Symlink enabled site + file: + src: "/etc/nginx/sites-available/mastodon.conf" + dest: "/etc/nginx/sites-enabled/mastodon.conf" + state: link + when: + - ansible_os_family == "Debian" + - mastodon_host is defined + +- name: Copy nginx config with RHEL folder stucture + template: + src: ../files/nginx/mastodon.conf.j2 + dest: /etc/nginx/conf.d/mastodon.conf + when: + - ansible_os_family == "RedHat" + - mastodon_host is defined + +- name: Create folder structure for Mastodon public folder + file: + path: "{{ mastodon_nginx_symlink }}" + state: directory + owner: "{{ mastodon_user }}" + group: "nginx" + recurse: true + when: + - ansible_os_family == "RedHat" + - mastodon_host is defined + +- name: Create folder structure for Mastodon public folder + file: + path: "{{ mastodon_nginx_symlink }}" + state: directory + owner: "{{ mastodon_user }}" + group: "www-data" + recurse: true + when: + - ansible_os_family == "Debian" + - mastodon_host is defined + +- name: Create a symbolic link of Mastodon public folder to comply with SELinux policy + become: true + file: + src: "{{ mastodon_home }}/{{ mastodon_path }}/public" + dest: "{{ mastodon_nginx_symlink }}" + state: link + owner: "{{ mastodon_user }}" + group: "nginx" + force: true + when: + - ansible_os_family == "RedHat" + - mastodon_host is defined + +- name: Create a symbolic link of Mastodon public folder + become: true + file: + src: "{{ mastodon_home }}/{{ mastodon_path }}/public" + dest: "{{ mastodon_nginx_symlink }}" + state: link + owner: "{{ mastodon_user }}" + group: "www-data" + force: true + when: + - ansible_os_family == "Debian" + - mastodon_host is defined + +- name: Permit NGINX SELinux permission to access filesystem + become: true + shell: "setsebool -P httpd_read_user_content 1" + when: + - ansible_os_family == "RedHat" + - mastodon_host is defined + +- name: Permit SELinux permission to allow NGINX to make proxy connections with httpd_can_network_connect + become: true + shell: "setsebool -P httpd_can_network_connect 1" + when: + - ansible_os_family == "RedHat" + - mastodon_host is defined + +- name: Permit SELinux permission to allow NGINX to make proxy connections with httpd_can_network_relay + become: true + shell: "setsebool -P httpd_can_network_relay 1" + when: + - ansible_os_family == "RedHat" + - mastodon_host is defined + +#Reading and writing into users home directories as a web server or executing any binary as systemd service is +#really pretty anomalous behaviour. SELinux is completely right to flag this as it looks like we're an attacker. +#Potential security issue? + +- name: Permit SELinux permission to allow NGINX to read contents of home folders (Required for Mastodon) + become: true + shell: "setsebool -P httpd_enable_homedirs on" + when: + - ansible_os_family == "RedHat" + - mastodon_host is defined + +- name: Change SELinux properties of Mastodon symlink + become: true + shell: "chcon -Rt httpd_sys_content_t {{ mastodon_nginx_symlink }}" + when: + - ansible_os_family == "RedHat" + - mastodon_host is defined + +- name: "Ensure that we have correct file permissions for /var/lib/nginx/ as we are not running NGINX under default user" + become: true + become_user: root + file: + path: "/var/lib/nginx/" + owner: "{{ mastodon_user }}" + group: "nginx" + recurse: true + when: + - ansible_os_family == "RedHat" + +- name: "Ensure that we have correct file permissions for /var/lib/nginx/ as we are not running NGINX under default user" + become: true + become_user: root + file: + path: "/var/lib/nginx/" + owner: "{{ mastodon_user }}" + group: "www-data" + recurse: true + when: + - ansible_os_family == "Debian" + +- name: Restart nginx + become: true + #Workaround for "Interactive authentication required" issue + become_user: root + service: name=nginx state=restarted + tags: + - systemd + +- name: Check if Mastodon instance is up and running + uri: + url: 'https://{{ mastodon_host }}/about' + validate_certs: no + register: result + until: 'result.status == 200' + retries: 5 + delay: 5 diff --git a/tasks/bare/nodejs.yml b/tasks/bare/nodejs.yml new file mode 100644 index 0000000..115c439 --- /dev/null +++ b/tasks/bare/nodejs.yml @@ -0,0 +1,48 @@ +#Ansible is not able to work with "dnf module" outside of installing them. +#Shell has to be used to check if a specific app stream is enabled, and then disable and enable +#the appropriate app streams to get the correct nodejs version for Yarn. +- name: Check if NodeJS 10 module is enabled + become: true + shell: "dnf module list nodejs | grep -q 'nodejs 10 \\[d\\]\\[e\\]' && echo true || echo false" + register: is_node10_enabled + ignore_errors: true + when: + - ansible_os_family == "RedHat" + - ansible_facts['distribution_major_version'] == "8" + +- name: Disable NodeJS 10 module + become: true + shell: "dnf module disable nodejs:10 -y" + ignore_errors: true + when: + - ansible_os_family == "RedHat" + - ansible_facts['distribution_major_version'] == "8" + - is_node10_enabled.stdout | bool + +- name: Enable NodeJS 16 module + become: true + shell: "dnf module enable nodejs:16 -y" + ignore_errors: true + when: + - ansible_os_family == "RedHat" + - ansible_facts['distribution_major_version'] == "8" + - is_node10_enabled.stdout | bool + +- name: Install NodeJS 16 via DNF + become: true + dnf: + name: "@nodejs:16" + state: present + when: + - ansible_os_family == "RedHat" + - ansible_facts['distribution_major_version'] == "8" + +#RHEL9 already installs NodeJS 16 by default +- name: Install NodeJS via DNF + become: true + dnf: + name: "nodejs" + state: present + when: + - ansible_os_family == "RedHat" + - ansible_facts['distribution_major_version'] == "9" diff --git a/tasks/bare/packages.yml b/tasks/bare/packages.yml new file mode 100644 index 0000000..50c9062 --- /dev/null +++ b/tasks/bare/packages.yml @@ -0,0 +1,32 @@ +--- + +#Speeds up the provisioning process as cache is not updated for every single package. +- name: Update package manager cache before installing packages. + become: true + package: + update_cache: true + +- name: Install packages + package: + name: "{{ packages | map(attribute='package') }}" + update_cache: no + #This makes the package module non-OS generic + #https://docs.ansible.com/ansible/latest/collections/ansible/builtin/package_module.html + # state: latest + # install_recommends: no + +#RHEL and Debian already has NodeJS in /usr/bin/node +#Unclear what this is supposed to fix +#- name: nodejs alternative +# alternatives: +# name: node +# link: /usr/bin/node +# path: /usr/bin/nodejs +# when: ansible_os_family == "Debian" + +- name: Install Perl for building OpenSSL 1.1 + become: true + package: + name: "perl" + state: present + when: (ansible_os_family == "RedHat" and ansible_facts['distribution_major_version'] == "9") or (ansible_facts['distribution'] == 'Ubuntu' and ansible_facts['distribution_release'] == 'jammy') diff --git a/tasks/bare/postgresql_database.yml b/tasks/bare/postgresql_database.yml new file mode 100644 index 0000000..1b6a98c --- /dev/null +++ b/tasks/bare/postgresql_database.yml @@ -0,0 +1,74 @@ +#Debian PostgreSQL installation automatically calls initdb i.e. it initializes the cluster with default encoding and locale. +#https://wiki.debian.org/PostgreSql +#RHEL requires manual initialisation + +- name: "Find out if PostgreSQL is initialized" + stat: + path: "/var/lib/pgsql/data/pg_hba.conf" + register: postgres_data + when: + - ansible_os_family == "RedHat" + +- name: "Initialize PostgreSQL" + shell: "postgresql-setup initdb" + when: + - ansible_os_family == "RedHat" + - not postgres_data.stat.exists + +- name: "Start and enable services" + become: true + #Workaround for "Interactive authentication required" issue + become_user: root + service: "name={{ item }} state=started enabled=yes" + with_items: + - postgresql + +- name: Create remote database {{ mastodon_db }} + postgresql_db: + name: "{{ mastodon_db }}" + login_host: "{{ mastodon_db_login_host }}" + login_password: "{{ mastodon_db_login_password }}" + login_user: "{{ mastodon_db_login_user }}" + port: "{{ mastodon_db_port }}" + register: create_remote_db + when: + - mastodon_db_login_user is defined + - mastodon_db_login_host is defined + - mastodon_db_login_password is defined + - mastodon_db_port is defined + +- name: Create remote database user {{ mastodon_db_user }} + postgresql_user: + db: "{{ mastodon_db }}" + name: "{{ mastodon_db_user }}" + password: "{{ mastodon_db_password }}" + login_host: "{{ mastodon_db_login_host }}" + login_password: "{{ mastodon_db_login_password }}" + login_user: "{{ mastodon_db_login_user }}" + port: "{{ mastodon_db_port }}" + role_attr_flags: CREATEDB + register: create_remote_db_user + when: + - mastodon_db_login_user is defined + - mastodon_db_login_host is defined + - mastodon_db_login_password is defined + - mastodon_db_port is defined + +- name: Create database {{ mastodon_db }} + become: true + postgresql_db: + name: "{{ mastodon_db }}" + login_unix_socket: "{{ mastodon_db_login_unix_socket }}" + register: create_local_db + when: create_remote_db is skipped + +- name: Create database user {{ mastodon_db_user }} + become: true + postgresql_user: + db: "{{ mastodon_db }}" + name: "{{ mastodon_db_user }}" + password: "{{ mastodon_db_password }}" + encrypted: true + login_unix_socket: "{{ mastodon_db_login_unix_socket }}" + role_attr_flags: CREATEDB + when: create_remote_db_user is skipped diff --git a/tasks/bare/postgresql_packages.yml b/tasks/bare/postgresql_packages.yml new file mode 100644 index 0000000..ee57a53 --- /dev/null +++ b/tasks/bare/postgresql_packages.yml @@ -0,0 +1,19 @@ +--- + +#Speeds up the provisioning process as cache is not updated for every single package. +- name: Update package manager cache before installing packages. + become: true + package: + update_cache: true + +- name: Install Postgres packages + become: true + package: + name: "{{ item.package }}" + update_cache: no + #This makes the package module non-OS generic + #https://docs.ansible.com/ansible/latest/collections/ansible/builtin/package_module.html + # cache_valid_time: 3600 + # state: latest + # install_recommends: no + with_items: "{{ postgres.packages }}" diff --git a/tasks/bare/preflight-checks.yml b/tasks/bare/preflight-checks.yml new file mode 100644 index 0000000..9e25d99 --- /dev/null +++ b/tasks/bare/preflight-checks.yml @@ -0,0 +1,183 @@ +--- +#We need different vars as register always fires off even when skipped +#This creates a mess with duplicate tasks that do different things, but its required +#Help wanted to clean up the preflight task + +- name: Check if a Mastodon installation already exists + stat: + path: "{{ mastodon_home }}/{{mastodon_path}}" + register: mastodon_install_exists + +- name: Verify that existing Mastodon installation is a valid git folder + stat: + path: "{{ mastodon_home }}/{{mastodon_path}}/.git" + register: mastodon_is_git + +- name: Verify if upgrade folder is valid + fail: + msg: + - "ERROR: A folder defined for Mastodon installation already exists but its not a valid git folder." + - "Halting playbook and bailing out to prevent a destructive operation." + - "If you think this is a mistake or you know what you're doing, set the run_preflight_checks variable to false" + when: + - mastodon_install_exists.stat.exists + - not mastodon_is_git.stat.exists + +#Have to run it under Mastodon user due of Git Security changes in newer OSes +#https://github.blog/2022-04-12-git-security-vulnerability-announced/ +- name: Get local major version + shell: "git tag --points-at HEAD | cut -c2-2" + args: + chdir: "{{ mastodon_home }}/{{ mastodon_path }}" + when: + - mastodon_install_exists.stat.exists + - mastodon_is_git.stat.exists + become: true + become_user: mastodon + register: local_major_ver + +#Have to run it under Mastodon user due of Git Security changes in newer OSes +#https://github.blog/2022-04-12-git-security-vulnerability-announced/ +- name: Get local minor version + shell: "git tag --points-at HEAD | cut -c4-4" + args: + chdir: "{{ mastodon_home }}/{{ mastodon_path }}" + when: + - mastodon_install_exists.stat.exists + - mastodon_is_git.stat.exists + register: local_minor_ver + become: true + become_user: mastodon + +- name: Fetch latest stable major Mastodon version number + shell: "git -c 'versionsort.suffix=-' ls-remote --tags --sort='v:refname' https://github.com/mastodon/mastodon.git | grep -v 'rc' | tail --lines=1 | cut -d '/' -f 3 | cut -c2-2" + when: + - mastodon_version == "latest" + - mastodon_allow_prerelease | bool == false + - mastodon_is_git.stat.exists + - mastodon_install_exists.stat.exists + register: latest_mastodon_tag_major + +- name: Fetch latest stable major Mastodon version number allowing release candidates + shell: "git -c 'versionsort.suffix=-' ls-remote --tags --sort='v:refname' https://github.com/mastodon/mastodon.git | tail --lines=1 | cut -d '/' -f 3 | cut -c2-2" + when: + - mastodon_version == "latest" + - mastodon_allow_prerelease | bool + - mastodon_is_git.stat.exists + - mastodon_install_exists.stat.exists + register: latest_mastodon_tag_rc_major + +- name: Fetch latest minor stable Mastodon version number + shell: "git -c 'versionsort.suffix=-' ls-remote --tags --sort='v:refname' https://github.com/mastodon/mastodon.git | grep -v 'rc' | tail --lines=1 | cut -d '/' -f 3 | cut -c4-4" + when: + - mastodon_version == "latest" + - mastodon_allow_prerelease | bool == false + - mastodon_is_git.stat.exists + - mastodon_install_exists.stat.exists + register: latest_mastodon_tag_minor + +- name: Fetch latest minor stable Mastodon version number allowing release candidates + shell: "git -c 'versionsort.suffix=-' ls-remote --tags --sort='v:refname' https://github.com/mastodon/mastodon.git | tail --lines=1 | cut -d '/' -f 3 | cut -c4-4" + when: + - mastodon_version == "latest" + - mastodon_allow_prerelease | bool + - mastodon_is_git.stat.exists + - mastodon_install_exists.stat.exists + register: latest_mastodon_tag_rc_minor + +- name: Fetch specified major Mastodon version number + shell: "echo '{{ mastodon_version }}' | cut -c2-2" + when: + - mastodon_version != "latest" + - mastodon_is_git.stat.exists + - mastodon_install_exists.stat.exists + register: specific_mastodon_tag_major + +- name: Fetch specified minor Mastodon version number + shell: "echo '{{ mastodon_version }}' | cut -c4-4" + when: + - mastodon_version != "latest" + - mastodon_is_git.stat.exists + - mastodon_install_exists.stat.exists + register: specific_mastodon_tag_minor + +- name: Verify MAJOR upgrade path for specific version of Mastodon + fail: + msg: + - "ERROR: You are attempting to perform a MAJOR version upgrade that is not supported for automation!" + - "It is HEAVILY recommended to do the upgrade by hand by following the upgrade instructions listed in the Mastodon release notes!" + - "Halting playbook and bailing out to prevent a destructive operation." + - "If you think this is a mistake or you know what you're doing, set the run_preflight_checks variable to false" + when: + - mastodon_version != "latest" + - mastodon_is_git.stat.exists + - mastodon_install_exists.stat.exists + - local_major_ver.stdout != specific_mastodon_tag_major.stdout + +- name: Verify MAJOR upgrade path for Latest Mastodon with allowed prerelease versions + fail: + msg: + - "ERROR: You are attempting to perform a MAJOR version upgrade that is not supported for automation!" + - "It is HEAVILY recommended to do the upgrade by hand by following the upgrade instructions listed in the Mastodon release notes!" + - "Halting playbook and bailing out to prevent a destructive operation." + - "If you think this is a mistake or you know what you're doing, set the run_preflight_checks variable to false" + when: + - mastodon_version == "latest" + - mastodon_allow_prerelease | bool + - mastodon_install_exists.stat.exists + - local_major_ver.stdout != latest_mastodon_tag_rc_major.stdout + +- name: Verify MAJOR upgrade path for Latest Mastodon + fail: + msg: + - "ERROR: You are attempting to perform a MAJOR version upgrade that is not supported for automation!" + - "It is HEAVILY recommended to do the upgrade by hand by following the upgrade instructions listed in the Mastodon release notes!" + - "Halting playbook and bailing out to prevent a destructive operation." + - "If you think this is a mistake or you know what you're doing, set the run_preflight_checks variable to false" + when: + - mastodon_version == "latest" + - mastodon_allow_prerelease | bool == false + - mastodon_install_exists.stat.exists + - mastodon_is_git.stat.exists + - local_major_ver.stdout != latest_mastodon_tag_major.stdout + +- name: Verify MINOR upgrade path for specific version of Mastodon + fail: + msg: + - "ERROR: You are attempting to perform a MINOR version upgrade that is not recommended to be upgraded by automation!" + - "It is HEAVILY recommended to do the upgrade by hand by following the upgrade instructions listed in the Mastodon release notes!" + - "Halting playbook and bailing out to prevent a destructive operation." + - "If you think this is a mistake or you know what you're doing, set the run_preflight_checks variable to false" + when: + - mastodon_version != "latest" + - mastodon_is_git.stat.exists + - mastodon_install_exists.stat.exists + - local_minor_ver.stdout != specific_mastodon_tag_minor.stdout + +- name: Verify MAJOR upgrade path for Latest Mastodon with allowed prerelease versions + fail: + msg: + - "ERROR: You are attempting to perform a MINOR version upgrade that is not recommended to be upgraded by automation!" + - "It is HEAVILY recommended to do the upgrade by hand by following the upgrade instructions listed in the Mastodon release notes!" + - "Halting playbook and bailing out to prevent a destructive operation." + - "If you think this is a mistake or you know what you're doing, set the run_preflight_checks variable to false" + when: + - mastodon_version == "latest" + - mastodon_allow_prerelease | bool + - mastodon_is_git.stat.exists + - mastodon_install_exists.stat.exists + - local_minor_ver.stdout != latest_mastodon_tag_rc_minor.stdout + +- name: Verify MINOR upgrade path for Latest Mastodon + fail: + msg: + - "ERROR: You are attempting to perform a MINOR version upgrade that is not recommended to be upgraded by automation!" + - "It is HEAVILY recommended to do the upgrade by hand by following the upgrade instructions listed in the Mastodon release notes!" + - "Halting playbook and bailing out to prevent a destructive operation." + - "If you think this is a mistake or you know what you're doing, set the run_preflight_checks variable to false" + when: + - mastodon_version == "latest" + - mastodon_allow_prerelease | bool == false + - mastodon_install_exists.stat.exists + - mastodon_is_git.stat.exists + - local_minor_ver.stdout != latest_mastodon_tag_minor.stdout diff --git a/tasks/bare/redis.yml b/tasks/bare/redis.yml new file mode 100644 index 0000000..9e9d2f7 --- /dev/null +++ b/tasks/bare/redis.yml @@ -0,0 +1,45 @@ +- name: "Start and enable redis service" + become: true + #Workaround for "Interactive authentication required" issue + become_user: root + service: "name={{ item }} state=started enabled=yes" + with_items: + - redis + +- name: Set Redis password for RHEL8 system + become: true + lineinfile: + dest: "/etc/redis.conf" + regexp: "ˆrequirepass" + line: "requirepass {{ redis_pass }}" + state: present + when: + - ansible_os_family == "RedHat" + - ansible_facts['distribution_major_version'] == "8" + +- name: Set Redis password for RHEL9 system + become: true + lineinfile: + dest: "/etc/redis/redis.conf" + regexp: "ˆrequirepass" + line: "requirepass {{ redis_pass }}" + state: present + when: + - ansible_os_family == "RedHat" + - ansible_facts['distribution_major_version'] == "9" + +- name: Set Redis password for Debian system + become: true + lineinfile: + dest: "/etc/redis/redis.conf" + regexp: "ˆrequirepass" + line: "requirepass {{ redis_pass }}" + state: present + when: + - ansible_os_family == "Debian" + +- name: Restart Redis + become: true + #Workaround for "Interactive authentication required" issue + become_user: root + service: name=redis state=restarted diff --git a/tasks/bare/repositories.yml b/tasks/bare/repositories.yml new file mode 100644 index 0000000..3d5fb76 --- /dev/null +++ b/tasks/bare/repositories.yml @@ -0,0 +1,71 @@ +--- + + +- name: Install required packages for HTTPS repositories + apt: name={{ item.package }} state=present update_cache=yes cache_valid_time=3600 + become: true + with_items: + - package: apt-transport-https + - package: ca-certificates + when: + - ansible_os_family == "Debian" + +- name: Install APT repository keys + apt_key: id={{ item.id }} url={{ item.url }} state=present + become: true + with_items: + - { id: "72ECF46A56B4AD39C907BBB71646B01B86E50310", url: "https://dl.yarnpkg.com/debian/pubkey.gpg" } + - { id: "9FD3B784BC1C6FC31A8A0A1C1655A0AB68576280", url: "https://deb.nodesource.com/gpgkey/nodesource.gpg.key" } + when: + - ansible_os_family == "Debian" + +- name: Install APT repositories + apt_repository: repo={{ item.repo }} state=present + become: true + with_items: + - repo: "deb https://dl.yarnpkg.com/debian/ stable main" + - repo: "deb https://deb.nodesource.com/node_{{ node_major_version }}.x {{ ubuntu_codename }} main" + when: + - ansible_os_family == "Debian" + +#This is meant for CentOS8/Rocky8/AlmaLinux8/Fedora +#On actual Redhat distro we need to execute subscription-manager repos --enable codeready-builder-for-rhel-8-x86_64-rpms +#Ansible is not able to work with "dnf config-manager" so have to execute as shell command. Ffmpeg and devel packages dependency +- name: Enable Powertools repository for RHEL8 + become: true + shell: dnf config-manager --set-enabled powertools + when: + - ansible_os_family == "RedHat" + - ansible_facts['distribution_major_version'] == "8" + +- name: Enable crb repository for RHEL9 + become: true + shell: dnf config-manager --set-enabled crb + when: + - ansible_os_family == "RedHat" + - ansible_facts['distribution_major_version'] == "9" + +- name: Install RPMFusion repository for RHEL + become: true + dnf: + name: "https://mirrors.rpmfusion.org/free/el/rpmfusion-free-release-{{ ansible_distribution_major_version }}.noarch.rpm" + #distribution-gpg-keys doesn't contain the up to date GPG keys for RPMFusion and even official RPMFusion install instructions + #specify to ignore GPG checks. + disable_gpg_check: true + state: present + when: ansible_os_family == "RedHat" + +- name: Install Yarn repository for RHEL + become: true + get_url: + url: https://dl.yarnpkg.com/rpm/yarn.repo + dest: /etc/yum.repos.d/yarn.repo + mode: '0644' + when: ansible_os_family == "RedHat" + +- name: Import the Yarn repository GPG key for RHEL + become: true + rpm_key: + state: present + key: https://dl.yarnpkg.com/rpm/pubkey.gpg + when: ansible_os_family == "RedHat" diff --git a/tasks/bare/ruby.yml b/tasks/bare/ruby.yml new file mode 100644 index 0000000..d35fb6c --- /dev/null +++ b/tasks/bare/ruby.yml @@ -0,0 +1,81 @@ +--- + +- name: Fetch Ruby version required by Mastodon + shell: "cat {{ mastodon_home }}/{{ mastodon_path }}/.ruby-version" + register: ruby_version + +- name: Fetch latest tagged release of rbenv + shell: "git -c 'versionsort.suffix=-' ls-remote --tags --sort='v:refname' https://github.com/rbenv/rbenv.git | tail --lines=1 | cut -d '/' -f 3" + register: rbenv_version + +- name: Fetch latest tagged release of ruby-build + shell: "git -c 'versionsort.suffix=-' ls-remote --tags --sort='v:refname' https://github.com/rbenv/ruby-build.git | tail --lines=1 | cut -d '/' -f 3" + register: ruby_build_version + +- name: "Clone rbenv version {{ rbenv_version.stdout }}" + git: + repo: "https://github.com/rbenv/rbenv.git" + dest: "~/.rbenv" + clone: true + version: "{{ rbenv_version.stdout }}" + +- name: "Clone ruby-build version {{ ruby_build_version.stdout }}" + git: + repo: "https://github.com/rbenv/ruby-build.git" + dest: "~/.rbenv/plugins/ruby-build" + clone: true + version: "{{ ruby_build_version.stdout }}" + register: ruby_build + +- name: Configure rbenv + command: ./configure + args: + chdir: "~/.rbenv/src" + register: rbenv_configure + +- name: Build rbenv + command: make + args: + chdir: "~/.rbenv/src" + when: rbenv_configure is succeeded + +- name: Update profile settings + copy: + dest: "~/.bashrc" + content: | + export PATH="~/.rbenv/bin:${PATH}" + eval "$(rbenv init -)" + +- name: Check if the Ruby version is already installed + shell: "~/.rbenv/bin/rbenv versions | grep -q {{ ruby_version.stdout }}" + register: ruby_installed + ignore_errors: true + check_mode: no + +- name: Install Ruby {{ ruby_version.stdout }} + shell: "~/.rbenv/bin/rbenv install {{ ruby_version.stdout }}" + args: + executable: /bin/bash + when: ruby_installed is failed + +- name: Set the default Ruby version to {{ ruby_version.stdout }} + shell: "~/.rbenv/bin/rbenv global {{ ruby_version.stdout }}" + args: + executable: /bin/bash + register: default_ruby_version + +#Locking the Bundler version by itself is no longer required +#https://bundler.io/blog/2022/01/23/bundler-v2-3.html +- name: Install bundler + shell: 'export PATH="$HOME/.rbenv/bin:$PATH"; eval "$(rbenv init -)"; gem install bundler' + args: + executable: /bin/bash + when: default_ruby_version is succeeded + +- name: Set SELinux policy for rbenv bundle same as /bin + become: true + #Workaround for "SELinux policy is not managed or store cannot be accessed" issue + become_user: root + shell: "chcon -R --reference /bin {{ mastodon_home }}/.rbenv/shims/bundle" + when: + - ansible_os_family == "RedHat" diff --git a/tasks/bare/selfsigned-ssl.yml b/tasks/bare/selfsigned-ssl.yml new file mode 100644 index 0000000..e2f3d9e --- /dev/null +++ b/tasks/bare/selfsigned-ssl.yml @@ -0,0 +1,20 @@ +--- + +- name: Create folder location for self-signed SSL certs + become: true + file: + path: "{{ item }}" + state: directory + owner: root + group: root + recurse: true + loop: + - "{{ self_signed_cert_location }}" + - "{{ self_signed_key_location }}" + +- name: Create self-signed certificate + become: true + shell: > + openssl req -x509 -nodes -subj '/CN={{ mastodon_host }}' -days 365 + -newkey rsa:4096 -sha256 -keyout '{{ self_signed_key_location }}/server.key' -out '{{ self_signed_cert_location }}/server.crt' + creates='{{ self_signed_cert_location }}/server.crt' diff --git a/tasks/bare/ufw.yml b/tasks/bare/ufw.yml new file mode 100644 index 0000000..277faa5 --- /dev/null +++ b/tasks/bare/ufw.yml @@ -0,0 +1,22 @@ +--- + +- name: Allow ssh through firewall + ufw: + proto: tcp + port: "22" + rule: allow +- name: Set ufw policy + ufw: + state: enabled + direction: incoming + policy: deny +- name: Allow nginx firewall + ufw: + proto: tcp + port: "80" + rule: allow +- name: Allow nginx ssl firewall + ufw: + proto: tcp + port: "443" + rule: allow diff --git a/tasks/bare/user.yml b/tasks/bare/user.yml new file mode 100644 index 0000000..762e61e --- /dev/null +++ b/tasks/bare/user.yml @@ -0,0 +1,7 @@ +- name: Create Mastodon user + user: + name: "{{ mastodon_user }}" + createhome: true + shell: /bin/bash + home: "{{ mastodon_home }}" + diff --git a/tasks/docker/core.yml b/tasks/docker/core.yml new file mode 100644 index 0000000..36bee9c --- /dev/null +++ b/tasks/docker/core.yml @@ -0,0 +1,48 @@ +--- + +- import_tasks: ubuntu.yml + when: ansible_distribution == 'Ubuntu' + +- name: Install docker-compose + get_url: + url: "https://github.com/docker/compose/releases/download/{{ docker_compose_version }}/docker-compose-Linux-x86_64" + dest: "/usr/bin/docker-compose" + sha256sum: "{{ docker_compose_hash }}" + mode: 0755 + become: true + +- name: Install Docker packages + package: + name: "{{ item.package }}" + state: latest + become: true + register: docker_packages + with_items: "{{ install_packages }}" + +- name: Install Python packages + pip: + name: "{{ item.package }}" + state: latest + with_items: "{{ install_python_packages }}" + +- name: Uninstall Python packages + pip: + name: "{{ item.package }}" + state: absent + with_items: "{{ uninstall_python_packages }}" + +- name: Install docker-compose Python + pip: + name: docker-compose + version: 1.17.0rc1 + +- name: Check for existing network + raw: "docker network inspect {{ mastodon_docker_network }}" + register: docker_network + ignore_errors: True + become: true + +- name: Create main docker network + raw: "docker network create -d bridge {{ mastodon_docker_network }}" + when: docker_network is failed + become: true diff --git a/tasks/docker/docker.yml b/tasks/docker/docker.yml new file mode 100644 index 0000000..dd59679 --- /dev/null +++ b/tasks/docker/docker.yml @@ -0,0 +1,5 @@ + +- name: Bootstrapping + import_tasks: docker/init.yml + +- import_tasks: docker/core.yml diff --git a/tasks/docker/init.yml b/tasks/docker/init.yml new file mode 100644 index 0000000..5d3acf2 --- /dev/null +++ b/tasks/docker/init.yml @@ -0,0 +1,26 @@ +--- + +- name: Check if Python is installed + raw: command -v python + register: python_installed + ignore_errors: True + +- name: Check if yum is installed + raw: command -v yum + register: yum_installed + ignore_errors: True + +- name: Check if apt is installed + raw: command -v apt + register: apt_installed + ignore_errors: True + +- name: Bootstrap Python on Amazon Linux + raw: yum update && yum install -y python27 + when: python_installed is failed and yum_installed is succeeded + become: true + +- name: Bootstrap Python on Ubuntu Linux + raw: apt update && apt install -y python + when: python_installed is failed and apt_installed is succeeded + become: true diff --git a/tasks/docker/ubuntu.yml b/tasks/docker/ubuntu.yml new file mode 100644 index 0000000..41cd6ab --- /dev/null +++ b/tasks/docker/ubuntu.yml @@ -0,0 +1,26 @@ +--- + +- name: Install required packages for HTTPS repositories + apt: name={{ item.package }} state=present update_cache=yes cache_valid_time=3600 + become: true + with_items: + - { package: apt-transport-https } + - { package: ca-certificates } + +- name: Add Docker repository for Ubuntu + apt_key: id={{ item.id }} url={{ item.url }} state=present + become: true + with_items: + - { id: "9DC858229FC7DD38854AE2D88D81803C0EBFCD88", url: "https://download.docker.com/linux/ubuntu/gpg" } + +- name: Install Docker repository + apt_repository: repo={{ item.repo }} state=present + become: true + with_items: + - { repo: "deb [arch=amd64] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable" } + +- name: Run apt update + apt: + update_cache: true + cache_valid_time: 3600 + become: true diff --git a/tasks/main.yml b/tasks/main.yml new file mode 100644 index 0000000..4f3eab1 --- /dev/null +++ b/tasks/main.yml @@ -0,0 +1,8 @@ +--- +- name: Ensure bare metal installation + include_tasks: bare.yml + when: mastodon_bare_installation + +- name: Ensure docker installation + include_tasks: docker.yml + when: not mastodon_bare_installation diff --git a/templates/env.j2 b/templates/env.j2 new file mode 100644 index 0000000..52e9f1d --- /dev/null +++ b/templates/env.j2 @@ -0,0 +1,190 @@ +# Service dependencies +# You may set REDIS_URL instead for more advanced options +# You may also set REDIS_NAMESPACE to share Redis between multiple Mastodon servers +REDIS_HOST={{ redis_host }} +REDIS_PORT={{ redis_port }} +# You may set DATABASE_URL instead for more advanced options +DB_HOST={{ db_host }} +DB_USER={{ db_user }} +DB_NAME={{ db_name }} +DB_PASS={{ db_pass }} +DB_PORT={{ db_port }} + +# Federation +# Note: Changing LOCAL_DOMAIN or LOCAL_HTTPS at a later time will cause unwanted side effects. +# LOCAL_DOMAIN should *NOT* contain the protocol part of the domain e.g https://example.com. +LOCAL_DOMAIN={{ local_domain }} +LOCAL_HTTPS={{ local_https }} + +{% if web_domain %} +WEB_DOMAIN={{ web_domain }} +{% endif %} + +{% if alternate_domains %} +ALTERNATE_DOMAINS={{ alternate_domains }} +{% endif %} + +# Application secrets +# Generate each with the `RAILS_ENV=production bundle exec rake secret` task (`docker-compose run --rm web rake secret` if you use docker compose) +PAPERCLIP_SECRET={{ paperclip_secret }} +SECRET_KEY_BASE={{ secret_key_base }} +OTP_SECRET={{ otp_secret }} + +# VAPID keys (used for push notifications +# You can generate the keys using the following command (first is the private key, second is the public one) +# You should only generate this once per instance. If you later decide to change it, all push subscription will +# be invalidated, requiring the users to access the website again to resubscribe. +# +# Generate with `RAILS_ENV=production bundle exec rake mastodon:webpush:generate_vapid_key` task (`docker-compose run --rm web rake mastodon:webpush:generate_vapid_key` if you use docker compose) +# +# For more information visit https://rossta.net/blog/using-the-web-push-api-with-vapid.html +VAPID_PRIVATE_KEY={{ vapid_private_key }} +VAPID_PUBLIC_KEY=({ vapid_public_key }) + +{% if single_user_mode %} +# Registrations +# Single user mode will disable registrations and redirect frontpage to the first profile +SINGLE_USER_MODE=true +{% endif %} +{% if email_domain_blacklist %} +# Prevent registrations with following e-mail domains +EMAIL_DOMAIN_BLACKLIST={{ email_domain_blacklist }} +{% endif %} +{% if email_domain_whitelist %} +# Only allow registrations with the following e-mail domains +EMAIL_DOMAIN_WHITELIST={{ email_domain_whitelist }} +{% endif %} + +# Optionally change default language +DEFAULT_LOCALE={{ default_locale }} + +# E-mail configuration +# Note: Mailgun and SparkPost (https://sparkpo.st/smtp) each have good free tiers +# If you want to use an SMTP server without authentication (e.g local Postfix relay) +# then set SMTP_AUTH_METHOD and SMTP_OPENSSL_VERIFY_MODE to 'none' and +# *comment* SMTP_LOGIN and SMTP_PASSWORD (leaving them blank is not enough). +SMTP_SERVER={{ smtp_server }} +SMTP_PORT={{ smtp_port }} +SMTP_LOGIN={{ smtp_login }} +SMTP_PASSWORD={{ smtp_password }} +SMTP_FROM_ADDRESS={{ smtp_from_address }} + +{% if smtp_domain %} +SMTP_DOMAIN={{ smtp_domain }} +{% endif %} +{% if smtp_delivery_method %} +SMTP_DELIVERY_METHOD={{ smtp_delivery_method }} +{% endif %} +{% if smtp_auth_method %} +SMTP_AUTH_METHOD={{ smtp_auth_method }} +{% endif %} +{% smtp_ca_file %} +SMTP_CA_FILE={{ smtp_ca_file }} +{% endif %} +{% if smtp_openssl_verify_mode %} +SMTP_OPENSSL_VERIFY_MODE={{ smtp_openssl_verify_mode }} +{% endif %} +{% if smtp_enable_starttls_auto %} +SMTP_ENABLE_STARTTLS_AUTO={{ smtp_enable_starttls_auto }} +{% endif %} +{% if smtp_tls %} +SMTP_TLS={{ smtp_tls }} +{% endif %} + +{% if paperclip_root_path %} +# Optional user upload path and URL (images, avatars). Default is :rails_root/public/system. If you set this variable, you are responsible for making your HTTP server (eg. nginx) serve these files. +PAPERCLIP_ROOT_PATH={{ paperclip_root_path }} +{% endif %} +{% if paperclip_root_url %} +PAPERCLIP_ROOT_URL={{ paperclip_root_url }} +{% endif %} + +{% if cdn_host %} +# Optional asset host for multi-server setups +CDN_HOST={{ cdn_host }} +{% endif %} + +{% if s3_enabled %} +S3_ENABLED={{ s3_enabled }} +{% endif %} +{% if s3_bucket %} +S3_BUCKET={{ s3_bucket }} +{% endif %} +{% if aws_access_key_id %} +AWS_ACCESS_KEY_ID={{ aws_access_key_id }} +{% endif %} +{% if aws_secret_access_key %} +AWS_SECRET_ACCESS_KEY={{ aws_secret_access_key }} +{% endif %} +{% if s3_region %} +S3_REGION={{ s3_region }} +{% endif %} +{% if s3_protocol %} +S3_PROTOCOL={{ s3_protocol }} +{% endif %} +{% if s3_hostname %} +S3_HOSTNAME={{ s3_hostname }} +{% endif %} +{% if s3_endpoint %} +S3_ENDPOINT={{ s3_endpoint }} +{% endif %} +{% if s3_signature_version %} +S3_SIGNATURE_VERSION={{ s3_signature_version }} +{% endif %} + +{% if swift_enabled %} +SWIFT_ENABLED={{ swift_enabled }} +{% endif %} +{% if swift_username %} +SWIFT_USERNAME={{ swift_username }} +{% endif %} +{% if swift_tenant %} +# For Keystone V3, the value for SWIFT_TENANT should be the project name +SWIFT_TENANT= +{% endif %} +{% if swift_password %} +SWIFT_PASSWORD={{ swift_password }} +{% endif %} +{% if swift_auth_url %} +# Keystone V2 and V3 URLs are supported. Use a V3 URL if possible to avoid +# issues with token rate-limiting during high load. +SWIFT_AUTH_URL={{ swift_auth_url }} +{% endif %} +{% if swift_container %} +SWIFT_CONTAINER={{ swift_container }} +{% endif %} +{% if swift_object_url %} +SWIFT_OBJECT_URL={{ swift_object_url }} +{% endif %} +{% if swift_region %} +SWIFT_REGION={{ swift_region }} +{% endif %} +{% if swift_domain_name %} +# Defaults to 'default' +# SWIFT_DOMAIN_NAME={{ swift_domain_name }} +{% endif %} +{% if swift_cache_ttl %} +# Defaults to 60 seconds. Set to 0 to disable +# SWIFT_CACHE_TTL={{ swift_cache_ttl }} +{% endif %} + +{% if s3_cloudfront_host %} +# Optional alias for S3 if you want to use Cloudfront or Cloudflare in front +S3_CLOUDFRONT_HOST={{ s3_cloudfront_host }} +{% endif %} +{% if streaming_api_base_url %} +# Streaming API integration +# STREAMING_API_BASE_URL={{ streaming_api_base_url }} +{% endif %} + +{% if prepared_statements %} +# Advanced settings +# If you need to use pgBouncer, you need to disable prepared statements: +# PREPARED_STATEMENTS={{ prepared_statements }} +{% endif %} + +{% if streaming_cluster_num %} +# Cluster number setting for streaming API server. +# If you comment out following line, cluster number will be `numOfCpuCores - 1`. +STREAMING_CLUSTER_NUM=1 +{% endif %}